@metalabel/dfos-web-relay 0.5.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -7
- package/dist/index.d.ts +150 -4
- package/dist/index.js +490 -63
- package/openapi.yaml +14 -2
- package/package.json +3 -4
- package/RELAY.md +0 -457
package/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,9 +672,58 @@ 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
|
-
import { dagCborCanonicalEncode as
|
|
726
|
+
import { dagCborCanonicalEncode as dagCborCanonicalEncode3, decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
|
|
545
727
|
import { Hono } from "hono";
|
|
546
728
|
import { z } from "zod";
|
|
547
729
|
|
|
@@ -571,6 +753,59 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
|
|
|
571
753
|
}
|
|
572
754
|
};
|
|
573
755
|
|
|
756
|
+
// src/sequencer.ts
|
|
757
|
+
import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
|
|
758
|
+
var isDependencyFailure = (error) => {
|
|
759
|
+
const patterns = [
|
|
760
|
+
"unknown previous operation",
|
|
761
|
+
"unknown identity:",
|
|
762
|
+
"content chain not found:",
|
|
763
|
+
"failed to compute state at fork point:"
|
|
764
|
+
];
|
|
765
|
+
return patterns.some((p) => error.includes(p));
|
|
766
|
+
};
|
|
767
|
+
var computeOpCID = async (jwsToken) => {
|
|
768
|
+
const decoded = decodeJwsUnsafe3(jwsToken);
|
|
769
|
+
if (!decoded) return void 0;
|
|
770
|
+
const encoded = await dagCborCanonicalEncode2(decoded.payload);
|
|
771
|
+
return encoded.cid.toString();
|
|
772
|
+
};
|
|
773
|
+
var sequenceOps = async (store) => {
|
|
774
|
+
const newOps = [];
|
|
775
|
+
const result = { sequenced: 0, rejected: 0, pending: 0 };
|
|
776
|
+
for (; ; ) {
|
|
777
|
+
const tokens = await store.getUnsequencedOps(1e4);
|
|
778
|
+
if (tokens.length === 0) break;
|
|
779
|
+
const results = await ingestOperations(tokens, store);
|
|
780
|
+
let progress = false;
|
|
781
|
+
const sequencedCIDs = [];
|
|
782
|
+
for (let i = 0; i < results.length; i++) {
|
|
783
|
+
const res = results[i];
|
|
784
|
+
if (!res.cid) continue;
|
|
785
|
+
if (res.status === "new") {
|
|
786
|
+
sequencedCIDs.push(res.cid);
|
|
787
|
+
newOps.push(tokens[i]);
|
|
788
|
+
result.sequenced++;
|
|
789
|
+
progress = true;
|
|
790
|
+
} else if (res.status === "duplicate") {
|
|
791
|
+
sequencedCIDs.push(res.cid);
|
|
792
|
+
progress = true;
|
|
793
|
+
} else if (res.status === "rejected" && !isDependencyFailure(res.error ?? "")) {
|
|
794
|
+
await store.markOpRejected(res.cid, res.error ?? "unknown");
|
|
795
|
+
result.rejected++;
|
|
796
|
+
progress = true;
|
|
797
|
+
} else {
|
|
798
|
+
result.pending++;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (sequencedCIDs.length > 0) {
|
|
802
|
+
await store.markOpsSequenced(sequencedCIDs);
|
|
803
|
+
}
|
|
804
|
+
if (!progress) break;
|
|
805
|
+
}
|
|
806
|
+
return { newOps, result };
|
|
807
|
+
};
|
|
808
|
+
|
|
574
809
|
// src/relay.ts
|
|
575
810
|
var IngestBody = z.object({
|
|
576
811
|
operations: z.array(z.string()).min(1).max(100)
|
|
@@ -578,17 +813,53 @@ var IngestBody = z.object({
|
|
|
578
813
|
var createRelay = async (options) => {
|
|
579
814
|
const { store } = options;
|
|
580
815
|
const contentEnabled = options.content !== false;
|
|
816
|
+
const logEnabled = options.log !== false;
|
|
817
|
+
const peers = options.peers ?? [];
|
|
818
|
+
const peerClient = options.peerClient;
|
|
819
|
+
const gossipPeers = peers.filter((p) => p.gossip !== false);
|
|
820
|
+
const readThroughPeers = peers.filter((p) => p.readThrough !== false);
|
|
821
|
+
const syncPeers = peers.filter((p) => p.sync !== false);
|
|
581
822
|
const identity = options.identity ?? await bootstrapRelayIdentity(store);
|
|
582
823
|
const relayDID = identity.did;
|
|
583
824
|
const profileArtifactJws = identity.profileArtifactJws;
|
|
825
|
+
const gossip = (ops) => {
|
|
826
|
+
if (ops.length === 0 || gossipPeers.length === 0 || !peerClient) return;
|
|
827
|
+
for (const peer of gossipPeers) {
|
|
828
|
+
peerClient.submitOperations(peer.url, ops).catch(() => {
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
};
|
|
832
|
+
const ingestWithGossip = async (tokens) => {
|
|
833
|
+
for (const token of tokens) {
|
|
834
|
+
const cid = await computeOpCID(token);
|
|
835
|
+
if (cid) await store.putRawOp(cid, token);
|
|
836
|
+
}
|
|
837
|
+
const results = await ingestOperations(tokens, store, { logEnabled });
|
|
838
|
+
const newOps = [];
|
|
839
|
+
for (let i = 0; i < results.length; i++) {
|
|
840
|
+
const res = results[i];
|
|
841
|
+
if (!res.cid) continue;
|
|
842
|
+
if (res.status === "new") {
|
|
843
|
+
await store.markOpsSequenced([res.cid]);
|
|
844
|
+
newOps.push(tokens[i]);
|
|
845
|
+
} else if (res.status === "duplicate") {
|
|
846
|
+
await store.markOpsSequenced([res.cid]);
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
const { newOps: seqNewOps } = await sequenceOps(store);
|
|
850
|
+
gossip(newOps);
|
|
851
|
+
gossip(seqNewOps);
|
|
852
|
+
return results;
|
|
853
|
+
};
|
|
584
854
|
const app = new Hono();
|
|
585
855
|
app.get("/.well-known/dfos-relay", (c) => {
|
|
586
856
|
return c.json({
|
|
587
857
|
did: relayDID,
|
|
588
858
|
protocol: "dfos-web-relay",
|
|
589
|
-
version: "0.
|
|
859
|
+
version: "0.6.0",
|
|
590
860
|
proof: true,
|
|
591
861
|
content: contentEnabled,
|
|
862
|
+
log: logEnabled,
|
|
592
863
|
profile: profileArtifactJws
|
|
593
864
|
});
|
|
594
865
|
});
|
|
@@ -603,7 +874,7 @@ var createRelay = async (options) => {
|
|
|
603
874
|
if (!parsed.success) {
|
|
604
875
|
return c.json({ error: "invalid request", details: parsed.error.issues }, 400);
|
|
605
876
|
}
|
|
606
|
-
const results = await
|
|
877
|
+
const results = await ingestWithGossip(parsed.data.operations);
|
|
607
878
|
return c.json({ results });
|
|
608
879
|
});
|
|
609
880
|
app.get("/operations/:cid", async (c) => {
|
|
@@ -624,7 +895,7 @@ var createRelay = async (options) => {
|
|
|
624
895
|
const after = c.req.query("after");
|
|
625
896
|
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
626
897
|
const entries = chain.log.map((jws) => {
|
|
627
|
-
const decoded =
|
|
898
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
628
899
|
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
629
900
|
});
|
|
630
901
|
let startIdx = 0;
|
|
@@ -638,7 +909,24 @@ var createRelay = async (options) => {
|
|
|
638
909
|
});
|
|
639
910
|
app.get("/identities/:did{.+}", async (c) => {
|
|
640
911
|
const did = c.req.param("did");
|
|
641
|
-
|
|
912
|
+
let chain = await store.getIdentityChain(did);
|
|
913
|
+
if (!chain && readThroughPeers.length > 0 && peerClient) {
|
|
914
|
+
for (const peer of readThroughPeers) {
|
|
915
|
+
let after;
|
|
916
|
+
while (true) {
|
|
917
|
+
const logPage = await peerClient.getIdentityLog(peer.url, did, {
|
|
918
|
+
...after ? { after } : {},
|
|
919
|
+
limit: 1e3
|
|
920
|
+
});
|
|
921
|
+
if (!logPage || logPage.entries.length === 0) break;
|
|
922
|
+
await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
|
|
923
|
+
if (!logPage.cursor) break;
|
|
924
|
+
after = logPage.cursor;
|
|
925
|
+
}
|
|
926
|
+
chain = await store.getIdentityChain(did);
|
|
927
|
+
if (chain) break;
|
|
928
|
+
}
|
|
929
|
+
}
|
|
642
930
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
643
931
|
return c.json({
|
|
644
932
|
did: chain.did,
|
|
@@ -653,7 +941,7 @@ var createRelay = async (options) => {
|
|
|
653
941
|
const after = c.req.query("after");
|
|
654
942
|
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
655
943
|
const entries = chain.log.map((jws) => {
|
|
656
|
-
const decoded =
|
|
944
|
+
const decoded = decodeJwsUnsafe4(jws);
|
|
657
945
|
return { cid: decoded?.header.cid || "", jwsToken: jws };
|
|
658
946
|
});
|
|
659
947
|
let startIdx = 0;
|
|
@@ -667,7 +955,24 @@ var createRelay = async (options) => {
|
|
|
667
955
|
});
|
|
668
956
|
app.get("/content/:contentId", async (c) => {
|
|
669
957
|
const contentId = c.req.param("contentId");
|
|
670
|
-
|
|
958
|
+
let chain = await store.getContentChain(contentId);
|
|
959
|
+
if (!chain && readThroughPeers.length > 0 && peerClient) {
|
|
960
|
+
for (const peer of readThroughPeers) {
|
|
961
|
+
let after;
|
|
962
|
+
while (true) {
|
|
963
|
+
const logPage = await peerClient.getContentLog(peer.url, contentId, {
|
|
964
|
+
...after ? { after } : {},
|
|
965
|
+
limit: 1e3
|
|
966
|
+
});
|
|
967
|
+
if (!logPage || logPage.entries.length === 0) break;
|
|
968
|
+
await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
|
|
969
|
+
if (!logPage.cursor) break;
|
|
970
|
+
after = logPage.cursor;
|
|
971
|
+
}
|
|
972
|
+
chain = await store.getContentChain(contentId);
|
|
973
|
+
if (chain) break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
671
976
|
if (!chain) return c.json({ error: "not found" }, 404);
|
|
672
977
|
return c.json({
|
|
673
978
|
contentId: chain.contentId,
|
|
@@ -706,6 +1011,7 @@ var createRelay = async (options) => {
|
|
|
706
1011
|
});
|
|
707
1012
|
});
|
|
708
1013
|
app.get("/log", async (c) => {
|
|
1014
|
+
if (!logEnabled) return c.json({ error: "global log not available" }, 501);
|
|
709
1015
|
const afterParam = c.req.query("after");
|
|
710
1016
|
const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
|
|
711
1017
|
const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
|
|
@@ -722,7 +1028,7 @@ var createRelay = async (options) => {
|
|
|
722
1028
|
let documentCID = null;
|
|
723
1029
|
let operationSignerDID = null;
|
|
724
1030
|
for (const token of chain.log) {
|
|
725
|
-
const decoded =
|
|
1031
|
+
const decoded = decodeJwsUnsafe4(token);
|
|
726
1032
|
if (!decoded) continue;
|
|
727
1033
|
if (decoded.header.cid !== operationCID) continue;
|
|
728
1034
|
const payload = decoded.payload;
|
|
@@ -739,7 +1045,7 @@ var createRelay = async (options) => {
|
|
|
739
1045
|
const bytes = new Uint8Array(await c.req.arrayBuffer());
|
|
740
1046
|
try {
|
|
741
1047
|
const parsed = JSON.parse(new TextDecoder().decode(bytes));
|
|
742
|
-
const encoded = await
|
|
1048
|
+
const encoded = await dagCborCanonicalEncode3(parsed);
|
|
743
1049
|
if (encoded.cid.toString() !== documentCID) {
|
|
744
1050
|
return c.json({ error: "blob bytes do not match documentCID" }, 400);
|
|
745
1051
|
}
|
|
@@ -771,7 +1077,24 @@ var createRelay = async (options) => {
|
|
|
771
1077
|
store
|
|
772
1078
|
});
|
|
773
1079
|
});
|
|
774
|
-
|
|
1080
|
+
const syncFromPeers = async () => {
|
|
1081
|
+
if (!peerClient) return;
|
|
1082
|
+
for (const peer of syncPeers) {
|
|
1083
|
+
let cursor = await store.getPeerCursor(peer.url);
|
|
1084
|
+
while (true) {
|
|
1085
|
+
const page = await peerClient.getOperationLog(peer.url, {
|
|
1086
|
+
...cursor ? { after: cursor } : {},
|
|
1087
|
+
limit: 1e3
|
|
1088
|
+
});
|
|
1089
|
+
if (!page || page.entries.length === 0) break;
|
|
1090
|
+
await ingestWithGossip(page.entries.map((e) => e.jwsToken));
|
|
1091
|
+
cursor = page.cursor ?? page.entries[page.entries.length - 1].cid;
|
|
1092
|
+
await store.setPeerCursor(peer.url, cursor);
|
|
1093
|
+
if (!page.cursor) break;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
return { app, did: relayDID, syncFromPeers };
|
|
775
1098
|
};
|
|
776
1099
|
var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
|
|
777
1100
|
status,
|
|
@@ -791,7 +1114,7 @@ var readBlob = async (params) => {
|
|
|
791
1114
|
documentCID = chain.state.currentDocumentCID;
|
|
792
1115
|
} else {
|
|
793
1116
|
for (const token of chain.log) {
|
|
794
|
-
const decoded =
|
|
1117
|
+
const decoded = decodeJwsUnsafe4(token);
|
|
795
1118
|
if (!decoded) continue;
|
|
796
1119
|
if (decoded.header.cid === ref) {
|
|
797
1120
|
operationFound = true;
|
|
@@ -819,7 +1142,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
819
1142
|
}
|
|
820
1143
|
const resolveKey = createCurrentKeyResolver(store);
|
|
821
1144
|
try {
|
|
822
|
-
const vcDecoded =
|
|
1145
|
+
const vcDecoded = decodeJwsUnsafe4(credHeader);
|
|
823
1146
|
if (!vcDecoded) throw new Error("invalid credential format");
|
|
824
1147
|
const vcHeader = vcDecoded.header;
|
|
825
1148
|
if (!vcHeader.kid) throw new Error("credential missing kid");
|
|
@@ -854,7 +1177,8 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
|
|
|
854
1177
|
};
|
|
855
1178
|
|
|
856
1179
|
// src/store.ts
|
|
857
|
-
import {
|
|
1180
|
+
import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
|
|
1181
|
+
import { decodeJwsUnsafe as decodeJwsUnsafe5 } from "@metalabel/dfos-protocol/crypto";
|
|
858
1182
|
var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
|
|
859
1183
|
var MemoryRelayStore = class {
|
|
860
1184
|
operations = /* @__PURE__ */ new Map();
|
|
@@ -864,6 +1188,7 @@ var MemoryRelayStore = class {
|
|
|
864
1188
|
blobs = /* @__PURE__ */ new Map();
|
|
865
1189
|
countersignatures = /* @__PURE__ */ new Map();
|
|
866
1190
|
operationLog = [];
|
|
1191
|
+
peerCursors = /* @__PURE__ */ new Map();
|
|
867
1192
|
async getOperation(cid) {
|
|
868
1193
|
return this.operations.get(cid);
|
|
869
1194
|
}
|
|
@@ -899,12 +1224,12 @@ var MemoryRelayStore = class {
|
|
|
899
1224
|
}
|
|
900
1225
|
async addCountersignature(operationCID, jwsToken) {
|
|
901
1226
|
const existing = this.countersignatures.get(operationCID) ?? [];
|
|
902
|
-
const decoded =
|
|
1227
|
+
const decoded = decodeJwsUnsafe5(jwsToken);
|
|
903
1228
|
if (decoded) {
|
|
904
1229
|
const kid = decoded.header.kid;
|
|
905
1230
|
const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
|
|
906
1231
|
for (const cs of existing) {
|
|
907
|
-
const d =
|
|
1232
|
+
const d = decodeJwsUnsafe5(cs);
|
|
908
1233
|
if (!d) continue;
|
|
909
1234
|
const existingKid = d.header.kid;
|
|
910
1235
|
const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
|
|
@@ -928,12 +1253,114 @@ var MemoryRelayStore = class {
|
|
|
928
1253
|
const cursor = entries.length === params.limit ? entries[entries.length - 1].cid : null;
|
|
929
1254
|
return { entries, cursor };
|
|
930
1255
|
}
|
|
1256
|
+
async getIdentityStateAtCID(did, cid) {
|
|
1257
|
+
const chain = this.identityChains.get(did);
|
|
1258
|
+
if (!chain) return null;
|
|
1259
|
+
const opsByCID = /* @__PURE__ */ new Map();
|
|
1260
|
+
for (const jws of chain.log) {
|
|
1261
|
+
const decoded = decodeJwsUnsafe5(jws);
|
|
1262
|
+
if (!decoded) continue;
|
|
1263
|
+
const payload = decoded.payload;
|
|
1264
|
+
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
1265
|
+
const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
1266
|
+
opsByCID.set(opCID, { jws, previousCID: prevCID });
|
|
1267
|
+
}
|
|
1268
|
+
if (!opsByCID.has(cid)) return null;
|
|
1269
|
+
const path = [];
|
|
1270
|
+
let currentCID = cid;
|
|
1271
|
+
while (currentCID) {
|
|
1272
|
+
const op = opsByCID.get(currentCID);
|
|
1273
|
+
if (!op) return null;
|
|
1274
|
+
path.unshift(op.jws);
|
|
1275
|
+
currentCID = op.previousCID;
|
|
1276
|
+
}
|
|
1277
|
+
const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
|
|
1278
|
+
const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
|
|
1279
|
+
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1280
|
+
return { state: identity, lastCreatedAt };
|
|
1281
|
+
}
|
|
1282
|
+
async getContentStateAtCID(contentId, cid) {
|
|
1283
|
+
const chain = this.contentChains.get(contentId);
|
|
1284
|
+
if (!chain) return null;
|
|
1285
|
+
const opsByCID = /* @__PURE__ */ new Map();
|
|
1286
|
+
for (const jws of chain.log) {
|
|
1287
|
+
const decoded = decodeJwsUnsafe5(jws);
|
|
1288
|
+
if (!decoded) continue;
|
|
1289
|
+
const payload = decoded.payload;
|
|
1290
|
+
const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
|
|
1291
|
+
const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
|
|
1292
|
+
opsByCID.set(opCID, { jws, previousCID: prevCID });
|
|
1293
|
+
}
|
|
1294
|
+
if (!opsByCID.has(cid)) return null;
|
|
1295
|
+
const path = [];
|
|
1296
|
+
let currentCID = cid;
|
|
1297
|
+
while (currentCID) {
|
|
1298
|
+
const op = opsByCID.get(currentCID);
|
|
1299
|
+
if (!op) return null;
|
|
1300
|
+
path.unshift(op.jws);
|
|
1301
|
+
currentCID = op.previousCID;
|
|
1302
|
+
}
|
|
1303
|
+
const resolveKey = createKeyResolver(this);
|
|
1304
|
+
const content = await verifyContentChain2({ log: path, resolveKey, enforceAuthorization: true });
|
|
1305
|
+
const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
|
|
1306
|
+
const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
|
|
1307
|
+
return { state: content, lastCreatedAt };
|
|
1308
|
+
}
|
|
1309
|
+
async getPeerCursor(peerUrl) {
|
|
1310
|
+
return this.peerCursors.get(peerUrl);
|
|
1311
|
+
}
|
|
1312
|
+
async setPeerCursor(peerUrl, cursor) {
|
|
1313
|
+
this.peerCursors.set(peerUrl, cursor);
|
|
1314
|
+
}
|
|
1315
|
+
// --- raw ops ---
|
|
1316
|
+
rawOps = /* @__PURE__ */ new Map();
|
|
1317
|
+
async putRawOp(cid, jwsToken) {
|
|
1318
|
+
if (!this.rawOps.has(cid)) {
|
|
1319
|
+
this.rawOps.set(cid, { jwsToken, status: "pending" });
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
async getUnsequencedOps(limit) {
|
|
1323
|
+
const out = [];
|
|
1324
|
+
for (const entry of this.rawOps.values()) {
|
|
1325
|
+
if (entry.status === "pending") {
|
|
1326
|
+
out.push(entry.jwsToken);
|
|
1327
|
+
if (out.length >= limit) break;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
return out;
|
|
1331
|
+
}
|
|
1332
|
+
async markOpsSequenced(cids) {
|
|
1333
|
+
for (const cid of cids) {
|
|
1334
|
+
const entry = this.rawOps.get(cid);
|
|
1335
|
+
if (entry) entry.status = "sequenced";
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async markOpRejected(cid, _reason) {
|
|
1339
|
+
const entry = this.rawOps.get(cid);
|
|
1340
|
+
if (entry) entry.status = "rejected";
|
|
1341
|
+
}
|
|
1342
|
+
async countUnsequenced() {
|
|
1343
|
+
let count = 0;
|
|
1344
|
+
for (const entry of this.rawOps.values()) {
|
|
1345
|
+
if (entry.status === "pending") count++;
|
|
1346
|
+
}
|
|
1347
|
+
return count;
|
|
1348
|
+
}
|
|
1349
|
+
async resetSequencer() {
|
|
1350
|
+
for (const entry of this.rawOps.values()) {
|
|
1351
|
+
if (entry.status !== "rejected") entry.status = "pending";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
931
1354
|
};
|
|
932
1355
|
export {
|
|
933
1356
|
MemoryRelayStore,
|
|
934
1357
|
bootstrapRelayIdentity,
|
|
1358
|
+
computeOpCID,
|
|
935
1359
|
createCurrentKeyResolver,
|
|
1360
|
+
createHttpPeerClient,
|
|
936
1361
|
createKeyResolver,
|
|
937
1362
|
createRelay,
|
|
938
|
-
ingestOperations
|
|
1363
|
+
ingestOperations,
|
|
1364
|
+
isDependencyFailure,
|
|
1365
|
+
sequenceOps
|
|
939
1366
|
};
|