@metalabel/dfos-web-relay 0.9.0 → 0.11.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/dist/index.js CHANGED
@@ -7,6 +7,7 @@ import {
7
7
  import {
8
8
  createNewEd25519Keypair,
9
9
  generateId,
10
+ importEd25519Keypair,
10
11
  signPayloadEd25519
11
12
  } from "@metalabel/dfos-protocol/crypto";
12
13
 
@@ -14,7 +15,6 @@ import {
14
15
  import {
15
16
  decodeMultikey,
16
17
  verifyArtifact,
17
- verifyBeacon,
18
18
  verifyContentChain,
19
19
  verifyContentExtensionFromTrustedState,
20
20
  verifyCountersignature,
@@ -26,6 +26,16 @@ import {
26
26
  verifyDFOSCredential
27
27
  } from "@metalabel/dfos-protocol/credentials";
28
28
  import { dagCborCanonicalEncode, decodeJwsUnsafe } from "@metalabel/dfos-protocol/crypto";
29
+ var FORK_POINT_STATE_ERROR_PREFIX = "failed to compute state at fork point: ";
30
+ var DEPENDENCY_FAILURE_SUBSTRINGS = ["unknown identity:", "unknown key "];
31
+ var isKeyResolutionFailure = (message) => DEPENDENCY_FAILURE_SUBSTRINGS.some((s) => message.includes(s));
32
+ var isCredentialDependencyFailure = (message) => message.includes("issuer identity not found:") || message.includes(" not found on identity ");
33
+ var computeOpCID = async (jwsToken) => {
34
+ const decoded = decodeJwsUnsafe(jwsToken);
35
+ if (!decoded) return "";
36
+ const encoded = await dagCborCanonicalEncode(decoded.payload);
37
+ return encoded.cid.toString();
38
+ };
29
39
  var MAX_FUTURE_TIMESTAMP_MS = 24 * 60 * 60 * 1e3;
30
40
  var isFutureTimestamp = (createdAt) => {
31
41
  const ts = new Date(createdAt).getTime();
@@ -60,17 +70,6 @@ var classify = (jwsToken) => {
60
70
  const opDID = typeof payload["did"] === "string" ? payload["did"] : null;
61
71
  return { ...base, kind: "content-op", referencedDID: null, signerDID: opDID, priority: 2 };
62
72
  }
63
- if (typ === "did:dfos:beacon") {
64
- const beaconDID = typeof payload["did"] === "string" ? payload["did"] : null;
65
- return {
66
- ...base,
67
- kind: "beacon",
68
- referencedDID: beaconDID,
69
- signerDID: null,
70
- priority: 1,
71
- previousCID: null
72
- };
73
- }
74
73
  if (typ === "did:dfos:countersign") {
75
74
  const witnessDID = typeof payload["did"] === "string" ? payload["did"] : null;
76
75
  return {
@@ -91,7 +90,7 @@ var classify = (jwsToken) => {
91
90
  referencedDID: artifactDID,
92
91
  signerDID: null,
93
92
  priority: 1,
94
- // same as beacons — needs identity keys resolved first
93
+ // needs identity keys resolved first
95
94
  previousCID: null
96
95
  };
97
96
  }
@@ -103,7 +102,7 @@ var classify = (jwsToken) => {
103
102
  referencedDID: revocationDID,
104
103
  signerDID: null,
105
104
  priority: 1,
106
- // same as beacons — needs identity keys to verify
105
+ // needs identity keys to verify
107
106
  previousCID: null
108
107
  };
109
108
  }
@@ -231,7 +230,13 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
231
230
  const opType = payload["type"];
232
231
  const isGenesis = opType === "create";
233
232
  if (isGenesis) {
234
- const identity = await verifyIdentityChain({ didPrefix: "did:dfos", log: [jwsToken] });
233
+ let identity;
234
+ try {
235
+ identity = await verifyIdentityChain({ didPrefix: "did:dfos", log: [jwsToken] });
236
+ } catch (err) {
237
+ const message = err instanceof Error ? err.message : "verification failed";
238
+ return { cid, status: "rejected", error: message };
239
+ }
235
240
  const createdAt = payload["createdAt"];
236
241
  const chain2 = {
237
242
  did: identity.did,
@@ -252,15 +257,27 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
252
257
  if (hashIdx < 0) return { cid, status: "rejected", error: "non-genesis kid must be a DID URL" };
253
258
  const did = kid.substring(0, hashIdx);
254
259
  const chain = await store.getIdentityChain(did);
255
- if (!chain) return { cid, status: "rejected", error: `unknown identity: ${did}` };
260
+ if (!chain)
261
+ return {
262
+ cid,
263
+ status: "rejected",
264
+ error: `unknown identity: ${did}`,
265
+ dependencyMissing: true
266
+ };
256
267
  const previousCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
257
268
  if (previousCID === chain.headCID) {
258
- const extResult2 = await verifyIdentityExtensionFromTrustedState({
259
- currentState: chain.state,
260
- headCID: chain.headCID,
261
- lastCreatedAt: chain.lastCreatedAt,
262
- newOp: jwsToken
263
- });
269
+ let extResult2;
270
+ try {
271
+ extResult2 = await verifyIdentityExtensionFromTrustedState({
272
+ currentState: chain.state,
273
+ headCID: chain.headCID,
274
+ lastCreatedAt: chain.lastCreatedAt,
275
+ newOp: jwsToken
276
+ });
277
+ } catch (err) {
278
+ const message = err instanceof Error ? err.message : "verification failed";
279
+ return { cid, status: "rejected", error: message };
280
+ }
264
281
  const updated2 = {
265
282
  did: chain.did,
266
283
  log: [...chain.log, jwsToken],
@@ -276,18 +293,34 @@ var ingestIdentityOp = async (jwsToken, store, logEnabled) => {
276
293
  return { cid, status: "new", kind: "identity-op", chainId: did };
277
294
  }
278
295
  if (!previousCID || !chainLogContainsCID(chain.log, previousCID)) {
279
- return { cid, status: "rejected", error: "unknown previous operation in identity chain" };
296
+ return {
297
+ cid,
298
+ status: "rejected",
299
+ error: "unknown previous operation in identity chain",
300
+ dependencyMissing: true
301
+ };
280
302
  }
281
303
  const forkState = await store.getIdentityStateAtCID(did, previousCID);
282
304
  if (!forkState) {
283
- return { cid, status: "rejected", error: "failed to compute state at fork point" };
305
+ return {
306
+ cid,
307
+ status: "rejected",
308
+ error: `${FORK_POINT_STATE_ERROR_PREFIX}${previousCID}`,
309
+ dependencyMissing: true
310
+ };
311
+ }
312
+ let extResult;
313
+ try {
314
+ extResult = await verifyIdentityExtensionFromTrustedState({
315
+ currentState: forkState.state,
316
+ headCID: previousCID,
317
+ lastCreatedAt: forkState.lastCreatedAt,
318
+ newOp: jwsToken
319
+ });
320
+ } catch (err) {
321
+ const message = err instanceof Error ? err.message : "verification failed";
322
+ return { cid, status: "rejected", error: message };
284
323
  }
285
- const extResult = await verifyIdentityExtensionFromTrustedState({
286
- currentState: forkState.state,
287
- headCID: previousCID,
288
- lastCreatedAt: forkState.lastCreatedAt,
289
- newOp: jwsToken
290
- });
291
324
  const updatedLog = [...chain.log, jwsToken];
292
325
  const head = selectDeterministicHead(updatedLog);
293
326
  let headState = chain.state;
@@ -345,15 +378,28 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
345
378
  }
346
379
  const resolveKey = createKeyResolver(store);
347
380
  const resolveIdentity = createHistoricalIdentityResolver(store);
381
+ const isRevoked = (issuerDID, credentialCID) => store.isCredentialRevoked(issuerDID, credentialCID);
348
382
  const opType = payload["type"];
349
383
  const isGenesis = opType === "create";
350
384
  if (isGenesis) {
351
- const content = await verifyContentChain({
352
- log: [jwsToken],
353
- resolveKey,
354
- enforceAuthorization: true,
355
- resolveIdentity
356
- });
385
+ let content;
386
+ try {
387
+ content = await verifyContentChain({
388
+ log: [jwsToken],
389
+ resolveKey,
390
+ enforceAuthorization: true,
391
+ resolveIdentity,
392
+ isRevoked
393
+ });
394
+ } catch (err) {
395
+ const message = err instanceof Error ? err.message : "verification failed";
396
+ return {
397
+ cid,
398
+ status: "rejected",
399
+ error: message,
400
+ dependencyMissing: isKeyResolutionFailure(message)
401
+ };
402
+ }
357
403
  const createdAt = payload["createdAt"];
358
404
  const chain2 = {
359
405
  contentId: content.contentId,
@@ -375,26 +421,48 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
375
421
  }
376
422
  const prevOp = await store.getOperation(previousCID);
377
423
  if (!prevOp)
378
- return { cid, status: "rejected", error: `unknown previous operation: ${previousCID}` };
424
+ return {
425
+ cid,
426
+ status: "rejected",
427
+ error: `unknown previous operation: ${previousCID}`,
428
+ dependencyMissing: true
429
+ };
379
430
  if (prevOp.chainType !== "content") {
380
431
  return { cid, status: "rejected", error: "previousOperationCID is not a content operation" };
381
432
  }
382
433
  const chain = await store.getContentChain(prevOp.chainId);
383
434
  if (!chain)
384
- return { cid, status: "rejected", error: `content chain not found: ${prevOp.chainId}` };
435
+ return {
436
+ cid,
437
+ status: "rejected",
438
+ error: `content chain not found: ${prevOp.chainId}`,
439
+ dependencyMissing: true
440
+ };
385
441
  const creatorIdentity = await store.getIdentityChain(chain.state.creatorDID);
386
442
  if (creatorIdentity?.state.isDeleted) {
387
443
  return { cid, status: "rejected", error: "content creator identity is deleted" };
388
444
  }
389
445
  if (chain.state.headCID === previousCID) {
390
- const extResult2 = await verifyContentExtensionFromTrustedState({
391
- currentState: chain.state,
392
- lastCreatedAt: chain.lastCreatedAt,
393
- newOp: jwsToken,
394
- resolveKey,
395
- enforceAuthorization: true,
396
- resolveIdentity
397
- });
446
+ let extResult2;
447
+ try {
448
+ extResult2 = await verifyContentExtensionFromTrustedState({
449
+ currentState: chain.state,
450
+ lastCreatedAt: chain.lastCreatedAt,
451
+ newOp: jwsToken,
452
+ resolveKey,
453
+ enforceAuthorization: true,
454
+ resolveIdentity,
455
+ isRevoked
456
+ });
457
+ } catch (err) {
458
+ const message = err instanceof Error ? err.message : "verification failed";
459
+ return {
460
+ cid,
461
+ status: "rejected",
462
+ error: message,
463
+ dependencyMissing: isKeyResolutionFailure(message)
464
+ };
465
+ }
398
466
  const updated2 = {
399
467
  contentId: chain.contentId,
400
468
  genesisCID: chain.genesisCID,
@@ -410,20 +478,42 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
410
478
  return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
411
479
  }
412
480
  if (!chainLogContainsCID(chain.log, previousCID)) {
413
- return { cid, status: "rejected", error: "unknown previous operation in content chain" };
481
+ return {
482
+ cid,
483
+ status: "rejected",
484
+ error: "unknown previous operation in content chain",
485
+ dependencyMissing: true
486
+ };
414
487
  }
415
488
  const forkState = await store.getContentStateAtCID(chain.contentId, previousCID);
416
489
  if (!forkState) {
417
- return { cid, status: "rejected", error: "failed to compute state at fork point" };
418
- }
419
- const extResult = await verifyContentExtensionFromTrustedState({
420
- currentState: forkState.state,
421
- lastCreatedAt: forkState.lastCreatedAt,
422
- newOp: jwsToken,
423
- resolveKey,
424
- enforceAuthorization: true,
425
- resolveIdentity
426
- });
490
+ return {
491
+ cid,
492
+ status: "rejected",
493
+ error: `${FORK_POINT_STATE_ERROR_PREFIX}${previousCID}`,
494
+ dependencyMissing: true
495
+ };
496
+ }
497
+ let extResult;
498
+ try {
499
+ extResult = await verifyContentExtensionFromTrustedState({
500
+ currentState: forkState.state,
501
+ lastCreatedAt: forkState.lastCreatedAt,
502
+ newOp: jwsToken,
503
+ resolveKey,
504
+ enforceAuthorization: true,
505
+ resolveIdentity,
506
+ isRevoked
507
+ });
508
+ } catch (err) {
509
+ const message = err instanceof Error ? err.message : "verification failed";
510
+ return {
511
+ cid,
512
+ status: "rejected",
513
+ error: message,
514
+ dependencyMissing: isKeyResolutionFailure(message)
515
+ };
516
+ }
427
517
  const updatedLog = [...chain.log, jwsToken];
428
518
  const head = selectDeterministicHead(updatedLog);
429
519
  let headState = chain.state;
@@ -446,36 +536,6 @@ var ingestContentOp = async (jwsToken, store, logEnabled) => {
446
536
  }
447
537
  return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
448
538
  };
449
- var ingestBeacon = async (jwsToken, store, logEnabled) => {
450
- const resolveKey = createKeyResolver(store);
451
- let verified;
452
- try {
453
- verified = await verifyBeacon({ jwsToken, resolveKey });
454
- } catch (err) {
455
- const message = err instanceof Error ? err.message : "verification failed";
456
- return { cid: "", status: "rejected", error: message };
457
- }
458
- const did = verified.did;
459
- const cid = verified.beaconCID;
460
- const identity = await store.getIdentityChain(did);
461
- if (identity?.state.isDeleted) {
462
- return { cid, status: "rejected", error: "identity is deleted" };
463
- }
464
- const existing = await store.getBeacon(did);
465
- if (existing) {
466
- const existingTime = new Date(existing.state.createdAt).getTime();
467
- const newTime = new Date(verified.createdAt).getTime();
468
- if (newTime <= existingTime) {
469
- return { cid, status: "duplicate", kind: "beacon", chainId: did };
470
- }
471
- }
472
- await store.putBeacon({ did, jwsToken, beaconCID: cid, state: verified });
473
- await store.putOperation({ cid, jwsToken, chainType: "beacon", chainId: did });
474
- if (logEnabled) {
475
- await store.appendToLog({ cid, jwsToken, kind: "beacon", chainId: did });
476
- }
477
- return { cid, status: "new", kind: "beacon", chainId: did };
478
- };
479
539
  var ingestCountersign = async (jwsToken, store, logEnabled) => {
480
540
  const resolveKey = createKeyResolver(store);
481
541
  let verified;
@@ -483,7 +543,12 @@ var ingestCountersign = async (jwsToken, store, logEnabled) => {
483
543
  verified = await verifyCountersignature({ jwsToken, resolveKey });
484
544
  } catch (err) {
485
545
  const message = err instanceof Error ? err.message : "verification failed";
486
- return { cid: "", status: "rejected", error: message };
546
+ return {
547
+ cid: await computeOpCID(jwsToken),
548
+ status: "rejected",
549
+ error: message,
550
+ dependencyMissing: isKeyResolutionFailure(message)
551
+ };
487
552
  }
488
553
  const cid = verified.countersignCID;
489
554
  const { witnessDID, targetCID } = verified;
@@ -500,7 +565,12 @@ var ingestCountersign = async (jwsToken, store, logEnabled) => {
500
565
  }
501
566
  const targetOp = await store.getOperation(targetCID);
502
567
  if (!targetOp) {
503
- return { cid, status: "rejected", error: `unknown target operation: ${targetCID}` };
568
+ return {
569
+ cid,
570
+ status: "rejected",
571
+ error: `unknown target operation: ${targetCID}`,
572
+ dependencyMissing: true
573
+ };
504
574
  }
505
575
  let targetAuthorDID = null;
506
576
  if (targetOp.chainType === "identity") {
@@ -542,7 +612,12 @@ var ingestArtifact = async (jwsToken, store, logEnabled) => {
542
612
  verified = await verifyArtifact({ jwsToken, resolveKey });
543
613
  } catch (err) {
544
614
  const message = err instanceof Error ? err.message : "verification failed";
545
- return { cid: "", status: "rejected", error: message };
615
+ return {
616
+ cid: await computeOpCID(jwsToken),
617
+ status: "rejected",
618
+ error: message,
619
+ dependencyMissing: isKeyResolutionFailure(message)
620
+ };
546
621
  }
547
622
  const cid = verified.artifactCID;
548
623
  const did = verified.payload.did;
@@ -574,7 +649,12 @@ var ingestRevocation = async (jwsToken, store, logEnabled) => {
574
649
  verified = await verifyRevocation({ jwsToken, resolveKey });
575
650
  } catch (err) {
576
651
  const message = err instanceof Error ? err.message : "verification failed";
577
- return { cid: "", status: "rejected", error: message };
652
+ return {
653
+ cid: await computeOpCID(jwsToken),
654
+ status: "rejected",
655
+ error: message,
656
+ dependencyMissing: isKeyResolutionFailure(message)
657
+ };
578
658
  }
579
659
  const cid = verified.revocationCID;
580
660
  const did = verified.did;
@@ -613,11 +693,16 @@ var ingestPublicCredential = async (jwsToken, store, logEnabled) => {
613
693
  verified = await verifyDFOSCredential(jwsToken, { resolveIdentity });
614
694
  } catch (err) {
615
695
  const message = err instanceof Error ? err.message : "verification failed";
616
- return { cid: "", status: "rejected", error: message };
696
+ return {
697
+ cid: await computeOpCID(jwsToken),
698
+ status: "rejected",
699
+ error: message,
700
+ dependencyMissing: isCredentialDependencyFailure(message)
701
+ };
617
702
  }
618
703
  const cid = verified.credentialCID;
619
704
  if (verified.aud !== "*") {
620
- return { cid: "", status: "rejected", error: "not a public credential" };
705
+ return { cid, status: "rejected", error: "not a public credential" };
621
706
  }
622
707
  const existing = await store.getOperation(cid);
623
708
  if (existing) {
@@ -726,9 +811,10 @@ var selectDeterministicHead = (log) => {
726
811
  if (previousCID) hasChild.add(previousCID);
727
812
  }
728
813
  const tips = ops.filter((op) => !hasChild.has(op.cid));
814
+ const cmp = (x, y) => x < y ? -1 : x > y ? 1 : 0;
729
815
  tips.sort((a, b) => {
730
- if (a.createdAt !== b.createdAt) return b.createdAt.localeCompare(a.createdAt);
731
- return b.cid.localeCompare(a.cid);
816
+ if (a.createdAt !== b.createdAt) return cmp(b.createdAt, a.createdAt);
817
+ return cmp(b.cid, a.cid);
732
818
  });
733
819
  return tips[0] ?? { cid: "", createdAt: "" };
734
820
  };
@@ -747,9 +833,6 @@ var ingestOperations = async (tokens, store, options) => {
747
833
  case "content-op":
748
834
  result = await ingestContentOp(op.jwsToken, store, logEnabled);
749
835
  break;
750
- case "beacon":
751
- result = await ingestBeacon(op.jwsToken, store, logEnabled);
752
- break;
753
836
  case "countersign":
754
837
  result = await ingestCountersign(op.jwsToken, store, logEnabled);
755
838
  break;
@@ -763,14 +846,18 @@ var ingestOperations = async (tokens, store, options) => {
763
846
  result = await ingestPublicCredential(op.jwsToken, store, logEnabled);
764
847
  break;
765
848
  default:
766
- result = { cid: "", status: "rejected", error: "unrecognized operation type" };
849
+ result = {
850
+ cid: await computeOpCID(op.jwsToken),
851
+ status: "rejected",
852
+ error: "unrecognized operation type"
853
+ };
767
854
  }
768
855
  indexedResults.push({ index: op.originalIndex, result });
769
856
  } catch (err) {
770
857
  const message = err instanceof Error ? err.message : "unexpected error";
771
858
  indexedResults.push({
772
859
  index: op.originalIndex,
773
- result: { cid: "", status: "rejected", error: message }
860
+ result: { cid: await computeOpCID(op.jwsToken), status: "rejected", error: message }
774
861
  });
775
862
  }
776
863
  }
@@ -781,8 +868,28 @@ var ingestOperations = async (tokens, store, options) => {
781
868
  var bootstrapRelayIdentity = async (store) => {
782
869
  const keypair = createNewEd25519Keypair();
783
870
  const keyId = generateId("key");
784
- const multibase = encodeEd25519Multikey(keypair.publicKey);
785
- const signer = async (msg) => signPayloadEd25519(msg, keypair.privateKey);
871
+ return bootstrapWithKeyMaterial(store, {
872
+ privateKey: keypair.privateKey,
873
+ publicKey: keypair.publicKey,
874
+ keyId
875
+ });
876
+ };
877
+ var bootstrapRelayIdentityFromKey = async (store, params) => {
878
+ const { publicKey } = importEd25519Keypair(params.privateKey);
879
+ return bootstrapWithKeyMaterial(store, {
880
+ privateKey: params.privateKey,
881
+ publicKey,
882
+ keyId: params.keyId,
883
+ ...params.name !== void 0 ? { name: params.name } : {},
884
+ ...params.createdAt !== void 0 ? { createdAt: params.createdAt } : {}
885
+ });
886
+ };
887
+ var bootstrapWithKeyMaterial = async (store, params) => {
888
+ const { privateKey, publicKey, keyId } = params;
889
+ const name = params.name ?? "DFOS Relay";
890
+ const createdAt = params.createdAt ?? (/* @__PURE__ */ new Date()).toISOString();
891
+ const multibase = encodeEd25519Multikey(publicKey);
892
+ const signer = async (msg) => signPayloadEd25519(msg, privateKey);
786
893
  const key = { id: keyId, type: "Multikey", publicKeyMultibase: multibase };
787
894
  const identityOp = {
788
895
  version: 1,
@@ -790,7 +897,7 @@ var bootstrapRelayIdentity = async (store) => {
790
897
  authKeys: [key],
791
898
  assertKeys: [key],
792
899
  controllerKeys: [key],
793
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
900
+ createdAt
794
901
  };
795
902
  const { jwsToken: identityJws } = await signIdentityOperation({
796
903
  operation: identityOp,
@@ -808,9 +915,9 @@ var bootstrapRelayIdentity = async (store) => {
808
915
  did,
809
916
  content: {
810
917
  $schema: "https://schemas.dfos.com/profile/v1",
811
- name: "DFOS Relay"
918
+ name
812
919
  },
813
- createdAt: (/* @__PURE__ */ new Date()).toISOString()
920
+ createdAt
814
921
  };
815
922
  const kid = `${did}#${keyId}`;
816
923
  const { jwsToken: profileArtifactJws } = await signArtifact({
@@ -865,11 +972,16 @@ var createHttpPeerClient = () => {
865
972
  },
866
973
  async submitOperations(peerUrl, operations) {
867
974
  try {
868
- await fetch(new URL("/operations", peerUrl).toString(), {
975
+ const res = await fetch(new URL("/operations", peerUrl).toString(), {
869
976
  method: "POST",
870
977
  headers: { "Content-Type": "application/json" },
871
978
  body: JSON.stringify({ operations })
872
979
  });
980
+ if (!res.ok) {
981
+ console.warn(
982
+ `gossip submitOperations to ${peerUrl} returned ${res.status} (${operations.length} ops dropped)`
983
+ );
984
+ }
873
985
  } catch {
874
986
  }
875
987
  }
@@ -878,7 +990,7 @@ var createHttpPeerClient = () => {
878
990
 
879
991
  // src/relay.ts
880
992
  import { createRequire } from "module";
881
- import { dagCborCanonicalEncode as dagCborCanonicalEncode3, decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
993
+ import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
882
994
  import { Hono } from "hono";
883
995
  import { z } from "zod";
884
996
 
@@ -890,7 +1002,8 @@ import {
890
1002
  verifyDFOSCredential as verifyDFOSCredential2
891
1003
  } from "@metalabel/dfos-protocol/credentials";
892
1004
  import { decodeJwsUnsafe as decodeJwsUnsafe2 } from "@metalabel/dfos-protocol/crypto";
893
- var authenticateRequest = async (authHeader, relayDID, store) => {
1005
+ var DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS = 86400;
1006
+ var authenticateRequest = async (authHeader, relayDID, store, maxAuthTokenTTLSeconds = DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS) => {
894
1007
  if (!authHeader) return null;
895
1008
  if (!authHeader.startsWith("Bearer ")) return null;
896
1009
  const token = authHeader.substring(7);
@@ -907,7 +1020,11 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
907
1020
  return null;
908
1021
  }
909
1022
  try {
910
- return verifyAuthToken({ token, publicKey, audience: relayDID });
1023
+ const verified = verifyAuthToken({ token, publicKey, audience: relayDID });
1024
+ if (maxAuthTokenTTLSeconds > 0 && verified.exp - verified.iat > maxAuthTokenTTLSeconds) {
1025
+ return null;
1026
+ }
1027
+ return verified;
911
1028
  } catch {
912
1029
  return null;
913
1030
  }
@@ -986,22 +1103,7 @@ var verifyContentAccess = async (options) => {
986
1103
  };
987
1104
 
988
1105
  // src/sequencer.ts
989
- import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
990
- var isDependencyFailure = (error) => {
991
- const patterns = [
992
- "unknown previous operation",
993
- "unknown identity:",
994
- "content chain not found:",
995
- "failed to compute state at fork point:"
996
- ];
997
- return patterns.some((p) => error.includes(p));
998
- };
999
- var computeOpCID = async (jwsToken) => {
1000
- const decoded = decodeJwsUnsafe3(jwsToken);
1001
- if (!decoded) return void 0;
1002
- const encoded = await dagCborCanonicalEncode2(decoded.payload);
1003
- return encoded.cid.toString();
1004
- };
1106
+ var isDependencyFailure = (res) => res.dependencyMissing === true;
1005
1107
  var sequenceOps = async (store) => {
1006
1108
  const newOps = [];
1007
1109
  const result = { sequenced: 0, rejected: 0, pending: 0 };
@@ -1022,7 +1124,7 @@ var sequenceOps = async (store) => {
1022
1124
  } else if (res.status === "duplicate") {
1023
1125
  sequencedCIDs.push(res.cid);
1024
1126
  progress = true;
1025
- } else if (res.status === "rejected" && !isDependencyFailure(res.error ?? "")) {
1127
+ } else if (res.status === "rejected" && !isDependencyFailure(res)) {
1026
1128
  await store.markOpRejected(res.cid, res.error ?? "unknown");
1027
1129
  result.rejected++;
1028
1130
  progress = true;
@@ -1041,13 +1143,37 @@ var sequenceOps = async (store) => {
1041
1143
  // src/relay.ts
1042
1144
  var require2 = createRequire(import.meta.url);
1043
1145
  var { version: RELAY_VERSION } = require2("../package.json");
1146
+ var MAX_OPERATIONS_PER_BATCH = 100;
1044
1147
  var IngestBody = z.object({
1045
- operations: z.array(z.string()).min(1).max(100)
1148
+ operations: z.array(z.string()).min(1).max(MAX_OPERATIONS_PER_BATCH)
1046
1149
  });
1150
+ var MAX_GOSSIP_BATCH = MAX_OPERATIONS_PER_BATCH;
1151
+ var chunkOps = (items, size) => {
1152
+ const chunks = [];
1153
+ for (let start = 0; start < items.length; start += size) {
1154
+ chunks.push(items.slice(start, start + size));
1155
+ }
1156
+ return chunks;
1157
+ };
1158
+ var MAX_BODY_BYTES = 16 << 20;
1159
+ var exceedsBodyCap = (contentLength) => {
1160
+ if (!contentLength) return false;
1161
+ const n = Number(contentLength);
1162
+ return Number.isFinite(n) && n > MAX_BODY_BYTES;
1163
+ };
1164
+ var parseLimit = (raw, defaultLimit, maxLimit) => {
1165
+ if (raw === void 0 || raw === "") return defaultLimit;
1166
+ if (!/^-?\d+$/.test(raw)) return defaultLimit;
1167
+ const n = Number(raw);
1168
+ if (!Number.isSafeInteger(n) || n < 1) return defaultLimit;
1169
+ if (n > maxLimit) return maxLimit;
1170
+ return n;
1171
+ };
1047
1172
  var createRelay = async (options) => {
1048
1173
  const { store } = options;
1049
1174
  const contentEnabled = options.content !== false;
1050
1175
  const logEnabled = options.log !== false;
1176
+ const maxAuthTokenTTLSeconds = options.maxAuthTokenTTLSeconds ?? DEFAULT_MAX_AUTH_TOKEN_TTL_SECONDS;
1051
1177
  const peers = options.peers ?? [];
1052
1178
  const peerClient = options.peerClient;
1053
1179
  const gossipPeers = peers.filter((p) => p.gossip !== false);
@@ -1059,8 +1185,10 @@ var createRelay = async (options) => {
1059
1185
  const gossip = (ops) => {
1060
1186
  if (ops.length === 0 || gossipPeers.length === 0 || !peerClient) return;
1061
1187
  for (const peer of gossipPeers) {
1062
- peerClient.submitOperations(peer.url, ops).catch(() => {
1063
- });
1188
+ for (const chunk of chunkOps(ops, MAX_GOSSIP_BATCH)) {
1189
+ peerClient.submitOperations(peer.url, chunk).catch(() => {
1190
+ });
1191
+ }
1064
1192
  }
1065
1193
  };
1066
1194
  const ingestWithGossip = async (tokens) => {
@@ -1086,6 +1214,20 @@ var createRelay = async (options) => {
1086
1214
  return results;
1087
1215
  };
1088
1216
  const app = new Hono();
1217
+ app.use("*", async (c, next) => {
1218
+ if (c.req.method === "OPTIONS") {
1219
+ return c.body(null, 204, {
1220
+ "Access-Control-Allow-Origin": "*",
1221
+ "Access-Control-Allow-Methods": "GET, POST, PUT, OPTIONS",
1222
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
1223
+ });
1224
+ }
1225
+ await next();
1226
+ c.res.headers.set("Access-Control-Allow-Origin", "*");
1227
+ c.res.headers.set("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS");
1228
+ c.res.headers.set("Access-Control-Allow-Headers", "Content-Type, Authorization");
1229
+ return;
1230
+ });
1089
1231
  app.get("/.well-known/dfos-relay", (c) => {
1090
1232
  return c.json({
1091
1233
  did: relayDID,
@@ -1101,6 +1243,9 @@ var createRelay = async (options) => {
1101
1243
  });
1102
1244
  });
1103
1245
  app.post("/operations", async (c) => {
1246
+ if (exceedsBodyCap(c.req.header("content-length"))) {
1247
+ return c.json({ error: "request body too large" }, 413);
1248
+ }
1104
1249
  let body;
1105
1250
  try {
1106
1251
  body = await c.req.json();
@@ -1130,9 +1275,9 @@ var createRelay = async (options) => {
1130
1275
  const chain = await store.getIdentityChain(did);
1131
1276
  if (!chain) return c.json({ error: "not found" }, 404);
1132
1277
  const after = c.req.query("after");
1133
- const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
1278
+ const limit = parseLimit(c.req.query("limit"), 100, 1e3);
1134
1279
  const entries = chain.log.map((jws) => {
1135
- const decoded = decodeJwsUnsafe4(jws);
1280
+ const decoded = decodeJwsUnsafe3(jws);
1136
1281
  return { cid: decoded?.header.cid || "", jwsToken: jws };
1137
1282
  });
1138
1283
  let startIdx = 0;
@@ -1176,9 +1321,9 @@ var createRelay = async (options) => {
1176
1321
  const chain = await store.getContentChain(contentId);
1177
1322
  if (!chain) return c.json({ error: "not found" }, 404);
1178
1323
  const after = c.req.query("after");
1179
- const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
1324
+ const limit = parseLimit(c.req.query("limit"), 100, 1e3);
1180
1325
  const entries = chain.log.map((jws) => {
1181
- const decoded = decodeJwsUnsafe4(jws);
1326
+ const decoded = decodeJwsUnsafe3(jws);
1182
1327
  return { cid: decoded?.header.cid || "", jwsToken: jws };
1183
1328
  });
1184
1329
  let startIdx = 0;
@@ -1197,7 +1342,12 @@ var createRelay = async (options) => {
1197
1342
  if (!chain) return c.json({ error: "not found" }, 404);
1198
1343
  const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
1199
1344
  if (!publicAccess) {
1200
- const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
1345
+ const auth = await authenticateRequest(
1346
+ c.req.header("authorization"),
1347
+ relayDID,
1348
+ store,
1349
+ maxAuthTokenTTLSeconds
1350
+ );
1201
1351
  if (!auth) return c.json({ error: "authentication required" }, 401);
1202
1352
  const accessError = await verifyReadAccess(
1203
1353
  auth,
@@ -1209,7 +1359,7 @@ var createRelay = async (options) => {
1209
1359
  if (accessError) return accessError;
1210
1360
  }
1211
1361
  const after = c.req.query("after");
1212
- const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
1362
+ const limit = parseLimit(c.req.query("limit"), 100, 1e3);
1213
1363
  const result = await store.getDocuments(contentId, {
1214
1364
  ...after ? { after } : {},
1215
1365
  limit
@@ -1266,22 +1416,10 @@ var createRelay = async (options) => {
1266
1416
  const countersigs = await store.getCountersignatures(cid);
1267
1417
  return c.json({ operationCID: cid, countersignatures: countersigs });
1268
1418
  });
1269
- app.get("/beacons/:did{.+}", async (c) => {
1270
- const did = c.req.param("did");
1271
- const beacon = await store.getBeacon(did);
1272
- if (!beacon) return c.json({ error: "not found" }, 404);
1273
- return c.json({
1274
- did: beacon.did,
1275
- jwsToken: beacon.jwsToken,
1276
- beaconCID: beacon.beaconCID,
1277
- manifestContentId: beacon.state.manifestContentId,
1278
- createdAt: beacon.state.createdAt
1279
- });
1280
- });
1281
1419
  app.get("/log", async (c) => {
1282
1420
  if (!logEnabled) return c.json({ error: "global log not available" }, 501);
1283
1421
  const afterParam = c.req.query("after");
1284
- const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
1422
+ const limit = parseLimit(c.req.query("limit"), 100, 1e3);
1285
1423
  const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
1286
1424
  return c.json(result);
1287
1425
  });
@@ -1289,14 +1427,22 @@ var createRelay = async (options) => {
1289
1427
  if (!contentEnabled) return c.json({ error: "content plane not available" }, 501);
1290
1428
  const contentId = c.req.param("contentId");
1291
1429
  const operationCID = c.req.param("operationCID");
1292
- const auth = await authenticateRequest(c.req.header("authorization"), relayDID, store);
1430
+ if (exceedsBodyCap(c.req.header("content-length"))) {
1431
+ return c.json({ error: "request body too large" }, 413);
1432
+ }
1433
+ const auth = await authenticateRequest(
1434
+ c.req.header("authorization"),
1435
+ relayDID,
1436
+ store,
1437
+ maxAuthTokenTTLSeconds
1438
+ );
1293
1439
  if (!auth) return c.json({ error: "authentication required" }, 401);
1294
1440
  const chain = await store.getContentChain(contentId);
1295
1441
  if (!chain) return c.json({ error: "content chain not found" }, 404);
1296
1442
  let documentCID = null;
1297
1443
  let operationSignerDID = null;
1298
1444
  for (const token of chain.log) {
1299
- const decoded = decodeJwsUnsafe4(token);
1445
+ const decoded = decodeJwsUnsafe3(token);
1300
1446
  if (!decoded) continue;
1301
1447
  if (decoded.header.cid !== operationCID) continue;
1302
1448
  const payload = decoded.payload;
@@ -1311,9 +1457,12 @@ var createRelay = async (options) => {
1311
1457
  return c.json({ error: "not authorized \u2014 must be chain creator or operation signer" }, 403);
1312
1458
  }
1313
1459
  const bytes = new Uint8Array(await c.req.arrayBuffer());
1460
+ if (bytes.byteLength > MAX_BODY_BYTES) {
1461
+ return c.json({ error: "request body too large" }, 413);
1462
+ }
1314
1463
  try {
1315
1464
  const parsed = JSON.parse(new TextDecoder().decode(bytes));
1316
- const encoded = await dagCborCanonicalEncode3(parsed);
1465
+ const encoded = await dagCborCanonicalEncode2(parsed);
1317
1466
  if (encoded.cid.toString() !== documentCID) {
1318
1467
  return c.json({ error: "blob bytes do not match documentCID" }, 400);
1319
1468
  }
@@ -1331,7 +1480,8 @@ var createRelay = async (options) => {
1331
1480
  authHeader: c.req.header("authorization"),
1332
1481
  credHeader: c.req.header("x-credential"),
1333
1482
  relayDID,
1334
- store
1483
+ store,
1484
+ maxAuthTokenTTLSeconds
1335
1485
  });
1336
1486
  });
1337
1487
  app.get("/content/:contentId/blob/:ref", async (c) => {
@@ -1342,20 +1492,24 @@ var createRelay = async (options) => {
1342
1492
  authHeader: c.req.header("authorization"),
1343
1493
  credHeader: c.req.header("x-credential"),
1344
1494
  relayDID,
1345
- store
1495
+ store,
1496
+ maxAuthTokenTTLSeconds
1346
1497
  });
1347
1498
  });
1499
+ const maxOpsPerSyncCycle = 5e3;
1348
1500
  const syncFromPeers = async () => {
1349
1501
  if (!peerClient) return;
1350
1502
  for (const peer of syncPeers) {
1351
1503
  let cursor = await store.getPeerCursor(peer.url);
1352
- while (true) {
1504
+ let fetched = 0;
1505
+ while (fetched < maxOpsPerSyncCycle) {
1353
1506
  const page = await peerClient.getOperationLog(peer.url, {
1354
1507
  ...cursor ? { after: cursor } : {},
1355
1508
  limit: 1e3
1356
1509
  });
1357
1510
  if (!page || page.entries.length === 0) break;
1358
1511
  await ingestWithGossip(page.entries.map((e) => e.jwsToken));
1512
+ fetched += page.entries.length;
1359
1513
  cursor = page.cursor ?? page.entries[page.entries.length - 1].cid;
1360
1514
  await store.setPeerCursor(peer.url, cursor);
1361
1515
  if (!page.cursor) break;
@@ -1369,12 +1523,12 @@ var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
1369
1523
  headers: { "content-type": "application/json" }
1370
1524
  });
1371
1525
  var readBlob = async (params) => {
1372
- const { contentId, ref, authHeader, credHeader, relayDID, store } = params;
1526
+ const { contentId, ref, authHeader, credHeader, relayDID, store, maxAuthTokenTTLSeconds } = params;
1373
1527
  const chain = await store.getContentChain(contentId);
1374
1528
  if (!chain) return jsonResponse({ error: "content chain not found" }, 404);
1375
1529
  const publicAccess = await hasPublicStandingAuth(contentId, "read", store);
1376
1530
  if (!publicAccess) {
1377
- const auth = await authenticateRequest(authHeader, relayDID, store);
1531
+ const auth = await authenticateRequest(authHeader, relayDID, store, maxAuthTokenTTLSeconds);
1378
1532
  if (!auth) return jsonResponse({ error: "authentication required" }, 401);
1379
1533
  const credError = await verifyReadAccess(auth, chain, contentId, credHeader, store);
1380
1534
  if (credError) return credError;
@@ -1385,7 +1539,7 @@ var readBlob = async (params) => {
1385
1539
  documentCID = chain.state.currentDocumentCID;
1386
1540
  } else {
1387
1541
  for (const token of chain.log) {
1388
- const decoded = decodeJwsUnsafe4(token);
1542
+ const decoded = decodeJwsUnsafe3(token);
1389
1543
  if (!decoded) continue;
1390
1544
  if (decoded.header.cid === ref) {
1391
1545
  operationFound = true;
@@ -1421,13 +1575,12 @@ var verifyReadAccess = async (auth, chain, contentId, credHeader, store) => {
1421
1575
 
1422
1576
  // src/store.ts
1423
1577
  import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
1424
- import { decodeJwsUnsafe as decodeJwsUnsafe5 } from "@metalabel/dfos-protocol/crypto";
1578
+ import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
1425
1579
  var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
1426
1580
  var MemoryRelayStore = class {
1427
1581
  operations = /* @__PURE__ */ new Map();
1428
1582
  identityChains = /* @__PURE__ */ new Map();
1429
1583
  contentChains = /* @__PURE__ */ new Map();
1430
- beacons = /* @__PURE__ */ new Map();
1431
1584
  blobs = /* @__PURE__ */ new Map();
1432
1585
  countersignatures = /* @__PURE__ */ new Map();
1433
1586
  operationLog = [];
@@ -1454,12 +1607,6 @@ var MemoryRelayStore = class {
1454
1607
  async putContentChain(chain) {
1455
1608
  this.contentChains.set(chain.contentId, chain);
1456
1609
  }
1457
- async getBeacon(did) {
1458
- return this.beacons.get(did);
1459
- }
1460
- async putBeacon(beacon) {
1461
- this.beacons.set(beacon.did, beacon);
1462
- }
1463
1610
  async getBlob(key) {
1464
1611
  return this.blobs.get(blobKeyString(key));
1465
1612
  }
@@ -1471,12 +1618,12 @@ var MemoryRelayStore = class {
1471
1618
  }
1472
1619
  async addCountersignature(operationCID, jwsToken) {
1473
1620
  const existing = this.countersignatures.get(operationCID) ?? [];
1474
- const decoded = decodeJwsUnsafe5(jwsToken);
1621
+ const decoded = decodeJwsUnsafe4(jwsToken);
1475
1622
  if (decoded) {
1476
1623
  const kid = decoded.header.kid;
1477
1624
  const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
1478
1625
  for (const cs of existing) {
1479
- const d = decodeJwsUnsafe5(cs);
1626
+ const d = decodeJwsUnsafe4(cs);
1480
1627
  if (!d) continue;
1481
1628
  const existingKid = d.header.kid;
1482
1629
  const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
@@ -1531,7 +1678,7 @@ var MemoryRelayStore = class {
1531
1678
  if (!chain) return { documents: [], cursor: null };
1532
1679
  const entries = [];
1533
1680
  for (const jws of chain.log) {
1534
- const decoded = decodeJwsUnsafe5(jws);
1681
+ const decoded = decodeJwsUnsafe4(jws);
1535
1682
  if (!decoded) continue;
1536
1683
  const payload = decoded.payload;
1537
1684
  const cid = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
@@ -1593,7 +1740,7 @@ var MemoryRelayStore = class {
1593
1740
  if (!chain) return null;
1594
1741
  const opsByCID = /* @__PURE__ */ new Map();
1595
1742
  for (const jws of chain.log) {
1596
- const decoded = decodeJwsUnsafe5(jws);
1743
+ const decoded = decodeJwsUnsafe4(jws);
1597
1744
  if (!decoded) continue;
1598
1745
  const payload = decoded.payload;
1599
1746
  const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
@@ -1610,7 +1757,7 @@ var MemoryRelayStore = class {
1610
1757
  currentCID = op.previousCID;
1611
1758
  }
1612
1759
  const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
1613
- const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
1760
+ const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1614
1761
  const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1615
1762
  return { state: identity, lastCreatedAt };
1616
1763
  }
@@ -1619,7 +1766,7 @@ var MemoryRelayStore = class {
1619
1766
  if (!chain) return null;
1620
1767
  const opsByCID = /* @__PURE__ */ new Map();
1621
1768
  for (const jws of chain.log) {
1622
- const decoded = decodeJwsUnsafe5(jws);
1769
+ const decoded = decodeJwsUnsafe4(jws);
1623
1770
  if (!decoded) continue;
1624
1771
  const payload = decoded.payload;
1625
1772
  const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
@@ -1646,7 +1793,7 @@ var MemoryRelayStore = class {
1646
1793
  enforceAuthorization: true,
1647
1794
  resolveIdentity
1648
1795
  });
1649
- const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
1796
+ const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1650
1797
  const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1651
1798
  return { state: content, lastCreatedAt };
1652
1799
  }
@@ -1680,8 +1827,7 @@ var MemoryRelayStore = class {
1680
1827
  }
1681
1828
  }
1682
1829
  async markOpRejected(cid, _reason) {
1683
- const entry = this.rawOps.get(cid);
1684
- if (entry) entry.status = "rejected";
1830
+ this.rawOps.delete(cid);
1685
1831
  }
1686
1832
  async countUnsequenced() {
1687
1833
  let count = 0;
@@ -1699,6 +1845,8 @@ var MemoryRelayStore = class {
1699
1845
  export {
1700
1846
  MemoryRelayStore,
1701
1847
  bootstrapRelayIdentity,
1848
+ bootstrapRelayIdentityFromKey,
1849
+ chunkOps,
1702
1850
  computeOpCID,
1703
1851
  createCurrentKeyResolver,
1704
1852
  createHistoricalIdentityResolver,