@metalabel/dfos-protocol 0.37.0 → 0.38.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.
@@ -0,0 +1,672 @@
1
+ import {
2
+ assertJwsProfile,
3
+ base64urlDecode,
4
+ base64urlEncode,
5
+ createJws,
6
+ dagCborCanonicalEncode,
7
+ decodeJwsUnsafe,
8
+ sha256,
9
+ verifyJws
10
+ } from "./chunk-4LG2GEB2.js";
11
+
12
+ // src/credentials/schemas.ts
13
+ import { z } from "zod";
14
+ var MAX_ATT = 32;
15
+ var MAX_PRF = 1;
16
+ var MAX_CREDENTIAL_SIZE = 262144;
17
+ var Attenuation = z.looseObject({
18
+ resource: z.string().min(1),
19
+ action: z.string().min(1)
20
+ });
21
+ var DFOSCredentialPayload = z.looseObject({
22
+ version: z.literal(1),
23
+ type: z.literal("DFOSCredential"),
24
+ /** Issuer DID */
25
+ iss: z.string().min(1),
26
+ /** Audience DID or "*" for public credentials */
27
+ aud: z.string().min(1),
28
+ /** Attenuations — resource + action pairs */
29
+ att: z.array(Attenuation).min(1).max(MAX_ATT),
30
+ /** Parent credential JWS tokens (for delegation chains) */
31
+ prf: z.array(z.string()).max(MAX_PRF).default([]),
32
+ /** Expiration — unix seconds */
33
+ exp: z.number().int().positive(),
34
+ /** Issued at — unix seconds */
35
+ iat: z.number().int().positive()
36
+ });
37
+
38
+ // src/chain/multikey.ts
39
+ import { base58btc } from "multiformats/bases/base58";
40
+ var ED25519_PUB_PREFIX = new Uint8Array([237, 1]);
41
+ var ED25519_PRIV_PREFIX = new Uint8Array([128, 38]);
42
+ var ED25519_PUB_MULTICODEC = 237;
43
+ var ED25519_PRIV_MULTICODEC = 4864;
44
+ var encodeEd25519Multikey = (publicKeyBytes) => {
45
+ if (publicKeyBytes.length !== 32) {
46
+ throw new Error(`expected 32-byte Ed25519 public key, got ${publicKeyBytes.length}`);
47
+ }
48
+ const prefixed = new Uint8Array(ED25519_PUB_PREFIX.length + publicKeyBytes.length);
49
+ prefixed.set(ED25519_PUB_PREFIX);
50
+ prefixed.set(publicKeyBytes, ED25519_PUB_PREFIX.length);
51
+ return base58btc.encode(prefixed);
52
+ };
53
+ var decodeMultikey = (multibase) => {
54
+ const bytes = base58btc.decode(multibase);
55
+ if (bytes.length < 2) {
56
+ throw new Error("multikey too short");
57
+ }
58
+ if (bytes[0] === ED25519_PUB_PREFIX[0] && bytes[1] === ED25519_PUB_PREFIX[1]) {
59
+ const keyBytes = bytes.slice(2);
60
+ if (keyBytes.length !== 32) {
61
+ throw new Error(`expected 32-byte Ed25519 public key, got ${keyBytes.length}`);
62
+ }
63
+ return { keyBytes, codec: ED25519_PUB_MULTICODEC };
64
+ }
65
+ if (bytes[0] === ED25519_PRIV_PREFIX[0] && bytes[1] === ED25519_PRIV_PREFIX[1]) {
66
+ const keyBytes = bytes.slice(2);
67
+ if (keyBytes.length !== 32) {
68
+ throw new Error(`expected 32-byte Ed25519 private key, got ${keyBytes.length}`);
69
+ }
70
+ return { keyBytes, codec: ED25519_PRIV_MULTICODEC };
71
+ }
72
+ throw new Error(
73
+ `unsupported multikey codec: [0x${bytes[0]?.toString(16)}, 0x${bytes[1]?.toString(16)}]`
74
+ );
75
+ };
76
+
77
+ // src/credentials/api-auth.ts
78
+ var REQUEST_PROOF_JWS_TYP = "did:dfos:request-proof";
79
+ var IDENTITY_PROOF_JWS_TYP = "did:dfos:identity-proof";
80
+ var EMPTY_BODY_SHA256 = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU";
81
+ var MAX_REQUEST_PROOF_SIZE = 4096;
82
+ var DEFAULT_PROOF_WINDOW_SECONDS = 60;
83
+ var DEFAULT_PROOF_SKEW_SECONDS = 60;
84
+ var MAX_PROOF_FRESHNESS_SPAN_SECONDS = 300;
85
+ var MAX_BODY_BYTES = 1048576;
86
+ var DFOS_AUTH_SCHEME = "DFOS";
87
+ var REQUEST_PROOF_SHAPE = {
88
+ label: "request proof",
89
+ typ: REQUEST_PROOF_JWS_TYP,
90
+ credentialed: true
91
+ };
92
+ var IDENTITY_PROOF_SHAPE = {
93
+ label: "identity proof",
94
+ typ: IDENTITY_PROOF_JWS_TYP,
95
+ credentialed: false
96
+ };
97
+ var encoder = new TextEncoder();
98
+ var EMPTY_BODY = new Uint8Array(0);
99
+ var UPPERCASE_METHOD = /^[A-Z0-9!#$%&'*+.^_`|~-]+$/;
100
+ var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
101
+ var CTL_OR_SPACE = /[\u0000-\u0020\u007f]/;
102
+ var BASE64URL_32 = /^[A-Za-z0-9_-]{43}$/;
103
+ var EXTRA_MEMBER_NAME = /^[A-Za-z0-9_.-]+$/;
104
+ var CANONICAL_MEMBERS = /* @__PURE__ */ new Set(["method", "host", "path", "bodyHash", "credentialCID", "iat"]);
105
+ var assertNoLoneSurrogate = (value, field, label) => {
106
+ if (LONE_SURROGATE.test(value)) {
107
+ throw new Error(`invalid ${label}: ${field} must be well-formed Unicode`);
108
+ }
109
+ };
110
+ var validateProofPayload = (value, shape) => {
111
+ const label = shape.label;
112
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
113
+ throw new Error(`invalid ${label}: expected a JSON object`);
114
+ }
115
+ const raw = value;
116
+ const stringFields = ["method", "host", "path", "bodyHash"];
117
+ if (shape.credentialed) stringFields.push("credentialCID");
118
+ for (const field of stringFields) {
119
+ if (typeof raw[field] !== "string" || raw[field] === "") {
120
+ throw new Error(`invalid ${label}: ${field} must be a non-empty string`);
121
+ }
122
+ assertNoLoneSurrogate(raw[field], field, label);
123
+ }
124
+ const method = raw["method"];
125
+ if (!UPPERCASE_METHOD.test(method)) {
126
+ throw new Error(`invalid ${label}: method must be an uppercase HTTP method token`);
127
+ }
128
+ const host = raw["host"];
129
+ if (host !== host.toLowerCase() || /[\s/\\?#]/.test(host)) {
130
+ throw new Error(`invalid ${label}: host must be a lowercase authority, without a scheme`);
131
+ }
132
+ const path = raw["path"];
133
+ if (!path.startsWith("/")) {
134
+ throw new Error(`invalid ${label}: path must begin with /`);
135
+ }
136
+ if (path.includes("#")) {
137
+ throw new Error(`invalid ${label}: path must not carry a fragment`);
138
+ }
139
+ if (CTL_OR_SPACE.test(path)) {
140
+ throw new Error(`invalid ${label}: path must not contain whitespace or control characters`);
141
+ }
142
+ const bodyHash = raw["bodyHash"];
143
+ if (!BASE64URL_32.test(bodyHash) || base64urlEncode(base64urlDecode(bodyHash)) !== bodyHash) {
144
+ throw new Error(
145
+ `invalid ${label}: bodyHash must be the canonical unpadded base64url of 32 bytes`
146
+ );
147
+ }
148
+ const iat = raw["iat"];
149
+ if (typeof iat !== "number" || !Number.isSafeInteger(iat) || iat <= 0) {
150
+ throw new Error(`invalid ${label}: iat must be a positive integer`);
151
+ }
152
+ return shape.credentialed ? { method, host, path, bodyHash, credentialCID: raw["credentialCID"], iat } : { method, host, path, bodyHash, iat };
153
+ };
154
+ var validateRequestProofPayload = (value) => {
155
+ const parsed = validateProofPayload(value, REQUEST_PROOF_SHAPE);
156
+ return {
157
+ method: parsed.method,
158
+ host: parsed.host,
159
+ path: parsed.path,
160
+ bodyHash: parsed.bodyHash,
161
+ credentialCID: parsed.credentialCID,
162
+ iat: parsed.iat
163
+ };
164
+ };
165
+ var validateIdentityProofPayload = (value) => {
166
+ const parsed = validateProofPayload(value, IDENTITY_PROOF_SHAPE);
167
+ return {
168
+ method: parsed.method,
169
+ host: parsed.host,
170
+ path: parsed.path,
171
+ bodyHash: parsed.bodyHash,
172
+ iat: parsed.iat
173
+ };
174
+ };
175
+ var canonicalExtraMembers = (extra, label) => {
176
+ if (!extra) return [];
177
+ const entries = [];
178
+ for (const name of Object.keys(extra)) {
179
+ if (!EXTRA_MEMBER_NAME.test(name)) {
180
+ throw new Error(
181
+ `invalid ${label}: additive member name ${JSON.stringify(name)} is not [A-Za-z0-9_.-]+`
182
+ );
183
+ }
184
+ if (CANONICAL_MEMBERS.has(name)) {
185
+ throw new Error(`invalid ${label}: ${name} is a canonical member and cannot be added`);
186
+ }
187
+ const value = extra[name];
188
+ if (typeof value !== "string" || value === "") {
189
+ throw new Error(`invalid ${label}: additive member ${name} must be a non-empty string`);
190
+ }
191
+ assertNoLoneSurrogate(value, name, label);
192
+ entries.push([name, value]);
193
+ }
194
+ entries.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
195
+ return entries;
196
+ };
197
+ var proofSigningInput = (parsed, shape, extra) => encoder.encode(JSON.stringify(proofPayloadObject(parsed, shape, extra)));
198
+ var proofPayloadObject = (parsed, shape, extra) => {
199
+ const out = {
200
+ method: parsed.method,
201
+ host: parsed.host,
202
+ path: parsed.path,
203
+ bodyHash: parsed.bodyHash
204
+ };
205
+ if (shape.credentialed) out["credentialCID"] = parsed.credentialCID;
206
+ out["iat"] = parsed.iat;
207
+ for (const [name, value] of extra) out[name] = value;
208
+ return out;
209
+ };
210
+ var apiRequestSigningInput = (payload, extraMembers) => proofSigningInput(
211
+ validateProofPayload(payload, REQUEST_PROOF_SHAPE),
212
+ REQUEST_PROOF_SHAPE,
213
+ canonicalExtraMembers(extraMembers, REQUEST_PROOF_SHAPE.label)
214
+ );
215
+ var apiIdentitySigningInput = (payload, extraMembers) => proofSigningInput(
216
+ validateProofPayload(payload, IDENTITY_PROOF_SHAPE),
217
+ IDENTITY_PROOF_SHAPE,
218
+ canonicalExtraMembers(extraMembers, IDENTITY_PROOF_SHAPE.label)
219
+ );
220
+ var sha256BodyHash = (body) => base64urlEncode(sha256(body));
221
+ var parseDfosAuthorization = (header) => {
222
+ if (!header) return null;
223
+ const trimmed = header.trim();
224
+ const space = trimmed.search(/\s/);
225
+ if (space < 0) return null;
226
+ if (trimmed.slice(0, space).toLowerCase() !== "dfos") return null;
227
+ const token = trimmed.slice(space).trim();
228
+ if (token === "" || /\s/.test(token)) return null;
229
+ return token;
230
+ };
231
+ var signApiRequest = async (input) => {
232
+ const payload = validateRequestProofPayload({
233
+ method: input.method,
234
+ host: input.host,
235
+ path: input.path,
236
+ bodyHash: sha256BodyHash(input.body ?? EMPTY_BODY),
237
+ credentialCID: input.credentialCID,
238
+ iat: input.iat ?? Math.floor(Date.now() / 1e3)
239
+ });
240
+ if (!input.kid.includes("#")) {
241
+ throw new Error("invalid request proof: kid must be a DID URL");
242
+ }
243
+ const extra = canonicalExtraMembers(input.extraMembers, REQUEST_PROOF_SHAPE.label);
244
+ const proof = await createJws({
245
+ header: { alg: "EdDSA", typ: REQUEST_PROOF_JWS_TYP, kid: input.kid },
246
+ payload: proofPayloadObject(payload, REQUEST_PROOF_SHAPE, extra),
247
+ sign: input.sign
248
+ });
249
+ if (proof.length > MAX_REQUEST_PROOF_SIZE) {
250
+ throw new Error(`request proof exceeds max size: ${proof.length} > ${MAX_REQUEST_PROOF_SIZE}`);
251
+ }
252
+ return { proof, payload };
253
+ };
254
+ var signApiIdentityRequest = async (input) => {
255
+ const payload = validateIdentityProofPayload({
256
+ method: input.method,
257
+ host: input.host,
258
+ path: input.path,
259
+ bodyHash: sha256BodyHash(input.body ?? EMPTY_BODY),
260
+ iat: input.iat ?? Math.floor(Date.now() / 1e3)
261
+ });
262
+ if (!input.kid.includes("#")) {
263
+ throw new Error("invalid identity proof: kid must be a DID URL");
264
+ }
265
+ const extra = canonicalExtraMembers(input.extraMembers, IDENTITY_PROOF_SHAPE.label);
266
+ const proof = await createJws({
267
+ header: { alg: "EdDSA", typ: IDENTITY_PROOF_JWS_TYP, kid: input.kid },
268
+ payload: proofPayloadObject(payload, IDENTITY_PROOF_SHAPE, extra),
269
+ sign: input.sign
270
+ });
271
+ if (proof.length > MAX_REQUEST_PROOF_SIZE) {
272
+ throw new Error(`identity proof exceeds max size: ${proof.length} > ${MAX_REQUEST_PROOF_SIZE}`);
273
+ }
274
+ return { proof, payload };
275
+ };
276
+ var buildApiAuthHeaders = (input) => ({
277
+ Authorization: `${DFOS_AUTH_SCHEME} ${input.proof}`,
278
+ "X-Credential": input.credential
279
+ });
280
+ var buildApiIdentityHeaders = (input) => ({
281
+ Authorization: `${DFOS_AUTH_SCHEME} ${input.proof}`
282
+ });
283
+ var ApiRequestVerifyError = class extends Error {
284
+ reason;
285
+ phase;
286
+ /** Recommended HTTP status: 401 proof-invalid, 403 credential-invalid, 503 unverifiable, 500 config. */
287
+ status;
288
+ constructor(reason, phase, status, message) {
289
+ super(message);
290
+ this.name = "ApiRequestVerifyError";
291
+ this.reason = reason;
292
+ this.phase = phase;
293
+ this.status = status;
294
+ }
295
+ };
296
+ var invalidProof = (message) => new ApiRequestVerifyError("invalid", "proof", 401, message);
297
+ var unverifiableProof = (message) => new ApiRequestVerifyError("unverifiable", "proof", 503, message);
298
+ var misconfiguredProof = (message) => new ApiRequestVerifyError("config", "config", 500, message);
299
+ var assertProofVerifierConfig = (input) => {
300
+ const window = input.windowSeconds ?? DEFAULT_PROOF_WINDOW_SECONDS;
301
+ const skew = input.skewSeconds ?? DEFAULT_PROOF_SKEW_SECONDS;
302
+ for (const [name, value] of [
303
+ ["windowSeconds", window],
304
+ ["skewSeconds", skew]
305
+ ]) {
306
+ if (!Number.isSafeInteger(value) || value < 0) {
307
+ throw misconfiguredProof(`${name} must be a non-negative integer`);
308
+ }
309
+ }
310
+ if (window + skew > MAX_PROOF_FRESHNESS_SPAN_SECONDS) {
311
+ throw misconfiguredProof(
312
+ `proof freshness span W + S exceeds ${MAX_PROOF_FRESHNESS_SPAN_SECONDS} seconds: ${window} + ${skew}`
313
+ );
314
+ }
315
+ const maxBodyBytes = input.maxBodyBytes ?? MAX_BODY_BYTES;
316
+ if (!Number.isSafeInteger(maxBodyBytes) || maxBodyBytes < 0) {
317
+ throw misconfiguredProof("maxBodyBytes must be a non-negative integer");
318
+ }
319
+ return { window, skew, maxBodyBytes };
320
+ };
321
+ var verifyProofEnvelope = async (input, shape, resolvePresenter) => {
322
+ const { window, skew, maxBodyBytes } = assertProofVerifierConfig(input);
323
+ if (input.proof.length > MAX_REQUEST_PROOF_SIZE) {
324
+ throw invalidProof(
325
+ `${shape.label} exceeds max size: ${input.proof.length} > ${MAX_REQUEST_PROOF_SIZE}`
326
+ );
327
+ }
328
+ const decoded = decodeJwsUnsafe(input.proof);
329
+ if (!decoded) throw invalidProof(`failed to decode ${shape.label} JWS`);
330
+ const rawHeader = decoded.header;
331
+ if (typeof rawHeader !== "object" || rawHeader === null || Array.isArray(rawHeader)) {
332
+ throw invalidProof(`${shape.label} protected header must be an object`);
333
+ }
334
+ assertJwsProfile(rawHeader, invalidProof);
335
+ if (decoded.header.typ !== shape.typ) {
336
+ throw invalidProof(`invalid typ: expected ${shape.typ}, got ${decoded.header.typ}`);
337
+ }
338
+ const kid = decoded.header.kid;
339
+ if (typeof kid !== "string" || !kid.includes("#")) {
340
+ throw invalidProof(`${shape.label} kid must be a DID URL`);
341
+ }
342
+ const presenterDID = kid.substring(0, kid.indexOf("#"));
343
+ const presenterKeyId = kid.substring(kid.indexOf("#") + 1);
344
+ const payloadSegment = input.proof.split(".")[1];
345
+ if (payloadSegment === void 0) throw invalidProof(`failed to decode ${shape.label} payload`);
346
+ let payload;
347
+ let rawPayload;
348
+ try {
349
+ const source = new TextDecoder("utf-8", { fatal: true }).decode(
350
+ base64urlDecode(payloadSegment)
351
+ );
352
+ const parsed = JSON.parse(source);
353
+ payload = validateProofPayload(parsed, shape);
354
+ rawPayload = parsed;
355
+ } catch (err) {
356
+ throw invalidProof(err instanceof Error ? err.message : `invalid ${shape.label} payload`);
357
+ }
358
+ const now = Math.floor((input.now ? input.now() : Date.now()) / 1e3);
359
+ if (now - payload.iat > window) throw invalidProof(`${shape.label} is stale`);
360
+ if (payload.iat - now > skew) {
361
+ throw invalidProof(`${shape.label} iat is beyond the clock-skew allowance`);
362
+ }
363
+ if (payload.method !== input.method) throw invalidProof(`${shape.label} method mismatch`);
364
+ if (payload.host !== input.host) throw invalidProof(`${shape.label} host mismatch`);
365
+ if (payload.path !== input.path) throw invalidProof(`${shape.label} path mismatch`);
366
+ const body = input.body ?? EMPTY_BODY;
367
+ if (body.length > maxBodyBytes) {
368
+ throw new ApiRequestVerifyError(
369
+ "invalid",
370
+ "proof",
371
+ 413,
372
+ `request body exceeds max size: ${body.length} > ${maxBodyBytes}`
373
+ );
374
+ }
375
+ if (payload.bodyHash !== sha256BodyHash(body)) {
376
+ throw invalidProof(`${shape.label} bodyHash mismatch`);
377
+ }
378
+ let state;
379
+ try {
380
+ state = await resolvePresenter(presenterDID);
381
+ } catch (err) {
382
+ if (err instanceof ApiRequestVerifyError) throw err;
383
+ throw unverifiableProof(
384
+ `failed to resolve ${shape.label} presenter: ${err instanceof Error ? err.message : String(err)}`
385
+ );
386
+ }
387
+ if (!state) {
388
+ throw unverifiableProof(`failed to resolve ${shape.label} presenter: ${presenterDID}`);
389
+ }
390
+ if (state.isDeleted) throw invalidProof(`${shape.label} presenter identity is deleted`);
391
+ const key = state.keys.find((candidate) => candidate.id === presenterKeyId);
392
+ if (!key) throw invalidProof(`${shape.label} signing key is not a current key of the presenter`);
393
+ try {
394
+ verifyJws({ token: input.proof, publicKey: decodeMultikey(key.publicKeyMultibase).keyBytes });
395
+ } catch (err) {
396
+ throw invalidProof(err instanceof Error ? err.message : `invalid ${shape.label} signature`);
397
+ }
398
+ return { payload, rawPayload, presenterDID, kid, now };
399
+ };
400
+ var verifyIdentityProofEnvelope = (input, resolvePresenter) => verifyProofEnvelope(input, IDENTITY_PROOF_SHAPE, resolvePresenter);
401
+ var verifyRequestProofEnvelope = (input, resolvePresenter) => verifyProofEnvelope(input, REQUEST_PROOF_SHAPE, resolvePresenter);
402
+
403
+ // src/credentials/dfos-credential.ts
404
+ var resolveKeyFromIdentity = (identity, kid) => {
405
+ const hashIdx = kid.indexOf("#");
406
+ if (hashIdx < 0) throw new CredentialVerificationError("kid must be a DID URL");
407
+ const keyId = kid.substring(hashIdx + 1);
408
+ const allKeys = [...identity.authKeys, ...identity.assertKeys, ...identity.controllerKeys];
409
+ const key = allKeys.find((k) => k.id === keyId);
410
+ if (!key) {
411
+ throw new CredentialVerificationError(`key ${keyId} not found on identity ${identity.did}`);
412
+ }
413
+ const { keyBytes } = decodeMultikey(key.publicKeyMultibase);
414
+ return keyBytes;
415
+ };
416
+ var createDFOSCredential = async (options) => {
417
+ const kid = `${options.issuerDID}#${options.keyId}`;
418
+ const now = options.iat ?? Math.floor(Date.now() / 1e3);
419
+ const payload = {
420
+ version: 1,
421
+ type: "DFOSCredential",
422
+ iss: options.issuerDID,
423
+ aud: options.audienceDID,
424
+ att: options.att,
425
+ prf: options.prf ?? [],
426
+ exp: options.exp,
427
+ iat: now
428
+ };
429
+ const parseResult = DFOSCredentialPayload.safeParse(payload);
430
+ if (!parseResult.success) {
431
+ const messages = parseResult.error.issues.map((e) => e.message).join(", ");
432
+ throw new Error(`invalid credential payload: ${messages}`);
433
+ }
434
+ const encoded = await dagCborCanonicalEncode(payload);
435
+ const credentialCID = encoded.cid.toString();
436
+ const jwsToken = await createJws({
437
+ header: { alg: "EdDSA", typ: "did:dfos:credential", kid, cid: credentialCID },
438
+ payload,
439
+ sign: options.signer
440
+ });
441
+ return jwsToken;
442
+ };
443
+ var verifyDFOSCredential = async (jwsToken, options) => {
444
+ if (jwsToken.length > MAX_CREDENTIAL_SIZE) {
445
+ throw new CredentialVerificationError(
446
+ `credential exceeds max size: ${jwsToken.length} > ${MAX_CREDENTIAL_SIZE}`
447
+ );
448
+ }
449
+ const decoded = decodeJwsUnsafe(jwsToken);
450
+ if (!decoded) throw new CredentialVerificationError("failed to decode credential JWS");
451
+ if (decoded.header.typ !== "did:dfos:credential") {
452
+ throw new CredentialVerificationError(`invalid typ: ${decoded.header.typ}`);
453
+ }
454
+ const result = DFOSCredentialPayload.safeParse(decoded.payload);
455
+ if (!result.success) {
456
+ const messages = result.error.issues.map((e) => e.message).join(", ");
457
+ throw new CredentialVerificationError(`invalid credential payload: ${messages}`);
458
+ }
459
+ const payload = result.data;
460
+ const kid = decoded.header.kid;
461
+ const hashIdx = kid.indexOf("#");
462
+ if (hashIdx < 0) throw new CredentialVerificationError("credential kid must be a DID URL");
463
+ const kidDid = kid.substring(0, hashIdx);
464
+ if (kidDid !== payload.iss) {
465
+ throw new CredentialVerificationError("credential kid DID does not match iss");
466
+ }
467
+ const identity = await options.resolveIdentity(payload.iss);
468
+ if (!identity) {
469
+ throw new CredentialVerificationError(`issuer identity not found: ${payload.iss}`);
470
+ }
471
+ if (identity.isDeleted) {
472
+ throw new CredentialVerificationError(`issuer identity is deleted: ${payload.iss}`);
473
+ }
474
+ const publicKey = resolveKeyFromIdentity(identity, kid);
475
+ try {
476
+ verifyJws({ token: jwsToken, publicKey });
477
+ } catch {
478
+ throw new CredentialVerificationError("invalid credential signature");
479
+ }
480
+ const encoded = await dagCborCanonicalEncode(payload);
481
+ const credentialCID = encoded.cid.toString();
482
+ if (!decoded.header.cid) {
483
+ throw new CredentialVerificationError("missing cid in credential header");
484
+ }
485
+ if (decoded.header.cid !== credentialCID) {
486
+ throw new CredentialVerificationError("credential cid mismatch");
487
+ }
488
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
489
+ if (payload.iat > now) {
490
+ throw new CredentialVerificationError("credential not yet valid (iat is in the future)");
491
+ }
492
+ if (payload.exp <= now) {
493
+ throw new CredentialVerificationError("credential expired");
494
+ }
495
+ return {
496
+ iss: payload.iss,
497
+ aud: payload.aud,
498
+ att: payload.att,
499
+ prf: payload.prf,
500
+ exp: payload.exp,
501
+ iat: payload.iat,
502
+ credentialCID,
503
+ signerKeyId: kid
504
+ };
505
+ };
506
+ var verifyDelegationChain = async (credential, options) => {
507
+ const chain = [credential];
508
+ let current = credential;
509
+ const maxDepth = 16;
510
+ for (let depth = 0; depth < maxDepth; depth++) {
511
+ if (current.prf.length === 0) {
512
+ if (current.iss !== options.rootDID) {
513
+ throw new CredentialVerificationError(
514
+ `delegation chain root issuer ${current.iss} does not match expected root ${options.rootDID}`
515
+ );
516
+ }
517
+ return { credential, chain, rootDID: options.rootDID };
518
+ }
519
+ if (current.prf.length > 1) {
520
+ throw new CredentialVerificationError(
521
+ "delegation chain: multi-parent credentials are not supported (prf must have at most one entry)"
522
+ );
523
+ }
524
+ const parent = await verifyDFOSCredential(current.prf[0], {
525
+ resolveIdentity: options.resolveIdentity,
526
+ ...options.now !== void 0 ? { now: options.now } : {}
527
+ });
528
+ if (options.isRevoked) {
529
+ const revoked = await options.isRevoked(parent.iss, parent.credentialCID, options.asOfUnix);
530
+ if (revoked) {
531
+ throw new CredentialVerificationError("parent credential in delegation chain is revoked");
532
+ }
533
+ }
534
+ if (parent.aud !== "*" && parent.aud !== current.iss) {
535
+ throw new CredentialVerificationError(
536
+ `delegation gap: parent credential audience ${parent.aud} does not match child issuer ${current.iss}`
537
+ );
538
+ }
539
+ if (current.exp > parent.exp) {
540
+ throw new CredentialVerificationError(
541
+ "delegation chain: child credential expiry exceeds parent expiry"
542
+ );
543
+ }
544
+ if (!isAttenuated(parent.att, current.att)) {
545
+ throw new CredentialVerificationError(
546
+ "delegation chain: child credential scope exceeds parent scope"
547
+ );
548
+ }
549
+ chain.push(parent);
550
+ current = parent;
551
+ }
552
+ throw new CredentialVerificationError("delegation chain too deep (max 16 credentials)");
553
+ };
554
+ var parseResource = (resource) => {
555
+ const colonIdx = resource.indexOf(":");
556
+ if (colonIdx < 0) return null;
557
+ return { type: resource.substring(0, colonIdx), id: resource.substring(colonIdx + 1) };
558
+ };
559
+ var parseActions = (action) => new Set(
560
+ action.split(",").map((a) => a.trim()).filter((a) => a !== "")
561
+ );
562
+ var isAttenuated = (parentAtt, childAtt) => {
563
+ return childAtt.every((childEntry) => {
564
+ const childRes = parseResource(childEntry.resource);
565
+ if (!childRes) return false;
566
+ const childActions = parseActions(childEntry.action);
567
+ return parentAtt.some((parentEntry) => {
568
+ const parentRes = parseResource(parentEntry.resource);
569
+ if (!parentRes) return false;
570
+ const parentActions = parseActions(parentEntry.action);
571
+ for (const a of childActions) {
572
+ if (!parentActions.has(a)) return false;
573
+ }
574
+ if (parentRes.type === "chain" && parentRes.id === "*") {
575
+ return childRes.type === "chain";
576
+ }
577
+ if (childRes.type === "chain" && childRes.id === "*") {
578
+ return false;
579
+ }
580
+ if (childRes.type === "chain" && parentRes.type === "chain") {
581
+ return childRes.id === parentRes.id;
582
+ }
583
+ if (childRes.type !== "chain" && parentRes.type !== "chain") {
584
+ return childEntry.resource === parentEntry.resource;
585
+ }
586
+ return false;
587
+ });
588
+ });
589
+ };
590
+ var matchesResource = async (att, resource, action) => {
591
+ const requestedRes = parseResource(resource);
592
+ if (!requestedRes) return false;
593
+ const requestedActions = parseActions(action);
594
+ for (const entry of att) {
595
+ const entryRes = parseResource(entry.resource);
596
+ if (!entryRes) continue;
597
+ const entryActions = parseActions(entry.action);
598
+ let actionsCovered = true;
599
+ for (const a of requestedActions) {
600
+ if (!entryActions.has(a)) {
601
+ actionsCovered = false;
602
+ break;
603
+ }
604
+ }
605
+ if (!actionsCovered) continue;
606
+ if (entryRes.type === "chain" && entryRes.id === "*" && requestedRes.type === "chain") {
607
+ return true;
608
+ }
609
+ if (entryRes.type === requestedRes.type && entryRes.id === requestedRes.id) {
610
+ return true;
611
+ }
612
+ }
613
+ return false;
614
+ };
615
+ var decodeDFOSCredentialUnsafe = (jwsToken) => {
616
+ const decoded = decodeJwsUnsafe(jwsToken);
617
+ if (!decoded) return null;
618
+ const result = DFOSCredentialPayload.safeParse(decoded.payload);
619
+ if (!result.success) return null;
620
+ return {
621
+ header: decoded.header,
622
+ payload: result.data
623
+ };
624
+ };
625
+ var CredentialVerificationError = class extends Error {
626
+ constructor(message) {
627
+ super(message);
628
+ this.name = "CredentialVerificationError";
629
+ }
630
+ };
631
+
632
+ export {
633
+ ED25519_PUB_MULTICODEC,
634
+ ED25519_PRIV_MULTICODEC,
635
+ encodeEd25519Multikey,
636
+ decodeMultikey,
637
+ MAX_CREDENTIAL_SIZE,
638
+ Attenuation,
639
+ DFOSCredentialPayload,
640
+ REQUEST_PROOF_JWS_TYP,
641
+ IDENTITY_PROOF_JWS_TYP,
642
+ EMPTY_BODY_SHA256,
643
+ MAX_REQUEST_PROOF_SIZE,
644
+ DEFAULT_PROOF_WINDOW_SECONDS,
645
+ DEFAULT_PROOF_SKEW_SECONDS,
646
+ MAX_PROOF_FRESHNESS_SPAN_SECONDS,
647
+ MAX_BODY_BYTES,
648
+ DFOS_AUTH_SCHEME,
649
+ canonicalExtraMembers,
650
+ apiRequestSigningInput,
651
+ apiIdentitySigningInput,
652
+ sha256BodyHash,
653
+ parseDfosAuthorization,
654
+ signApiRequest,
655
+ signApiIdentityRequest,
656
+ buildApiAuthHeaders,
657
+ buildApiIdentityHeaders,
658
+ ApiRequestVerifyError,
659
+ invalidProof,
660
+ unverifiableProof,
661
+ misconfiguredProof,
662
+ assertProofVerifierConfig,
663
+ verifyIdentityProofEnvelope,
664
+ verifyRequestProofEnvelope,
665
+ createDFOSCredential,
666
+ verifyDFOSCredential,
667
+ verifyDelegationChain,
668
+ isAttenuated,
669
+ matchesResource,
670
+ decodeDFOSCredentialUnsafe,
671
+ CredentialVerificationError
672
+ };
@@ -5,7 +5,7 @@ import {
5
5
  matchesResource,
6
6
  verifyDFOSCredential,
7
7
  verifyDelegationChain
8
- } from "./chunk-XP644GQK.js";
8
+ } from "./chunk-NXQW6EBF.js";
9
9
  import {
10
10
  assertJwsProfile,
11
11
  base64urlDecode,