@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 +63 -22
- package/DID-METHOD.md +7 -8
- package/PROTOCOL.md +174 -86
- package/README.md +8 -10
- package/dist/chain/index.d.ts +93 -1
- package/dist/chain/index.js +13 -1
- package/dist/{chunk-VEBMLR37.js → chunk-ASGEXSVT.js} +172 -2
- package/dist/chunk-E5CFQG2B.js +99 -0
- package/dist/index.d.ts +2 -4
- package/dist/index.js +26 -32
- package/dist/merkle/index.d.ts +45 -0
- package/dist/merkle/index.js +14 -0
- package/examples/content-delete.json +5 -4
- package/examples/content-lifecycle.json +9 -7
- package/package.json +6 -9
- package/schemas/manifest.v1.json +29 -0
- package/schemas/post.v1.json +5 -0
- package/schemas/profile.v1.json +5 -0
- package/REGISTRY-API.md +0 -242
- package/dist/chunk-U6DANYPT.js +0 -311
- package/dist/registry/index.d.ts +0 -143
- package/dist/registry/index.js +0 -34
- package/openapi.yaml +0 -376
- package/schemas/document-envelope.v1.json +0 -37
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
46
|
-
|
|
|
47
|
-
| `$schema`
|
|
48
|
-
| `format`
|
|
49
|
-
| `title`
|
|
50
|
-
| `body`
|
|
51
|
-
| `cover`
|
|
52
|
-
| `attachments`
|
|
53
|
-
| `topics`
|
|
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
|
|
60
|
-
|
|
|
61
|
-
| `$schema`
|
|
62
|
-
| `name`
|
|
63
|
-
| `description`
|
|
64
|
-
| `avatar`
|
|
65
|
-
| `banner`
|
|
66
|
-
| `background`
|
|
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.
|
|
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** —
|
|
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
|
|
23
|
+
The DFOS protocol has four components:
|
|
24
24
|
|
|
25
|
-
|
|
|
25
|
+
| Component | Concern |
|
|
26
26
|
| --------------------- | ---------------------------------------------------------------------------- |
|
|
27
27
|
| **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
|
|
28
|
-
| **
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
418
|
-
6.
|
|
419
|
-
7.
|
|
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.
|
|
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 (
|
|
657
|
+
Document (flat content object):
|
|
579
658
|
|
|
580
659
|
```json
|
|
581
660
|
{
|
|
582
|
-
"
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
"
|
|
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
|
-
|
|
703
|
+
46feaf973e4c7ebc2a0d4ad25481ace197de05b91051205c5e1c7067a85fb9d4abe4cc61625d3c853a8b0ce0345b534c8cdd07b34216f635d3c0bc0fd5d30306
|
|
627
704
|
```
|
|
628
705
|
|
|
629
706
|
Content Create JWS Token:
|
|
630
707
|
|
|
631
708
|
```
|
|
632
|
-
|
|
709
|
+
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg
|
|
633
710
|
```
|
|
634
711
|
|
|
635
712
|
Content Operation CID:
|
|
636
713
|
|
|
637
714
|
```
|
|
638
|
-
|
|
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
|
-
"
|
|
650
|
-
"
|
|
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
|
-
"
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
|
|
750
|
+
bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu
|
|
676
751
|
```
|
|
677
752
|
|
|
678
753
|
Content Update CID:
|
|
679
754
|
|
|
680
755
|
```
|
|
681
|
-
|
|
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
|
|
804
|
+
8. **Document CID**: dag-cbor canonical encode the flat content object → SHA-256 → CIDv1 → should be:
|
|
722
805
|
|
|
723
806
|
```
|
|
724
|
-
|
|
807
|
+
bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4
|
|
725
808
|
```
|
|
726
809
|
|
|
727
|
-
9. **Content
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 |
|
|
759
|
-
| Python |
|
|
760
|
-
| Go |
|
|
761
|
-
| Rust |
|
|
762
|
-
| 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
|
|