@metalabel/dfos-web-relay 0.5.0 → 0.6.1

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
@@ -14,6 +14,55 @@ interface RelayOptions {
14
14
  identity?: RelayIdentity;
15
15
  /** Whether content plane routes are enabled (default: true) */
16
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>;
17
66
  }
18
67
  interface StoredIdentityChain {
19
68
  did: string;
@@ -94,10 +143,53 @@ interface RelayStore {
94
143
  entries: LogEntry[];
95
144
  cursor: string | null;
96
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>;
171
+ /** Store a raw JWS token by CID — idempotent, ignores duplicates */
172
+ putRawOp(cid: string, jwsToken: string): Promise<void>;
173
+ /** Return JWS tokens for unsequenced (pending) ops */
174
+ getUnsequencedOps(limit: number): Promise<string[]>;
175
+ /** Mark ops as successfully sequenced */
176
+ markOpsSequenced(cids: string[]): Promise<void>;
177
+ /** Mark an op as permanently rejected */
178
+ markOpRejected(cid: string, reason: string): Promise<void>;
179
+ /** Count of pending (unsequenced) raw ops */
180
+ countUnsequenced(): Promise<number>;
181
+ /** Reset all non-rejected raw ops to pending (re-sequence) */
182
+ resetSequencer(): Promise<void>;
183
+ }
184
+ /** Result of a sequencer run */
185
+ interface SequenceResult {
186
+ sequenced: number;
187
+ rejected: number;
188
+ pending: number;
97
189
  }
98
190
  interface IngestionResult {
99
191
  cid: string;
100
- status: 'accepted' | 'rejected';
192
+ status: 'new' | 'duplicate' | 'rejected';
101
193
  error?: string;
102
194
  /** What was ingested */
103
195
  kind?: OperationKind;
@@ -115,6 +207,23 @@ interface IngestionResult {
115
207
  */
116
208
  declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdentity>;
117
209
 
210
+ /**
211
+ * Create an HTTP-based PeerClient.
212
+ *
213
+ * Each method makes a single HTTP request to the peer relay URL. On any
214
+ * failure (network error, non-2xx response, invalid JSON), returns null
215
+ * for read operations or silently fails for write operations.
216
+ */
217
+ declare const createHttpPeerClient: () => PeerClient;
218
+
219
+ interface CreatedRelay {
220
+ /** Hono application implementing the DFOS web relay HTTP API */
221
+ app: Hono;
222
+ /** The relay's DID */
223
+ did: string;
224
+ /** Sync operations from all configured sync peers (call on a schedule) */
225
+ syncFromPeers: () => Promise<void>;
226
+ }
118
227
  /**
119
228
  * Create a DFOS web relay Hono application
120
229
  *
@@ -124,7 +233,7 @@ declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdenti
124
233
  * When `identity` is provided, the relay uses the given DID and profile. When
125
234
  * omitted, a JIT identity and profile artifact are generated at startup.
126
235
  */
127
- declare const createRelay: (options: RelayOptions) => Promise<Hono>;
236
+ declare const createRelay: (options: RelayOptions) => Promise<CreatedRelay>;
128
237
 
129
238
  /**
130
239
  * In-memory relay store — all data lives in Maps, lost on restart
@@ -139,6 +248,7 @@ declare class MemoryRelayStore implements RelayStore {
139
248
  private blobs;
140
249
  private countersignatures;
141
250
  private operationLog;
251
+ private peerCursors;
142
252
  getOperation(cid: string): Promise<StoredOperation | undefined>;
143
253
  putOperation(op: StoredOperation): Promise<void>;
144
254
  getIdentityChain(did: string): Promise<StoredIdentityChain | undefined>;
@@ -159,6 +269,23 @@ declare class MemoryRelayStore implements RelayStore {
159
269
  entries: LogEntry[];
160
270
  cursor: string | null;
161
271
  }>;
272
+ getIdentityStateAtCID(did: string, cid: string): Promise<{
273
+ state: VerifiedIdentity;
274
+ lastCreatedAt: string;
275
+ } | null>;
276
+ getContentStateAtCID(contentId: string, cid: string): Promise<{
277
+ state: VerifiedContentChain;
278
+ lastCreatedAt: string;
279
+ } | null>;
280
+ getPeerCursor(peerUrl: string): Promise<string | undefined>;
281
+ setPeerCursor(peerUrl: string, cursor: string): Promise<void>;
282
+ private rawOps;
283
+ putRawOp(cid: string, jwsToken: string): Promise<void>;
284
+ getUnsequencedOps(limit: number): Promise<string[]>;
285
+ markOpsSequenced(cids: string[]): Promise<void>;
286
+ markOpRejected(cid: string, _reason: string): Promise<void>;
287
+ countUnsequenced(): Promise<number>;
288
+ resetSequencer(): Promise<void>;
162
289
  }
163
290
 
164
291
  /**
@@ -191,6 +318,25 @@ declare const createCurrentKeyResolver: (store: RelayStore) => (kid: string) =>
191
318
  * are processed first so content chains and beacons can resolve their keys.
192
319
  * Within each kind, genesis operations are processed before extensions.
193
320
  */
194
- declare const ingestOperations: (tokens: string[], store: RelayStore) => Promise<IngestionResult[]>;
321
+ declare const ingestOperations: (tokens: string[], store: RelayStore, options?: {
322
+ logEnabled?: boolean;
323
+ }) => Promise<IngestionResult[]>;
324
+
325
+ /**
326
+ * Returns true if the rejection is due to a missing dependency that may
327
+ * arrive later via sync or gossip. Only these specific patterns are
328
+ * retryable — everything else is treated as permanent.
329
+ */
330
+ declare const isDependencyFailure: (error: string) => boolean;
331
+ /** Derive the operation CID from a JWS token */
332
+ declare const computeOpCID: (jwsToken: string) => Promise<string | undefined>;
333
+ /**
334
+ * Process unsequenced raw ops in a fixed-point loop until no more progress
335
+ * is made. Returns the JWS tokens of newly sequenced ops and aggregate stats.
336
+ */
337
+ declare const sequenceOps: (store: RelayStore) => Promise<{
338
+ newOps: string[];
339
+ result: SequenceResult;
340
+ }>;
195
341
 
196
- export { type BlobKey, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type RelayIdentity, type RelayOptions, type RelayStore, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, createCurrentKeyResolver, createKeyResolver, createRelay, ingestOperations };
342
+ 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 SequenceResult, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, computeOpCID, createCurrentKeyResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };