@synoi/sraid 0.2.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/PROJECTION_SPEC.md +245 -0
  3. package/README.md +149 -0
  4. package/SPEC.md +216 -0
  5. package/dist/attestation.d.ts +92 -0
  6. package/dist/attestation.d.ts.map +1 -0
  7. package/dist/attestation.js +155 -0
  8. package/dist/attestation.js.map +1 -0
  9. package/dist/authority.d.ts +351 -0
  10. package/dist/authority.d.ts.map +1 -0
  11. package/dist/authority.js +563 -0
  12. package/dist/authority.js.map +1 -0
  13. package/dist/canonicalize.d.ts +70 -0
  14. package/dist/canonicalize.d.ts.map +1 -0
  15. package/dist/canonicalize.js +151 -0
  16. package/dist/canonicalize.js.map +1 -0
  17. package/dist/ed25519.d.ts +26 -0
  18. package/dist/ed25519.d.ts.map +1 -0
  19. package/dist/ed25519.js +53 -0
  20. package/dist/ed25519.js.map +1 -0
  21. package/dist/index.d.ts +28 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +46 -0
  24. package/dist/index.js.map +1 -0
  25. package/dist/internal/base64.d.ts +28 -0
  26. package/dist/internal/base64.d.ts.map +1 -0
  27. package/dist/internal/base64.js +45 -0
  28. package/dist/internal/base64.js.map +1 -0
  29. package/dist/internal/key-cache.d.ts +47 -0
  30. package/dist/internal/key-cache.d.ts.map +1 -0
  31. package/dist/internal/key-cache.js +77 -0
  32. package/dist/internal/key-cache.js.map +1 -0
  33. package/dist/lineage.d.ts +104 -0
  34. package/dist/lineage.d.ts.map +1 -0
  35. package/dist/lineage.js +0 -0
  36. package/dist/lineage.js.map +1 -0
  37. package/dist/mldsa.d.ts +41 -0
  38. package/dist/mldsa.d.ts.map +1 -0
  39. package/dist/mldsa.js +119 -0
  40. package/dist/mldsa.js.map +1 -0
  41. package/dist/oid.d.ts +109 -0
  42. package/dist/oid.d.ts.map +1 -0
  43. package/dist/oid.js +140 -0
  44. package/dist/oid.js.map +1 -0
  45. package/dist/sensitivity.d.ts +104 -0
  46. package/dist/sensitivity.d.ts.map +1 -0
  47. package/dist/sensitivity.js +117 -0
  48. package/dist/sensitivity.js.map +1 -0
  49. package/dist/signature.d.ts +46 -0
  50. package/dist/signature.d.ts.map +1 -0
  51. package/dist/signature.js +89 -0
  52. package/dist/signature.js.map +1 -0
  53. package/dist/types.d.ts +313 -0
  54. package/dist/types.d.ts.map +1 -0
  55. package/dist/types.js +16 -0
  56. package/dist/types.js.map +1 -0
  57. package/dist/validate.d.ts +88 -0
  58. package/dist/validate.d.ts.map +1 -0
  59. package/dist/validate.js +339 -0
  60. package/dist/validate.js.map +1 -0
  61. package/package.json +69 -0
  62. package/src/attestation.ts +204 -0
  63. package/src/authority.ts +849 -0
  64. package/src/canonicalize.ts +167 -0
  65. package/src/ed25519.ts +60 -0
  66. package/src/index.ts +118 -0
  67. package/src/internal/base64.ts +44 -0
  68. package/src/internal/key-cache.ts +79 -0
  69. package/src/lineage.ts +0 -0
  70. package/src/mldsa.ts +131 -0
  71. package/src/oid.ts +146 -0
  72. package/src/sensitivity.ts +154 -0
  73. package/src/signature.ts +119 -0
  74. package/src/types.ts +351 -0
  75. package/src/validate.ts +402 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @synoi/sraid — lineage.ts
