@metalabel/dfos-protocol 0.1.0 → 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.
package/PROTOCOL.md CHANGED
@@ -20,11 +20,12 @@ The protocol is not coupled to the DFOS platform. Any system implementing the sa
20
20
 
21
21
  ## Protocol Overview
22
22
 
23
- The DFOS protocol has four components:
23
+ The DFOS protocol has five components:
24
24
 
25
25
  | Component | Concern |
26
26
  | --------------------- | ---------------------------------------------------------------------------- |
27
27
  | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
28
+ | **Credentials** | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
28
29
  | **Beacons** | Signed merkle root announcements — periodic commitment over content sets |
29
30
  | **Countersignatures** | Witness attestation — third-party signatures over existing chain operations |
30
31
  | **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots |
@@ -92,7 +93,7 @@ This is a self-sovereign invariant: the identity chain defines its own valid sig
92
93
 
93
94
  Content chain verification requires a **valid EdDSA signature** and delegates key resolution to the caller. The `kid` in each operation's JWS header is a DID URL (`did:dfos:<id>#<keyId>`). The verifier calls `resolveKey(kid)` to obtain the raw Ed25519 public key bytes for that key on that identity. How the resolver obtains and validates the identity's key state is application-defined.
94
95
 
95
- Identity chains are self-sovereign they define their own valid signers via `controllerKeys`. Content chains are externally signed a content chain with operations signed by multiple different identities is valid at the protocol level, as long as each signature verifies against the resolved key.
96
+ **Creator sovereignty**: The DID that signs the genesis (create) operation is the **chain creator** and permanently owns the chain. The creator can sign subsequent operations directly no credential needed. Other DIDs require a **DFOSContentWrite** VC-JWT credential in the operation's `authorization` field, issued by the creator DID. See [Credentials](#credentials) for the VC-JWT format.
96
97
 
97
98
  **Signer-payload consistency**: The `kid` DID in the JWS header MUST match the `did` field in the content operation payload. This enables discrimination between author operations and countersignatures — if the kid DID differs from the payload `did`, it is a countersignature (witness attestation), not a chain operation.
98
99
 
@@ -101,13 +102,12 @@ Identity chains are self-sovereign — they define their own valid signers via `
101
102
  - The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)`
102
103
  - Chain integrity (CID links, timestamp ordering, terminal state)
103
104
  - The `kid` DID matches the payload `did` for chain operations
105
+ - Creator-sovereignty authorization (when `enforceAuthorization` is enabled): non-creator signers must present a valid DFOSContentWrite VC-JWT issued by the creator
104
106
 
105
107
  **What the protocol does NOT enforce (application concerns):**
106
108
 
107
- - Which identities are authorized to sign operations on a given chain
108
109
  - Which key role (auth, assert, controller) the signing key must have
109
- - Whether a chain must have a single signer or may have multiple signers
110
- - Ownership or attribution semantics between signers and content chains
110
+ - Ownership or attribution semantics beyond creator sovereignty
111
111
 
112
112
  ### Terminal States and Special Operations
113
113
 
@@ -121,14 +121,15 @@ Identity chains are self-sovereign — they define their own valid signers via `
121
121
 
122
122
  The JWS `typ` header uses protocol-specific values (not IANA media types):
123
123
 
124
- | `typ` value | Usage |
125
- | ---------------------- | ------------------------- |
126
- | `did:dfos:identity-op` | Identity chain operations |
127
- | `did:dfos:content-op` | Content chain operations |
128
- | `did:dfos:beacon` | Beacon announcements |
129
- | `JWT` | Device auth tokens |
124
+ | `typ` value | Usage |
125
+ | ---------------------- | --------------------------------------------- |
126
+ | `did:dfos:identity-op` | Identity chain operations |
127
+ | `did:dfos:content-op` | Content chain operations |
128
+ | `did:dfos:beacon` | Beacon announcements |
129
+ | `JWT` | Auth tokens (DID-signed relay authentication) |
130
+ | `vc+jwt` | VC-JWT credentials (W3C VC Data Model v2) |
130
131
 
131
- These are non-standard per JOSE convention, documented intentionally. The `typ` header aids routing but is not security-critical. Implementations SHOULD validate it but MUST NOT rely on it for security decisions.
132
+ Protocol-specific `typ` values are non-standard per JOSE convention, documented intentionally. `JWT` and `vc+jwt` follow IANA conventions. The `typ` header aids routing but is not security-critical. Implementations SHOULD validate it but MUST NOT rely on it for security decisions.
132
133
 
133
134
  ### Operation Field Limits
134
135
 
@@ -303,14 +304,16 @@ DID: did:dfos:e3vvtck42d4eacdnzvtrn6
303
304
  documentCID: string | null,
304
305
  baseDocumentCID: string | null,
305
306
  createdAt: string,
306
- note: string | null }
307
+ note: string | null,
308
+ authorization?: string } // VC-JWT for delegated operations
307
309
 
308
310
  // Permanent destruction
309
311
  { version: 1, type: "delete",
310
312
  did: string, // author DID
311
313
  previousOperationCID: string,
312
314
  createdAt: string,
313
- note: string | null }
315
+ note: string | null,
316
+ authorization?: string } // VC-JWT for delegated operations
314
317
  ```
