UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

270 lines 8.97 kB
import { resolveProviderInstrumentationPort, } from "../providers/instrumentation.js"; import { redactValue } from "./redaction.js"; /** * Create an anonymous actor descriptor for unauthenticated activity. * * This helper only creates a normalized context value. It does not perform * authentication. * * @example * ```ts * const actor = createAnonymousActor(); * ``` * * @param options - Optional display name or metadata to include. * @returns An activity actor with `type: "anonymous"`. */ export function createAnonymousActor(options = {}) { return { type: "anonymous", ...options }; } /** * Create a service actor descriptor for work initiated by another service or * integration. * * This is useful for webhooks, internal service calls, or integration-driven * background jobs. * * @example * ```ts * const actor = createServiceActor("stripe-webhook"); * ``` * * @param id - Stable service or integration ID. * @param options - Optional display name or metadata to include. * @returns An activity actor with `type: "service"`. */ export function createServiceActor(id, options = {}) { return { type: "service", id, ...options }; } /** * Create a system actor descriptor for framework or app-owned background work. * * Use this for schedules, scripts, maintenance jobs, and other work that * is not directly caused by a user or external service. * * @example * ```ts * const actor = createSystemActor("nightly-maintenance"); * ``` * * @param id - Stable system actor ID. Defaults to `"system"`. * @param options - Optional display name or metadata to include. * @returns An activity actor with `type: "system"`. */ export function createSystemActor(id = "system", options = {}) { return { type: "system", id, ...options }; } /** * Create a user actor descriptor for authenticated user activity. * * This helper only normalizes a known user ID for context, authorization, * audit, and diagnostics. It does not verify a session or load a user record. * Resolve authentication first, then call this helper with the authenticated * user ID. * * @example * ```ts * const actor = createUserActor(session.user.id, { * displayName: session.user.name, * }); * ``` * * @param id - Stable application user ID. * @param options - Optional display name or metadata to include. * @returns An activity actor with `type: "user"`. */ export function createUserActor(id, options = {}) { return { type: "user", id, ...options }; } /** * Create a tenant/account/workspace descriptor for request or background * context. * * This helper only creates a normalized context value used by audit, * authorization, logs, and diagnostics. It does not create, load, or persist a * tenant record. * * @example * ```ts * const tenant = createTenant(session.organizationId, { * slug: session.organizationSlug, * }); * ``` * * @param id - Stable tenant/account/workspace ID. * @param options - Optional slug or metadata to include. * @returns A normalized activity tenant descriptor. */ export function createTenant(id, options = {}) { return { id, ...options }; } /** * Fill default audit fields for an input entry. * * @param entry - Partial audit entry accepted by `AuditLogPort.record(...)`. * @returns A complete audit entry with `actor`, `occurredAt`, and `outcome` * populated. Entries without an actor default to an anonymous actor. */ export function normalizeAuditLogEntry(entry) { return { ...entry, actor: entry.actor ?? createAnonymousActor(), occurredAt: entry.occurredAt ?? new Date(), outcome: entry.outcome ?? "success", }; } /** * Redact metadata on an already-normalized audit entry. * * This redacts metadata values on the entry, actor, tenant, and resource using * the default redaction rules from `redactValue(...)`. * * @param entry - Audit entry to redact. * @returns A shallow copy with redacted metadata fields. */ export function redactAuditLogEntry(entry) { return { ...entry, actor: { ...entry.actor, metadata: entry.actor.metadata ? redactValue(entry.actor.metadata) : entry.actor.metadata, }, tenant: entry.tenant ? { ...entry.tenant, metadata: entry.tenant.metadata ? redactValue(entry.tenant.metadata) : entry.tenant.metadata, } : entry.tenant, resource: entry.resource ? { ...entry.resource, metadata: entry.resource.metadata ? redactValue(entry.resource.metadata) : entry.resource.metadata, } : entry.resource, metadata: entry.metadata ? redactValue(entry.metadata) : entry.metadata, }; } /** * Wrap an audit log port with default audit metadata redaction. * * Use this around durable adapters so application code can record entries * without each call site remembering to redact metadata. * * @param audit - Underlying audit log port to write to after redaction. * @param options - Optional final redaction/customization hook. * @returns An audit log port that normalizes and redacts before writing. */ export function createRedactedAuditLog(audit, options = {}) { return { record(entry) { const normalized = normalizeAuditLogEntry(entry); const redacted = options.redact ? options.redact(redactAuditLogEntry(normalized)) : redactAuditLogEntry(normalized); return audit.record(redacted); }, }; } function prepareInstrumentedEntry(input, redact) { const redacted = redactAuditLogEntry(normalizeAuditLogEntry(input)); return redact ? redact(redacted) : redacted; } function auditSummary(entry) { const resource = entry.resource?.id ? `${entry.resource.type}:${entry.resource.id}` : entry.resource?.type; const outcome = entry.outcome === "failure" ? "failed" : "succeeded"; return resource ? `${entry.action} ${outcome} for ${resource}` : `${entry.action} ${outcome}`; } /** * Wrap an audit log so durable audit writes also appear in instrumentation * sinks such as devtools. * * Instrumentation failures are ignored so audit persistence remains the * source of truth. * * @example * ```ts * const audit = createInstrumentedAuditLog({ * audit: createDrizzleSqliteAuditLogPort(db), * instrumentation: ports, * }); * ``` */ export function createInstrumentedAuditLog(options) { return { async record(input) { const entry = prepareInstrumentedEntry(input, options.redact); await options.audit.record(entry); if (options.emit === false) return; const port = resolveProviderInstrumentationPort(options.instrumentation); if (!port) return; try { port.record({ type: "custom", watcher: "audit", name: entry.action, label: "Audit", summary: auditSummary(entry), requestId: entry.requestId, traceId: entry.traceId, details: { action: entry.action, actor: entry.actor, tenant: entry.tenant, resource: entry.resource, outcome: entry.outcome, message: entry.message, metadata: entry.metadata, occurredAt: entry.occurredAt.toISOString(), }, }); } catch { // Instrumentation is an observer; durable audit writes must not depend on it. } }, }; } /** * Create an in-memory audit log for tests and local examples. * * Entries are normalized and redacted before being pushed into the shared * `entries` array. * * @example * ```ts * const audit = createMemoryAuditLog(); * await audit.record({ * action: "posts.publish", * actor: createUserActor("user_1"), * }); * expect(audit.entries).toHaveLength(1); * ``` * * @param entries - Optional backing array, useful when tests need shared state. * @param options - Optional final redaction/customization hook. * @returns An in-memory audit log port with captured `entries`. */ export function createMemoryAuditLog(entries = [], options = {}) { return { entries, record(entry) { const normalized = normalizeAuditLogEntry(entry); entries.push(options.redact ? options.redact(redactAuditLogEntry(normalized)) : redactAuditLogEntry(normalized)); }, }; } //# sourceMappingURL=audit.js.map