@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/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: "accepted", kind: "identity-op", chainId: existing.chainId };
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
- await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: identity.did });
171
- return { cid, status: "accepted", kind: "identity-op", chainId: identity.did };
180
+ if (logEnabled) {
181
+ await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: identity.did });
182
+ }
183
+ return { cid, status: "new", kind: "identity-op", chainId: identity.did };
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: chain.state,
181
- headCID: chain.headCID,
182
- lastCreatedAt: chain.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: [...chain.log, jwsToken],
188
- headCID: extResult.operationCID,
189
- lastCreatedAt: extResult.createdAt,
190
- state: extResult.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
- await store.appendToLog({ cid, jwsToken, kind: "identity-op", chainId: did });
195
- return { cid, status: "accepted", kind: "identity-op", chainId: did };
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: "accepted", kind: "content-op", chainId: existing.chainId };
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
- await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: content.contentId });
241
- return { cid, status: "accepted", kind: "content-op", chainId: content.contentId };
300
+ if (logEnabled) {
301
+ await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: content.contentId });
302
+ }
303
+ return { cid, status: "new", kind: "content-op", chainId: content.contentId };
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 !== previousCID) {
261
- return { cid, status: "rejected", error: "chain has diverged (first-seen-wins)" };
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: chain.state,
265
- lastCreatedAt: chain.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: [...chain.log, jwsToken],
274
- lastCreatedAt: extResult.createdAt,
275
- state: extResult.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
- await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: chain.contentId });
280
- return { cid, status: "accepted", kind: "content-op", chainId: chain.contentId };
375
+ if (logEnabled) {
376
+ await store.appendToLog({ cid, jwsToken, kind: "content-op", chainId: chain.contentId });
377
+ }
378
+ return { cid, status: "new", kind: "content-op", chainId: chain.contentId };
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: "accepted", kind: "beacon", chainId: did };
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
- await store.appendToLog({ cid, jwsToken, kind: "beacon", chainId: did });
308
- return { cid, status: "accepted", kind: "beacon", chainId: did };
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: "accepted", kind: "countersign", chainId: targetCID };
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: "accepted", kind: "countersign", chainId: targetCID };
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
- await store.appendToLog({ cid, jwsToken, kind: "countersign", chainId: targetCID });
365
- return { cid, status: "accepted", kind: "countersign", chainId: targetCID };
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: "accepted", kind: "artifact", chainId: did };
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
- await store.appendToLog({ cid, jwsToken, kind: "artifact", chainId: did });
395
- return { cid, status: "accepted", kind: "artifact", chainId: did };
496
+ if (logEnabled) {
497
+ await store.appendToLog({ cid, jwsToken, kind: "artifact", chainId: did });
498
+ }
499
+ return { cid, status: "new", kind: "artifact", chainId: did };
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 ingestOperations = async (tokens, store) => {
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 !== "accepted" || !identityResult.chainId) {
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 !== "accepted") {
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 dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
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.1.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 ingestOperations(parsed.data.operations, store);
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 = decodeJwsUnsafe3(jws);
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
- const chain = await store.getIdentityChain(did);
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 = decodeJwsUnsafe3(jws);
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
- const chain = await store.getContentChain(contentId);
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 = decodeJwsUnsafe3(token);
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 dagCborCanonicalEncode2(parsed);
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
- return app;
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 = decodeJwsUnsafe3(token);
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 = decodeJwsUnsafe3(credHeader);
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 { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
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 = decodeJwsUnsafe4(jwsToken);
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 = decodeJwsUnsafe4(cs);
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
  };