@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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Portable HTTP relay for the [DFOS protocol](https://protocol.dfos.com). Receives, verifies, stores, and serves identity chains, content chains, beacons, countersignatures, and content blobs.
4
4
 
5
- See [RELAY.md](./RELAY.md) for the full relay specification.
5
+ See [WEB-RELAY.md](../../specs/WEB-RELAY.md) for the full relay specification.
6
6
 
7
7
  ## Install
8
8
 
@@ -17,13 +17,15 @@ npm install @metalabel/dfos-web-relay @metalabel/dfos-protocol
17
17
  ```typescript
18
18
  import { createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
19
19
 
20
- const relay = createRelay({
21
- relayDID: 'did:dfos:myrelay00000000000000',
20
+ const relay = await createRelay({
22
21
  store: new MemoryRelayStore(),
23
22
  });
24
23
 
25
- // Mount on any Hono-compatible runtime
26
- export default relay;
24
+ // relay.app — Hono application
25
+ // relay.did — the relay's auto-generated DID
26
+ // relay.syncFromPeers() — pull operations from configured peers
27
+
28
+ export default relay.app;
27
29
  ```
28
30
 
29
31
  ### Standalone (Node.js)
@@ -68,11 +70,11 @@ RELAY_URL=http://localhost:4444 go test -v -count=1 ./conformance/
68
70
  RELAY_URL=https://registry.imajin.ai/relay go test -v -count=1 ./conformance/
69
71
  ```
70
72
 
71
- 71 tests covering:
73
+ 77 tests covering:
72
74
 
73
75
  - Well-known discovery
74
76
  - Identity lifecycle (create, update, delete, batch, idempotency, controller key rotation)
75
- - Content lifecycle (create, update, delete, fork rejection, post-delete rejection, notes, long chains)
77
+ - Content lifecycle (create, update, delete, fork acceptance, DAG logs, deterministic head selection, post-delete rejection, notes, long chains)
76
78
  - Content update after auth key rotation, multiple independent chains
77
79
  - Operations by CID
78
80
  - Beacons (create, replacement, not-found, unknown/deleted identity)
@@ -84,9 +86,30 @@ RELAY_URL=https://registry.imajin.ai/relay go test -v -count=1 ./conformance/
84
86
  - Auth edge cases (wrong audience, expired token, rotated-out key)
85
87
  - Batch processing (3-step dependency sort, content-identity sort, large batch, dedup, mixed valid/invalid, multi-chain)
86
88
  - Input validation (malformed JSON, empty operations, invalid JWS)
89
+ - Future timestamp guard (reject identity/content ops >24h ahead)
87
90
 
88
91
  The conformance suite depends on [`dfos-protocol-go`](../dfos-protocol-go) for protocol operations.
89
92
 
93
+ ## Peering
94
+
95
+ Relays can replicate operations via three composable behaviors configured per-peer:
96
+
97
+ - **Gossip-out**: push new operations to peers (fire-and-forget)
98
+ - **Read-through**: fetch from peers on local 404
99
+ - **Sync-in**: cursor-based log polling from peers
100
+
101
+ ```typescript
102
+ import { createHttpPeerClient, createRelay, MemoryRelayStore } from '@metalabel/dfos-web-relay';
103
+
104
+ const relay = await createRelay({
105
+ store: new MemoryRelayStore(),
106
+ peerClient: createHttpPeerClient(),
107
+ peers: [{ url: 'https://other-relay.example.com' }],
108
+ });
109
+ ```
110
+
111
+ See [WEB-RELAY.md](../../specs/WEB-RELAY.md) for the full peering specification.
112
+
90
113
  ## Custom Store
91
114
 
92
115
  Implement the `RelayStore` interface to use any persistence backend:
package/dist/index.d.ts CHANGED
@@ -1,16 +1,77 @@
1
- import { Hono } from 'hono';
2
1
  import { VerifiedIdentity, VerifiedContentChain, VerifiedBeacon } from '@metalabel/dfos-protocol/chain';
2
+ import { Hono } from 'hono';
3
3
 
4
+ interface RelayIdentity {
5
+ /** The relay's DID */
6
+ did: string;
7
+ /** Profile artifact JWS token (signed by the relay DID) */
8
+ profileArtifactJws: string;
9
+ }
4
10
  interface RelayOptions {
5
- /** The relay's DID — used as auth token audience and published in well-known */
6
- relayDID: string;
7
11
  /** Storage backend */
8
12
  store: RelayStore;
13
+ /** Pre-created relay identity — if omitted, a JIT identity and profile are generated */
14
+ identity?: RelayIdentity;
15
+ /** Whether content plane routes are enabled (default: true) */
16
+ content?: boolean;
17
+ /** Whether the global operation log is enabled (default: true) */
18
+ log?: boolean;
19
+ /** Peer relay configurations */
20
+ peers?: PeerConfig[];
21
+ /** Injected peer client — if omitted, a default HTTP implementation is used */
22
+ peerClient?: PeerClient;
23
+ }
24
+ interface PeerConfig {
25
+ url: string;
26
+ /** Push new ops to this peer (default: true) */
27
+ gossip?: boolean;
28
+ /** Fetch from this peer on local 404 (default: true) */
29
+ readThrough?: boolean;
30
+ /** Poll this peer's /log for background sync (default: true) */
31
+ sync?: boolean;
32
+ }
33
+ /** A log entry returned by a peer — CID and JWS token */
34
+ interface PeerLogEntry {
35
+ cid: string;
36
+ jwsToken: string;
37
+ }
38
+ /** Injected peer transport — the relay expresses intent, the caller decides transport */
39
+ interface PeerClient {
40
+ /** Fetch identity chain log from a peer */
41
+ getIdentityLog(peerUrl: string, did: string, params?: {
42
+ after?: string;
43
+ limit?: number;
44
+ }): Promise<{
45
+ entries: PeerLogEntry[];
46
+ cursor: string | null;
47
+ } | null>;
48
+ /** Fetch content chain log from a peer */
49
+ getContentLog(peerUrl: string, contentId: string, params?: {
50
+ after?: string;
51
+ limit?: number;
52
+ }): Promise<{
53
+ entries: PeerLogEntry[];
54
+ cursor: string | null;
55
+ } | null>;
56
+ /** Fetch global operation log from a peer */
57
+ getOperationLog(peerUrl: string, params?: {
58
+ after?: string;
59
+ limit?: number;
60
+ }): Promise<{
61
+ entries: PeerLogEntry[];
62
+ cursor: string | null;
63
+ } | null>;
64
+ /** Push operations to a peer (fire-and-forget) */
65
+ submitOperations(peerUrl: string, operations: string[]): Promise<void>;
9
66
  }
10
67
  interface StoredIdentityChain {
11
68
  did: string;
12
69
  /** Ordered JWS tokens from genesis to head */
13
70
  log: string[];
71
+ /** CID of the most recent operation */
72
+ headCID: string;
73
+ /** createdAt timestamp of the most recent operation */
74
+ lastCreatedAt: string;
14
75
  state: VerifiedIdentity;
15
76
  }
16
77
  interface StoredContentChain {
@@ -18,6 +79,8 @@ interface StoredContentChain {
18
79
  genesisCID: string;
19
80
  /** Ordered JWS tokens from genesis to head */
20
81
  log: string[];
82
+ /** createdAt timestamp of the most recent operation */
83
+ lastCreatedAt: string;
21
84
  state: VerifiedContentChain;
22
85
  }
23
86
  interface StoredBeacon {
@@ -30,8 +93,8 @@ interface StoredOperation {
30
93
  cid: string;
31
94
  jwsToken: string;
32
95
  /** Which chain type this operation belongs to */
33
- chainType: 'identity' | 'content';
34
- /** The chain identifier — DID for identity, contentId for content */
96
+ chainType: 'identity' | 'content' | 'artifact' | 'beacon' | 'countersign';
97
+ /** The chain identifier — DID for identity/beacon/artifact, contentId for content, targetCID for countersign */
35
98
  chainId: string;
36
99
  }
37
100
  /** Key for blob storage — deduplicates across chains sharing the same document */
@@ -39,6 +102,15 @@ interface BlobKey {
39
102
  creatorDID: string;
40
103
  documentCID: string;
41
104
  }
105
+ /** A single entry in the global append-only operation log */
106
+ interface LogEntry {
107
+ cid: string;
108
+ jwsToken: string;
109
+ kind: OperationKind;
110
+ chainId: string;
111
+ }
112
+ /** All operation kinds in the protocol */
113
+ type OperationKind = 'identity-op' | 'content-op' | 'beacon' | 'artifact' | 'countersign';
42
114
  /**
43
115
  * Storage backend for a DFOS web relay
44
116
  *
@@ -63,24 +135,87 @@ interface RelayStore {
63
135
  putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
64
136
  getCountersignatures(operationCID: string): Promise<string[]>;
65
137
  addCountersignature(operationCID: string, jwsToken: string): Promise<void>;
138
+ appendToLog(entry: LogEntry): Promise<void>;
139
+ readLog(params: {
140
+ after?: string;
141
+ limit: number;
142
+ }): Promise<{
143
+ entries: LogEntry[];
144
+ cursor: string | null;
145
+ }>;
146
+ /**
147
+ * Get the materialized identity state at a specific operation CID.
148
+ *
149
+ * Used by fork verification — the ingestion pipeline needs state at the fork
150
+ * point to verify signer authority and createdAt ordering.
151
+ *
152
+ * Implementations decide how to compute this:
153
+ * - MemoryStore: replay from genesis (chains are short in tests)
154
+ * - SQLiteStore: check snapshot table, replay from nearest snapshot
155
+ *
156
+ * Returns null if the CID is not in this chain's log.
157
+ */
158
+ getIdentityStateAtCID(did: string, cid: string): Promise<{
159
+ state: VerifiedIdentity;
160
+ lastCreatedAt: string;
161
+ } | null>;
162
+ /** Same for content chains */
163
+ getContentStateAtCID(contentId: string, cid: string): Promise<{
164
+ state: VerifiedContentChain;
165
+ lastCreatedAt: string;
166
+ } | null>;
167
+ /** Get last-synced log cursor for a peer relay */
168
+ getPeerCursor(peerUrl: string): Promise<string | undefined>;
169
+ /** Update last-synced log cursor for a peer relay */
170
+ setPeerCursor(peerUrl: string, cursor: string): Promise<void>;
66
171
  }
67
172
  interface IngestionResult {
68
173
  cid: string;
69
- status: 'accepted' | 'rejected';
174
+ status: 'new' | 'duplicate' | 'rejected';
70
175
  error?: string;
71
176
  /** What was ingested */
72
- kind?: 'identity-op' | 'content-op' | 'beacon' | 'countersig' | 'beacon-countersig';
177
+ kind?: OperationKind;
73
178
  /** Chain identifier if applicable */
74
179
  chainId?: string;
75
180
  }
76
181
 
182
+ /**
183
+ * Generate a relay identity and profile artifact, ingest both into the store
184
+ *
185
+ * Creates an Ed25519 keypair, signs an identity genesis operation, derives
186
+ * the DID, then signs a profile artifact with the relay's name. Both the
187
+ * identity genesis and profile artifact are ingested into the store so
188
+ * they are available via the relay's proof plane routes.
189
+ */
190
+ declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdentity>;
191
+
192
+ /**
193
+ * Create an HTTP-based PeerClient.
194
+ *
195
+ * Each method makes a single HTTP request to the peer relay URL. On any
196
+ * failure (network error, non-2xx response, invalid JSON), returns null
197
+ * for read operations or silently fails for write operations.
198
+ */
199
+ declare const createHttpPeerClient: () => PeerClient;
200
+
201
+ interface CreatedRelay {
202
+ /** Hono application implementing the DFOS web relay HTTP API */
203
+ app: Hono;
204
+ /** The relay's DID */
205
+ did: string;
206
+ /** Sync operations from all configured sync peers (call on a schedule) */
207
+ syncFromPeers: () => Promise<void>;
208
+ }
77
209
  /**
78
210
  * Create a DFOS web relay Hono application
79
211
  *
80
212
  * The returned app is portable — mount it on any Hono-compatible runtime
81
213
  * (Node.js, Cloudflare Workers, Deno, Bun, etc.).
214
+ *
215
+ * When `identity` is provided, the relay uses the given DID and profile. When
216
+ * omitted, a JIT identity and profile artifact are generated at startup.
82
217
  */
83
- declare const createRelay: (options: RelayOptions) => Hono;
218
+ declare const createRelay: (options: RelayOptions) => Promise<CreatedRelay>;
84
219
 
85
220
  /**
86
221
  * In-memory relay store — all data lives in Maps, lost on restart
@@ -94,6 +229,8 @@ declare class MemoryRelayStore implements RelayStore {
94
229
  private beacons;
95
230
  private blobs;
96
231
  private countersignatures;
232
+ private operationLog;
233
+ private peerCursors;
97
234
  getOperation(cid: string): Promise<StoredOperation | undefined>;
98
235
  putOperation(op: StoredOperation): Promise<void>;
99
236
  getIdentityChain(did: string): Promise<StoredIdentityChain | undefined>;
@@ -106,6 +243,24 @@ declare class MemoryRelayStore implements RelayStore {
106
243
  putBlob(key: BlobKey, data: Uint8Array): Promise<void>;
107
244
  getCountersignatures(operationCID: string): Promise<string[]>;
108
245
  addCountersignature(operationCID: string, jwsToken: string): Promise<void>;
246
+ appendToLog(entry: LogEntry): Promise<void>;
247
+ readLog(params: {
248
+ after?: string;
249
+ limit: number;
250
+ }): Promise<{
251
+ entries: LogEntry[];
252
+ cursor: string | null;
253
+ }>;
254
+ getIdentityStateAtCID(did: string, cid: string): Promise<{
255
+ state: VerifiedIdentity;
256
+ lastCreatedAt: string;
257
+ } | null>;
258
+ getContentStateAtCID(contentId: string, cid: string): Promise<{
259
+ state: VerifiedContentChain;
260
+ lastCreatedAt: string;
261
+ } | null>;
262
+ getPeerCursor(peerUrl: string): Promise<string | undefined>;
263
+ setPeerCursor(peerUrl: string, cursor: string): Promise<void>;
109
264
  }
110
265
 
111
266
  /**
@@ -138,6 +293,8 @@ declare const createCurrentKeyResolver: (store: RelayStore) => (kid: string) =>
138
293
  * are processed first so content chains and beacons can resolve their keys.
139
294
  * Within each kind, genesis operations are processed before extensions.
140
295
  */
141
- declare const ingestOperations: (tokens: string[], store: RelayStore) => Promise<IngestionResult[]>;
296
+ declare const ingestOperations: (tokens: string[], store: RelayStore, options?: {
297
+ logEnabled?: boolean;
298
+ }) => Promise<IngestionResult[]>;
142
299
 
143
- export { type BlobKey, type IngestionResult, MemoryRelayStore, type RelayOptions, type RelayStore, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, createCurrentKeyResolver, createKeyResolver, createRelay, ingestOperations };
300
+ export { type BlobKey, type CreatedRelay, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type PeerClient, type PeerConfig, type PeerLogEntry, type RelayIdentity, type RelayOptions, type RelayStore, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, createCurrentKeyResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations };