@zanii/certify 0.0.0-stage → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +159 -0
  2. package/README.md +112 -2
  3. package/dist/appliance-execution.d.ts +18 -0
  4. package/dist/appliance-execution.js +12 -0
  5. package/dist/appliance.d.ts +135 -0
  6. package/dist/appliance.js +253 -0
  7. package/dist/assurance-status.d.ts +58 -0
  8. package/dist/assurance-status.js +86 -0
  9. package/dist/audit-renewal.d.ts +39 -0
  10. package/dist/audit-renewal.js +62 -0
  11. package/dist/audit.d.ts +69 -0
  12. package/dist/audit.js +112 -0
  13. package/dist/beacon-client.d.ts +38 -0
  14. package/dist/beacon-client.js +52 -0
  15. package/dist/beacon.d.ts +84 -0
  16. package/dist/beacon.js +106 -0
  17. package/dist/bitstring-status.d.ts +56 -0
  18. package/dist/bitstring-status.js +135 -0
  19. package/dist/board.d.ts +218 -0
  20. package/dist/board.js +428 -0
  21. package/dist/bundle.d.ts +92 -0
  22. package/dist/bundle.js +109 -0
  23. package/dist/certificate.d.ts +161 -0
  24. package/dist/certificate.js +345 -0
  25. package/dist/cli-messages.json +148 -0
  26. package/dist/cli.d.ts +16 -0
  27. package/dist/cli.js +369 -0
  28. package/dist/current-issuer-vc.d.ts +20 -0
  29. package/dist/current-issuer-vc.js +60 -0
  30. package/dist/evidence-share.d.ts +89 -0
  31. package/dist/evidence-share.js +479 -0
  32. package/dist/federation.d.ts +103 -0
  33. package/dist/federation.js +240 -0
  34. package/dist/feed-snapshot.d.ts +97 -0
  35. package/dist/feed-snapshot.js +186 -0
  36. package/dist/fingerprint.d.ts +23 -0
  37. package/dist/fingerprint.js +48 -0
  38. package/dist/formats/status-v2.schema.json +31 -0
  39. package/dist/governance.d.ts +242 -0
  40. package/dist/governance.js +411 -0
  41. package/dist/identity.d.ts +79 -0
  42. package/dist/identity.js +146 -0
  43. package/dist/impartiality.d.ts +47 -0
  44. package/dist/impartiality.js +85 -0
  45. package/dist/index.d.ts +58 -0
  46. package/dist/index.js +59 -0
  47. package/dist/issuer-authorization.d.ts +4 -0
  48. package/dist/issuer-authorization.js +52 -0
  49. package/dist/issuer-resolution.d.ts +25 -0
  50. package/dist/issuer-resolution.js +89 -0
  51. package/dist/licence-owner-entry.d.ts +20 -0
  52. package/dist/licence-owner-entry.js +33 -0
  53. package/dist/licence-owner.d.ts +67 -0
  54. package/dist/licence-owner.js +180 -0
  55. package/dist/licence.d.ts +174 -0
  56. package/dist/licence.js +400 -0
  57. package/dist/live-evidence.d.ts +135 -0
  58. package/dist/live-evidence.js +210 -0
  59. package/dist/nitro-roots.d.ts +3 -0
  60. package/dist/nitro-roots.js +20 -0
  61. package/dist/nitro-x509.d.ts +16 -0
  62. package/dist/nitro-x509.js +141 -0
  63. package/dist/nitro.d.ts +74 -0
  64. package/dist/nitro.js +329 -0
  65. package/dist/normalization.d.ts +8 -0
  66. package/dist/normalization.js +10 -0
  67. package/dist/pq.d.ts +32 -0
  68. package/dist/pq.js +69 -0
  69. package/dist/profile.d.ts +20 -0
  70. package/dist/profile.js +161 -0
  71. package/dist/register.d.ts +79 -0
  72. package/dist/register.js +134 -0
  73. package/dist/rotation.d.ts +49 -0
  74. package/dist/rotation.js +124 -0
  75. package/dist/royalty-reconciliation.d.ts +24 -0
  76. package/dist/royalty-reconciliation.js +88 -0
  77. package/dist/runtime-client.d.ts +38 -0
  78. package/dist/runtime-client.js +185 -0
  79. package/dist/runtime-gateway.d.ts +24 -0
  80. package/dist/runtime-gateway.js +167 -0
  81. package/dist/runtime-knowledge.d.ts +28 -0
  82. package/dist/runtime-knowledge.js +75 -0
  83. package/dist/runtime-nitro.d.ts +30 -0
  84. package/dist/runtime-nitro.js +57 -0
  85. package/dist/runtime.d.ts +226 -0
  86. package/dist/runtime.js +254 -0
  87. package/dist/sandbox-delegation.d.ts +38 -0
  88. package/dist/sandbox-delegation.js +61 -0
  89. package/dist/schema-dialect.d.ts +1 -0
  90. package/dist/schema-dialect.js +97 -0
  91. package/dist/schemas/clause-map.schema.json +52 -0
  92. package/dist/schemas/profile.schema.json +80 -0
  93. package/dist/schemas/scenarios.schema.json +573 -0
  94. package/dist/schemas/suite.schema.json +138 -0
  95. package/dist/scheme-authority.d.ts +5 -0
  96. package/dist/scheme-authority.js +15 -0
  97. package/dist/scheme-entry.d.ts +25 -0
  98. package/dist/scheme-entry.js +85 -0
  99. package/dist/scheme-history.d.ts +5 -0
  100. package/dist/scheme-history.js +87 -0
  101. package/dist/scheme-log.d.ts +98 -0
  102. package/dist/scheme-log.js +266 -0
  103. package/dist/scheme-merkle.d.ts +2 -0
  104. package/dist/scheme-merkle.js +20 -0
  105. package/dist/scheme-text.d.ts +25 -0
  106. package/dist/scheme-text.js +46 -0
  107. package/dist/scheme-timing.d.ts +16 -0
  108. package/dist/scheme-timing.js +22 -0
  109. package/dist/scheme-transition.d.ts +47 -0
  110. package/dist/scheme-transition.js +101 -0
  111. package/dist/scoring.d.ts +35 -0
  112. package/dist/scoring.js +50 -0
  113. package/dist/shared.d.ts +16 -0
  114. package/dist/shared.js +20 -0
  115. package/dist/signer.d.ts +18 -0
  116. package/dist/signer.js +49 -0
  117. package/dist/signing-identity.d.ts +1 -0
  118. package/dist/signing-identity.js +11 -0
  119. package/dist/status.d.ts +113 -0
  120. package/dist/status.js +204 -0
  121. package/dist/succession.d.ts +21 -0
  122. package/dist/succession.js +67 -0
  123. package/dist/suite.d.ts +257 -0
  124. package/dist/suite.js +441 -0
  125. package/dist/vc.d.ts +29 -0
  126. package/dist/vc.js +96 -0
  127. package/dist/verification-bounds.d.ts +5 -0
  128. package/dist/verification-bounds.js +76 -0
  129. package/dist/verification-policy.d.ts +32 -0
  130. package/dist/verification-policy.js +85 -0
  131. package/dist/witness.d.ts +19 -0
  132. package/dist/witness.js +80 -0
  133. package/package.json +57 -3
