@metalabel/dfos-web-relay 0.6.0 → 0.7.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.d.ts CHANGED
@@ -168,6 +168,24 @@ interface RelayStore {
168
168
  getPeerCursor(peerUrl: string): Promise<string | undefined>;
169
169
  /** Update last-synced log cursor for a peer relay */
170
170
  setPeerCursor(peerUrl: string, cursor: string): Promise<void>;
171
+ /** Store a raw JWS token by CID — idempotent, ignores duplicates */
172
+ putRawOp(cid: string, jwsToken: string): Promise<void>;
173
+ /** Return JWS tokens for unsequenced (pending) ops */
174
+ getUnsequencedOps(limit: number): Promise<string[]>;
175
+ /** Mark ops as successfully sequenced */
176
+ markOpsSequenced(cids: string[]): Promise<void>;
177
+ /** Mark an op as permanently rejected */
178
+ markOpRejected(cid: string, reason: string): Promise<void>;
179
+ /** Count of pending (unsequenced) raw ops */
180
+ countUnsequenced(): Promise<number>;
181
+ /** Reset all non-rejected raw ops to pending (re-sequence) */
182
+ resetSequencer(): Promise<void>;
183
+ }
184
+ /** Result of a sequencer run */
185
+ interface SequenceResult {
186
+ sequenced: number;
187
+ rejected: number;
188
+ pending: number;
171
189
  }
172
190
  interface IngestionResult {
173
191
  cid: string;
@@ -261,6 +279,13 @@ declare class MemoryRelayStore implements RelayStore {
261
279
  } | null>;
262
280
  getPeerCursor(peerUrl: string): Promise<string | undefined>;
263
281
  setPeerCursor(peerUrl: string, cursor: string): Promise<void>;
282
+ private rawOps;
283
+ putRawOp(cid: string, jwsToken: string): Promise<void>;
284
+ getUnsequencedOps(limit: number): Promise<string[]>;
285
+ markOpsSequenced(cids: string[]): Promise<void>;
286
+ markOpRejected(cid: string, _reason: string): Promise<void>;
287
+ countUnsequenced(): Promise<number>;
288
+ resetSequencer(): Promise<void>;
264
289
  }
265
290
 
266
291
  /**
@@ -297,4 +322,21 @@ declare const ingestOperations: (tokens: string[], store: RelayStore, options?:
297
322
  logEnabled?: boolean;
298
323
  }) => Promise<IngestionResult[]>;
299
324
 
300
- export { type BlobKey, type CreatedRelay, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type PeerClient, type PeerConfig, type PeerLogEntry, type RelayIdentity, type RelayOptions, type RelayStore, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, createCurrentKeyResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations };
325
+ /**
326
+ * Returns true if the rejection is due to a missing dependency that may
327
+ * arrive later via sync or gossip. Only these specific patterns are
328
+ * retryable — everything else is treated as permanent.
329
+ */
330
+ declare const isDependencyFailure: (error: string) => boolean;
331
+ /** Derive the operation CID from a JWS token */
332
+ declare const computeOpCID: (jwsToken: string) => Promise<string | undefined>;
333
+ /**
334
+ * Process unsequenced raw ops in a fixed-point loop until no more progress
335
+ * is made. Returns the JWS tokens of newly sequenced ops and aggregate stats.
336
+ */
337
+ declare const sequenceOps: (store: RelayStore) => Promise<{
338
+ newOps: string[];
339
+ result: SequenceResult;
340
+ }>;
341
+
342
+ export { type BlobKey, type CreatedRelay, type IngestionResult, type LogEntry, MemoryRelayStore, type OperationKind, type PeerClient, type PeerConfig, type PeerLogEntry, type RelayIdentity, type RelayOptions, type RelayStore, type SequenceResult, type StoredBeacon, type StoredContentChain, type StoredIdentityChain, type StoredOperation, bootstrapRelayIdentity, computeOpCID, createCurrentKeyResolver, createHttpPeerClient, createKeyResolver, createRelay, ingestOperations, isDependencyFailure, sequenceOps };
package/dist/index.js CHANGED
@@ -723,7 +723,7 @@ var createHttpPeerClient = () => {
723
723
 
724
724
  // src/relay.ts
725
725
  import { VC_TYPE_CONTENT_READ, verifyCredential } from "@metalabel/dfos-protocol/credentials";
726
- 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";
727
727
  import { Hono } from "hono";
728
728
  import { z } from "zod";
729
729
 
@@ -753,6 +753,59 @@ var authenticateRequest = async (authHeader, relayDID, store) => {
753
753
  }
754
754
  };
