218 lines
4.9 KiB
JavaScript
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)}`;
|
|
}
|