@@ -0,0 +1,185 @@
1
+ // The attester must run inside approved measured code with a protected key; HTTP signatures alone do not establish that.
2
+ import { randomBytes } from "node:crypto";
3
+ import { publicKeyFromDid } from "@zanii/core";
4
+ import { sha256Bytes } from "./fingerprint.js";
5
+ import { RUNTIME_NITRO_MAX_BYTES, runtimeNitroBindingHash } from "./runtime-nitro.js";
6
+ import { buildRuntimeChallengeRequest, buildRuntimeProof, runtimePolicyHash, verifyRuntimeApproval } from "./runtime.js";
7
+ import { isDid, isIso, isObj } from "./shared.js";
8
+ import { signerDid } from "./signer.js";
9
+ const DEADLINE_MS = 10_000, MAX_RESPONSE_BYTES = 32 * 1024;
10
+ const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
11
+ /** One reporter has at most one underlying operation, including an uncancellable attester after timeout. */
12
+ export function createRuntimeReporter(options) {
13
+ const authority = options.authority;
14
+ let url;
15
+ try {
16
+ url = new URL(authority);
17
+ }
18
+ catch {
19
+ throw new Error("runtime authority must be a canonical HTTPS origin");
20
+ }
21
+ if (typeof authority !== "string" || authority.length > 2048 || !/^[\x21-\x7e]+$/.test(authority) || url.protocol !== "https:"
22
+ || authority !== url.origin || url.username || url.password)
23
+ throw new Error("runtime authority must be a canonical HTTPS origin");
24
+ if (!Array.isArray(options.issuers) || !options.issuers.length || options.issuers.length > 100 || !options.issuers.every(isDid))
25
+ throw new Error("explicit runtime issuer DIDs are required");
26
+ const issuers = [...options.issuers], clock = options.clock ?? Date.now, call = options.fetch ?? fetch, attest = options.attest;
27
+ if (typeof attest !== "function" || !(options.signer?.publicKey instanceof Uint8Array) || options.signer.publicKey.length !== 32 || typeof options.signer.sign !== "function")
28
+ throw new Error("a workload signer and trusted attester are required");
29
+ const signer = { publicKey: new Uint8Array(options.signer.publicKey), sign: options.signer.sign.bind(options.signer) };
30
+ const workload = signerDid(signer), deployment = options.deployment;
31
+ const policyHash = runtimePolicyHash(options.policy);
32
+ const approve = (approval) => {
33
+ const check = verifyRuntimeApproval(approval, { signers: issuers, now: new Date(clock()).toISOString(), certificate: approval?.certificate,
34
+ release: approval?.release, policy: policyHash });
35
+ if (!check.ok)
36
+ throw new Error(`runtime approval refused: ${check.reasons.join("; ")}`);
37
+ };
38
+ approve(options.approval);
39
+ const approval = structuredClone(options.approval), policy = structuredClone(options.policy);
40
+ const context = { certificate: approval.certificate, release: approval.release, policy: policyHash, deployment, workload };
41
+ runtimeNitroBindingHash({ ...context, challenge: "0".repeat(32) });
42
+ const interval = options.intervalMs ?? Math.min(15_000, Math.floor(policy.evidence_max_age_s * 1000 / 3));
43
+ if (!Number.isSafeInteger(interval) || interval < 1_000 || interval >= policy.evidence_max_age_s * 1000)
44
+ throw new Error("runtime report interval must be at least 1000ms and below the evidence lifetime");
45
+ let running = null, controller = null, timer = null;
46
+ let started = false, stopped = false;
47
+ const abortCheck = (signal) => { if (signal.aborted)
48
+ throw signal.reason; };
49
+ async function post(path, body, signal) {
50
+ abortCheck(signal);
51
+ const response = await call(`${authority}/v1/runtime/deployments/${deployment}/${path}`, { method: "POST", redirect: "error",
52
+ credentials: "omit", headers: { "content-type": "application/json", accept: "application/json" }, body: JSON.stringify(body), signal });
53
+ abortCheck(signal);
54
+ if (response.redirected || (response.url && new URL(response.url).origin !== authority))
55
+ throw new Error("runtime authority redirected the request");
56
+ const length = response.headers.get("content-length");
57
+ if (length && (!/^\d+$/.test(length) || Number(length) > MAX_RESPONSE_BYTES)) {
58
+ void response.body?.cancel().catch(() => { });
59
+ throw new Error("runtime authority response exceeds 32KiB");
60
+ }
61
+ const reader = response.body?.getReader();
62
+ const chunks = [];
63
+ let size = 0;
64
+ try {
65
+ if (reader)
66
+ for (;;) {
67
+ const part = await reader.read();
68
+ abortCheck(signal);
69
+ if (part.done)
70
+ break;
71
+ size += part.value.byteLength;
72
+ if (size > MAX_RESPONSE_BYTES)
73
+ throw new Error("runtime authority response exceeds 32KiB");
74
+ chunks.push(part.value);
75
+ }
76
+ }
77
+ catch (error) {
78
+ void reader?.cancel().catch(() => { });
79
+ throw error;
80
+ }
81
+ finally {
82
+ reader?.releaseLock();
83
+ }
84
+ if (!response.ok)
85
+ throw new Error(`runtime authority refused ${path} (HTTP ${response.status})`);
86
+ let value;
87
+ try {
88
+ value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks)));
89
+ }
90
+ catch {
91
+ throw new Error("runtime authority returned invalid JSON");
92
+ }
93
+ if (!isObj(value))
94
+ throw new Error("runtime authority returned an invalid response");
95
+ return value;
96
+ }
97
+ async function report(signal) {
98
+ approve(approval);
99
+ const request = await buildRuntimeChallengeRequest({ deployment, certificate: approval.certificate, release: approval.release,
100
+ nonce: randomBytes(32).toString("base64url"), issued_at: new Date(clock()).toISOString() }, signer);
101
+ const challenge = await post("challenge", request, signal);
102
+ if (Object.keys(challenge).length !== 7 || Object.entries(context).some(([key, value]) => challenge[key] !== value)
103
+ || typeof challenge.challenge !== "string" || !/^[A-Za-z0-9_-]{32,128}$/.test(challenge.challenge)
104
+ || !isIso(challenge.expires_at) || Date.parse(challenge.expires_at) <= clock()
105
+ || Date.parse(challenge.expires_at) > clock() + policy.challenge_ttl_s * 1000 + 5000)
106
+ throw new Error("runtime challenge does not match the approved deployment");
107
+ const binding = { ...context, challenge: challenge.challenge };
108
+ const document = await attest({ ...binding, nonce: binding.challenge, publicKey: Buffer.concat([SPKI_PREFIX, publicKeyFromDid(workload)]), userData: Buffer.from(runtimeNitroBindingHash(binding), "utf8") });
109
+ abortCheck(signal);
110
+ if (!(document instanceof Uint8Array) || document.byteLength < 1 || document.byteLength > RUNTIME_NITRO_MAX_BYTES)
111
+ throw new Error("runtime attestation must contain 1 to 16384 bytes");
112
+ const quote = new Uint8Array(document);
113
+ if (Date.parse(challenge.expires_at) <= clock())
114
+ throw new Error("runtime challenge expired during attestation");
115
+ const proof = await buildRuntimeProof({ deployment, certificate: approval.certificate, release: approval.release, challenge: binding.challenge,
116
+ public_key: workload, attestation_sha256: sha256Bytes(quote), observed_at: new Date(clock()).toISOString() }, signer);
117
+ const result = await post("evidence", { proof, attestation_document: Buffer.from(quote).toString("base64") }, signal);
118
+ if (result.accepted !== true || !isObj(result.deployment) || result.deployment.id !== deployment || result.deployment.state !== "fresh"
119
+ || !isIso(result.deployment.evidence_until) || Date.parse(result.deployment.evidence_until) <= clock())
120
+ throw new Error("runtime authority did not accept fresh deployment evidence");
121
+ return { accepted: true, deployment, evidence_until: result.deployment.evidence_until };
122
+ }
123
+ function schedule() {
124
+ if (started && !stopped && !running)
125
+ timer = setTimeout(() => { timer = null; void send().catch(() => { }); }, interval);
126
+ }
127
+ function send() {
128
+ if (stopped)
129
+ return Promise.reject(new Error("runtime reporter is stopped"));
130
+ if (running)
131
+ return Promise.reject(new Error("runtime reporter is busy"));
132
+ if (timer) {
133
+ clearTimeout(timer);
134
+ timer = null;
135
+ }
136
+ const control = new AbortController();
137
+ controller = control;
138
+ let finish, reject;
139
+ const result = new Promise((resolve, fail) => { finish = resolve; reject = fail; });
140
+ let settled = false;
141
+ const notify = (value) => {
142
+ try {
143
+ void Promise.resolve(options.onResult?.(value)).catch(() => { });
144
+ }
145
+ catch { /* Observers cannot break reporting or create retry queues. */ }
146
+ };
147
+ const fail = (error) => { if (!settled) {
148
+ settled = true;
149
+ reject(error);
150
+ notify({ accepted: false, error: error instanceof Error ? error : new Error("runtime report failed") });
151
+ } };
152
+ const abort = () => fail(control.signal.reason);
153
+ control.signal.addEventListener("abort", abort, { once: true });
154
+ const deadline = setTimeout(() => control.abort(new Error("runtime report exceeded its 10000ms deadline")), DEADLINE_MS);
155
+ const cleanup = () => {
156
+ clearTimeout(deadline);
157
+ control.signal.removeEventListener("abort", abort);
158
+ controller = null;
159
+ running = null;
160
+ schedule();
161
+ };
162
+ running = report(control.signal).then((value) => {
163
+ cleanup();
164
+ if (!settled) {
165
+ settled = true;
166
+ finish(value);
167
+ notify(value);
168
+ }
169
+ }, (error) => { cleanup(); fail(error); });
170
+ // Abort clears the deadline even if an uncancellable callback never settles.
171
+ control.signal.addEventListener("abort", () => clearTimeout(deadline), { once: true });
172
+ return result;
173
+ }
174
+ function start() { if (stopped)
175
+ throw new Error("runtime reporter is stopped"); if (!started) {
176
+ started = true;
177
+ if (!running)
178
+ void send().catch(() => { });
179
+ } }
180
+ async function stop() { stopped = true; started = false; if (timer) {
181
+ clearTimeout(timer);
182
+ timer = null;
183
+ } controller?.abort(new Error("runtime reporter stopped")); }
184
+ return { send, start, stop };
185
+ }
@@ -0,0 +1,24 @@
1
+ import { type IncomingMessage, type Server, type ServerResponse } from "node:http";
2
+ export interface RuntimeGatewayOptions {
3
+ authority: string;
4
+ issuers: readonly string[];
5
+ targets: Readonly<Record<string, {
6
+ url: string;
7
+ method: string;
8
+ headers?: Readonly<Record<string, string>>;
9
+ knowledgeRoot?: string;
10
+ }>>;
11
+ maxConcurrent?: number;
12
+ maxRequestBytes?: number;
13
+ maxResponseBytes?: number;
14
+ timeoutMs?: number;
15
+ clock?: () => number;
16
+ fetch?: typeof fetch;
17
+ }
18
+ export declare function createRuntimeGateway(options: RuntimeGatewayOptions): {
19
+ handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
20
+ server: Server;
21
+ stats: () => {
22
+ active: number;
23
+ };
24
+ };
@@ -0,0 +1,167 @@
1
+ // Reference resource enforcement point. Run separately from the agent; protected backends must reject bypass paths.
2
+ import { canonicalBytes, jcsHash, publicKeyFromDid } from "@zanii/core";
3
+ import { createServer } from "node:http";
4
+ import { runtimeRequestHash, verifyRuntimeAction, verifyRuntimeLease } from "./runtime.js";
5
+ import { isDid, isIso, isObj } from "./shared.js";
6
+ import { signatureOk } from "./signer.js";
7
+ import { sha256Bytes } from "./fingerprint.js";
8
+ import { verifyRuntimeKnowledgeChunk } from "./runtime-knowledge.js";
9
+ class GatewayError extends Error {
10
+ status;
11
+ constructor(status, message) { super(message); this.status = status; }
12
+ }
13
+ const httpsUrl = (raw) => {
14
+ const url = new URL(raw);
15
+ if (url.protocol !== "https:" || url.username || url.password || url.hash || url.href !== raw)
16
+ throw new Error("gateway URLs must be canonical HTTPS without credentials or fragments");
17
+ return url;
18
+ };
19
+ const cap = (value, fallback, max) => {
20
+ const out = value ?? fallback;
21
+ if (!Number.isSafeInteger(out) || out < 1 || out > max)
22
+ throw new Error("invalid gateway resource limit");
23
+ return out;
24
+ };
25
+ export function createRuntimeGateway(options) {
26
+ const authority = new URL(options.authority);
27
+ if (authority.protocol !== "https:" || ![authority.origin, `${authority.origin}/`].includes(options.authority))
28
+ throw new Error("authority must be a canonical HTTPS origin");
29
+ if (!options.issuers.length || options.issuers.length > 16 || options.issuers.some((did) => !isDid(did) || !publicKeyFromDid(did)))
30
+ throw new Error("explicit trusted issuer DIDs are required");
31
+ const issuers = [...options.issuers];
32
+ const targets = Object.entries(options.targets).map(([permission, target]) => [permission,
33
+ { url: target.url, method: target.method, headers: { ...target.headers }, knowledgeRoot: target.knowledgeRoot }]);
34
+ if (!targets.length || targets.length > 100)
35
+ throw new Error("configure 1 to 100 fixed gateway targets");
36
+ for (const [permission, target] of targets) {
37
+ if (!/^[A-Za-z0-9._:-]{1,128}$/.test(permission) || permission.trim() !== permission || !["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"].includes(target.method))
38
+ throw new Error("invalid gateway target permission or method");
39
+ httpsUrl(target.url);
40
+ if (target.knowledgeRoot !== undefined && (typeof target.knowledgeRoot !== "string" || !/^sha256:[0-9a-f]{64}$/.test(target.knowledgeRoot) || target.knowledgeRoot.trim() !== target.knowledgeRoot))
41
+ throw new Error("knowledge root must be a pinned SHA-256 digest");
42
+ for (const [key, value] of Object.entries(target.headers ?? {})) {
43
+ if (!/^[A-Za-z0-9-]{1,64}$/.test(key) || key.trim() !== key || typeof value !== "string" || value.length > 8192 || /[\r\n]/.test(value) || ["host", "content-type", "content-length", "transfer-encoding", "connection", "idempotency-key"].includes(key.toLowerCase()))
44
+ throw new Error("invalid protected backend header");
45
+ }
46
+ }
47
+ const targetMap = new Map(targets);
48
+ const maxConcurrent = cap(options.maxConcurrent, 16, 64);
49
+ const maxRequest = cap(options.maxRequestBytes, 65536, 1048576);
50
+ const maxResponse = cap(options.maxResponseBytes, 1048576, 8388608);
51
+ const timeout = cap(options.timeoutMs, 10000, 30000);
52
+ const clock = options.clock ?? Date.now;
53
+ const call = options.fetch ?? fetch;
54
+ let active = 0;
55
+ const reply = (res, status, body) => {
56
+ if (!res.headersSent && !res.destroyed)
57
+ res.writeHead(status, { "content-type": "application/json", "cache-control": "no-store", "x-content-type-options": "nosniff" }).end(JSON.stringify(body));
58
+ };
59
+ async function bounded(body, limit) {
60
+ const chunks = [];
61
+ let size = 0;
62
+ if (body)
63
+ for await (const chunk of body) {
64
+ size += chunk.length;
65
+ if (size > limit)
66
+ throw new GatewayError(413, "gateway payload limit exceeded");
67
+ chunks.push(Buffer.from(chunk));
68
+ }
69
+ return Buffer.concat(chunks);
70
+ }
71
+ async function handle(req, res) {
72
+ if (active >= maxConcurrent) {
73
+ reply(res, 503, { error: "gateway is busy" });
74
+ req.resume();
75
+ return;
76
+ }
77
+ active++;
78
+ try {
79
+ const path = /^\/invoke\/([A-Za-z0-9._:-]{1,128})$/.exec((req.url ?? "").split("?")[0]);
80
+ const permission = path?.[1];
81
+ const target = permission ? targetMap.get(permission) : undefined;
82
+ if (req.method !== "POST" || !target)
83
+ throw new GatewayError(404, "no such gateway operation");
84
+ const raw = await bounded(req, maxRequest);
85
+ let body;
86
+ try {
87
+ body = JSON.parse(raw.toString("utf8"));
88
+ }
89
+ catch {
90
+ throw new GatewayError(400, "expected a JSON operation");
91
+ }
92
+ if (!isObj(body) || Object.keys(body).length !== 3 || !isObj(body.lease) || !isObj(body.action) || !isObj(body.payload))
93
+ throw new GatewayError(400, "expected lease, action and payload");
94
+ const lease = body.lease;
95
+ const action = body.action;
96
+ if (target.knowledgeRoot !== undefined) {
97
+ const documents = body.payload.documents;
98
+ if (!Array.isArray(documents) || documents.length < 1 || documents.length > 32)
99
+ throw new GatewayError(403, "approved knowledge proofs are required");
100
+ const ids = new Set();
101
+ for (const doc of documents) {
102
+ if (!isObj(doc) || Object.keys(doc).length !== 3 || typeof doc.content !== "string" || !doc.content.isWellFormed()
103
+ || !isObj(doc.metadata) || !isObj(doc.proof) || typeof doc.metadata.id !== "string" || ids.has(doc.metadata.id)
104
+ || !verifyRuntimeKnowledgeChunk({ metadata: doc.metadata, content: Buffer.from(doc.content), proof: doc.proof }, { root: target.knowledgeRoot }).ok)
105
+ throw new GatewayError(403, "knowledge content does not match the pinned approved snapshot");
106
+ ids.add(doc.metadata.id);
107
+ }
108
+ }
109
+ const noBody = target.method === "GET" || target.method === "HEAD";
110
+ if (noBody && Object.keys(body.payload).length)
111
+ throw new GatewayError(400, "GET and HEAD operations require an empty payload");
112
+ const data = noBody ? Buffer.alloc(0) : canonicalBytes(body.payload);
113
+ const request = runtimeRequestHash({ method: target.method, url: target.url, body_sha256: sha256Bytes(data) });
114
+ const audience = new URL(target.url).origin;
115
+ const at = new Date(clock()).toISOString();
116
+ const checkedLease = verifyRuntimeLease(lease, { signers: issuers, now: at, certificate: action.certificate,
117
+ release: lease.release, policy: lease.policy, deployment: action.deployment, workload: action.signer,
118
+ audience, permission: permission, request, epoch: lease.epoch, maxSkewSeconds: 0 });
119
+ const checkedAction = verifyRuntimeAction(action, { signers: [lease.workload], now: at, certificate: lease.certificate,
120
+ lease: lease.id, deployment: lease.deployment, audience, permission: permission, request, maxSkewSeconds: 0 });
121
+ if (!checkedLease.ok || !checkedAction.ok || lease.max_uses !== 1)
122
+ throw new GatewayError(403, "operation does not match its single-use runtime authority");
123
+ // No protected effect starts before the live authority service atomically consumes the grant.
124
+ const authorization = await call(new URL("/v1/runtime/consume", authority), { method: "POST", redirect: "error",
125
+ headers: { "content-type": "application/json" }, body: JSON.stringify({ lease, action }), signal: AbortSignal.timeout(timeout) });
126
+ const authorizationBytes = await bounded(authorization.body, 16384);
127
+ if (!authorization.ok)
128
+ throw new GatewayError(403, "live runtime authority refused the operation");
129
+ let result;
130
+ try {
131
+ result = JSON.parse(authorizationBytes.toString("utf8"));
132
+ }
133
+ catch {
134
+ throw new GatewayError(502, "invalid authority response");
135
+ }
136
+ const decision = isObj(result) && isObj(result.decision) ? result.decision : null;
137
+ if (!decision)
138
+ throw new GatewayError(502, "missing signed authority decision");
139
+ const { sig, ...unsigned } = decision;
140
+ if (Object.keys(decision).length !== 10 || decision.v !== 1 || decision.type !== "certify.runtime-decision" || decision.allowed !== true
141
+ || typeof decision.signer !== "string" || !issuers.includes(decision.signer) || !signatureOk(unsigned, sig, decision.signer)
142
+ || decision.lease !== lease.id || decision.action !== jcsHash(action) || decision.request !== request
143
+ || !isIso(decision.issued_at) || !isIso(decision.expires_at) || Date.parse(decision.issued_at) > clock()
144
+ || clock() - Date.parse(decision.issued_at) > timeout || Date.parse(decision.expires_at) <= clock() || Date.parse(lease.expires_at) <= clock())
145
+ throw new GatewayError(403, "authority decision is invalid, stale or for a different operation");
146
+ const response = await call(target.url, { method: target.method, redirect: "error",
147
+ headers: { ...target.headers, "content-type": "application/json", "idempotency-key": `${lease.id}:${action.nonce}` },
148
+ body: noBody ? undefined : Buffer.from(data), signal: AbortSignal.timeout(timeout) });
149
+ const bytes = await bounded(response.body, maxResponse);
150
+ if (!res.destroyed)
151
+ res.writeHead(response.status, { "content-type": response.headers.get("content-type") ?? "application/octet-stream",
152
+ "cache-control": "no-store", "x-content-type-options": "nosniff" }).end(bytes);
153
+ }
154
+ catch (error) {
155
+ reply(res, error instanceof GatewayError ? error.status : 502, { error: error instanceof GatewayError ? error.message : "runtime gateway could not complete the operation" });
156
+ }
157
+ finally {
158
+ active--;
159
+ }
160
+ }
161
+ const server = createServer((req, res) => void handle(req, res));
162
+ server.maxConnections = maxConcurrent * 4;
163
+ server.maxRequestsPerSocket = 1000;
164
+ server.requestTimeout = 30000;
165
+ server.headersTimeout = 10000;
166
+ return { handle, server, stats: () => ({ active }) };
167
+ }
@@ -0,0 +1,28 @@
1
+ import type { RuntimeCheck } from "./runtime.ts";
2
+ export interface RuntimeKnowledgeMetadata {
3
+ id: string;
4
+ source: string;
5
+ content_sha256: string;
6
+ acl_sha256: string;
7
+ }
8
+ export interface RuntimeKnowledgeSnapshot {
9
+ root: string;
10
+ size: number;
11
+ metadata: readonly RuntimeKnowledgeMetadata[];
12
+ }
13
+ export interface RuntimeKnowledgeProof {
14
+ index: number;
15
+ size: number;
16
+ path: string[];
17
+ }
18
+ export interface RuntimeKnowledgeOptions {
19
+ root: string;
20
+ maxContentBytes?: number;
21
+ }
22
+ /** Only metadata enters a snapshot; raw documents stay outside the manifest and tree builder. */
23
+ export declare function buildRuntimeKnowledgeSnapshot(metadata: readonly RuntimeKnowledgeMetadata[]): RuntimeKnowledgeSnapshot;
24
+ /** For multiple chunks prefer runtimeKnowledgeProofs, which builds the tree once per batch. */
25
+ export declare function runtimeKnowledgeProof(snapshot: RuntimeKnowledgeSnapshot, index: number): RuntimeKnowledgeProof;
26
+ /** O(n) preparation and O(log n) per proof, without any global snapshot cache. */
27
+ export declare function runtimeKnowledgeProofs(snapshot: RuntimeKnowledgeSnapshot, indices: readonly number[]): RuntimeKnowledgeProof[];
28
+ export declare function verifyRuntimeKnowledgeChunk(chunk: unknown, options: RuntimeKnowledgeOptions): RuntimeCheck;
@@ -0,0 +1,75 @@
1
+ // Inclusion proves approved snapshot membership and bytes, not truth or permission to read them.
2
+ import { canonicalBytes, leafHash, merkleRoot, verifyInclusion } from "@zanii/core";
3
+ import { sha256Bytes } from "./fingerprint.js";
4
+ import { isInt, isObj, isSha } from "./shared.js";
5
+ const MAX_ITEMS = 50_000, MAX_CONTENT = 1_048_576;
6
+ const keys = (v, names) => isObj(v) && Object.keys(v).length === names.split(" ").length && names.split(" ").every((key) => Object.hasOwn(v, key));
7
+ const digest = (v) => isSha(v) && v.length === 71;
8
+ const metadataShape = (v) => keys(v, "id source content_sha256 acl_sha256")
9
+ && typeof v.id === "string" && v.id.length <= 128 && /^[A-Za-z0-9._:-]+$/.test(v.id) && v.id.trim() === v.id
10
+ && typeof v.source === "string" && v.source.length <= 2048 && /^[A-Za-z][A-Za-z0-9+.-]*:[\x21-\x7e]+$/.test(v.source) && v.source.trim() === v.source
11
+ && digest(v.content_sha256) && digest(v.acl_sha256);
12
+ const leaf = (metadata) => leafHash(canonicalBytes({ v: 1, type: "certify.runtime-knowledge-chunk", ...metadata }));
13
+ function tree(metadata) {
14
+ const levels = [metadata.map(leaf)];
15
+ while (levels.at(-1).length > 1) {
16
+ const level = levels.at(-1);
17
+ const next = [];
18
+ for (let i = 0; i < level.length; i += 2)
19
+ next.push(i + 1 < level.length ? merkleRoot([level[i], level[i + 1]]) : level[i]);
20
+ levels.push(next);
21
+ }
22
+ return { levels, root: `sha256:${Buffer.from(levels.at(-1)[0] ?? merkleRoot([])).toString("hex")}` };
23
+ }
24
+ /** Only metadata enters a snapshot; raw documents stay outside the manifest and tree builder. */
25
+ export function buildRuntimeKnowledgeSnapshot(metadata) {
26
+ if (!Array.isArray(metadata) || metadata.length > MAX_ITEMS || !Array.from(metadata).every(metadataShape))
27
+ throw new Error("invalid runtime knowledge metadata");
28
+ const ordered = metadata.map((m) => Object.freeze({ ...m })).sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
29
+ if (ordered.some((m, i) => i > 0 && ordered[i - 1].id === m.id))
30
+ throw new Error("duplicate runtime knowledge chunk id");
31
+ return Object.freeze({ root: tree(ordered).root, size: ordered.length, metadata: Object.freeze(ordered) });
32
+ }
33
+ /** For multiple chunks prefer runtimeKnowledgeProofs, which builds the tree once per batch. */
34
+ export function runtimeKnowledgeProof(snapshot, index) {
35
+ return runtimeKnowledgeProofs(snapshot, [index])[0];
36
+ }
37
+ /** O(n) preparation and O(log n) per proof, without any global snapshot cache. */
38
+ export function runtimeKnowledgeProofs(snapshot, indices) {
39
+ if (!keys(snapshot, "root size metadata") || !digest(snapshot.root) || !isInt(snapshot.size, 0, MAX_ITEMS)
40
+ || !Array.isArray(snapshot.metadata) || snapshot.metadata.length !== snapshot.size
41
+ || !Array.isArray(indices) || indices.length < 1 || indices.length > 100 || !Array.from(indices).every((i) => isInt(i, 0, snapshot.size - 1))
42
+ || !Array.from(snapshot.metadata).every((m, i) => metadataShape(m) && (i === 0 || snapshot.metadata[i - 1].id < m.id)))
43
+ throw new Error("invalid runtime knowledge snapshot or index");
44
+ const built = tree(snapshot.metadata);
45
+ if (built.root !== snapshot.root)
46
+ throw new Error("runtime knowledge snapshot root does not match metadata");
47
+ return indices.map((index) => {
48
+ const path = [];
49
+ let at = index;
50
+ for (const level of built.levels.slice(0, -1)) {
51
+ const sibling = at % 2 ? at - 1 : at + 1;
52
+ if (sibling < level.length)
53
+ path.push(Buffer.from(level[sibling]).toString("hex"));
54
+ at = Math.floor(at / 2);
55
+ }
56
+ return { index, size: snapshot.size, path };
57
+ });
58
+ }
59
+ export function verifyRuntimeKnowledgeChunk(chunk, options) {
60
+ const denied = (reason) => ({ ok: false, reasons: [reason] });
61
+ if (!isObj(options) || !Object.hasOwn(options, "root") || Object.keys(options).some((k) => k !== "root" && k !== "maxContentBytes") || !digest(options.root)
62
+ || !isInt(options.maxContentBytes ?? MAX_CONTENT, 1, MAX_CONTENT))
63
+ return denied("an independently pinned knowledge root and bounded content limit are required");
64
+ if (!keys(chunk, "metadata content proof") || !metadataShape(chunk.metadata) || !(chunk.content instanceof Uint8Array)
65
+ || chunk.content.byteLength > (options.maxContentBytes ?? MAX_CONTENT))
66
+ return denied("invalid or oversized runtime knowledge chunk");
67
+ const proof = chunk.proof;
68
+ if (!keys(proof, "index size path") || !isInt(proof.size, 1, MAX_ITEMS) || !isInt(proof.index, 0, proof.size - 1)
69
+ || !Array.isArray(proof.path) || proof.path.length > 32 || !Array.from(proof.path).every((p) => typeof p === "string" && p.length === 64 && /^[0-9a-f]{64}$/.test(p)))
70
+ return denied("invalid runtime knowledge inclusion proof");
71
+ if (sha256Bytes(chunk.content) !== chunk.metadata.content_sha256)
72
+ return denied("runtime knowledge content hash does not match");
73
+ return verifyInclusion(leaf(chunk.metadata), proof.index, proof.size, proof.path.map((p) => Buffer.from(p, "hex")), Buffer.from(options.root.slice(7), "hex"))
74
+ ? { ok: true, reasons: [] } : denied("runtime knowledge chunk is not in the pinned snapshot");
75
+ }
@@ -0,0 +1,30 @@
1
+ import { type NitroOptions } from "./nitro.ts";
2
+ export interface RuntimeNitroBinding {
3
+ certificate: string;
4
+ release: string;
5
+ policy: string;
6
+ deployment: string;
7
+ workload: string;
8
+ challenge: string;
9
+ }
10
+ export interface RuntimeNitroOptions extends RuntimeNitroBinding {
11
+ /** This certified release's accepted PCRs. PCR0 is required and debug measurements are refused. */
12
+ pcrs: Readonly<Record<number, string>>;
13
+ maxAgeSeconds: number;
14
+ now?: string;
15
+ /** Trusted application configuration only; the pinned commercial AWS root is the default. */
16
+ root?: NitroOptions["root"];
17
+ }
18
+ export interface RuntimeNitroCheck {
19
+ ok: boolean;
20
+ reasons: string[];
21
+ /** Populated only after every quote and binding check succeeds. */
22
+ measurement: string | null;
23
+ observed_at?: string;
24
+ }
25
+ export declare const RUNTIME_NITRO_MAX_BYTES: number;
26
+ export declare const RUNTIME_NITRO_MAX_SKEW_SECONDS = 5;
27
+ /** The quote's USER DATA is this hash as UTF-8 bytes, not the unhashed context or raw hash bytes. */
28
+ export declare function runtimeNitroBindingHash(context: RuntimeNitroBinding): string;
29
+ /** Verify the raw signed quote against one release's policy and the verifier's expected challenge and workload. */
30
+ export declare function verifyRuntimeNitroDocument(bytes: Uint8Array, opts: RuntimeNitroOptions): RuntimeNitroCheck;
@@ -0,0 +1,57 @@
1
+ // Runtime appraisal over the raw Nitro quote. Challenge consumption and channel authentication belong to the relying service.
2
+ import { jcsHash, publicKeyFromDid } from "@zanii/core";
3
+ import { nitroMeasurement, verifyNitroDocument } from "./nitro.js";
4
+ import { isDid, isIso, isObj, isSha } from "./shared.js";
5
+ export const RUNTIME_NITRO_MAX_BYTES = 16 * 1024;
6
+ export const RUNTIME_NITRO_MAX_SKEW_SECONDS = 5;
7
+ const ED25519_SPKI = Buffer.from("302a300506032b6570032100", "hex");
8
+ /** The quote's USER DATA is this hash as UTF-8 bytes, not the unhashed context or raw hash bytes. */
9
+ export function runtimeNitroBindingHash(context) {
10
+ if (!isObj(context) || !isSha(context.certificate) || !isSha(context.release) || !isSha(context.policy)
11
+ || typeof context.deployment !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(context.deployment)
12
+ || typeof context.challenge !== "string" || !/^[A-Za-z0-9_-]{32,128}$/.test(context.challenge)
13
+ || !isDid(context.workload) || !publicKeyFromDid(context.workload))
14
+ throw new Error("invalid runtime Nitro binding context");
15
+ return jcsHash({ v: 1, type: "certify.runtime-binding", certificate: context.certificate, release: context.release,
16
+ policy: context.policy, deployment: context.deployment, workload: context.workload, challenge: context.challenge });
17
+ }
18
+ /** Verify the raw signed quote against one release's policy and the verifier's expected challenge and workload. */
19
+ export function verifyRuntimeNitroDocument(bytes, opts) {
20
+ const fail = (...reasons) => ({ ok: false, reasons, measurement: null });
21
+ if (!(bytes instanceof Uint8Array) || bytes.byteLength < 1 || bytes.byteLength > RUNTIME_NITRO_MAX_BYTES)
22
+ return fail("runtime Nitro quote must contain 1 to 16384 bytes");
23
+ try {
24
+ const binding = runtimeNitroBindingHash(opts);
25
+ if (!Number.isFinite(opts.maxAgeSeconds) || opts.maxAgeSeconds < 1 || opts.maxAgeSeconds > 300)
26
+ return fail("runtime Nitro maxAgeSeconds must be finite and between 1 and 300");
27
+ if (!isObj(opts.pcrs))
28
+ return fail("runtime Nitro PCR policy must contain PCR0 and at most 8 PCRs");
29
+ const pcrs = Object.entries(opts.pcrs);
30
+ if (pcrs.length < 1 || pcrs.length > 8 || !pcrs.some(([index]) => index === "0")
31
+ || pcrs.some(([index, value]) => !/^(?:[0-9]|[12][0-9]|3[01])$/.test(index) || typeof value !== "string" || !/^[0-9a-fA-F]{96}$/.test(value)))
32
+ return fail("runtime Nitro PCR policy must map at most 8 indexes from 0 to 31 to SHA-384 hex values, including PCR0");
33
+ if (/^0{96}$/.test(opts.pcrs[0]))
34
+ return fail("runtime Nitro PCR0 must not be a debug measurement");
35
+ const now = opts.now !== undefined ? opts.now : new Date().toISOString();
36
+ if (!isIso(now))
37
+ return fail("runtime Nitro now must be an ISO UTC timestamp");
38
+ const checked = verifyNitroDocument(bytes, { root: opts.root, now, nonce: opts.challenge, pcrs: Object.fromEntries(pcrs), maxAgeSeconds: opts.maxAgeSeconds });
39
+ if (!checked.ok || !checked.document)
40
+ return fail(...checked.reasons);
41
+ const doc = checked.document;
42
+ const reasons = [];
43
+ if (doc.timestamp > Date.parse(now) + RUNTIME_NITRO_MAX_SKEW_SECONDS * 1000)
44
+ reasons.push("runtime Nitro document timestamp exceeds the 5-second clock skew");
45
+ const expectedKey = Buffer.concat([ED25519_SPKI, publicKeyFromDid(opts.workload)]);
46
+ if (doc.publicKey === null || !expectedKey.equals(doc.publicKey))
47
+ reasons.push("runtime Nitro public_key does not match the workload Ed25519 SPKI key");
48
+ if (doc.userData === null || !Buffer.from(binding, "utf8").equals(doc.userData))
49
+ reasons.push("runtime Nitro user_data does not match the expected binding hash");
50
+ if (reasons.length)
51
+ return fail(...reasons);
52
+ return { ok: true, reasons: [], measurement: nitroMeasurement(doc), observed_at: new Date(doc.timestamp).toISOString() };
53
+ }
54
+ catch {
55
+ return fail("runtime Nitro quote or trusted verification context is invalid");
56
+ }
57
+ }