import { ENVIRONMENT_DEV, assertProtocolRecord, isFrozenServiceId } from "../protocol/index.mjs"; export const CLOUD_API_SERVICE_ID = "hwlab-cloud-api"; export const AUDIT_FIELD_NAMES = Object.freeze([ "requestId", "actor", "source", "operation", "target", "result", "timestamp" ]); export const PROTOCOL_AUDIT_FIELD_NAMES = Object.freeze([ "auditId", "traceId", "actorType", "actorId", "action", "targetType", "targetId", "serviceId", "environment", "occurredAt" ]); export function createAuditRecord(input = {}) { const record = { requestId: asString(input.requestId, "req_unassigned"), actor: normalizeActor(input.actor), source: normalizeSource(input.source, input.traceId), operation: asString(input.operation, "cloud_api.request"), target: normalizeTarget(input.target), result: asString(input.result, "unknown"), timestamp: asString(input.timestamp, new Date().toISOString()) }; assertAuditFields(record); return record; } export function createProtocolAuditEvent(input = {}) { const event = { auditId: asString(input.auditId, `aud_${cryptoRandomId()}`), traceId: asString(input.traceId, `trc_${cryptoRandomId()}`), actorType: normalizeActorType(input.actorType), actorId: asString(input.actorId, `system_${CLOUD_API_SERVICE_ID}`), action: asString(input.action, "cloud_api.request"), targetType: asString(input.targetType, "cloud_api"), targetId: asString(input.targetId, `svc_${CLOUD_API_SERVICE_ID}`), serviceId: isFrozenServiceId(input.serviceId) ? input.serviceId : CLOUD_API_SERVICE_ID, environment: ENVIRONMENT_DEV, occurredAt: asString(input.occurredAt, new Date().toISOString()) }; copyOptional(event, input, [ "projectId", "gatewaySessionId", "workerSessionId", "operationId", "outcome", "reason", "metadata" ]); assertProtocolRecord("auditEvent", event); return event; } export function deriveActorFromMeta(meta = {}) { return { type: meta.actorType || "system", id: meta.actorId || `system_${CLOUD_API_SERVICE_ID}` }; } export function deriveProtocolActorFromMeta(meta = {}, fallback = {}) { return { actorType: normalizeActorType(fallback.actorType || meta.actorType || "service"), actorId: asString(fallback.actorId || meta.actorId, `system_${CLOUD_API_SERVICE_ID}`) }; } export function assertAuditFields(record) { if (!record || typeof record !== "object" || Array.isArray(record)) { throw new Error("audit record must be an object"); } for (const field of AUDIT_FIELD_NAMES) { if (!Object.hasOwn(record, field)) { throw new Error(`audit.${field} is required`); } } if (Number.isNaN(Date.parse(record.timestamp))) { throw new Error("audit.timestamp must be an RFC 3339 timestamp"); } } function normalizeActor(actor) { if (!actor) { return { type: "system", id: `system_${CLOUD_API_SERVICE_ID}` }; } if (typeof actor === "string") { return { type: "user", id: actor }; } if (typeof actor === "object" && !Array.isArray(actor)) { return { type: asString(actor.type, "user"), id: asString(actor.id, `system_${CLOUD_API_SERVICE_ID}`) }; } return { type: "system", id: `system_${CLOUD_API_SERVICE_ID}` }; } function normalizeSource(source, traceId) { const base = { serviceId: CLOUD_API_SERVICE_ID, environment: ENVIRONMENT_DEV }; if (traceId) { base.traceId = traceId; } if (!source) { return base; } if (typeof source === "string") { return { ...base, serviceId: source }; } if (typeof source === "object" && !Array.isArray(source)) { return { ...base, ...source }; } return base; } function normalizeTarget(target) { if (!target) { return { type: "cloud_api", id: CLOUD_API_SERVICE_ID }; } if (typeof target === "string") { return { type: "resource", id: target }; } if (typeof target === "object" && !Array.isArray(target)) { return { type: asString(target.type, "resource"), id: asString(target.id, "target_unspecified") }; } return { type: "cloud_api", id: CLOUD_API_SERVICE_ID }; } function asString(value, fallback) { if (value === undefined || value === null || value === "") { return fallback; } return String(value); } function normalizeActorType(value) { const actorType = asString(value, "system"); if (["user", "service", "agent", "worker", "system"].includes(actorType)) { return actorType; } return "system"; } function copyOptional(target, source, fields) { for (const field of fields) { if (source[field] !== undefined && source[field] !== null) { target[field] = source[field]; } } } function cryptoRandomId() { return globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(16).slice(2)}`; }