@metalabel/dfos-client 0.29.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 ADDED
@@ -0,0 +1,766 @@
1
+ import {
2
+ memoryStore
3
+ } from "./chunk-63XCFYOE.js";
4
+
5
+ // src/client.ts
6
+ import {
7
+ CONTENT_ID_ANCHOR_RE,
8
+ verifyRevocation as verifyRevocation2
9
+ } from "@metalabel/dfos-protocol/chain";
10
+ import { verifyDFOSCredential } from "@metalabel/dfos-protocol/credentials";
11
+ import { dagCborCanonicalEncode, decodeJwsUnsafe as decodeJwsUnsafe2 } from "@metalabel/dfos-protocol/crypto";
12
+ import { createHttpPeerClient } from "@metalabel/dfos-web-relay/peer-client";
13
+
14
+ // src/transport.ts
15
+ var PAGE_LIMIT = 1e3;
16
+ var MAX_PAGES = 1e4;
17
+ var normalizeRelays = (relays) => {
18
+ const seen = /* @__PURE__ */ new Set();
19
+ const out = [];
20
+ for (const raw of relays) {
21
+ const url = raw.trim().replace(/\/+$/, "");
22
+ if (url.length === 0 || seen.has(url)) continue;
23
+ seen.add(url);
24
+ out.push(url);
25
+ }
26
+ return out;
27
+ };
28
+ var digestOps = (ops) => {
29
+ let h = 2166136261;
30
+ const joined = ops.map((o) => `${o.cid} ${o.jwsToken}`).join("\n");
31
+ for (let i = 0; i < joined.length; i++) {
32
+ h ^= joined.charCodeAt(i);
33
+ h = Math.imul(h, 16777619);
34
+ }
35
+ return (h >>> 0).toString(16).padStart(8, "0");
36
+ };
37
+ var drainRelay = async (fetchPage, url) => {
38
+ const out = [];
39
+ let cursor;
40
+ let restarted = false;
41
+ for (let page = 0; page < MAX_PAGES; page++) {
42
+ const params = cursor ? { after: cursor, limit: PAGE_LIMIT } : { limit: PAGE_LIMIT };
43
+ const res = await fetchPage(url, params);
44
+ if (res === "invalid-cursor") {
45
+ if (!cursor || restarted) return null;
46
+ out.length = 0;
47
+ cursor = void 0;
48
+ restarted = true;
49
+ continue;
50
+ }
51
+ if (res === null) return null;
52
+ out.push(...res.entries);
53
+ if (!res.next || res.entries.length === 0) break;
54
+ cursor = res.next;
55
+ }
56
+ return out;
57
+ };
58
+ var StaleAnswerError = class extends Error {
59
+ };
60
+ var fanOutLog = async (fetchPage, relays, quorum, verifyCandidate) => {
61
+ const responses = [];
62
+ const byDigest = /* @__PURE__ */ new Map();
63
+ const badDigests = /* @__PURE__ */ new Set();
64
+ let lastVerifyError;
65
+ for (const url of relays) {
66
+ let entries = null;
67
+ try {
68
+ entries = await drainRelay(fetchPage, url);
69
+ } catch {
70
+ entries = null;
71
+ }
72
+ if (entries === null) {
73
+ responses.push({ url, ok: false, digest: "" });
74
+ continue;
75
+ }
76
+ const digest = digestOps(entries);
77
+ responses.push({ url, ok: true, digest });
78
+ if (badDigests.has(digest)) continue;
79
+ const group = byDigest.get(digest) ?? { url, entries, count: 0 };
80
+ group.count += 1;
81
+ byDigest.set(digest, group);
82
+ if (group.count >= quorum) {
83
+ try {
84
+ const value = await verifyCandidate(group.entries);
85
+ return {
86
+ outcome: "verified",
87
+ value,
88
+ entries: group.entries,
89
+ provenance: { answeredBy: group.url, responses, agreed: true, fromCache: false }
90
+ };
91
+ } catch (err) {
92
+ if (!(err instanceof StaleAnswerError)) lastVerifyError = err;
93
+ badDigests.add(digest);
94
+ }
95
+ }
96
+ }
97
+ const remaining = [...byDigest.entries()].filter(([digest]) => !badDigests.has(digest)).sort((a, b) => b[1].count - a[1].count);
98
+ for (const [digest, group] of remaining) {
99
+ try {
100
+ const value = await verifyCandidate(group.entries);
101
+ return {
102
+ outcome: "verified",
103
+ value,
104
+ entries: group.entries,
105
+ provenance: { answeredBy: group.url, responses, agreed: false, fromCache: false }
106
+ };
107
+ } catch (err) {
108
+ if (!(err instanceof StaleAnswerError)) lastVerifyError = err;
109
+ badDigests.add(digest);
110
+ }
111
+ }
112
+ if (lastVerifyError !== void 0) {
113
+ const message = lastVerifyError instanceof Error ? lastVerifyError.message : "unknown error";
114
+ throw new Error(`all candidate logs failed verification: ${message}`);
115
+ }
116
+ return {
117
+ outcome: "unreachable",
118
+ entries: [],
119
+ provenance: { answeredBy: "", responses, agreed: false, fromCache: false }
120
+ };
121
+ };
122
+ var identityPager = (peerClient, did) => (url, params) => peerClient.getIdentityLog(url, did, params);
123
+ var contentPager = (peerClient, contentId) => (url, params) => peerClient.getContentLog(url, contentId, params);
124
+ var operationPager = (peerClient) => (url, params) => peerClient.getOperationLog(url, params);
125
+
126
+ // src/index-query.ts
127
+ var INDEX_BASE_PATH = "/index/v0";
128
+ var setParam = (url, key, value) => {
129
+ if (value === void 0) return;
130
+ url.searchParams.set(key, String(value));
131
+ };
132
+ var fetchIndexPage = async (relays, fetchImpl, build, empty, throwOnDecline = false) => {
133
+ for (const url of relays) {
134
+ try {
135
+ const res = await fetchImpl(build(url).toString());
136
+ if (!res.ok) continue;
137
+ return await res.json();
138
+ } catch {
139
+ continue;
140
+ }
141
+ }
142
+ if (throwOnDecline) throw new Error("no index-capable relay served the requested route");
143
+ return empty;
144
+ };
145
+ var createIndexQueries = (relays, fetchImpl) => {
146
+ const relaysFor = (o) => normalizeRelays(o?.relays ?? relays);
147
+ const indexIdentities = (params, options) => fetchIndexPage(
148
+ relaysFor(options),
149
+ fetchImpl,
150
+ (base) => {
151
+ const url = new URL(`${INDEX_BASE_PATH}/identities`, base);
152
+ setParam(url, "did", params?.did);
153
+ setParam(url, "hasPublicProfile", params?.hasPublicProfile);
154
+ setParam(url, "nameContains", params?.nameContains);
155
+ setParam(url, "order", params?.order);
156
+ setParam(url, "after", params?.after);
157
+ setParam(url, "limit", params?.limit);
158
+ return url;
159
+ },
160
+ { identities: [], next: null }
161
+ );
162
+ const indexContent = (params, options) => fetchIndexPage(
163
+ relaysFor(options),
164
+ fetchImpl,
165
+ (base) => {
166
+ const url = new URL(`${INDEX_BASE_PATH}/content`, base);
167
+ setParam(url, "contentId", params?.contentId);
168
+ setParam(url, "creator", params?.creator);
169
+ setParam(url, "signer", params?.signer);
170
+ setParam(url, "docSchema", params?.docSchema);
171
+ setParam(url, "documentCID", params?.documentCID);
172
+ setParam(url, "publicRead", params?.publicRead);
173
+ setParam(url, "isDeleted", params?.isDeleted);
174
+ setParam(url, "titleContains", params?.titleContains);
175
+ setParam(url, "order", params?.order);
176
+ setParam(url, "after", params?.after);
177
+ setParam(url, "limit", params?.limit);
178
+ return url;
179
+ },
180
+ { content: [], next: null }
181
+ );
182
+ const indexCountersignatures = (witness, params, options) => fetchIndexPage(
183
+ relaysFor(options),
184
+ fetchImpl,
185
+ (base) => {
186
+ const url = new URL(`${INDEX_BASE_PATH}/countersignatures`, base);
187
+ setParam(url, "witness", witness);
188
+ setParam(url, "relation", params?.relation);
189
+ setParam(url, "order", params?.order);
190
+ setParam(url, "after", params?.after);
191
+ setParam(url, "limit", params?.limit);
192
+ return url;
193
+ },
194
+ { witness, countersignatures: [], next: null }
195
+ );
196
+ const indexCredentials = (params, options) => fetchIndexPage(
197
+ relaysFor(options),
198
+ fetchImpl,
199
+ (base) => {
200
+ const url = new URL(`${INDEX_BASE_PATH}/credentials`, base);
201
+ setParam(url, "issuer", params?.issuer);
202
+ setParam(url, "resource", params?.resource);
203
+ setParam(url, "action", params?.action);
204
+ setParam(url, "after", params?.after);
205
+ setParam(url, "limit", params?.limit);
206
+ return url;
207
+ },
208
+ { credentials: [], next: null },
209
+ // route is newer than the `index` capability flag: throw on all-declined so a
210
+ // relay that advertises index but predates /credentials makes the caller fall
211
+ // back to its local fold instead of rendering a false-empty.
212
+ true
213
+ );
214
+ const capabilities = async (options) => {
215
+ const merged = { index: false };
216
+ for (const url of relaysFor(options)) {
217
+ try {
218
+ const res = await fetchImpl(new URL("/.well-known/dfos-relay", url).toString());
219
+ if (!res.ok) continue;
220
+ const body = await res.json();
221
+ if (body.capabilities?.["index"] === true) merged.index = true;
222
+ } catch {
223
+ continue;
224
+ }
225
+ }
226
+ return merged;
227
+ };
228
+ return { indexIdentities, indexContent, indexCountersignatures, indexCredentials, capabilities };
229
+ };
230
+
231
+ // src/resolvers.ts
232
+ import {
233
+ decodeMultikey,
234
+ verifyContentChain,
235
+ verifyContentExtensionFromTrustedState,
236
+ verifyIdentityChain,
237
+ verifyIdentityExtensionFromTrustedState
238
+ } from "@metalabel/dfos-protocol/chain";
239
+ import { decodeJwsUnsafe } from "@metalabel/dfos-protocol/crypto";
240
+ var DID_PREFIX = "did:dfos";
241
+ var opMeta = (jws) => {
242
+ const decoded = decodeJwsUnsafe(jws);
243
+ const cid = typeof decoded?.header.cid === "string" ? decoded.header.cid : "";
244
+ const createdAt = typeof decoded?.payload?.["createdAt"] === "string" ? (decoded?.payload)["createdAt"] : "";
245
+ return { cid, createdAt };
246
+ };
247
+ var allCurrentKeys = (state) => [
248
+ ...state.authKeys,
249
+ ...state.assertKeys,
250
+ ...state.controllerKeys
251
+ ];
252
+ var mergeHistoricalIdentity = (state, log) => {
253
+ const maps = {
254
+ authKeys: new Map(state.authKeys.map((k) => [k.id, k])),
255
+ assertKeys: new Map(state.assertKeys.map((k) => [k.id, k])),
256
+ controllerKeys: new Map(state.controllerKeys.map((k) => [k.id, k]))
257
+ };
258
+ for (const jws of log) {
259
+ const decoded = decodeJwsUnsafe(jws);
260
+ if (!decoded) continue;
261
+ const payload = decoded.payload;
262
+ if (payload["type"] !== "create" && payload["type"] !== "update") continue;
263
+ for (const field of ["authKeys", "assertKeys", "controllerKeys"]) {
264
+ const keys = payload[field];
265
+ if (!Array.isArray(keys)) continue;
266
+ for (const k of keys) {
267
+ if (k && typeof k === "object" && "id" in k && "publicKeyMultibase" in k) {
268
+ const id = k.id;
269
+ if (!maps[field].has(id)) maps[field].set(id, k);
270
+ }
271
+ }
272
+ }
273
+ }
274
+ return {
275
+ ...state,
276
+ authKeys: [...maps.authKeys.values()],
277
+ assertKeys: [...maps.assertKeys.values()],
278
+ controllerKeys: [...maps.controllerKeys.values()]
279
+ };
280
+ };
281
+ var keyBytesFor = (state, log, keyId) => {
282
+ const current = allCurrentKeys(state).find((k) => k.id === keyId);
283
+ if (current) return decodeMultikey(current.publicKeyMultibase).keyBytes;
284
+ for (const jws of log) {
285
+ const decoded = decodeJwsUnsafe(jws);
286
+ if (!decoded) continue;
287
+ const payload = decoded.payload;
288
+ if (payload["type"] !== "create" && payload["type"] !== "update") continue;
289
+ for (const field of ["authKeys", "assertKeys", "controllerKeys"]) {
290
+ const keys = payload[field];
291
+ if (!Array.isArray(keys)) continue;
292
+ for (const k of keys) {
293
+ if (k && typeof k === "object" && "id" in k && k.id === keyId && "publicKeyMultibase" in k) {
294
+ return decodeMultikey(k.publicKeyMultibase).keyBytes;
295
+ }
296
+ }
297
+ }
298
+ }
299
+ return null;
300
+ };
301
+ var createResolvers = (deps) => {
302
+ const relaysFor = (o) => normalizeRelays(o?.relays ?? deps.relays);
303
+ const getIdentityChain = async (did, options) => {
304
+ const key = `identity:${did}`;
305
+ const cached = options?.fresh ? void 0 : await deps.store.get(key);
306
+ const verifyCandidate = async (entries) => {
307
+ if (!cached) {
308
+ const log2 = entries.map((e) => e.jwsToken);
309
+ if (log2.length === 0) throw new Error(`identity not found: ${did}`);
310
+ const state2 = await verifyIdentityChain({ didPrefix: DID_PREFIX, log: log2 });
311
+ if (state2.did !== did) {
312
+ throw new Error(`relay served a mismatched identity: asked ${did}, got ${state2.did}`);
313
+ }
314
+ const last = opMeta(log2[log2.length - 1]);
315
+ return { state: state2, log: log2, headCID: last.cid, lastCreatedAt: last.createdAt };
316
+ }
317
+ if (cached.log.some((jws, index) => index < entries.length && entries[index].jwsToken !== jws)) {
318
+ throw new Error(`identity log diverges from the verified cached prefix: ${did}`);
319
+ }
320
+ if (entries.length < cached.log.length) {
321
+ throw new StaleAnswerError(`identity log is behind the verified cached prefix: ${did}`);
322
+ }
323
+ let state = cached.state;
324
+ let headCID = cached.headCID;
325
+ let lastCreatedAt = cached.lastCreatedAt;
326
+ const log = [...cached.log];
327
+ for (const entry of entries.slice(cached.log.length)) {
328
+ const r = await verifyIdentityExtensionFromTrustedState({
329
+ currentState: state,
330
+ headCID,
331
+ lastCreatedAt,
332
+ newOp: entry.jwsToken
333
+ });
334
+ state = r.state;
335
+ headCID = r.operationCID;
336
+ lastCreatedAt = r.createdAt;
337
+ log.push(entry.jwsToken);
338
+ }
339
+ return { state, log, headCID, lastCreatedAt };
340
+ };
341
+ const fetched = await fanOutLog(
342
+ identityPager(deps.peerClient, did),
343
+ relaysFor(options),
344
+ deps.quorum,
345
+ verifyCandidate
346
+ );
347
+ if (fetched.outcome === "unreachable") {
348
+ if (cached) {
349
+ return {
350
+ state: cached.state,
351
+ log: cached.log,
352
+ provenance: { ...fetched.provenance, fromCache: true },
353
+ tipUnverified: true
354
+ };
355
+ }
356
+ throw new Error(`identity not found on any relay: ${did}`);
357
+ }
358
+ const candidate = fetched.value;
359
+ if (fetched.provenance.agreed && (!cached || candidate.log.length > cached.log.length)) {
360
+ await deps.store.set(key, {
361
+ log: candidate.log,
362
+ state: candidate.state,
363
+ headCID: candidate.headCID,
364
+ lastCreatedAt: candidate.lastCreatedAt
365
+ });
366
+ }
367
+ return {
368
+ state: candidate.state,
369
+ log: candidate.log,
370
+ provenance: fetched.provenance,
371
+ // an unchanged full answer is still a relay CLAIM of freshness, not proof
372
+ tipUnverified: cached !== void 0 && candidate.log.length === cached.log.length
373
+ };
374
+ };
375
+ const resolveIdentity = async (did) => {
376
+ try {
377
+ const { state, log } = await getIdentityChain(did);
378
+ return mergeHistoricalIdentity(state, log);
379
+ } catch {
380
+ return void 0;
381
+ }
382
+ };
383
+ const resolveKey = async (kid) => {
384
+ const hashIdx = kid.indexOf("#");
385
+ if (hashIdx < 0) throw new Error(`kid must be a DID URL: ${kid}`);
386
+ const did = kid.substring(0, hashIdx);
387
+ const keyId = kid.substring(hashIdx + 1);
388
+ const { state, log } = await getIdentityChain(did);
389
+ const bytes = keyBytesFor(state, log, keyId);
390
+ if (!bytes) throw new Error(`unknown key ${keyId} on identity ${did}`);
391
+ return bytes;
392
+ };
393
+ const callbacks = () => ({
394
+ resolveKey,
395
+ resolveIdentity,
396
+ isRevoked: deps.isRevoked
397
+ });
398
+ const getContentChain = async (contentId, options) => {
399
+ const key = `content:${contentId}`;
400
+ const cached = options?.fresh ? void 0 : await deps.store.get(key);
401
+ const verifyCandidate = async (entries) => {
402
+ if (!cached) {
403
+ const log2 = entries.map((e) => e.jwsToken);
404
+ if (log2.length === 0) throw new Error(`content not found: ${contentId}`);
405
+ const state2 = await verifyContentChain({
406
+ log: log2,
407
+ resolveKey,
408
+ enforceAuthorization: true,
409
+ resolveIdentity,
410
+ isRevoked: deps.isRevoked
411
+ });
412
+ if (state2.contentId !== contentId) {
413
+ throw new Error(
414
+ `relay served a mismatched content chain: asked ${contentId}, got ${state2.contentId}`
415
+ );
416
+ }
417
+ const last = opMeta(log2[log2.length - 1]);
418
+ return { state: state2, log: log2, headCID: last.cid, lastCreatedAt: last.createdAt };
419
+ }
420
+ if (cached.log.some((jws, index) => index < entries.length && entries[index].jwsToken !== jws)) {
421
+ throw new Error(`content log diverges from the verified cached prefix: ${contentId}`);
422
+ }
423
+ if (entries.length < cached.log.length) {
424
+ throw new StaleAnswerError(
425
+ `content log is behind the verified cached prefix: ${contentId}`
426
+ );
427
+ }
428
+ let state = cached.state;
429
+ let lastCreatedAt = cached.lastCreatedAt;
430
+ const log = [...cached.log];
431
+ for (const entry of entries.slice(cached.log.length)) {
432
+ const r = await verifyContentExtensionFromTrustedState({
433
+ currentState: state,
434
+ lastCreatedAt,
435
+ newOp: entry.jwsToken,
436
+ resolveKey,
437
+ enforceAuthorization: true,
438
+ resolveIdentity,
439
+ isRevoked: deps.isRevoked
440
+ });
441
+ state = r.state;
442
+ lastCreatedAt = r.createdAt;
443
+ log.push(entry.jwsToken);
444
+ }
445
+ return { state, log, headCID: state.headCID, lastCreatedAt };
446
+ };
447
+ const fetched = await fanOutLog(
448
+ contentPager(deps.peerClient, contentId),
449
+ relaysFor(options),
450
+ deps.quorum,
451
+ verifyCandidate
452
+ );
453
+ if (fetched.outcome === "unreachable") {
454
+ if (cached) {
455
+ return {
456
+ state: cached.state,
457
+ log: cached.log,
458
+ provenance: { ...fetched.provenance, fromCache: true },
459
+ tipUnverified: true
460
+ };
461
+ }
462
+ throw new Error(`content not found on any relay: ${contentId}`);
463
+ }
464
+ const candidate = fetched.value;
465
+ if (fetched.provenance.agreed && (!cached || candidate.log.length > cached.log.length)) {
466
+ await deps.store.set(key, {
467
+ log: candidate.log,
468
+ state: candidate.state,
469
+ headCID: candidate.headCID,
470
+ lastCreatedAt: candidate.lastCreatedAt
471
+ });
472
+ }
473
+ return {
474
+ state: candidate.state,
475
+ log: candidate.log,
476
+ provenance: fetched.provenance,
477
+ tipUnverified: cached !== void 0 && candidate.log.length === cached.log.length
478
+ };
479
+ };
480
+ return { getIdentityChain, getContentChain, callbacks };
481
+ };
482
+
483
+ // src/revocation.ts
484
+ import { parseProtocolTimestampUnix, verifyRevocation } from "@metalabel/dfos-protocol/chain";
485
+ import { REVOCATIONS_BASE_PATH } from "@metalabel/dfos-web-relay/peer-client";
486
+ var createRevocationChecker = (relays, fetchImpl, resolveKey) => {
487
+ const relaySet = normalizeRelays(relays);
488
+ return async (issuerDID, credentialCID, asOfUnix) => {
489
+ for (const url of relaySet) {
490
+ let body = null;
491
+ try {
492
+ const target = new URL(
493
+ `${REVOCATIONS_BASE_PATH}/credential/${encodeURIComponent(credentialCID)}`,
494
+ url
495
+ ).toString();
496
+ const res = await fetchImpl(target);
497
+ if (res.status === 501) continue;
498
+ if (!res.ok) continue;
499
+ body = await res.json();
500
+ } catch {
501
+ continue;
502
+ }
503
+ if (!body?.revoked || !body.revocation) continue;
504
+ try {
505
+ const verified = await verifyRevocation({ jwsToken: body.revocation, resolveKey });
506
+ if (verified.did === issuerDID && verified.credentialCID === credentialCID) {
507
+ if (asOfUnix === void 0 || asOfUnix <= 0) return true;
508
+ const revokedAtUnix = parseProtocolTimestampUnix(verified.createdAt);
509
+ if (revokedAtUnix === null || revokedAtUnix <= asOfUnix) return true;
510
+ continue;
511
+ }
512
+ } catch {
513
+ }
514
+ }
515
+ return false;
516
+ };
517
+ };
518
+
519
+ // src/client.ts
520
+ var DEFAULT_TIMEOUT_MS = 1e4;
521
+ var RETRY_BACKOFF_MS = 250;
522
+ var isAbortError = (err) => err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError");
523
+ var delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
524
+ var withFetchPolicy = (base, timeoutMs) => {
525
+ const attempt = (input, init) => {
526
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
527
+ const signal = init?.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
528
+ return base(input, { ...init, signal });
529
+ };
530
+ return async (input, init) => {
531
+ try {
532
+ const res = await attempt(input, init);
533
+ if (res.status >= 500 && res.status !== 501) {
534
+ await delay(RETRY_BACKOFF_MS);
535
+ return await attempt(input, init);
536
+ }
537
+ return res;
538
+ } catch (err) {
539
+ if (isAbortError(err)) throw err;
540
+ await delay(RETRY_BACKOFF_MS);
541
+ return attempt(input, init);
542
+ }
543
+ };
544
+ };
545
+ var trust = (ok, axes) => axes.length > 0 ? { ok, unverifiable: axes } : { ok };
546
+ var hasDelegatedOps = (log) => log.some((jws) => {
547
+ const decoded = decodeJwsUnsafe2(jws);
548
+ return typeof decoded?.payload?.["authorization"] === "string";
549
+ });
550
+ var opMeta2 = (jws) => {
551
+ const decoded = decodeJwsUnsafe2(jws);
552
+ return { cid: typeof decoded?.header.cid === "string" ? decoded.header.cid : "", jwsToken: jws };
553
+ };
554
+ var createClient = (config) => {
555
+ const relays = normalizeRelays(config.relays);
556
+ if (relays.length === 0) throw new Error("createClient requires at least one relay");
557
+ const quorum = config.quorum ?? 1;
558
+ const store = config.store ?? memoryStore();
559
+ const timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
560
+ const baseFetch = config.fetch ?? ((input, init) => globalThis.fetch(input, init));
561
+ const fetchImpl = withFetchPolicy(baseFetch, timeoutMs);
562
+ const nowMs = config.now ?? Date.now;
563
+ const peerClient = config.peerClient ?? createHttpPeerClient({ fetch: fetchImpl });
564
+ let isRevokedImpl = async () => false;
565
+ const isRevoked = (issuerDID, credentialCID, asOfUnix) => isRevokedImpl(issuerDID, credentialCID, asOfUnix);
566
+ const resolvers2 = createResolvers({ relays, quorum, store, peerClient, isRevoked });
567
+ isRevokedImpl = config.isRevoked ?? createRevocationChecker(relays, fetchImpl, (kid) => resolvers2.callbacks().resolveKey(kid));
568
+ const relaysFor = (o) => normalizeRelays(o?.relays ?? relays);
569
+ const identity = async (did, options) => {
570
+ const { state, provenance, tipUnverified } = await resolvers2.getIdentityChain(did, options);
571
+ const axes = tipUnverified ? ["tip"] : [];
572
+ return { value: state, trust: trust(true, axes), provenance };
573
+ };
574
+ const content = async (contentId, options) => {
575
+ const { state, log: log2, provenance, tipUnverified } = await resolvers2.getContentChain(
576
+ contentId,
577
+ options
578
+ );
579
+ const creator = await resolvers2.getIdentityChain(state.creatorDID, options);
580
+ const axes = [];
581
+ if (tipUnverified) axes.push("tip");
582
+ if (hasDelegatedOps(log2)) axes.push("revocation");
583
+ const value = { chain: state, creator: creator.state };
584
+ return { value, trust: trust(true, axes), provenance };
585
+ };
586
+ const credential = async (jws, options) => {
587
+ const decoded = decodeJwsUnsafe2(jws);
588
+ const iss = decoded?.payload?.["iss"];
589
+ if (typeof iss !== "string") throw new Error("credential payload missing iss");
590
+ const issuer = await resolvers2.getIdentityChain(iss, options);
591
+ const verified = await verifyDFOSCredential(jws, {
592
+ resolveIdentity: resolvers2.callbacks().resolveIdentity,
593
+ now: Math.floor(nowMs() / 1e3)
594
+ });
595
+ const revoked = await isRevoked(verified.iss, verified.credentialCID);
596
+ const axes = [];
597
+ if (issuer.tipUnverified) axes.push("tip");
598
+ if (!revoked) axes.push("revocation");
599
+ const value = { credential: verified, issuer: issuer.state, revoked };
600
+ return { value, trust: trust(!revoked, axes), provenance: issuer.provenance };
601
+ };
602
+ const document = async (contentId, options) => {
603
+ const { state, provenance, tipUnverified } = await resolvers2.getContentChain(
604
+ contentId,
605
+ options
606
+ );
607
+ const documentCID = state.currentDocumentCID;
608
+ if (!documentCID) throw new Error(`content ${contentId} has no current document`);
609
+ const blob = await fetchBlob(contentId, relaysFor(options), fetchImpl);
610
+ if (!blob) throw new Error(`no relay served the blob for ${contentId}`);
611
+ let decodedDoc;
612
+ let integrity = false;
613
+ try {
614
+ decodedDoc = JSON.parse(new TextDecoder().decode(blob.bytes));
615
+ const encoded = await dagCborCanonicalEncode(decodedDoc);
616
+ integrity = encoded.cid.toString() === documentCID;
617
+ } catch {
618
+ integrity = false;
619
+ }
620
+ const value = {
621
+ bytes: blob.bytes,
622
+ documentCID,
623
+ integrity,
624
+ ...blob.mediaType ? { mediaType: blob.mediaType } : {},
625
+ ...decodedDoc !== void 0 ? { decoded: decodedDoc } : {}
626
+ };
627
+ const axes = tipUnverified ? ["tip"] : [];
628
+ return { value, trust: trust(integrity, axes), provenance };
629
+ };
630
+ const verify = async (jws) => {
631
+ try {
632
+ const decoded = decodeJwsUnsafe2(jws);
633
+ if (!decoded) return { ok: false, error: "failed to decode JWS" };
634
+ const typ = decoded.header.typ;
635
+ const cb = resolvers2.callbacks();
636
+ if (typ === "did:dfos:credential") {
637
+ const verified = await verifyDFOSCredential(jws, {
638
+ resolveIdentity: cb.resolveIdentity,
639
+ now: Math.floor(nowMs() / 1e3)
640
+ });
641
+ const revoked = await isRevoked(verified.iss, verified.credentialCID);
642
+ if (revoked) return { ok: false, error: "credential revoked", value: verified };
643
+ return { ok: true, value: verified, unverifiable: ["revocation"] };
644
+ }
645
+ if (typ === "did:dfos:revocation") {
646
+ const verified = await verifyRevocation2({ jwsToken: jws, resolveKey: cb.resolveKey });
647
+ return { ok: true, value: verified };
648
+ }
649
+ return { ok: false, error: `unsupported token type for standalone verify: ${typ}` };
650
+ } catch (err) {
651
+ return { ok: false, error: err instanceof Error ? err.message : "verification failed" };
652
+ }
653
+ };
654
+ const resolve = async (ref, options) => {
655
+ const trimmed = ref.trim();
656
+ if (trimmed.startsWith("did:dfos:") && !trimmed.includes(".")) {
657
+ const r = await identity(trimmed, options);
658
+ return { kind: "identity", ...r };
659
+ }
660
+ if (CONTENT_ID_ANCHOR_RE.test(trimmed)) {
661
+ const r = await content(trimmed, options);
662
+ return { kind: "content", ...r };
663
+ }
664
+ if (trimmed.split(".").length === 3) {
665
+ const decoded = decodeJwsUnsafe2(trimmed);
666
+ const typ = decoded?.header.typ;
667
+ if (typ === "did:dfos:credential") {
668
+ const r = await credential(trimmed, options);
669
+ return { kind: "credential", ...r };
670
+ }
671
+ throw new Error(`unsupported JWS type for resolve: ${typ}`);
672
+ }
673
+ throw new Error(`unrecognized reference: ${ref}`);
674
+ };
675
+ const log = async (kind, id, options) => {
676
+ const res = kind === "identity" ? await resolvers2.getIdentityChain(id, options) : await resolvers2.getContentChain(id, options);
677
+ const value = res.log.map(opMeta2);
678
+ const axes = res.tipUnverified ? ["tip"] : [];
679
+ return { value, trust: trust(true, axes), provenance: res.provenance };
680
+ };
681
+ const globalLog = async (after, options) => {
682
+ const pager = operationPager(peerClient);
683
+ const limit = Math.max(1, Math.min(1e3, Math.floor(options?.limit ?? 100)));
684
+ for (const url of relaysFor(options)) {
685
+ let page = null;
686
+ try {
687
+ page = await pager(url, after ? { after, limit } : { limit });
688
+ } catch {
689
+ page = null;
690
+ }
691
+ if (page === "invalid-cursor") {
692
+ return "invalid-cursor";
693
+ }
694
+ if (page === null) continue;
695
+ const provenance = {
696
+ answeredBy: url,
697
+ responses: [{ url, ok: true, digest: "" }],
698
+ agreed: true,
699
+ fromCache: false
700
+ };
701
+ return { entries: page.entries, next: page.next, provenance };
702
+ }
703
+ return {
704
+ entries: [],
705
+ next: null,
706
+ provenance: { answeredBy: "", responses: [], agreed: false, fromCache: false }
707
+ };
708
+ };
709
+ const health = async (options) => {
710
+ const out = [];
711
+ for (const url of relaysFor(options)) {
712
+ try {
713
+ const res = await fetchImpl(new URL("/.well-known/dfos-relay", url).toString());
714
+ if (!res.ok) {
715
+ out.push({ url, ok: false });
716
+ continue;
717
+ }
718
+ const body = await res.json();
719
+ out.push({ url, ok: true, ...body });
720
+ } catch {
721
+ out.push({ url, ok: false });
722
+ }
723
+ }
724
+ return out;
725
+ };
726
+ const callbacks = () => resolvers2.callbacks();
727
+ const { indexIdentities, indexContent, indexCountersignatures, indexCredentials, capabilities } = createIndexQueries(relays, fetchImpl);
728
+ return {
729
+ callbacks,
730
+ resolve,
731
+ identity,
732
+ content,
733
+ credential,
734
+ document,
735
+ verify,
736
+ log,
737
+ globalLog,
738
+ health,
739
+ capabilities,
740
+ indexIdentities,
741
+ indexContent,
742
+ indexCountersignatures,
743
+ indexCredentials
744
+ };
745
+ };
746
+ var fetchBlob = async (contentId, relays, fetchImpl) => {
747
+ for (const url of relays) {
748
+ try {
749
+ const target = new URL(`/content/${encodeURIComponent(contentId)}/blob`, url).toString();
750
+ const res = await fetchImpl(target);
751
+ if (!res.ok) continue;
752
+ const bytes = new Uint8Array(await res.arrayBuffer());
753
+ return { bytes, mediaType: res.headers.get("content-type") };
754
+ } catch {
755
+ continue;
756
+ }
757
+ }
758
+ return null;
759
+ };
760
+ var resolvers = (relays) => createClient({ relays }).callbacks();
761
+ export {
762
+ createClient,
763
+ createRevocationChecker,
764
+ memoryStore,
765
+ resolvers
766
+ };