79 lines
3.8 KiB
TypeScript
79 lines
3.8 KiB
TypeScript
/** Process/module ABI shared by generated adapters. Values are decoded against the checked
|
|
* operation/port schema by the coordinator, not trusted because this header parsed. */
|
|
export const transactionPortAbi = "quixos-transaction-ports-v1";
|
|
export type PortAction =
|
|
| { kind: "state"; primitive: "read" | "write" | "edit" }
|
|
| { kind: "edge"; primitive: "resolve" | "connect" | "disconnect" }
|
|
| { kind: "interface"; operationId: string; key?: string }
|
|
| { kind: "query" }
|
|
| { kind: "creation"; key: string };
|
|
export interface PortInvocation {
|
|
abi: typeof transactionPortAbi;
|
|
/** Coordinator-issued frame and invocation handles, not application object IDs. */
|
|
frame: string;
|
|
invocation: string;
|
|
sequence: number;
|
|
portId: string;
|
|
action: PortAction;
|
|
input: unknown;
|
|
}
|
|
export interface PackageInvocation {
|
|
abi: typeof transactionPortAbi;
|
|
frame: string;
|
|
invocation: string;
|
|
exportId: string;
|
|
effect: "snapshot" | "transaction" | "remote";
|
|
/** Capability arguments/receiver use the versioned managed-reference codec. */
|
|
input: unknown;
|
|
receiver: unknown | null;
|
|
}
|
|
export type InvocationReply =
|
|
| { abi: typeof transactionPortAbi; invocation: string; status: "returned"; output: unknown }
|
|
| { abi: typeof transactionPortAbi; invocation: string; status: "failed"; error: { code: string; message: string } };
|
|
export type ServiceControl =
|
|
| { abi: typeof transactionPortAbi; kind: "ready" }
|
|
| { abi: typeof transactionPortAbi; kind: "cancel"; invocation: string }
|
|
| { abi: typeof transactionPortAbi; kind: "shutdown" };
|
|
/** ABI-only header check. LF-08 resolves handles and enforces the frame/port authority. */
|
|
export function checkPortInvocation(value: unknown): PortInvocation {
|
|
if (!value || typeof value !== "object" || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype)
|
|
throw new Error("Invalid port invocation");
|
|
const fields = value as Record<string, unknown>;
|
|
const keys = ["abi", "frame", "invocation", "sequence", "portId", "action", "input"];
|
|
if (
|
|
Object.keys(fields).length !== keys.length ||
|
|
keys.some((key) => !Object.hasOwn(fields, key)) ||
|
|
fields.abi !== transactionPortAbi
|
|
)
|
|
throw new Error("Invalid port invocation fields or ABI");
|
|
for (const key of ["frame", "invocation", "portId"])
|
|
if (typeof fields[key] !== "string" || !fields[key] || fields[key].length > 4096)
|
|
throw new Error(`Invalid port invocation ${key}`);
|
|
if (!Number.isSafeInteger(fields.sequence) || (fields.sequence as number) < 1)
|
|
throw new Error("Invalid port invocation sequence");
|
|
const action = fields.action as Record<string, unknown>;
|
|
if (
|
|
!action ||
|
|
typeof action !== "object" ||
|
|
Array.isArray(action) ||
|
|
Object.getPrototypeOf(action) !== Object.prototype
|
|
)
|
|
throw new Error("Invalid port action");
|
|
const exact = (names: string[]) =>
|
|
Object.keys(action).length === names.length && names.every((name) => Object.hasOwn(action, name));
|
|
const identifier = (value: unknown) => typeof value === "string" && value.length > 0 && value.length <= 1024;
|
|
const valid =
|
|
action.kind === "state"
|
|
? exact(["kind", "primitive"]) && ["read", "write", "edit"].includes(action.primitive as string)
|
|
: action.kind === "edge"
|
|
? exact(["kind", "primitive"]) && ["resolve", "connect", "disconnect"].includes(action.primitive as string)
|
|
: action.kind === "interface"
|
|
? (exact(["kind", "operationId"]) || (exact(["kind", "operationId", "key"]) && identifier(action.key))) &&
|
|
identifier(action.operationId)
|
|
: action.kind === "creation"
|
|
? exact(["kind", "key"]) && identifier(action.key)
|
|
: action.kind === "query" && exact(["kind"]);
|
|
if (!valid) throw new Error("Invalid port action");
|
|
return { ...fields, action: { ...action } } as unknown as PortInvocation;
|
|
}
|