@sanning/proof 0.17.0 → 0.19.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.
@@ -0,0 +1,145 @@
1
+ // The environment marker: is this sandbox evidence?
2
+ //
3
+ // `environment` is an OPTIONAL skeleton field inside every envelope's signed
4
+ // scope, `"dev"` or `"production"` (envelope-spec §2, A1 and A9). The spec
5
+ // says a verifier MAY surface the value as `environment_marker` in its result
6
+ // and a profile MAY refuse to count dev-marked envelopes toward production
7
+ // evidence. Both commands now do the first, and print a pack or case that
8
+ // holds a `dev` envelope as sandbox evidence rather than as `VERIFIED`.
9
+ //
10
+ // The marker is read from the signed envelopes the pack holds, never from a
11
+ // wrapper field, a pack name or anything else the holder could write without a
12
+ // key. It never moves the verdict word: a sandbox pack that verifies is still
13
+ // `verified`, and the commands map that to the headline `SANDBOX` and exit 5
14
+ // (`sandboxExit` in `./exit-codes.ts`). The Python twin is
15
+ // `src/sanning_proof/environment.py`.
16
+
17
+ import { isAgentProofSpec } from "./agent-proof.js";
18
+ import {
19
+ ANCHOR_TRACE_BODY_TYPE,
20
+ EXPORT_BODY_TYPE,
21
+ type EvidenceBundleResult,
22
+ SIMULATED_WITNESS_KIND,
23
+ STAMP_EVIDENCE_BODY_TYPE,
24
+ } from "./evidence.js";
25
+
26
+ /** `"dev"` when every envelope is signed `dev`, `"mixed"` when only some are. */
27
+ export type EnvironmentMarker = "dev" | "mixed";
28
+
29
+ /** The headline `verify` prints for sandbox evidence in place of `VERIFIED`. */
30
+ export const SANDBOX_HEADLINE = "SANDBOX";
31
+
32
+ /** The title `case` prints for sandbox evidence in place of `Verified` (case titles are title case). */
33
+ export const SANDBOX_TITLE = "Sandbox";
34
+
35
+ /** The one line printed with it, and with any other headline, wherever the marker is present. */
36
+ export const SANDBOX_LINE = "Sandbox evidence (environment dev): not production evidence";
37
+
38
+ // The same bound `resolveTraceEvents` puts on unwrapping export wrappers.
39
+ const MAX_EXPORT_UNWRAP = 4;
40
+
41
+ function isObject(v: unknown): v is Record<string, unknown> {
42
+ return v !== null && typeof v === "object" && !Array.isArray(v);
43
+ }
44
+
45
+ function own(o: Record<string, unknown>, key: string): unknown {
46
+ return Object.prototype.hasOwnProperty.call(o, key) ? o[key] : undefined;
47
+ }
48
+
49
+ function entryEnvelopes(list: unknown, out: Record<string, unknown>[]): void {
50
+ if (!Array.isArray(list)) return;
51
+ for (const entry of list) {
52
+ if (!isObject(entry)) continue;
53
+ const env = own(entry, "envelope");
54
+ if (isObject(env)) out.push(env);
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The signed envelopes a pack holds: every checkpoint and event envelope of an
60
+ * anchor-trace body, every event envelope of a stamp-evidence body, the same
61
+ * inside an export's inline source bundle, and an agent proof's checkpoint
62
+ * envelope. A stamp-trace body holds none. A value that is not an object is
63
+ * not an envelope and is not counted.
64
+ */
65
+ export function packEnvelopes(bundle: unknown): Record<string, unknown>[] {
66
+ const out: Record<string, unknown>[] = [];
67
+ if (isObject(bundle) && isAgentProofSpec(own(bundle, "spec_version"))) {
68
+ const env = own(bundle, "checkpoint_envelope");
69
+ if (isObject(env)) out.push(env);
70
+ return out;
71
+ }
72
+ let cur: unknown = bundle;
73
+ for (let depth = 0; depth < MAX_EXPORT_UNWRAP; depth++) {
74
+ if (!isObject(cur)) return out;
75
+ const bodyType = own(cur, "body_type");
76
+ const body = own(cur, "body");
77
+ if (!isObject(body)) return out;
78
+ if (bodyType === ANCHOR_TRACE_BODY_TYPE) {
79
+ entryEnvelopes(own(body, "checkpoints"), out);
80
+ entryEnvelopes(own(body, "events"), out);
81
+ return out;
82
+ }
83
+ if (bodyType === STAMP_EVIDENCE_BODY_TYPE) {
84
+ entryEnvelopes(own(body, "events"), out);
85
+ return out;
86
+ }
87
+ if (bodyType !== EXPORT_BODY_TYPE) return out;
88
+ cur = own(body, "source_bundle");
89
+ }
90
+ return out;
91
+ }
92
+
93
+ /**
94
+ * The environment marker over one or more packs: `"dev"` where every envelope
95
+ * they hold is signed `environment: "dev"`, `"mixed"` where some are and some
96
+ * are not (`production`, or no `environment` at all), and `null` where none
97
+ * is, which is every production pack and every pack that holds no envelope.
98
+ */
99
+ export function environmentMarker(bundles: readonly unknown[]): EnvironmentMarker | null {
100
+ let total = 0;
101
+ let dev = 0;
102
+ for (const bundle of bundles) {
103
+ for (const env of packEnvelopes(bundle)) {
104
+ total++;
105
+ if (own(env, "environment") === "dev") dev++;
106
+ }
107
+ }
108
+ if (dev === 0) return null;
109
+ return dev === total ? "dev" : "mixed";
110
+ }
111
+
112
+ /**
113
+ * Whether a body's only unsettled witnesses are simulated: the body has events
114
+ * and witnesses (checkpoints, stamps or both), every witness is named, at
115
+ * least one is simulated, and every other one a gateway confirmed. Then the
116
+ * one thing that keeps the bundle from `verified` is a simulated witness
117
+ * nobody can look up: no mismatch, no unreachable gateway, no witness of
118
+ * another kind, no checkpoint or stamp witnessed by nobody.
119
+ */
120
+ export function onlySimulatedUnsettled(result: EvidenceBundleResult): boolean {
121
+ const witnesses = [...result.checkpoints, ...(result.stamps ?? [])];
122
+ if (witnesses.length === 0 || result.events.length === 0) return false;
123
+ let simulated = 0;
124
+ for (const w of witnesses) {
125
+ if (w.anchorRef === null) return false;
126
+ if (w.anchorRef.kind === SIMULATED_WITNESS_KIND) simulated++;
127
+ else if (w.onChain?.rollup !== "confirm") return false;
128
+ }
129
+ return simulated > 0;
130
+ }
131
+
132
+ /**
133
+ * Whether a run with gateways counted only simulated witnesses, so nothing
134
+ * was looked up on a chain: gateways were passed, the run is not counted as
135
+ * checked on a chain, and it holds a simulated witness. A run that stopped
136
+ * before its witnesses were read (a bad wrapper signature or `body_hash`, an
137
+ * unknown `body_type`, an export whose source is not inline) holds none, so it
138
+ * is not one, and prints what it printed before simulated witnesses existed.
139
+ */
140
+ export function simulatedOnlyRun(result: EvidenceBundleResult, gateways: readonly string[]): boolean {
141
+ if (gateways.length === 0 || result.onChainChecked) return false;
142
+ return [...result.checkpoints, ...(result.stamps ?? [])].some(
143
+ (w) => w.anchorRef?.kind === SIMULATED_WITNESS_KIND,
144
+ );
145
+ }
package/src/evidence.ts CHANGED
@@ -1662,6 +1662,10 @@ async function verifyAnchorTrace(
1662
1662
  let gatewayUnavailable = false;
1663
1663
  let uncheckedKind = false;
1664
1664
  let uncheckedId = false;
1665
+ // Simulated witnesses against witnesses of any other kind, counted over the
1666
+ // checkpoints that name one, as `refetchStamps` counts stamps.
1667
+ let simulated = 0;
1668
+ let onChainKinds = 0;
1665
1669
  if (onChainChecked) {
1666
1670
  if (typeof fetchImpl !== "function") {
1667
1671
  gatewayUnavailable = true;
@@ -1701,6 +1705,8 @@ async function verifyAnchorTrace(
1701
1705
  cpr.errors.push("checkpoint has no anchor_ref -- not yet witnessed, nothing to re-fetch");
1702
1706
  continue;
1703
1707
  }
1708
+ if (cpr.anchorRef.kind === SIMULATED_WITNESS_KIND) simulated++;
1709
+ else onChainKinds++;
1704
1710
  const outcome = await refetchCheckpoint(cp, cpr.anchorRef, gateways, fetchImpl);
1705
1711
  cpr.onChain = outcome.onChain;
1706
1712
  cpr.onChainOk = outcome.onChain?.onChainOk ?? null;
@@ -1764,7 +1770,10 @@ async function verifyAnchorTrace(
1764
1770
  bodyHashOk: true,
1765
1771
  signatureOk: true,
1766
1772
  bodyType,
1767
- onChainChecked,
1773
+ // A run whose only witnessed checkpoints are simulated looked nothing up
1774
+ // on a chain, so it is not counted as checked on one (the stamp rule in
1775
+ // `refetchStamps`). The checkpoints stay not checked and the run partial.
1776
+ onChainChecked: onChainChecked && !(simulated > 0 && onChainKinds === 0),
1768
1777
  checkpoints: checkpointResults,
1769
1778
  events: eventResults,
1770
1779
  assertedStatus,
@@ -2679,12 +2688,16 @@ async function refetchStamps(
2679
2688
  let gatewayUnavailable = false;
2680
2689
  let uncheckedKind = false;
2681
2690
  let uncheckedId = false;
2691
+ let simulated = 0;
2692
+ let onChainKinds = 0;
2682
2693
  for (let i = 0; i < stamps.length; i++) {
2683
2694
  const st = stamps[i]!;
2684
2695
  const witnessedHex = witnessed[i];
2685
2696
  // No witness named, or bytes we could not read: nothing to re-fetch, and
2686
2697
  // nothing a gateway could confirm or deny. Left as `onChain: null`.
2687
2698
  if (st.anchorRef === null || witnessedHex === undefined) continue;
2699
+ if (st.anchorRef.kind === SIMULATED_WITNESS_KIND) simulated++;
2700
+ else onChainKinds++;
2688
2701
 
2689
2702
  const outcome = await refetchStamp(witnessedHex, st.anchorRef, gateways, fetchImpl);
2690
2703
  st.onChain = outcome.onChain;
@@ -2704,9 +2717,24 @@ async function refetchStamps(
2704
2717
  // unreachable note would fail a stamp for a gateway being down.
2705
2718
  st.ok = st.ok && st.onChainOk !== false;
2706
2719
  }
2720
+ // A simulated witness is a sandbox plane's stand-in for a chain: there is no
2721
+ // chain to look it up on, so it is never counted as checked on one.
2722
+ // It stays not checked like any kind this verifier has no adapter for, and
2723
+ // the run stays undetermined; what changes is that a run whose only
2724
+ // witnesses are simulated no longer says it checked anything on a chain.
2725
+ if (simulated > 0 && onChainKinds === 0) {
2726
+ return { onChainChecked: false, gatewayUnavailable, uncheckedKind, uncheckedId };
2727
+ }
2707
2728
  return { onChainChecked, gatewayUnavailable, uncheckedKind, uncheckedId };
2708
2729
  }
2709
2730
 
2731
+ /**
2732
+ * The `anchor_ref.kind` a sandbox plane writes for a witness it simulated
2733
+ * (chain id `sanning-sandbox`). Never looked up and never counted as checked
2734
+ * on a chain.
2735
+ */
2736
+ export const SIMULATED_WITNESS_KIND = "simulated";
2737
+
2710
2738
  interface StampRefetchSummary {
2711
2739
  onChainChecked: boolean;
2712
2740
  gatewayUnavailable: boolean;
package/src/exit-codes.ts CHANGED
@@ -16,6 +16,14 @@ export const EXIT_FAILED = 1;
16
16
  export const EXIT_MALFORMED = 2;
17
17
  export const EXIT_UNDETERMINED = 3;
18
18
  export const EXIT_MIXED = 4;
19
+ /**
20
+ * Sandbox evidence (envelope-spec §2): a run that would exit 0 over a pack or
21
+ * case holding a record signed `environment: "dev"`, or a run over records
22
+ * that are ALL signed `dev` whose only unsettled witnesses are simulated.
23
+ * Not a verdict word: the verdict stays what the checks established
24
+ * (`sandboxExit`). No flag turns 5 into 0.
25
+ */
26
+ export const EXIT_SANDBOX = 5;
19
27
 
20
28
  /**
21
29
  * The exit code of each of the eight verdict words (`evidence-bundle.md` §3).
@@ -40,6 +48,7 @@ export const EXIT_CODES_HELP = [
40
48
  " 2 usage or malformed",
41
49
  " 3 undetermined: partial, pending, incomplete or not_found",
42
50
  " 4 mixed: more than one outcome, none failed; read the exceptions",
51
+ " 5 sandbox: a record is signed environment dev; would be 0, or only a simulated witness is unchecked",
43
52
  ].join("\n");
44
53
 
45
54
  /**
@@ -52,3 +61,22 @@ export function exitCodeFor(verdict: unknown): number {
52
61
  }
53
62
  return EXIT_MALFORMED;
54
63
  }
64
+
65
+ /**
66
+ * The code a verb exits with once the environment marker is read. 5 where the
67
+ * run would otherwise exit 0 and the pack or case holds a record signed `dev`;
68
+ * 5 where the run is undetermined only because a simulated witness could not
69
+ * be looked up (`onlySimulatedUnsettled`, which the caller works out) and
70
+ * EVERY record is signed `dev`, so a production or mixed pack carrying a
71
+ * simulated witness keeps 3; the code unchanged everywhere else. A failure
72
+ * stays 1, malformed 2 and mixed 4.
73
+ */
74
+ export function sandboxExit(
75
+ code: number,
76
+ environmentMarker: string | null,
77
+ onlySimulatedUnsettled = false,
78
+ ): number {
79
+ if (code === EXIT_VERIFIED && environmentMarker !== null) return EXIT_SANDBOX;
80
+ if (code === EXIT_UNDETERMINED && environmentMarker === "dev" && onlySimulatedUnsettled) return EXIT_SANDBOX;
81
+ return code;
82
+ }
package/src/index.ts CHANGED
@@ -25,9 +25,19 @@ export {
25
25
  specVersionSupported,
26
26
  verifyEnvelope,
27
27
  } from "./verifier.js";
28
+ // The strict reader of bytes that claim to be JCS, the twin of Python's public
29
+ // `canonicalize.jcs_loads`. The enclave attestation adapters read a token's
30
+ // JSON through it, so they refuse what the kernel refuses. Additive.
31
+ export { jcsParse } from "./verifier.js";
32
+ // The same reader within the enclave readers' limits (enclave-attestation
33
+ // section 2.1): depth checked on the bytes first, every number finite. The
34
+ // adapters read a token's header and claims and the manifest through it.
35
+ // Additive.
36
+ export { exceedsTextDepth, hasNonFiniteNumber, jcsParseWithinLimits, MAX_READ_DEPTH } from "./verifier.js";
28
37
  export {
29
38
  bytesToHex,
30
39
  deriveOperatorAddress,
40
+ ed25519Refusal,
31
41
  ed25519Verify,
32
42
  hexToBytes,
33
43
  sha256Bytes,
@@ -174,6 +184,7 @@ export {
174
184
  DECLARATION_VERDICTS,
175
185
  DECLARATION_VERSION,
176
186
  DECLARATION_VERSION_V2,
187
+ DECLARATION_VERSION_V3,
177
188
  DECLARATION_VERSIONS,
178
189
  checkDeclaration,
179
190
  declarationClaims,
@@ -352,7 +363,10 @@ export type {
352
363
  // `vectors/chain-reading.json` beside the pack-grading table for the same
353
364
  // reason: a rule consumers must agree with needs a statement they can fetch.
354
365
  //
355
- // It returns FINDINGS, never a verdict, and it feeds no status. Chain links are
366
+ // It returns FINDINGS, never a verdict, and it feeds no status on its own: a
367
+ // pack's signed v3 declaration can claim a chain complete and in order, and
368
+ // `checkDeclaration` holds that claim to the links `readChainLinks` reads
369
+ // (evidence-bundle.md 5.4.2 rule 11). Chain links are
356
370
  // deletion/reorder-evidence for the events present and NOT a completeness proof
357
371
  // (`anchor/docs/profile-sanning.events-v2.md` §5), drop the last N events of a
358
372
  // chain and nothing dangles. `CHAIN_READING_SCOPE` is that sentence, exported so
@@ -361,12 +375,14 @@ export type {
361
375
  export {
362
376
  ADAPTER_CHAIN_NAMESPACES,
363
377
  CHAIN_READING_SCOPE,
378
+ readChainLinks,
364
379
  readChains,
365
380
  renderChainFinding,
366
381
  } from "./chain.js";
367
382
  export type {
368
383
  ChainFinding,
369
384
  ChainFindingKind,
385
+ ChainLink,
370
386
  ChainReading,
371
387
  ChainSource,
372
388
  ChainSummary,
@@ -384,10 +400,24 @@ export {
384
400
  EXIT_FAILED,
385
401
  EXIT_MALFORMED,
386
402
  EXIT_MIXED,
403
+ EXIT_SANDBOX,
387
404
  EXIT_UNDETERMINED,
388
405
  EXIT_VERIFIED,
389
406
  exitCodeFor,
407
+ sandboxExit,
390
408
  } from "./exit-codes.js";
409
+ // Sandbox evidence: the environment marker over the envelopes a pack or case
410
+ // holds (envelope-spec §2, the `environment_marker` MAY), and the headline and
411
+ // line both commands print for it. Additive.
412
+ export {
413
+ SANDBOX_HEADLINE,
414
+ SANDBOX_LINE,
415
+ SANDBOX_TITLE,
416
+ environmentMarker,
417
+ onlySimulatedUnsettled,
418
+ packEnvelopes,
419
+ } from "./environment.js";
420
+ export type { EnvironmentMarker } from "./environment.js";
391
421
  export { attestationSignatureToHex, composeExport } from "./compose.js";
392
422
  export type { ComposeExportOptions, ComposeExportResult, ExporterKey } from "./compose.js";
393
423
 
@@ -0,0 +1,44 @@
1
+ // Loading the enclave attestation adapters a reader has installed. Kept out of
2
+ // `index.ts` on purpose: it imports packages by name, which a browser bundle
3
+ // cannot resolve, and only the command line needs it. `run.ts` imports nothing.
4
+ //
5
+ // The packages are named by `ADAPTER_MODULES` and by nothing else: a provider
6
+ // id read from a pack is never turned into a module name.
7
+
8
+ import { ADAPTER_MODULES, declaredKinds, type EnclaveAdapter } from "./run.js";
9
+
10
+ /** Import a module by name. A parameter so a test can watch what is asked for. */
11
+ export type Importer = (name: string) => Promise<unknown>;
12
+
13
+ const dynamicImport: Importer = (name) => import(/* @vite-ignore */ name);
14
+
15
+ /**
16
+ * The adapters this environment holds, loaded from `ADAPTER_MODULES` and
17
+ * nothing else. A module counts only when it exports the provider it was
18
+ * loaded for as `provider`, a `check` function, and the failure kinds it
19
+ * declares as `kinds` (`declaredKinds`).
20
+ */
21
+ export async function loadInstalledAdapters(
22
+ importer: Importer = dynamicImport,
23
+ ): Promise<Map<string, EnclaveAdapter>> {
24
+ const found = new Map<string, EnclaveAdapter>();
25
+ for (const [provider, name] of Object.entries(ADAPTER_MODULES)) {
26
+ let module: unknown;
27
+ try {
28
+ module = await importer(name);
29
+ } catch {
30
+ continue;
31
+ }
32
+ const m = module as Partial<EnclaveAdapter> | null;
33
+ if (
34
+ m !== null &&
35
+ typeof m === "object" &&
36
+ m.provider === provider &&
37
+ typeof m.check === "function" &&
38
+ declaredKinds(m) !== null
39
+ ) {
40
+ found.set(provider, m as EnclaveAdapter);
41
+ }
42
+ }
43
+ return found;
44
+ }