@zanii/certify 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -97,6 +97,38 @@ proof establishes commitment to supplied data; it does not establish content tru
97
97
  ACL permission or quality. The Node package also exposes Nitro verification, a runtime
98
98
  reporter and a reference gateway; those host integrations are not Python SDK equivalents.
99
99
 
100
+ ## Beacon reporting
101
+
102
+ `startBeacon` sends immediately, then waits the configured interval after each completed
103
+ operation. Intervals must be between 1,000 and 2,147,483,647 milliseconds. Each client allows
104
+ one underlying operation; concurrent `send()` calls return a busy failure without queuing.
105
+ The full operation has a ten second deadline, including fingerprint collection, signing and
106
+ reading the response. Replies are capped at 32KiB; redirects and credentialed URLs are refused.
107
+
108
+ `stop()` disables automatic reporting and aborts the active request. Manual `send()` remains
109
+ available. An injected callback that ignores cancellation keeps its slot until it returns;
110
+ late callbacks cannot submit a report. Result observer exceptions do not interrupt reporting.
111
+
112
+ `startBeacon`, `createRuntimeReporter` and `createRuntimeGateway` accept optional
113
+ `transportKey: process.env.CERTIFY_TRANSPORT_KEY`. Use an agent-bound `ctk_` admission
114
+ credential issued by the owner/admin transport-key controls, never a management API key.
115
+ It travels only in the authority's Authorization header, outside signed documents and
116
+ hashes. The gateway never forwards it to a protected backend. Invalid prefixes and
117
+ non-HTTPS beacon endpoints are refused before I/O, except literal loopback/localhost
118
+ for local development. Runtime reporter/gateway authority URLs keep their existing
119
+ HTTPS requirement. Anonymous signed clients still work; transport admission does not
120
+ replace signatures, approved workload binding or replay checks. Redirects remain refused.
121
+ Callback error messages and beacon response echoes redact the supplied credential.
122
+
123
+ The reporter handles challenge/evidence and the gateway handles consume. Lease request
124
+ builders produce signed documents; this package does not provide a general HTTP lease
125
+ acquisition or management client. Injected transports must keep credentials private and
126
+ honor the same redirect/deadline contract.
127
+
128
+ The package root also exports `JsonFetchResult`, `logPositionOk`,
129
+ `RUNTIME_NITRO_MAX_BYTES` and `RUNTIME_NITRO_MAX_SKEW_SECONDS` for callers validating
130
+ issuer responses, status log positions and raw runtime attestations.
131
+
100
132
  ## Check changes
101
133
 
102
134
  Run from the repository root:
@@ -12,10 +12,12 @@ export interface BeaconOptions {
12
12
  signer: Signer;
13
13
  /** The fingerprint the agent runs now, or a function returning it (read at every report). */
14
14
  fingerprint: FingerprintValue | (() => FingerprintValue | Promise<FingerprintValue>);
15
- /** Default 300 000 (five minutes); at least 1 000. */
15
+ /** Default 300 000 (five minutes); 1 000 to 2 147 483 647. */
16
16
  intervalMs?: number;
17
17
  fetch?: typeof fetch;
18
- /** Called with every result, including failures; the timer never throws. */
18
+ /** Optional agent-bound admission credential; never use a management API key. */
19
+ transportKey?: string;
20
+ /** Called when a started report finishes, including failures; busy rejections are returned only. */
19
21
  onResult?: (result: BeaconResult) => void;
20
22
  }
