@metalabel/dfos-protocol 0.0.1 → 0.0.3
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 +107 -0
- package/DID-METHOD.md +326 -0
- package/PROTOCOL.md +137 -241
- package/README.md +9 -7
- package/REGISTRY-API.md +242 -0
- package/dist/chain/index.d.ts +5 -5
- package/dist/chain/index.js +3 -3
- package/dist/{chunk-3PB644X2.js → chunk-U6DANYPT.js} +32 -51
- package/dist/{chunk-LWC4PWGZ.js → chunk-VEBMLR37.js} +5 -5
- package/dist/index.d.ts +2 -2
- package/dist/index.js +10 -12
- package/dist/registry/index.d.ts +12 -20
- package/dist/registry/index.js +8 -10
- package/examples/content-delete.json +1 -1
- package/examples/content-lifecycle.json +1 -1
- package/openapi.yaml +20 -52
- package/package.json +11 -6
package/PROTOCOL.md
CHANGED
|
@@ -1,45 +1,33 @@
|
|
|
1
|
-
# DFOS Protocol
|
|
1
|
+
# DFOS Protocol
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
- **Status**: Implemented and tested — TypeScript + Go + Python + Rust + Swift cross-language verification. This spec is currently under review. Discuss the DFOS protocol in the [clear.txt](https://clear.dfos.com) space.
|
|
5
|
-
- **Source**: `packages/dfos-protocol` (self-contained, zero monorepo dependencies, OSS-ready)
|
|
6
|
-
- **Gist**: https://gist.github.com/bvalosek/ed4c96fd4b841302de544ffaee871648 (synced from this file)
|
|
3
|
+
Verifiable identity and content chains — Ed25519 signatures, content-addressed CIDs, W3C DIDs. Cross-language verification in TypeScript, Go, Python, Rust, and Swift.
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
## Philosophy
|
|
11
|
-
|
|
12
|
-
DFOS is a dark forest operating system. Content lives in private spaces — visible only to members, governed by the communities that create it. The forest floor is dark by default.
|
|
13
|
-
|
|
14
|
-
But the cryptographic proof layer is public and verifiable. Every piece of content, every identity, every edit has a signed chain of commitments that anyone can independently verify. You don't need to trust the platform. You don't need access to the database. You need a public key and a chain of JWS tokens.
|
|
15
|
-
|
|
16
|
-
If you have content — from the official app, from an API export, from a screenshot someone sent you, from a pirate mirror, from anywhere — you can verify it's authentic. Hash the content, check the CID, walk the chain, verify the signature. The content is dark; the proof is light.
|
|
5
|
+
This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS.
|
|
17
6
|
|
|
18
|
-
|
|
7
|
+
[Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-protocol) · [npm](https://www.npmjs.com/package/@metalabel/dfos-protocol) · [Gist](https://gist.github.com/bvalosek/ed4c96fd4b841302de544ffaee871648)
|
|
19
8
|
|
|
20
|
-
|
|
9
|
+
---
|
|
21
10
|
|
|
22
|
-
|
|
11
|
+
## Philosophy
|
|
23
12
|
|
|
24
|
-
|
|
13
|
+
DFOS is a dark forest operating system. Content lives in private spaces — visible only to members, governed by the communities that create it. The cryptographic proof layer is public: signed chains of commitments that anyone can independently verify with a public key and any standard EdDSA library.
|
|
25
14
|
|
|
26
|
-
|
|
15
|
+
Two chain types — identity and content — use the same mechanics: Ed25519 signatures, JWS compact tokens, content-addressed CIDs. The protocol knows about keys and document hashes. It doesn't know about posts, profiles, or any application concept. Document semantics are application layer — free to evolve without protocol changes.
|
|
27
16
|
|
|
28
|
-
|
|
17
|
+
The protocol is not coupled to the DFOS platform. Any system implementing the same chain primitives produces interoperable, cross-verifiable proofs. An identity created on one system can sign content on another.
|
|
29
18
|
|
|
30
19
|
---
|
|
31
20
|
|
|
32
21
|
## Protocol Overview
|
|
33
22
|
|
|
34
|
-
The DFOS protocol has
|
|
23
|
+
The DFOS protocol has two layers:
|
|
35
24
|
|
|
36
25
|
| Layer | Concern |
|
|
37
26
|
| --------------------- | ---------------------------------------------------------------------------- |
|
|
38
27
|
| **Crypto core** | Identity chains + content chains — Ed25519 signatures, JWS tokens, CID links |
|
|
39
28
|
| **Document envelope** | Standard wrapper: `content` + `baseDocumentCID` + `createdByDID` + timestamp |
|
|
40
|
-
| **Content schemas** | JSON Schema definitions for what goes inside `content` (post, profile, etc.) |
|
|
41
29
|
|
|
42
|
-
The crypto core is the trust boundary — everything below it is cryptographically verified. The document envelope provides structural metadata (attribution, edit lineage, timestamps).
|
|
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.
|
|
43
31
|
|
|
44
32
|
### Crypto Core: Two Chain Types
|
|
45
33
|
|
|
@@ -60,7 +48,7 @@ Every document committed to by a content chain uses a standard envelope, defined
|
|
|
60
48
|
```json
|
|
61
49
|
{
|
|
62
50
|
"content": { "$schema": "https://schemas.dfos.com/post/v1", ... },
|
|
63
|
-
"baseDocumentCID":
|
|
51
|
+
"baseDocumentCID": null,
|
|
64
52
|
"createdByDID": "did:dfos:...",
|
|
65
53
|
"createdAt": "2026-03-07T00:02:00.000Z"
|
|
66
54
|
}
|
|
@@ -69,27 +57,29 @@ Every document committed to by a content chain uses a standard envelope, defined
|
|
|
69
57
|
| Field | Type | Description |
|
|
70
58
|
| ----------------- | ------------ | --------------------------------------------------------------------------- |
|
|
71
59
|
| `content` | object | Application-defined content — must include `$schema` URI, opaque to chains |
|
|
72
|
-
| `baseDocumentCID` | string\|null | CID of
|
|
60
|
+
| `baseDocumentCID` | string\|null | Optional CID of a prior document version. Semantics are application-defined |
|
|
73
61
|
| `createdByDID` | string | DID of the identity that created this document version |
|
|
74
62
|
| `createdAt` | ISO 8601 | When this document version was created |
|
|
75
63
|
|
|
76
|
-
The `documentCID` in a content chain operation is `CID(dagCborEncode(envelope))`. The envelope provides attribution
|
|
77
|
-
|
|
78
|
-
### Content Schemas
|
|
79
|
-
|
|
80
|
-
The `content` field inside the document envelope is validated by JSON Schema. The protocol ships a standard library of schemas (post, profile) — see [Standard Document Schemas](#standard-document-schemas). These are conventions, not requirements. Any implementation can define custom schemas.
|
|
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.
|
|
81
65
|
|
|
82
66
|
### Addressing
|
|
83
67
|
|
|
84
68
|
Three canonical representations:
|
|
85
69
|
|
|
86
|
-
| Thing
|
|
87
|
-
|
|
|
88
|
-
| Operation or document
|
|
89
|
-
|
|
|
90
|
-
| Identity
|
|
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` |
|
|
91
75
|
|
|
92
|
-
|
|
76
|
+
Example CID:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
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.
|
|
93
83
|
|
|
94
84
|
Application code may add prefixes for routing (e.g., `post_xxxx`) — these are strippable semantic sugar, not part of the protocol identifier.
|
|
95
85
|
|
|
@@ -99,17 +89,20 @@ Application code may add prefixes for routing (e.g., `post_xxxx`) — these are
|
|
|
99
89
|
|
|
100
90
|
### Commitment Scheme
|
|
101
91
|
|
|
102
|
-
|
|
92
|
+
Both operations and documents are content-addressed via **CID** (`dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1). Operations are additionally signed via **JWS**.
|
|
103
93
|
|
|
104
|
-
|
|
94
|
+
| Representation | Encoding | Purpose |
|
|
95
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
|
|
96
|
+
| CID | `dagCborCanonicalEncode(payload)` → SHA-256 → CIDv1 | Deterministic content addressing for operations and documents |
|
|
97
|
+
| JWS | `base64url(JSON.stringify(header))` + `.` + `base64url(JSON.stringify(payload))` → EdDSA signature covers both | Signature verification for operations |
|
|
105
98
|
|
|
106
|
-
|
|
99
|
+
CID uses [dag-cbor canonical encoding](https://ipld.io/specs/codecs/dag-cbor/spec/) for determinism — given the same logical payload, the CID MUST be identical regardless of implementation language or platform. JWS uses standard JSON for library interoperability. The dag-cbor hex test vectors in this document allow byte-level verification.
|
|
107
100
|
|
|
108
101
|
### Chain Validity
|
|
109
102
|
|
|
110
103
|
A valid chain is a **linear sequence** of operations. Each operation (after genesis) links to its predecessor via `previousOperationCID`. The chain provides structural ordering independent of timestamps.
|
|
111
104
|
|
|
112
|
-
**Forks are invalid at the protocol level.** Two operations referencing the same `previousOperationCID` constitute a fork. The protocol does not define fork resolution — this is application-defined
|
|
105
|
+
**Forks are invalid at the protocol level.** Two operations referencing the same `previousOperationCID` constitute a fork. The protocol does not define fork resolution — this is application-defined (e.g., longest chain, first-seen, advisory locks).
|
|
113
106
|
|
|
114
107
|
**Timestamp ordering**: `createdAt` SHOULD be strictly increasing within a chain. Implementations SHOULD reject operations with non-increasing timestamps as a sanity check against replayed or mis-ordered operations. However, the chain link (CID reference) is the authoritative ordering mechanism, not the timestamp. Implementations MAY relax timestamp ordering in constrained environments where clock synchronization is impractical.
|
|
115
108
|
|
|
@@ -121,11 +114,9 @@ This is a self-sovereign invariant: the identity chain defines its own valid sig
|
|
|
121
114
|
|
|
122
115
|
### Content Chain Signer Model
|
|
123
116
|
|
|
124
|
-
Content chain verification requires a **valid EdDSA signature**
|
|
125
|
-
|
|
126
|
-
The signing key is resolved via the `kid` (DID URL), which references a key on an external identity. The content chain verifier delegates key resolution to the caller via a `resolveKey` callback — the protocol does not prescribe how to look up an identity's current key state.
|
|
117
|
+
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.
|
|
127
118
|
|
|
128
|
-
|
|
119
|
+
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.
|
|
129
120
|
|
|
130
121
|
**What the protocol enforces:**
|
|
131
122
|
|
|
@@ -137,37 +128,23 @@ This is a deliberate asymmetry with identity chains. Identity chains are self-so
|
|
|
137
128
|
- Which identities are authorized to sign operations on a given chain
|
|
138
129
|
- Which key role (auth, assert, controller) the signing key must have
|
|
139
130
|
- Whether a chain must have a single signer or may have multiple signers
|
|
140
|
-
- Ownership or attribution semantics between signers and
|
|
141
|
-
|
|
142
|
-
### Terminal States
|
|
131
|
+
- Ownership or attribution semantics between signers and content chains
|
|
143
132
|
|
|
144
|
-
|
|
133
|
+
### Terminal States and Special Operations
|
|
145
134
|
|
|
146
|
-
|
|
135
|
+
**`delete` is the only terminal state.** No valid operations may follow a delete. An implementation MUST reject any operation after a delete. Delete prevents future operations but does NOT remove data — the complete chain remains intact for verification. Data removal is an application concern.
|
|
147
136
|
|
|
148
|
-
|
|
137
|
+
**Controller key requirement:** `update` operations on identity chains MUST include at least one controller key. If decommissioning is intended, `delete` is the correct terminal operation.
|
|
149
138
|
|
|
150
|
-
`update`
|
|
151
|
-
|
|
152
|
-
### Content-Null Semantics
|
|
153
|
-
|
|
154
|
-
An `update` operation on a content chain with `documentCID: null` means **the entity exists but its current content is cleared**. This is not a delete — the chain continues, and a subsequent update can set content again. Think of it as "unpublish" rather than "destroy."
|
|
139
|
+
**Content-null:** An `update` on a content chain with `documentCID: null` means the content exists but its document is cleared. The chain continues — a subsequent update can set content again.
|
|
155
140
|
|
|
156
141
|
### `typ` Header
|
|
157
142
|
|
|
158
|
-
The JWS `typ` header (`did:dfos:identity-op`, `did:dfos:content-op`)
|
|
159
|
-
|
|
160
|
-
### JWT `kid` vs Operation `kid`
|
|
161
|
-
|
|
162
|
-
JWT tokens (for device auth, MCP sessions, etc.) use `kid` as a simple key identifier for lookup — e.g., `key_ez9a874tckr3dv933d3ckd`. This does NOT follow the same DID URL convention used in operation JWS headers. Operation `kid` uses bare key ID for identity genesis and DID URL (`did:dfos:xxx#key_id`) for everything else. JWT `kid` is always a bare key ID — the JWT's `sub` claim carries the DID separately.
|
|
163
|
-
|
|
164
|
-
### ID Modulo Bias
|
|
165
|
-
|
|
166
|
-
The ID encoding uses `byte % 19` where each byte ranges 0-255. Since 256 is not evenly divisible by 19, values 0-8 (alphabet positions) appear with probability ~5.26% while values 9-18 appear with probability ~5.22%. This is a ~0.3% bias — not security-relevant for identifiers but acknowledged here for completeness. A rejection-sampling approach (retry if `byte >= 247`) would eliminate the bias entirely.
|
|
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.
|
|
167
144
|
|
|
168
145
|
### Operation Field Limits
|
|
169
146
|
|
|
170
|
-
The protocol defines maximum sizes for all operation fields
|
|
147
|
+
The protocol defines maximum sizes for all operation fields as abuse-prevention ceilings. Implementations MUST reject operations that exceed these bounds. Implementations MAY impose stricter limits.
|
|
171
148
|
|
|
172
149
|
| Field | Max | Rationale |
|
|
173
150
|
| -------------------------------------------- | --------- | -------------------------------------- |
|
|
@@ -188,84 +165,6 @@ The protocol does NOT limit:
|
|
|
188
165
|
|
|
189
166
|
---
|
|
190
167
|
|
|
191
|
-
## Standard Document Schemas
|
|
192
|
-
|
|
193
|
-
The crypto core commits to `documentCID` values without inspecting their contents. The document envelope provides structural metadata. The **content** inside the envelope is where JSON Schema validation applies.
|
|
194
|
-
|
|
195
|
-
The protocol ships a standard library of content schemas as JSON Schema (draft 2020-12) definitions. These are not required — any implementation can define its own content types. They are provided as a starting point for content built on the DFOS protocol, and they are what DFOS uses internally.
|
|
196
|
-
|
|
197
|
-
### Schema Convention
|
|
198
|
-
|
|
199
|
-
Documents declare their type via a `$schema` field pointing to a schema URI:
|
|
200
|
-
|
|
201
|
-
```json
|
|
202
|
-
{
|
|
203
|
-
"$schema": "https://schemas.dfos.com/post/v1",
|
|
204
|
-
"format": "short-post",
|
|
205
|
-
"body": "Hello world."
|
|
206
|
-
}
|
|
207
|
-
```
|
|
208
|
-
|
|
209
|
-
Because the `$schema` field 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.
|
|
210
|
-
|
|
211
|
-
### Schema Evolution
|
|
212
|
-
|
|
213
|
-
Schemas are versioned via the URI path (`/post/v1`, `/post/v2`). Evolution rules:
|
|
214
|
-
|
|
215
|
-
- **Strictly additive within a version** — new optional fields can be added to an existing version at any time without breaking existing documents
|
|
216
|
-
- **Breaking changes require a new version** — removing fields, changing types, or adding new required fields means a new version URI
|
|
217
|
-
- **Implementations declare which versions they understand** — a registry or application can accept `post/v1` and `post/v2` simultaneously, or only `post/v1`
|
|
218
|
-
|
|
219
|
-
### Standard Schemas
|
|
220
|
-
|
|
221
|
-
Schema files live in `schemas/` in the protocol package. Each is a standalone JSON Schema (draft 2020-12).
|
|
222
|
-
|
|
223
|
-
#### Post (`https://schemas.dfos.com/post/v1`)
|
|
224
|
-
|
|
225
|
-
The primary content type. Covers short posts, long-form posts, comments, and replies via the `format` discriminator.
|
|
226
|
-
|
|
227
|
-
| Field | Type | Required | Description |
|
|
228
|
-
| ------------- | -------- | -------- | ---------------------------------------------------------------------------------- |
|
|
229
|
-
| `$schema` | string | yes | `"https://schemas.dfos.com/post/v1"` |
|
|
230
|
-
| `format` | enum | yes | `"short-post"`, `"long-post"`, `"comment"`, `"reply"` — immutable, set at creation |
|
|
231
|
-
| `title` | string | no | Post title (typically for long-post format) |
|
|
232
|
-
| `body` | string | no | Post body content |
|
|
233
|
-
| `cover` | media | no | Cover image |
|
|
234
|
-
| `attachments` | media[] | no | Attached media objects |
|
|
235
|
-
| `topics` | string[] | no | Topic names (stored as names for portability) |
|
|
236
|
-
|
|
237
|
-
#### Profile (`https://schemas.dfos.com/profile/v1`)
|
|
238
|
-
|
|
239
|
-
The displayable identity for any agent, person, group, or space.
|
|
240
|
-
|
|
241
|
-
| Field | Type | Required | Description |
|
|
242
|
-
| ------------- | ------ | -------- | --------------------------------------- |
|
|
243
|
-
| `$schema` | string | yes | `"https://schemas.dfos.com/profile/v1"` |
|
|
244
|
-
| `name` | string | no | Display name |
|
|
245
|
-
| `description` | string | no | Short bio or description |
|
|
246
|
-
| `avatar` | media | no | Avatar image |
|
|
247
|
-
| `banner` | media | no | Banner image |
|
|
248
|
-
| `background` | media | no | Background image |
|
|
249
|
-
|
|
250
|
-
### Media Object
|
|
251
|
-
|
|
252
|
-
Several schemas reference media objects. The standard representation:
|
|
253
|
-
|
|
254
|
-
```json
|
|
255
|
-
{
|
|
256
|
-
"id": "media_abc123",
|
|
257
|
-
"uri": "https://cdn.example.com/media/abc123.jpg"
|
|
258
|
-
}
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
`id` is required (opaque identifier). `uri` is optional.
|
|
262
|
-
|
|
263
|
-
### Custom Schemas
|
|
264
|
-
|
|
265
|
-
Any implementation can define custom document schemas following the same pattern — a JSON Schema with a `$schema` const field pointing to a unique URI. The protocol will commit to the document via CID regardless of what's inside. The standard schemas are conventions, not constraints.
|
|
266
|
-
|
|
267
|
-
---
|
|
268
|
-
|
|
269
168
|
## Standards and Dependencies
|
|
270
169
|
|
|
271
170
|
| Component | Standard / Library |
|
|
@@ -275,7 +174,6 @@ Any implementation can define custom document schemas following the same pattern
|
|
|
275
174
|
| Key encoding | W3C Multikey (multicodec `0xed01` + base58btc multibase) |
|
|
276
175
|
| Signed envelopes | JWS Compact Serialization (RFC 7515) with `alg: "EdDSA"` |
|
|
277
176
|
| Content addressing | CIDv1 with dag-cbor codec (`0x71`) + SHA-256 multihash (`0x12`) |
|
|
278
|
-
| Auth tokens | JWT (RFC 7519) with `alg: "EdDSA"` |
|
|
279
177
|
| ID encoding | SHA-256 → custom 19-char alphabet, 22 characters |
|
|
280
178
|
|
|
281
179
|
### ID Alphabet
|
|
@@ -286,7 +184,7 @@ Length: 22 characters
|
|
|
286
184
|
Entropy: ~93.4 bits (19^22)
|
|
287
185
|
```
|
|
288
186
|
|
|
289
|
-
Process: `SHA-256(input) → for each of first 22 bytes: alphabet[byte % 19]
|
|
187
|
+
Process: `SHA-256(input) → for each of first 22 bytes: alphabet[byte % 19]`. The modulo introduces a ~0.3% bias (256 is not evenly divisible by 19) — not security-relevant for identifiers.
|
|
290
188
|
|
|
291
189
|
DIDs: `did:dfos:` + 22-char ID derived from `SHA-256(genesis CID raw bytes)`
|
|
292
190
|
Key IDs: `key_` + 22-char ID. Convention: derive from public key hash (`key_` + `customAlpha(SHA-256(publicKey))`), making key IDs deterministic and verifiable. Not a protocol requirement — key IDs can be any string.
|
|
@@ -414,7 +312,7 @@ DID: did:dfos:e3vvtck42d4eacdnzvtrn6
|
|
|
414
312
|
createdAt: string,
|
|
415
313
|
note: string | null }
|
|
416
314
|
|
|
417
|
-
// Permanent
|
|
315
|
+
// Permanent destruction
|
|
418
316
|
{ version: 1, type: "delete",
|
|
419
317
|
previousOperationCID: string,
|
|
420
318
|
createdAt: string,
|
|
@@ -443,11 +341,18 @@ token = signingInput + "." + base64url(signature)
|
|
|
443
341
|
|
|
444
342
|
### kid Rules
|
|
445
343
|
|
|
446
|
-
| Context | kid format | Example
|
|
447
|
-
| ------------------------- | ----------- |
|
|
448
|
-
| Identity create (genesis) | Bare key ID | `key_r9ev34fvc23z999veaaft8`
|
|
449
|
-
| Identity update/delete | DID URL |
|
|
450
|
-
| All content ops | DID URL |
|
|
344
|
+
| Context | kid format | Example |
|
|
345
|
+
| ------------------------- | ----------- | ---------------------------- |
|
|
346
|
+
| Identity create (genesis) | Bare key ID | `key_r9ev34fvc23z999veaaft8` |
|
|
347
|
+
| Identity update/delete | DID URL | See below |
|
|
348
|
+
| All content ops | DID URL | See below |
|
|
349
|
+
|
|
350
|
+
DID URL examples:
|
|
351
|
+
|
|
352
|
+
```
|
|
353
|
+
did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
354
|
+
did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
|
|
355
|
+
```
|
|
451
356
|
|
|
452
357
|
### `cid` Header
|
|
453
358
|
|
|
@@ -465,11 +370,7 @@ Every operation JWS (identity-op and content-op) includes a `cid` field in the p
|
|
|
465
370
|
- `header.cid` is missing
|
|
466
371
|
- `header.cid` does not match the derived CID
|
|
467
372
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
- **Pre-verification routing**: The operation CID can be read from the header without parsing the payload or running dag-cbor encoding
|
|
471
|
-
- **Cross-implementation consistency**: A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations
|
|
472
|
-
- **Self-documenting tokens**: Each JWS token declares its content-addressed identity
|
|
373
|
+
A CID mismatch between header and derived value immediately surfaces dag-cbor encoding disagreements across implementations.
|
|
473
374
|
|
|
474
375
|
Note: JWT tokens (device auth) do NOT include a `cid` header — this field is specific to operation JWS tokens.
|
|
475
376
|
|
|
@@ -521,7 +422,7 @@ Where `idEncode` is the 19-char alphabet encoding described above.
|
|
|
521
422
|
|
|
522
423
|
## Deterministic Reference Artifacts
|
|
523
424
|
|
|
524
|
-
All
|
|
425
|
+
All artifacts below are deterministic and reproducible from fixed seeds. An independent implementer can verify every value using standard Ed25519 + dag-cbor libraries. Private keys are derived from `SHA-256(UTF8("dfos-protocol-reference-key-N"))`.
|
|
525
426
|
|
|
526
427
|
### Key 1 (Genesis Controller)
|
|
527
428
|
|
|
@@ -599,7 +500,11 @@ JWS Token:
|
|
|
599
500
|
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJjaWQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSJ9.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiYXV0aEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImFzc2VydEtleXMiOlt7ImlkIjoia2V5X3I5ZXYzNGZ2YzIzejk5OXZlYWFmdDgiLCJ0eXBlIjoiTXVsdGlrZXkiLCJwdWJsaWNLZXlNdWx0aWJhc2UiOiJ6Nk1rcnpMTU53b0pTVjRQM1ljY1djYnRrOHZkOUx0Z01LbkxlYURMVXFMdUFTamIifV0sImNvbnRyb2xsZXJLZXlzIjpbeyJpZCI6ImtleV9yOWV2MzRmdmMyM3o5OTl2ZWFhZnQ4IiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa3J6TE1Od29KU1Y0UDNZY2NXY2J0azh2ZDlMdGdNS25MZWFETFVxTHVBU2piIn1dLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAwOjAwLjAwMFoifQ.EDryDK1uvtix-17cHun9t6MacFIx2rMmMF1QLzfD5TFlSsOvMcue97pCgGn3CXeLVFtVxgpCoh0kGSXioKKzAw
|
|
600
501
|
```
|
|
601
502
|
|
|
602
|
-
Operation CID:
|
|
503
|
+
Operation CID:
|
|
504
|
+
|
|
505
|
+
```
|
|
506
|
+
bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
507
|
+
```
|
|
603
508
|
|
|
604
509
|
**Derived DID: `did:dfos:e3vvtck42d4eacdnzvtrn6`**
|
|
605
510
|
|
|
@@ -660,7 +565,11 @@ JWS Token:
|
|
|
660
565
|
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmlkZW50aXR5LW9wIiwia2lkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiNrZXlfcjlldjM0ZnZjMjN6OTk5dmVhYWZ0OCIsImNpZCI6ImJhZnlyZWljeW00Y3lpZWRubGQ3M3NtYngzMnN6YWVpN3hkdWxxbjRnM3N0ZTVlMncydWxhanIzb3FtIn0.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwicHJldmlvdXNPcGVyYXRpb25DSUQiOiJiYWZ5cmVpYmFuanBnY3FmZmNmaHI0c3B0empmdGhoNXN6b2hoYm81dGpmdWxlbWt3N3VoZGVuNXVxeSIsImF1dGhLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJhc3NlcnRLZXlzIjpbeyJpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwidHlwZSI6Ik11bHRpa2V5IiwicHVibGljS2V5TXVsdGliYXNlIjoiejZNa2ZVZDY1SnJBaGZkZ0Z1TUNjY1U5VGhRdmpCMmZKQU1VSGt1dWFqRjk5MmdLIn1dLCJjb250cm9sbGVyS2V5cyI6W3siaWQiOiJrZXlfZXo5YTg3NHRja3IzZHY5MzNkM2NrZCIsInR5cGUiOiJNdWx0aWtleSIsInB1YmxpY0tleU11bHRpYmFzZSI6Ino2TWtmVWQ2NUpyQWhmZGdGdU1DY2NVOVRoUXZqQjJmSkFNVUhrdXVhakY5OTJnSyJ9XSwiY3JlYXRlZEF0IjoiMjAyNi0wMy0wN1QwMDowMTowMC4wMDBaIn0.MScuoBlgOK3j5QX9tFcw1ou0o4LgJziGJEsZ5pvqiBr1SagAyAv5h-wajQhtg8IP7dLlM0U4leW2iRra945cDg
|
|
661
566
|
```
|
|
662
567
|
|
|
663
|
-
Operation CID:
|
|
568
|
+
Operation CID:
|
|
569
|
+
|
|
570
|
+
```
|
|
571
|
+
bafyreicym4cyiednld73smbx32szaei7xdulqn4g3ste5e2w2ulajr3oqm
|
|
572
|
+
```
|
|
664
573
|
|
|
665
574
|
Post-rotation: DID unchanged (`did:dfos:e3vvtck42d4eacdnzvtrn6`), controller rotated to `key_ez9a874tckr3dv933d3ckd`.
|
|
666
575
|
|
|
@@ -682,7 +591,11 @@ Document (application layer):
|
|
|
682
591
|
}
|
|
683
592
|
```
|
|
684
593
|
|
|
685
|
-
Document CID:
|
|
594
|
+
Document CID:
|
|
595
|
+
|
|
596
|
+
```
|
|
597
|
+
bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne
|
|
598
|
+
```
|
|
686
599
|
|
|
687
600
|
Content Create JWS Header:
|
|
688
601
|
|
|
@@ -719,7 +632,11 @@ Content Create JWS Token:
|
|
|
719
632
|
eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWE1ejd6eGtuYWU1ZHM3MmV1aWh1ZjJyZzNpeGw2dDRmYnpqZWZoY29nZzNucXBweW9ncXUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZG9jdW1lbnRDSUQiOiJiYWZ5cmVpZnB2d3Vhcm1sNjJzZm9nZHBpMnZsbHR2ZzJldjZvNHh0dzc0emZ1ZDdjcGtnNzQyNnpuZSIsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.t_DDkJ_TmNekIGUFO22G-W78QoE4XTg9LKQ4gzAQHaK3B6491Tir9b-wtp-hcwmENu2Hqnieqv5ASiqfFrEbDw
|
|
720
633
|
```
|
|
721
634
|
|
|
722
|
-
Content Operation CID:
|
|
635
|
+
Content Operation CID:
|
|
636
|
+
|
|
637
|
+
```
|
|
638
|
+
bafyreia5z7zxknae5ds72euihuf2rg3ixl6t4fbzjefhcogg3nqppyogqu
|
|
639
|
+
```
|
|
723
640
|
|
|
724
641
|
### Content Chain: Update
|
|
725
642
|
|
|
@@ -752,34 +669,16 @@ Updated document:
|
|
|
752
669
|
}
|
|
753
670
|
```
|
|
754
671
|
|
|
755
|
-
Document CID (edited):
|
|
756
|
-
Content Update CID: `bafyreibb4lsvqmz4j76rsvhkqw3v2b4vp23t7dimm6vl5g5wlninvkemxq`
|
|
757
|
-
|
|
758
|
-
### EdDSA JWT
|
|
759
|
-
|
|
760
|
-
Header:
|
|
672
|
+
Document CID (edited):
|
|
761
673
|
|
|
762
|
-
```json
|
|
763
|
-
{ "alg": "EdDSA", "typ": "JWT", "kid": "key_ez9a874tckr3dv933d3ckd" }
|
|
764
674
|
```
|
|
765
|
-
|
|
766
|
-
Payload:
|
|
767
|
-
|
|
768
|
-
```json
|
|
769
|
-
{
|
|
770
|
-
"iss": "dfos",
|
|
771
|
-
"sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
772
|
-
"aud": "dfos-api",
|
|
773
|
-
"exp": 1772902800,
|
|
774
|
-
"iat": 1772899200,
|
|
775
|
-
"jti": "session_ref_example_01"
|
|
776
|
-
}
|
|
675
|
+
bafyreieuo26zfmjxwpmw5jk6bqzqhvivxcbckgxtyeuc7ypf3p4sihgq4q
|
|
777
676
|
```
|
|
778
677
|
|
|
779
|
-
|
|
678
|
+
Content Update CID:
|
|
780
679
|
|
|
781
680
|
```
|
|
782
|
-
|
|
681
|
+
bafyreibb4lsvqmz4j76rsvhkqw3v2b4vp23t7dimm6vl5g5wlninvkemxq
|
|
783
682
|
```
|
|
784
683
|
|
|
785
684
|
---
|
|
@@ -788,86 +687,83 @@ eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCIsImtpZCI6ImtleV9lejlhODc0dGNrcjNkdjkzM2QzY2tk
|
|
|
788
687
|
|
|
789
688
|
Given the artifacts above, verify:
|
|
790
689
|
|
|
791
|
-
1. **Multikey decode**:
|
|
690
|
+
1. **Multikey decode**: strip `z`, base58btc decode, strip `[0xed, 0x01]` prefix → raw public key:
|
|
792
691
|
|
|
793
|
-
|
|
692
|
+
```
|
|
693
|
+
z6MkrzLMNwoJSV4P3YccWcbtk8vd9LtgMKnLeaDLUqLuASjb
|
|
694
|
+
→ ba421e272fad4f941c221e47f87d9253bdc04f7d4ad2625ae667ab9f0688ce32
|
|
695
|
+
```
|
|
794
696
|
|
|
795
|
-
|
|
697
|
+
2. **Genesis JWS verify**: split token on `.`, take first two segments as signing input (UTF-8 bytes), base64url-decode third segment as 64-byte signature, `ed25519.verify(signature, signingInputBytes, publicKey)` → true. The header contains `cid` alongside `alg`, `typ`, and `kid`.
|
|
698
|
+
|
|
699
|
+
3. **Genesis CID**: base64url-decode JWS payload → parse JSON → dag-cbor canonical encode → SHA-256 → CIDv1 → should be:
|
|
700
|
+
|
|
701
|
+
```
|
|
702
|
+
bafyreibanjpgcqffcfhr4sptzjfthh5szohhbo5tjfulemkw7uhden5uqy
|
|
703
|
+
```
|
|
796
704
|
|
|
797
705
|
4. **CID header**: Verify each operation JWS header contains `cid` matching the derived operation CID
|
|
798
706
|
|
|
799
707
|
5. **DID derivation**: take raw CID bytes of genesis CID → SHA-256 → first 22 bytes → `byte % 19` → alphabet lookup → should be `e3vvtck42d4eacdnzvtrn6` → DID = `did:dfos:e3vvtck42d4eacdnzvtrn6`
|
|
800
708
|
|
|
801
|
-
6. **Rotation JWS**:
|
|
709
|
+
6. **Rotation JWS**: signed by OLD controller key (key 1). Verify with key 1's public key. kid:
|
|
802
710
|
|
|
803
|
-
|
|
711
|
+
```
|
|
712
|
+
did:dfos:e3vvtck42d4eacdnzvtrn6#key_r9ev34fvc23z999veaaft8
|
|
713
|
+
```
|
|
804
714
|
|
|
805
|
-
|
|
715
|
+
7. **Content create JWS**: signed by NEW controller key (key 2, post-rotation). Verify with key 2's public key. kid:
|
|
806
716
|
|
|
807
|
-
|
|
717
|
+
```
|
|
718
|
+
did:dfos:e3vvtck42d4eacdnzvtrn6#key_ez9a874tckr3dv933d3ckd
|
|
719
|
+
```
|
|
808
720
|
|
|
809
|
-
|
|
721
|
+
8. **Document CID**: dag-cbor canonical encode the document JSON → SHA-256 → CIDv1 → should be:
|
|
810
722
|
|
|
811
|
-
|
|
723
|
+
```
|
|
724
|
+
bafyreifpvwuarml62sfogdpi2vlltvg2ev6o4xtw74zfud7cpkg7426zne
|
|
725
|
+
```
|
|
812
726
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
All source lives in `packages/dfos-protocol/` — self-contained, zero monorepo dependencies.
|
|
816
|
-
|
|
817
|
-
| File | Contents |
|
|
818
|
-
| ----------------------------------- | -------------------------------------------------------------------------------------------------- |
|
|
819
|
-
| `src/crypto/ed25519.ts` | `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature` |
|
|
820
|
-
| `src/crypto/jws.ts` | `createJws`, `verifyJws`, `decodeJwsUnsafe`, `JwsVerificationError` |
|
|
821
|
-
| `src/crypto/jwt.ts` | `createJwt`, `verifyJwt`, `decodeJwtUnsafe` (EdDSA only) |
|
|
822
|
-
| `src/crypto/base64url.ts` | `base64urlEncode`, `base64urlDecode` |
|
|
823
|
-
| `src/crypto/multiformats.ts` | `dagCborCanonicalEncode`, `dagCborCanonicalEqual` |
|
|
824
|
-
| `src/crypto/id.ts` | `generateId`, `generateIdNoPrefix`, `isValidId` |
|
|
825
|
-
| `src/chain/multikey.ts` | `encodeEd25519Multikey`, `decodeMultikey` |
|
|
826
|
-
| `src/chain/schemas.ts` | `IdentityOperation`, `ContentOperation`, `MultikeyPublicKey`, `VerifiedIdentity` |
|
|
827
|
-
| `src/chain/identity-chain.ts` | `signIdentityOperation`, `verifyIdentityChain` |
|
|
828
|
-
| `src/chain/content-chain.ts` | `signContentOperation`, `verifyContentChain` |
|
|
829
|
-
| `src/chain/derivation.ts` | `deriveChainIdentifier` |
|
|
830
|
-
| `src/registry/schemas.ts` | Registry API Zod types (wire contract) |
|
|
831
|
-
| `src/registry/server.ts` | Reference Hono registry server |
|
|
832
|
-
| `src/registry/store.ts` | In-memory chain store with linear enforcement |
|
|
833
|
-
| `openapi.yaml` | OpenAPI 3.1 spec for registry API |
|
|
834
|
-
| `schemas/document-envelope.v1.json` | JSON Schema for the document envelope wrapper |
|
|
835
|
-
| `schemas/post.v1.json` | JSON Schema for post documents |
|
|
836
|
-
| `schemas/profile.v1.json` | JSON Schema for profile documents |
|
|
837
|
-
| `tests/protocol-reference.spec.ts` | Deterministic artifact generator (this doc's source) |
|
|
838
|
-
| `verify/go/` | Go cross-language verification (9 tests) |
|
|
839
|
-
| `verify/python/` | Python cross-language verification (32 checks) |
|
|
840
|
-
| `verify/rust/` | Rust cross-language verification (9 tests) |
|
|
841
|
-
| `verify/swift/` | Swift cross-language verification (8 tests) |
|
|
727
|
+
9. **Content chain integrity**: update's `previousOperationCID` matches create's operation CID
|
|
842
728
|
|
|
843
|
-
|
|
729
|
+
10. **Chain completeness**: all operation CIDs, DID derivation, key rotation, and content chain linkage verified end-to-end.
|
|
844
730
|
|
|
845
|
-
|
|
731
|
+
---
|
|
846
732
|
|
|
847
|
-
|
|
733
|
+
## Source and Verification
|
|
848
734
|
|
|
849
|
-
|
|
850
|
-
- `tests/chain.spec.ts` (39): multikey encoding, identity chain (genesis, DID, rotation, delete, cid header, errors), content chain (lifecycle, clear, delete, cid header, errors)
|
|
851
|
-
- `tests/registry.spec.ts` (18): HTTP contract — submission, resubmission, extension, fork rejection, pagination, cross-chain key resolution, 404s
|
|
852
|
-
- `tests/schemas.spec.ts` (28): JSON Schema compilation + validation for document envelope, post, profile — conforming documents, missing fields, invalid values, additional properties
|
|
853
|
-
- `tests/protocol-reference.spec.ts` (1): deterministic artifact generator
|
|
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.
|
|
854
736
|
|
|
855
|
-
|
|
737
|
+
- [`crypto/ed25519`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/ed25519.ts) — `createNewEd25519Keypair`, `importEd25519Keypair`, `signPayloadEd25519`, `isValidEd25519Signature`
|
|
738
|
+
- [`crypto/jws`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/jws.ts) — `createJws`, `verifyJws`, `decodeJwsUnsafe`
|
|
739
|
+
- [`crypto/base64url`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/base64url.ts) — `base64urlEncode`, `base64urlDecode`
|
|
740
|
+
- [`crypto/multiformats`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/multiformats.ts) — `dagCborCanonicalEncode`, `dagCborCanonicalEqual`
|
|
741
|
+
- [`crypto/id`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/crypto/id.ts) — `generateId`, `generateIdNoPrefix`, `isValidId`
|
|
742
|
+
- [`chain/multikey`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/multikey.ts) — `encodeEd25519Multikey`, `decodeMultikey`
|
|
743
|
+
- [`chain/schemas`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/schemas.ts) — `IdentityOperation`, `ContentOperation`, `MultikeyPublicKey`, `VerifiedIdentity`
|
|
744
|
+
- [`chain/identity-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/identity-chain.ts) — `signIdentityOperation`, `verifyIdentityChain`
|
|
745
|
+
- [`chain/content-chain`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/content-chain.ts) — `signContentOperation`, `verifyContentChain`
|
|
746
|
+
- [`chain/derivation`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/chain/derivation.ts) — `deriveChainIdentifier`, `deriveContentId`
|
|
856
747
|
|
|
857
|
-
|
|
858
|
-
- Dependencies: `pynacl`, `dag-cbor`, `base58`
|
|
748
|
+
### Related Specifications
|
|
859
749
|
|
|
860
|
-
|
|
750
|
+
- [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
|
|
861
753
|
|
|
862
|
-
|
|
863
|
-
- Dependencies: `fxamacker/cbor/v2`, `mr-tron/base58`
|
|
754
|
+
### Cross-Language Verification
|
|
864
755
|
|
|
865
|
-
|
|
756
|
+
| Language | Tests | Source |
|
|
757
|
+
| ---------- | ----- | ---------------------------------------------------------------------------------------------------- |
|
|
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) |
|
|
866
763
|
|
|
867
|
-
|
|
868
|
-
- Dependencies: `ed25519-dalek`, `ciborium`, `sha2`, `bs58`, `base64`, `data-encoding`
|
|
764
|
+
---
|
|
869
765
|
|
|
870
|
-
|
|
766
|
+
## Special Thanks
|
|
871
767
|
|
|
872
|
-
-
|
|
873
|
-
-
|
|
768
|
+
- **Vinny Bellavia** — [stcisgood.com](https://stcisgood.com)
|
|
769
|
+
- **Allison Clift-Jennings** — [Jura Labs](https://juralabs.com)
|