@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 +30 -7
- package/dist/index.d.ts +167 -10
- package/dist/index.js +635 -131
- package/dist/serve.d.ts +1 -1
- package/openapi.yaml +14 -2
- package/package.json +8 -7
- package/RELAY.md +0 -228
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
|
@@ -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: '
|
|
174
|
+
status: 'new' | 'duplicate' | 'rejected';
|
|
70
175
|
error?: string;
|
|
71
176
|
/** What was ingested */
|
|
72
|
-
kind?:
|
|
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) =>
|
|
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
|
|
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 };
|