@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/dist/index.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
|
-
// src/
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
// src/bootstrap.ts
|
|
2
|
+
import {
|
|
3
|
+
encodeEd25519Multikey,
|
|
4
|
+
signArtifact,
|
|
5
|
+
signIdentityOperation
|
|
6
|
+
} from "@metalabel/dfos-protocol/chain";
|
|
7
|
+
import {
|
|
8
|
+
createNewEd25519Keypair,
|
|
9
|
+
generateId,
|
|
10
|
+
signPayloadEd25519
|
|
11
|
+
} from "@metalabel/dfos-protocol/crypto";
|
|
10
12
|
|
|
11
13
|
// src/ingest.ts
|
|
12
14
|
import {
|
|
13
15
|
decodeMultikey,
|
|
16
|
+
verifyArtifact,
|
|
14
17
|
verifyBeacon,
|
|
15
|
-
verifyBeaconCountersignature,
|
|
16
18
|
verifyContentChain,
|
|
19
|
+
verifyContentExtensionFromTrustedState,
|
|
17
20
|
verifyCountersignature,
|
|
18
|
-
verifyIdentityChain
|
|
21
|
+
verifyIdentityChain,
|
|
22
|
+
verifyIdentityExtensionFromTrustedState
|
|
19
23
|
} from "@metalabel/dfos-protocol/chain";
|
|
20
24
|
import { dagCborCanonicalEncode, decodeJwsUnsafe } from "@metalabel/dfos-protocol/crypto";
|
|
25
|
+
var MAX_FUTURE_TIMESTAMP_MS = 24 * 60 * 60 * 1e3;
|
|
26
|
+
var isFutureTimestamp = (createdAt) => {
|
|
27
|
+
const ts = new Date(createdAt).getTime();
|
|
28
|
+
if (isNaN(ts)) return false;
|
|
29
|
+
return ts > Date.now() + MAX_FUTURE_TIMESTAMP_MS;
|
|
30
|
+
};
|
|
21
31
|
var classify = (jwsToken) => {
|
|
22
32
|
const unknown = {
|
|
23
33
|
jwsToken,
|
|
@@ -44,30 +54,10 @@ var classify = (jwsToken) => {
|
|
|
44
54
|
}
|
|
45
55
|
if (typ === "did:dfos:content-op") {
|
|
46
56
|
const opDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
47
|
-
if (opDID && kidDID && kidDID !== opDID) {
|
|
48
|
-
return {
|
|
49
|
-
...base,
|
|
50
|
-
kind: "countersig",
|
|
51
|
-
referencedDID: opDID,
|
|
52
|
-
signerDID: kidDID,
|
|
53
|
-
priority: 3,
|
|
54
|
-
previousCID: null
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
57
|
return { ...base, kind: "content-op", referencedDID: null, signerDID: opDID, priority: 2 };
|
|
58
58
|
}
|
|
59
59
|
if (typ === "did:dfos:beacon") {
|
|
60
60
|
const beaconDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
61
|
-
if (beaconDID && kidDID && kidDID !== beaconDID) {
|
|
62
|
-
return {
|
|
63
|
-
...base,
|
|
64
|
-
kind: "beacon-countersig",
|
|
65
|
-
referencedDID: beaconDID,
|
|
66
|
-
signerDID: kidDID,
|
|
67
|
-
priority: 3,
|
|
68
|
-
previousCID: null
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
61
|
return {
|
|
72
62
|
...base,
|
|
73
63
|
kind: "beacon",
|
|
@@ -77,6 +67,30 @@ var classify = (jwsToken) => {
|
|
|
77
67
|
previousCID: null
|
|
78
68
|
};
|
|
79
69
|
}
|
|
70
|
+
if (typ === "did:dfos:countersign") {
|
|
71
|
+
const witnessDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
72
|
+
return {
|
|
73
|
+
...base,
|
|
74
|
+
kind: "countersign",
|
|
75
|
+
referencedDID: witnessDID,
|
|
76
|
+
signerDID: null,
|
|
77
|
+
priority: 3,
|
|
78
|
+
// processed last — target must already be ingested
|
|
79
|
+
previousCID: null
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (typ === "did:dfos:artifact") {
|
|
83
|
+
const artifactDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
kind: "artifact",
|
|
87
|
+
referencedDID: artifactDID,
|
|
88
|
+
signerDID: null,
|
|
89
|
+
priority: 1,
|
|
90
|
+
// same as beacons — needs identity keys resolved first
|
|
91
|
+
previousCID: null
|
|
92
|
+
};
|
|
93
|
+
}
|
|
80
94
|
return unknown;
|
|
81
95
|
};
|
|
82
96
|
var createKeyResolver = (store) => async (kid) => {
|
|
@@ -128,12 +142,16 @@ var createCurrentKeyResolver = (store) => async (kid) => {
|
|
|
128
142
|
if (currentKey) return decodeMultikey(currentKey.publicKeyMultibase).keyBytes;
|
|
129
143
|
throw new Error(`unknown key ${keyId} on identity ${did}`);
|
|
130
144
|
};
|
|
131
|
-
var ingestIdentityOp = async (jwsToken, store) => {
|
|
145
|
+
var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
|
|
132
146
|
const decoded = decodeJwsUnsafe(jwsToken);
|
|
133
147
|
if (!decoded) return { cid: "", status: "rejected", error: "failed to decode JWS" };
|
|
134
148
|
const payload = decoded.payload;
|
|
135
149
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
136
150
|
const cid = encoded.cid.toString();
|
|
151
|
+
const createdAtVal = payload["createdAt"];
|
|
152
|
+
if (typeof createdAtVal === "string" && isFutureTimestamp(createdAtVal)) {
|
|
153
|
+
return { cid, status: "rejected", error: "createdAt is too far in the future" };
|
|
154
|
+
}
|
|
137
155
|
const existing = await store.getOperation(cid);
|
|
138
156
|
if (existing) {
|
|
139
157
|
if (existing.jwsToken !== jwsToken) {
|
|
@@ -143,16 +161,26 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
143
161
|
error: "operation already exists with a different signature"
|
|
144
162
|
};
|
|
145
163
|
}
|
|
146
|
-
return { cid, status: "
|
|
164
|
+
return { cid, status: "duplicate", kind: "identity-op", chainId: existing.chainId };
|
|
147
165
|
}
|
|
148
166
|
const opType = payload["type"];
|
|
149
167
|
const isGenesis = opType === "create";
|
|
150
168
|
if (isGenesis) {
|
|
151
|
-
const
|
|
152
|
-
const
|
|
169
|
+
const identity = await verifyIdentityChain({ didPrefix: "did:dfos", log: [jwsToken] });
|
|
170
|
+
const createdAt = payload["createdAt"];
|
|
171
|
+
const chain2 = {
|
|
172
|
+
did: identity.did,
|
|
173
|
+
log: [jwsToken],
|
|
174
|
+
headCID: cid,
|
|
175
|
+
lastCreatedAt: createdAt,
|
|
176
|
+
state: identity
|
|
177
|
+
};
|
|
153
178
|
await store.putIdentityChain(chain2);
|
|
154
|
-
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId:
|
|
155
|
-
|
|
179
|
+
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId: identity.did });
|
|
180
|
+
if (logEnabled) {
|
|
181
|
+
await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: identity.did });
|
|
182
|
+
}
|
|
183
|
+
return { cid, status: "new", kind: "identity-op", chainId: identity.did };
|
|
156
184
|
}
|
|
157
185
|
const kid = decoded.header.kid;
|
|
158
186
|
const hashIdx = kid.indexOf("#");
|
|
@@ -160,19 +188,78 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
160
188
|
const did = kid.substring(0, hashIdx);
|
|
161
189
|
const chain = await store.getIdentityChain(did);
|
|
162
190
|
if (!chain) return { cid, status: "rejected", error: `unknown identity: ${did}` };
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
191
|
+
const previousCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
192
|
+
if (previousCID === chain.headCID) {
|
|
193
|
+
const extResult2 = await verifyIdentityExtensionFromTrustedState({
|
|
194
|
+
currentState: chain.state,
|
|
195
|
+
headCID: chain.headCID,
|
|
196
|
+
lastCreatedAt: chain.lastCreatedAt,
|
|
197
|
+
newOp: jwsToken
|
|
198
|
+
});
|
|
199
|
+
const updated2 = {
|
|
200
|
+
did: chain.did,
|
|
201
|
+
log: [...chain.log, jwsToken],
|
|
202
|
+
headCID: extResult2.operationCID,
|
|
203
|
+
lastCreatedAt: extResult2.createdAt,
|
|
204
|
+
state: extResult2.state
|
|
205
|
+
};
|
|
206
|
+
await store.putIdentityChain(updated2);
|
|
207
|
+
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId: did });
|
|
208
|
+
if (logEnabled) {
|
|
209
|
+
await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: did });
|
|
210
|
+
}
|
|
211
|
+
return { cid, status: "new", kind: "identity-op", chainId: did };
|
|
212
|
+
}
|
|
213
|
+
if (!previousCID || !chainLogContainsCID(chain.log, previousCID)) {
|
|
214
|
+
return { cid, status: "rejected", error: "unknown previous operation in identity chain" };
|
|
215
|
+
}
|
|
216
|
+
const forkState = await store.getIdentityStateAtCID(did, previousCID);
|
|
217
|
+
if (!forkState) {
|
|
218
|
+
return { cid, status: "rejected", error: "failed to compute state at fork point" };
|
|
219
|
+
}
|
|
220
|
+
const extResult = await verifyIdentityExtensionFromTrustedState({
|
|
221
|
+
currentState: forkState.state,
|
|
222
|
+
headCID: previousCID,
|
|
223
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
224
|
+
newOp: jwsToken
|
|
225
|
+
});
|
|
226
|
+
const updatedLog = [...chain.log, jwsToken];
|
|
227
|
+
const head = selectDeterministicHead(updatedLog);
|
|
228
|
+
let headState = chain.state;
|
|
229
|
+
let headLastCreatedAt = chain.lastCreatedAt;
|
|
230
|
+
let headCID = chain.headCID;
|
|
231
|
+
if (head.cid === cid) {
|
|
232
|
+
headState = extResult.state;
|
|
233
|
+
headLastCreatedAt = extResult.createdAt;
|
|
234
|
+
headCID = cid;
|
|
235
|
+
} else {
|
|
236
|
+
headCID = head.cid;
|
|
237
|
+
headLastCreatedAt = head.createdAt;
|
|
238
|
+
}
|
|
239
|
+
const updated = {
|
|
240
|
+
did: chain.did,
|
|
241
|
+
log: updatedLog,
|
|
242
|
+
headCID,
|
|
243
|
+
lastCreatedAt: headLastCreatedAt,
|
|
244
|
+
state: headState
|
|
245
|
+
};
|
|
166
246
|
await store.putIdentityChain(updated);
|
|
167
247
|
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId: did });
|
|
168
|
-
|
|
248
|
+
if (logEnabled) {
|
|
249
|
+
await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: did });
|
|
250
|
+
}
|
|
251
|
+
return { cid, status: "new", kind: "identity-op", chainId: did };
|
|
169
252
|
};
|
|
170
|
-
var ingestContentOp = async (jwsToken, store) => {
|
|
253
|
+
var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
171
254
|
const decoded = decodeJwsUnsafe(jwsToken);
|
|
172
255
|
if (!decoded) return { cid: "", status: "rejected", error: "failed to decode JWS" };
|
|
173
256
|
const payload = decoded.payload;
|
|
174
257
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
175
258
|
const cid = encoded.cid.toString();
|
|
259
|
+
const createdAtVal = payload["createdAt"];
|
|
260
|
+
if (typeof createdAtVal === "string" && isFutureTimestamp(createdAtVal)) {
|
|
261
|
+
return { cid, status: "rejected", error: "createdAt is too far in the future" };
|
|
262
|
+
}
|
|
176
263
|
const existing = await store.getOperation(cid);
|
|
177
264
|
if (existing) {
|
|
178
265
|
if (existing.jwsToken !== jwsToken) {
|
|
@@ -182,7 +269,7 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
182
269
|
error: "operation already exists with a different signature"
|
|
183
270
|
};
|
|
184
271
|
}
|
|
185
|
-
return { cid, status: "
|
|
272
|
+
return { cid, status: "duplicate", kind: "content-op", chainId: existing.chainId };
|
|
186
273
|
}
|
|
187
274
|
const signerDID = payload["did"];
|
|
188
275
|
if (typeof signerDID === "string") {
|
|
@@ -195,20 +282,25 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
195
282
|
const opType = payload["type"];
|
|
196
283
|
const isGenesis = opType === "create";
|
|
197
284
|
if (isGenesis) {
|
|
198
|
-
const
|
|
285
|
+
const content = await verifyContentChain({
|
|
199
286
|
log: [jwsToken],
|
|
200
287
|
resolveKey,
|
|
201
288
|
enforceAuthorization: true
|
|
202
289
|
});
|
|
290
|
+
const createdAt = payload["createdAt"];
|
|
203
291
|
const chain2 = {
|
|
204
|
-
contentId:
|
|
205
|
-
genesisCID:
|
|
292
|
+
contentId: content.contentId,
|
|
293
|
+
genesisCID: content.genesisCID,
|
|
206
294
|
log: [jwsToken],
|
|
207
|
-
|
|
295
|
+
lastCreatedAt: createdAt,
|
|
296
|
+
state: content
|
|
208
297
|
};
|
|
209
298
|
await store.putContentChain(chain2);
|
|
210
|
-
await store.putOperation({ cid, jwsToken, chainType: "content", chainId:
|
|
211
|
-
|
|
299
|
+
await store.putOperation({ cid, jwsToken, chainType: "content", chainId: content.contentId });
|
|
300
|
+
if (logEnabled) {
|
|
301
|
+
await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: content.contentId });
|
|
302
|
+
}
|
|
303
|
+
return { cid, status: "new", kind: "content-op", chainId: content.contentId };
|
|
212
304
|
}
|
|
213
305
|
const previousCID = payload["previousOperationCID"];
|
|
214
306
|
if (typeof previousCID !== "string") {
|
|
@@ -227,26 +319,65 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
227
319
|
if (creatorIdentity?.state.isDeleted) {
|
|
228
320
|
return { cid, status: "rejected", error: "content creator identity is deleted" };
|
|
229
321
|
}
|
|
230
|
-
if (chain.state.headCID
|
|
231
|
-
|
|
322
|
+
if (chain.state.headCID === previousCID) {
|
|
323
|
+
const extResult2 = await verifyContentExtensionFromTrustedState({
|
|
324
|
+
currentState: chain.state,
|
|
325
|
+
lastCreatedAt: chain.lastCreatedAt,
|
|
326
|
+
newOp: jwsToken,
|
|
327
|
+
resolveKey,
|
|
328
|
+
enforceAuthorization: true
|
|
329
|
+
});
|
|
330
|
+
const updated2 = {
|
|
331
|
+
contentId: chain.contentId,
|
|
332
|
+
genesisCID: chain.genesisCID,
|
|
333
|
+
log: [...chain.log, jwsToken],
|
|
334
|
+
lastCreatedAt: extResult2.createdAt,
|
|
335
|
+
state: extResult2.state
|
|
336
|
+
};
|
|
337
|
+
await store.putContentChain(updated2);
|
|
338
|
+
await store.putOperation({ cid, jwsToken, chainType: "content", chainId: chain.contentId });
|
|
339
|
+
if (logEnabled) {
|
|
340
|
+
await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: chain.contentId });
|
|
341
|
+
}
|
|
342
|
+
return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
|
|
343
|
+
}
|
|
344
|
+
if (!chainLogContainsCID(chain.log, previousCID)) {
|
|
345
|
+
return { cid, status: "rejected", error: "unknown previous operation in content chain" };
|
|
346
|
+
}
|
|
347
|
+
const forkState = await store.getContentStateAtCID(chain.contentId, previousCID);
|
|
348
|
+
if (!forkState) {
|
|
349
|
+
return { cid, status: "rejected", error: "failed to compute state at fork point" };
|
|
232
350
|
}
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
|
|
351
|
+
const extResult = await verifyContentExtensionFromTrustedState({
|
|
352
|
+
currentState: forkState.state,
|
|
353
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
354
|
+
newOp: jwsToken,
|
|
236
355
|
resolveKey,
|
|
237
356
|
enforceAuthorization: true
|
|
238
357
|
});
|
|
358
|
+
const updatedLog = [...chain.log, jwsToken];
|
|
359
|
+
const head = selectDeterministicHead(updatedLog);
|
|
360
|
+
let headState = chain.state;
|
|
361
|
+
let headLastCreatedAt = chain.lastCreatedAt;
|
|
362
|
+
if (head.cid === cid) {
|
|
363
|
+
headState = extResult.state;
|
|
364
|
+
headLastCreatedAt = extResult.createdAt;
|
|
365
|
+
}
|
|
239
366
|
const updated = {
|
|
240
|
-
contentId:
|
|
241
|
-
genesisCID:
|
|
242
|
-
log:
|
|
243
|
-
|
|
367
|
+
contentId: chain.contentId,
|
|
368
|
+
genesisCID: chain.genesisCID,
|
|
369
|
+
log: updatedLog,
|
|
370
|
+
lastCreatedAt: headLastCreatedAt,
|
|
371
|
+
state: headState
|
|
244
372
|
};
|
|
245
373
|
await store.putContentChain(updated);
|
|
246
|
-
await store.putOperation({ cid, jwsToken, chainType: "content", chainId:
|
|
247
|
-
|
|
374
|
+
await store.putOperation({ cid, jwsToken, chainType: "content", chainId: chain.contentId });
|
|
375
|
+
if (logEnabled) {
|
|
376
|
+
await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: chain.contentId });
|
|
377
|
+
}
|
|
378
|
+
return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
|
|
248
379
|
};
|
|
249
|
-
var ingestBeacon = async (jwsToken, store) => {
|
|
380
|
+
var ingestBeacon = async (jwsToken, store, logEnabled) => {
|
|
250
381
|
const resolveKey = createKeyResolver(store);
|
|
251
382
|
let verified;
|
|
252
383
|
try {
|
|
@@ -266,76 +397,106 @@ var ingestBeacon = async (jwsToken, store) => {
|
|
|
266
397
|
const existingTime = new Date(existing.state.payload.createdAt).getTime();
|
|
267
398
|
const newTime = new Date(verified.payload.createdAt).getTime();
|
|
268
399
|
if (newTime <= existingTime) {
|
|
269
|
-
return { cid, status: "
|
|
400
|
+
return { cid, status: "duplicate", kind: "beacon", chainId: did };
|
|
270
401
|
}
|
|
271
402
|
}
|
|
272
403
|
await store.putBeacon({ did, jwsToken, beaconCID: cid, state: verified });
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const decoded = decodeJwsUnsafe(jwsToken);
|
|
277
|
-
if (!decoded) return { cid: "", status: "rejected", error: "failed to decode JWS" };
|
|
278
|
-
const payload = decoded.payload;
|
|
279
|
-
const encoded = await dagCborCanonicalEncode(payload);
|
|
280
|
-
const operationCID = encoded.cid.toString();
|
|
281
|
-
const existingOp = await store.getOperation(operationCID);
|
|
282
|
-
if (!existingOp) {
|
|
283
|
-
return { cid: operationCID, status: "rejected", error: `unknown operation: ${operationCID}` };
|
|
404
|
+
await store.putOperation({ cid, jwsToken, chainType: "beacon", chainId: did });
|
|
405
|
+
if (logEnabled) {
|
|
406
|
+
await store.appendToLog({ cid, jwsToken, kind: "beacon", chainId: did });
|
|
284
407
|
}
|
|
408
|
+
return { cid, status: "new", kind: "beacon", chainId: did };
|
|
409
|
+
};
|
|
410
|
+
var ingestCountersign = async (jwsToken, store, logEnabled) => {
|
|
285
411
|
const resolveKey = createKeyResolver(store);
|
|
412
|
+
let verified;
|
|
286
413
|
try {
|
|
287
|
-
await verifyCountersignature({ jwsToken,
|
|
414
|
+
verified = await verifyCountersignature({ jwsToken, resolveKey });
|
|
288
415
|
} catch (err) {
|
|
289
416
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
290
|
-
return { cid:
|
|
417
|
+
return { cid: "", status: "rejected", error: message };
|
|
291
418
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
const encoded = await dagCborCanonicalEncode(payload);
|
|
305
|
-
const beaconCID = encoded.cid.toString();
|
|
306
|
-
const beaconDID = typeof payload["did"] === "string" ? payload["did"] : null;
|
|
307
|
-
if (!beaconDID) {
|
|
308
|
-
return { cid: beaconCID, status: "rejected", error: "missing beacon DID" };
|
|
419
|
+
const cid = verified.countersignCID;
|
|
420
|
+
const { witnessDID, targetCID } = verified;
|
|
421
|
+
const existing = await store.getOperation(cid);
|
|
422
|
+
if (existing) {
|
|
423
|
+
if (existing.jwsToken !== jwsToken) {
|
|
424
|
+
return {
|
|
425
|
+
cid,
|
|
426
|
+
status: "rejected",
|
|
427
|
+
error: "countersign already exists with a different signature"
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return { cid, status: "duplicate", kind: "countersign", chainId: targetCID };
|
|
309
431
|
}
|
|
310
|
-
const
|
|
311
|
-
if (!
|
|
312
|
-
return { cid
|
|
432
|
+
const targetOp = await store.getOperation(targetCID);
|
|
433
|
+
if (!targetOp) {
|
|
434
|
+
return { cid, status: "rejected", error: `unknown target operation: ${targetCID}` };
|
|
313
435
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
436
|
+
let targetAuthorDID = null;
|
|
437
|
+
if (targetOp.chainType === "identity") {
|
|
438
|
+
targetAuthorDID = targetOp.chainId;
|
|
439
|
+
} else {
|
|
440
|
+
const targetDecoded = decodeJwsUnsafe(targetOp.jwsToken);
|
|
441
|
+
if (targetDecoded) {
|
|
442
|
+
const targetPayload = targetDecoded.payload;
|
|
443
|
+
targetAuthorDID = typeof targetPayload["did"] === "string" ? targetPayload["did"] : null;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (targetAuthorDID && witnessDID === targetAuthorDID) {
|
|
447
|
+
return { cid, status: "rejected", error: "witness DID must differ from target author DID" };
|
|
320
448
|
}
|
|
449
|
+
const witnessIdentity = await store.getIdentityChain(witnessDID);
|
|
450
|
+
if (witnessIdentity?.state.isDeleted) {
|
|
451
|
+
return { cid, status: "rejected", error: "witness identity is deleted" };
|
|
452
|
+
}
|
|
453
|
+
const existingCountersigns = await store.getCountersignatures(targetCID);
|
|
454
|
+
for (const csJws of existingCountersigns) {
|
|
455
|
+
const csDecoded = decodeJwsUnsafe(csJws);
|
|
456
|
+
if (!csDecoded) continue;
|
|
457
|
+
const csPayload = csDecoded.payload;
|
|
458
|
+
if (csPayload["did"] === witnessDID) {
|
|
459
|
+
return { cid, status: "duplicate", kind: "countersign", chainId: targetCID };
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
await store.putOperation({ cid, jwsToken, chainType: "countersign", chainId: targetCID });
|
|
463
|
+
await store.addCountersignature(targetCID, jwsToken);
|
|
464
|
+
if (logEnabled) {
|
|
465
|
+
await store.appendToLog({ cid, jwsToken, kind: "countersign", chainId: targetCID });
|
|
466
|
+
}
|
|
467
|
+
return { cid, status: "new", kind: "countersign", chainId: targetCID };
|
|
468
|
+
};
|
|
469
|
+
var ingestArtifact = async (jwsToken, store, logEnabled) => {
|
|
321
470
|
const resolveKey = createKeyResolver(store);
|
|
471
|
+
let verified;
|
|
322
472
|
try {
|
|
323
|
-
await
|
|
324
|
-
jwsToken,
|
|
325
|
-
expectedCID: beaconCID,
|
|
326
|
-
resolveKey
|
|
327
|
-
});
|
|
473
|
+
verified = await verifyArtifact({ jwsToken, resolveKey });
|
|
328
474
|
} catch (err) {
|
|
329
475
|
const message = err instanceof Error ? err.message : "verification failed";
|
|
330
|
-
return { cid:
|
|
476
|
+
return { cid: "", status: "rejected", error: message };
|
|
331
477
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
478
|
+
const cid = verified.artifactCID;
|
|
479
|
+
const did = verified.payload.did;
|
|
480
|
+
const existing = await store.getOperation(cid);
|
|
481
|
+
if (existing) {
|
|
482
|
+
if (existing.jwsToken !== jwsToken) {
|
|
483
|
+
return {
|
|
484
|
+
cid,
|
|
485
|
+
status: "rejected",
|
|
486
|
+
error: "artifact already exists with a different signature"
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
return { cid, status: "duplicate", kind: "artifact", chainId: did };
|
|
490
|
+
}
|
|
491
|
+
const identity = await store.getIdentityChain(did);
|
|
492
|
+
if (identity?.state.isDeleted) {
|
|
493
|
+
return { cid, status: "rejected", error: "identity is deleted" };
|
|
494
|
+
}
|
|
495
|
+
await store.putOperation({ cid, jwsToken, chainType: "artifact", chainId: did });
|
|
496
|
+
if (logEnabled) {
|
|
497
|
+
await store.appendToLog({ cid, jwsToken, kind: "artifact", chainId: did });
|
|
498
|
+
}
|
|
499
|
+
return { cid, status: "new", kind: "artifact", chainId: did };
|
|
339
500
|
};
|
|
340
501
|
var dependencySort = (ops) => {
|
|
341
502
|
const buckets = /* @__PURE__ */ new Map();
|
|
@@ -394,7 +555,36 @@ var topologicalSortBucket = (ops) => {
|
|
|
394
555
|
}
|
|
395
556
|
return sorted;
|
|
396
557
|
};
|
|
397
|
-
var
|
|
558
|
+
var chainLogContainsCID = (log, targetCID) => {
|
|
559
|
+
for (const jws of log) {
|
|
560
|
+
const decoded = decodeJwsUnsafe(jws);
|
|
561
|
+
if (!decoded) continue;
|
|
562
|
+
if (decoded.header.cid === targetCID) return true;
|
|
563
|
+
}
|
|
564
|
+
return false;
|
|
565
|
+
};
|
|
566
|
+
var selectDeterministicHead = (log) => {
|
|
567
|
+
const ops = [];
|
|
568
|
+
const hasChild = /* @__PURE__ */ new Set();
|
|
569
|
+
for (const jws of log) {
|
|
570
|
+
const decoded = decodeJwsUnsafe(jws);
|
|
571
|
+
if (!decoded) continue;
|
|
572
|
+
const payload = decoded.payload;
|
|
573
|
+
const cid = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
574
|
+
const previousCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
575
|
+
const createdAt = typeof payload["createdAt"] === "string" ? payload["createdAt"] : "";
|
|
576
|
+
ops.push({ cid, previousCID, createdAt });
|
|
577
|
+
if (previousCID) hasChild.add(previousCID);
|
|
578
|
+
}
|
|
579
|
+
const tips = ops.filter((op) => !hasChild.has(op.cid));
|
|
580
|
+
tips.sort((a, b) => {
|
|
581
|
+
if (a.createdAt !== b.createdAt) return b.createdAt.localeCompare(a.createdAt);
|
|
582
|
+
return b.cid.localeCompare(a.cid);
|
|
583
|
+
});
|
|
584
|
+
return tips[0] ?? { cid: "", createdAt: "" };
|
|
585
|
+
};
|
|
586
|
+
var ingestOperations = async (tokens, store, options) => {
|
|
587
|
+
const logEnabled = options?.logEnabled !== false;
|
|
398
588
|
const classified = tokens.map((token, i) => ({ ...classify(token), originalIndex: i }));
|
|
399
589
|
const sorted = dependencySort(classified);
|
|
400
590
|
const indexedResults = [];
|
|
@@ -403,19 +593,19 @@ var ingestOperations = async (tokens, store) => {
|
|
|
403
593
|
let result;
|
|
404
594
|
switch (op.kind) {
|
|
405
595
|
case "identity-op":
|
|
406
|
-
result = await ingestIdentityOp(op.jwsToken, store);
|
|
596
|
+
result = await ingestIdentityOp(op.jwsToken, store, logEnabled);
|
|
407
597
|
break;
|
|
408
598
|
case "content-op":
|
|
409
|
-
result = await ingestContentOp(op.jwsToken, store);
|
|
599
|
+
result = await ingestContentOp(op.jwsToken, store, logEnabled);
|
|
410
600
|
break;
|
|
411
601
|
case "beacon":
|
|
412
|
-
result = await ingestBeacon(op.jwsToken, store);
|
|
602
|
+
result = await ingestBeacon(op.jwsToken, store, logEnabled);
|
|
413
603
|
break;
|
|
414
|
-
case "
|
|
415
|
-
result = await
|
|
604
|
+
case "countersign":
|
|
605
|
+
result = await ingestCountersign(op.jwsToken, store, logEnabled);
|
|
416
606
|
break;
|
|
417
|
-
case "
|
|
418
|
-
result = await
|
|
607
|
+
case "artifact":
|
|
608
|
+
result = await ingestArtifact(op.jwsToken, store, logEnabled);
|
|
419
609
|
break;
|
|
420
610
|
default:
|
|
421
611
|
result = { cid: "", status: "rejected", error: "unrecognized operation type" };
|
|
@@ -432,7 +622,114 @@ var ingestOperations = async (tokens, store) => {
|
|
|
432
622
|
return indexedResults.sort((a, b) => a.index - b.index).map((r) => r.result);
|
|
433
623
|
};
|
|
434
624
|
|
|
625
|
+
// src/bootstrap.ts
|
|
626
|
+
var bootstrapRelayIdentity = async (store) => {
|
|
627
|
+
const keypair = createNewEd25519Keypair();
|
|
628
|
+
const keyId = generateId("key");
|
|
629
|
+
const multibase = encodeEd25519Multikey(keypair.publicKey);
|
|
630
|
+
const signer = async (msg) => signPayloadEd25519(msg, keypair.privateKey);
|
|
631
|
+
const key = { id: keyId, type: "Multikey", publicKeyMultibase: multibase };
|
|
632
|
+
const identityOp = {
|
|
633
|
+
version: 1,
|
|
634
|
+
type: "create",
|
|
635
|
+
authKeys: [key],
|
|
636
|
+
assertKeys: [key],
|
|
637
|
+
controllerKeys: [key],
|
|
638
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
639
|
+
};
|
|
640
|
+
const { jwsToken: identityJws } = await signIdentityOperation({
|
|
641
|
+
operation: identityOp,
|
|
642
|
+
signer,
|
|
643
|
+
keyId
|
|
644
|
+
});
|
|
645
|
+
const [identityResult] = await ingestOperations([identityJws], store);
|
|
646
|
+
if (!identityResult || identityResult.status === "rejected" || !identityResult.chainId) {
|
|
647
|
+
throw new Error(`failed to bootstrap relay identity: ${identityResult?.error ?? "unknown"}`);
|
|
648
|
+
}
|
|
649
|
+
const did = identityResult.chainId;
|
|
650
|
+
const profilePayload = {
|
|
651
|
+
version: 1,
|
|
652
|
+
type: "artifact",
|
|
653
|
+
did,
|
|
654
|
+
content: {
|
|
655
|
+
$schema: "https://schemas.dfos.com/profile/v1",
|
|
656
|
+
name: "DFOS Relay"
|
|
657
|
+
},
|
|
658
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
659
|
+
};
|
|
660
|
+
const kid = `${did}#${keyId}`;
|
|
661
|
+
const { jwsToken: profileArtifactJws } = await signArtifact({
|
|
662
|
+
payload: profilePayload,
|
|
663
|
+
signer,
|
|
664
|
+
kid
|
|
665
|
+
});
|
|
666
|
+
const [artifactResult] = await ingestOperations([profileArtifactJws], store);
|
|
667
|
+
if (!artifactResult || artifactResult.status === "rejected") {
|
|
668
|
+
throw new Error(
|
|
669
|
+
`failed to ingest relay profile artifact: ${artifactResult?.error ?? "unknown"}`
|
|
670
|
+
);
|
|
671
|
+
}
|
|
672
|
+
return { did, profileArtifactJws };
|
|
673
|
+
};
|
|
674
|
+
|
|
675
|
+
// src/peer-client.ts
|
|
676
|
+
var createHttpPeerClient = () => {
|
|
677
|
+
const fetchJSON = async (url) => {
|
|
678
|
+
try {
|
|
679
|
+
const res = await fetch(url);
|
|
680
|
+
if (!res.ok) return null;
|
|
681
|
+
return await res.json();
|
|
682
|
+
} catch {
|
|
683
|
+
return null;
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
async getIdentityLog(peerUrl, did, params) {
|
|
688
|
+
const url = new URL(`/identities/${encodeURIComponent(did)}/log`, peerUrl);
|
|
689
|
+
if (params?.after) url.searchParams.set("after", params.after);
|
|
690
|
+
if (params?.limit) url.searchParams.set("limit", String(params.limit));
|
|
691
|
+
const data = await fetchJSON(url.toString());
|
|
692
|
+
if (!data?.entries) return null;
|
|
693
|
+
return { entries: data.entries, cursor: data.cursor ?? null };
|
|
694
|
+
},
|
|
695
|
+
async getContentLog(peerUrl, contentId, params) {
|
|
696
|
+
const url = new URL(`/content/${encodeURIComponent(contentId)}/log`, peerUrl);
|
|
697
|
+
if (params?.after) url.searchParams.set("after", params.after);
|
|
698
|
+
if (params?.limit) url.searchParams.set("limit", String(params.limit));
|
|
699
|
+
const data = await fetchJSON(url.toString());
|
|
700
|
+
if (!data?.entries) return null;
|
|
701
|
+
return { entries: data.entries, cursor: data.cursor ?? null };
|
|
702
|
+
},
|
|
703
|
+
async getOperationLog(peerUrl, params) {
|
|
704
|
+
const url = new URL("/log", peerUrl);
|
|
705
|
+
if (params?.after) url.searchParams.set("after", params.after);
|
|
706
|
+
if (params?.limit) url.searchParams.set("limit", String(params.limit));
|
|
707
|
+
const data = await fetchJSON(url.toString());
|
|
708
|
+
if (!data?.entries) return null;
|
|
709
|
+
return { entries: data.entries, cursor: data.cursor ?? null };
|
|
710
|
+
},
|
|
711
|
+
async submitOperations(peerUrl, operations) {
|
|
712
|
+
try {
|
|
713
|
+
await fetch(new URL("/operations", peerUrl).toString(), {
|
|
714
|
+
method: "POST",
|
|
715
|
+
headers: { "Content-Type": "application/json" },
|
|
716
|
+
body: JSON.stringify({ operations })
|
|
717
|
+
});
|
|
718
|
+
} catch {
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
// src/relay.ts
|
|
725
|
+
import { VC_TYPE_CONTENT_READ, verifyCredential } from "@metalabel/dfos-protocol/credentials";
|
|
726
|
+
import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
|
|
727
|
+
import { Hono } from "hono";
|
|
728
|
+
import { z } from "zod";
|
|
729
|
+
|
|
435
730
|
// src/auth.ts
|
|
731
|
+
import { verifyAuthToken } from "@metalabel/dfos-protocol/credentials";
|
|
732
|
+
import { decodeJwsUnsafe as decodeJwsUnsafe2 } from "@metalabel/dfos-protocol/crypto";
|
|
436
733
|
var authenticateRequest = async (authHeader, relayDID, store) => {
|
|
437
734
|
if (!authHeader) return null;
|
|
438
735
|
if (!authHeader.startsWith("Bearer ")) return null;
|
|
@@ -460,14 +757,44 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
|
|
|
460
757
|
var IngestBody = z.object({
|
|
461
758
|
operations: z.array(z.string()).min(1).max(100)
|
|
462
759
|
});
|
|
463
|
-
var createRelay = (options) => {
|
|
464
|
-
const {
|
|
760
|
+
var createRelay = async (options) => {
|
|
761
|
+
const { store } = options;
|
|
762
|
+
const contentEnabled = options.content !== false;
|
|
763
|
+
const logEnabled = options.log !== false;
|
|
764
|
+
const peers = options.peers ?? [];
|
|
765
|
+
const peerClient = options.peerClient;
|
|
766
|
+
const gossipPeers = peers.filter((p) => p.gossip !== false);
|
|
767
|
+
const readThroughPeers = peers.filter((p) => p.readThrough !== false);
|
|
768
|
+
const syncPeers = peers.filter((p) => p.sync !== false);
|
|
769
|
+
const identity = options.identity ?? await bootstrapRelayIdentity(store);
|
|
770
|
+
const relayDID = identity.did;
|
|
771
|
+
const profileArtifactJws = identity.profileArtifactJws;
|
|
772
|
+
const ingestWithGossip = async (tokens) => {
|
|
773
|
+
const results = await ingestOperations(tokens, store, { logEnabled });
|
|
774
|
+
if (gossipPeers.length > 0 && peerClient) {
|
|
775
|
+
const newOps = [];
|
|
776
|
+
for (let i = 0; i < results.length; i++) {
|
|
777
|
+
if (results[i].status === "new") newOps.push(tokens[i]);
|
|
778
|
+
}
|
|
779
|
+
if (newOps.length > 0) {
|
|
780
|
+
for (const peer of gossipPeers) {
|
|
781
|
+
peerClient.submitOperations(peer.url, newOps).catch(() => {
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return results;
|
|
787
|
+
};
|
|
465
788
|
const app = new Hono();
|
|
466
789
|
app.get("/.well-known/dfos-relay", (c) => {
|
|
467
790
|
return c.json({
|
|
468
791
|
did: relayDID,
|
|
469
792
|
protocol: "dfos-web-relay",
|
|
470
|
-
version: "0.
|
|
793
|
+
version: "0.6.0",
|
|
794
|
+
proof: true,
|
|
795
|
+
content: contentEnabled,
|
|
796
|
+
log: logEnabled,
|
|
797
|
+
profile: profileArtifactJws
|
|
471
798
|
});
|
|
472
799
|
});
|
|
473
800
|
app.post("/operations", async (c) => {
|
|
@@ -481,7 +808,7 @@ var createRelay = (options) => {
|
|
|
481
808
|
if (!parsed.success) {
|
|
482
809
|
return c.json({ error: "invalid request", details: parsed.error.issues }, 400);
|
|
483
810
|
}
|
|
484
|
-
const results = await
|
|
811
|
+
const results = await ingestWithGossip(parsed.data.operations);
|
|
485
812
|
return c.json({ results });
|
|
486
813
|
});
|
|
487
814
|
app.get("/operations/:cid", async (c) => {
|
|
@@ -495,24 +822,96 @@ var createRelay = (options) => {
|
|
|
495
822
|
chainId: op.chainId
|
|
496
823
|
});
|
|
497
824
|
});
|
|
498
|
-
app.get("/identities/:did
|
|
825
|
+
app.get("/identities/:did/log", async (c) => {
|
|
499
826
|
const did = c.req.param("did");
|
|
500
827
|
const chain = await store.getIdentityChain(did);
|
|
501
828
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
829
|
+
const after = c.req.query("after");
|
|
830
|
+
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
831
|
+
const entries = chain.log.map((jws) => {
|
|
832
|
+
const decoded = decodeJwsUnsafe3(jws);
|
|
833
|
+
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
834
|
+
});
|
|
835
|
+
let startIdx = 0;
|
|
836
|
+
if (after) {
|
|
837
|
+
const idx = entries.findIndex((e) => e.cid === after);
|
|
838
|
+
startIdx = idx >= 0 ? idx + 1 : entries.length;
|
|
839
|
+
}
|
|
840
|
+
const page = entries.slice(startIdx, startIdx + limit);
|
|
841
|
+
const cursor = page.length === limit ? page[page.length - 1].cid : null;
|
|
842
|
+
return c.json({ entries: page, cursor });
|
|
843
|
+
});
|
|
844
|
+
app.get("/identities/:did{.+}", async (c) => {
|
|
845
|
+
const did = c.req.param("did");
|
|
846
|
+
let chain = await store.getIdentityChain(did);
|
|
847
|
+
if (!chain && readThroughPeers.length > 0 && peerClient) {
|
|
848
|
+
for (const peer of readThroughPeers) {
|
|
849
|
+
let after;
|
|
850
|
+
while (true) {
|
|
851
|
+
const logPage = await peerClient.getIdentityLog(peer.url, did, {
|
|
852
|
+
...after ? { after } : {},
|
|
853
|
+
limit: 1e3
|
|
854
|
+
});
|
|
855
|
+
if (!logPage || logPage.entries.length === 0) break;
|
|
856
|
+
await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
|
|
857
|
+
if (!logPage.cursor) break;
|
|
858
|
+
after = logPage.cursor;
|
|
859
|
+
}
|
|
860
|
+
chain = await store.getIdentityChain(did);
|
|
861
|
+
if (chain) break;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
if (!chain) return c.json({ error: "not found" }, 404);
|
|
502
865
|
return c.json({
|
|
503
866
|
did: chain.did,
|
|
504
|
-
|
|
867
|
+
headCID: chain.headCID,
|
|
505
868
|
state: chain.state
|
|
506
869
|
});
|
|
507
870
|
});
|
|
508
|
-
app.get("/content/:contentId", async (c) => {
|
|
871
|
+
app.get("/content/:contentId/log", async (c) => {
|
|
509
872
|
const contentId = c.req.param("contentId");
|
|
510
873
|
const chain = await store.getContentChain(contentId);
|
|
511
874
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
875
|
+
const after = c.req.query("after");
|
|
876
|
+
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
877
|
+
const entries = chain.log.map((jws) => {
|
|
878
|
+
const decoded = decodeJwsUnsafe3(jws);
|
|
879
|
+
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
880
|
+
});
|
|
881
|
+
let startIdx = 0;
|
|
882
|
+
if (after) {
|
|
883
|
+
const idx = entries.findIndex((e) => e.cid === after);
|
|
884
|
+
startIdx = idx >= 0 ? idx + 1 : entries.length;
|
|
885
|
+
}
|
|
886
|
+
const page = entries.slice(startIdx, startIdx + limit);
|
|
887
|
+
const cursor = page.length === limit ? page[page.length - 1].cid : null;
|
|
888
|
+
return c.json({ entries: page, cursor });
|
|
889
|
+
});
|
|
890
|
+
app.get("/content/:contentId", async (c) => {
|
|
891
|
+
const contentId = c.req.param("contentId");
|
|
892
|
+
let chain = await store.getContentChain(contentId);
|
|
893
|
+
if (!chain && readThroughPeers.length > 0 && peerClient) {
|
|
894
|
+
for (const peer of readThroughPeers) {
|
|
895
|
+
let after;
|
|
896
|
+
while (true) {
|
|
897
|
+
const logPage = await peerClient.getContentLog(peer.url, contentId, {
|
|
898
|
+
...after ? { after } : {},
|
|
899
|
+
limit: 1e3
|
|
900
|
+
});
|
|
901
|
+
if (!logPage || logPage.entries.length === 0) break;
|
|
902
|
+
await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
|
|
903
|
+
if (!logPage.cursor) break;
|
|
904
|
+
after = logPage.cursor;
|
|
905
|
+
}
|
|
906
|
+
chain = await store.getContentChain(contentId);
|
|
907
|
+
if (chain) break;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
if (!chain) return c.json({ error: "not found" }, 404);
|
|
512
911
|
return c.json({
|
|
513
912
|
contentId: chain.contentId,
|
|
514
913
|
genesisCID: chain.genesisCID,
|
|
515
|
-
|
|
914
|
+
headCID: chain.state.headCID,
|
|
516
915
|
state: chain.state
|
|
517
916
|
});
|
|
518
917
|
});
|
|
@@ -545,7 +944,15 @@ var createRelay = (options) => {
|
|
|
545
944
|
payload: beacon.state.payload
|
|
546
945
|
});
|
|
547
946
|
});
|
|
947
|
+
app.get("/log", async (c) => {
|
|
948
|
+
if (!logEnabled) return c.json({ error: "global log not available" }, 501);
|
|
949
|
+
const afterParam = c.req.query("after");
|
|
950
|
+
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
951
|
+
const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
|
|
952
|
+
return c.json(result);
|
|
953
|
+
});
|
|
548
954
|
app.put("/content/:contentId/blob/:operationCID", async (c) => {
|
|
955
|
+
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
549
956
|
const contentId = c.req.param("contentId");
|
|
550
957
|
const operationCID = c.req.param("operationCID");
|
|
551
958
|
const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
|
|
@@ -583,6 +990,7 @@ var createRelay = (options) => {
|
|
|
583
990
|
return c.json({ status: "stored", contentId, documentCID, operationCID });
|
|
584
991
|
});
|
|
585
992
|
app.get("/content/:contentId/blob", async (c) => {
|
|
993
|
+
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
586
994
|
return await readBlob({
|
|
587
995
|
contentId: c.req.param("contentId"),
|
|
588
996
|
ref: "head",
|
|
@@ -593,6 +1001,7 @@ var createRelay = (options) => {
|
|
|
593
1001
|
});
|
|
594
1002
|
});
|
|
595
1003
|
app.get("/content/:contentId/blob/:ref", async (c) => {
|
|
1004
|
+
if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
|
|
596
1005
|
return await readBlob({
|
|
597
1006
|
contentId: c.req.param("contentId"),
|
|
598
1007
|
ref: c.req.param("ref"),
|
|
@@ -602,7 +1011,24 @@ var createRelay = (options) => {
|
|
|
602
1011
|
store
|
|
603
1012
|
});
|
|
604
1013
|
});
|
|
605
|
-
|
|
1014
|
+
const syncFromPeers = async () => {
|
|
1015
|
+
if (!peerClient) return;
|
|
1016
|
+
for (const peer of syncPeers) {
|
|
1017
|
+
let cursor = await store.getPeerCursor(peer.url);
|
|
1018
|
+
while (true) {
|
|
1019
|
+
const page = await peerClient.getOperationLog(peer.url, {
|
|
1020
|
+
...cursor ? { after: cursor } : {},
|
|
1021
|
+
limit: 1e3
|
|
1022
|
+
});
|
|
1023
|
+
if (!page || page.entries.length === 0) break;
|
|
1024
|
+
await ingestWithGossip(page.entries.map((e) => e.jwsToken));
|
|
1025
|
+
cursor = page.cursor ?? page.entries[page.entries.length - 1].cid;
|
|
1026
|
+
await store.setPeerCursor(peer.url, cursor);
|
|
1027
|
+
if (!page.cursor) break;
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
};
|
|
1031
|
+
return { app, did: relayDID, syncFromPeers };
|
|
606
1032
|
};
|
|
607
1033
|
var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
608
1034
|
status,
|
|
@@ -685,6 +1111,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
685
1111
|
};
|
|
686
1112
|
|
|
687
1113
|
// src/store.ts
|
|
1114
|
+
import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
|
|
688
1115
|
import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
689
1116
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
690
1117
|
var MemoryRelayStore = class {
|
|
@@ -694,6 +1121,8 @@ var MemoryRelayStore = class {
|
|
|
694
1121
|
beacons = /* @__PURE__ */ new Map();
|
|
695
1122
|
blobs = /* @__PURE__ */ new Map();
|
|
696
1123
|
countersignatures = /* @__PURE__ */ new Map();
|
|
1124
|
+
operationLog = [];
|
|
1125
|
+
peerCursors = /* @__PURE__ */ new Map();
|
|
697
1126
|
async getOperation(cid) {
|
|
698
1127
|
return this.operations.get(cid);
|
|
699
1128
|
}
|
|
@@ -744,10 +1173,85 @@ var MemoryRelayStore = class {
|
|
|
744
1173
|
existing.push(jwsToken);
|
|
745
1174
|
this.countersignatures.set(operationCID, existing);
|
|
746
1175
|
}
|
|
1176
|
+
async appendToLog(entry) {
|
|
1177
|
+
this.operationLog.push(entry);
|
|
1178
|
+
}
|
|
1179
|
+
async readLog(params) {
|
|
1180
|
+
let startIdx = 0;
|
|
1181
|
+
if (params.after) {
|
|
1182
|
+
const idx = this.operationLog.findIndex((e) => e.cid === params.after);
|
|
1183
|
+
if (idx >= 0) startIdx = idx + 1;
|
|
1184
|
+
else startIdx = this.operationLog.length;
|
|
1185
|
+
}
|
|
1186
|
+
const entries = this.operationLog.slice(startIdx, startIdx + params.limit);
|
|
1187
|
+
const cursor = entries.length === params.limit ? entries[entries.length - 1].cid : null;
|
|
1188
|
+
return { entries, cursor };
|
|
1189
|
+
}
|
|
1190
|
+
async getIdentityStateAtCID(did, cid) {
|
|
1191
|
+
const chain = this.identityChains.get(did);
|
|
1192
|
+
if (!chain) return null;
|
|
1193
|
+
const opsByCID = /* @__PURE__ */ new Map();
|
|
1194
|
+
for (const jws of chain.log) {
|
|
1195
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
1196
|
+
if (!decoded) continue;
|
|
1197
|
+
const payload = decoded.payload;
|
|
1198
|
+
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
1199
|
+
const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
1200
|
+
opsByCID.set(opCID, { jws, previousCID: prevCID });
|
|
1201
|
+
}
|
|
1202
|
+
if (!opsByCID.has(cid)) return null;
|
|
1203
|
+
const path = [];
|
|
1204
|
+
let currentCID = cid;
|
|
1205
|
+
while (currentCID) {
|
|
1206
|
+
const op = opsByCID.get(currentCID);
|
|
1207
|
+
if (!op) return null;
|
|
1208
|
+
path.unshift(op.jws);
|
|
1209
|
+
currentCID = op.previousCID;
|
|
1210
|
+
}
|
|
1211
|
+
const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
|
|
1212
|
+
const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
|
|
1213
|
+
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1214
|
+
return { state: identity, lastCreatedAt };
|
|
1215
|
+
}
|
|
1216
|
+
async getContentStateAtCID(contentId, cid) {
|
|
1217
|
+
const chain = this.contentChains.get(contentId);
|
|
1218
|
+
if (!chain) return null;
|
|
1219
|
+
const opsByCID = /* @__PURE__ */ new Map();
|
|
1220
|
+
for (const jws of chain.log) {
|
|
1221
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
1222
|
+
if (!decoded) continue;
|
|
1223
|
+
const payload = decoded.payload;
|
|
1224
|
+
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
1225
|
+
const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
1226
|
+
opsByCID.set(opCID, { jws, previousCID: prevCID });
|
|
1227
|
+
}
|
|
1228
|
+
if (!opsByCID.has(cid)) return null;
|
|
1229
|
+
const path = [];
|
|
1230
|
+
let currentCID = cid;
|
|
1231
|
+
while (currentCID) {
|
|
1232
|
+
const op = opsByCID.get(currentCID);
|
|
1233
|
+
if (!op) return null;
|
|
1234
|
+
path.unshift(op.jws);
|
|
1235
|
+
currentCID = op.previousCID;
|
|
1236
|
+
}
|
|
1237
|
+
const resolveKey = createKeyResolver(this);
|
|
1238
|
+
const content = await verifyContentChain2({ log: path, resolveKey, enforceAuthorization: true });
|
|
1239
|
+
const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
|
|
1240
|
+
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1241
|
+
return { state: content, lastCreatedAt };
|
|
1242
|
+
}
|
|
1243
|
+
async getPeerCursor(peerUrl) {
|
|
1244
|
+
return this.peerCursors.get(peerUrl);
|
|
1245
|
+
}
|
|
1246
|
+
async setPeerCursor(peerUrl, cursor) {
|
|
1247
|
+
this.peerCursors.set(peerUrl, cursor);
|
|
1248
|
+
}
|
|
747
1249
|
};
|
|
748
1250
|
export {
|
|
749
1251
|
MemoryRelayStore,
|
|
1252
|
+
bootstrapRelayIdentity,
|
|
750
1253
|
createCurrentKeyResolver,
|
|
1254
|
+
createHttpPeerClient,
|
|
751
1255
|
createKeyResolver,
|
|
752
1256
|
createRelay,
|
|
753
1257
|
ingestOperations
|