Files
pikasTech-HWLAB/internal/audit/index.mjs
T
2026-05-21 18:33:01 +00:00

218 lines
4.9 KiB
JavaScript

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)}`;
}