@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/dist/index.js
CHANGED
|
@@ -22,6 +22,12 @@ import {
|
|
|
22
22
|
verifyIdentityExtensionFromTrustedState
|
|
23
23
|
} from "@metalabel/dfos-protocol/chain";
|
|
24
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
|
+
};
|
|
25
31
|
var classify = (jwsToken) => {
|
|
26
32
|
const unknown = {
|
|
27
33
|
jwsToken,
|
|
@@ -136,12 +142,16 @@ var createCurrentKeyResolver = (store) => async (kid) => {
|
|
|
136
142
|
if (currentKey) return decodeMultikey(currentKey.publicKeyMultibase).keyBytes;
|
|
137
143
|
throw new Error(`unknown key ${keyId} on identity ${did}`);
|
|
138
144
|
};
|
|
139
|
-
var ingestIdentityOp = async (jwsToken, store) => {
|
|
145
|
+
var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
|
|
140
146
|
const decoded = decodeJwsUnsafe(jwsToken);
|
|
141
147
|
if (!decoded) return { cid: "", status: "rejected", error: "failed to decode JWS" };
|
|
142
148
|
const payload = decoded.payload;
|
|
143
149
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
144
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
|
+
}
|
|
145
155
|
const existing = await store.getOperation(cid);
|
|
146
156
|
if (existing) {
|
|
147
157
|
if (existing.jwsToken !== jwsToken) {
|
|
@@ -151,7 +161,7 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
151
161
|
error: "operation already exists with a different signature"
|
|
152
162
|
};
|
|
153
163
|
}
|
|
154
|
-
return { cid, status: "
|
|
164
|
+
return { cid, status: "duplicate", kind: "identity-op", chainId: existing.chainId };
|
|
155
165
|
}
|
|
156
166
|
const opType = payload["type"];
|
|
157
167
|
const isGenesis = opType === "create";
|
|
@@ -167,8 +177,10 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
167
177
|
};
|
|
168
178
|
await store.putIdentityChain(chain2);
|
|
169
179
|
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId: identity.did });
|
|
170
|
-
|
|
171
|
-
|
|
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 };
|
|
172
184
|
}
|
|
173
185
|
const kid = decoded.header.kid;
|
|
174
186
|
const hashIdx = kid.indexOf("#");
|
|
@@ -176,30 +188,78 @@ var ingestIdentityOp = async (jwsToken, store) => {
|
|
|
176
188
|
const did = kid.substring(0, hashIdx);
|
|
177
189
|
const chain = await store.getIdentityChain(did);
|
|
178
190
|
if (!chain) return { cid, status: "rejected", error: `unknown identity: ${did}` };
|
|
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
|
+
}
|
|
179
220
|
const extResult = await verifyIdentityExtensionFromTrustedState({
|
|
180
|
-
currentState:
|
|
181
|
-
headCID:
|
|
182
|
-
lastCreatedAt:
|
|
221
|
+
currentState: forkState.state,
|
|
222
|
+
headCID: previousCID,
|
|
223
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
183
224
|
newOp: jwsToken
|
|
184
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
|
+
}
|
|
185
239
|
const updated = {
|
|
186
240
|
did: chain.did,
|
|
187
|
-
log:
|
|
188
|
-
headCID
|
|
189
|
-
lastCreatedAt:
|
|
190
|
-
state:
|
|
241
|
+
log: updatedLog,
|
|
242
|
+
headCID,
|
|
243
|
+
lastCreatedAt: headLastCreatedAt,
|
|
244
|
+
state: headState
|
|
191
245
|
};
|
|
192
246
|
await store.putIdentityChain(updated);
|
|
193
247
|
await store.putOperation({ cid, jwsToken, chainType: "identity", chainId: did });
|
|
194
|
-
|
|
195
|
-
|
|
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 };
|
|
196
252
|
};
|
|
197
|
-
var ingestContentOp = async (jwsToken, store) => {
|
|
253
|
+
var ingestContentOp = async (jwsToken, store, logEnabled) => {
|
|
198
254
|
const decoded = decodeJwsUnsafe(jwsToken);
|
|
199
255
|
if (!decoded) return { cid: "", status: "rejected", error: "failed to decode JWS" };
|
|
200
256
|
const payload = decoded.payload;
|
|
201
257
|
const encoded = await dagCborCanonicalEncode(payload);
|
|
202
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
|
+
}
|
|
203
263
|
const existing = await store.getOperation(cid);
|
|
204
264
|
if (existing) {
|
|
205
265
|
if (existing.jwsToken !== jwsToken) {
|
|
@@ -209,7 +269,7 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
209
269
|
error: "operation already exists with a different signature"
|
|
210
270
|
};
|
|
211
271
|
}
|
|
212
|
-
return { cid, status: "
|
|
272
|
+
return { cid, status: "duplicate", kind: "content-op", chainId: existing.chainId };
|
|
213
273
|
}
|
|
214
274
|
const signerDID = payload["did"];
|
|
215
275
|
if (typeof signerDID === "string") {
|
|
@@ -237,8 +297,10 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
237
297
|
};
|
|
238
298
|
await store.putContentChain(chain2);
|
|
239
299
|
await store.putOperation({ cid, jwsToken, chainType: "content", chainId: content.contentId });
|
|
240
|
-
|
|
241
|
-
|
|
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 };
|
|
242
304
|
}
|
|
243
305
|
const previousCID = payload["previousOperationCID"];
|
|
244
306
|
if (typeof previousCID !== "string") {
|
|
@@ -257,29 +319,65 @@ var ingestContentOp = async (jwsToken, store) => {
|
|
|
257
319
|
if (creatorIdentity?.state.isDeleted) {
|
|
258
320
|
return { cid, status: "rejected", error: "content creator identity is deleted" };
|
|
259
321
|
}
|
|
260
|
-
if (chain.state.headCID
|
|
261
|
-
|
|
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" };
|
|
262
350
|
}
|
|
263
351
|
const extResult = await verifyContentExtensionFromTrustedState({
|
|
264
|
-
currentState:
|
|
265
|
-
lastCreatedAt:
|
|
352
|
+
currentState: forkState.state,
|
|
353
|
+
lastCreatedAt: forkState.lastCreatedAt,
|
|
266
354
|
newOp: jwsToken,
|
|
267
355
|
resolveKey,
|
|
268
356
|
enforceAuthorization: true
|
|
269
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
|
+
}
|
|
270
366
|
const updated = {
|
|
271
367
|
contentId: chain.contentId,
|
|
272
368
|
genesisCID: chain.genesisCID,
|
|
273
|
-
log:
|
|
274
|
-
lastCreatedAt:
|
|
275
|
-
state:
|
|
369
|
+
log: updatedLog,
|
|
370
|
+
lastCreatedAt: headLastCreatedAt,
|
|
371
|
+
state: headState
|
|
276
372
|
};
|
|
277
373
|
await store.putContentChain(updated);
|
|
278
374
|
await store.putOperation({ cid, jwsToken, chainType: "content", chainId: chain.contentId });
|
|
279
|
-
|
|
280
|
-
|
|
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 };
|
|
281
379
|
};
|
|
282
|
-
var ingestBeacon = async (jwsToken, store) => {
|
|
380
|
+
var ingestBeacon = async (jwsToken, store, logEnabled) => {
|
|
283
381
|
const resolveKey = createKeyResolver(store);
|
|
284
382
|
let verified;
|
|
285
383
|
try {
|
|
@@ -299,15 +397,17 @@ var ingestBeacon = async (jwsToken, store) => {
|
|
|
299
397
|
const existingTime = new Date(existing.state.payload.createdAt).getTime();
|
|
300
398
|
const newTime = new Date(verified.payload.createdAt).getTime();
|
|
301
399
|
if (newTime <= existingTime) {
|
|
302
|
-
return { cid, status: "
|
|
400
|
+
return { cid, status: "duplicate", kind: "beacon", chainId: did };
|
|
303
401
|
}
|
|
304
402
|
}
|
|
305
403
|
await store.putBeacon({ did, jwsToken, beaconCID: cid, state: verified });
|
|
306
404
|
await store.putOperation({ cid, jwsToken, chainType: "beacon", chainId: did });
|
|
307
|
-
|
|
308
|
-
|
|
405
|
+
if (logEnabled) {
|
|
406
|
+
await store.appendToLog({ cid, jwsToken, kind: "beacon", chainId: did });
|
|
407
|
+
}
|
|
408
|
+
return { cid, status: "new", kind: "beacon", chainId: did };
|
|
309
409
|
};
|
|
310
|
-
var ingestCountersign = async (jwsToken, store) => {
|
|
410
|
+
var ingestCountersign = async (jwsToken, store, logEnabled) => {
|
|
311
411
|
const resolveKey = createKeyResolver(store);
|
|
312
412
|
let verified;
|
|
313
413
|
try {
|
|
@@ -327,7 +427,7 @@ var ingestCountersign = async (jwsToken, store) => {
|
|
|
327
427
|
error: "countersign already exists with a different signature"
|
|
328
428
|
};
|
|
329
429
|
}
|
|
330
|
-
return { cid, status: "
|
|
430
|
+
return { cid, status: "duplicate", kind: "countersign", chainId: targetCID };
|
|
331
431
|
}
|
|
332
432
|
const targetOp = await store.getOperation(targetCID);
|
|
333
433
|
if (!targetOp) {
|
|
@@ -356,15 +456,17 @@ var ingestCountersign = async (jwsToken, store) => {
|
|
|
356
456
|
if (!csDecoded) continue;
|
|
357
457
|
const csPayload = csDecoded.payload;
|
|
358
458
|
if (csPayload["did"] === witnessDID) {
|
|
359
|
-
return { cid, status: "
|
|
459
|
+
return { cid, status: "duplicate", kind: "countersign", chainId: targetCID };
|
|
360
460
|
}
|
|
361
461
|
}
|
|
362
462
|
await store.putOperation({ cid, jwsToken, chainType: "countersign", chainId: targetCID });
|
|
363
463
|
await store.addCountersignature(targetCID, jwsToken);
|
|
364
|
-
|
|
365
|
-
|
|
464
|
+
if (logEnabled) {
|
|
465
|
+
await store.appendToLog({ cid, jwsToken, kind: "countersign", chainId: targetCID });
|
|
466
|
+
}
|
|
467
|
+
return { cid, status: "new", kind: "countersign", chainId: targetCID };
|
|
366
468
|
};
|
|
367
|
-
var ingestArtifact = async (jwsToken, store) => {
|
|
469
|
+
var ingestArtifact = async (jwsToken, store, logEnabled) => {
|
|
368
470
|
const resolveKey = createKeyResolver(store);
|
|
369
471
|
let verified;
|
|
370
472
|
try {
|
|
@@ -384,15 +486,17 @@ var ingestArtifact = async (jwsToken, store) => {
|
|
|
384
486
|
error: "artifact already exists with a different signature"
|
|
385
487
|
};
|
|
386
488
|
}
|
|
387
|
-
return { cid, status: "
|
|
489
|
+
return { cid, status: "duplicate", kind: "artifact", chainId: did };
|
|
388
490
|
}
|
|
389
491
|
const identity = await store.getIdentityChain(did);
|
|
390
492
|
if (identity?.state.isDeleted) {
|
|
391
493
|
return { cid, status: "rejected", error: "identity is deleted" };
|
|
392
494
|
}
|
|
393
495
|
await store.putOperation({ cid, jwsToken, chainType: "artifact", chainId: did });
|
|
394
|
-
|
|
395
|
-
|
|
496
|
+
if (logEnabled) {
|
|
497
|
+
await store.appendToLog({ cid, jwsToken, kind: "artifact", chainId: did });
|
|
498
|
+
}
|
|
499
|
+
return { cid, status: "new", kind: "artifact", chainId: did };
|
|
396
500
|
};
|
|
397
501
|
var dependencySort = (ops) => {
|
|
398
502
|
const buckets = /* @__PURE__ */ new Map();
|
|
@@ -451,7 +555,36 @@ var topologicalSortBucket = (ops) => {
|
|
|
451
555
|
}
|
|
452
556
|
return sorted;
|
|
453
557
|
};
|
|
454
|
-
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;
|
|
455
588
|
const classified = tokens.map((token, i) => ({ ...classify(token), originalIndex: i }));
|
|
456
589
|
const sorted = dependencySort(classified);
|
|
457
590
|
const indexedResults = [];
|
|
@@ -460,19 +593,19 @@ var ingestOperations = async (tokens, store) => {
|
|
|
460
593
|
let result;
|
|
461
594
|
switch (op.kind) {
|
|
462
595
|
case "identity-op":
|
|
463
|
-
result = await ingestIdentityOp(op.jwsToken, store);
|
|
596
|
+
result = await ingestIdentityOp(op.jwsToken, store, logEnabled);
|
|
464
597
|
break;
|
|
465
598
|
case "content-op":
|
|
466
|
-
result = await ingestContentOp(op.jwsToken, store);
|
|
599
|
+
result = await ingestContentOp(op.jwsToken, store, logEnabled);
|
|
467
600
|
break;
|
|
468
601
|
case "beacon":
|
|
469
|
-
result = await ingestBeacon(op.jwsToken, store);
|
|
602
|
+
result = await ingestBeacon(op.jwsToken, store, logEnabled);
|
|
470
603
|
break;
|
|
471
604
|
case "countersign":
|
|
472
|
-
result = await ingestCountersign(op.jwsToken, store);
|
|
605
|
+
result = await ingestCountersign(op.jwsToken, store, logEnabled);
|
|
473
606
|
break;
|
|
474
607
|
case "artifact":
|
|
475
|
-
result = await ingestArtifact(op.jwsToken, store);
|
|
608
|
+
result = await ingestArtifact(op.jwsToken, store, logEnabled);
|
|
476
609
|
break;
|
|
477
610
|
default:
|
|
478
611
|
result = { cid: "", status: "rejected", error: "unrecognized operation type" };
|
|
@@ -510,7 +643,7 @@ var bootstrapRelayIdentity = async (store) => {
|
|
|
510
643
|
keyId
|
|
511
644
|
});
|
|
512
645
|
const [identityResult] = await ingestOperations([identityJws], store);
|
|
513
|
-
if (!identityResult || identityResult.status
|
|
646
|
+
if (!identityResult || identityResult.status === "rejected" || !identityResult.chainId) {
|
|
514
647
|
throw new Error(`failed to bootstrap relay identity: ${identityResult?.error ?? "unknown"}`);
|
|
515
648
|
}
|
|
516
649
|
const did = identityResult.chainId;
|
|
@@ -531,7 +664,7 @@ var bootstrapRelayIdentity = async (store) => {
|
|
|
531
664
|
kid
|
|
532
665
|
});
|
|
533
666
|
const [artifactResult] = await ingestOperations([profileArtifactJws], store);
|
|
534
|
-
if (!artifactResult || artifactResult.status
|
|
667
|
+
if (!artifactResult || artifactResult.status === "rejected") {
|
|
535
668
|
throw new Error(
|
|
536
669
|
`failed to ingest relay profile artifact: ${artifactResult?.error ?? "unknown"}`
|
|
537
670
|
);
|
|
@@ -539,6 +672,55 @@ var bootstrapRelayIdentity = async (store) => {
|
|
|
539
672
|
return { did, profileArtifactJws };
|
|
540
673
|
};
|
|
541
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
|
+
|
|
542
724
|
// src/relay.ts
|
|
543
725
|
import { VC_TYPE_CONTENT_READ, verifyCredential } from "@metalabel/dfos-protocol/credentials";
|
|
544
726
|
import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
|
|
@@ -578,17 +760,40 @@ var IngestBody = z.object({
|
|
|
578
760
|
var createRelay = async (options) => {
|
|
579
761
|
const { store } = options;
|
|
580
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);
|
|
581
769
|
const identity = options.identity ?? await bootstrapRelayIdentity(store);
|
|
582
770
|
const relayDID = identity.did;
|
|
583
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
|
+
};
|
|
584
788
|
const app = new Hono();
|
|
585
789
|
app.get("/.well-known/dfos-relay", (c) => {
|
|
586
790
|
return c.json({
|
|
587
791
|
did: relayDID,
|
|
588
792
|
protocol: "dfos-web-relay",
|
|
589
|
-
version: "0.
|
|
793
|
+
version: "0.6.0",
|
|
590
794
|
proof: true,
|
|
591
795
|
content: contentEnabled,
|
|
796
|
+
log: logEnabled,
|
|
592
797
|
profile: profileArtifactJws
|
|
593
798
|
});
|
|
594
799
|
});
|
|
@@ -603,7 +808,7 @@ var createRelay = async (options) => {
|
|
|
603
808
|
if (!parsed.success) {
|
|
604
809
|
return c.json({ error: "invalid request", details: parsed.error.issues }, 400);
|
|
605
810
|
}
|
|
606
|
-
const results = await
|
|
811
|
+
const results = await ingestWithGossip(parsed.data.operations);
|
|
607
812
|
return c.json({ results });
|
|
608
813
|
});
|
|
609
814
|
app.get("/operations/:cid", async (c) => {
|
|
@@ -638,7 +843,24 @@ var createRelay = async (options) => {
|
|
|
638
843
|
});
|
|
639
844
|
app.get("/identities/:did{.+}", async (c) => {
|
|
640
845
|
const did = c.req.param("did");
|
|
641
|
-
|
|
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
|
+
}
|
|
642
864
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
643
865
|
return c.json({
|
|
644
866
|
did: chain.did,
|
|
@@ -667,7 +889,24 @@ var createRelay = async (options) => {
|
|
|
667
889
|
});
|
|
668
890
|
app.get("/content/:contentId", async (c) => {
|
|
669
891
|
const contentId = c.req.param("contentId");
|
|
670
|
-
|
|
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
|
+
}
|
|
671
910
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
672
911
|
return c.json({
|
|
673
912
|
contentId: chain.contentId,
|
|
@@ -706,6 +945,7 @@ var createRelay = async (options) => {
|
|
|
706
945
|
});
|
|
707
946
|
});
|
|
708
947
|
app.get("/log", async (c) => {
|
|
948
|
+
if (!logEnabled) return c.json({ error: "global log not available" }, 501);
|
|
709
949
|
const afterParam = c.req.query("after");
|
|
710
950
|
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
711
951
|
const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
|
|
@@ -771,7 +1011,24 @@ var createRelay = async (options) => {
|
|
|
771
1011
|
store
|
|
772
1012
|
});
|
|
773
1013
|
});
|
|
774
|
-
|
|
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 };
|
|
775
1032
|
};
|
|
776
1033
|
var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
777
1034
|
status,
|
|
@@ -854,6 +1111,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
854
1111
|
};
|
|
855
1112
|
|
|
856
1113
|
// src/store.ts
|
|
1114
|
+
import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
|
|
857
1115
|
import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
858
1116
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
859
1117
|
var MemoryRelayStore = class {
|
|
@@ -864,6 +1122,7 @@ var MemoryRelayStore = class {
|
|
|
864
1122
|
blobs = /* @__PURE__ */ new Map();
|
|
865
1123
|
countersignatures = /* @__PURE__ */ new Map();
|
|
866
1124
|
operationLog = [];
|
|
1125
|
+
peerCursors = /* @__PURE__ */ new Map();
|
|
867
1126
|
async getOperation(cid) {
|
|
868
1127
|
return this.operations.get(cid);
|
|
869
1128
|
}
|
|
@@ -928,11 +1187,71 @@ var MemoryRelayStore = class {
|
|
|
928
1187
|
const cursor = entries.length === params.limit ? entries[entries.length - 1].cid : null;
|
|
929
1188
|
return { entries, cursor };
|
|
930
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
|
+
}
|
|
931
1249
|
};
|
|
932
1250
|
export {
|
|
933
1251
|
MemoryRelayStore,
|
|
934
1252
|
bootstrapRelayIdentity,
|
|
935
1253
|
createCurrentKeyResolver,
|
|
1254
|
+
createHttpPeerClient,
|
|
936
1255
|
createKeyResolver,
|
|
937
1256
|
createRelay,
|
|
938
1257
|
ingestOperations
|