315
318
 
316
319
  ### MultikeyPublicKey
@@ -366,7 +369,7 @@ Every operation JWS (identity-op and content-op) includes a `cid` field in the p
366
369
 
367
370
  A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations.
368
371
 
369
- Note: JWT tokens (device auth) do NOT include a `cid` header — this field is specific to operation JWS tokens.
372
+ Note: JWT auth tokens and VC-JWT credentials do NOT include a `cid` header — this field is specific to operation JWS tokens and beacons.
370
373
 
371
374
  ### CID Derivation
372
375
 
@@ -386,6 +389,122 @@ Where `idEncode` is the 19-char alphabet encoding described above.
386
389
 
387
390
  ---
388
391
 
392
+ ## Credentials
393
+
394
+ Two credential types handle authentication and authorization. Both are DID-signed JWTs using Ed25519 (`alg: "EdDSA"`).
395
+
396
+ ### Auth Tokens (Relay Authentication)
397
+
398
+ A DID-signed JWT proving the caller controls a DID. Short-lived, scoped to a specific relay via the `aud` (audience) claim. Used for relay AuthN — establishing identity before making requests.
399
+
400
+ **JWT Header:**
401
+
402
+ ```json
403
+ {
404
+ "alg": "EdDSA",
405
+ "typ": "JWT",
406
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
407
+ }
408
+ ```
409
+
410
+ **JWT Payload:**
411
+
412
+ ```json
413
+ {
414
+ "iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
415
+ "sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
416
+ "aud": "relay.example.com",
417
+ "exp": 1772845200,
418
+ "iat": 1772841600
419
+ }
420
+ ```
421
+
422
+ | Field | Type | Description |
423
+ | ----- | ------ | ---------------------------------------------------------- |
424
+ | `iss` | string | DID proving identity (the signer) |
425
+ | `sub` | string | Same as `iss` for auth tokens |
426
+ | `aud` | string | Target relay hostname (prevents cross-relay replay) |
427
+ | `exp` | number | Expiration — unix seconds (short-lived, typically minutes) |
428
+ | `iat` | number | Issued-at — unix seconds |
429
+
430
+ **Verification:** Standard JWT verification — EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time), audience match. The `kid` MUST be a DID URL (`did:dfos:xxx#key_yyy`) and the `kid` DID MUST match `iss`.
431
+
432
+ Auth tokens do NOT include a `cid` header — they are ephemeral session tokens, not content-addressed artifacts.
433
+
434
+ ### VC-JWT Credentials (Authorization)
435
+
436
+ W3C Verifiable Credential Data Model v2 credentials encoded as JWT (`typ: "vc+jwt"`). Two credential types:
437
+
438
+ | Credential Type | Purpose |
439
+ | ------------------ | ------------------------------------------------------------ |
440
+ | `DFOSContentWrite` | Authorize extending a content chain (embedded in operations) |
441
+ | `DFOSContentRead` | Authorize reading content plane data (presented to relay) |
442
+
443
+ **VC-JWT Header:**
444
+
445
+ ```json
446
+ {
447
+ "alg": "EdDSA",
448
+ "typ": "vc+jwt",
449
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8"
450
+ }
451
+ ```
452
+
453
+ **VC-JWT Payload:**
454
+
455
+ ```json
456
+ {
457
+ "iss": "did:dfos:e3vvtck42d4eacdnzvtrn6",
458
+ "sub": "did:dfos:nzkf838efr424433rn2rzk",
459
+ "exp": 1798761600,
460
+ "iat": 1772841600,
461
+ "vc": {
462
+ "@context": ["https://www.w3.org/ns/credentials/v2"],
463
+ "type": ["VerifiableCredential", "DFOSContentWrite"],
464
+ "credentialSubject": {}
465
+ }
466
+ }
467
+ ```
468
+
469
+ | Field | Type | Description |
470
+ | ---------------------- | -------- | -------------------------------------------------------- |
471
+ | `iss` | string | DID granting the credential (content creator/controller) |
472
+ | `sub` | string | DID receiving the credential (collaborator/reader) |
473
+ | `exp` | number | Expiration — unix seconds |
474
+ | `iat` | number | Issued-at — unix seconds |
475
+ | `vc.@context` | string[] | Must be `["https://www.w3.org/ns/credentials/v2"]` |
476
+ | `vc.type` | string[] | `["VerifiableCredential", "<DFOSCredentialType>"]` |
477
+ | `vc.credentialSubject` | object | Optional narrowing — see scope narrowing below |
478
+
479
+ **Scope narrowing:** The `credentialSubject` object may contain a `contentId` field. If absent, the credential grants broad access to all content by the issuer. If present, the credential is narrowed to the specific content chain.
480
+
481
+ ```json
482
+ // Broad — all content by this DID
483
+ { "credentialSubject": {} }
484
+
485
+ // Narrow — specific content chain only
486
+ { "credentialSubject": { "contentId": "a82z92a3hndk6c97thcrn8" } }
487
+ ```
488
+
489
+ **Verification:** EdDSA signature check, temporal validity (`iat` must not be in the future, `exp` must be after current time — using operation `createdAt` for chain-embedded VCs, wall clock for relay-presented VCs), `kid` DID URL format, `kid` DID matches `iss`, payload structure via Zod schema. Optionally verify `sub` and credential type match expectations.
490
+
491
+ ### Content Chain Authorization
492
+
493
+ When `enforceAuthorization` is enabled on content chain verification:
494
+
495
+ 1. **Genesis operation**: The signer is the chain creator, always authorized
496
+ 2. **Creator signs subsequent ops**: Authorized directly — no credential needed
497
+ 3. **Different DID signs**: Must include an `authorization` field containing a valid `DFOSContentWrite` VC-JWT where:
498
+ - `iss` matches the chain creator DID
499
+ - `sub` matches the signing DID
500
+ - The credential is temporally valid (`iat <= op.createdAt < exp`, not wall clock)
501
+ - If `contentId` is present in `credentialSubject`, it must match this chain's contentId
502
+ - The credential type is `DFOSContentWrite`
503
+
504
+ The `authorization` field is available on `update` and `delete` content operations. It is absent for creator-signed operations.
505
+
506
+ ---
507
+
389
508
  ## Beacons
