@metalabel/dfos-web-relay 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,6 +672,55 @@ var bootstrapRelayIdentity = async (store) => {
539
672
  return { did, profileArtifactJws };
540
673
  };
541
674
 
675
+ // src/peer-client.ts
676
+ var createHttpPeerClient = () => {
677
+ const fetchJSON = async (url) => {
678
+ try {
679
+ const res = await fetch(url);
680
+ if (!res.ok) return null;
681
+ return await res.json();
682
+ } catch {
683
+ return null;
684
+ }
685
+ };
686
+ return {
687
+ async getIdentityLog(peerUrl, did, params) {
688
+ const url = new URL(`/identities/${encodeURIComponent(did)}/log`, peerUrl);
689
+ if (params?.after) url.searchParams.set("after", params.after);
690
+ if (params?.limit) url.searchParams.set("limit", String(params.limit));
691
+ const data = await fetchJSON(url.toString());
692
+ if (!data?.entries) return null;
693
+ return { entries: data.entries, cursor: data.cursor ?? null };
694
+ },
695
+ async getContentLog(peerUrl, contentId, params) {
696
+ const url = new URL(`/content/${encodeURIComponent(contentId)}/log`, peerUrl);
697
+ if (params?.after) url.searchParams.set("after", params.after);
698
+ if (params?.limit) url.searchParams.set("limit", String(params.limit));
699
+ const data = await fetchJSON(url.toString());
700
+ if (!data?.entries) return null;
701
+ return { entries: data.entries, cursor: data.cursor ?? null };
702
+ },
703
+ async getOperationLog(peerUrl, params) {
704
+ const url = new URL("/log", peerUrl);
705
+ if (params?.after) url.searchParams.set("after", params.after);
706
+ if (params?.limit) url.searchParams.set("limit", String(params.limit));
707
+ const data = await fetchJSON(url.toString());
708
+ if (!data?.entries) return null;
709
+ return { entries: data.entries, cursor: data.cursor ?? null };
710
+ },
711
+ async submitOperations(peerUrl, operations) {
712
+ try {
713
+ await fetch(new URL("/operations", peerUrl).toString(), {
714
+ method: "POST",
715
+ headers: { "Content-Type": "application/json" },
716
+ body: JSON.stringify({ operations })
717
+ });
718
+ } catch {
719
+ }
720
+ }
721
+ };
722
+ };
723
+
542
724
  // src/relay.ts
543
725
  import { VC_TYPE_CONTENT_READ, verifyCredential } from "@metalabel/dfos-protocol/credentials";
544
726
  import { dagCborCanonicalEncode as dagCborCanonicalEncode2, decodeJwsUnsafe as decodeJwsUnsafe3 } from "@metalabel/dfos-protocol/crypto";
