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

100% Statements 67/67
71.42% Branches 15/21
100% Functions 1/1
100% Lines 67/67

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 681x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 42x 7x  
import {type IMeta, handler} from '@feasibleone/blong';
 
import * as model from './accessModel.ts';
 
type KnexQb = any;
 
/**
 * A single audit entry.  `isSuccess` is required; all other context fields
 * are optional.  `detail` is an object (stored JSON) — for access-table DML
 * events it should be a sanitised summary (entity + key), never full params.
 */
export interface IAuditEntry {
    userId?: string | null;
    actorId?: string | null;
    sessionId?: string | null;
    actionName: string;
    credentialType?: string | null;
    ipAddress?: string | null;
    isSuccess: boolean;
    failureReason?: string | null;
    statusCode?: number | null;
    detail?: object | null;
    occurredAt?: Date | string | null;
}
 
/**
 * `access.audit.record` — append audit rows.
 *
 * Append-only writer used by the gateway access-check audit hook and by the
 * login/refresh/revoke flows.  PK is a ULID (`access.audit.auditId`), the
 * `*JSON`-style detail is stored as a string.  Batch insert for efficiency.
 * Returns the inserted ULID keys (`auditIds`) so callers can correlate — the
 * gateway exposes the first one on `$meta.auth.auditId`.
 *
 * Wire: `access.audit.record`.
 */
export default handler(({lib: {ulid, crockfordDecode}}) => ({
    async accessAuditRecord(
        params: {audit: IAuditEntry[]},
        _$meta: IMeta,
    ): Promise<{inserted: number; auditIds: string[]}> {
        const qb: KnexQb = this.config?.context?.queryBuilder;
        if (!qb) throw new Error('Database not available');
        const entries = Array.isArray(params.audit) ? params.audit : [];
        if (!entries.length) return {inserted: 0, auditIds: []};
        const auditIds = entries.map(() => ulid());
        const rows = entries.map((entry, index) => {
            const userHex = entry.userId ? model.binHex(entry.userId) : undefined;
            return {
                auditId: Buffer.from(crockfordDecode(auditIds[index])),
                userId: userHex ? Buffer.from(userHex, 'hex') : null,
                actorId: entry.actorId ?? null,
                sessionId: entry.sessionId ?? null,
                actionName: entry.actionName,
                credentialType: entry.credentialType ?? null,
                ipAddress: entry.ipAddress ?? null,
                isSuccess: entry.isSuccess ? 1 : 0,
                failureReason: entry.failureReason ?? null,
                statusCode: entry.statusCode ?? null,
                detail: entry.detail ? JSON.stringify(entry.detail) : null,
                occurredAt: entry.occurredAt ? new Date(entry.occurredAt) : new Date(),
            };
        });
        await qb('access_audit').insert(rows);
        return {inserted: rows.length, auditIds};
    },
}));