390
509
 
391
510
  A beacon is a signed announcement of a merkle root — a periodic commitment over a set of content IDs. Beacons are floating signed artifacts, not chained. They provide a compact, verifiable snapshot of an identity's content set at a point in time.
@@ -397,8 +516,8 @@ A beacon is a signed announcement of a merkle root — a periodic commitment ove
397
516
  "version": 1,
398
517
  "type": "beacon",
399
518
  "did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
400
- "merkleRoot": "a3f8b2c1d4e5f6071829304a5b6c7d8e9f0a1b2c3d4e5f6071829304a5b6c7d8",
401
- "createdAt": "2026-03-07T00:04:00.000Z"
519
+ "merkleRoot": "7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e",
520
+ "createdAt": "2026-03-07T00:05:00.000Z"
402
521
  }
403
522
  ```
404
523
 
@@ -416,11 +535,41 @@ A beacon is a signed announcement of a merkle root — a periodic commitment ove
416
535
  {
417
536
  "alg": "EdDSA",
418
537
  "typ": "did:dfos:beacon",
419
- "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd",
420
- "cid": "bafyrei..."
538
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8",
539
+ "cid": "bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu"
421
540
  }
422
541
  ```
423
542
 
543
+ ### Worked Example: Beacon
544
+
545
+ Using the reference identity (`did:dfos:e3vvtck42d4eacdnzvtrn6`) and key 1 from the identity chain examples. The beacon commits to a merkle root over 5 content IDs (see Merkle Tree worked example below).
546
+
547
+ **Beacon CID** (dag-cbor canonical encode → CIDv1):
548
+
549
+ ```
550
+ bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
551
+ ```
552
+
553
+ **Controller JWS** (key 1 signs):
554
+
555
+ ```
556
+ kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
557
+ typ: did:dfos:beacon
558
+ cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
559
+ ```
560
+
561
+ **Witness countersignature** (key 2 signs the same payload — same CID, different kid):
562
+
563
+ ```
564
+ kid: did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
565
+ typ: did:dfos:beacon
566
+ cid: bafyreihholuui7s7ns74iem6ahfxsb472hwogbqd32yrrp5fztc3kxa5qu
567
+ ```
568
+
569
+ Both JWS tokens commit to identical bytes (same CID). The controller/witness distinction is determined at verification time by comparing the `kid` DID to the payload `did`.
570
+
571
+ Full JWS tokens are in [`examples/beacon.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/beacon.json).
572
+
424
573
  ### Beacon Semantics
425
574
 
426
575
  Beacons are not chained — there is no `previousOperationCID`. For a given DID, the latest beacon with a strictly-greater `createdAt` timestamp wins. Beacons replace, not accumulate.
@@ -445,10 +594,84 @@ Beacons commit to a set of content IDs via a pure SHA-256 binary Merkle tree. Th
445
594
 
446
595
  An empty set of content IDs produces a `null` root. A single content ID produces a root equal to `hex(SHA-256(UTF-8(contentId)))`.
447
596
 
597
+ ### Worked Example: Merkle Tree
598
+
599
+ 5 content IDs: `["alpha", "bravo", "charlie", "delta", "echo"]`
600
+
601
+ Already sorted lexicographically. Hash each leaf:
602
+
603
+ ```
604
+ alpha → SHA-256("alpha") → 8ed3f6ad685b959ead7022518e1af76cd816f8e8ec7ccdda1ed4018e8f2223f8
605
+ bravo → SHA-256("bravo") → 4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398
606
+ charlie → SHA-256("charlie") → 36ef585cd42d49706cd2827a77d86c91bfdaf87a3f22b8f0e0308bd2c16cf85f
607
+ delta → SHA-256("delta") → 18ac3e7343f016890c510e93f935261169d9e3f565436429830faf0934f4f8e4
608
+ echo → SHA-256("echo") → 092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d
609
+ ```
610
+
611
+ Build tree bottom-up, pairing left-to-right. Odd nodes promote unpaired:
612
+
613
+ ```
614
+ Level 0 (leaves): [alpha] [bravo] [charlie] [delta] [echo]
615
+ Level 1: [alpha‖bravo] [charlie‖delta] [echo] ← promoted
616
+ Level 2: [L1-left‖L1-mid] [echo] ← promoted
617
+ Level 3 (root): [L2-left‖echo]
618
+ ```
619
+
620
+ Interior hashes:
621
+
622
+ ```
623
+ SHA-256(alpha‖bravo) → 90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3
624
+ SHA-256(charlie‖delta) → 6b55e77bef32937d9ccce2bd4b18127b0483f0be8e5b63c30bcc2b0d09f7dd44
625
+ SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb862e3b6a86eb2dfa0ea8ba0edcf1c3f3b8f14abc5eb9d72eab2edc2f7
626
+ ```
627
+
628
+ **Root** (level 3):
629
+
630
+ ```
631
+ SHA-256(23c83c...edc2f7 ‖ 092c79...f7431d) → 7e80d4780f454e0fca0b090d8c646f572b49354f54154531606105aad2fda28e
632
+ ```
633
+
448
634
  ### Inclusion Proofs
449
635
 
450
636
  A Merkle inclusion proof demonstrates that a specific content ID is part of the committed set without revealing the full set. The proof consists of sibling hashes along the path from leaf to root, plus a direction (left/right) for each step.
451
637
 
638
+ ### Worked Example: Inclusion Proof for "charlie"
639
+
640
+ Starting from the leaf hash of "charlie" (`36ef58...`), walk to the root using sibling hashes:
641
+
642
+ ```
643
+ Step 1: charlie (index 2) paired with delta (index 3)
644
+ sibling: 4f4a9410...045398 (delta leaf) position: right
645
+ → SHA-256(charlie ‖ delta) → 6b55e77b...f7dd44
646
+
647
+ Step 2: charlie‖delta paired with alpha‖bravo
648
+ sibling: 90d39555...a9b4f3 (alpha‖bravo) position: left
649
+ → SHA-256(alpha‖bravo ‖ charlie‖delta) → 23c83cb8...edc2f7
650
+
651
+ Step 3: L2-left paired with echo (promoted)
652
+ sibling: 092c79e8...f7431d (echo leaf) position: right
653
+ → SHA-256(L2-left ‖ echo) → 7e80d478...fda28e ✓ matches root
654
+ ```
655
+
656
+ Proof path (from [`examples/merkle-tree.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/merkle-tree.json)):
657
+
658
+ ```json
659
+ [
660
+ {
661
+ "hash": "4f4a9410ffcdf895c4adb880659e9b5c0dd1f23a30790684340b3eaacb045398",
662
+ "position": "right"
663
+ },
664
+ {
665
+ "hash": "90d39555bb3c223e12f5a375c3011d2462fe2e1e36b8416a0b623d5831a9b4f3",
666
+ "position": "left"
667
+ },
668
+ {
669
+ "hash": "092c79e8f80e559e404bcf660c48f3522b67aba9ff1484b0367e1a4ddef7431d",
670
+ "position": "right"
671
+ }
672
+ ]
673
+ ```
674
+
452
675
  ---