3
+ *
4
+ * L3 lineage helpers (the Merkle-DAG layer). Two jobs:
5
+ *
6
+ * 1. UNIFY the three legacy supersession mechanisms — the self-asserted
7
+ * `supersedes` string on a CDRO, the standalone SRO object, and the
8
+ * `prev` edge — into ONE set of typed lineage edges, so every consumer
9
+ * reads supersession the same way (SRAID_FOUNDATION_PUNCHLIST A2,
10
+ * OBJECT_MODEL_CLEANSHEET §3.3).
11
+ *
12
+ * 2. Give a verifier a LATEST-WINS / MONOTONE rule over a set of versions.
13
+ * The legacy `supersedes` string was an unwitnessed pointer: nothing
14
+ * stopped an attacker from re-presenting a superseded object as current
15
+ * (SRAID F10 / Adversary A3 rollback-replay). Because `prev`/`links`
16
+ * are hashed into the OID (see oid.ts `cdroContentCore`), the lineage
17
+ * edges are identity-bound and tamper-evident, and `latestWins` resolves
18
+ * a version set to its single head by following those edges — given the
19
+ * complete in-set version history, a superseded version is detectably NOT
20
+ * the head. Full rollback-replay resistance additionally requires that the
21
+ * superseding object cannot be withheld; that is a Resolver / transparency-
22
+ * log property (DESIGN, not yet deployed in this package). `set_complete`
23
+ * on `LatestWinsResult` is the local signal for callers to detect an
24
+ * incomplete (potentially withheld) set.
25
+ *
26
+ * Pure functions, no I/O. Reachability/coverage of the referenced objects
27
+ * (do they exist, are they signed, are they unrevoked) is a resolver concern
28
+ * and out of scope here, exactly as for `verifyAuthority`.
29
+ */
30
+ import type { CDRO, LineageLink } from './types.js';
31
+ /**
32
+ * Coalesce a CDRO's three legacy supersession mechanisms into one
33
+ * de-duplicated list of typed lineage edges:
34
+ *
35
+ * - `prev` → `{ rel: 'supersedes', oid: prev }`
36
+ * - `supersedes` → `{ rel: 'supersedes', oid: supersedes }`
37
+ * - each `links[]` edge → kept as-is
38
+ *
39
+ * `prev` and `supersedes` that point at the same OID collapse to a single
40
+ * `supersedes` edge, and an explicit `{ rel: 'supersedes' }` link to the same
41
+ * OID is not duplicated. The result is the canonical typed-edge view of an
42
+ * object's lineage that every consumer (verifier, resolver, drift engine)
43
+ * should read instead of poking at the three fields individually.
44
+ *
45
+ * Order: synthesized supersession edges (from `prev`/`supersedes`) first,
46
+ * then the object's own `links[]` in declared order, minus duplicates.
47
+ */
48
+ export declare function lineageLinks(cdro: Pick<CDRO, 'prev' | 'supersedes' | 'links'>): LineageLink[];
49
+ /**
50
+ * The set of predecessor OIDs this object claims to supersede, drawn from the
51
+ * unified edge view (`prev`, legacy `supersedes`, and any
52
+ * `{ rel: 'supersedes' }` links). De-duplicated.
53
+ */
54
+ export declare function supersededOids(cdro: Pick<CDRO, 'prev' | 'supersedes' | 'links'>): string[];
55
+ /** A CDRO that already carries its own OID (needed to resolve a version set). */
56
+ type Identified = Pick<CDRO, 'oid' | 'prev' | 'supersedes' | 'links'>;
57
+ export interface LatestWinsResult {
58
+ /** True iff the version set resolves to exactly one head with no cycle. */
59
+ ok: boolean;
60
+ /** The head object's OID (the latest version), when `ok`. */
61
+ head?: string;
62
+ /**
63
+ * True iff every in-set version is reachable from the head via supersedes
64
+ * edges, i.e. the set is one connected monotone chain/DAG with no unreachable
65
+ * fragment. Always false when `ok` is false. `ok && set_complete` is the
66
+ * strict signal that this set IS the complete version history from a single
67
+ * unambiguous head.
68
+ */
69
+ set_complete: boolean;
70
+ /** Machine-readable reasons when not ok or not set_complete. */
71
+ reasons: string[];
72
+ }
73
+ /**
74
+ * LATEST-WINS / MONOTONE resolution over a set of versions of the same
75
+ * logical record. Given the candidate objects, find the single head: the one
76
+ * object that is not superseded by any other object in the set, where the
77
+ * supersession edges form an acyclic DAG.
78
+ *
79
+ * Given the complete in-set version history, an attacker who re-presents a
80
+ * superseded version cannot make it the head, because some other object in the
81
+ * set carries a hashed (identity-bound) `supersedes` edge pointing at it. The
82
+ * rule is monotone: presenting MORE of the version set can only move the head
83
+ * forward (toward a newer object), never backward. Note: this guarantee is
84
+ * scoped to the provided set. Withholding the superseding object is a separate
85
+ * concern (Resolver / transparency-log layer; DESIGN, not yet deployed).
86
+ * `set_complete` signals whether every in-set node is reachable from the head.
87
+ *
88
+ * - 0 heads → a supersession cycle (no monotone order); `ok: false`.
89
+ * - 1 head → the unambiguous latest; `ok: true, head`.
90
+ * - >1 heads → a fork (concurrent versions, no single latest); `ok: false`
91
+ * with all fork heads listed in `reasons`.
92
+ *
93
+ * Diamond merges (a head supersedes two predecessors that share a common
94
+ * ancestor) are legal and do not trigger a cycle error. Only a back-edge in
95
+ * the DFS (a node encountered on the active stack) is a true cycle.
96
+ *
97
+ * Edges that point OUTSIDE the provided set are ignored for head-finding (the
98
+ * predecessor may simply not be in hand); they do not make an in-set object a
99
+ * non-head. Only an in-set object being superseded by another in-set object
100
+ * removes it from head candidacy.
101
+ */
102
+ export declare function latestWins(versions: readonly Identified[]): LatestWinsResult;
103
+ export {};
104
+ //# sourceMappingURL=lineage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineage.d.ts","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAW,MAAM,YAAY,CAAA;AAE5D;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,GAAG,WAAW,EAAE,CAwB7F;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,GAAG,MAAM,EAAE,CAU1F;AAED,iFAAiF;AACjF,KAAK,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,GAAG,MAAM,GAAG,YAAY,GAAG,OAAO,CAAC,CAAA;AAErE,MAAM,WAAW,gBAAgB;IAC/B,2EAA2E;IAC3E,EAAE,EAAE,OAAO,CAAA;IACX,6DAA6D;IAC7D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;;;;OAMG;IACH,YAAY,EAAE,OAAO,CAAA;IACrB,gEAAgE;IAChE,OAAO,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,SAAS,UAAU,EAAE,GAAG,gBAAgB,CA4F5E"}
Binary file
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lineage.js","sourceRoot":"","sources":["../src/lineage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * @synoi/sraid — mldsa.ts
3
+ *
4
+ * ML-DSA-65 (FIPS 204) verification with a native fast path.
5
+ *
6
+ * When the runtime ships OpenSSL 3.5+ (Node 24+), node:crypto exposes a
7
+ * native `ml-dsa-65` key type whose verify is materially faster than the
8
+ * pure-JS @noble/post-quantum implementation: ~207µs vs ~1.6ms verify, a
9
+ * ~7.8x op-level speedup measured on Node 24.16.0 / OpenSSL 3.5.6
10
+ * (bench/mldsa-verify.mjs).
11
+ *
12
+ * This module feature-detects native ML-DSA-65 support ONCE at first use and
13
+ * routes verification accordingly:
14
+ * • native available → node:crypto (OpenSSL)
15
+ * • native absent → @noble/post-quantum fallback
16
+ *
17
+ * Both paths are byte-for-byte interoperable: a signature minted by either
18
+ * implementation verifies identically under the other, and tampered or
19
+ * wrong-key inputs fail under both (see test/mldsa-kat.test.ts cross-impl
20
+ * known-answer vectors). The native path is therefore a transparent
21
+ * performance swap, NOT a behavior change. Node < 24 keeps working unchanged.
22
+ *
23
+ * The public API takes a RAW ML-DSA-65 public key (1952 bytes) and a RAW
24
+ * signature (3309 bytes), matching the @noble surface. The native path wraps
25
+ * the raw key in the fixed SubjectPublicKeyInfo DER prefix to build a
26
+ * KeyObject; @noble consumes the raw bytes directly.
27
+ */
28
+ /**
29
+ * True when this runtime verifies ML-DSA-65 via native node:crypto (OpenSSL
30
+ * 3.5+, Node 24+). False when it falls back to @noble/post-quantum. Exposed
31
+ * for benchmarks, diagnostics, and tests that must exercise both paths.
32
+ */
33
+ export declare function isNativeMlDsaAvailable(): boolean;
34
+ /**
35
+ * Verify a raw ML-DSA-65 signature over `message` against a raw 1952-byte
36
+ * public key. Uses the native node:crypto path when available, otherwise
37
+ * @noble/post-quantum. Returns false (never throws) on any malformed input or
38
+ * verification failure, so callers get a clean boolean.
39
+ */
40
+ export declare function verifyMlDsa65(signature: Uint8Array, message: Uint8Array, publicKeyRaw: Uint8Array): boolean;
41
+ //# sourceMappingURL=mldsa.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mldsa.d.ts","sourceRoot":"","sources":["../src/mldsa.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAsDH;;;;GAIG;AACH,wBAAgB,sBAAsB,IAAI,OAAO,CAEhD;AAoBD;;;;;GAKG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,UAAU,EACrB,OAAO,EAAE,UAAU,EACnB,YAAY,EAAE,UAAU,GACvB,OAAO,CAaT"}
package/dist/mldsa.js ADDED
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @synoi/sraid — mldsa.ts
3
+ *
4
+ * ML-DSA-65 (FIPS 204) verification with a native fast path.
5
+ *
6
+ * When the runtime ships OpenSSL 3.5+ (Node 24+), node:crypto exposes a
7
+ * native `ml-dsa-65` key type whose verify is materially faster than the
8
+ * pure-JS @noble/post-quantum implementation: ~207µs vs ~1.6ms verify, a
9
+ * ~7.8x op-level speedup measured on Node 24.16.0 / OpenSSL 3.5.6
10
+ * (bench/mldsa-verify.mjs).
11
+ *
12
+ * This module feature-detects native ML-DSA-65 support ONCE at first use and
13
+ * routes verification accordingly:
14
+ * • native available → node:crypto (OpenSSL)
15
+ * • native absent → @noble/post-quantum fallback
16
+ *
17
+ * Both paths are byte-for-byte interoperable: a signature minted by either
18
+ * implementation verifies identically under the other, and tampered or
19
+ * wrong-key inputs fail under both (see test/mldsa-kat.test.ts cross-impl
20
+ * known-answer vectors). The native path is therefore a transparent
21
+ * performance swap, NOT a behavior change. Node < 24 keeps working unchanged.
22
+ *
23
+ * The public API takes a RAW ML-DSA-65 public key (1952 bytes) and a RAW
24
+ * signature (3309 bytes), matching the @noble surface. The native path wraps
25
+ * the raw key in the fixed SubjectPublicKeyInfo DER prefix to build a
26
+ * KeyObject; @noble consumes the raw bytes directly.
27
+ */
28
+ import { createPublicKey, verify as nodeVerify } from 'node:crypto';
29
+ import { webcrypto } from 'node:crypto';
30
+ // Node 18 doesn't expose globalThis.crypto.getRandomValues by default, and the
31
+ // @noble libraries expect it during module initialization. Idempotent polyfill
32
+ // — MUST run BEFORE the @noble/post-quantum import below so ml_dsa65's
33
+ // module-init sees a populated globalThis.crypto.
34
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
35
+ if (!globalThis.crypto)
36
+ globalThis.crypto = webcrypto;
37
+ import { ml_dsa65 } from '@noble/post-quantum/ml-dsa.js';
38
+ import { BoundedKeyCache, KEY_CACHE_MAX } from './internal/key-cache.js';
39
+ // Fixed DER prefix for an ML-DSA-65 SubjectPublicKeyInfo
40
+ // (draft-ietf-lamps-dilithium-certificates):
41
+ // SEQUENCE {
42
+ // SEQUENCE { OID 2.16.840.1.101.3.4.3.18 (id-ml-dsa-65) }
43
+ // BIT STRING (0 unused bits || 1952 raw public-key bytes)
44
+ // }
45
+ // The only variable part is the 1952-byte key, so the 22-byte header is
46
+ // constant for every ML-DSA-65 key.
47
+ const MLDSA65_SPKI_PREFIX = Buffer.from('308207b2300b0609608648016503040312038207a100', 'hex');
48
+ const MLDSA65_PUBKEY_LEN = 1952;
49
+ // One-time capability detection. `undefined` = not yet probed.
50
+ let nativeAvailable;
51
+ /**
52
+ * Detect (once, then cache) whether this runtime's node:crypto can build a
53
+ * native ML-DSA-65 public key. We probe by constructing a KeyObject from a
54
+ * structurally-valid dummy SPKI: OpenSSL 3.5+ accepts it and reports
55
+ * asymmetricKeyType === 'ml-dsa-65'; older OpenSSL throws ("Failed to read
56
+ * asymmetric key"). The probe does NOT verify anything, so a dummy key body
57
+ * is fine — only the algorithm support is being detected.
58
+ */
59
+ function detectNative() {
60
+ if (nativeAvailable !== undefined)
61
+ return nativeAvailable;
62
+ try {
63
+ const dummy = Buffer.concat([MLDSA65_SPKI_PREFIX, Buffer.alloc(MLDSA65_PUBKEY_LEN)]);
64
+ const ko = createPublicKey({ key: dummy, format: 'der', type: 'spki' });
65
+ // @types/node ^20 predates the ml-dsa-* key types, so widen to string.
66
+ nativeAvailable = ko.asymmetricKeyType === 'ml-dsa-65';
67
+ }
68
+ catch {
69
+ nativeAvailable = false;
70
+ }
71
+ return nativeAvailable;
72
+ }
73
+ /**
74
+ * True when this runtime verifies ML-DSA-65 via native node:crypto (OpenSSL
75
+ * 3.5+, Node 24+). False when it falls back to @noble/post-quantum. Exposed
76
+ * for benchmarks, diagnostics, and tests that must exercise both paths.
77
+ */
78
+ export function isNativeMlDsaAvailable() {
79
+ return detectNative();
80
+ }
81
+ // Small bounded LRU: building a KeyObject parses DER, so cache by raw-key hex.
82
+ // Bounded so an attacker streaming distinct keys cannot grow it without limit.
83
+ const keyCache = new BoundedKeyCache(KEY_CACHE_MAX);
84
+ function keyObjectFromRaw(raw) {
85
+ if (raw.length !== MLDSA65_PUBKEY_LEN) {
86
+ throw new Error(`ml-dsa-65 public key must be ${MLDSA65_PUBKEY_LEN} bytes`);
87
+ }
88
+ const hex = Buffer.from(raw).toString('hex');
89
+ let ko = keyCache.get(hex);
90
+ if (!ko) {
91
+ const der = Buffer.concat([MLDSA65_SPKI_PREFIX, Buffer.from(raw)]);
92
+ ko = createPublicKey({ key: der, format: 'der', type: 'spki' });
93
+ keyCache.set(hex, ko);
94
+ }
95
+ return ko;
96
+ }
97
+ /**
98
+ * Verify a raw ML-DSA-65 signature over `message` against a raw 1952-byte
99
+ * public key. Uses the native node:crypto path when available, otherwise
100
+ * @noble/post-quantum. Returns false (never throws) on any malformed input or
101
+ * verification failure, so callers get a clean boolean.
102
+ */
103
+ export function verifyMlDsa65(signature, message, publicKeyRaw) {
104
+ if (detectNative()) {
105
+ try {
106
+ return nodeVerify(null, message, keyObjectFromRaw(publicKeyRaw), signature);
107
+ }
108
+ catch {
109
+ return false;
110
+ }
111
+ }
112
+ try {
113
+ return ml_dsa65.verify(signature, message, publicKeyRaw);
114
+ }
115
+ catch {
116
+ return false;
117
+ }
118
+ }
119
+ //# sourceMappingURL=mldsa.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mldsa.js","sourceRoot":"","sources":["../src/mldsa.ts"],"names":[],"mappings":"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"}
package/dist/oid.d.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * @synoi/sraid — oid.ts
3
+ *
4
+ * OID (Object IDentifier) computation. An OID is a content-addressed
5
+ * identifier for a CDRO object:
6
+ *
7
+ * OID = "sha256:" + hex(sha256(canonicalize(content)))
8
+ *
9
+ * The hash input is the canonical form of the OBJECT MINUS the six detached
10
+ * signature / envelope fields (see `CDRO_ENVELOPE_FIELDS`). Higher-layer code
11
+ * (e.g. GAP) typically passes the object's `body` directly; this function
12
+ * accepts any value and canonicalizes it without further interpretation, so
13
+ * callers stay in control of what enters the hash.
14
+ *
15
+ * This module is the SINGLE NORMATIVE SOURCE of the CDRO OID projection
16
+ * (ADR_019). The strip-set and number rule are specified in prose in
17
+ * PROJECTION_SPEC.md; all other surfaces derive from here.
18
+ *
19
+ * This matches the gateway's `computeGapOid` (src/gap/oid.ts) and
20
+ * `payloadOid` (src/inference/receipts.ts) byte-for-byte.
21
+ */
22
+ /**
23
+ * Compute an OID over an arbitrary canonical-compatible value.
24
+ *
25
+ * oidOf({ a: 1, b: 2 }) === oidOf({ b: 2, a: 1 })
26
+ *
27
+ * Returns `sha256:` followed by 64 lowercase hex characters.
28
+ *
29
+ * Uses the platform SHA-256 (node:crypto / OpenSSL), which is byte-identical
30
+ * to any conformant SHA-256 and materially faster than a pure-JS hash. The
31
+ * output contract is unchanged.
32
+ */
33
+ export declare function oidOf(canonical: unknown): string;
34
+ /**
35
+ * Compute an OID directly from already-canonicalized bytes. Useful when
36
+ * the caller has produced the canonical string itself (for example, when
37
+ * the same bytes will also feed into a signature) and wants to avoid
38
+ * canonicalizing twice.
39
+ */
40
+ export declare function oidOfCanonical(canonical: string | Uint8Array): string;
41
+ /**
42
+ * The detached-signature / envelope fields removed by `cdroContentCore`
43
+ * before hashing. This is the SINGLE NORMATIVE strip-set for the CDRO OID
44
+ * projection (ADR_019 decision 1); every other surface (GAP SDKs, the
45
+ * gateway signer, IMPLEMENTING.md) derives from THIS set, never re-lists it.
46
+ *
47
+ * The set is defined SEMANTICALLY: it is "every field produced BY the signer
48
+ * after canonicalization, plus the OID output itself." Concretely:
49
+ *
50
+ * oid — the projection OUTPUT (cannot be an input to itself).
51
+ * signature — legacy hybrid SignatureEnvelope (attaches after hash).
52
+ * ml_dsa_signature — detached PQ signature (attaches after hash).
53
+ * signature_key_id — signer-stamped key id (produced by the signer).
54
+ * signature_algorithm — signer-stamped alg id (produced by the signer).
55
+ * attestation — DSSE AttestationEnvelope (attaches after hash).
56
+ *
57
+ * EVERYTHING ELSE IS KEPT and hashed into the OID, including in particular:
58
+ * - `gap_version` — IN identity so a protocol downgrade is OID-detectable.
59
+ * - `supersedes` — IN identity because the SRAID Merkle-DAG head-proves-
60
+ * history property requires every lineage edge inside the
61
+ * hash. Superseding mints a NEW object; it never mutates
62
+ * the old one's bytes, so keeping it here is safe and
63
+ * makes the lineage edge tamper-evident.
64
+ * - `type`, `sraid_version`, `tenant_id`, `created_at_ms`, `created_by`,
65
+ * `body`, `authority`, `sensitivity`, `prev`, `links`, and any other
66
+ * content field.
67
+ *
68
+ * This is the ONE projection that yields the SAME OID whether the object is
69
+ * pre- or post-attestation: attaching an `attestation` (or `signature`,
70
+ * `ml_dsa_signature`, `signature_key_id`, `signature_algorithm`) after hashing
71
+ * is stripped back out here, so `cdroOid(obj)` is invariant across signing.
72
+ *
73
+ * It is FROZEN so no caller can mutate the normative set at runtime.
74
+ */
75
+ export declare const CDRO_ENVELOPE_FIELDS: readonly string[];
76
+ /**
77
+ * Build the OID content core of a full CDRO: the object with EXACTLY the six
78
+ * detached-signature / envelope fields in `CDRO_ENVELOPE_FIELDS` removed at
79
+ * the top level, and everything else kept.
80
+ *
81
+ * This is the mechanism that makes the L4 `authority` block, the L3 lineage
82
+ * edges (`prev`, `links`, `supersedes`), the propagating `sensitivity` tier,
83
+ * and `gap_version` tamper-evident: they are hashed into identity by
84
+ * construction, so a field cannot be added, stripped, re-pointed, or
85
+ * downgraded without producing a different OID (and invalidating the
86
+ * signature, which is computed over these same bytes). Because `prev`/`links`/
87
+ * `supersedes` OIDs are inside the hash, a node's OID transitively commits its
88
+ * whole reachable history (the Merkle-DAG "head proves history" property).
89
+ *
90
+ * Returns a plain object suitable for `canonicalize` / `oidOf`.
91
+ */
92
+ export declare function cdroContentCore(cdro: unknown): Record<string, unknown>;
93
+ /**
94
+ * Compute the OID of a full CDRO over its content core (see
95
+ * `cdroContentCore`). This is the correct way to derive identity for a
96
+ * complete CDRO: it hashes `authority`, `supersedes`, `gap_version`, `body`,
97
+ * and every other content field, so the L4 authority block is identity-bound
98
+ * and cannot be silently dropped.
99
+ *
100
+ * INVARIANT (ADR_019): `cdroOid(obj)` yields the SAME OID whether `obj` is
101
+ * pre- or post-attestation, because the detached envelope fields are stripped
102
+ * (see `CDRO_ENVELOPE_FIELDS`). This is what lets a third party recompute the
103
+ * OID of a signed receipt and match the value the signer stamped.
104
+ *
105
+ * Note this differs from `oidOf(cdro.body)`: a CDRO's identity is over the
106
+ * whole content core, not just its body.
107
+ */
108
+ export declare function cdroOid(cdro: unknown): string;
109
+ //# sourceMappingURL=oid.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oid.d.ts","sourceRoot":"","sources":["../src/oid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH;;;;;;;;;;GAUG;AACH,wBAAgB,KAAK,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,CAGhD;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAMrE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAOhD,CAAA;AAIF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUtE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,GAAG,MAAM,CAE7C"}
package/dist/oid.js ADDED
@@ -0,0 +1,140 @@
1
+ /**
2
+ * @synoi/sraid — oid.ts
3
+ *
4
+ * OID (Object IDentifier) computation. An OID is a content-addressed
5
+ * identifier for a CDRO object:
6
+ *
7
+ * OID = "sha256:" + hex(sha256(canonicalize(content)))
8
+ *
9
+ * The hash input is the canonical form of the OBJECT MINUS the six detached
10
+ * signature / envelope fields (see `CDRO_ENVELOPE_FIELDS`). Higher-layer code
11
+ * (e.g. GAP) typically passes the object's `body` directly; this function
12
+ * accepts any value and canonicalizes it without further interpretation, so
13
+ * callers stay in control of what enters the hash.
14
+ *
15
+ * This module is the SINGLE NORMATIVE SOURCE of the CDRO OID projection
16
+ * (ADR_019). The strip-set and number rule are specified in prose in
17
+ * PROJECTION_SPEC.md; all other surfaces derive from here.
18
+ *
19
+ * This matches the gateway's `computeGapOid` (src/gap/oid.ts) and
20
+ * `payloadOid` (src/inference/receipts.ts) byte-for-byte.
21
+ */
22
+ import { createHash } from 'node:crypto';
23
+ import { canonicalize } from './canonicalize.js';
24
+ /**
25
+ * Compute an OID over an arbitrary canonical-compatible value.
26
+ *
27
+ * oidOf({ a: 1, b: 2 }) === oidOf({ b: 2, a: 1 })
28
+ *
29
+ * Returns `sha256:` followed by 64 lowercase hex characters.
30
+ *
31
+ * Uses the platform SHA-256 (node:crypto / OpenSSL), which is byte-identical
32
+ * to any conformant SHA-256 and materially faster than a pure-JS hash. The
33
+ * output contract is unchanged.
34
+ */
35
+ export function oidOf(canonical) {
36
+ const bytes = new TextEncoder().encode(canonicalize(canonical));
37
+ return 'sha256:' + createHash('sha256').update(bytes).digest('hex');
38
+ }
39
+ /**
40
+ * Compute an OID directly from already-canonicalized bytes. Useful when
41
+ * the caller has produced the canonical string itself (for example, when
42
+ * the same bytes will also feed into a signature) and wants to avoid
43
+ * canonicalizing twice.
44
+ */
45
+ export function oidOfCanonical(canonical) {
46
+ const bytes = typeof canonical === 'string'
47
+ ? new TextEncoder().encode(canonical)
48
+ : canonical;
49
+ return 'sha256:' + createHash('sha256').update(bytes).digest('hex');
50
+ }
51
+ /**
52
+ * The detached-signature / envelope fields removed by `cdroContentCore`
53
+ * before hashing. This is the SINGLE NORMATIVE strip-set for the CDRO OID
54
+ * projection (ADR_019 decision 1); every other surface (GAP SDKs, the
55
+ * gateway signer, IMPLEMENTING.md) derives from THIS set, never re-lists it.
56
+ *
57
+ * The set is defined SEMANTICALLY: it is "every field produced BY the signer
58
+ * after canonicalization, plus the OID output itself." Concretely:
59
+ *
60
+ * oid — the projection OUTPUT (cannot be an input to itself).
61
+ * signature — legacy hybrid SignatureEnvelope (attaches after hash).
62
+ * ml_dsa_signature — detached PQ signature (attaches after hash).
63
+ * signature_key_id — signer-stamped key id (produced by the signer).
64
+ * signature_algorithm — signer-stamped alg id (produced by the signer).
65
+ * attestation — DSSE AttestationEnvelope (attaches after hash).
66
+ *
67
+ * EVERYTHING ELSE IS KEPT and hashed into the OID, including in particular:
68
+ * - `gap_version` — IN identity so a protocol downgrade is OID-detectable.
69
+ * - `supersedes` — IN identity because the SRAID Merkle-DAG head-proves-
70
+ * history property requires every lineage edge inside the
71
+ * hash. Superseding mints a NEW object; it never mutates
72
+ * the old one's bytes, so keeping it here is safe and
73
+ * makes the lineage edge tamper-evident.
74
+ * - `type`, `sraid_version`, `tenant_id`, `created_at_ms`, `created_by`,
75
+ * `body`, `authority`, `sensitivity`, `prev`, `links`, and any other
76
+ * content field.
77
+ *
78
+ * This is the ONE projection that yields the SAME OID whether the object is
79
+ * pre- or post-attestation: attaching an `attestation` (or `signature`,
80
+ * `ml_dsa_signature`, `signature_key_id`, `signature_algorithm`) after hashing
81
+ * is stripped back out here, so `cdroOid(obj)` is invariant across signing.
82
+ *
83
+ * It is FROZEN so no caller can mutate the normative set at runtime.
84
+ */
85
+ export const CDRO_ENVELOPE_FIELDS = Object.freeze([
86
+ 'oid',
87
+ 'signature',
88
+ 'ml_dsa_signature',
89
+ 'signature_key_id',
90
+ 'signature_algorithm',
91
+ 'attestation',
92
+ ]);
93
+ const CDRO_ENVELOPE_FIELD_SET = new Set(CDRO_ENVELOPE_FIELDS);
94
+ /**
95
+ * Build the OID content core of a full CDRO: the object with EXACTLY the six
96
+ * detached-signature / envelope fields in `CDRO_ENVELOPE_FIELDS` removed at
97
+ * the top level, and everything else kept.
98
+ *
99
+ * This is the mechanism that makes the L4 `authority` block, the L3 lineage
100
+ * edges (`prev`, `links`, `supersedes`), the propagating `sensitivity` tier,
101
+ * and `gap_version` tamper-evident: they are hashed into identity by
102
+ * construction, so a field cannot be added, stripped, re-pointed, or
103
+ * downgraded without producing a different OID (and invalidating the
104
+ * signature, which is computed over these same bytes). Because `prev`/`links`/
105
+ * `supersedes` OIDs are inside the hash, a node's OID transitively commits its
106
+ * whole reachable history (the Merkle-DAG "head proves history" property).
107
+ *
108
+ * Returns a plain object suitable for `canonicalize` / `oidOf`.
109
+ */
110
+ export function cdroContentCore(cdro) {
111
+ if (cdro === null || typeof cdro !== 'object' || Array.isArray(cdro)) {
112
+ throw new TypeError('cdroContentCore: argument must be a CDRO object');
113
+ }
114
+ const core = {};
115
+ for (const [k, v] of Object.entries(cdro)) {
116
+ if (CDRO_ENVELOPE_FIELD_SET.has(k))
117
+ continue;
118
+ core[k] = v;
119
+ }
120
+ return core;
121
+ }
122
+ /**
123
+ * Compute the OID of a full CDRO over its content core (see
124
+ * `cdroContentCore`). This is the correct way to derive identity for a
125
+ * complete CDRO: it hashes `authority`, `supersedes`, `gap_version`, `body`,
126
+ * and every other content field, so the L4 authority block is identity-bound
127
+ * and cannot be silently dropped.
128
+ *
129
+ * INVARIANT (ADR_019): `cdroOid(obj)` yields the SAME OID whether `obj` is
130
+ * pre- or post-attestation, because the detached envelope fields are stripped
131
+ * (see `CDRO_ENVELOPE_FIELDS`). This is what lets a third party recompute the
132
+ * OID of a signed receipt and match the value the signer stamped.
133
+ *
134
+ * Note this differs from `oidOf(cdro.body)`: a CDRO's identity is over the
135
+ * whole content core, not just its body.
136
+ */
137
+ export function cdroOid(cdro) {
138
+ return oidOf(cdroContentCore(cdro));
139
+ }
140
+ //# sourceMappingURL=oid.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oid.js","sourceRoot":"","sources":["../src/oid.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAEhD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,KAAK,CAAC,SAAkB;IACtC,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC,CAAA;IAC/D,OAAO,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACrE,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,SAA8B;IAC3D,MAAM,KAAK,GACT,OAAO,SAAS,KAAK,QAAQ;QAC3B,CAAC,CAAC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC;QACrC,CAAC,CAAC,SAAS,CAAA;IACf,OAAO,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACrE,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAsB,MAAM,CAAC,MAAM,CAAC;IACnE,KAAK;IACL,WAAW;IACX,kBAAkB;IAClB,kBAAkB;IAClB,qBAAqB;IACrB,aAAa;CACd,CAAC,CAAA;AAEF,MAAM,uBAAuB,GAAwB,IAAI,GAAG,CAAC,oBAAoB,CAAC,CAAA;AAElF;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAAC,IAAa;IAC3C,IAAI,IAAI,KAAK,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QACrE,MAAM,IAAI,SAAS,CAAC,iDAAiD,CAAC,CAAA;IACxE,CAAC;IACD,MAAM,IAAI,GAA4B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,IAA+B,CAAC,EAAE,CAAC;QACrE,IAAI,uBAAuB,CAAC,GAAG,CAAC,CAAC,CAAC;YAAE,SAAQ;QAC5C,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IACb,CAAC;IACD,OAAO,IAAI,CAAA;AACb,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,OAAO,CAAC,IAAa;IACnC,OAAO,KAAK,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC,CAAA;AACrC,CAAC"}
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @synoi/sraid — sensitivity.ts
3
+ *
4
+ * L4 GOVERNANCE — the propagating sensitivity label (OBJECT_MODEL_CLEANSHEET
5
+ * §3.4, SRAID_FOUNDATION_PUNCHLIST A5). A single coarse, OPAQUE tier on the
6
+ * CDRO that retrieval, summarization, and consolidation MUST carry forward
7
+ * monotonically: a derived object inherits the HIGHEST tier among its sources,
8
+ * so a summary of high-sensitivity inputs cannot be downgraded.
9
+ *
10
+ * ── Why the tiers are OPAQUE ORDINALS, not literal categories ───────────────
11
+ *
12
+ * SPEC §7 is normative and explicit: the envelope is a NON-encrypted, signed,
13
+ * publicly visible surface. A literal classification label (e.g. `"phi"`,
14
+ * `"health"`, `"financial"`) on that surface would LEAK the nature of the
15
+ * encrypted `body` — an observer who cannot read the ciphertext could still
16
+ * read "this is health data" off the envelope. SPEC §7 therefore requires a
17
+ * "coarse, OPAQUE sensitivity classifier", and the §9 reserved note says the
18
+ * same.
19
+ *
20
+ * So the tier set here is a small, COARSE, ORDERED ladder of opaque levels
21
+ * (`s0` < `s1` < `s2` < `s3` < `s4`). It carries an ORDERING (needed for the
22
+ * monotone `max()` lattice — standard lattice-based information-flow labeling,
23
+ * Denning 1976) but NO semantic category. The mapping from a regulatory
24
+ * category (PHI, PII, secret, …) to a tier is a higher-layer POLICY concern
25
+ * kept OFF the public envelope: an operator's policy decides "PHI → s3"
26
+ * privately; the wire only ever shows `s3`. This is the "coarse, opaque tier
27
+ * (or opaque policy-scope handle)" required by the task and SPEC §7.
28
+ *
29
+ * The field is hashed into the OID (it is a content-core field; see
30
+ * `cdroContentCore` in oid.ts), so it cannot be silently stripped or
31
+ * downgraded without changing the OID and breaking the signature.
32
+ *
33
+ * NOTE on maturity (CLAIMS_DISCIPLINE): this module ships the FIELD, the
34
+ * lattice, the monotone `max()` carry-forward, and the validator, with
35
+ * vectors. The commitment-based selective-disclosure scheme that would
36
+ * CONCEAL the tier itself (SPEC §7 / §9) is reserved and NOT built here.
37
+ */
38
+ /**
39
+ * The coarse, opaque sensitivity tiers, lowest to highest. These are ORDINAL
40
+ * and OPAQUE by design — they convey relative sensitivity for the monotone
41
+ * propagation lattice WITHOUT naming a content category (per SPEC §7, a
42
+ * literal category on the public envelope would leak what the encrypted body
43
+ * is). The mapping of a real-world classification to a tier is private policy,
44
+ * not part of this taxonomy.
45
+ *
46
+ * s0 — lowest / unclassified (the default floor; freely shareable)
47
+ * s1 — low
48
+ * s2 — moderate
49
+ * s3 — high
50
+ * s4 — highest / most restricted
51
+ *
52
+ * Five coarse levels is deliberate: enough granularity for a useful lattice,
53
+ * few enough to stay coarse (so the tier alone is weakly distinguishing).
54
+ */
55
+ export type SensitivityTier = 's0' | 's1' | 's2' | 's3' | 's4';
56
+ /**
57
+ * The ordered tier ladder, lowest-first. The index IS the rank used by the
58
+ * `max()` lattice. Frozen so the ordering is immutable at runtime.
59
+ */
60
+ export declare const SENSITIVITY_TIERS: readonly SensitivityTier[];
61
+ /** The default tier for an object that declares none — the lattice floor. */
62
+ export declare const SENSITIVITY_DEFAULT: SensitivityTier;
63
+ /** True iff `x` is a defined `SensitivityTier`. */
64
+ export declare function isSensitivityTier(x: unknown): x is SensitivityTier;
65
+ /**
66
+ * The ordinal rank of a tier (0 = lowest). Throws on an unknown tier so a
67
+ * typo can never silently rank as the floor (which would be a downgrade
68
+ * footgun in the lattice).
69
+ */
70
+ export declare function sensitivityRank(tier: SensitivityTier): number;
71
+ /**
72
+ * The monotone lattice join: the HIGHER (more restricted) of two tiers.
73
+ * `max('s1','s3') === 's3'`. This is the per-pair operation behind
74
+ * carry-forward.
75
+ */
76
+ export declare function sensitivityMax(a: SensitivityTier, b: SensitivityTier): SensitivityTier;
77
+ /**
78
+ * Carry-forward over a set of source tiers: the highest tier present.
79
+ *
80
+ * This is the rule that makes sensitivity MONOTONE under derivation
81
+ * (retrieval / summarization / consolidation): a derived object's tier =
82
+ * max(tiers of all its `consolidated_from` / `derived_from` sources). It can
83
+ * only ever go UP, never down — a summary of high-sensitivity inputs stays at
84
+ * the highest input tier.
85
+ *
86
+ * `undefined`/absent source tiers are treated as the floor (`s0`): a source
87
+ * that declared no tier cannot pull the result down, and an explicit higher
88
+ * source always wins. An empty input set returns the floor.
89
+ */
90
+ export declare function sensitivityCarryForward(sources: ReadonlyArray<SensitivityTier | undefined | null>): SensitivityTier;
91
+ /**
92
+ * Guard a proposed sensitivity assignment against monotone violation: the
93
+ * `proposed` tier of a derived object MUST be at least as high as the
94
+ * carry-forward of its `sources`. Returns the floor the derived object is NOT
95
+ * allowed to fall below, plus whether `proposed` honors it.
96
+ *
97
+ * Use this to REJECT an attempt to label a summary of `s3` inputs as `s1`.
98
+ */
99
+ export declare function sensitivityMonotoneCheck(proposed: SensitivityTier, sources: ReadonlyArray<SensitivityTier | undefined | null>): {
100
+ ok: boolean;
101
+ floor: SensitivityTier;
102
+ proposed: SensitivityTier;
103
+ };
104
+ //# sourceMappingURL=sensitivity.d.ts.map