All files / realm/blong-access/adapter/db accessCredentialAdd.ts

98.61% Statements 71/72
33.33% Branches 2/6
100% Functions 1/1
98.61% Lines 71/72

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 731x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x   3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x  
import {type IMeta, handler} from '@feasibleone/blong';
 
import * as account from './account.ts';
import {type PasswordParams} from './password.ts';
 
type KnexQb = any;
 
/**
 * Create a credential for a subject resource (user OR application).
 *
 * Wire: `access.credential.add` — reusable credential creation shared by the
 * access and gateway realms.  Uses the same PBKDF2 library + active policy as
 * password credentials, so client secrets benefit from the same hashing
 * parameters and verification path (`access.credential.checkClient`).
 */
export default handler(
    ({lib: {hashPassword, credentialPolicyParams}}) =>
        async function accessCredentialAdd(
            params: {
                /** The subject resource id (hex UUID) the credential belongs to. */
                subjectResourceId: string;
                credentialType: 'password' | 'clientSecret' | 'google';
                /** Secret to hash (required for password/clientSecret). */
                secret?: string;
                /** Provider subject id (for google type). */
                googleSubjectId?: string;
                isActive?: boolean;
            },
            // eslint-disable-next-line @typescript-eslint/no-unused-vars
            $meta: IMeta,
        ): Promise<{success: boolean}> {
            const qb: KnexQb = this.config?.context?.queryBuilder;
            if (!qb) throw new Error('Database not available');
 
            const secret =
                params.credentialType === 'google'
                    ? (params.googleSubjectId ?? '')
                    : (params.secret ?? '');
            const salt = account.newUuid();
            const policyParams = await credentialPolicyParams(qb, params.credentialType);
            const {hash, params: credentialParams} = hashPassword<{
                hash: string;
                params: PasswordParams;
            }>(secret, salt, policyParams);
 
            // Rotate: deactivate any previously active credential of the same
            // subject + type so the new one becomes the single active credential.
            // This makes credential creation idempotent — re-registering a
            // client (or resetting a password) replaces the old secret instead
            // of stacking a second active row (which would break `.first()`
            // verification that reads the oldest active credential).
            await qb('access_credential')
                .where('userId', account.uuidBuf(params.subjectResourceId))
                .where('credentialType', params.credentialType)
                .update({isActive: 0});
 
            await qb('access_credential')
                .insert({
                    userId: account.uuidBuf(params.subjectResourceId),
                    credentialType: params.credentialType,
                    credentialHash: hash,
                    credentialSalt: salt,
                    // `*JSON` column — the knex adapter stores this object as JSON.
                    credentialParamsJSON: credentialParams,
                    isActive: params.isActive ?? 1,
                })
                .onConflict()
                .ignore();
 
            return {success: true};
        },
);