453
676
 
454
677
  ## Countersignatures
@@ -489,13 +712,14 @@ Countersignatures are not part of the chain — they do not have `previousOperat
489
712
  ### Content Chain
490
713
 
491
714
  1. Decode each JWS, parse payload as ContentOperation
492
- 2. First op must be `type: "create"`
715
+ 2. First op must be `type: "create"` — the signer is the chain creator
493
716
  3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
494
717
  4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
495
718
  5. Verify the `kid` DID matches the payload `did` field (mismatches indicate a countersignature, not a chain operation)
496
719
  6. Resolve `kid` via external key resolver (caller provides)
497
720
  7. Verify EdDSA JWS signature
498
- 8. Apply state change (set document, clear, or delete)
721
+ 8. If `enforceAuthorization` is enabled and the signer DID differs from the chain creator: verify the `authorization` field contains a valid `DFOSContentWrite` VC-JWT issued by the creator DID, with `sub` matching the signer, not expired at `op.createdAt`, and `contentId` (if present) matching this chain
722
+ 9. Apply state change (set document, clear, or delete)
499
723
 
500
724
  ---
501
725
 
@@ -813,14 +1037,19 @@ Given the artifacts above, verify:
813
1037
 
814
1038
  11. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
815
1039
 
1040
+ 12. **VC-JWT credential verify**: using the issuer's public key, verify a `DFOSContentWrite` or `DFOSContentRead` credential: check EdDSA signature, `typ: "vc+jwt"`, expiration, `kid` DID URL format, `kid` DID matches `iss`, `vc` claim structure matches W3C VC Data Model v2, credential type matches expected DFOS type. Test vectors in [`examples/credential-write.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-write.json) and [`examples/credential-read.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/credential-read.json).
1041
+
1042
+ 13. **Delegated content chain verify**: using [`examples/content-delegated.json`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/examples/content-delegated.json), verify a content chain where the genesis is signed by the creator and a subsequent update is signed by a delegate with an embedded `DFOSContentWrite` VC-JWT in the `authorization` field. The VC must be issued by the creator DID, with `sub` matching the delegate DID.
1043
+
816
1044
  ---
817
1045
 
818
1046
  ## Source and Verification
819
1047
 
820
- All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 235 checks across 5 languages.
1048
+ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 293 checks across 5 languages.
821
1049
 
822
1050
  - [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
823
1051
  - [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
1052
+ - [`crypto/jwt`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jwt.ts) — `createJwt`, `verifyJwt`
824
1053
  - [`crypto/base64url`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/base64url.ts) — `base64urlEncode`, `base64urlDecode`
825
1054
  - [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual`
826
1055
  - [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId`
@@ -831,6 +1060,9 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
831
1060
  - [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
832
1061
  - [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon`
833
1062
  - [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature`
1063
+ - [`credentials/auth-token`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/auth-token.ts) — `createAuthToken`, `verifyAuthToken`
1064
+ - [`credentials/credential`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/credential.ts) — `createCredential`, `verifyCredential`, `decodeCredentialUnsafe`
1065
+ - [`credentials/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/credentials/schemas.ts) — `AuthTokenClaims`, `CredentialClaims`, `VCClaim`, `DFOSCredentialType`
834
1066
  - [`merkle/tree`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/tree.ts) — `buildMerkleTree`, `hashLeaf`
835
1067
  - [`merkle/proof`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/proof.ts) — `generateMerkleProof`, `verifyMerkleProof`
836
1068
 
@@ -843,11 +1075,11 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
843
1075
 
844
1076
  | Language | Tests | Source |
845
1077
  | ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
846
- | TypeScript | 149 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
847
- | Python | 48 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
848
- | Go | 13 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
849
- | Rust | 13 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
850
- | Swift | 12 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
1078
+ | TypeScript | 190 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
1079
+ | Python | 59 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
1080
+ | Go | 15 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
1081
+ | Rust | 15 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
1082
+ | Swift | 14 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
851
1083
 
852
1084
  ---
853
1085
 
package/README.md CHANGED
@@ -13,6 +13,8 @@ npm install @metalabel/dfos-protocol
13
13
  ```ts
14
14
  // Chain verification
15
15
  import { verifyContentChain, verifyIdentityChain } from '@metalabel/dfos-protocol/chain';
16
+ // Credentials (auth tokens + VC-JWT)
17
+ import { createAuthToken, verifyCredential } from '@metalabel/dfos-protocol/credentials';
16
18
  // Crypto primitives
17
19
  import { createJws, dagCborCanonicalEncode, verifyJws } from '@metalabel/dfos-protocol/crypto';
18
20
  // Merkle trees
@@ -21,11 +23,12 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
21
23
 
22
24
  ## Subpath Exports
23
25
 
24
- | Export | Description |
25
- | --------------------------------- | ----------------------------------------------------------------------- |
26
- | `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, beacons, countersigns |
27
- | `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
28
- | `@metalabel/dfos-protocol/merkle` | SHA-256 binary merkle tree, inclusion proofs |
26
+ | Export | Description |
27
+ | -------------------------------------- | ----------------------------------------------------------------------- |
28
+ | `@metalabel/dfos-protocol/chain` | Identity and content chain signing, verification, beacons, countersigns |
29
+ | `@metalabel/dfos-protocol/credentials` | Auth tokens (DID-signed JWT) and VC-JWT credentials for authorization |
30
+ | `@metalabel/dfos-protocol/crypto` | Ed25519, JWS, JWT, dag-cbor, base64url, ID generation |
31
+ | `@metalabel/dfos-protocol/merkle` | SHA-256 binary merkle tree, inclusion proofs |
29
32
 
30
33
  ## Specifications
31
34
 
@@ -37,13 +40,18 @@ import { buildMerkleTree, verifyMerkleProof } from '@metalabel/dfos-protocol/mer
37
40
 
38
41
  ## Examples
39
42
 
40
- The `examples/` directory contains deterministic reference chain fixtures that can be independently verified by any Ed25519 + dag-cbor implementation:
43
+ The `examples/` directory contains deterministic reference fixtures that can be independently verified by any Ed25519 + dag-cbor implementation:
41
44
 
42
45
  - `identity-genesis.json` — single create operation
43
46
  - `identity-rotation.json` — genesis + key rotation
44
47
  - `identity-delete.json` — genesis + delete (terminal)
45
48
  - `content-lifecycle.json` — create + update (with both documents)
46
49
  - `content-delete.json` — create + delete
50
+ - `content-delegated.json` — creator genesis + delegated update with DFOSContentWrite VC-JWT
51
+ - `credential-write.json` — DFOSContentWrite VC-JWT (broad + content-narrowed)
52
+ - `credential-read.json` — DFOSContentRead VC-JWT
53
+ - `merkle-tree.json` — 5 content IDs → sorted tree → root, with inclusion proof
54
+ - `beacon.json` — signed merkle root announcement with witness countersignature
47
55
 
48
56
  ## License
49
57
 
@@ -91,6 +91,8 @@ declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
91
91
  baseDocumentCID: z.ZodNullable<z.ZodString>;
92
92
  createdAt: z.ZodISODateTime;
93
93
  note: z.ZodNullable<z.ZodString>;
94
+ /** VC-JWT authorizing this operation when signer is not the chain creator */
95
+ authorization: z.ZodOptional<z.ZodString>;
94
96
  }, z.core.$strict>, z.ZodObject<{
95
97
  version: z.ZodLiteral<1>;
96
98
  type: z.ZodLiteral<"delete">;
@@ -98,6 +100,8 @@ declare const ContentOperation: z.ZodDiscriminatedUnion<[z.ZodObject<{
98
100
  previousOperationCID: z.ZodString;
99
101
  createdAt: z.ZodISODateTime;
100
102
  note: z.ZodNullable<z.ZodString>;
103
+ /** VC-JWT authorizing this operation when signer is not the chain creator */
104
+ authorization: z.ZodOptional<z.ZodString>;
101
105
  }, z.core.$strict>], "type">;
102
106
  type ContentOperation = z.infer<typeof ContentOperation>;
103
107
  /** Beacon: floating signed merkle root announcement */
@@ -182,6 +186,8 @@ interface VerifiedContentChain {
182
186
  currentDocumentCID: string | null;
183
187
  /** Number of operations in the chain */
184
188
  length: number;
189
+ /** The DID that created the chain (signer of genesis operation) */
190
+ creatorDID: string;
185
191
  }
186
192
  /**
187
193
  * Sign a content chain operation as a JWS and derive the operation CID
@@ -196,15 +202,30 @@ declare const signContentOperation: (input: {
196
202
  operationCID: string;
197
203
  }>;
198
204
  /**
199
- * Verify a content chain's structural integrity and signatures
205
+ * Verify a content chain's structural integrity, signatures, and authorization
200
206
  *
201
207
  * The caller provides a key resolver to look up public keys from kid values.
202
208
  * This keeps the content chain protocol independent of identity resolution.
209
+ *
210
+ * Authorization rules:
211
+ * - Genesis (create) operation: the signer is the chain creator, always authorized
212
+ * - Subsequent operations signed by the creator DID: authorized (no credential needed)
213
+ * - Subsequent operations signed by a different DID: must include an `authorization`
214
+ * field containing a valid DFOSContentWrite VC-JWT issued by the creator DID
203
215
  */
204
216
  declare const verifyContentChain: (input: {
205
217
  log: string[];
206
218
  /** Resolve a kid (DID URL) to the raw Ed25519 public key bytes */
207
219
  resolveKey: (kid: string) => Promise<Uint8Array>;
220
+ /**
221
+ * Enforce creator-sovereignty authorization. When true, non-creator signers
222
+ * must include a DFOSContentWrite VC-JWT in the operation's `authorization`
223
+ * field. When false (default), any signer with a valid signature is accepted.
224
+ *
225
+ * Web relays should set this to true. Applications migrating to VC-based
226
+ * authorization can enable this once all chains include authorization fields.
227
+ */
228
+ enforceAuthorization?: boolean;
208
229
  }) => Promise<VerifiedContentChain>;
209
230
 
210
231
  /**
@@ -19,7 +19,8 @@ import {
19
19
  verifyContentChain,
20
20
  verifyCountersignature,
21
21
  verifyIdentityChain
22
- } from "../chunk-ASGEXSVT.js";
22
+ } from "../chunk-GEVJ3SEV.js";
23
+ import "../chunk-CZSEEZLL.js";
23
24
  import "../chunk-ZXXP5W5N.js";
24
25
  export {
25
26
  BeaconPayload,