@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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
declare const hexToBytes: (hex: string) => Uint8Array;
|
|
2
|
+
/**
|
|
3
|
+
* Hash a leaf node — SHA-256 of UTF-8 encoded contentId
|
|
4
|
+
*/
|
|
5
|
+
declare const hashLeaf: (contentId: string) => Promise<Uint8Array>;
|
|
6
|
+
/**
|
|
7
|
+
* Build a sorted binary Merkle tree over content identifiers
|
|
8
|
+
*
|
|
9
|
+
* ContentIds are sorted lexicographically, hashed to leaves, then paired
|
|
10
|
+
* and hashed up to the root. Odd nodes are promoted to the next level.
|
|
11
|
+
*
|
|
12
|
+
* Returns null root for empty input. Deduplicates contentIds.
|
|
13
|
+
*/
|
|
14
|
+
declare const buildMerkleTree: (contentIds: string[]) => Promise<{
|
|
15
|
+
root: string | null;
|
|
16
|
+
leafCount: number;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
interface MerkleProof {
|
|
20
|
+
/** The contentId being proven */
|
|
21
|
+
contentId: string;
|
|
22
|
+
/** Hex SHA-256 root of the tree */
|
|
23
|
+
root: string;
|
|
24
|
+
/** Sibling hashes along the path from leaf to root */
|
|
25
|
+
path: Array<{
|
|
26
|
+
/** Hex SHA-256 of sibling node */
|
|
27
|
+
hash: string;
|
|
28
|
+
/** Position of the sibling relative to the current node */
|
|
29
|
+
position: 'left' | 'right';
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Generate an inclusion proof for a contentId in the set
|
|
34
|
+
*
|
|
35
|
+
* Returns null if the contentId is not in the set.
|
|
36
|
+
*/
|
|
37
|
+
declare const generateMerkleProof: (contentIds: string[], targetId: string) => Promise<MerkleProof | null>;
|
|
38
|
+
/**
|
|
39
|
+
* Verify a Merkle inclusion proof
|
|
40
|
+
*
|
|
41
|
+
* Recomputes the root from the leaf and proof path, compares to the claimed root.
|
|
42
|
+
*/
|
|
43
|
+
declare const verifyMerkleProof: (proof: MerkleProof) => Promise<boolean>;
|
|
44
|
+
|
|
45
|
+
export { type MerkleProof, buildMerkleTree, generateMerkleProof, hashLeaf, hexToBytes, verifyMerkleProof };
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"description": "Content chain: create + delete",
|
|
3
3
|
"type": "content",
|
|
4
4
|
"chain": [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg",
|
|
6
|
+
"eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWQydjUyN2lxMzZ2dnUyNWc3b3BjczJudjRjbmRhcWdrd29naHEzdHB3Z215cnBxZWVuZ2EifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiZGVsZXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsInByZXZpb3VzT3BlcmF0aW9uQ0lEIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUiLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAzOjAwLjAwMFoiLCJub3RlIjoicmVtb3ZpbmcgY29udGVudCJ9.9DTnVeYjVV6Kc971etvWeNn9iMme7RnrRZRmYBO0iaYeePJDblL_p2uuEIuxtAQQp1bATpsQ475zpK6N2A0eAg"
|
|
7
7
|
],
|
|
8
8
|
"signerPublicKey": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK",
|
|
9
9
|
"documents": [
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"$schema": "https://schemas.dfos.com/post/v1",
|
|
13
13
|
"format": "short-post",
|
|
14
14
|
"title": "Hello World",
|
|
15
|
-
"body": "First post on the protocol."
|
|
15
|
+
"body": "First post on the protocol.",
|
|
16
|
+
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
|
|
16
17
|
},
|
|
17
18
|
"baseDocumentCID": null,
|
|
18
19
|
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
}
|
|
21
22
|
],
|
|
22
23
|
"expected": {
|
|
23
|
-
"contentId": "
|
|
24
|
+
"contentId": "a82z92a3hndk6c97thcrn8",
|
|
24
25
|
"isDeleted": true,
|
|
25
26
|
"currentDocumentCID": null,
|
|
26
27
|
"length": 2
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
"description": "Content chain: create + update (with both documents)",
|
|
3
3
|
"type": "content",
|
|
4
4
|
"chain": [
|
|
5
|
-
"
|
|
6
|
-
"
|
|
5
|
+
"eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoiY3JlYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsImRvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJiYXNlRG9jdW1lbnRDSUQiOm51bGwsImNyZWF0ZWRBdCI6IjIwMjYtMDMtMDdUMDA6MDI6MDAuMDAwWiIsIm5vdGUiOm51bGx9.Rv6vlz5MfrwqDUrSVIGs4ZfeBbkQUSBcXhxwZ6hfudSr5MxhYl08hTqLDOA0W1NMjN0Hs0IW9jXTwLwP1dMDBg",
|
|
6
|
+
"eyJhbGciOiJFZERTQSIsInR5cCI6ImRpZDpkZm9zOmNvbnRlbnQtb3AiLCJraWQiOiJkaWQ6ZGZvczplM3Z2dGNrNDJkNGVhY2RuenZ0cm42I2tleV9lejlhODc0dGNrcjNkdjkzM2QzY2tkIiwiY2lkIjoiYmFmeXJlaWg2ZTVjYmppdHBvemh6aGdtZmt0bWlvaG14eW4zdWN3aHFkM21qaXhpenZ3bWxodjdobTQifQ.eyJ2ZXJzaW9uIjoxLCJ0eXBlIjoidXBkYXRlIiwiZGlkIjoiZGlkOmRmb3M6ZTN2dnRjazQyZDRlYWNkbnp2dHJuNiIsInByZXZpb3VzT3BlcmF0aW9uQ0lEIjoiYmFmeXJlaWFlZGhqcTY0YWFqcHdvY2lhaGw1dzM3ajZ1b3hyNW1vam9xNWRuYWg2ZnB2eHI1ZDRseHUiLCJkb2N1bWVudENJRCI6ImJhZnlyZWlkaDdlMzZjdnd5M3V3NXlwaXRjcWs3dW9rdGJra2tqN2U2aHhoa3k0bzc1cnhuN2t4aWx1IiwiYmFzZURvY3VtZW50Q0lEIjoiYmFmeXJlaWh6d3VvdXBmZzNkeGlwNnhtZ3pteHN5d3lpaTJqZW94eHpiZ3gzenhtMmluN2tub2kzZzQiLCJjcmVhdGVkQXQiOiIyMDI2LTAzLTA3VDAwOjAzOjAwLjAwMFoiLCJub3RlIjoiZWRpdGVkIHRpdGxlIGFuZCBib2R5In0.46H3WBKigCzUrIOvS2ekYJFdK1OeLhqvcZTj8FBqFNKO1ivs3oC0ui_hzTKkKO-8uevMjf9-k0GOjyw2u16PDA"
|
|
7
7
|
],
|
|
8
8
|
"signerPublicKey": "z6MkfUd65JrAhfdgFuMCccU9ThQvjB2fJAMUHkuuajF992gK",
|
|
9
9
|
"documents": [
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"$schema": "https://schemas.dfos.com/post/v1",
|
|
13
13
|
"format": "short-post",
|
|
14
14
|
"title": "Hello World",
|
|
15
|
-
"body": "First post on the protocol."
|
|
15
|
+
"body": "First post on the protocol.",
|
|
16
|
+
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
|
|
16
17
|
},
|
|
17
18
|
"baseDocumentCID": null,
|
|
18
19
|
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
@@ -23,17 +24,18 @@
|
|
|
23
24
|
"$schema": "https://schemas.dfos.com/post/v1",
|
|
24
25
|
"format": "short-post",
|
|
25
26
|
"title": "Hello World (edited)",
|
|
26
|
-
"body": "Updated content."
|
|
27
|
+
"body": "Updated content.",
|
|
28
|
+
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6"
|
|
27
29
|
},
|
|
28
|
-
"baseDocumentCID": "
|
|
30
|
+
"baseDocumentCID": "bafyreihzwuoupfg3dxip6xmgzmxsywyii2jeoxxzbgx3zxm2in7knoi3g4",
|
|
29
31
|
"createdByDID": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
30
32
|
"createdAt": "2026-03-07T00:03:00.000Z"
|
|
31
33
|
}
|
|
32
34
|
],
|
|
33
35
|
"expected": {
|
|
34
|
-
"contentId": "
|
|
36
|
+
"contentId": "a82z92a3hndk6c97thcrn8",
|
|
35
37
|
"isDeleted": false,
|
|
36
|
-
"currentDocumentCID": "
|
|
38
|
+
"currentDocumentCID": "bafyreidh7e36cvwy3uw5ypitcqk7uoktbkkkj7e6hxhky4o75rxn7kxilu",
|
|
37
39
|
"length": 2
|
|
38
40
|
}
|
|
39
41
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metalabel/dfos-protocol",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DFOS Protocol — Ed25519 signed chain primitives,
|
|
5
|
+
"description": "DFOS Protocol — Ed25519 signed chain primitives, beacons, merkle trees, and verification",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "Metalabel <hello@metalabel.com> (https://metalabel.com)",
|
|
8
8
|
"repository": {
|
|
@@ -37,9 +37,9 @@
|
|
|
37
37
|
"import": "./dist/chain/index.js",
|
|
38
38
|
"types": "./dist/chain/index.d.ts"
|
|
39
39
|
},
|
|
40
|
-
"./
|
|
41
|
-
"import": "./dist/
|
|
42
|
-
"types": "./dist/
|
|
40
|
+
"./merkle": {
|
|
41
|
+
"import": "./dist/merkle/index.js",
|
|
42
|
+
"types": "./dist/merkle/index.d.ts"
|
|
43
43
|
}
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
@@ -49,8 +49,6 @@
|
|
|
49
49
|
"PROTOCOL.md",
|
|
50
50
|
"DID-METHOD.md",
|
|
51
51
|
"CONTENT-MODEL.md",
|
|
52
|
-
"REGISTRY-API.md",
|
|
53
|
-
"openapi.yaml",
|
|
54
52
|
"LICENSE",
|
|
55
53
|
"README.md"
|
|
56
54
|
],
|
|
@@ -58,7 +56,6 @@
|
|
|
58
56
|
"@ipld/dag-cbor": "^9.2.5",
|
|
59
57
|
"@noble/curves": "^2.0.1",
|
|
60
58
|
"@noble/hashes": "^2.0.1",
|
|
61
|
-
"hono": "^4.12.3",
|
|
62
59
|
"multiformats": "^13.4.2",
|
|
63
60
|
"zod": "^4.3.6"
|
|
64
61
|
},
|
|
@@ -75,6 +72,6 @@
|
|
|
75
72
|
"clean": "rm -rf dist",
|
|
76
73
|
"typecheck": "tsc --noEmit",
|
|
77
74
|
"test": "vitest run",
|
|
78
|
-
"generate-examples": "
|
|
75
|
+
"generate-examples": "tsx scripts/generate-examples.ts"
|
|
79
76
|
}
|
|
80
77
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://schemas.dfos.com/manifest/v1",
|
|
4
|
+
"title": "Manifest",
|
|
5
|
+
"description": "A manifest — a named map of protocol object references for semantic navigation. Keys are path-like labels. Values are protocol references: content chain identifiers (22-char bare hash), DIDs (did:dfos:...), or CIDs (bafyrei...). The document layer of the dark forest. Discovery is social or out-of-band.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["$schema", "entries"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"$schema": {
|
|
10
|
+
"const": "https://schemas.dfos.com/manifest/v1"
|
|
11
|
+
},
|
|
12
|
+
"entries": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"propertyNames": {
|
|
15
|
+
"pattern": "^[a-z0-9][a-z0-9._/-]*[a-z0-9]$",
|
|
16
|
+
"minLength": 2,
|
|
17
|
+
"maxLength": 128
|
|
18
|
+
},
|
|
19
|
+
"additionalProperties": {
|
|
20
|
+
"type": "string",
|
|
21
|
+
"pattern": "^[a-z0-9][a-z0-9:._/-]*$",
|
|
22
|
+
"minLength": 1,
|
|
23
|
+
"maxLength": 512
|
|
24
|
+
},
|
|
25
|
+
"description": "Named entries mapping path-like keys to protocol object references (contentId, DID, or CID)."
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"additionalProperties": false
|
|
29
|
+
}
|
package/schemas/post.v1.json
CHANGED
|
@@ -35,6 +35,11 @@
|
|
|
35
35
|
"type": "array",
|
|
36
36
|
"items": { "type": "string" },
|
|
37
37
|
"description": "Topic names this post belongs to. Stored as names for portability."
|
|
38
|
+
},
|
|
39
|
+
"createdByDID": {
|
|
40
|
+
"type": "string",
|
|
41
|
+
"pattern": "^did:",
|
|
42
|
+
"description": "DID of the identity that authored this content. Optional — omit when the signer IS the author."
|
|
38
43
|
}
|
|
39
44
|
},
|
|
40
45
|
"additionalProperties": false,
|
package/schemas/profile.v1.json
CHANGED
|
@@ -28,6 +28,11 @@
|
|
|
28
28
|
"background": {
|
|
29
29
|
"$ref": "#/$defs/media",
|
|
30
30
|
"description": "Background image."
|
|
31
|
+
},
|
|
32
|
+
"createdByDID": {
|
|
33
|
+
"type": "string",
|
|
34
|
+
"pattern": "^did:",
|
|
35
|
+
"description": "DID of the identity that authored this content. Optional — omit when the signer IS the author."
|
|
31
36
|
}
|
|
32
37
|
},
|
|
33
38
|
"additionalProperties": false,
|
package/REGISTRY-API.md
DELETED
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
# DFOS Registry API
|
|
2
|
-
|
|
3
|
-
Minimal HTTP API for chain storage, retrieval, and resolution. Any server implementing these endpoints with these semantics is a compatible DFOS registry.
|
|
4
|
-
|
|
5
|
-
The protocol is transport-agnostic — chains can be exchanged through any mechanism. This API defines one standard transport binding: a REST interface for submitting chains, resolving identities and content, and retrieving operations and documents.
|
|
6
|
-
|
|
7
|
-
[Protocol Specification](https://protocol.dfos.com/spec) · [OpenAPI Spec](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/openapi.yaml) · [Reference Implementation](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/registry/server.ts)
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Overview
|
|
12
|
-
|
|
13
|
-
The registry stores verified chains and serves resolved state. It enforces **linear chain integrity** — it accepts chains that are the same length or longer than what's stored, and rejects forks (two different operations at the same chain position).
|
|
14
|
-
|
|
15
|
-
Six endpoints, two concerns:
|
|
16
|
-
|
|
17
|
-
| Concern | Endpoints |
|
|
18
|
-
| ------------------- | ----------------------------------------------- |
|
|
19
|
-
| **Identity chains** | Submit chain, resolve identity, list operations |
|
|
20
|
-
| **Content chains** | Submit chain, resolve content, list operations |
|
|
21
|
-
| **Lookup** | Resolve operation by CID |
|
|
22
|
-
|
|
23
|
-
All request and response bodies are JSON (`application/json`).
|
|
24
|
-
|
|
25
|
-
---
|
|
26
|
-
|
|
27
|
-
## Identity Endpoints
|
|
28
|
-
|
|
29
|
-
### `POST /identities` — Submit or extend an identity chain
|
|
30
|
-
|
|
31
|
-
Submit an ordered array of JWS tokens (genesis-first). The registry verifies the chain, derives the DID from the genesis CID, and stores it.
|
|
32
|
-
|
|
33
|
-
**Request:**
|
|
34
|
-
|
|
35
|
-
```json
|
|
36
|
-
{
|
|
37
|
-
"chain": ["eyJhbGciOiJFZERTQSI...", "eyJhbGciOiJFZERTQSI..."]
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
| Field | Type | Description |
|
|
42
|
-
| ------- | -------- | ---------------------------------------------------------- |
|
|
43
|
-
| `chain` | string[] | Ordered JWS compact tokens, genesis-first. Minimum 1 item. |
|
|
44
|
-
|
|
45
|
-
**Responses:**
|
|
46
|
-
|
|
47
|
-
| Status | Meaning |
|
|
48
|
-
| ------ | ---------------------------------------------------------- |
|
|
49
|
-
| `201` | Chain accepted (new or extended) — returns `IdentityState` |
|
|
50
|
-
| `200` | Chain already stored, no change — returns `IdentityState` |
|
|
51
|
-
| `400` | Invalid chain (verification failed) |
|
|
52
|
-
| `409` | Fork conflict with stored chain |
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
### `GET /identities/{did}` — Resolve current identity state
|
|
57
|
-
|
|
58
|
-
Returns the current key state for a DID.
|
|
59
|
-
|
|
60
|
-
**Parameters:**
|
|
61
|
-
|
|
62
|
-
| Name | In | Pattern | Example |
|
|
63
|
-
| ----- | ---- | ------------------------------------ | --------------------------------- |
|
|
64
|
-
| `did` | path | `did:dfos:[2346789acdefhknrtvz]{22}` | `did:dfos:e3vvtck42d4eacdnzvtrn6` |
|
|
65
|
-
|
|
66
|
-
**Response (`IdentityState`):**
|
|
67
|
-
|
|
68
|
-
```json
|
|
69
|
-
{
|
|
70
|
-
"did": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
71
|
-
"isDeleted": false,
|
|
72
|
-
"authKeys": [{ "id": "key_...", "type": "Multikey", "publicKeyMultibase": "z6Mk..." }],
|
|
73
|
-
"assertKeys": [{ "id": "key_...", "type": "Multikey", "publicKeyMultibase": "z6Mk..." }],
|
|
74
|
-
"controllerKeys": [{ "id": "key_...", "type": "Multikey", "publicKeyMultibase": "z6Mk..." }]
|
|
75
|
-
}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
### `GET /identities/{did}/operations` — List identity chain operations
|
|
81
|
-
|
|
82
|
-
Returns operations newest-first, paginated.
|
|
83
|
-
|
|
84
|
-
**Parameters:**
|
|
85
|
-
|
|
86
|
-
| Name | In | Description |
|
|
87
|
-
| -------- | ----- | ------------------------------------------------------------ |
|
|
88
|
-
| `did` | path | The DID to list operations for |
|
|
89
|
-
| `cursor` | query | Opaque pagination cursor (CID of last item on previous page) |
|
|
90
|
-
| `limit` | query | Page size, 1–100, default 25 |
|
|
91
|
-
|
|
92
|
-
**Response (`PaginatedOperations`):**
|
|
93
|
-
|
|
94
|
-
```json
|
|
95
|
-
{
|
|
96
|
-
"operations": [
|
|
97
|
-
{ "cid": "bafyrei...", "jwsToken": "eyJ...", "createdAt": "2026-03-07T00:01:00.000Z" },
|
|
98
|
-
{ "cid": "bafyrei...", "jwsToken": "eyJ...", "createdAt": "2026-03-07T00:00:00.000Z" }
|
|
99
|
-
],
|
|
100
|
-
"nextCursor": null
|
|
101
|
-
}
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
---
|
|
105
|
-
|
|
106
|
-
## Content Endpoints
|
|
107
|
-
|
|
108
|
-
### `POST /content` — Submit or extend a content chain
|
|
109
|
-
|
|
110
|
-
Same mechanics as identity submission. The registry verifies the chain (resolving signing keys from stored identity chains), derives the content ID from the genesis CID, and stores it.
|
|
111
|
-
|
|
112
|
-
**Request:** Same `{ "chain": [...] }` format as identity submission.
|
|
113
|
-
|
|
114
|
-
**Responses:** Same status code semantics — `201` accepted, `200` noop, `400` invalid, `409` fork.
|
|
115
|
-
|
|
116
|
-
---
|
|
117
|
-
|
|
118
|
-
### `GET /content/{contentId}` — Resolve current content state
|
|
119
|
-
|
|
120
|
-
Returns the current state for a content chain.
|
|
121
|
-
|
|
122
|
-
**Parameters:**
|
|
123
|
-
|
|
124
|
-
| Name | In | Pattern | Example |
|
|
125
|
-
| ----------- | ---- | --------------------------- | ------------------------ |
|
|
126
|
-
| `contentId` | path | `[2346789acdefhknrtvz]{22}` | `67t27rzc83v7c22n9t6z7c` |
|
|
127
|
-
|
|
128
|
-
**Response (`ContentState`):**
|
|
129
|
-
|
|
130
|
-
```json
|
|
131
|
-
{
|
|
132
|
-
"contentId": "67t27rzc83v7c22n9t6z7c",
|
|
133
|
-
"isDeleted": false,
|
|
134
|
-
"currentDocumentCID": "bafyrei...",
|
|
135
|
-
"genesisCID": "bafyrei...",
|
|
136
|
-
"headCID": "bafyrei..."
|
|
137
|
-
}
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
| Field | Description |
|
|
141
|
-
| -------------------- | --------------------------------------------------- |
|
|
142
|
-
| `currentDocumentCID` | CID of current document, null if cleared or deleted |
|
|
143
|
-
| `genesisCID` | CID of the genesis operation |
|
|
144
|
-
| `headCID` | CID of the most recent operation |
|
|
145
|
-
|
|
146
|
-
---
|
|
147
|
-
|
|
148
|
-
### `GET /content/{contentId}/operations` — List content chain operations
|
|
149
|
-
|
|
150
|
-
Same pagination mechanics as identity operations.
|
|
151
|
-
|
|
152
|
-
---
|
|
153
|
-
|
|
154
|
-
## Lookup Endpoints
|
|
155
|
-
|
|
156
|
-
### `GET /operations/{cid}` — Resolve a single operation by CID
|
|
157
|
-
|
|
158
|
-
Returns the JWS token for any operation (identity or content) by its CID.
|
|
159
|
-
|
|
160
|
-
**Response:**
|
|
161
|
-
|
|
162
|
-
```json
|
|
163
|
-
{
|
|
164
|
-
"cid": "bafyrei...",
|
|
165
|
-
"jwsToken": "eyJ..."
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
---
|
|
170
|
-
|
|
171
|
-
## Errors
|
|
172
|
-
|
|
173
|
-
All error responses follow a standard shape:
|
|
174
|
-
|
|
175
|
-
```json
|
|
176
|
-
{
|
|
177
|
-
"error": "BAD_REQUEST",
|
|
178
|
-
"message": "Human-readable description"
|
|
179
|
-
}
|
|
180
|
-
```
|
|
181
|
-
|
|
182
|
-
| Error Code | Used By |
|
|
183
|
-
| ------------- | ---------------------------------------------------------- |
|
|
184
|
-
| `BAD_REQUEST` | Invalid chain, malformed request |
|
|
185
|
-
| `NOT_FOUND` | Identity, content, or operation not found |
|
|
186
|
-
| `CONFLICT` | Fork detected — submitted chain diverges from stored chain |
|
|
187
|
-
|
|
188
|
-
---
|
|
189
|
-
|
|
190
|
-
## Chain Submission Semantics
|
|
191
|
-
|
|
192
|
-
The registry enforces **linear chain extension**:
|
|
193
|
-
|
|
194
|
-
- **New chain** — genesis operation not seen before → store and return `201`
|
|
195
|
-
- **Extension** — submitted chain is longer than stored, shares the same prefix → replace stored chain, return `201`
|
|
196
|
-
- **Noop** — submitted chain is identical to stored → return `200`
|
|
197
|
-
- **Fork** — submitted chain diverges from stored chain at some operation → reject with `409`
|
|
198
|
-
|
|
199
|
-
This means a registry is **eventually consistent with the longest valid chain** it receives. It does not implement consensus — if two registries receive different valid extensions, they may diverge. Fork detection is the caller's responsibility.
|
|
200
|
-
|
|
201
|
-
---
|
|
202
|
-
|
|
203
|
-
## Authentication
|
|
204
|
-
|
|
205
|
-
Registry endpoints that require authentication use **EdDSA JWTs** signed with the same Ed25519 keys from identity chains. The JWT convention is not part of the chain protocol — it's an application-layer auth mechanism for services.
|
|
206
|
-
|
|
207
|
-
```json
|
|
208
|
-
{
|
|
209
|
-
"alg": "EdDSA",
|
|
210
|
-
"typ": "JWT",
|
|
211
|
-
"kid": "key_ez9a874tckr3dv933d3ckd"
|
|
212
|
-
}
|
|
213
|
-
```
|
|
214
|
-
|
|
215
|
-
```json
|
|
216
|
-
{
|
|
217
|
-
"iss": "dfos",
|
|
218
|
-
"sub": "did:dfos:e3vvtck42d4eacdnzvtrn6",
|
|
219
|
-
"aud": "dfos-api",
|
|
220
|
-
"exp": 1772902800,
|
|
221
|
-
"iat": 1772899200,
|
|
222
|
-
"jti": "session_ref_example_01"
|
|
223
|
-
}
|
|
224
|
-
```
|
|
225
|
-
|
|
226
|
-
| Field | Description |
|
|
227
|
-
| ----- | ------------------------------------------------------------- |
|
|
228
|
-
| `kid` | Bare key ID (not a DID URL — the `sub` claim carries the DID) |
|
|
229
|
-
| `sub` | The DID of the authenticating identity |
|
|
230
|
-
| `aud` | Target audience (e.g., `"dfos-api"`) |
|
|
231
|
-
|
|
232
|
-
The signing mechanics are identical to operation JWS — `ed25519.sign(UTF8(base64url(header) + "." + base64url(payload)), privateKey)`. The key is resolved from the identity chain via `kid` + `sub`.
|
|
233
|
-
|
|
234
|
-
---
|
|
235
|
-
|
|
236
|
-
## Reference Implementation
|
|
237
|
-
|
|
238
|
-
A complete reference server is available in the protocol package:
|
|
239
|
-
|
|
240
|
-
- [`registry/server.ts`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/registry/server.ts) — Hono-based HTTP server implementing all endpoints
|
|
241
|
-
- [`registry/store.ts`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/src/registry/store.ts) — In-memory chain store with linear enforcement
|
|
242
|
-
- [`openapi.yaml`](https://github.com/metalabel/dfos/blob/main/packages/dfos-protocol/openapi.yaml) — OpenAPI 3.1 machine-readable specification
|