Bifrost/tools/bifrost-crossing-documents.mjs

336 lines
13 KiB
JavaScript

import { createRequire } from "node:module";
import { randomUUID } from "node:crypto";
import { existsSync } from "node:fs";
import { mkdir } from "node:fs/promises";
import { dirname, resolve } from "node:path";
const repoRoot = resolve(import.meta.dirname, "..");
const projectsRoot = resolve(repoRoot, "..");
const cultLibRoot = resolve(process.env.VOIDBOT_CULTLIB_ROOT || resolve(projectsRoot, "CultLib"));
export const crossingReceiptDocumentType = "bifrost.crossing_receipt";
export const crossingReceiptSchemaId = "bifrost.crossing_receipt.v1";
export const defaultCrossingReceiptStorePath = resolve(repoRoot, ".bifrost", "bridge-receipts.cc");
export const crossingReceiptStatuses = Object.freeze(["requested", "running", "completed", "failed", "cancelled"]);
const validStatuses = new Set(crossingReceiptStatuses);
let cachedRuntime;
let cachedDefinition;
export function crossingReceiptDefinition(defineDocumentTypeInput) {
const defineDocumentType = defineDocumentTypeInput ?? loadCultCacheRuntime().defineDocumentType;
cachedDefinition ??= defineDocumentType({
type: crossingReceiptDocumentType,
schemaId: crossingReceiptSchemaId,
schemaName: crossingReceiptDocumentType,
schemaVersion: crossingReceiptSchemaId,
contentHash: crossingReceiptSchemaId,
global: false,
name: "receiptId",
indexes: {
commandId: "commandId",
crossingKind: "crossingKind",
status: "status",
},
schema: { parse: parseCrossingReceipt },
members: [
{ slot: 1, memberName: "receiptId", typeName: "string", isName: true },
{ slot: 2, memberName: "commandId", typeName: "string", indexAlias: "commandId" },
{ slot: 3, memberName: "crossingKind", typeName: "string", indexAlias: "crossingKind" },
{ slot: 4, memberName: "status", typeName: "string", indexAlias: "status" },
{ slot: 5, memberName: "ok", typeName: "boolean" },
{ slot: 6, memberName: "requestedAt", typeName: "string" },
{ slot: 7, memberName: "startedAt", typeName: "string" },
{ slot: 8, memberName: "completedAt", typeName: "string" },
{ slot: 9, memberName: "actor", typeName: "object" },
{ slot: 10, memberName: "source", typeName: "object" },
{ slot: 11, memberName: "authority", typeName: "object" },
{ slot: 12, memberName: "epiphany", typeName: "object" },
{ slot: 13, memberName: "target", typeName: "object" },
{ slot: 14, memberName: "externalReceipt", typeName: "object" },
{ slot: 15, memberName: "error", typeName: "object" },
{ slot: 16, memberName: "supersedes", typeName: "string" },
{ slot: 17, memberName: "relatedReceiptIds", typeName: "string", isMany: true },
{ slot: 18, memberName: "surfaceReceipt", typeName: "object" },
],
});
return cachedDefinition;
}
export async function openCrossingReceiptStore(storePath = defaultCrossingReceiptStorePath) {
const { CultCache, SingleFileMessagePackBackingStore } = loadCultCacheRuntime();
await mkdir(dirname(storePath), { recursive: true });
const cache = CultCache.builder()
.withDocumentType(crossingReceiptDefinition())
.withGenericStore(new SingleFileMessagePackBackingStore(storePath))
.build();
await cache.pullAllBackingStores();
return {
storePath,
definition: crossingReceiptDefinition(),
cache,
async put(receipt) {
const parsed = buildCrossingReceipt(receipt);
await cache.put(crossingReceiptDefinition(), parsed.receiptId, parsed);
return parsed;
},
get(receiptId) {
return cache.get(crossingReceiptDefinition(), receiptId);
},
getAll() {
return cache.getAll(crossingReceiptDefinition());
},
};
}
export function resolveCrossingReceiptStorePath(options = {}) {
return resolveOptionPath(
options["receipt-store"] ??
process.env.BIFROST_CROSSING_RECEIPT_STORE ??
process.env.BIFROST_BRIDGE_RECEIPT_STORE ??
defaultCrossingReceiptStorePath,
);
}
export async function writeCrossingReceipt(receipt, options = {}) {
const store = await openCrossingReceiptStore(resolveCrossingReceiptStorePath(options));
return store.put(receipt);
}
export async function beginCrossingReceipt(options, input) {
const requestedAt = input.requestedAt ?? new Date().toISOString();
const commandId = requiredString(input.commandId ?? commandIdFromOptions(options), "commandId");
const receiptId = input.receiptId ?? `crossing_${commandId}`;
const base = buildCrossingReceipt({
schemaName: crossingReceiptDocumentType,
schemaVersion: crossingReceiptSchemaId,
receiptId,
commandId,
crossingKind: requiredString(input.crossingKind, "crossingKind"),
status: "requested",
ok: false,
requestedAt,
startedAt: "",
completedAt: "",
actor: normalizeObject(input.actor),
source: normalizeObject(input.source),
authority: normalizeObject(input.authority),
epiphany: normalizeObject(input.epiphany),
target: normalizeObject(input.target),
externalReceipt: {},
error: {},
supersedes: optionalString(input.supersedes) ?? "",
relatedReceiptIds: normalizeStringArray(input.relatedReceiptIds),
surfaceReceipt: normalizeObject(input.surfaceReceipt),
});
await writeCrossingReceipt(base, options);
let current = base;
return {
receiptId,
commandId,
requested: base,
async start(extra = {}) {
current = await writeCrossingReceipt({
...current,
...extra,
status: "running",
ok: false,
startedAt: extra.startedAt ?? new Date().toISOString(),
}, options);
return current;
},
async complete(extra = {}) {
current = await writeCrossingReceipt({
...current,
...extra,
status: "completed",
ok: true,
startedAt: extra.startedAt ?? current.startedAt,
completedAt: extra.completedAt ?? new Date().toISOString(),
externalReceipt: normalizeObject(extra.externalReceipt ?? current.externalReceipt),
surfaceReceipt: normalizeObject(extra.surfaceReceipt ?? current.surfaceReceipt),
}, options);
return current;
},
async fail(error, extra = {}) {
current = await writeCrossingReceipt({
...current,
...extra,
status: "failed",
ok: false,
startedAt: extra.startedAt ?? current.startedAt,
completedAt: extra.completedAt ?? new Date().toISOString(),
error: normalizeError(error),
externalReceipt: normalizeObject(extra.externalReceipt ?? current.externalReceipt),
surfaceReceipt: normalizeObject(extra.surfaceReceipt ?? current.surfaceReceipt),
}, options);
return current;
},
};
}
export function buildCrossingReceipt(input) {
return parseCrossingReceipt({
schemaName: crossingReceiptDocumentType,
schemaVersion: crossingReceiptSchemaId,
...input,
});
}
export function crossingProvenanceFromOptions(options = {}) {
const commandId = commandIdFromOptions(options);
return {
commandId,
actor: {
bifrostIdentity: optionalString(options.identity) ?? optionalString(process.env.BIFROST_IDENTITY) ?? "",
kind: optionalString(options["actor-kind"]) ?? "",
name: optionalString(options["actor-name"]) ?? optionalString(options.identity) ?? optionalString(process.env.BIFROST_IDENTITY) ?? "",
},
source: {
kind: optionalString(options["source-kind"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_KIND) ?? "cultmesh-command",
id: optionalString(options["source-id"]) ?? optionalString(process.env.BIFROST_BRIDGE_SOURCE_ID) ?? commandId,
topicId: optionalString(options["topic-id"]) ?? "",
requestId: optionalString(options["request-id"]) ?? "",
workItemId: optionalString(options["work-item-id"]) ?? "",
motionId: optionalString(options["motion-id"]) ?? "",
channelId: optionalString(options["source-channel-id"]) ?? "",
messageId: optionalString(options["source-message-id"]) ?? "",
},
authority: {
authorityRef: optionalString(options["authority-ref"]) ?? optionalString(process.env.BIFROST_BRIDGE_AUTHORITY_REF) ?? `cultmesh-command:${commandId}`,
heimdallCapabilityRef:
optionalString(options["heimdall-capability-ref"]) ??
optionalString(process.env.HEIMDALL_CAPABILITY_REF) ??
optionalString(process.env.BIFROST_HEIMDALL_CAPABILITY_REF) ??
"",
heimdallClaimJti: optionalString(options["heimdall-claim-jti"]) ?? optionalString(process.env.HEIMDALL_CLAIM_JTI) ?? "",
heimdallAccountId: optionalString(options["heimdall-account-id"]) ?? optionalString(process.env.HEIMDALL_ACCOUNT_ID) ?? "",
heimdallAccessRevision: optionalString(options["heimdall-access-revision"]) ?? optionalString(process.env.HEIMDALL_ACCESS_REVISION) ?? "",
heimdallGrantRef: optionalString(options["heimdall-grant-ref"]) ?? optionalString(process.env.HEIMDALL_GRANT_REF) ?? "",
heimdallClaimExpiresAt: optionalString(options["heimdall-claim-exp"]) ?? optionalString(process.env.HEIMDALL_CLAIM_EXP) ?? "",
policyDecisionId: optionalString(options["policy-decision-id"]) ?? optionalString(process.env.BIFROST_POLICY_DECISION_ID) ?? "",
},
epiphany: {
runId: optionalString(options["epiphany-run-id"]) ?? optionalString(process.env.EPIPHANY_RUN_ID) ?? "",
laneId: optionalString(options["epiphany-lane-id"]) ?? optionalString(process.env.EPIPHANY_LANE_ID) ?? "",
agentIdentity: optionalString(options["epiphany-agent-identity"]) ?? optionalString(process.env.EPIPHANY_AGENT_IDENTITY) ?? "",
},
};
}
export function commandIdFromOptions(options = {}) {
return optionalString(options["cultmesh-command-id"]) ?? optionalString(process.env.BIFROST_CULTMESH_COMMAND_ID) ?? "";
}
function parseCrossingReceipt(input) {
if (!input || typeof input !== "object" || Array.isArray(input)) {
throw new Error("Bifrost crossing receipt must be an object.");
}
const receipt = {
schemaName: optionalString(input.schemaName) ?? crossingReceiptDocumentType,
schemaVersion: optionalString(input.schemaVersion) ?? crossingReceiptSchemaId,
receiptId: requiredString(input.receiptId, "receiptId"),
commandId: requiredString(input.commandId, "commandId"),
crossingKind: requiredString(input.crossingKind, "crossingKind"),
status: requiredString(input.status, "status"),
ok: Boolean(input.ok),
requestedAt: requiredString(input.requestedAt, "requestedAt"),
startedAt: optionalString(input.startedAt) ?? "",
completedAt: optionalString(input.completedAt) ?? "",
actor: normalizeObject(input.actor),
source: normalizeObject(input.source),
authority: normalizeObject(input.authority),
epiphany: normalizeObject(input.epiphany),
target: normalizeObject(input.target),
externalReceipt: normalizeObject(input.externalReceipt),
error: normalizeObject(input.error),
supersedes: optionalString(input.supersedes) ?? "",
relatedReceiptIds: normalizeStringArray(input.relatedReceiptIds),
surfaceReceipt: normalizeObject(input.surfaceReceipt),
};
if (!validStatuses.has(receipt.status)) {
throw new Error(`Bifrost crossing receipt status "${receipt.status}" is not valid.`);
}
if (!receipt.source.kind && !receipt.source.id) {
throw new Error("Bifrost crossing receipt requires source provenance.");
}
if (!receipt.authority.authorityRef && !receipt.authority.heimdallCapabilityRef && !receipt.authority.heimdallClaimJti) {
throw new Error("Bifrost crossing receipt requires authority provenance.");
}
return receipt;
}
function loadCultCacheRuntime() {
if (cachedRuntime) {
return cachedRuntime;
}
const entryPoint = resolveFirstExisting("CultCache TypeScript runtime", [
resolve(cultLibRoot, "packages", "cultcache-ts", "dist", "index.js"),
resolve(projectsRoot, "CultCacheTS", "dist", "index.js"),
]);
const requireCultCache = createRequire(entryPoint);
cachedRuntime = requireCultCache(entryPoint);
return cachedRuntime;
}
function resolveFirstExisting(label, candidates) {
for (const candidate of candidates) {
if (existsSync(candidate)) {
return candidate;
}
}
throw new Error(`${label} is unavailable. Tried: ${candidates.join(", ")}`);
}
function normalizeError(error) {
if (!error) {
return {};
}
if (error instanceof Error) {
return {
code: error.name || "Error",
message: error.message,
};
}
return {
code: "Error",
message: String(error),
};
}
function normalizeObject(value) {
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
}
function normalizeStringArray(value) {
if (value === undefined || value === null) {
return [];
}
if (!Array.isArray(value)) {
return [];
}
return value.map((item) => optionalString(item)).filter(Boolean);
}
function requiredString(value, field) {
const normalized = optionalString(value);
if (!normalized) {
throw new Error(`Bifrost crossing receipt field "${field}" must be a non-empty string.`);
}
return normalized;
}
function optionalString(value) {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function resolveOptionPath(path) {
return resolve(process.cwd(), path);
}