@metalabel/dfos-web-relay 0.6.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.d.ts +43 -1
- package/dist/index.js +133 -25
- package/package.json +2 -2
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
|
-
|
|
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
|
|
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
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
3
|
+
"version": "0.6.1",
|
|
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.
|
|
55
|
+
"@metalabel/dfos-protocol": "0.6.1"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {
|
|
58
58
|
"build": "tsup",
|