@metalabel/dfos-web-relay 0.5.0 → 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 +30 -7
- package/dist/index.d.ts +108 -4
- package/dist/index.js +371 -52
- 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,35 @@ 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>;
|
|
97
171
|
}
|
|
98
172
|
interface IngestionResult {
|
|
99
173
|
cid: string;
|
|
100
|
-
status: '
|
|
174
|
+
status: 'new' | 'duplicate' | 'rejected';
|
|
101
175
|
error?: string;
|
|
102
176
|
/** What was ingested */
|
|
103
177
|
kind?: OperationKind;
|
|
@@ -115,6 +189,23 @@ interface IngestionResult {
|
|
|
115
189
|
*/
|
|
116
190
|
declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdentity>;
|
|
117
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
|
+
}
|
|
118
209
|
/**
|
|
119
210
|
* Create a DFOS web relay Hono application
|
|
120
211
|
*
|
|
@@ -124,7 +215,7 @@ declare const bootstrapRelayIdentity: (store: RelayStore) => Promise<RelayIdenti
|
|
|
124
215
|
* When `identity` is provided, the relay uses the given DID and profile. When
|
|
125
216
|
* omitted, a JIT identity and profile artifact are generated at startup.
|
|
126
217
|
*/
|
|
127
|
-
declare const createRelay: (options: RelayOptions) => Promise<
|
|
218
|
+
declare const createRelay: (options: RelayOptions) => Promise<CreatedRelay>;
|
|
128
219
|
|
|
129
220
|
/**
|
|
130
221
|
* In-memory relay store — all data lives in Maps, lost on restart
|
|
@@ -139,6 +230,7 @@ declare class MemoryRelayStore implements RelayStore {
|
|
|
139
230
|
private blobs;
|
|
140
231
|
private countersignatures;
|
|
141
232
|
private operationLog;
|
|
233
|
+
private peerCursors;
|
|
142
234
|
getOperation(cid: string): Promise<StoredOperation | undefined>;
|
|
143
235
|
putOperation(op: StoredOperation): Promise<void>;
|
|
144
236
|
getIdentityChain(did: string): Promise<StoredIdentityChain | undefined>;
|
|
@@ -159,6 +251,16 @@ declare class MemoryRelayStore implements RelayStore {
|
|
|
159
251
|
entries: LogEntry[];
|
|
160
252
|
cursor: string | null;
|
|
161
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>;
|
|
162
264
|
}
|
|
163
265
|
|
|
164
266
|
/**
|
|
@@ -191,6 +293,8 @@ declare const createCurrentKeyResolver: (store: RelayStore) => (kid: string) =>
|
|
|
191
293
|
* are processed first so content chains and beacons can resolve their keys.
|
|
192
294
|
* Within each kind, genesis operations are processed before extensions.
|
|
193
295
|
*/
|
|
194
|
-
declare const ingestOperations: (tokens: string[], store: RelayStore
|
|
296
|
+
declare const ingestOperations: (tokens: string[], store: RelayStore, options?: {
|
|
297
|
+
logEnabled?: boolean;
|
|
298
|
+
}) => Promise<IngestionResult[]>;
|
|
195
299
|
|
|
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 };
|
|
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 };
|