@metalabel/dfos-web-relay 0.4.1 → 0.6.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/dist/serve.d.ts CHANGED
@@ -12,7 +12,7 @@ interface ServeOptions {
12
12
  * import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
13
13
  * import { serve } from '@metalabel/dfos-web-relay/node';
14
14
  *
15
- * const relay = createRelay({ relayDID: 'did:dfos:myrelay', store: new MemoryRelayStore() });
15
+ * const relay = await createRelay({ store: new MemoryRelayStore() });
16
16
  * serve(relay, { port: 4444 });
17
17
  * ```
18
18
  */
package/openapi.yaml CHANGED
@@ -28,7 +28,7 @@ paths:
28
28
  application/json:
29
29
  schema:
30
30
  type: object
31
- required: [did, protocol, version]
31
+ required: [did, protocol, version, proof, content, log, profile]
32
32
  properties:
33
33
  did:
34
34
  type: string
@@ -40,6 +40,18 @@ paths:
40
40
  version:
41
41
  type: string
42
42
  example: '0.1.0'
43
+ proof:
44
+ type: boolean
45
+ description: Always true — a relay without proof plane is not a relay
46
+ content:
47
+ type: boolean
48
+ description: Whether the relay supports content plane (blob upload/download)
49
+ log:
50
+ type: boolean
51
+ description: Whether the global operation log is available (GET /log)
52
+ profile:
53
+ type: string
54
+ description: The relay's profile artifact as a compact JWS token
43
55
 
44
56
  /operations:
45
57
  post:
@@ -388,7 +400,7 @@ components:
388
400
  description: CID of the operation
389
401
  status:
390
402
  type: string
391
- enum: [accepted, rejected]
403
+ enum: [new, duplicate, rejected]
392
404
  error:
393
405
  type: string
394
406
  description: Error message if rejected
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-web-relay",
3
- "version": "0.4.1",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
6
6
  "license": "MIT",
@@ -36,28 +36,29 @@
36
36
  },
37
37
  "files": [
38
38
  "dist",
39
- "RELAY.md",
40
39
  "openapi.yaml",
41
40
  "LICENSE",
42
41
  "README.md"
43
42
  ],
44
43
  "dependencies": {
45
- "hono": "^4.12.5",
44
+ "hono": "^4.12.8",
46
45
  "zod": "^4.3.6"
47
46
  },
48
47
  "peerDependencies": {
49
- "@metalabel/dfos-protocol": "^0.4.0"
48
+ "@metalabel/dfos-protocol": "^0.6.0"
50
49
  },
51
50
  "devDependencies": {
52
51
  "@types/node": "^24.10.4",
53
52
  "tsup": "^8.5.1",
54
- "vitest": "^4.0.18",
55
- "@metalabel/dfos-protocol": "0.4.0"
53
+ "tsx": "^4.20.3",
54
+ "vitest": "^4.1.0",
55
+ "@metalabel/dfos-protocol": "0.6.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",
59
59
  "clean": "rm -rf dist",
60
60
  "typecheck": "tsc --noEmit",
61
- "test": "vitest run"
61
+ "test": "vitest run",
62
+ "test:conformance": "./tests/run-conformance.sh"
62
63
  }
63
64
  }
package/RELAY.md DELETED
@@ -1,228 +0,0 @@
1
- # DFOS Web Relay
2
-
3
- An HTTP relay for the DFOS protocol — receives, verifies, stores, and serves identity chains, content chains, beacons, countersignatures, and content blobs.
4
-
5
- This spec is under active review. Discuss it in the [clear.txt](https://clear.dfos.com) space on DFOS.
6
-
7
- [Source](https://github.com/metalabel/dfos/tree/main/packages/dfos-web-relay) · [npm](https://www.npmjs.com/package/@metalabel/dfos-web-relay) · [Protocol](https://protocol.dfos.com)
8
-
9
- ---
10
-
11
- ## Philosophy
12
-
13
- The DFOS protocol defines signed chain primitives — identity and content chains, beacons, credentials — but says nothing about transport. A web relay is the HTTP layer that carries these primitives between participants.
14
-
15
- Relays are not authorities. They verify what they receive and serve what they've verified, but they don't issue identity, grant permissions, or define content semantics. Any relay implementing the same verification rules produces the same acceptance/rejection decisions for the same operations. Clients can replicate their data across multiple relays without coordination.
16
-
17
- A relay is a library, not a service. `createRelay()` returns a portable Hono application that any runtime can host — Node.js, Cloudflare Workers, Deno, Bun, a Docker container, a Raspberry Pi. The consumer provides a storage backend and configuration. The relay handles verification and HTTP semantics.
18
-
19
- ---
20
-
21
- ## Two Planes
22
-
23
- The relay serves two distinct planes of data with different access models:
24
-
25
- ### Proof Plane (public)
26
-
27
- Signed chain operations, beacons, and countersignatures. These are cryptographic proofs — anyone can verify them with a public key. The proof plane gossips freely: relays push operations to peers, peers verify and store independently.
28
-
29
- All proof plane routes are unauthenticated. The operations themselves carry their own authentication (Ed25519 signatures).
30
-
31
- ### Content Plane (private)
32
-
33
- Raw content blobs — the actual documents that content chains commit to via `documentCID`. The content plane never gossips. Blobs are stored by the relay that received them and served only to authorized readers.
34
-
35
- Content plane access requires two credentials:
36
-
37
- - **Auth token**: A DID-signed JWT proving the caller controls an identity (AuthN)
38
- - **Read credential** (for non-creators): A `DFOSContentRead` VC-JWT issued by the content creator, granting the caller read access (AuthZ)
39
-
40
- The content creator (the DID that signed the genesis content operation) can always read their own blobs with just an auth token.
41
-
42
- ---
43
-
44
- ## Operation Ingestion
45
-
46
- All proof plane artifacts enter through a single endpoint: `POST /operations`. The request body is an array of JWS tokens — identity operations, content operations, beacons, and countersignatures can be mixed freely in the same batch.
47
-
48
- ### Classification
49
-
50
- Each token is classified by its JWS `typ` header:
51
-
52
- | `typ` header | kid DID == payload DID? | Classification |
53
- | ---------------------- | ----------------------- | ------------------------ |
54
- | `did:dfos:identity-op` | — | Identity chain operation |
55
- | `did:dfos:content-op` | yes | Content chain operation |
56
- | `did:dfos:content-op` | no | Countersignature |
57
- | `did:dfos:beacon` | yes | Beacon announcement |
58
- | `did:dfos:beacon` | no | Beacon countersignature |
59
-
60
- When the signing key's DID differs from the payload DID, the operation is classified as a countersignature (witness attestation) rather than a primary operation.
61
-
62
- ### Dependency Sort
63
-
64
- Within a batch, operations are sorted by dependency priority before processing:
65
-
66
- 1. **Identity operations** — must be processed first so their keys are available
67
- 2. **Beacons** — reference identity keys
68
- 3. **Content operations** — reference identity keys for signature verification
69
- 4. **Countersignatures** — reference both identity keys and existing operations
70
-
71
- Within each priority level, genesis operations (no `previousOperationCID`) are processed before extensions. This ensures that a single batch can bootstrap an entire identity-and-content lifecycle — including chained create + update operations — without multiple round trips.
72
-
73
- ### Verification
74
-
75
- Each operation is verified against the relay's stored state:
76
-
77
- - **Identity operations**: The full chain (stored log + new operation) is re-verified from genesis. The relay uses `verifyIdentityChain()` from the protocol library
78
- - **Content operations**: The full chain is re-verified with `enforceAuthorization: true`. Non-creator signers must include a `DFOSContentWrite` VC-JWT
79
- - **Beacons**: Signature, CID integrity, and clock skew are verified. Replace-on-newer: only the most recent beacon per DID is retained
80
- - **Countersignatures**: The referenced operation or beacon must already exist. Signature is verified against the witness DID's identity chain
81
- - **Beacon countersignatures**: The referenced beacon must exist and the countersignature CID must match the current beacon CID
82
-
83
- ### Fork Policy
84
-
85
- First-seen-wins. If a chain head has already advanced past the `previousOperationCID` referenced by an incoming operation, the new operation is rejected. The relay does not attempt to resolve forks — the first valid extension wins.
86
-
87
- Duplicate submissions (same operation CID, same JWS token) are silently accepted (idempotent). Submissions with the same CID but a different JWS token are rejected — since Ed25519 is deterministic, a different token for the same payload means a different signing key, which is either a self-countersign attempt or an unauthorized re-sign.
88
-
89
- Duplicate countersignatures (same witness DID, same target CID) MUST be deduplicated. The relay MUST NOT increase the countersignature count on resubmission. Resubmission SHOULD return `accepted` (idempotent).
90
-
91
- ### Deletion Semantics
92
-
93
- Deletion means the identity stops being an active participant. Historical operations remain verifiable — keys persist in state for signature verification — but no new acts flow from a deleted identity.
94
-
95
- Specifically:
96
-
97
- - **Identity operations after deletion**: Rejected. A deleted identity chain is sealed.
98
- - **Content operations after deletion**: Rejected. A deleted content chain is sealed.
99
- - **Beacons from deleted identities**: Rejected. A deleted identity MUST NOT publish new beacons.
100
- - **Credentials from deleted issuers**: Rejected. Identity deletion revokes all authority, including outstanding `DFOSContentRead` and `DFOSContentWrite` credentials issued by the deleted identity. Credentials that were valid at time of issuance cease to be honored once the issuer is deleted.
101
- - **Countersignatures on existing operations**: Still accepted. Deletion of the original author does not prevent other identities from attesting to operations that already exist in the relay.
102
-
103
- Self-countersignatures — where the witness DID matches the operation author DID — are rejected. A countersignature's semantic is "a distinct witness attests." Signing your own operation is redundant with the original signature.
104
-
105
- ### Result Ordering
106
-
107
- Ingestion results are returned in the same order as the input `operations` array, regardless of internal processing order. `results[i]` corresponds to `operations[i]`.
108
-
109
- ---
110
-
111
- ## Relay Identity
112
-
113
- Every relay has a DID, published at `GET /.well-known/dfos-relay`. The relay DID serves as:
114
-
115
- - **Auth token audience**: Auth tokens are scoped to a specific relay via the JWT `aud` claim, preventing cross-relay token replay
116
- - **Peer identity**: When relays gossip proof plane data to each other, the relay DID identifies the peer
117
-
118
- The relay does not currently sign operations or participate in the protocol as an identity. It's a passive verifier and store.
119
-
120
- ---
121
-
122
- ## Content Plane Access
123
-
124
- ### Blob Upload (`PUT /content/:contentId/blob/:operationCID`)
125
-
126
- The upload path mirrors the download path — the operation CID identifies which operation's document is being uploaded.
127
-
128
- Requirements:
129
-
130
- - Valid auth token (Bearer header)
131
- - The operation CID must reference an operation in this content chain that has a `documentCID`
132
- - The authenticated DID must be either the chain creator OR the signer of the referenced operation (enabling delegated uploads)
133
- - The uploaded bytes must hash to the operation's `documentCID` (dag-cbor + sha-256 verification)
134
-
135
- Blobs are stored by `(creatorDID, documentCID)` — always keyed to the chain creator regardless of who uploads. If multiple content chains by the same creator reference the same document, the blob is shared (deduplication).
136
-
137
- ### Blob Download (`GET /content/:contentId/blob[/:ref]`)
138
-
139
- Requirements:
140
-
141
- - Valid auth token (Bearer header)
142
- - If the caller is the chain creator: no further credentials needed
143
- - If the caller is not the creator: must present a `DFOSContentRead` VC-JWT in the `X-Credential` header, issued by the creator to the caller
144
-
145
- The optional `:ref` parameter selects which operation's document to return:
146
-
147
- - `head` (default): the current document at chain head
148
- - An operation CID: the document committed by that specific operation
149
-
150
- ---
151
-
152
- ## Key Resolution
153
-
154
- The relay uses two key resolution strategies:
155
-
156
- - **Historical resolver** (for chain re-verification): searches all keys that have ever appeared in an identity chain's log, including rotated-out keys. This is necessary because re-verifying a full content chain from genesis must resolve keys from operations signed before a key rotation.
157
- - **Current-state resolver** (for live authentication): only resolves keys in the identity's current state. After a key rotation, the old key immediately stops working for auth tokens and credentials. This prevents a compromised rotated-out key from being used to authenticate new requests.
158
-
159
- ---
160
-
161
- ## Storage Interface
162
-
163
- The relay delegates persistence to a `RelayStore` interface. Implementations handle how data is stored — the relay handles what to store and when.
164
-
165
- ```typescript
166
- interface RelayStore {
167
- getOperation(cid: string): Promise<StoredOperation | undefined>;
168
- putOperation(op: StoredOperation): Promise<void>;
169
-
170
- getIdentityChain(did: string): Promise<StoredIdentityChain | undefined>;
171
- putIdentityChain(chain: StoredIdentityChain): Promise<void>;
172
-
173
- getContentChain(contentId: string): Promise<StoredContentChain | undefined>;
174
- putContentChain(chain: StoredContentChain): Promise<void>;
175
-
176
- getBeacon(did: string): Promise<StoredBeacon | undefined>;
177
- putBeacon(beacon: StoredBeacon): Promise<void>;
178
-
179
- getBlob(key: BlobKey): Promise<Uint8Array | undefined>;
180
- putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
181
-
182
- getCountersignatures(operationCID: string): Promise<string[]>;
183
- addCountersignature(operationCID: string, jwsToken: string): Promise<void>;
184
- }
185
- ```
186
-
187
- The package includes `MemoryRelayStore` for development and testing. Production deployments would implement the interface over Postgres, SQLite, S3, or any durable store.
188
-
189
- ---
190
-
191
- ## Quick Start
192
-
193
- ```typescript
194
- import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
195
-
196
- const relay = createRelay({
197
- relayDID: 'did:dfos:myrelay00000000000000',
198
- store: new MemoryRelayStore(),
199
- });
200
-
201
- // Mount on any Hono-compatible runtime
202
- export default relay;
203
- ```
204
-
205
- The returned Hono app exposes:
206
-
207
- | Method | Path | Plane | Auth |
208
- | ------ | ------------------------------------ | ------- | ----------------------- |
209
- | `GET` | `/.well-known/dfos-relay` | meta | none |
210
- | `POST` | `/operations` | proof | none |
211
- | `GET` | `/operations/:cid` | proof | none |
212
- | `GET` | `/operations/:cid/countersignatures` | proof | none |
213
- | `GET` | `/countersignatures/:cid` | proof | none |
214
- | `GET` | `/identities/:did` | proof | none |
215
- | `GET` | `/content/:contentId` | proof | none |
216
- | `GET` | `/beacons/:did` | proof | none |
217
- | `PUT` | `/content/:contentId/blob/:opCID` | content | auth token |
218
- | `GET` | `/content/:contentId/blob[/:ref]` | content | auth token + credential |
219
-
220
- ---
221
-
222
- ## What's Deferred
223
-
224
- - **Peer gossip**: Proactive push of proof plane operations to other relays
225
- - **Rate limiting / anti-spam**: Operational concern, not protocol concern
226
- - **Docker/CF reference deployments**: Focus on the core library first
227
- - **Pagination**: Chain logs are returned in full — fine for v1, needs pagination at scale
228
- - **Blob size limits**: No enforcement yet — production deployments should add limits at the middleware layer