21
23
  export interface BeaconResult {
@@ -29,7 +31,7 @@ export interface BeaconResult {
29
31
  error?: string;
30
32
  }
31
33
  export interface BeaconHandle {
32
- /** Send one report now. */
34
+ /** Send one report now; a concurrent underlying report returns a busy failure. */
33
35
  send: () => Promise<BeaconResult>;
34
36
  stop: () => void;
35
37
  }
@@ -4,49 +4,148 @@ import { randomBytes } from "node:crypto";
4
4
  import { credentialHash } from "@zanii/credentials";
5
5
  import { buildBeacon } from "./beacon.js";
6
6
  import { isSha } from "./shared.js";
7
+ import { machineTransportHeaders, redactTransportText } from "./transport-key.js";
7
8
  export const BEACON_INTERVAL_MS = 300_000;
8
9
  const MIN_INTERVAL_MS = 1_000;
10
+ const MAX_INTERVAL_MS = 2_147_483_647;
9
11
  const TIMEOUT_MS = 10_000;
12
+ const MAX_RESPONSE_BYTES = 32 * 1024;
10
13
  /** Start reporting: one report now, then one every `intervalMs`. */
11
14
  export function startBeacon(opts) {
12
15
  const endpoint = new URL("/v1/beacon", opts.url);
13
- if (endpoint.protocol !== "https:" && endpoint.protocol !== "http:")
14
- throw new Error("url must be http(s)");
16
+ if ((endpoint.protocol !== "https:" && endpoint.protocol !== "http:") || endpoint.username || endpoint.password)
17
+ throw new Error("url must be http(s) without credentials");
18
+ const transportKey = opts.transportKey, transportHeaders = machineTransportHeaders(endpoint, transportKey);
15
19
  const certificate = typeof opts.certificate === "string" ? opts.certificate : credentialHash(opts.certificate);
16
20
  if (!isSha(certificate))
17
21
  throw new Error("certificate must be a certificate or its hash");
18
22
  const intervalMs = opts.intervalMs ?? BEACON_INTERVAL_MS;
19
- if (!Number.isInteger(intervalMs) || intervalMs < MIN_INTERVAL_MS)
20
- throw new Error(`intervalMs must be an integer of at least ${MIN_INTERVAL_MS}`);
23
+ if (!Number.isSafeInteger(intervalMs) || intervalMs < MIN_INTERVAL_MS || intervalMs > MAX_INTERVAL_MS)
24
+ throw new Error(`intervalMs must be an integer from ${MIN_INTERVAL_MS} to ${MAX_INTERVAL_MS}`);
21
25
  const doFetch = opts.fetch ?? fetch;
22
- const send = async () => {
23
- let result;
26
+ let running = false, automatic = true;
27
+ let timer = null, controller = null;
28
+ const failure = (error) => ({ ok: false, status: 0, body: null, error: redactTransportText(error instanceof Error ? error.message : String(error), transportKey) });
29
+ const notify = (result) => {
24
30
  try {
25
- const fp = typeof opts.fingerprint === "function" ? await opts.fingerprint() : opts.fingerprint;
26
- const report = await buildBeacon({
27
- certificate,
28
- fingerprint: typeof fp === "string" ? fp : fp.id,
29
- ...(typeof fp === "string" ? {} : { fingerprintManifest: fp }),
30
- observedAt: new Date().toISOString(),
31
- nonce: randomBytes(18).toString("base64url"),
32
- }, opts.signer);
33
- const res = await doFetch(endpoint.href, {
34
- method: "POST",
35
- headers: { "content-type": "application/json; charset=utf-8" },
36
- body: JSON.stringify(report),
37
- signal: AbortSignal.timeout(TIMEOUT_MS),
38
- });
39
- const body = await res.json().catch(() => null);
40
- result = { ok: res.status === 202, status: res.status, body };
41
- }
42
- catch (e) {
43
- result = { ok: false, status: 0, body: null, error: e instanceof Error ? e.message : String(e) };
44
- }
45
- opts.onResult?.(result);
46
- return result;
31
+ void Promise.resolve(opts.onResult?.(result)).catch(() => { });
32
+ }
33
+ catch { /* Observers cannot interrupt reporting. */ }
47
34
  };
35
+ const check = (signal) => { if (signal.aborted)
36
+ throw signal.reason; };
37
+ async function report(signal) {
38
+ check(signal);
39
+ const fp = typeof opts.fingerprint === "function" ? await opts.fingerprint() : opts.fingerprint;
40
+ check(signal);
41
+ const report = await buildBeacon({
42
+ certificate,
43
+ fingerprint: typeof fp === "string" ? fp : fp.id,
44
+ ...(typeof fp === "string" ? {} : { fingerprintManifest: fp }),
45
+ observedAt: new Date().toISOString(),
46
+ nonce: randomBytes(18).toString("base64url"),
47
+ }, opts.signer);
48
+ check(signal);
49
+ const res = await doFetch(endpoint.href, {
50
+ method: "POST",
51
+ redirect: "error", credentials: "omit",
52
+ headers: { "content-type": "application/json; charset=utf-8", ...transportHeaders },
53
+ body: JSON.stringify(report),
54
+ signal,
55
+ });
56
+ if (signal.aborted) {
57
+ void res.body?.cancel().catch(() => { });
58
+ check(signal);
59
+ }
60
+ if (res.redirected || (res.url && new URL(res.url).origin !== endpoint.origin)) {
61
+ void res.body?.cancel().catch(() => { });
62
+ throw new Error("beacon endpoint redirected the request");
63
+ }
64
+ const length = res.headers.get("content-length");
65
+ if (length && (!/^\d+$/.test(length) || Number(length) > MAX_RESPONSE_BYTES)) {
66
+ void res.body?.cancel().catch(() => { });
67
+ throw new Error("beacon response exceeds 32KiB");
68
+ }
69
+ const reader = res.body?.getReader(), bytes = new Uint8Array(MAX_RESPONSE_BYTES);
70
+ const cancelled = () => { void reader?.cancel().catch(() => { }); };
71
+ signal.addEventListener("abort", cancelled, { once: true });
72
+ let size = 0, reads = 0;
73
+ try {
74
+ if (reader)
75
+ for (;;) {
76
+ const part = await reader.read();
77
+ check(signal);
78
+ if (part.done)
79
+ break;
80
+ if (size + part.value.byteLength > MAX_RESPONSE_BYTES)
81
+ throw new Error("beacon response exceeds 32KiB");
82
+ bytes.set(part.value, size);
83
+ size += part.value.byteLength;
84
+ if (++reads % 64 === 0)
85
+ await new Promise((resolve) => setImmediate(resolve));
86
+ check(signal);
87
+ }
88
+ }
89
+ catch (error) {
90
+ cancelled();
91
+ throw error;
92
+ }
93
+ finally {
94
+ signal.removeEventListener("abort", cancelled);
95
+ reader?.releaseLock();
96
+ }
97
+ let body = null;
98
+ try {
99
+ body = JSON.parse(redactTransportText(new TextDecoder("utf-8", { fatal: true }).decode(bytes.subarray(0, size)), transportKey));
100
+ }
101
+ catch { /* Non-JSON replies retain their HTTP status. */ }
102
+ return { ok: res.status === 202, status: res.status, body };
103
+ }
104
+ function schedule() {
105
+ if (automatic && !running) {
106
+ timer = setTimeout(() => { timer = null; void send(); }, intervalMs);
107
+ timer.unref();
108
+ }
109
+ }
110
+ function send() {
111
+ if (running)
112
+ return Promise.resolve(failure("beacon report is busy"));
113
+ if (timer) {
114
+ clearTimeout(timer);
115
+ timer = null;
116
+ }
117
+ running = true;
118
+ const control = new AbortController();
119
+ controller = control;
120
+ let finish, settled = false;
121
+ const result = new Promise((resolve) => { finish = resolve; });
122
+ const settle = (value) => { if (!settled) {
123
+ settled = true;
124
+ finish(value);
125
+ notify(value);
126
+ } };
127
+ const abort = () => settle(failure(control.signal.reason));
128
+ control.signal.addEventListener("abort", abort, { once: true });
129
+ const deadline = setTimeout(() => control.abort(new Error("beacon report exceeded its 10000ms deadline")), TIMEOUT_MS);
130
+ control.signal.addEventListener("abort", () => clearTimeout(deadline), { once: true });
131
+ const cleanup = () => {
132
+ clearTimeout(deadline);
133
+ control.signal.removeEventListener("abort", abort);
134
+ controller = null;
135
+ running = false;
136
+ schedule();
137
+ };
138
+ // Keep the slot until callbacks actually settle, even when timeout already returned a failure.
139
+ void report(control.signal).then((value) => { cleanup(); settle(value); }, (error) => { cleanup(); settle(failure(error)); });
140
+ return result;
141
+ }
48
142
  void send();
49
- const timer = setInterval(() => void send(), intervalMs);
50
- timer.unref();
51
- return { send, stop: () => clearInterval(timer) };
143
+ return { send, stop: () => {
144
+ automatic = false;
145
+ if (timer) {
146
+ clearTimeout(timer);
147
+ timer = null;
148
+ }
149
+ controller?.abort(new Error("beacon reporting stopped"));
150
+ } };
52
151
  }
package/dist/bundle.d.ts CHANGED
@@ -43,6 +43,13 @@ export interface BundleInput {
43
43
  partial?: {
44
44
  covered_by: string;
45
45
  } | null;
46
+ /** Section 2a (scheme 0.4): the categories assessed as simulated multi-step workflows, and the agent interface tested. */
47
+ assessment?: Assessment | null;
48
+ }
49
+ /** What a 0.4 run assessed: `voice_text` is a voice agent tested through its text interface only, never its audio. */
50
+ export interface Assessment {
51
+ trajectory_categories: string[];
52
+ interface: "text" | "voice_text";
46
53
  }
47
54
  export interface ResultBundle {
48
55
  execution?: ApplianceExecution;
@@ -69,6 +76,7 @@ export interface ResultBundle {
69
76
  partial?: {
70
77
  covered_by: string;
71
78
  };
79
+ assessment?: Assessment;
72
80
  }
73
81
  export interface ApplianceExportBundle extends Omit<ResultBundle, "v" | "staff" | "execution" | "partial"> {
74
82
  v: 2;
package/dist/bundle.js CHANGED
@@ -70,6 +70,7 @@ function normalizedBundle(input, source) {
70
70
  const partial = input.partial ?? null;
71
71
  if (partial !== null && (Object.keys(partial).length !== 1 || !isSha(partial.covered_by)))
72
72
  throw new Error("partial must be {covered_by: <bundle hash>}");
73
+ const assessment = input.assessment == null ? null : assessmentOf(input.assessment, categories, scheme);
73
74
  const execution = input.execution == null ? undefined : applianceExecution(input.execution), machine = source?.appliance ?? execution?.appliance;
74
75
  if (machine && [...testers, reviewer, decider].includes(machine))
75
76
  throw new Error("an appliance cannot be recorded as human staff");
@@ -89,6 +90,7 @@ function normalizedBundle(input, source) {
89
90
  ended_at: input.ended_at,
90
91
  ...(partial ? { partial: { covered_by: partial.covered_by } } : {}),
91
92
  ...(execution ? { execution } : {}),
93
+ ...(assessment ? { assessment } : {}),
92
94
  };
93
95
  if (source) {
94
96
  const { partial: ignored, ...base } = bundle;
@@ -96,6 +98,17 @@ function normalizedBundle(input, source) {
96
98
  }
97
99
  return bundle;
98
100
  }
101
+ function assessmentOf(a, categories, scheme) {
102
+ if (scheme !== "zanii-certify/0.4")
103
+ throw new Error("assessment needs scheme zanii-certify/0.4");
104
+ if (typeof a !== "object" || Object.keys(a).sort().join() !== "interface,trajectory_categories" || !Array.isArray(a.trajectory_categories)
105
+ || (a.interface !== "text" && a.interface !== "voice_text"))
106
+ throw new Error("assessment must be {trajectory_categories, interface}");
107
+ const workflow = sortedUnique(a.trajectory_categories);
108
+ if (workflow.length !== a.trajectory_categories.length || !workflow.every((c) => categories.includes(c)))
109
+ throw new Error("assessment.trajectory_categories must be distinct planned categories");
110
+ return { trajectory_categories: workflow, interface: a.interface };
111
+ }
99
112
  /** Existing v1 output is unchanged unless explicit verified appliance provenance is supplied. */
100
113
  export function buildBundle(input) { return normalizedBundle(input); }
101
114
  /** An appliance supplies execution evidence; it never occupies a human reviewer or decider role. */
@@ -10,7 +10,7 @@ import { type StatusDocument, type StatusEntry, type StatusWitness } from "./sta
10
10
  import { type IssuerPolicy, type ReleaseVerificationPolicy } from "./verification-policy.ts";
11
11
  import { type AuditRenewalPolicy } from "./audit-renewal.ts";
12
12
  /** scheme.md section 8: every scheme version a certificate or bundle may name. */
13
- export declare const SCHEMES: readonly ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3"];
13
+ export declare const SCHEMES: readonly ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"];
14
14
  export type Scheme = (typeof SCHEMES)[number];
15
15
  /** The current scheme. */
16
16
  export declare const SCHEME: Scheme;
@@ -16,15 +16,16 @@ import { verifyAuditRenewal } from "./audit-renewal.js";
16
16
  import { withinVerificationBudget } from "./verification-bounds.js";
17
17
  import { signingIdentityReasons } from "./signing-identity.js";
18
18
  /** scheme.md section 8: every scheme version a certificate or bundle may name. */
19
- export const SCHEMES = ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3"];
19
+ export const SCHEMES = ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"];
20
20
  /** The current scheme. */
21
- export const SCHEME = "zanii-certify/0.3";
21
+ export const SCHEME = "zanii-certify/0.4";
22
22
  export const LEVELS = [1, 2, 3];
23
23
  /** scheme.md section 2: the levels each scheme version issues. */
24
24
  export const SCHEME_LEVELS = {
25
25
  "zanii-certify/0.1": [1, 2],
26
26
  "zanii-certify/0.2": [1, 2, 3],
27
27
  "zanii-certify/0.3": [1, 2, 3],
28
+ "zanii-certify/0.4": [1, 2, 3],
28
29
  };
29
30
  /**
30
31
  * scheme.md section 8: the scheme a certificate or bundle is issued under. Given, it must issue
@@ -39,8 +40,8 @@ export function pickScheme(scheme, level, needs02 = false, needs03 = false) {
39
40
  throw new Error(`level ${level} is not issued under ${s}`);
40
41
  if (needs02 && s === "zanii-certify/0.1")
41
42
  throw new Error("a post-quantum binding needs scheme zanii-certify/0.2");
42
- if (needs03 && s !== "zanii-certify/0.3")
43
- throw new Error("a licence needs scheme zanii-certify/0.3");
43
+ if (needs03 && s !== "zanii-certify/0.3" && s !== "zanii-certify/0.4")
44
+ throw new Error("a licence needs scheme zanii-certify/0.3 or later");
44
45
  return s;
45
46
  }
46
47
  export const CONFLICTS = [
@@ -168,10 +169,10 @@ export function claimReasons(cert) {
168
169
  out.push("claim fingerprint or bundle is not a hash");
169
170
  if (c.level === 3 ? !isSha(c.audit) : "audit" in c)
170
171
  out.push("claim audit is an audit attestation hash, required at level 3 and only there");
171
- if ("pq_binding" in c && ((scheme !== "zanii-certify/0.2" && scheme !== "zanii-certify/0.3") || !isSha(c.pq_binding)))
172
+ if ("pq_binding" in c && (!scheme || scheme === "zanii-certify/0.1" || !isSha(c.pq_binding)))
172
173
  out.push("claim pq_binding needs scheme zanii-certify/0.2 and a binding hash");
173
- if ("licence" in c && (scheme !== "zanii-certify/0.3" || !isSha(c.licence)))
174
- out.push("claim licence needs scheme zanii-certify/0.3 and a licence hash");
174
+ if ("licence" in c && ((scheme !== "zanii-certify/0.3" && scheme !== "zanii-certify/0.4") || !isSha(c.licence)))
175
+ out.push("claim licence needs scheme zanii-certify/0.3 or later and a licence hash");
175
176
  return out;
176
177
  }
177
178
  /** `verifyCredential`'s revocation check, for a list signed by another DID of the issuer document. */
package/dist/cli.js CHANGED
File without changes
@@ -52,6 +52,8 @@ export interface RiskProfileInput {
52
52
  at: string;
53
53
  incident: boolean;
54
54
  }[];
55
+ /** Complete retained-history counts, when a caller aggregates rather than loading history. */
56
+ historyFacts?: Pick<CertifyRiskFacts, "suspensions" | "incident_suspensions" | "retests_passed" | "retests_failed" | "incident_retests">;
55
57
  overdueRetests: number;
56
58
  underCommitteeReview: boolean;
57
59
  asOf: string;
@@ -25,6 +25,17 @@ export function certifyRiskProfile(input) {
25
25
  if (!Number.isInteger(input.overdueRetests) || input.overdueRetests < 0)
26
26
  throw new Error("overdueRetests must be a non-negative integer");
27
27
  const count = (xs, f) => xs.filter(f).length;
28
+ const totals = input.historyFacts ?? {
29
+ suspensions: count(input.history, h => h.action === "suspend"),
30
+ incident_suspensions: count(input.history, h => h.action === "suspend" && h.reason === "INCIDENT"),
31
+ retests_passed: count(input.retests, r => r.outcome === "grant"),
32
+ retests_failed: count(input.retests, r => r.outcome === "refuse"),
33
+ incident_retests: count(input.retests, r => r.incident),
34
+ };
35
+ if (Object.keys(totals).sort().join(",") !== "incident_retests,incident_suspensions,retests_failed,retests_passed,suspensions" ||
36
+ !Object.values(totals).every(n => Number.isSafeInteger(n) && n >= 0) ||
37
+ totals.incident_suspensions > totals.suspensions || totals.incident_retests > totals.retests_passed + totals.retests_failed)
38
+ throw new Error("historyFacts must contain consistent non-negative safe integer counts");
28
39
  const facts = {
29
40
  certificate: credentialHash(c),
30
41
  profile: claim.profile,
@@ -40,11 +51,7 @@ export function certifyRiskProfile(input) {
40
51
  categories: b.results.length,
41
52
  categories_passed: count(b.results, (r) => r.verdict === "pass"),
42
53
  min_margin_ppm: Math.min(...b.results.map((r) => r.low_ppm - r.threshold_ppm)),
43
- suspensions: count(input.history, (h) => h.action === "suspend"),
44
- incident_suspensions: count(input.history, (h) => h.action === "suspend" && h.reason === "INCIDENT"),
45
- retests_passed: count(input.retests, (r) => r.outcome === "grant"),
46
- retests_failed: count(input.retests, (r) => r.outcome === "refuse"),
47
- incident_retests: count(input.retests, (r) => r.incident),
54
+ ...totals,
48
55
  overdue_retests: input.overdueRetests,
49
56
  conflicts: [...claim.conflicts],
50
57
  under_committee_review: input.underCommitteeReview === true,
package/dist/index.d.ts CHANGED
@@ -13,7 +13,7 @@ export { FEED_SNAPSHOT_CHUNK_ENTRIES, MAX_FEED_SNAPSHOT_BYTES, MAX_FEED_SNAPSHOT
13
13
  export { buildFeedPage, type FederatedFeed, type FeedCheck, type FeedConflict, type FeedEntry, type FeedPage, type FeedPageInput, type FeedCursor, feedCheckpoint, MAX_FEED_ENTRIES, MAX_FEED_PAGE_BYTES, type MergedRegistry, mergeFeeds, verifyFeed, verifyFeedPage, } from "./federation.ts";
14
14
  export { buildLicence, buildLicenceStatus, buildRoyaltyStatement, certificateListHash, countersignRoyaltyStatement, LICENCE_ACTIONS, LICENCE_REASONS, type Licence, type LicenceAction, type LicenceCheck, type LicenceInput, type LicenceOptions, type LicenceReason, type LicenceScope, type LicenceState, type LicenceStatus, licenceCoverage, licenceReasons, licensedKey, licenceStateAt, MAX_ROYALTY_MINOR, MAX_STATEMENT_CERTIFICATES, type RoyaltyStatement, type RoyaltyStatementBody, type RoyaltyStatementInput, verifyLicence, verifyLicenceLog, verifyRoyaltyStatement, } from "./licence.ts";
15
15
  export { type AttestedBeaconCheck, type BeaconAttestation, type BeaconCheck, type BeaconReport, beaconChallenge, buildBeacon, MAX_SKEW_SECONDS, NONCE, verifyAttestedBeacon, verifyBeacon, } from "./beacon.ts";
16
- export { type BundleInput, type ApplianceExecution, type ApplianceExportIdentity, type ApplianceExportBundle, type ApplianceExportBundleInput, bundleHash, buildBundle, buildApplianceExportBundle, type CategoryCount, type CategoryResult, type Evidence, type ResultBundle, } from "./bundle.ts";
16
+ export { type BundleInput, type ApplianceExecution, type ApplianceExportIdentity, type ApplianceExportBundle, type ApplianceExportBundleInput, bundleHash, buildBundle, buildApplianceExportBundle, type CategoryCount, type CategoryResult, type Evidence, type ResultBundle, type Assessment, } from "./bundle.ts";
17
17
  export { applianceExecution, applianceExportIdentity } from "./appliance-execution.ts";
18
18
  export { type CertificateCheck, type CertificateInput, type CertificateVerifyOptions, type CertifyClaim, type PqPolicy, CONFLICTS, type Conflict, certificateScope, claimReasons, issueCertificate, LEVELS, type Level, pickScheme, SCHEME, SCHEME_LEVELS, SCHEMES, type Scheme, SURVEILLANCE, VALIDITY_DAYS, verifyCertificate, } from "./certificate.ts";
19
19
  export { buildFingerprint, checkFingerprint, type Fingerprint, type FingerprintInput, promptSha256, sha256Bytes, toolsSha256, } from "./fingerprint.ts";
@@ -28,18 +28,18 @@ export { type CategoryScore, type Overall, overallVerdict, type ProfileCategoryR
28
28
  export { activeRotation, buildRotation, type Rotation, type RotationCheck, type RotationInput, type SuiteRef, verifyRotationLog, verifyRotationRecord, } from "./rotation.ts";
29
29
  export { type Check, type CheckContext, type CheckResult, casesOf, DEFAULT_FINDINGS, type Delegation, type DelegationLink, effectiveDelegation, judgementResult, normaliseText, type Rubric, runCheck, type Scenario, type ScenarioLibrary, scenarioCases, scenariosSchema, type Suite, type SubAgent, type SuiteCase, suiteSchema, type ToolCall, suiteSnapshot, validateScenarios, validateSuite, verifyCaseDelegation, } from "./suite.ts";
30
30
  export { keySigner, type Signer, signerDid, signWith } from "./signer.ts";
31
- export { applyStatus, buildStatus, type CertificateStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, type StatusVersion, type Reason, revocationListFromStatus, type StatusAction, type StatusCheck, type StatusDocument, type StatusEntries, type StatusEntry, type StatusVerifyOptions, type StatusWitness, verifyStatus, } from "./status.ts";
31
+ export { applyStatus, buildStatus, type CertificateStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, type StatusVersion, type Reason, revocationListFromStatus, type StatusAction, type StatusCheck, type StatusDocument, type StatusEntries, type StatusEntry, type StatusVerifyOptions, type StatusWitness, verifyStatus, logPositionOk, } from "./status.ts";
32
32
  export { type CertificateVcCheck, certificateToVc, VC_STATUS_TYPE, VC_TYPE, verifyCertificateVc, } from "./vc.ts";
33
33
  export { statusCheckpoint, statusConsistencyProof, statusInclusionProof, verifyStatusLog, } from "./witness.ts";
34
34
  export { type AccreditationExport, buildAccreditationExport, buildFilingPack, buildGovernanceOutcome, buildSigningKeyApproval, CASE_KINDS, type CaseKind, type CertifyRiskFacts, type CertifyRiskProfile, certifyRiskProfile, type FilingPack, type FilingPackInput, type GovernanceCheck, type GovernanceOutcome, type GovernanceOutcomeInput, governanceOutcomeBody, OUTCOMES, RISK_NOTE, type RiskProfileInput, type SignedRiskProfile, type SigningKeyApproval, type SigningKeyApprovalInput, signingKeyApprovalBody, signRiskProfile, verifyRiskProfile, verifyAccreditationExport, verifyFilingPack, verifyGovernanceOutcome, verifySigningKeyApproval, } from "./governance.ts";
35
35
  export { AWS_NITRO_ROOT_PEM, AWS_NITRO_ROOT_SHA256, AWS_NITRO_GOV_ROOT_PEM, AWS_NITRO_GOV_ROOT_SHA256, checkNitroAttestation, NITRO_MAX_AGE_SECONDS, NITRO_MAX_SKEW_SECONDS, type NitroCheck, type NitroDocument, type NitroOptions, type NitroPartition, type NitroQuoteOptions, nitroMeasurement, nitroQuoteVerifier, nitroRootForPartition, parseNitroDocument, sigStructure, verifyNitroDocument } from "./nitro.ts";
36
36
  export { canonicalBytes, jcsHash } from "@zanii/core";
37
37
  export { buildRuntimeRelease, buildRuntimePolicy, buildRuntimeApproval, buildRuntimeProof, buildRuntimeChallengeRequest, buildRuntimeLease, buildRuntimeAction, buildRuntimeLeaseRequest, buildRuntimeMandate, verifyRuntimeRelease, verifyRuntimePolicy, verifyRuntimeApproval, verifyRuntimeProof, verifyRuntimeChallengeRequest, verifyRuntimeLease, verifyRuntimeAction, verifyRuntimeLeaseRequest, verifyRuntimeMandate, runtimeReleaseHash, runtimePolicyHash, runtimeRequestHash, type RuntimeRelease, type RuntimePolicy, type RuntimeApproval, type RuntimeProof, type RuntimeChallengeRequest, type RuntimeLease, type RuntimeAction, type RuntimeLeaseRequest, type RuntimeMandate, type RuntimeCheck, type RuntimeTrustOptions, type RuntimeReleaseOptions, type RuntimeApprovalOptions, type RuntimeProofOptions, type RuntimeChallengeOptions, type RuntimeLeaseOptions, type RuntimeActionOptions, type RuntimeLeaseRequestOptions, type RuntimeMandateOptions, } from "./runtime.ts";
38
- export { runtimeNitroBindingHash, verifyRuntimeNitroDocument, type RuntimeNitroBinding, type RuntimeNitroOptions, type RuntimeNitroCheck } from "./runtime-nitro.ts";
38
+ export { runtimeNitroBindingHash, verifyRuntimeNitroDocument, RUNTIME_NITRO_MAX_BYTES, RUNTIME_NITRO_MAX_SKEW_SECONDS, type RuntimeNitroBinding, type RuntimeNitroOptions, type RuntimeNitroCheck } from "./runtime-nitro.ts";
39
39
  export { createRuntimeGateway, type RuntimeGatewayOptions } from "./runtime-gateway.ts";
40
40
  export { createRuntimeReporter, type RuntimeReporterOptions, type RuntimeAttestationBinding, type RuntimeReportResult } from "./runtime-client.ts";
41
41
  export { buildRuntimeKnowledgeSnapshot, runtimeKnowledgeProof, runtimeKnowledgeProofs, verifyRuntimeKnowledgeChunk, type RuntimeKnowledgeMetadata, type RuntimeKnowledgeSnapshot, type RuntimeKnowledgeProof, type RuntimeKnowledgeOptions, } from "./runtime-knowledge.ts";
42
- export { resolveIssuerDetailed, fetchJsonBounded, type IssuerResolution, type IssuerResolutionCode, type ResolutionOptions, type ResolutionFailure } from "./issuer-resolution.ts";
42
+ export { resolveIssuerDetailed, fetchJsonBounded, type JsonFetchResult, type IssuerResolution, type IssuerResolutionCode, type ResolutionOptions, type ResolutionFailure } from "./issuer-resolution.ts";
43
43
  export { isIssuerPolicy, issuerPolicyCheck, releasePolicyReasons, type IssuerPolicy, type ReleaseVerificationPolicy } from "./verification-policy.ts";
44
44
  export { buildSandboxDelegationPolicy, verifySandboxDelegationPolicy, type SandboxDelegationPolicy, type SandboxDelegationContext } from "./sandbox-delegation.ts";
45
45
  export { teamReceiptTask, type TeamReceiptPolicy } from "./suite.ts";
@@ -56,3 +56,6 @@ export { signingIdentityReasons } from "./signing-identity.ts";
56
56
  export { normalizationPolicyReasons, runtimeNormalizationVersion, type NormalizationPolicy } from "./normalization.ts";
57
57
  export { schemaDialectErrors } from "./schema-dialect.ts";
58
58
  export { type Credential, credentialHash, type RevocationList } from "@zanii/credentials";
59
+ export { TRANSCRIPT_TURNS_BYTES, transcriptTurnsHash, validateTranscriptProvenance, type TranscriptProvenance } from "./transcript-provenance.ts";
60
+ export { validateSuiteDocument, type SuiteDocument } from "./suite.ts";
61
+ export { TRAJECTORY_LIMITS, trajectorySchema, isTrajectoryCase, validateTrajectorySuite, validateTrajectoryTrace, validateTrajectoryArguments, parseTrajectoryArguments, trajectoryTools, resolveTrajectoryRef, trajectoryConditions, trajectoryStateHash, trajectoryCaseHash, trajectorySuiteHash, checkTrajectory, type TrajectoryScalar, type TrajectoryState, type TrajectoryRef, type TrajectoryCondition, type TrajectoryEffect, type TrajectoryArgument, type TrajectoryRule, type TrajectoryTool, type TrajectoryBudget, type TrajectoryCase, type TrajectorySuite, type TrajectoryCall, type TrajectoryEvent, type TrajectoryTrace, type TrajectoryCheck } from "./trajectory.ts";
package/dist/index.js CHANGED
@@ -29,14 +29,14 @@ export { overallVerdict, scoreCategory, } from "./scoring.js";
29
29
  export { activeRotation, buildRotation, verifyRotationLog, verifyRotationRecord, } from "./rotation.js";
30
30
  export { casesOf, DEFAULT_FINDINGS, effectiveDelegation, judgementResult, normaliseText, runCheck, scenarioCases, scenariosSchema, suiteSchema, suiteSnapshot, validateScenarios, validateSuite, verifyCaseDelegation, } from "./suite.js";
31
31
  export { keySigner, signerDid, signWith } from "./signer.js";
32
- export { applyStatus, buildStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, revocationListFromStatus, verifyStatus, } from "./status.js";
32
+ export { applyStatus, buildStatus, REASONS, REASONS_V2, STATUS_V2_SCHEMA, STATUS_V2_MAX_ENTRIES, revocationListFromStatus, verifyStatus, logPositionOk, } from "./status.js";
33
33
  export { certificateToVc, VC_STATUS_TYPE, VC_TYPE, verifyCertificateVc, } from "./vc.js";
34
34
  export { statusCheckpoint, statusConsistencyProof, statusInclusionProof, verifyStatusLog, } from "./witness.js";
35
35
  export { buildAccreditationExport, buildFilingPack, buildGovernanceOutcome, buildSigningKeyApproval, CASE_KINDS, certifyRiskProfile, governanceOutcomeBody, OUTCOMES, RISK_NOTE, signingKeyApprovalBody, signRiskProfile, verifyRiskProfile, verifyAccreditationExport, verifyFilingPack, verifyGovernanceOutcome, verifySigningKeyApproval, } from "./governance.js";
36
36
  export { AWS_NITRO_ROOT_PEM, AWS_NITRO_ROOT_SHA256, AWS_NITRO_GOV_ROOT_PEM, AWS_NITRO_GOV_ROOT_SHA256, checkNitroAttestation, NITRO_MAX_AGE_SECONDS, NITRO_MAX_SKEW_SECONDS, nitroMeasurement, nitroQuoteVerifier, nitroRootForPartition, parseNitroDocument, sigStructure, verifyNitroDocument } from "./nitro.js";
37
37
  export { canonicalBytes, jcsHash } from "@zanii/core";
38
38
  export { buildRuntimeRelease, buildRuntimePolicy, buildRuntimeApproval, buildRuntimeProof, buildRuntimeChallengeRequest, buildRuntimeLease, buildRuntimeAction, buildRuntimeLeaseRequest, buildRuntimeMandate, verifyRuntimeRelease, verifyRuntimePolicy, verifyRuntimeApproval, verifyRuntimeProof, verifyRuntimeChallengeRequest, verifyRuntimeLease, verifyRuntimeAction, verifyRuntimeLeaseRequest, verifyRuntimeMandate, runtimeReleaseHash, runtimePolicyHash, runtimeRequestHash, } from "./runtime.js";
39
- export { runtimeNitroBindingHash, verifyRuntimeNitroDocument } from "./runtime-nitro.js";
39
+ export { runtimeNitroBindingHash, verifyRuntimeNitroDocument, RUNTIME_NITRO_MAX_BYTES, RUNTIME_NITRO_MAX_SKEW_SECONDS } from "./runtime-nitro.js";
40
40
  export { createRuntimeGateway } from "./runtime-gateway.js";
41
41
  export { createRuntimeReporter } from "./runtime-client.js";
42
42
  export { buildRuntimeKnowledgeSnapshot, runtimeKnowledgeProof, runtimeKnowledgeProofs, verifyRuntimeKnowledgeChunk, } from "./runtime-knowledge.js";
@@ -57,3 +57,7 @@ export { signingIdentityReasons } from "./signing-identity.js";
57
57
  export { normalizationPolicyReasons, runtimeNormalizationVersion } from "./normalization.js";
58
58
  export { schemaDialectErrors } from "./schema-dialect.js";
59
59
  export { credentialHash } from "@zanii/credentials";
60
+ // Draft public-practice trajectory extension; existing v1 formats/certification scope are unchanged.
61
+ export { TRANSCRIPT_TURNS_BYTES, transcriptTurnsHash, validateTranscriptProvenance } from "./transcript-provenance.js";
62
+ export { validateSuiteDocument } from "./suite.js";
63
+ export { TRAJECTORY_LIMITS, trajectorySchema, isTrajectoryCase, validateTrajectorySuite, validateTrajectoryTrace, validateTrajectoryArguments, parseTrajectoryArguments, trajectoryTools, resolveTrajectoryRef, trajectoryConditions, trajectoryStateHash, trajectoryCaseHash, trajectorySuiteHash, checkTrajectory } from "./trajectory.js";
package/dist/profile.d.ts CHANGED
@@ -9,11 +9,6 @@ export interface ProfileCheck {
9
9
  ok: boolean;
10
10
  errors: string[];
11
11
  }
12
- /**
13
- * Section 2: a profile is valid when it matches the schema, its category ids are distinct, every
14
- * category's required levels are levels the profile offers, and (when `clauseMaps` are given)
15
- * every clause it cites exists in a clause map the profile lists.
16
- */
17
12
  export declare function validateProfile(profile: unknown, clauseMaps?: readonly unknown[]): ProfileCheck;
18
13
  /** Section 3: a clause map is valid when it matches the schema and its clause ids are distinct. */
19
14
  export declare function validateClauseMap(map: unknown): ProfileCheck;
package/dist/profile.js CHANGED
@@ -116,6 +116,8 @@ export function schemaErrors(value, schema) {
116
116
  * category's required levels are levels the profile offers, and (when `clauseMaps` are given)
117
117
  * every clause it cites exists in a clause map the profile lists.
118
118
  */
119
+ /** scheme.md section 2a: the scheme versions whose profiles may assess categories as multi-step workflows. */
120
+ const TRAJECTORY_SCHEMES = ["zanii-certify/0.4"];
119
121
  export function validateProfile(profile, clauseMaps = []) {
120
122
  const errors = schemaErrors(profile, profileSchema);
121
123
  if (!errors.length) {
@@ -127,6 +129,8 @@ export function validateProfile(profile, clauseMaps = []) {
127
129
  if (seen.has(c.id))
128
130
  errors.push(`$.categories: duplicate id ${c.id}`);
129
131
  seen.add(c.id);
132
+ if (c.execution === "trajectory" && !TRAJECTORY_SCHEMES.includes(p.scheme))
133
+ errors.push(`$.categories.${c.id}: trajectory execution needs scheme zanii-certify/0.4`);
130
134
  for (const l of c.required_levels)
131
135
  if (!p.levels.includes(l))
132
136
  errors.push(`$.categories.${c.id}: level ${l} not in levels`);
@@ -18,6 +18,8 @@ export interface RuntimeReporterOptions {
18
18
  signer: Signer;
19
19
  attest: (binding: RuntimeAttestationBinding) => Promise<Uint8Array>;
20
20
  fetch?: typeof fetch;
21
+ /** Optional agent-bound admission credential, separate from workload proof. */
22
+ transportKey?: string;
21
23
  clock?: () => number;
22
24
  intervalMs?: number;
23
25
  onResult?: (result: RuntimeReportResult | {
@@ -6,6 +6,7 @@ import { RUNTIME_NITRO_MAX_BYTES, runtimeNitroBindingHash } from "./runtime-nitr
6
6
  import { buildRuntimeChallengeRequest, buildRuntimeProof, runtimePolicyHash, verifyRuntimeApproval } from "./runtime.js";
7
7
  import { isDid, isIso, isObj } from "./shared.js";
8
8
  import { signerDid } from "./signer.js";
9
+ import { machineTransportHeaders, redactTransportText } from "./transport-key.js";
9
10
  const DEADLINE_MS = 10_000, MAX_RESPONSE_BYTES = 32 * 1024;
10
11
  const SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
11
12
  /** One reporter has at most one underlying operation, including an uncancellable attester after timeout. */
@@ -21,6 +22,7 @@ export function createRuntimeReporter(options) {
21
22
  if (typeof authority !== "string" || authority.length > 2048 || !/^[\x21-\x7e]+$/.test(authority) || url.protocol !== "https:"
22
23
  || authority !== url.origin || url.username || url.password)
23
24
  throw new Error("runtime authority must be a canonical HTTPS origin");
25
+ const transportKey = options.transportKey, transportHeaders = machineTransportHeaders(url, transportKey);
24
26
  if (!Array.isArray(options.issuers) || !options.issuers.length || options.issuers.length > 100 || !options.issuers.every(isDid))
25
27
  throw new Error("explicit runtime issuer DIDs are required");
26
28
  const issuers = [...options.issuers], clock = options.clock ?? Date.now, call = options.fetch ?? fetch, attest = options.attest;
@@ -49,7 +51,7 @@ export function createRuntimeReporter(options) {
49
51
  async function post(path, body, signal) {
50
52
  abortCheck(signal);
51
53
  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 });
54
+ credentials: "omit", headers: { "content-type": "application/json", accept: "application/json", ...transportHeaders }, body: JSON.stringify(body), signal });
53
55
  abortCheck(signal);
54
56
  if (response.redirected || (response.url && new URL(response.url).origin !== authority))
55
57
  throw new Error("runtime authority redirected the request");
@@ -144,11 +146,14 @@ export function createRuntimeReporter(options) {
144
146
  }
145
147
  catch { /* Observers cannot break reporting or create retry queues. */ }
146
148
  };
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
- } };
149
+ const fail = (error) => {
150
+ if (!settled) {
151
+ settled = true;
152
+ const safe = new Error(redactTransportText(error instanceof Error ? error.message : "runtime report failed", transportKey));
153
+ reject(safe);
154
+ notify({ accepted: false, error: safe });
155
+ }
156
+ };
152
157
  const abort = () => fail(control.signal.reason);
153
158
  control.signal.addEventListener("abort", abort, { once: true });
154
159
  const deadline = setTimeout(() => control.abort(new Error("runtime report exceeded its 10000ms deadline")), DEADLINE_MS);
@@ -14,6 +14,8 @@ export interface RuntimeGatewayOptions {
14
14
  timeoutMs?: number;
15
15
  clock?: () => number;
16
16
  fetch?: typeof fetch;
17
+ /** Admission credential sent only to the authority, never to a protected target. */
18
+ transportKey?: string;
17
19
  }
18
20
  export declare function createRuntimeGateway(options: RuntimeGatewayOptions): {
19
21
  handle: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
@@ -6,6 +6,7 @@ import { isDid, isIso, isObj } from "./shared.js";
6
6
  import { signatureOk } from "./signer.js";
7
7
  import { sha256Bytes } from "./fingerprint.js";
8
8
  import { verifyRuntimeKnowledgeChunk } from "./runtime-knowledge.js";
9
+ import { machineTransportHeaders } from "./transport-key.js";
9
10
  class GatewayError extends Error {
10
11
  status;
11
12
  constructor(status, message) { super(message); this.status = status; }
@@ -26,6 +27,7 @@ export function createRuntimeGateway(options) {
26
27
  const authority = new URL(options.authority);
27
28
  if (authority.protocol !== "https:" || ![authority.origin, `${authority.origin}/`].includes(options.authority))
28
29
  throw new Error("authority must be a canonical HTTPS origin");
30
+ const transportHeaders = machineTransportHeaders(authority, options.transportKey);
29
31
  if (!options.issuers.length || options.issuers.length > 16 || options.issuers.some((did) => !isDid(did) || !publicKeyFromDid(did)))
30
32
  throw new Error("explicit trusted issuer DIDs are required");
31
33
  const issuers = [...options.issuers];
@@ -122,7 +124,7 @@ export function createRuntimeGateway(options) {
122
124
  throw new GatewayError(403, "operation does not match its single-use runtime authority");
123
125
  // No protected effect starts before the live authority service atomically consumes the grant.
124
126
  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) });
127
+ credentials: "omit", headers: { "content-type": "application/json", ...transportHeaders }, body: JSON.stringify({ lease, action }), signal: AbortSignal.timeout(timeout) });
126
128
  const authorizationBytes = await bounded(authorization.body, 16384);
127
129
  if (!authorization.ok)
128
130
  throw new GatewayError(403, "live runtime authority refused the operation");
@@ -10,7 +10,7 @@
10
10
  "type": { "const": "certify.profile" },
11
11
  "slug": { "$ref": "#/$defs/slug" },
12
12
  "version": { "type": "integer", "minimum": 1 },
13
- "scheme": { "enum": ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3"] },
13
+ "scheme": { "enum": ["zanii-certify/0.1", "zanii-certify/0.2", "zanii-certify/0.3", "zanii-certify/0.4"] },
14
14
  "title": { "$ref": "#/$defs/text" },
15
15
  "description": { "$ref": "#/$defs/text" },
16
16
  "levels": {
@@ -69,6 +69,7 @@
69
69
  "min_trials": { "type": "integer", "minimum": 1, "maximum": 1000000 },
70
70
  "threshold_ppm": { "type": "integer", "minimum": 0, "maximum": 1000000 },
71
71
  "ci_rule": { "const": "wilson95-low" },
72
+ "execution": { "enum": ["single_turn", "trajectory"] },
72
73
  "clauses": {
73
74
  "type": "array",
74
75
  "uniqueItems": true,