@beignet/core
Version:
Core framework primitives for Beignet
270 lines • 8.97 kB
JavaScript
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