@metalabel/dfos-protocol 0.0.1 → 0.0.2

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.
@@ -0,0 +1,242 @@
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 entities, 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 entity, 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 /entities` — 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 entity 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 /entities/{entityId}` — Resolve current entity state
119
+
120
+ Returns the current state for a content chain entity.
121
+
122
+ **Parameters:**
123
+
124
+ | Name | In | Pattern | Example |
125
+ | ---------- | ---- | --------------------------- | ------------------------ |
126
+ | `entityId` | path | `[2346789acdefhknrtvz]{22}` | `67t27rzc83v7c22n9t6z7c` |
127
+
128
+ **Response (`EntityState`):**
129
+
130
+ ```json
131
+ {
132
+ "entityId": "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 /entities/{entityId}/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, entity, operation, or document 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
@@ -54,10 +54,6 @@ var ResolveOperationResponse = z.strictObject({
54
54
  cid: z.string(),
55
55
  jwsToken: z.string()
56
56
  });
57
- var ResolveDocumentResponse = z.strictObject({
58
- cid: z.string(),
59
- content: z.unknown()
60
- });
61
57
  var RegistryError = z.strictObject({
62
58
  error: z.string(),
63
59
  message: z.string()
@@ -70,7 +66,6 @@ import { Hono } from "hono";
70
66
  var ChainStore = class {
71
67
  identities = /* @__PURE__ */ new Map();
72
68
  entities = /* @__PURE__ */ new Map();
73
- documents = /* @__PURE__ */ new Map();
74
69
  // --- identities ---
75
70
  getIdentityChain(did) {
76
71
  return this.identities.get(did);
@@ -91,13 +86,6 @@ var ChainStore = class {
91
86
  submitEntityChain(entityId, operations) {
92
87
  return this.submitChain(this.entities, entityId, operations);
93
88
  }
94
- // --- documents ---
95
- getDocument(cid) {
96
- return this.documents.get(cid);
97
- }
98
- setDocument(cid, content) {
99
- this.documents.set(cid, content);
100
- }
101
89
  // --- operations (lookup across all chains) ---
102
90
  getOperation(cid) {
103
91
  for (const chain of this.identities.values()) {
@@ -302,12 +290,6 @@ var createRegistryServer = (store = new ChainStore()) => {
302
290
  if (!op) return c.json(err("NOT_FOUND", "operation not found"), 404);
303
291
  return c.json({ cid: op.cid, jwsToken: op.jwsToken });
304
292
  });
305
- app.get("/documents/:cid", (c) => {
306
- const cid = c.req.param("cid");
307
- const content = store.getDocument(cid);
308
- if (content === void 0) return c.json(err("NOT_FOUND", "document not found"), 404);
309
- return c.json({ cid, content });
310
- });
311
293
  return { app, store };
312
294
  };
313
295
 
@@ -323,7 +305,6 @@ export {
323
305
  EntityOperationsParams,
324
306
  EntityOperationsResponse,
325
307
  ResolveOperationResponse,
326
- ResolveDocumentResponse,
327
308
  RegistryError,
328
309
  ChainStore,
329
310
  createRegistryServer
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { JwsHeader, JwsVerificationError, JwtClaims, JwtCreateOptions, JwtHeader, JwtVerificationError, JwtVerifyOptions, PrefixedID, base64urlDecode, base64urlEncode, createJws, createJwt, createNewEd25519Keypair, dagCborCanonicalEncode, decodeJwsUnsafe, decodeJwtUnsafe, generateId, generateIdNoPrefix, importEd25519Keypair, isCanonicallyEqual, isValidEd25519Signature, isValidId, normalizedId, parseDagCborCID, signPayloadEd25519, verifyJws, verifyJwt } from './crypto/index.js';
2
2
  export { ContentOperation, ED25519_PRIV_MULTICODEC, ED25519_PUB_MULTICODEC, IdentityOperation, MultikeyPublicKey, Signer, VerifiedContentChain, VerifiedIdentity, decodeMultikey, deriveChainIdentifier, deriveEntityId, encodeEd25519Multikey, signContentOperation, signIdentityOperation, verifyContentChain, verifyIdentityChain } from './chain/index.js';
3
- export { ChainStore, EntityOperationsParams, EntityOperationsResponse, IdentityOperationsParams, IdentityOperationsResponse, OperationEntry, PaginationParams, RegistryError, ResolveDocumentResponse, ResolveEntityResponse, ResolveIdentityResponse, ResolveOperationResponse, StoredChain, SubmitContentChainRequest, SubmitContentChainResponse, SubmitIdentityChainRequest, SubmitIdentityChainResponse, createRegistryServer } from './registry/index.js';
3
+ export { ChainStore, EntityOperationsParams, EntityOperationsResponse, IdentityOperationsParams, IdentityOperationsResponse, OperationEntry, PaginationParams, RegistryError, ResolveEntityResponse, ResolveIdentityResponse, ResolveOperationResponse, StoredChain, SubmitContentChainRequest, SubmitContentChainResponse, SubmitIdentityChainRequest, SubmitIdentityChainResponse, createRegistryServer } from './registry/index.js';
4
4
  import 'multiformats';
5
5
  import 'multiformats/cid';
6
6
  import 'zod';
package/dist/index.js CHANGED
@@ -5,7 +5,6 @@ import {
5
5
  IdentityOperationsParams,
6
6
  IdentityOperationsResponse,
7
7
  RegistryError,
8
- ResolveDocumentResponse,
9
8
  ResolveEntityResponse,
10
9
  ResolveIdentityResponse,
11
10
  ResolveOperationResponse,
@@ -14,7 +13,7 @@ import {
14
13
  SubmitIdentityChainRequest,
15
14
  SubmitIdentityChainResponse,
16
15
  createRegistryServer
17
- } from "./chunk-3PB644X2.js";
16
+ } from "./chunk-4MMWM2PC.js";
18
17
  import {
19
18
  ContentOperation,
20
19
  ED25519_PRIV_MULTICODEC,
@@ -68,7 +67,6 @@ export {
68
67
  JwtVerificationError,
69
68
  MultikeyPublicKey,
70
69
  RegistryError,
71
- ResolveDocumentResponse,
72
70
  ResolveEntityResponse,
73
71
  ResolveIdentityResponse,
74
72
  ResolveOperationResponse,
@@ -108,11 +108,6 @@ declare const ResolveOperationResponse: z.ZodObject<{
108
108
  jwsToken: z.ZodString;
109
109
  }, z.core.$strict>;
110
110
  type ResolveOperationResponse = z.infer<typeof ResolveOperationResponse>;
111
- declare const ResolveDocumentResponse: z.ZodObject<{
112
- cid: z.ZodString;
113
- content: z.ZodUnknown;
114
- }, z.core.$strict>;
115
- type ResolveDocumentResponse = z.infer<typeof ResolveDocumentResponse>;
116
111
  declare const RegistryError: z.ZodObject<{
117
112
  error: z.ZodString;
118
113
  message: z.ZodString;
@@ -126,7 +121,6 @@ interface StoredChain {
126
121
  declare class ChainStore {
127
122
  private identities;
128
123
  private entities;
129
- private documents;
130
124
  getIdentityChain(did: string): StoredChain | undefined;
131
125
  /**
132
126
  * Submit an identity chain. Returns 'accepted' | 'noop' | 'conflict'.
@@ -137,8 +131,6 @@ declare class ChainStore {
137
131
  submitIdentityChain(did: string, operations: OperationEntry[]): 'accepted' | 'noop' | 'conflict';
138
132
  getEntityChain(entityId: string): StoredChain | undefined;
139
133
  submitEntityChain(entityId: string, operations: OperationEntry[]): 'accepted' | 'noop' | 'conflict';
140
- getDocument(cid: string): unknown | undefined;
141
- setDocument(cid: string, content: unknown): void;
142
134
  getOperation(cid: string): OperationEntry | undefined;
143
135
  private submitChain;
144
136
  }
@@ -148,4 +140,4 @@ declare const createRegistryServer: (store?: ChainStore) => {
148
140
  store: ChainStore;
149
141
  };
150
142
 
151
- export { ChainStore, EntityOperationsParams, EntityOperationsResponse, IdentityOperationsParams, IdentityOperationsResponse, OperationEntry, PaginationParams, RegistryError, ResolveDocumentResponse, ResolveEntityResponse, ResolveIdentityResponse, ResolveOperationResponse, type StoredChain, SubmitContentChainRequest, SubmitContentChainResponse, SubmitIdentityChainRequest, SubmitIdentityChainResponse, createRegistryServer };
143
+ export { ChainStore, EntityOperationsParams, EntityOperationsResponse, IdentityOperationsParams, IdentityOperationsResponse, OperationEntry, PaginationParams, RegistryError, ResolveEntityResponse, ResolveIdentityResponse, ResolveOperationResponse, type StoredChain, SubmitContentChainRequest, SubmitContentChainResponse, SubmitIdentityChainRequest, SubmitIdentityChainResponse, createRegistryServer };
@@ -5,7 +5,6 @@ import {
5
5
  IdentityOperationsParams,
6
6
  IdentityOperationsResponse,
7
7
  RegistryError,
8
- ResolveDocumentResponse,
9
8
  ResolveEntityResponse,
10
9
  ResolveIdentityResponse,
11
10
  ResolveOperationResponse,
@@ -14,7 +13,7 @@ import {
14
13
  SubmitIdentityChainRequest,
15
14
  SubmitIdentityChainResponse,
16
15
  createRegistryServer
17
- } from "../chunk-3PB644X2.js";
16
+ } from "../chunk-4MMWM2PC.js";
18
17
  import "../chunk-LWC4PWGZ.js";
19
18
  import "../chunk-ZXXP5W5N.js";
20
19
  export {
@@ -24,7 +23,6 @@ export {
24
23
  IdentityOperationsParams,
25
24
  IdentityOperationsResponse,
26
25
  RegistryError,
27
- ResolveDocumentResponse,
28
26
  ResolveEntityResponse,
29
27
  ResolveIdentityResponse,
30
28
  ResolveOperationResponse,
package/openapi.yaml CHANGED
@@ -203,29 +203,6 @@ paths:
203
203
  schema:
204
204
  $ref: '#/components/schemas/Error'
205
205
 
206
- /documents/{cid}:
207
- get:
208
- operationId: resolveDocument
209
- summary: Resolve a document by CID
210
- description: |
211
- This is the only endpoint that touches actual content.
212
- All other endpoints deal purely in chain mechanics.
213
- parameters:
214
- - $ref: '#/components/parameters/cid'
215
- responses:
216
- '200':
217
- description: Document found
218
- content:
219
- application/json:
220
- schema:
221
- $ref: '#/components/schemas/Document'
222
- '404':
223
- description: Document not found
224
- content:
225
- application/json:
226
- schema:
227
- $ref: '#/components/schemas/Error'
228
-
229
206
  # ---------------------------------------------------------------------------
230
207
 
231
208
  components:
@@ -388,15 +365,6 @@ components:
388
365
  jwsToken:
389
366
  type: string
390
367
 
391
- Document:
392
- type: object
393
- required: [cid, content]
394
- properties:
395
- cid:
396
- type: string
397
- content:
398
- description: Opaque document content (application-defined)
399
-
400
368
  Error:
401
369
  type: object
402
370
  required: [error, message]
package/package.json CHANGED
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-protocol",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
+ "type": "module",
4
5
  "description": "DFOS Protocol — Ed25519 signed chain primitives, registry, and verification",
5
6
  "license": "MIT",
6
7
  "author": "Metalabel <hello@metalabel.com> (https://metalabel.com)",
7
8
  "repository": {
8
9
  "type": "git",
9
- "url": "https://github.com/metalabel/metalabel-dfos.git",
10
+ "url": "https://github.com/metalabel/dfos.git",
10
11
  "directory": "packages/dfos-protocol"
11
12
  },
12
- "homepage": "https://github.com/metalabel/metalabel-dfos/tree/main/packages/dfos-protocol",
13
+ "homepage": "https://protocol.dfos.com",
13
14
  "keywords": [
14
15
  "dfos",
15
16
  "protocol",
@@ -23,7 +24,6 @@
23
24
  "cid",
24
25
  "metalabel"
25
26
  ],
26
- "type": "module",
27
27
  "exports": {
28
28
  ".": {
29
29
  "import": "./dist/index.js",
@@ -47,6 +47,9 @@
47
47
  "schemas",
48
48
  "examples",
49
49
  "PROTOCOL.md",
50
+ "DID-METHOD.md",
51
+ "CONTENT-MODEL.md",
52
+ "REGISTRY-API.md",
50
53
  "openapi.yaml",
51
54
  "LICENSE",
52
55
  "README.md"
@@ -68,8 +71,10 @@
68
71
  "vitest": "^4.0.18"
69
72
  },
70
73
  "scripts": {
74
+ "build": "tsup",
75
+ "clean": "rm -rf dist",
76
+ "typecheck": "tsc --noEmit",
71
77
  "test": "vitest run",
72
- "generate-examples": "vitest run --config vitest.scripts.config.ts",
73
- "build": "tsup"
78
+ "generate-examples": "vitest run --config vitest.scripts.config.ts"
74
79
  }
75
80
  }