@metalabel/dfos-protocol 0.0.3 → 0.1.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/CONTENT-MODEL.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # DFOS Content Model
2
2
 
3
- Standard content schemas for documents committed to DFOS content chains. JSON Schema (draft 2020-12) definitions for what goes inside the document envelope's `content` field.
3
+ Standard content schemas for documents committed to DFOS content chains. JSON Schema (draft 2020-12) definitions for content objects committed directly by CID.
4
4
 
5
- These schemas are conventions, not protocol requirements. The DFOS Protocol commits to documents by CID without inspecting their contents — any valid JSON object with a `$schema` field can be committed. The content model defines the vocabulary that DFOS uses internally, provided as a starting point for applications built on the protocol.
5
+ These schemas are conventions, not protocol requirements. The DFOS Protocol commits to content objects by CID without inspecting their contents — any valid JSON object with a `$schema` field can be committed. The content model defines the vocabulary that DFOS uses internally, provided as a starting point for applications built on the protocol.
6
6
 
7
7
  [Protocol Specification](https://protocol.dfos.com/spec) · [schemas.dfos.com](https://schemas.dfos.com) · [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/schemas)
8
8
 
@@ -10,7 +10,13 @@ These schemas are conventions, not protocol requirements. The DFOS Protocol comm
10
10
 
11
11
  ## Schema Convention
12
12
 
13
- Every document committed to a content chain is wrapped in a [document envelope](https://protocol.dfos.com/spec#document-envelope). The envelope's `content` field holds the application-defined payload. The protocol requires one thing of this payload: it must include a `$schema` property identifying its content type.
13
+ Content objects are committed directly to a content chain by CID. The CID is derived from the canonical dag-cbor encoding of the content object itself:
14
+
15
+ ```
16
+ documentCID = CID(dagCborCanonicalEncode(contentObject))
17
+ ```
18
+
19
+ The protocol requires one thing of the content object: it must include a `$schema` property identifying its content type.
14
20
 
15
21
  ```json
16
22
  {
@@ -20,7 +26,7 @@ Every document committed to a content chain is wrapped in a [document envelope](
20
26
  }
21
27
  ```
22
28
 
23
- Because `$schema` is part of the document, it is behind the `documentCID` — cryptographically committed in the content chain. Any verifier can resolve the document, read `$schema`, and validate against the schema. Documents are self-describing.
29
+ Because `$schema` is part of the content object, it is behind the `documentCID` — cryptographically committed in the content chain. Any verifier can resolve the document, read `$schema`, and validate against the schema. Documents are self-describing.
24
30
 
25
31
  ---
26
32
 
@@ -42,28 +48,63 @@ Schema files live in [`schemas/`](https://github.com/metalabel/dfos/tree/main/pa
42
48
 
43
49
  The primary content type. Covers short posts, long-form posts, comments, and replies via the `format` discriminator.
44
50
 
45
- | Field | Type | Required | Description |
46
- | ------------- | -------- | -------- | ---------------------------------------------------------------------------------- |
47
- | `$schema` | string | yes | `"https://schemas.dfos.com/post/v1"` |
48
- | `format` | enum | yes | `"short-post"`, `"long-post"`, `"comment"`, `"reply"` — immutable, set at creation |
49
- | `title` | string | no | Post title (typically for long-post format) |
50
- | `body` | string | no | Post body content |
51
- | `cover` | media | no | Cover image |
52
- | `attachments` | media[] | no | Attached media objects |
53
- | `topics` | string[] | no | Topic names (stored as names for portability) |
51
+ | Field | Type | Required | Description |
52
+ | -------------- | -------- | -------- | ---------------------------------------------------------------------------------- |
53
+ | `$schema` | string | yes | `"https://schemas.dfos.com/post/v1"` |
54
+ | `format` | enum | yes | `"short-post"`, `"long-post"`, `"comment"`, `"reply"` — immutable, set at creation |
55
+ | `title` | string | no | Post title (typically for long-post format) |
56
+ | `body` | string | no | Post body content |
57
+ | `cover` | media | no | Cover image |
58
+ | `attachments` | media[] | no | Attached media objects |
59
+ | `topics` | string[] | no | Topic names (stored as names for portability) |
60
+ | `createdByDID` | string | no | DID of the content author — distinct from the chain operation signer |
61
+
62
+ `createdByDID` answers "who authored this content", which may differ from the signer of the chain operation (the `kid` DID). For example, an agent acting on behalf of a user commits the operation, but `createdByDID` records the human author.
54
63
 
55
64
  ### Profile (`https://schemas.dfos.com/profile/v1`)
56
65
 
57
66
  The displayable identity for any agent, person, group, or space.
58
67
 
59
- | Field | Type | Required | Description |
60
- | ------------- | ------ | -------- | --------------------------------------- |
61
- | `$schema` | string | yes | `"https://schemas.dfos.com/profile/v1"` |
62
- | `name` | string | no | Display name |
63
- | `description` | string | no | Short bio or description |
64
- | `avatar` | media | no | Avatar image |
65
- | `banner` | media | no | Banner image |
66
- | `background` | media | no | Background image |
68
+ | Field | Type | Required | Description |
69
+ | -------------- | ------ | -------- | --------------------------------------- |
70
+ | `$schema` | string | yes | `"https://schemas.dfos.com/profile/v1"` |
71
+ | `name` | string | no | Display name |
72
+ | `description` | string | no | Short bio or description |
73
+ | `avatar` | media | no | Avatar image |
74
+ | `banner` | media | no | Banner image |
75
+ | `background` | media | no | Background image |
76
+ | `createdByDID` | string | no | DID of the identity subject |
77
+
78
+ ### Manifest (`https://schemas.dfos.com/manifest/v1`)
79
+
80
+ A semantic index mapping path-like labels to protocol object references. The navigation layer for a DID's content.
81
+
82
+ | Field | Type | Required | Description |
83
+ | --------- | ------ | -------- | --------------------------------------------------- |
84
+ | `$schema` | string | yes | `"https://schemas.dfos.com/manifest/v1"` |
85
+ | `entries` | object | yes | Map of path-like keys to protocol object references |
86
+
87
+ Entry keys: lowercase alphanumeric with dots, underscores, hyphens, forward slashes. 2–128 chars. Must start and end with alphanumeric. Examples: `profile`, `posts`, `drafts/post-1`, `v1.0/release-notes`.
88
+
89
+ Entry values are protocol object references, self-describing by format:
90
+
91
+ - **contentId** (22-char bare hash) — references a living content chain
92
+ - **DID** (`did:dfos:...`) — references an identity
93
+ - **CID** (`bafyrei...`) — references a specific immutable document snapshot
94
+
95
+ ```json
96
+ {
97
+ "$schema": "https://schemas.dfos.com/manifest/v1",
98
+ "entries": {
99
+ "profile": "67t27rzc83v7c22n9t6z7c",
100
+ "posts": "a4b8c2d3e5f6g7h8i9j0k1",
101
+ "dark-publisher": "did:dfos:e3vvtck42d4eacdnzvtrn6",
102
+ "pinned-charter": "bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy"
103
+ }
104
+ }
105
+ ```
106
+
107
+ Manifests are content chains — same signing, same verification, same CIDs. A manifest's contentId appears in the DID's content set like any other chain. The semantic index (the document) is dark forest content — requires authorization to read. The operation chain (proof substrate) is public.
67
108
 
68
109
  ### Media Object
69
110
 
@@ -88,7 +129,7 @@ A content chain is a signed append-only log. The protocol enforces ordering, aut
88
129
 
89
130
  The chain represents a single evolving thing — a profile, a post, a policy document. Each operation is a **revision**. The resolved state is the latest `documentCID`. History is audit trail. The content _is_ the current version.
90
131
 
91
- This is the default interpretation for the standard schemas. The document envelope's `baseDocumentCID` field supports edit lineage — each new document version can point back to the one it replaced.
132
+ This is the default interpretation for the standard schemas. Edit lineage is tracked via `baseDocumentCID` on the content operation payload — each new operation can reference the document CID it replaced.
92
133
 
93
134
  ### Stream
94
135
 
package/DID-METHOD.md CHANGED
@@ -201,7 +201,7 @@ The critical property of `did:dfos` resolution: **the DID is verified against th
201
201
 
202
202
  The `did:dfos` method is transport-agnostic. Any system that can deliver an ordered sequence of JWS tokens (the identity chain) is a valid transport. Examples include:
203
203
 
204
- - **HTTP API** — The DFOS Registry API ([OpenAPI spec](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/openapi.yaml)) provides a REST interface for chain storage and retrieval.
204
+ - **HTTP API** — Any HTTP service that stores and retrieves ordered JWS logs can serve as a transport binding.
205
205
  - **Peer-to-peer exchange** — Chains can be exchanged directly between parties.
206
206
  - **Local storage** — Chains can be stored in local files, databases, or key-value stores.
207
207
  - **Bundle export** — Applications can export chains as portable bundles (e.g., JSON arrays of JWS tokens).
@@ -317,10 +317,9 @@ A complete reference implementation is available as the `@metalabel/dfos-protoco
317
317
 
318
318
  ### 9.2 Informative References
319
319
 
320
- | Reference | URI |
321
- | ----------------------- | ------------------------------------------------------------------------------- |
322
- | W3C DID Spec Registries | https://w3c.github.io/did-spec-registries/ |
323
- | Multicodec Table | https://github.com/multiformats/multicodec |
324
- | CIDv1 Specification | https://github.com/multiformats/cid |
325
- | dag-cbor Codec | https://ipld.io/specs/codecs/dag-cbor/spec/ |
326
- | DFOS Registry OpenAPI | https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/openapi.yaml |
320
+ | Reference | URI |
321
+ | ----------------------- | ------------------------------------------- |
322
+ | W3C DID Spec Registries | https://w3c.github.io/did-spec-registries/ |
323
+ | Multicodec Table | https://github.com/multiformats/multicodec |
324
+ | CIDv1 Specification | https://github.com/multiformats/cid |
325
+ | dag-cbor Codec | https://ipld.io/specs/codecs/dag-cbor/spec/ |
package/PROTOCOL.md CHANGED
@@ -20,14 +20,16 @@ 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 two layers:
23
+ The DFOS protocol has four components:
24
24
 
25
- | Layer | Concern |
25
+ | Component | Concern |
26
26
  | --------------------- | ---------------------------------------------------------------------------- |
27
27
  | **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
28
- | **Document envelope** | Standard wrapper: `content` + `baseDocumentCID` + `createdByDID` + timestamp |
28
+ | **Beacons** | Signed merkle root announcements periodic commitment over content sets |
29
+ | **Countersignatures** | Witness attestation — third-party signatures over existing chain operations |
30
+ | **Merkle trees** | SHA-256 binary trees over content IDs — inclusion proofs for beacon roots |
29
31
 
30
- The crypto core is the trust boundary — everything below it is cryptographically verified. The document envelope provides structural metadata (attribution, edit lineage, timestamps). What goes inside the envelope's `content` field is application-defined — see the [DFOS Content Model](https://protocol.dfos.com/content-model) for the standard schema library.
32
+ The crypto core is the trust boundary — everything below it is cryptographically verified. Documents are flat content objects, content-addressed directly: `documentCID = CID(dagCborCanonicalEncode(contentObject))`. What goes inside the content object is application-defined — see the [DFOS Content Model](https://protocol.dfos.com/content-model) for the standard schema library.
31
33
 
32
34
  ### Crypto Core: Two Chain Types
33
35
 
@@ -39,45 +41,19 @@ The crypto core is the trust boundary — everything below it is cryptographical
39
41
  | JWS typ | `did:dfos:identity-op` | `did:dfos:content-op` |
40
42
  | Self-sovereign | Yes (signs own operations) | No (signed by external identity) |
41
43
 
42
- Both chains are signed linked lists of state commitments. Identity chains embed their state (key sets). Content chains reference their state via `documentCID` — a content-addressed pointer to a document envelope.
43
-
44
- ### Document Envelope
45
-
46
- Every document committed to by a content chain uses a standard envelope, defined by JSON Schema at [`schemas/document-envelope.v1.json`](schemas/document-envelope.v1.json) (`https://schemas.dfos.com/document-envelope/v1`):
47
-
48
- ```json
49
- {
50
- "content": { "$schema": "https://schemas.dfos.com/post/v1", ... },
51
- "baseDocumentCID": null,
52
- "createdByDID": "did:dfos:...",
53
- "createdAt": "2026-03-07T00:02:00.000Z"
54
- }
55
- ```
56
-
57
- | Field | Type | Description |
58
- | ----------------- | ------------ | --------------------------------------------------------------------------- |
59
- | `content` | object | Application-defined content — must include `$schema` URI, opaque to chains |
60
- | `baseDocumentCID` | string\|null | Optional CID of a prior document version. Semantics are application-defined |
61
- | `createdByDID` | string | DID of the identity that created this document version |
62
- | `createdAt` | ISO 8601 | When this document version was created |
63
-
64
- The `documentCID` in a content chain operation is `CID(dagCborEncode(envelope))`. The envelope provides attribution at the protocol level. The `content` object must include a `$schema` property identifying its content type — this makes every document self-describing and its schema cryptographically committed via the CID.
44
+ Both chains are signed linked lists of state commitments. Identity chains embed their state (key sets). Content chains reference their state via `documentCID` — a content-addressed pointer to a flat content object.
65
45
 
66
46
  ### Addressing
67
47
 
68
- Three canonical representations:
69
-
70
- | Thing | Form | Example |
71
- | --------------------- | -------------------------- | --------------------------------- |
72
- | Operation or document | CID (dag-cbor + SHA-256) | See below |
73
- | Content chain | `<hash>` (bare, no prefix) | `67t27rzc83v7c22n9t6z7c` |
74
- | Identity chain | `did:dfos:<hash>` | `did:dfos:e3vvtck42d4eacdnzvtrn6` |
48
+ Three addressing modes, self-describing by format:
75
49
 
76
- Example CID:
50
+ | Thing | Form | Example |
51
+ | --------------------- | ------------------------ | --------------------------------- |
52
+ | Operation or document | CID (dag-cbor + SHA-256) | `bafyrei...` (base32lower) |
53
+ | Content chain | contentId (22-char hash) | `a82z92a3hndk6c97thcrn8` |
54
+ | Identity chain | DID | `did:dfos:e3vvtck42d4eacdnzvtrn6` |
77
55
 
78
- ```
79
- bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
80
- ```
56
+ CIDs are specific immutable artifacts — a pointer to an exact operation or document. Content IDs are living content chain entities — the 22-char bare hash derived from the genesis CID. DIDs are living identity chain entities.
81
57
 
82
58
  Operations and documents are CIDs — standard IPLD content addresses. Content chains and identity chains use derived identifiers — `customAlpha(SHA-256(genesis CID bytes))`. Same derivation for both. Identity chains prepend `did:dfos:` (W3C DID spec). Content identifiers are bare — just the 22-char hash, no prefix.
83
59
 
@@ -118,10 +94,13 @@ Content chain verification requires a **valid EdDSA signature** and delegates ke
118
94
 
119
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.
120
96
 
97
+ **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
+
121
99
  **What the protocol enforces:**
122
100
 
123
101
  - The EdDSA signature on each operation is valid against the key returned by `resolveKey(kid)`
124
102
  - Chain integrity (CID links, timestamp ordering, terminal state)
103
+ - The `kid` DID matches the payload `did` for chain operations
125
104
 
126
105
  **What the protocol does NOT enforce (application concerns):**
127
106
 
@@ -140,7 +119,16 @@ Identity chains are self-sovereign — they define their own valid signers via `
140
119
 
141
120
  ### `typ` Header
142
121
 
143
- The JWS `typ` header (`did:dfos:identity-op`, `did:dfos:content-op`) aids routing but is not security-critical. Implementations SHOULD validate it but MUST NOT rely on it for security decisions.
122
+ The JWS `typ` header uses protocol-specific values (not IANA media types):
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 |
130
+
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.
144
132
 
145
133
  ### Operation Field Limits
146
134
 
@@ -148,6 +136,7 @@ The protocol defines maximum sizes for all operation fields as abuse-prevention
148
136
 
149
137
  | Field | Max | Rationale |
150
138
  | -------------------------------------------- | --------- | -------------------------------------- |
139
+ | `did` | 256 chars | ~8× typical `did:dfos:` (~31 chars) |
151
140
  | `key.id` | 64 chars | ~3× typical key ID (`key_` + 22 chars) |
152
141
  | `key.publicKeyMultibase` | 128 chars | ~2× Ed25519 multikey (~50 chars) |
153
142
  | `authKeys` / `assertKeys` / `controllerKeys` | 16 items | Generous for key rotation |
@@ -301,19 +290,24 @@ DID: did:dfos:e3vvtck42d4eacdnzvtrn6
301
290
  ```typescript
302
291
  // Genesis — starts the content chain, commits initial document
303
292
  { version: 1, type: "create",
304
- documentCID: string, // CID of document content
293
+ did: string, // author DID, committed to by CID
294
+ documentCID: string, // CID of flat content object
295
+ baseDocumentCID: string | null, // edit lineage — CID of prior document version
305
296
  createdAt: string,
306
297
  note: string | null }
307
298
 
308
299
  // Content change (null documentCID = clear content)
309
300
  { version: 1, type: "update",
301
+ did: string, // author DID
310
302
  previousOperationCID: string,
311
303
  documentCID: string | null,
304
+ baseDocumentCID: string | null,
312
305
  createdAt: string,
313
306
  note: string | null }
314
307
 
315
308
  // Permanent destruction
316
309
  { version: 1, type: "delete",
310
+ did: string, // author DID
317
311
  previousOperationCID: string,
318
312
  createdAt: string,
319
313
  note: string | null }
@@ -392,6 +386,90 @@ Where `idEncode` is the 19-char alphabet encoding described above.
392
386
 
393
387
  ---
394
388
 
389
+ ## Beacons
390
+
391
+ 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.
392
+
393
+ ### Beacon Payload
394
+
395
+ ```json
396
+ {
397
+ "version": 1,
398
+ "type": "beacon",
399
+ "did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
400
+ "merkleRoot": "a3f8b2c1d4e5f6071829304a5b6c7d8e9f0a1b2c3d4e5f6071829304a5b6c7d8",
401
+ "createdAt": "2026-03-07T00:04:00.000Z"
402
+ }
403
+ ```
404
+
405
+ | Field | Type | Description |
406
+ | ------------ | ------ | ------------------------------------------------------- |
407
+ | `version` | 1 | Protocol version |
408
+ | `type` | string | Literal `"beacon"` |
409
+ | `did` | string | DID of the identity publishing the beacon |
410
+ | `merkleRoot` | string | Hex-encoded SHA-256 root (64 chars, `/^[0-9a-f]{64}$/`) |
411
+ | `createdAt` | string | ISO 8601 timestamp |
412
+
413
+ ### Beacon JWS Header
414
+
415
+ ```json
416
+ {
417
+ "alg": "EdDSA",
418
+ "typ": "did:dfos:beacon",
419
+ "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd",
420
+ "cid": "bafyrei..."
421
+ }
422
+ ```
423
+
424
+ ### Beacon Semantics
425
+
426
+ 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.
427
+
428
+ **Clock skew tolerance**: Implementations MUST reject beacons with a `createdAt` more than 5 minutes in the future relative to the verifier's clock. This prevents pre-dating attacks while accommodating reasonable clock drift.
429
+
430
+ **merkleRoot**: A hex-encoded SHA-256 hash (64 characters). This is a commitment, not a CID — it uses raw SHA-256, not dag-cbor encoding. See the Merkle Tree section below for construction. An empty content set produces a `null` merkle root (no beacon needed).
431
+
432
+ ---
433
+
434
+ ## Merkle Trees
435
+
436
+ Beacons commit to a set of content IDs via a pure SHA-256 binary Merkle tree. The tree has no dag-cbor dependency — it uses only SHA-256 over raw bytes.
437
+
438
+ ### Construction
439
+
440
+ 1. **Collect** all content IDs (22-char bare hashes) in the set
441
+ 2. **Sort** content IDs lexicographically (UTF-8 byte order)
442
+ 3. **Hash leaves**: for each content ID, `SHA-256(UTF-8(contentId))` → 32-byte leaf hash
443
+ 4. **Build tree**: recursively pair adjacent hashes. For each pair, `SHA-256(left || right)` → 32 bytes. If a level has an odd number of nodes, the last node is promoted to the next level unpaired.
444
+ 5. **Root**: the final 32-byte hash, hex-encoded to a 64-character string
445
+
446
+ 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
+
448
+ ### Inclusion Proofs
449
+
450
+ 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
+
452
+ ---
453
+
454
+ ## Countersignatures
455
+
456
+ A countersignature is a witness attestation — a third-party identity signing the same CID-committed bytes as an existing chain operation. Countersignatures use the same JWS format and `typ` (`did:dfos:content-op`) as the original operation.
457
+
458
+ ### Discrimination Rule
459
+
460
+ The protocol distinguishes author operations from countersignatures by comparing the `kid` DID in the JWS header to the `did` field in the operation payload:
461
+
462
+ - **`kid` DID === payload `did`** → author operation (chain operation)
463
+ - **`kid` DID !== payload `did`** → witness countersignature
464
+
465
+ ### Semantics
466
+
467
+ A countersignature proves that a witness identity has seen and attested to a specific operation. The witness signs the exact same payload (same CID), but with their own key. The countersignature's JWS header will contain the witness's `kid` (their DID URL), while the payload's `did` field remains the original author's DID.
468
+
469
+ Countersignatures are not part of the chain — they do not have `previousOperationCID` links and do not affect chain state. They are auxiliary attestations stored alongside chain operations.
470
+
471
+ ---
472
+
395
473
  ## Verification
396
474
 
397
475
  ### Identity Chain
@@ -414,9 +492,10 @@ Where `idEncode` is the 19-char alphabet encoding described above.
414
492
  2. First op must be `type: "create"`
415
493
  3. For each subsequent op: verify `previousOperationCID` matches, verify `createdAt` increasing
416
494
  4. Derive the operation CID via dag-cbor canonical encoding. Verify `header.cid` matches the derived CID.
417
- 5. Resolve `kid` via external key resolver (caller provides)
418
- 6. Verify EdDSA JWS signature
419
- 7. Apply state change (set document, clear, or delete)
495
+ 5. Verify the `kid` DID matches the payload `did` field (mismatches indicate a countersignature, not a chain operation)
496
+ 6. Resolve `kid` via external key resolver (caller provides)
497
+ 7. Verify EdDSA JWS signature
498
+ 8. Apply state change (set document, clear, or delete)
420
499
 
421
500
  ---
422
501
 
@@ -497,7 +576,7 @@ JWS Signature (hex):
497
576
  JWS Token:
498
577
 
499
578
  ```
500
- eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTl2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw
579
+ eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTF2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw
501
580
  ```
502
581
 
503
582
  Operation CID:
@@ -575,26 +654,22 @@ Post-rotation: DID unchanged (`did:dfos:e3vvtck42d4eacdnzvtrn6`), controller rot
575
654
 
576
655
  ### Content Chain: Document + Create
577
656
 
578
- Document (application layer):
657
+ Document (flat content object):
579
658
 
580
659
  ```json
581
660
  {
582
- "content": {
583
- "$schema": "https://schemas.dfos.com/post/v1",
584
- "format": "short-post",
585
- "title": "Hello World",
586
- "body": "First post on the protocol."
587
- },
588
- "baseDocumentCID": null,
589
- "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
590
- "createdAt": "2026-03-07T00:02:00.000Z"
661
+ "$schema": "https://schemas.dfos.com/post/v1",
662
+ "format": "short-post",
663
+ "title": "Hello World",
664
+ "body": "First post on the protocol.",
665
+ "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
591
666
  }
592
667
  ```
593
668
 
594
669
  Document CID:
595
670
 
596
671
  ```
597
- bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne
672
+ bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4
598
673
  ```
599
674
 
600
675
  Content Create JWS Header:
@@ -604,7 +679,7 @@ Content Create JWS Header:
604
679
  "alg": "EdDSA",
605
680
  "typ": "did:dfos:content-op",
606
681
  "kid": "did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd",
607
- "cid": "bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu"
682
+ "cid": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu"
608
683
  }
609
684
  ```
610
685
 
@@ -614,7 +689,9 @@ Content Create Payload:
614
689
  {
615
690
  "version": 1,
616
691
  "type": "create",
617
- "documentCID": "bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne",
692
+ "did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
693
+ "documentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4",
694
+ "baseDocumentCID": null,
618
695
  "createdAt": "2026-03-07T00:02:00.000Z",
619
696
  "note": null
620
697
  }
@@ -623,19 +700,19 @@ Content Create Payload:
623
700
  Content Create JWS Signature (hex):
624
701
 
625
702
  ```
626
- b7f0c3909fd398d7a42065053b6d86f96efc4281385d383d2ca4388330101da2b707ae3dd538abf5bfb0b69fa173098436ed87aa789eaafe404a2a9f16b11b0f
703
+ 46feaf973e4c7ebc2a0d4ad25481ace197de05b91051205c5e1c7067a85fb9d4abe4cc61625d3c853a8b0ce0345b534c8cdd07b34216f635d3c0bc0fd5d30306
627
704
  ```
628
705
 
629
706
  Content Create JWS Token:
630
707
 
631
708
  ```
632
- eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWE1ejd6eGtuYWU1ZHM3MmV1aWh1ZjJyZzNpeGw2dDRmYnpqZWZoY29nZzNucXBweW9ncXUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZG9jdW1lbnRDSUQiOiJiYWZ5cmVpZnB2d3Vhcm1sNjJzZm9nZHBpMnZsbHR2ZzJldjZvNHh0dzc0emZ1ZDdjcGtnNzQyNnpuZSIsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.t_DDkJ_TmNekIGUFO22G-W78QoE4XTg9LKQ4gzAQHaK3B6491Tir9b-wtp-hcwmENu2Hqnieqv5ASiqfFrEbDw
709
+ eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg
633
710
  ```
634
711
 
635
712
  Content Operation CID:
636
713
 
637
714
  ```
638
- bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu
715
+ bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu
639
716
  ```
640
717
 
641
718
  ### Content Chain: Update
@@ -646,39 +723,45 @@ Content Update Payload:
646
723
  {
647
724
  "version": 1,
648
725
  "type": "update",
649
- "previousOperationCID": "bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu",
650
- "documentCID": "bafyreieuo26zfmjxwpmw5jk6bqzqhvivxcbckgxtyeuc7ypf3p4sihgq4q",
726
+ "did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
727
+ "previousOperationCID": "bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu",
728
+ "documentCID": "bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu",
729
+ "baseDocumentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4",
651
730
  "createdAt": "2026-03-07T00:03:00.000Z",
652
731
  "note": "edited title and body"
653
732
  }
654
733
  ```
655
734
 
656
- Updated document:
735
+ Updated document (flat content object):
657
736
 
658
737
  ```json
659
738
  {
660
- "content": {
661
- "$schema": "https://schemas.dfos.com/post/v1",
662
- "format": "short-post",
663
- "title": "Hello World (edited)",
664
- "body": "Updated content."
665
- },
666
- "baseDocumentCID": "bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne",
667
- "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
668
- "createdAt": "2026-03-07T00:03:00.000Z"
739
+ "$schema": "https://schemas.dfos.com/post/v1",
740
+ "format": "short-post",
741
+ "title": "Hello World (edited)",
742
+ "body": "Updated content.",
743
+ "createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
669
744
  }
670
745
  ```
671
746
 
672
747
  Document CID (edited):
673
748
 
674
749
  ```
675
- bafyreieuo26zfmjxwpmw5jk6bqzqhvivxcbckgxtyeuc7ypf3p4sihgq4q
750
+ bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu
676
751
  ```
677
752
 
678
753
  Content Update CID:
679
754
 
680
755
  ```
681
- bafyreibb4lsvqmz4j76rsvhkqw3v2b4vp23t7dimm6vl5g5wlninvkemxq
756
+ bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4
757
+ ```
758
+
759
+ ### Content Chain Verified State
760
+
761
+ ```
762
+ Content ID: a82z92a3hndk6c97thcrn8
763
+ Genesis CID: bafyreiaedhjq64aajpwociahl5w37j6uoxr5mojoq5dnah6fpvxr5d4lxu
764
+ Head CID: bafyreih6e5cbjitpozhzhgmfktmiohmxyn3ucwhqd3mjixizvwmlhv7hm4
682
765
  ```
683
766
 
684
767
  ---
@@ -718,21 +801,23 @@ Given the artifacts above, verify:
718
801
  did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
719
802
  ```
720
803
 
721
- 8. **Document CID**: dag-cbor canonical encode the document JSON → SHA-256 → CIDv1 → should be:
804
+ 8. **Document CID**: dag-cbor canonical encode the flat content object → SHA-256 → CIDv1 → should be:
722
805
 
723
806
  ```
724
- bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne
807
+ bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4
725
808
  ```
726
809
 
727
- 9. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID
810
+ 9. **Content operation `did` field**: verify the `did` field in each content operation matches the `kid` DID in the JWS header
811
+
812
+ 10. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID
728
813
 
729
- 10. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
814
+ 11. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
730
815
 
731
816
  ---
732
817
 
733
818
  ## Source and Verification
734
819
 
735
- All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) — self-contained, zero monorepo dependencies. 160 checks across 5 languages.
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.
736
821
 
737
822
  - [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
738
823
  - [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
@@ -744,22 +829,25 @@ All source lives in [`packages/dfos-protocol/`](https://github.com/metalabel/dfo
744
829
  - [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`
745
830
  - [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`
746
831
  - [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
832
+ - [`chain/beacon`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/beacon.ts) — `signBeacon`, `verifyBeacon`
833
+ - [`chain/countersign`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/countersign.ts) — `signCountersignature`, `verifyCountersignature`
834
+ - [`merkle/tree`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/tree.ts) — `buildMerkleTree`, `hashLeaf`
835
+ - [`merkle/proof`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/merkle/proof.ts) — `generateMerkleProof`, `verifyMerkleProof`
747
836
 
748
837
  ### Related Specifications
749
838
 
750
839
  - [DID Method: `did:dfos`](https://protocol.dfos.com/did-method) — W3C DID method specification for identity chains
751
- - [Content Model](https://protocol.dfos.com/content-model) — Standard content schemas (post, profile) for the document envelope
752
- - [Registry API](https://protocol.dfos.com/registry-api) — HTTP API for chain storage and resolution
840
+ - [Content Model](https://protocol.dfos.com/content-model) — Standard content schemas (post, profile) for document content objects
753
841
 
754
842
  ### Cross-Language Verification
755
843
 
756
844
  | Language | Tests | Source |
757
845
  | ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
758
- | TypeScript | 99 | [`tests/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/tests) |
759
- | Python | 35 | [`verify/python/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/python) |
760
- | Go | 9 | [`verify/go/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/go) |
761
- | Rust | 9 | [`verify/rust/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/rust) |
762
- | Swift | 8 | [`verify/swift/`](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol/verify/swift) |
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) |
763
851
 
764
852
  ---
765
853