@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 +30 -7
- package/dist/index.d.ts +150 -4
- package/dist/index.js +490 -63
- package/openapi.yaml +14 -2
- package/package.json +3 -4
- package/RELAY.md +0 -457
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](
|
|
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
|
-
//
|
|
26
|
-
|
|
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
|
-
|
|
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
|
|
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: '
|
|
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<
|
|
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
|
|
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 };
|