@@ -578,17 +760,40 @@ var IngestBody = z.object({
578
760
  var createRelay = async (options) => {
579
761
  const { store } = options;
580
762
  const contentEnabled = options.content !== false;
763
+ const logEnabled = options.log !== false;
764
+ const peers = options.peers ?? [];
765
+ const peerClient = options.peerClient;
766
+ const gossipPeers = peers.filter((p) => p.gossip !== false);
767
+ const readThroughPeers = peers.filter((p) => p.readThrough !== false);
768
+ const syncPeers = peers.filter((p) => p.sync !== false);
581
769
  const identity = options.identity ?? await bootstrapRelayIdentity(store);
582
770
  const relayDID = identity.did;
583
771
  const profileArtifactJws = identity.profileArtifactJws;
772
+ const ingestWithGossip = async (tokens) => {
773
+ const results = await ingestOperations(tokens, store, { logEnabled });
774
+ if (gossipPeers.length > 0 && peerClient) {
775
+ const newOps = [];
776
+ for (let i = 0; i < results.length; i++) {
777
+ if (results[i].status === "new") newOps.push(tokens[i]);
778
+ }
779
+ if (newOps.length > 0) {
780
+ for (const peer of gossipPeers) {
781
+ peerClient.submitOperations(peer.url, newOps).catch(() => {
782
+ });
783
+ }
784
+ }
785
+ }
786
+ return results;
787
+ };
584
788
  const app = new Hono();
585
789
  app.get("/.well-known/dfos-relay", (c) => {
586
790
  return c.json({
587
791
  did: relayDID,
588
792
  protocol: "dfos-web-relay",
589
- version: "0.1.0",
793
+ version: "0.6.0",
590
794
  proof: true,
591
795
  content: contentEnabled,
796
+ log: logEnabled,
592
797
  profile: profileArtifactJws
593
798
  });
594
799
  });
@@ -603,7 +808,7 @@ var createRelay = async (options) => {
603
808
  if (!parsed.success) {
604
809
  return c.json({ error: "invalid request", details: parsed.error.issues }, 400);
605
810
  }
606
- const results = await ingestOperations(parsed.data.operations, store);
811
+ const results = await ingestWithGossip(parsed.data.operations);
607
812
  return c.json({ results });
608
813
  });
609
814
  app.get("/operations/:cid", async (c) => {
@@ -638,7 +843,24 @@ var createRelay = async (options) => {
638
843
  });
639
844
  app.get("/identities/:did{.+}", async (c) => {
640
845
  const did = c.req.param("did");
641
- const chain = await store.getIdentityChain(did);
846
+ let chain = await store.getIdentityChain(did);
847
+ if (!chain && readThroughPeers.length > 0 && peerClient) {
848
+ for (const peer of readThroughPeers) {
849
+ let after;
850
+ while (true) {
851
+ const logPage = await peerClient.getIdentityLog(peer.url, did, {
852
+ ...after ? { after } : {},
853
+ limit: 1e3
854
+ });
855
+ if (!logPage || logPage.entries.length === 0) break;
856
+ await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
857
+ if (!logPage.cursor) break;
858
+ after = logPage.cursor;
859
+ }
860
+ chain = await store.getIdentityChain(did);
861
+ if (chain) break;
862
+ }
863
+ }
642
864
  if (!chain) return c.json({ error: "not found" }, 404);
643
865
  return c.json({
644
866
  did: chain.did,
@@ -667,7 +889,24 @@ var createRelay = async (options) => {
667
889
  });
668
890
  app.get("/content/:contentId", async (c) => {
669
891
  const contentId = c.req.param("contentId");
670
- const chain = await store.getContentChain(contentId);
892
+ let chain = await store.getContentChain(contentId);
893
+ if (!chain && readThroughPeers.length > 0 && peerClient) {
894
+ for (const peer of readThroughPeers) {
895
+ let after;
896
+ while (true) {
897
+ const logPage = await peerClient.getContentLog(peer.url, contentId, {
898
+ ...after ? { after } : {},
899
+ limit: 1e3
900
+ });
901
+ if (!logPage || logPage.entries.length === 0) break;
902
+ await ingestWithGossip(logPage.entries.map((e) => e.jwsToken));
903
+ if (!logPage.cursor) break;
904
+ after = logPage.cursor;
905
+ }
906
+ chain = await store.getContentChain(contentId);
907
+ if (chain) break;
908
+ }
909
+ }
671
910
  if (!chain) return c.json({ error: "not found" }, 404);
672
911
  return c.json({
673
912
  contentId: chain.contentId,
@@ -706,6 +945,7 @@ var createRelay = async (options) => {
706
945
  });
707
946
  });
708
947
  app.get("/log", async (c) => {
948
+ if (!logEnabled) return c.json({ error: "global log not available" }, 501);
709
949
  const afterParam = c.req.query("after");
710
950
  const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
711
951
  const result = await store.readLog(afterParam ? { after: afterParam, limit } : { limit });
@@ -771,7 +1011,24 @@ var createRelay = async (options) => {
771
1011
  store
772
1012
  });
773
1013
  });
774
- return app;
1014
+ const syncFromPeers = async () => {
1015
+ if (!peerClient) return;
1016
+ for (const peer of syncPeers) {
1017
+ let cursor = await store.getPeerCursor(peer.url);
1018
+ while (true) {
1019
+ const page = await peerClient.getOperationLog(peer.url, {
1020
+ ...cursor ? { after: cursor } : {},
1021
+ limit: 1e3
1022
+ });
1023
+ if (!page || page.entries.length === 0) break;
1024
+ await ingestWithGossip(page.entries.map((e) => e.jwsToken));
1025
+ cursor = page.cursor ?? page.entries[page.entries.length - 1].cid;
1026
+ await store.setPeerCursor(peer.url, cursor);
1027
+ if (!page.cursor) break;
1028
+ }
1029
+ }
1030
+ };
1031
+ return { app, did: relayDID, syncFromPeers };
775
1032
  };
776
1033
  var jsonResponse = (body, status = 200) => new Response(JSON.stringify(body), {
777
1034
  status,
@@ -854,6 +1111,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
854
1111
  };
855
1112
 
856
1113
  // src/store.ts
1114
+ import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
857
1115
  import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
858
1116
  var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
859
1117
  var MemoryRelayStore = class {
@@ -864,6 +1122,7 @@ var MemoryRelayStore = class {
864
1122
  blobs = /* @__PURE__ */ new Map();
865
1123
  countersignatures = /* @__PURE__ */ new Map();
866
1124
  operationLog = [];
1125
+ peerCursors = /* @__PURE__ */ new Map();
867
1126
  async getOperation(cid) {
868
1127
  return this.operations.get(cid);
869
1128
  }
@@ -928,11 +1187,71 @@ var MemoryRelayStore = class {
928
1187
  const cursor = entries.length === params.limit ? entries[entries.length - 1].cid : null;
929
1188
  return { entries, cursor };
930
1189
  }
1190
+ async getIdentityStateAtCID(did, cid) {
1191
+ const chain = this.identityChains.get(did);
1192
+ if (!chain) return null;
1193
+ const opsByCID = /* @__PURE__ */ new Map();
1194
+ for (const jws of chain.log) {
1195
+ const decoded = decodeJwsUnsafe4(jws);
1196
+ if (!decoded) continue;
1197
+ const payload = decoded.payload;
1198
+ const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
1199
+ const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
1200
+ opsByCID.set(opCID, { jws, previousCID: prevCID });
1201
+ }
1202
+ if (!opsByCID.has(cid)) return null;
1203
+ const path = [];
1204
+ let currentCID = cid;
1205
+ while (currentCID) {
1206
+ const op = opsByCID.get(currentCID);
1207
+ if (!op) return null;
1208
+ path.unshift(op.jws);
1209
+ currentCID = op.previousCID;
1210
+ }
1211
+ const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
1212
+ const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1213
+ const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1214
+ return { state: identity, lastCreatedAt };
1215
+ }
1216
+ async getContentStateAtCID(contentId, cid) {
1217
+ const chain = this.contentChains.get(contentId);
1218
+ if (!chain) return null;
1219
+ const opsByCID = /* @__PURE__ */ new Map();
1220
+ for (const jws of chain.log) {
1221
+ const decoded = decodeJwsUnsafe4(jws);
1222
+ if (!decoded) continue;
1223
+ const payload = decoded.payload;
1224
+ const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
1225
+ const prevCID = typeof payload["previousOperationCID"] === "string" ? payload["previousOperationCID"] : null;
1226
+ opsByCID.set(opCID, { jws, previousCID: prevCID });
1227
+ }
1228
+ if (!opsByCID.has(cid)) return null;
1229
+ const path = [];
1230
+ let currentCID = cid;
1231
+ while (currentCID) {
1232
+ const op = opsByCID.get(currentCID);
1233
+ if (!op) return null;
1234
+ path.unshift(op.jws);
1235
+ currentCID = op.previousCID;
1236
+ }
1237
+ const resolveKey = createKeyResolver(this);
1238
+ const content = await verifyContentChain2({ log: path, resolveKey, enforceAuthorization: true });
1239
+ const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1240
+ const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1241
+ return { state: content, lastCreatedAt };
1242
+ }
1243
+ async getPeerCursor(peerUrl) {
1244
+ return this.peerCursors.get(peerUrl);
1245
+ }
1246
+ async setPeerCursor(peerUrl, cursor) {
1247
+ this.peerCursors.set(peerUrl, cursor);
1248
+ }
931
1249
  };
932
1250
  export {
933
1251
  MemoryRelayStore,
934
1252
  bootstrapRelayIdentity,
935
1253
  createCurrentKeyResolver,
1254
+ createHttpPeerClient,
936
1255
  createKeyResolver,
937
1256
  createRelay,
938
1257
  ingestOperations