755
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
+
756
809
  // src/relay.ts
757
810
  var IngestBody = z.object({
758
811
  operations: z.array(z.string()).min(1).max(100)
@@ -769,20 +822,33 @@ var createRelay = async (options) => {
769
822
  const identity = options.identity ?? await bootstrapRelayIdentity(store);
770
823
  const relayDID = identity.did;
771
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
+ };
772
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
+ }
773
837
  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
- }
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]);
784
847
  }
785
848
  }
849
+ const { newOps: seqNewOps } = await sequenceOps(store);
850
+ gossip(newOps);
851
+ gossip(seqNewOps);
786
852
  return results;
787
853
  };
788
854
  const app = new Hono();
@@ -829,7 +895,7 @@ var createRelay = async (options) => {
829
895
  const after = c.req.query("after");
830
896
  const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
831
897
  const entries = chain.log.map((jws) => {
832
- const decoded = decodeJwsUnsafe3(jws);
898
+ const decoded = decodeJwsUnsafe4(jws);
833
899
  return { cid: decoded?.header.cid || "", jwsToken: jws };
834
900
  });
835
901
  let startIdx = 0;
@@ -875,7 +941,7 @@ var createRelay = async (options) => {
875
941
  const after = c.req.query("after");
876
942
  const limit = Math.min(Number(c.req.query("limit") || 100), 1e3);
877
943
  const entries = chain.log.map((jws) => {
878
- const decoded = decodeJwsUnsafe3(jws);
944
+ const decoded = decodeJwsUnsafe4(jws);
879
945
  return { cid: decoded?.header.cid || "", jwsToken: jws };
880
946
  });
881
947
  let startIdx = 0;
@@ -962,7 +1028,7 @@ var createRelay = async (options) => {
962
1028
  let documentCID = null;
963
1029
  let operationSignerDID = null;
964
1030
  for (const token of chain.log) {
965
- const decoded = decodeJwsUnsafe3(token);
1031
+ const decoded = decodeJwsUnsafe4(token);
966
1032
  if (!decoded) continue;
967
1033
  if (decoded.header.cid !== operationCID) continue;
968
1034
  const payload = decoded.payload;
@@ -979,7 +1045,7 @@ var createRelay = async (options) => {
979
1045
  const bytes = new Uint8Array(await c.req.arrayBuffer());
980
1046
  try {
981
1047
  const parsed = JSON.parse(new TextDecoder().decode(bytes));
982
- const encoded = await dagCborCanonicalEncode2(parsed);
1048
+ const encoded = await dagCborCanonicalEncode3(parsed);
983
1049
  if (encoded.cid.toString() !== documentCID) {
984
1050
  return c.json({ error: "blob bytes do not match documentCID" }, 400);
985
1051
  }
@@ -1048,7 +1114,7 @@ var readBlob = async (params) => {
1048
1114
  documentCID = chain.state.currentDocumentCID;
1049
1115
  } else {
1050
1116
  for (const token of chain.log) {
1051
- const decoded = decodeJwsUnsafe3(token);
1117
+ const decoded = decodeJwsUnsafe4(token);
1052
1118
  if (!decoded) continue;
1053
1119
  if (decoded.header.cid === ref) {
1054
1120
  operationFound = true;
@@ -1076,7 +1142,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
1076
1142
  }
1077
1143
  const resolveKey = createCurrentKeyResolver(store);
1078
1144
  try {
1079
- const vcDecoded = decodeJwsUnsafe3(credHeader);
1145
+ const vcDecoded = decodeJwsUnsafe4(credHeader);
1080
1146
  if (!vcDecoded) throw new Error("invalid credential format");
1081
1147
  const vcHeader = vcDecoded.header;
1082
1148
  if (!vcHeader.kid) throw new Error("credential missing kid");
@@ -1112,7 +1178,7 @@ var verifyReadCredential = async (auth, chain, contentId, credHeader, store) =>
1112
1178
 
1113
1179
  // src/store.ts
1114
1180
  import { verifyContentChain as verifyContentChain2, verifyIdentityChain as verifyIdentityChain2 } from "@metalabel/dfos-protocol/chain";
1115
- import { decodeJwsUnsafe as decodeJwsUnsafe4 } from "@metalabel/dfos-protocol/crypto";
1181
+ import { decodeJwsUnsafe as decodeJwsUnsafe5 } from "@metalabel/dfos-protocol/crypto";
1116
1182
  var blobKeyString = (key) => `${key.creatorDID}::${key.documentCID}`;
1117
1183
  var MemoryRelayStore = class {
1118
1184
  operations = /* @__PURE__ */ new Map();
@@ -1158,12 +1224,12 @@ var MemoryRelayStore = class {
1158
1224
  }
1159
1225
  async addCountersignature(operationCID, jwsToken) {
1160
1226
  const existing = this.countersignatures.get(operationCID) ?? [];
1161
- const decoded = decodeJwsUnsafe4(jwsToken);
1227
+ const decoded = decodeJwsUnsafe5(jwsToken);
1162
1228
  if (decoded) {
1163
1229
  const kid = decoded.header.kid;
1164
1230
  const witnessDID = kid.includes("#") ? kid.split("#")[0] : kid;
1165
1231
  for (const cs of existing) {
1166
- const d = decodeJwsUnsafe4(cs);
1232
+ const d = decodeJwsUnsafe5(cs);
1167
1233
  if (!d) continue;
1168
1234
  const existingKid = d.header.kid;
1169
1235
  const existingDID = existingKid.includes("#") ? existingKid.split("#")[0] : existingKid;
@@ -1192,7 +1258,7 @@ var MemoryRelayStore = class {
1192
1258
  if (!chain) return null;
1193
1259
  const opsByCID = /* @__PURE__ */ new Map();
1194
1260
  for (const jws of chain.log) {
1195
- const decoded = decodeJwsUnsafe4(jws);
1261
+ const decoded = decodeJwsUnsafe5(jws);
1196
1262
  if (!decoded) continue;
1197
1263
  const payload = decoded.payload;
1198
1264
  const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
@@ -1209,7 +1275,7 @@ var MemoryRelayStore = class {
1209
1275
  currentCID = op.previousCID;
1210
1276
  }
1211
1277
  const identity = await verifyIdentityChain2({ didPrefix: "did:dfos", log: path });
1212
- const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1278
+ const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
1213
1279
  const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1214
1280
  return { state: identity, lastCreatedAt };
1215
1281
  }
@@ -1218,7 +1284,7 @@ var MemoryRelayStore = class {
1218
1284
  if (!chain) return null;
1219
1285
  const opsByCID = /* @__PURE__ */ new Map();
1220
1286
  for (const jws of chain.log) {
1221
- const decoded = decodeJwsUnsafe4(jws);
1287
+ const decoded = decodeJwsUnsafe5(jws);
1222
1288
  if (!decoded) continue;
1223
1289
  const payload = decoded.payload;
1224
1290
  const opCID = typeof decoded.header.cid === "string" ? decoded.header.cid : "";
@@ -1236,7 +1302,7 @@ var MemoryRelayStore = class {
1236
1302
  }
1237
1303
  const resolveKey = createKeyResolver(this);
1238
1304
  const content = await verifyContentChain2({ log: path, resolveKey, enforceAuthorization: true });
1239
- const targetDecoded = decodeJwsUnsafe4(opsByCID.get(cid).jws);
1305
+ const targetDecoded = decodeJwsUnsafe5(opsByCID.get(cid).jws);
1240
1306
  const lastCreatedAt = typeof targetDecoded?.payload?.["createdAt"] === "string" ? (targetDecoded?.payload)["createdAt"] : "";
1241
1307
  return { state: content, lastCreatedAt };
1242
1308
  }
@@ -1246,13 +1312,55 @@ var MemoryRelayStore = class {
1246
1312
  async setPeerCursor(peerUrl, cursor) {
1247
1313
  this.peerCursors.set(peerUrl, cursor);
1248
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
+ }
1249
1354
  };
1250
1355
  export {
1251
1356
  MemoryRelayStore,
1252
1357
  bootstrapRelayIdentity,
1358
+ computeOpCID,
1253
1359
  createCurrentKeyResolver,
1254
1360
  createHttpPeerClient,
1255
1361
  createKeyResolver,
1256
1362
  createRelay,
1257
- ingestOperations
1363
+ ingestOperations,
1364
+ isDependencyFailure,
1365
+ sequenceOps
1258
1366
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metalabel/dfos-web-relay",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "description": "DFOS Web Relay — verifying HTTP relay for identity chains, content chains, beacons, and content blobs",
6
6
  "license": "MIT",
@@ -52,7 +52,7 @@
52
52
  "tsup": "^8.5.1",
53
53
  "tsx": "^4.20.3",
54
54
  "vitest": "^4.1.0",
55
- "@metalabel/dfos-protocol": "0.6.0"
55
+ "@metalabel/dfos-protocol": "0.7.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "tsup",