@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
package/src/types.ts ADDED
@@ -0,0 +1,351 @@
1
+ /**
2
+ * @synoi/sraid — types.ts
3
+ *
4
+ * Type definitions for the SRAID (Self-Routing Addressable Identity Data) protocol — L0 of the
5
+ * SRAID Stack. Mirrors the shapes used by synoi-gateway and @synoi/vault so
6
+ * external implementations can interoperate without depending on the
7
+ * internal packages.
8
+ *
9
+ * CDRO objects are content-addressed: the OID is derived from the canonical
10
+ * form of the object (minus signature fields), so any byte-level change to
11
+ * the canonical content yields a different OID. Signatures are computed
12
+ * over the canonical bytes and attached after — they are NOT in the hash
13
+ * input, so signature rotation does not change the OID.
14
+ */
15
+
16
+ // ── L4 authority block ────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * The six GAP decision verbs. Authority records carry the full verb set;
20
+ * they are NOT folded to allow/deny/defer before persistence (SRAID
21
+ * Foundation Punch List item A4). When an authority block describes a
22
+ * decision (e.g. a Decision Receipt), `decision` is one of these.
23
+ *
24
+ * allow — the action was permitted outright
25
+ * deny — the action was refused
26
+ * defer — the decision was postponed (queued / pending)
27
+ * step_up — additional authentication / HITL was required
28
+ * delegate — authority was passed to another actor
29
+ * revoke — a prior grant/decision was withdrawn
30
+ *
31
+ * The clean-sheet model (OBJECT_MODEL_CLEANSHEET §3.5) shows the core four
32
+ * (allow|deny|defer|step_up); delegate/revoke complete the AARM verb set
33
+ * so the enum does not have to widen later.
34
+ */
35
+ export type AuthorityDecision =
36
+ | 'allow'
37
+ | 'deny'
38
+ | 'defer'
39
+ | 'step_up'
40
+ | 'delegate'
41
+ | 'revoke'
42
+
43
+ /**
44
+ * L4 GOVERNANCE — the authority block (the "authorized axis").
45
+ *
46
+ * This is the structural realization of the authorized axis
47
+ * (OBJECT_MODEL_CLEANSHEET §3.5, SRAID_FOUNDATION_PUNCHLIST A1): a state
48
+ * change or event is *normal* iff a governing authority record explains
49
+ * it. Every CDRO MAY carry an `authority` block referencing the GAP grant,
50
+ * the decision verb, and the session intent anchor that authorized it.
51
+ *
52
+ * IMPORTANT — identity-bound. The authority block lives physically INSIDE
53
+ * the hashed content core (it is one of the L4 fields hashed into the OID),
54
+ * so it cannot be silently stripped or swapped without changing the OID and
55
+ * therefore breaking the signature. Use `cdroOid` / `cdroContentCore` in
56
+ * `oid.ts` to compute identity over the content core (which includes
57
+ * `authority`), never `oidOf(body)` alone for a full CDRO.
58
+ *
59
+ * `decision` is `null`-able by design: a `state:change_event` carries
60
+ * `authority` with a null/absent decision until a correlation pass binds it
61
+ * to an authorizing receipt — a null authorized axis IS the orphan test
62
+ * (OBJECT_MODEL_CLEANSHEET R2.4). An absent `authority` field means the
63
+ * object asserts no authority; a present block with `grant_oid` asserts one.
64
+ */
65
+ export interface AuthorityBlock {
66
+ /**
67
+ * OID of the GAP capability grant (or decision/intent record) that
68
+ * permitted this object to exist. `sha256:<hex>` form. Optional only for
69
+ * an as-yet-uncorrelated state-change event; for any object that claims
70
+ * to be authorized, this MUST be present.
71
+ */
72
+ grant_oid?: string
73
+ /**
74
+ * The decision verb, when this object IS (or records) a decision. One of
75
+ * the six GAP verbs. May be null/absent for objects that merely reference
76
+ * an authorizing grant without themselves being a decision, or for an
77
+ * uncorrelated state-change event.
78
+ */
79
+ decision?: AuthorityDecision | null
80
+ /**
81
+ * OID of the session / task intent anchor under which the authority was
82
+ * exercised. `sha256:<hex>` form. Optional.
83
+ */
84
+ intent_oid?: string
85
+ }
86
+
87
+ // ── L3 lineage (Merkle-DAG) ───────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Typed lineage edge relations (OBJECT_MODEL_CLEANSHEET §3.3,
91
+ * SRAID_FOUNDATION_PUNCHLIST A2). A `links[]` edge names *why* one object
92
+ * points at another. The taxonomy is the union the four products need; it
93
+ * is OPEN (a verifier ignores rels it does not understand) but these are the
94
+ * named, reserved relations.
95
+ *
96
+ * supersedes — this object replaces the target (the unified, witnessed
97
+ * form of the legacy self-asserted `supersedes` string and
98
+ * the standalone SRO). Combined with `prev`, gives a
99
+ * verifier a latest-wins / monotone rule instead of an
100
+ * unwitnessed pointer (fixes SRAID F10 / Adversary A3).
101
+ * derived_from — this object was derived/transformed from the target.
102
+ * snapshot_of — this object is a checkpoint snapshot of the target chain.
103
+ * consolidated_from — Saga: a consolidated memory record built from raw chunks.
104
+ * predecessor — generic prior-version edge (when not a full supersession).
105
+ * encounter_of — Vitni: an encounter/disclosure references a prior one.
106
+ * subject_digest — Hlif: the content digest of the attested artifact.
107
+ * invocation_of — Althing: the GAP capability invocation this records.
108
+ * hitl_evidence — Althing: a HITL channel event backing a decision.
109
+ * entity_of — state-drift: the entity state-spine head this observes.
110
+ * prev_observation — state-drift: the prior observation of the same entity.
111
+ *
112
+ * Unknown rels are permitted (forward-compatible); validation only checks
113
+ * that each edge is well-formed (a non-empty `rel` + a `sha256:` `oid`).
114
+ */
115
+ export type LinkRel =
116
+ | 'supersedes'
117
+ | 'derived_from'
118
+ | 'snapshot_of'
119
+ | 'consolidated_from'
120
+ | 'predecessor'
121
+ | 'encounter_of'
122
+ | 'subject_digest'
123
+ | 'invocation_of'
124
+ | 'hitl_evidence'
125
+ | 'entity_of'
126
+ | 'prev_observation'
127
+ | (string & {})
128
+
129
+ /**
130
+ * L3 LINEAGE — one typed Merkle-DAG edge.
131
+ *
132
+ * Both `prev` and every `links[]` edge are HASHED INTO THE OID (they live in
133
+ * the content core, see `cdroContentCore` in oid.ts), so lineage is
134
+ * identity-bound and tamper-evident: a node's OID transitively commits its
135
+ * entire reachable history (the Git/IPFS "head hash proves all history"
136
+ * property, OBJECT_MODEL_CLEANSHEET §3.3). An edge cannot be added, removed,
137
+ * or re-pointed without changing the OID and invalidating the signature.
138
+ */
139
+ export interface LineageLink {
140
+ /** Why this edge exists. One of the reserved `LinkRel`s, or any string. */
141
+ rel: LinkRel
142
+ /** OID of the target object. `sha256:<hex>` form. */
143
+ oid: string
144
+ }
145
+
146
+ // ── L4 sensitivity (propagating, opaque tier) ─────────────────────────────────
147
+
148
+ export type { SensitivityTier } from './sensitivity.js'
149
+ import type { SensitivityTier } from './sensitivity.js'
150
+
151
+ // ── Core CDRO envelope ────────────────────────────────────────────────────────
152
+
153
+ /**
154
+ * Hybrid signature envelope — LEGACY. Carries an Ed25519 signature
155
+ * (classical) and an ML-DSA-65 signature (post-quantum), both computed over
156
+ * the bare canonical bytes with NO payload-type binding. `verifySignature`
157
+ * in this package requires BOTH to be valid before returning `valid: true`.
158
+ *
159
+ * DEPRECATED in favour of the DSSE `AttestationEnvelope` (the L2 attestation
160
+ * layer). The bare-bytes form has no `payloadType` binding, which allows a
161
+ * signature minted for one object type to be replayed against a different
162
+ * type whose canonical bytes match (SRAID F7 / Adversary A4). New objects
163
+ * SHOULD carry an `attestation` (DSSE) envelope; `SignatureEnvelope` is
164
+ * retained only so existing callers and stored objects keep verifying during
165
+ * the migration.
166
+ *
167
+ * Encoded as base64 (standard, with `=` padding). `signer_kid` is a
168
+ * stable string that identifies which keypair produced the signatures.
169
+ */
170
+ export interface SignatureEnvelope {
171
+ /** Base64-encoded 64-byte Ed25519 signature. */
172
+ ed25519: string
173
+ /** Base64-encoded ML-DSA-65 signature. */
174
+ ml_dsa_65: string
175
+ /** Key ID — opaque string identifying which keypair signed. */
176
+ signer_kid: string
177
+ }
178
+
179
+ // ── L2 attestation (DSSE) ──────────────────────────────────────────────────────
180
+
181
+ /**
182
+ * One signature entry inside a DSSE `AttestationEnvelope`. The signature is
183
+ * computed over the envelope's PAE (Pre-Authentication Encoding) — see
184
+ * `pae()` / `verifyAttestation()` in attestation.ts — so the `payloadType`
185
+ * is structurally bound into what each signature covers.
186
+ */
187
+ export interface AttestationSignature {
188
+ /**
189
+ * Algorithm identifier. SynOI requires both `'ed25519'` and `'ml-dsa-65'`
190
+ * to be present (the hybrid AND policy). Other algs are ignored by the
191
+ * SynOI verifier but permitted in the array (DSSE is forward-compatible).
192
+ */
193
+ alg: 'ed25519' | 'ml-dsa-65' | (string & {})
194
+ /** Base64-encoded signature bytes over the PAE. */
195
+ sig: string
196
+ /** Optional key ID — opaque string identifying which keypair signed. */
197
+ keyid?: string
198
+ }
199
+
200
+ /**
201
+ * L2 ATTESTATION — a DSSE (Dead Simple Signing Envelope), JSON profile.
202
+ *
203
+ * Replaces the legacy `SignatureEnvelope`. Each signature in `signatures[]`
204
+ * covers `PAE(payloadType, payload)` rather than the bare payload bytes, so
205
+ * the payload TYPE is bound into the signed bytes — closing the cross-type
206
+ * confusion gap (SRAID F7 / Adversary A4). The hybrid both-required rule is
207
+ * preserved: a conformant SRAID attestation MUST carry both an `ed25519` and
208
+ * an `ml-dsa-65` entry over the same PAE, and `verifyAttestation` requires
209
+ * both to verify.
210
+ *
211
+ * `payload` is the canonical UTF-8 string of the content core (the same
212
+ * bytes `canonicalize()` produces and `oidOf` hashes). Because the envelope
213
+ * is detached (it is the CDRO's `attestation` field, excluded from the OID
214
+ * hash), adding/rotating a signature never changes the OID.
215
+ *
216
+ * A future CBOR profile would use COSE (RFC 9052); it is reserved, not
217
+ * implemented in this package.
218
+ */
219
+ export interface AttestationEnvelope {
220
+ /**
221
+ * The payload media type, bound into the PAE. SynOI objects use
222
+ * `application/vnd.synoi.sraid+json`; Hlif supply-chain attestations use
223
+ * `application/vnd.in-toto+json` for in-toto/SLSA/Sigstore interop.
224
+ */
225
+ payloadType: string
226
+ /** Canonical UTF-8 payload string (the signed content core). */
227
+ payload: string
228
+ /** Detached signatures, each over `PAE(payloadType, payload)`. */
229
+ signatures: AttestationSignature[]
230
+ }
231
+
232
+ /**
233
+ * CDRO — Canonical Data Record Object.
234
+ *
235
+ * The base shape every signed CDRO object takes. The `oid` field is
236
+ * `sha256:` + hex(sha256(canonicalize(body))) where the body is the CDRO
237
+ * minus the `oid` and `signature` fields.
238
+ *
239
+ * Mirrors `GapCdroEnvelope` in synoi-gateway so a GAP object IS a CDRO.
240
+ * Higher-layer packages narrow `body` with their own type.
241
+ */
242
+ export interface CDRO<TBody = unknown> {
243
+ /** "sha256:" + hex(sha256(canonicalize(cdro_minus_oid_and_signature))). */
244
+ oid: string
245
+ /** Object type discriminator (e.g. "gap:capability_grant"). */
246
+ type: string
247
+ /** SRAID protocol version. v2 is the only defined version today. */
248
+ sraid_version: '2.0'
249
+ /** Tenant that owns this object. */
250
+ tenant_id: string
251
+ /** Unix ms timestamp of creation. */
252
+ created_at_ms: number
253
+ /** OID of the actor (skill, user, device, …) that created the object. */
254
+ created_by: string
255
+ /** Body — type-specific payload. Higher layers narrow this. */
256
+ body: TBody
257
+ /**
258
+ * Optional L4 authority block — the authorized axis (§3.5). When present
259
+ * it is HASHED INTO THE OID (it is part of the content core), so it cannot
260
+ * be stripped without changing identity. See `AuthorityBlock` and the
261
+ * `cdroOid` helper in oid.ts.
262
+ */
263
+ authority?: AuthorityBlock
264
+ /**
265
+ * Optional L4 propagating sensitivity tier — a COARSE, OPAQUE level
266
+ * (`s0`..`s4`, lowest-to-highest), NOT a literal content category (§3.4,
267
+ * SRAID_FOUNDATION_PUNCHLIST A5). It is OPAQUE on purpose: SPEC §7 forbids
268
+ * leaking the nature of an encrypted `body` via the public, signed
269
+ * envelope, so a literal label like `"phi"`/`"health"` is NOT allowed here;
270
+ * the regulatory-category → tier mapping is private higher-layer policy.
271
+ *
272
+ * HASHED INTO THE OID (it is a content-core field, see `cdroContentCore` in
273
+ * oid.ts), so the tier cannot be silently stripped or downgraded without
274
+ * changing identity and invalidating the signature. Carry-forward is
275
+ * monotone (`max`): a consolidated/summarized object inherits the HIGHEST
276
+ * tier among its sources — see `sensitivityCarryForward` in sensitivity.ts
277
+ * and the Vault consolidation path. Absent means "unclassified" (the floor,
278
+ * `s0`); it does not assert low sensitivity, only that none was declared.
279
+ */
280
+ sensitivity?: SensitivityTier
281
+ /**
282
+ * Optional L3 lineage — the Merkle edge to the immediately superseded
283
+ * version (null/absent at a root object). HASHED INTO THE OID (it is part
284
+ * of the content core), so the predecessor link is identity-bound: a node's
285
+ * OID transitively commits its predecessor's OID and therefore its whole
286
+ * reachable history. This is the unified, witnessed replacement for the
287
+ * legacy self-asserted `supersedes` string — see `lineageLinks` /
288
+ * `latestWins` in lineage.ts for the latest-wins / monotone rule.
289
+ */
290
+ prev?: string
291
+ /**
292
+ * Optional L3 lineage — typed Merkle-DAG edges (§3.3). HASHED INTO THE OID
293
+ * (content core), so every edge is identity-bound and tamper-evident. The
294
+ * standalone SRO and the self-asserted `supersedes` pointer both become
295
+ * expressible here as `{ rel: 'supersedes', oid }` edges, unifying the
296
+ * three legacy supersession mechanisms onto one model.
297
+ */
298
+ links?: LineageLink[]
299
+ /**
300
+ * Legacy supersession pointer — OID of the previous version. RETAINED for
301
+ * back-compat. It is now routed through the unified lineage model: treat it
302
+ * as equivalent to `prev` (and to a `{ rel: 'supersedes' }` link). New
303
+ * objects SHOULD set `prev`/`links` instead; `lineageLinks` in lineage.ts
304
+ * coalesces all three so a verifier sees one set of typed edges. Still
305
+ * hashed into the OID (it is a content-core field).
306
+ */
307
+ supersedes?: string
308
+ /**
309
+ * Optional L2 attestation — the DSSE envelope (the preferred signing path).
310
+ * Carries `payloadType` + `signatures[]`, each over `PAE(payloadType,
311
+ * payload)`. Detached: NOT part of the OID hash (see `cdroContentCore` in
312
+ * oid.ts), so adding/rotating a signature never changes identity.
313
+ */
314
+ attestation?: AttestationEnvelope
315
+ /**
316
+ * Optional LEGACY signature envelope (bare-bytes, no payload-type binding).
317
+ * Retained for back-compat with objects/callers minted before the DSSE
318
+ * `attestation` envelope. New objects SHOULD use `attestation`. May be
319
+ * absent on draft / unsigned objects.
320
+ */
321
+ signature?: SignatureEnvelope
322
+ }
323
+
324
+ // ── SRO ───────────────────────────────────────────────────────────────────────
325
+
326
+ /**
327
+ * SRO — Mutation / Supersession Record.
328
+ *
329
+ * Links a successor CDRO to its predecessor and the actor that authorized
330
+ * the supersession. SROs are themselves CDRO objects (so they're CDROs with
331
+ * an `oid` and `signature`), but they have a specific body shape.
332
+ *
333
+ * Where CDROs are the "what" of the system, SROs are the "what changed."
334
+ * They form an append-only chain: every supersedes pointer in a CDRO is
335
+ * accompanied by an SRO that explains and witnesses the change.
336
+ */
337
+ export interface SROBody {
338
+ /** OID of the predecessor object being superseded. */
339
+ predecessor_oid: string
340
+ /** OID of the successor object that supersedes it. */
341
+ successor_oid: string
342
+ /** Why the predecessor was superseded. Free-form short string. */
343
+ reason: string
344
+ /** OID of the actor that authorized this mutation. */
345
+ authorized_by: string
346
+ /** Optional list of supporting evidence OIDs (audit trail). */
347
+ evidence_oids?: string[]
348
+ }
349
+
350
+ /** SRO is just a CDRO with a fixed body type. */
351
+ export type SRO = CDRO<SROBody> & { type: 'sraid:sro' }