@peac/protocol 0.10.14 → 0.11.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.cjs CHANGED
@@ -140,6 +140,10 @@ async function issue(options) {
140
140
  ...options.purpose_enforced && { purpose_enforced: options.purpose_enforced },
141
141
  ...options.purpose_reason && { purpose_reason: options.purpose_reason }
142
142
  };
143
+ const constraintResult = schema.validateKernelConstraints(claims);
144
+ if (!constraintResult.valid) {
145
+ throw new IssueError(schema.createConstraintViolationError(constraintResult.violations));
146
+ }
143
147
  try {
144
148
  schema.ReceiptClaims.parse(claims);
145
149
  } catch (err) {
@@ -148,7 +152,10 @@ async function issue(options) {
148
152
  (issue2) => issue2.path.some((p) => p === "evidence" || p === "payment")
149
153
  );
150
154
  if (evidenceIssue && evidenceIssue.path.includes("evidence")) {
151
- const peacError = schema.createEvidenceNotJsonError(evidenceIssue.message, evidenceIssue.path);
155
+ const peacError = schema.createEvidenceNotJsonError(
156
+ evidenceIssue.message,
157
+ evidenceIssue.path
158
+ );
152
159
  throw new IssueError(peacError);
153
160
  }
154
161
  }
@@ -172,605 +179,253 @@ async function issueJws(options) {
172
179
  const result = await issue(options);
173
180
  return result.jws;
174
181
  }
175
- var jwksCache = /* @__PURE__ */ new Map();
176
- var CACHE_TTL_MS = 5 * 60 * 1e3;
177
- async function fetchJWKS(issuerUrl) {
178
- if (!issuerUrl.startsWith("https://")) {
179
- throw new Error("Issuer URL must be https://");
182
+ function parseIssuerConfig(json) {
183
+ let config;
184
+ if (typeof json === "string") {
185
+ const bytes = new TextEncoder().encode(json).length;
186
+ if (bytes > schema.PEAC_ISSUER_CONFIG_MAX_BYTES) {
187
+ throw new Error(`Issuer config exceeds ${schema.PEAC_ISSUER_CONFIG_MAX_BYTES} bytes (got ${bytes})`);
188
+ }
189
+ try {
190
+ config = JSON.parse(json);
191
+ } catch {
192
+ throw new Error("Issuer config is not valid JSON");
193
+ }
194
+ } else {
195
+ config = json;
196
+ }
197
+ if (typeof config !== "object" || config === null) {
198
+ throw new Error("Issuer config must be an object");
199
+ }
200
+ const obj = config;
201
+ if (typeof obj.version !== "string" || !obj.version) {
202
+ throw new Error("Missing required field: version");
203
+ }
204
+ if (typeof obj.issuer !== "string" || !obj.issuer) {
205
+ throw new Error("Missing required field: issuer");
206
+ }
207
+ if (typeof obj.jwks_uri !== "string" || !obj.jwks_uri) {
208
+ throw new Error("Missing required field: jwks_uri");
209
+ }
210
+ if (!obj.issuer.startsWith("https://")) {
211
+ throw new Error("issuer must be an HTTPS URL");
180
212
  }
181
- const discoveryUrl = `${issuerUrl}/.well-known/peac.txt`;
182
213
  try {
183
- const discoveryResp = await fetch(discoveryUrl, {
184
- headers: { Accept: "text/plain" },
185
- // Timeout after 5 seconds
186
- signal: AbortSignal.timeout(5e3)
187
- });
188
- if (!discoveryResp.ok) {
189
- throw new Error(`Discovery fetch failed: ${discoveryResp.status}`);
214
+ new URL(obj.jwks_uri);
215
+ } catch {
216
+ throw new Error("jwks_uri must be a valid URL");
217
+ }
218
+ if (obj.verify_endpoint !== void 0) {
219
+ if (typeof obj.verify_endpoint !== "string") {
220
+ throw new Error("verify_endpoint must be a string");
221
+ }
222
+ if (!obj.verify_endpoint.startsWith("https://")) {
223
+ throw new Error("verify_endpoint must be an HTTPS URL");
224
+ }
225
+ }
226
+ if (obj.receipt_versions !== void 0) {
227
+ if (!Array.isArray(obj.receipt_versions)) {
228
+ throw new Error("receipt_versions must be an array");
190
229
  }
191
- const discoveryText = await discoveryResp.text();
192
- const jwksLine = discoveryText.split("\n").find((line) => line.startsWith("jwks:"));
193
- if (!jwksLine) {
194
- throw new Error("No jwks field in discovery");
230
+ }
231
+ if (obj.algorithms !== void 0) {
232
+ if (!Array.isArray(obj.algorithms)) {
233
+ throw new Error("algorithms must be an array");
195
234
  }
196
- const jwksUrl = jwksLine.replace("jwks:", "").trim();
197
- if (!jwksUrl.startsWith("https://")) {
198
- throw new Error("JWKS URL must be https://");
235
+ }
236
+ if (obj.payment_rails !== void 0) {
237
+ if (!Array.isArray(obj.payment_rails)) {
238
+ throw new Error("payment_rails must be an array");
199
239
  }
200
- const jwksResp = await fetch(jwksUrl, {
240
+ }
241
+ return {
242
+ version: obj.version,
243
+ issuer: obj.issuer,
244
+ jwks_uri: obj.jwks_uri,
245
+ verify_endpoint: obj.verify_endpoint,
246
+ receipt_versions: obj.receipt_versions,
247
+ algorithms: obj.algorithms,
248
+ payment_rails: obj.payment_rails,
249
+ security_contact: obj.security_contact
250
+ };
251
+ }
252
+ async function fetchIssuerConfig(issuerUrl) {
253
+ if (!issuerUrl.startsWith("https://")) {
254
+ throw new Error("Issuer URL must be https://");
255
+ }
256
+ const baseUrl = issuerUrl.replace(/\/$/, "");
257
+ const configUrl = `${baseUrl}${schema.PEAC_ISSUER_CONFIG_PATH}`;
258
+ try {
259
+ const resp = await fetch(configUrl, {
201
260
  headers: { Accept: "application/json" },
202
- signal: AbortSignal.timeout(5e3)
261
+ signal: AbortSignal.timeout(1e4)
203
262
  });
204
- if (!jwksResp.ok) {
205
- throw new Error(`JWKS fetch failed: ${jwksResp.status}`);
263
+ if (!resp.ok) {
264
+ throw new Error(`Issuer config fetch failed: ${resp.status}`);
265
+ }
266
+ const text = await resp.text();
267
+ const config = parseIssuerConfig(text);
268
+ const normalizedExpected = baseUrl.replace(/\/$/, "");
269
+ const normalizedActual = config.issuer.replace(/\/$/, "");
270
+ if (normalizedActual !== normalizedExpected) {
271
+ throw new Error(`Issuer mismatch: expected ${normalizedExpected}, got ${normalizedActual}`);
206
272
  }
207
- const jwks = await jwksResp.json();
208
- return jwks;
273
+ return config;
209
274
  } catch (err) {
210
- throw new Error(`JWKS fetch failed: ${err instanceof Error ? err.message : String(err)}`, {
211
- cause: err
212
- });
275
+ throw new Error(
276
+ `Failed to fetch issuer config from ${issuerUrl}: ${err instanceof Error ? err.message : String(err)}`,
277
+ { cause: err }
278
+ );
213
279
  }
214
280
  }
215
- async function getJWKS(issuerUrl) {
216
- const now = Date.now();
217
- const cached = jwksCache.get(issuerUrl);
218
- if (cached && cached.expiresAt > now) {
219
- return { jwks: cached.keys, fromCache: true };
220
- }
221
- const jwks = await fetchJWKS(issuerUrl);
222
- jwksCache.set(issuerUrl, {
223
- keys: jwks,
224
- expiresAt: now + CACHE_TTL_MS
225
- });
226
- return { jwks, fromCache: false };
281
+ function isJsonContent(text, contentType) {
282
+ if (contentType?.includes("application/json")) {
283
+ return true;
284
+ }
285
+ const firstChar = text.trimStart()[0];
286
+ return firstChar === "{";
227
287
  }
228
- function jwkToPublicKey(jwk) {
229
- if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
230
- throw new Error("Only Ed25519 keys (OKP/Ed25519) are supported");
288
+ function parsePolicyManifest(text, contentType) {
289
+ const bytes = new TextEncoder().encode(text).length;
290
+ if (bytes > schema.PEAC_POLICY_MAX_BYTES) {
291
+ throw new Error(`Policy manifest exceeds ${schema.PEAC_POLICY_MAX_BYTES} bytes (got ${bytes})`);
231
292
  }
232
- const xBytes = Buffer.from(jwk.x, "base64url");
233
- if (xBytes.length !== 32) {
234
- throw new Error("Ed25519 public key must be 32 bytes");
293
+ let manifest;
294
+ if (isJsonContent(text, contentType)) {
295
+ try {
296
+ manifest = JSON.parse(text);
297
+ } catch {
298
+ throw new Error("Policy manifest is not valid JSON");
299
+ }
300
+ } else {
301
+ manifest = parseSimpleYaml(text);
235
302
  }
236
- return new Uint8Array(xBytes);
303
+ if (typeof manifest.version !== "string" || !manifest.version) {
304
+ throw new Error("Missing required field: version");
305
+ }
306
+ if (!manifest.version.startsWith("peac-policy/")) {
307
+ throw new Error(
308
+ `Invalid version format: "${manifest.version}". Must start with "peac-policy/" (e.g., "peac-policy/0.1")`
309
+ );
310
+ }
311
+ if (manifest.usage !== "open" && manifest.usage !== "conditional") {
312
+ throw new Error('Missing or invalid field: usage (must be "open" or "conditional")');
313
+ }
314
+ return {
315
+ version: manifest.version,
316
+ usage: manifest.usage,
317
+ purposes: manifest.purposes,
318
+ receipts: manifest.receipts,
319
+ attribution: manifest.attribution,
320
+ rate_limit: manifest.rate_limit,
321
+ daily_limit: manifest.daily_limit,
322
+ negotiate: manifest.negotiate,
323
+ contact: manifest.contact,
324
+ license: manifest.license,
325
+ price: manifest.price,
326
+ currency: manifest.currency,
327
+ payment_methods: manifest.payment_methods,
328
+ payment_endpoint: manifest.payment_endpoint
329
+ };
237
330
  }
238
- async function verifyReceipt(optionsOrJws) {
239
- const receiptJws = typeof optionsOrJws === "string" ? optionsOrJws : optionsOrJws.receiptJws;
240
- const inputSnapshot = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.subject_snapshot;
241
- const telemetry = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.telemetry;
242
- const startTime = performance.now();
243
- let jwksFetchTime;
244
- try {
245
- const { header, payload } = crypto.decode(receiptJws);
246
- schema.ReceiptClaims.parse(payload);
247
- if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) {
248
- const durationMs = performance.now() - startTime;
249
- fireTelemetryHook(telemetry?.onReceiptVerified, {
250
- receiptHash: hashReceipt(receiptJws),
251
- valid: false,
252
- reasonCode: "expired",
253
- issuer: payload.iss,
254
- kid: header.kid,
255
- durationMs
256
- });
257
- return {
258
- ok: false,
259
- reason: "expired",
260
- details: `Receipt expired at ${new Date(payload.exp * 1e3).toISOString()}`
261
- };
262
- }
263
- const jwksFetchStart = performance.now();
264
- const { jwks, fromCache } = await getJWKS(payload.iss);
265
- if (!fromCache) {
266
- jwksFetchTime = performance.now() - jwksFetchStart;
331
+ function parseSimpleYaml(text) {
332
+ const lines = text.split("\n");
333
+ const result = {};
334
+ if (text.includes("<<:")) {
335
+ throw new Error("YAML merge keys are not allowed");
336
+ }
337
+ if (text.includes("&") || text.includes("*")) {
338
+ throw new Error("YAML anchors and aliases are not allowed");
339
+ }
340
+ if (/!\w+/.test(text)) {
341
+ throw new Error("YAML custom tags are not allowed");
342
+ }
343
+ const docSeparators = text.match(/^---$/gm);
344
+ if (docSeparators && docSeparators.length > 1) {
345
+ throw new Error("Multi-document YAML is not allowed");
346
+ }
347
+ for (const line of lines) {
348
+ const trimmed = line.trim();
349
+ if (!trimmed || trimmed.startsWith("#") || trimmed === "---") {
350
+ continue;
267
351
  }
268
- const jwk = jwks.keys.find((k) => k.kid === header.kid);
269
- if (!jwk) {
270
- const durationMs = performance.now() - startTime;
271
- fireTelemetryHook(telemetry?.onReceiptVerified, {
272
- receiptHash: hashReceipt(receiptJws),
273
- valid: false,
274
- reasonCode: "unknown_key",
275
- issuer: payload.iss,
276
- kid: header.kid,
277
- durationMs
278
- });
279
- return {
280
- ok: false,
281
- reason: "unknown_key",
282
- details: `No key found with kid=${header.kid}`
283
- };
352
+ const colonIndex = trimmed.indexOf(":");
353
+ if (colonIndex === -1) continue;
354
+ const key = trimmed.slice(0, colonIndex).trim();
355
+ let value = trimmed.slice(colonIndex + 1).trim();
356
+ if (value === "") {
357
+ value = void 0;
358
+ } else if (typeof value === "string") {
359
+ if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
360
+ value = value.slice(1, -1);
361
+ } else if (value.startsWith("[") && value.endsWith("]")) {
362
+ const inner = value.slice(1, -1);
363
+ value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
364
+ } else if (/^-?\d+(\.\d+)?$/.test(value)) {
365
+ value = parseFloat(value);
366
+ } else if (value === "true") {
367
+ value = true;
368
+ } else if (value === "false") {
369
+ value = false;
370
+ }
284
371
  }
285
- const publicKey = jwkToPublicKey(jwk);
286
- const result = await crypto.verify(receiptJws, publicKey);
287
- if (!result.valid) {
288
- const durationMs = performance.now() - startTime;
289
- fireTelemetryHook(telemetry?.onReceiptVerified, {
290
- receiptHash: hashReceipt(receiptJws),
291
- valid: false,
292
- reasonCode: "invalid_signature",
293
- issuer: payload.iss,
294
- kid: header.kid,
295
- durationMs
296
- });
297
- return {
298
- ok: false,
299
- reason: "invalid_signature",
300
- details: "Ed25519 signature verification failed"
301
- };
372
+ if (value !== void 0) {
373
+ result[key] = value;
302
374
  }
303
- const validatedSnapshot = schema.validateSubjectSnapshot(inputSnapshot);
304
- const verifyTime = performance.now() - startTime;
305
- fireTelemetryHook(telemetry?.onReceiptVerified, {
306
- receiptHash: hashReceipt(receiptJws),
307
- valid: true,
308
- issuer: payload.iss,
309
- kid: header.kid,
310
- durationMs: verifyTime
311
- });
312
- return {
313
- ok: true,
314
- claims: payload,
315
- ...validatedSnapshot && { subject_snapshot: validatedSnapshot },
316
- perf: {
317
- verify_ms: verifyTime,
318
- ...jwksFetchTime && { jwks_fetch_ms: jwksFetchTime }
319
- }
320
- };
321
- } catch (err) {
322
- const durationMs = performance.now() - startTime;
323
- fireTelemetryHook(telemetry?.onReceiptVerified, {
324
- receiptHash: hashReceipt(receiptJws),
325
- valid: false,
326
- reasonCode: "verification_error",
327
- durationMs
328
- });
329
- return {
330
- ok: false,
331
- reason: "verification_error",
332
- details: err instanceof Error ? err.message : String(err)
333
- };
334
375
  }
376
+ return result;
335
377
  }
336
- function isCryptoError(err) {
337
- return err !== null && typeof err === "object" && "name" in err && err.name === "CryptoError" && "code" in err && typeof err.code === "string" && err.code.startsWith("CRYPTO_") && "message" in err && typeof err.message === "string";
338
- }
339
- var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
340
- "CRYPTO_INVALID_JWS_FORMAT",
341
- "CRYPTO_INVALID_TYP",
342
- "CRYPTO_INVALID_ALG",
343
- "CRYPTO_INVALID_KEY_LENGTH"
344
- ]);
345
- var MAX_PARSE_ISSUES = 25;
346
- function sanitizeParseIssues(issues) {
347
- if (!Array.isArray(issues)) return void 0;
348
- return issues.slice(0, MAX_PARSE_ISSUES).map((issue2) => ({
349
- path: Array.isArray(issue2?.path) ? issue2.path.join(".") : "",
350
- message: typeof issue2?.message === "string" ? issue2.message : String(issue2)
351
- }));
352
- }
353
- async function verifyLocal(jws, publicKey, options = {}) {
354
- const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
355
- const now = options.now ?? Math.floor(Date.now() / 1e3);
378
+ async function fetchPolicyManifest(baseUrl) {
379
+ if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
380
+ throw new Error("Base URL must be https://");
381
+ }
382
+ const normalizedBase = baseUrl.replace(/\/$/, "");
383
+ const primaryUrl = `${normalizedBase}${schema.PEAC_POLICY_PATH}`;
384
+ const fallbackUrl = `${normalizedBase}${schema.PEAC_POLICY_FALLBACK_PATH}`;
356
385
  try {
357
- const result = await crypto.verify(jws, publicKey);
358
- if (!result.valid) {
359
- return {
360
- valid: false,
361
- code: "E_INVALID_SIGNATURE",
362
- message: "Ed25519 signature verification failed"
363
- };
364
- }
365
- const pr = schema.parseReceiptClaims(result.payload);
366
- if (!pr.ok) {
367
- return {
368
- valid: false,
369
- code: "E_INVALID_FORMAT",
370
- message: `Receipt schema validation failed: ${pr.error.message}`,
371
- details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
372
- };
373
- }
374
- if (issuer !== void 0 && pr.claims.iss !== issuer) {
375
- return {
376
- valid: false,
377
- code: "E_INVALID_ISSUER",
378
- message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
379
- };
380
- }
381
- if (audience !== void 0 && pr.claims.aud !== audience) {
382
- return {
383
- valid: false,
384
- code: "E_INVALID_AUDIENCE",
385
- message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
386
- };
387
- }
388
- if (rid !== void 0 && pr.claims.rid !== rid) {
389
- return {
390
- valid: false,
391
- code: "E_INVALID_RECEIPT_ID",
392
- message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
393
- };
394
- }
395
- if (requireExp && pr.claims.exp === void 0) {
396
- return {
397
- valid: false,
398
- code: "E_MISSING_EXP",
399
- message: "Receipt missing required exp claim"
400
- };
401
- }
402
- if (pr.claims.iat > now + maxClockSkew) {
403
- return {
404
- valid: false,
405
- code: "E_NOT_YET_VALID",
406
- message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
407
- };
408
- }
409
- if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
410
- return {
411
- valid: false,
412
- code: "E_EXPIRED",
413
- message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
414
- };
386
+ const resp = await fetch(primaryUrl, {
387
+ headers: { Accept: "text/plain, application/json" },
388
+ signal: AbortSignal.timeout(5e3)
389
+ });
390
+ if (resp.ok) {
391
+ const text = await resp.text();
392
+ const contentType = resp.headers.get("content-type") || void 0;
393
+ return parsePolicyManifest(text, contentType);
415
394
  }
416
- if (pr.variant === "commerce") {
417
- const claims = pr.claims;
418
- if (subjectUri !== void 0 && claims.subject?.uri !== subjectUri) {
419
- return {
420
- valid: false,
421
- code: "E_INVALID_SUBJECT",
422
- message: `Subject mismatch: expected "${subjectUri}", got "${claims.subject?.uri ?? "undefined"}"`
423
- };
424
- }
425
- return {
426
- valid: true,
427
- variant: "commerce",
428
- claims,
429
- kid: result.header.kid,
430
- policy_binding: "unavailable"
431
- };
432
- } else {
433
- const claims = pr.claims;
434
- if (subjectUri !== void 0 && claims.sub !== subjectUri) {
435
- return {
436
- valid: false,
437
- code: "E_INVALID_SUBJECT",
438
- message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
439
- };
395
+ if (resp.status === 404) {
396
+ const fallbackResp = await fetch(fallbackUrl, {
397
+ headers: { Accept: "text/plain, application/json" },
398
+ signal: AbortSignal.timeout(5e3)
399
+ });
400
+ if (fallbackResp.ok) {
401
+ const text = await fallbackResp.text();
402
+ const contentType = fallbackResp.headers.get("content-type") || void 0;
403
+ return parsePolicyManifest(text, contentType);
440
404
  }
441
- return {
442
- valid: true,
443
- variant: "attestation",
444
- claims,
445
- kid: result.header.kid,
446
- policy_binding: "unavailable"
447
- };
405
+ throw new Error("Policy manifest not found at primary or fallback location");
448
406
  }
407
+ throw new Error(`Policy manifest fetch failed: ${resp.status}`);
449
408
  } catch (err) {
450
- if (isCryptoError(err)) {
451
- if (FORMAT_ERROR_CODES.has(err.code)) {
452
- return {
453
- valid: false,
454
- code: "E_INVALID_FORMAT",
455
- message: err.message
456
- };
457
- }
458
- if (err.code === "CRYPTO_INVALID_SIGNATURE") {
459
- return {
460
- valid: false,
461
- code: "E_INVALID_SIGNATURE",
462
- message: err.message
463
- };
464
- }
465
- }
466
- if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
467
- const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
468
- return {
469
- valid: false,
470
- code: "E_INVALID_FORMAT",
471
- message: `Invalid receipt payload: ${syntaxMessage}`
472
- };
473
- }
474
- const message = err instanceof Error ? err.message : String(err);
475
- return {
476
- valid: false,
477
- code: "E_INTERNAL",
478
- message: `Unexpected verification error: ${message}`
479
- };
409
+ throw new Error(
410
+ `Failed to fetch policy manifest from ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
411
+ { cause: err }
412
+ );
480
413
  }
481
414
  }
482
- function isCommerceResult(r) {
483
- return r.valid === true && r.variant === "commerce";
484
- }
485
- function isAttestationResult(r) {
486
- return r.valid === true && r.variant === "attestation";
487
- }
488
- function setReceiptHeader(headers, receiptJws) {
489
- headers.set(schema.PEAC_RECEIPT_HEADER, receiptJws);
490
- }
491
- function getReceiptHeader(headers) {
492
- return headers.get(schema.PEAC_RECEIPT_HEADER);
493
- }
494
- function setVaryHeader(headers) {
495
- const existing = headers.get("Vary");
496
- if (existing) {
497
- const varies = existing.split(",").map((v) => v.trim());
498
- if (!varies.includes(schema.PEAC_RECEIPT_HEADER)) {
499
- headers.set("Vary", `${existing}, ${schema.PEAC_RECEIPT_HEADER}`);
500
- }
501
- } else {
502
- headers.set("Vary", schema.PEAC_RECEIPT_HEADER);
415
+ function parseDiscovery(text) {
416
+ const bytes = new TextEncoder().encode(text).length;
417
+ if (bytes > 2e3) {
418
+ throw new Error(`Discovery manifest exceeds 2000 bytes (got ${bytes})`);
503
419
  }
504
- }
505
- function getPurposeHeader(headers) {
506
- const value = headers.get(schema.PEAC_PURPOSE_HEADER);
507
- if (!value) {
508
- return [];
420
+ const lines = text.trim().split("\n");
421
+ if (lines.length > 20) {
422
+ throw new Error(`Discovery manifest exceeds 20 lines (got ${lines.length})`);
509
423
  }
510
- return schema.parsePurposeHeader(value);
511
- }
512
- function setPurposeAppliedHeader(headers, purpose) {
513
- headers.set(schema.PEAC_PURPOSE_APPLIED_HEADER, purpose);
514
- }
515
- function setPurposeReasonHeader(headers, reason) {
516
- headers.set(schema.PEAC_PURPOSE_REASON_HEADER, reason);
517
- }
518
- function setVaryPurposeHeader(headers) {
519
- const existing = headers.get("Vary");
520
- if (existing) {
521
- const varies = existing.split(",").map((v) => v.trim());
522
- if (!varies.includes(schema.PEAC_PURPOSE_HEADER)) {
523
- headers.set("Vary", `${existing}, ${schema.PEAC_PURPOSE_HEADER}`);
524
- }
525
- } else {
526
- headers.set("Vary", schema.PEAC_PURPOSE_HEADER);
527
- }
528
- }
529
- function parseIssuerConfig(json) {
530
- let config;
531
- if (typeof json === "string") {
532
- const bytes = new TextEncoder().encode(json).length;
533
- if (bytes > schema.PEAC_ISSUER_CONFIG_MAX_BYTES) {
534
- throw new Error(`Issuer config exceeds ${schema.PEAC_ISSUER_CONFIG_MAX_BYTES} bytes (got ${bytes})`);
535
- }
536
- try {
537
- config = JSON.parse(json);
538
- } catch {
539
- throw new Error("Issuer config is not valid JSON");
540
- }
541
- } else {
542
- config = json;
543
- }
544
- if (typeof config !== "object" || config === null) {
545
- throw new Error("Issuer config must be an object");
546
- }
547
- const obj = config;
548
- if (typeof obj.version !== "string" || !obj.version) {
549
- throw new Error("Missing required field: version");
550
- }
551
- if (typeof obj.issuer !== "string" || !obj.issuer) {
552
- throw new Error("Missing required field: issuer");
553
- }
554
- if (typeof obj.jwks_uri !== "string" || !obj.jwks_uri) {
555
- throw new Error("Missing required field: jwks_uri");
556
- }
557
- if (!obj.issuer.startsWith("https://")) {
558
- throw new Error("issuer must be an HTTPS URL");
559
- }
560
- if (!obj.jwks_uri.startsWith("https://")) {
561
- throw new Error("jwks_uri must be an HTTPS URL");
562
- }
563
- if (obj.verify_endpoint !== void 0) {
564
- if (typeof obj.verify_endpoint !== "string") {
565
- throw new Error("verify_endpoint must be a string");
566
- }
567
- if (!obj.verify_endpoint.startsWith("https://")) {
568
- throw new Error("verify_endpoint must be an HTTPS URL");
569
- }
570
- }
571
- if (obj.receipt_versions !== void 0) {
572
- if (!Array.isArray(obj.receipt_versions)) {
573
- throw new Error("receipt_versions must be an array");
574
- }
575
- }
576
- if (obj.algorithms !== void 0) {
577
- if (!Array.isArray(obj.algorithms)) {
578
- throw new Error("algorithms must be an array");
579
- }
580
- }
581
- if (obj.payment_rails !== void 0) {
582
- if (!Array.isArray(obj.payment_rails)) {
583
- throw new Error("payment_rails must be an array");
584
- }
585
- }
586
- return {
587
- version: obj.version,
588
- issuer: obj.issuer,
589
- jwks_uri: obj.jwks_uri,
590
- verify_endpoint: obj.verify_endpoint,
591
- receipt_versions: obj.receipt_versions,
592
- algorithms: obj.algorithms,
593
- payment_rails: obj.payment_rails,
594
- security_contact: obj.security_contact
595
- };
596
- }
597
- async function fetchIssuerConfig(issuerUrl) {
598
- if (!issuerUrl.startsWith("https://")) {
599
- throw new Error("Issuer URL must be https://");
600
- }
601
- const baseUrl = issuerUrl.replace(/\/$/, "");
602
- const configUrl = `${baseUrl}${schema.PEAC_ISSUER_CONFIG_PATH}`;
603
- try {
604
- const resp = await fetch(configUrl, {
605
- headers: { Accept: "application/json" },
606
- signal: AbortSignal.timeout(1e4)
607
- });
608
- if (!resp.ok) {
609
- throw new Error(`Issuer config fetch failed: ${resp.status}`);
610
- }
611
- const text = await resp.text();
612
- const config = parseIssuerConfig(text);
613
- const normalizedExpected = baseUrl.replace(/\/$/, "");
614
- const normalizedActual = config.issuer.replace(/\/$/, "");
615
- if (normalizedActual !== normalizedExpected) {
616
- throw new Error(`Issuer mismatch: expected ${normalizedExpected}, got ${normalizedActual}`);
617
- }
618
- return config;
619
- } catch (err) {
620
- throw new Error(
621
- `Failed to fetch issuer config from ${issuerUrl}: ${err instanceof Error ? err.message : String(err)}`,
622
- { cause: err }
623
- );
624
- }
625
- }
626
- function isJsonContent(text, contentType) {
627
- if (contentType?.includes("application/json")) {
628
- return true;
629
- }
630
- const firstChar = text.trimStart()[0];
631
- return firstChar === "{";
632
- }
633
- function parsePolicyManifest(text, contentType) {
634
- const bytes = new TextEncoder().encode(text).length;
635
- if (bytes > schema.PEAC_POLICY_MAX_BYTES) {
636
- throw new Error(`Policy manifest exceeds ${schema.PEAC_POLICY_MAX_BYTES} bytes (got ${bytes})`);
637
- }
638
- let manifest;
639
- if (isJsonContent(text, contentType)) {
640
- try {
641
- manifest = JSON.parse(text);
642
- } catch {
643
- throw new Error("Policy manifest is not valid JSON");
644
- }
645
- } else {
646
- manifest = parseSimpleYaml(text);
647
- }
648
- if (typeof manifest.version !== "string" || !manifest.version) {
649
- throw new Error("Missing required field: version");
650
- }
651
- if (!manifest.version.startsWith("peac-policy/")) {
652
- throw new Error(
653
- `Invalid version format: "${manifest.version}". Must start with "peac-policy/" (e.g., "peac-policy/0.1")`
654
- );
655
- }
656
- if (manifest.usage !== "open" && manifest.usage !== "conditional") {
657
- throw new Error('Missing or invalid field: usage (must be "open" or "conditional")');
658
- }
659
- return {
660
- version: manifest.version,
661
- usage: manifest.usage,
662
- purposes: manifest.purposes,
663
- receipts: manifest.receipts,
664
- attribution: manifest.attribution,
665
- rate_limit: manifest.rate_limit,
666
- daily_limit: manifest.daily_limit,
667
- negotiate: manifest.negotiate,
668
- contact: manifest.contact,
669
- license: manifest.license,
670
- price: manifest.price,
671
- currency: manifest.currency,
672
- payment_methods: manifest.payment_methods,
673
- payment_endpoint: manifest.payment_endpoint
674
- };
675
- }
676
- function parseSimpleYaml(text) {
677
- const lines = text.split("\n");
678
- const result = {};
679
- if (text.includes("<<:")) {
680
- throw new Error("YAML merge keys are not allowed");
681
- }
682
- if (text.includes("&") || text.includes("*")) {
683
- throw new Error("YAML anchors and aliases are not allowed");
684
- }
685
- if (/!\w+/.test(text)) {
686
- throw new Error("YAML custom tags are not allowed");
687
- }
688
- const docSeparators = text.match(/^---$/gm);
689
- if (docSeparators && docSeparators.length > 1) {
690
- throw new Error("Multi-document YAML is not allowed");
691
- }
692
- for (const line of lines) {
693
- const trimmed = line.trim();
694
- if (!trimmed || trimmed.startsWith("#") || trimmed === "---") {
695
- continue;
696
- }
697
- const colonIndex = trimmed.indexOf(":");
698
- if (colonIndex === -1) continue;
699
- const key = trimmed.slice(0, colonIndex).trim();
700
- let value = trimmed.slice(colonIndex + 1).trim();
701
- if (value === "") {
702
- value = void 0;
703
- } else if (typeof value === "string") {
704
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
705
- value = value.slice(1, -1);
706
- } else if (value.startsWith("[") && value.endsWith("]")) {
707
- const inner = value.slice(1, -1);
708
- value = inner.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
709
- } else if (/^-?\d+(\.\d+)?$/.test(value)) {
710
- value = parseFloat(value);
711
- } else if (value === "true") {
712
- value = true;
713
- } else if (value === "false") {
714
- value = false;
715
- }
716
- }
717
- if (value !== void 0) {
718
- result[key] = value;
719
- }
720
- }
721
- return result;
722
- }
723
- async function fetchPolicyManifest(baseUrl) {
724
- if (!baseUrl.startsWith("https://") && !baseUrl.startsWith("http://localhost")) {
725
- throw new Error("Base URL must be https://");
726
- }
727
- const normalizedBase = baseUrl.replace(/\/$/, "");
728
- const primaryUrl = `${normalizedBase}${schema.PEAC_POLICY_PATH}`;
729
- const fallbackUrl = `${normalizedBase}${schema.PEAC_POLICY_FALLBACK_PATH}`;
730
- try {
731
- const resp = await fetch(primaryUrl, {
732
- headers: { Accept: "text/plain, application/json" },
733
- signal: AbortSignal.timeout(5e3)
734
- });
735
- if (resp.ok) {
736
- const text = await resp.text();
737
- const contentType = resp.headers.get("content-type") || void 0;
738
- return parsePolicyManifest(text, contentType);
739
- }
740
- if (resp.status === 404) {
741
- const fallbackResp = await fetch(fallbackUrl, {
742
- headers: { Accept: "text/plain, application/json" },
743
- signal: AbortSignal.timeout(5e3)
744
- });
745
- if (fallbackResp.ok) {
746
- const text = await fallbackResp.text();
747
- const contentType = fallbackResp.headers.get("content-type") || void 0;
748
- return parsePolicyManifest(text, contentType);
749
- }
750
- throw new Error("Policy manifest not found at primary or fallback location");
751
- }
752
- throw new Error(`Policy manifest fetch failed: ${resp.status}`);
753
- } catch (err) {
754
- throw new Error(
755
- `Failed to fetch policy manifest from ${baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
756
- { cause: err }
757
- );
758
- }
759
- }
760
- function parseDiscovery(text) {
761
- const bytes = new TextEncoder().encode(text).length;
762
- if (bytes > 2e3) {
763
- throw new Error(`Discovery manifest exceeds 2000 bytes (got ${bytes})`);
764
- }
765
- const lines = text.trim().split("\n");
766
- if (lines.length > 20) {
767
- throw new Error(`Discovery manifest exceeds 20 lines (got ${lines.length})`);
768
- }
769
- const discovery = {};
770
- for (const line of lines) {
771
- const trimmed = line.trim();
772
- if (!trimmed || trimmed.startsWith("#")) {
773
- continue;
424
+ const discovery = {};
425
+ for (const line of lines) {
426
+ const trimmed = line.trim();
427
+ if (!trimmed || trimmed.startsWith("#")) {
428
+ continue;
774
429
  }
775
430
  if (trimmed.includes(":")) {
776
431
  const [key, ...valueParts] = trimmed.split(":");
@@ -822,116 +477,6 @@ async function fetchDiscovery(issuerUrl) {
822
477
  );
823
478
  }
824
479
  }
825
- var DEFAULT_VERIFIER_LIMITS = {
826
- max_receipt_bytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
827
- max_jwks_bytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
828
- max_jwks_keys: kernel.VERIFIER_LIMITS.maxJwksKeys,
829
- max_redirects: kernel.VERIFIER_LIMITS.maxRedirects,
830
- fetch_timeout_ms: kernel.VERIFIER_LIMITS.fetchTimeoutMs,
831
- max_extension_bytes: kernel.VERIFIER_LIMITS.maxExtensionBytes
832
- };
833
- var DEFAULT_NETWORK_SECURITY = {
834
- https_only: kernel.VERIFIER_NETWORK.httpsOnly,
835
- block_private_ips: kernel.VERIFIER_NETWORK.blockPrivateIps,
836
- allow_redirects: kernel.VERIFIER_NETWORK.allowRedirects,
837
- allow_cross_origin_redirects: true,
838
- // Allow for CDN compatibility
839
- dns_failure_behavior: "block"
840
- // Fail-closed by default
841
- };
842
- function createDefaultPolicy(mode) {
843
- return {
844
- policy_version: kernel.VERIFIER_POLICY_VERSION,
845
- mode,
846
- limits: { ...DEFAULT_VERIFIER_LIMITS },
847
- network: { ...DEFAULT_NETWORK_SECURITY }
848
- };
849
- }
850
- var CHECK_IDS = [
851
- "jws.parse",
852
- "limits.receipt_bytes",
853
- "jws.protected_header",
854
- "claims.schema_unverified",
855
- "issuer.trust_policy",
856
- "issuer.discovery",
857
- "key.resolve",
858
- "jws.signature",
859
- "claims.time_window",
860
- "extensions.limits",
861
- "transport.profile_binding",
862
- "policy.binding"
863
- ];
864
- var NON_DETERMINISTIC_ARTIFACT_KEYS = [
865
- "issuer_jwks_digest"
866
- ];
867
- function createDigest(hexValue) {
868
- return {
869
- alg: "sha-256",
870
- value: hexValue.toLowerCase()
871
- };
872
- }
873
- function createEmptyReport(policy) {
874
- return {
875
- report_version: kernel.VERIFICATION_REPORT_VERSION,
876
- policy
877
- };
878
- }
879
- function ssrfErrorToReasonCode(ssrfReason, fetchType) {
880
- const prefix = fetchType === "key" ? "key_fetch" : "pointer_fetch";
881
- switch (ssrfReason) {
882
- case "not_https":
883
- case "private_ip":
884
- case "loopback":
885
- case "link_local":
886
- case "cross_origin_redirect":
887
- case "dns_failure":
888
- return `${prefix}_blocked`;
889
- case "timeout":
890
- return `${prefix}_timeout`;
891
- case "response_too_large":
892
- return fetchType === "pointer" ? "pointer_fetch_too_large" : "jwks_too_large";
893
- case "jwks_too_many_keys":
894
- return "jwks_too_many_keys";
895
- case "too_many_redirects":
896
- case "scheme_downgrade":
897
- case "network_error":
898
- case "invalid_url":
899
- default:
900
- return `${prefix}_failed`;
901
- }
902
- }
903
- function reasonCodeToSeverity(reason) {
904
- if (reason === "ok") return "info";
905
- return "error";
906
- }
907
- function reasonCodeToErrorCode(reason) {
908
- const mapping = {
909
- ok: "",
910
- receipt_too_large: "E_VERIFY_RECEIPT_TOO_LARGE",
911
- malformed_receipt: "E_VERIFY_MALFORMED_RECEIPT",
912
- signature_invalid: "E_VERIFY_SIGNATURE_INVALID",
913
- issuer_not_allowed: "E_VERIFY_ISSUER_NOT_ALLOWED",
914
- key_not_found: "E_VERIFY_KEY_NOT_FOUND",
915
- key_fetch_blocked: "E_VERIFY_KEY_FETCH_BLOCKED",
916
- key_fetch_failed: "E_VERIFY_KEY_FETCH_FAILED",
917
- key_fetch_timeout: "E_VERIFY_KEY_FETCH_TIMEOUT",
918
- pointer_fetch_blocked: "E_VERIFY_POINTER_FETCH_BLOCKED",
919
- pointer_fetch_failed: "E_VERIFY_POINTER_FETCH_FAILED",
920
- pointer_fetch_timeout: "E_VERIFY_POINTER_FETCH_TIMEOUT",
921
- pointer_fetch_too_large: "E_VERIFY_POINTER_FETCH_TOO_LARGE",
922
- pointer_digest_mismatch: "E_VERIFY_POINTER_DIGEST_MISMATCH",
923
- jwks_too_large: "E_VERIFY_JWKS_TOO_LARGE",
924
- jwks_too_many_keys: "E_VERIFY_JWKS_TOO_MANY_KEYS",
925
- expired: "E_VERIFY_EXPIRED",
926
- not_yet_valid: "E_VERIFY_NOT_YET_VALID",
927
- audience_mismatch: "E_VERIFY_AUDIENCE_MISMATCH",
928
- schema_invalid: "E_VERIFY_SCHEMA_INVALID",
929
- policy_violation: "E_VERIFY_POLICY_VIOLATION",
930
- extension_too_large: "E_VERIFY_EXTENSION_TOO_LARGE",
931
- invalid_transport: "E_VERIFY_INVALID_TRANSPORT"
932
- };
933
- return mapping[reason] || "E_VERIFY_POLICY_VIOLATION";
934
- }
935
480
  var cachedCapabilities = null;
936
481
  function getSSRFCapabilities() {
937
482
  if (cachedCapabilities) {
@@ -1339,49 +884,685 @@ async function ssrfSafeFetch(url, options = {}) {
1339
884
  }, new Uint8Array());
1340
885
  const body = new TextDecoder().decode(rawBytes);
1341
886
  return {
1342
- ok: true,
1343
- status: response.status,
1344
- body,
1345
- rawBytes,
1346
- contentType: response.headers.get("content-type") ?? void 0
887
+ ok: true,
888
+ status: response.status,
889
+ body,
890
+ rawBytes,
891
+ contentType: response.headers.get("content-type") ?? void 0
892
+ };
893
+ } catch (err) {
894
+ if (err instanceof Error) {
895
+ if (err.name === "AbortError" || err.message.includes("timeout")) {
896
+ return {
897
+ ok: false,
898
+ reason: "timeout",
899
+ message: `Fetch timeout after ${timeoutMs}ms: ${currentUrl}`
900
+ };
901
+ }
902
+ }
903
+ return {
904
+ ok: false,
905
+ reason: "network_error",
906
+ message: `Network error: ${err instanceof Error ? err.message : String(err)}`
907
+ };
908
+ }
909
+ }
910
+ }
911
+ async function fetchJWKSSafe(jwksUrl, options) {
912
+ return ssrfSafeFetch(jwksUrl, {
913
+ ...options,
914
+ maxBytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
915
+ headers: {
916
+ Accept: "application/json",
917
+ ...options?.headers
918
+ }
919
+ });
920
+ }
921
+ async function fetchPointerSafe(pointerUrl, options) {
922
+ return ssrfSafeFetch(pointerUrl, {
923
+ ...options,
924
+ maxBytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
925
+ headers: {
926
+ Accept: "application/jose, application/json",
927
+ ...options?.headers
928
+ }
929
+ });
930
+ }
931
+
932
+ // src/jwks-resolver.ts
933
+ var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
934
+ var DEFAULT_MAX_CACHE_ENTRIES = 1e3;
935
+ var jwksCache = /* @__PURE__ */ new Map();
936
+ function cacheGet(key, now) {
937
+ const entry = jwksCache.get(key);
938
+ if (!entry) return void 0;
939
+ if (entry.expiresAt <= now) {
940
+ jwksCache.delete(key);
941
+ return void 0;
942
+ }
943
+ jwksCache.delete(key);
944
+ jwksCache.set(key, entry);
945
+ return entry;
946
+ }
947
+ function cacheSet(key, entry, maxEntries) {
948
+ if (jwksCache.has(key)) jwksCache.delete(key);
949
+ jwksCache.set(key, entry);
950
+ while (jwksCache.size > maxEntries) {
951
+ const oldestKey = jwksCache.keys().next().value;
952
+ if (oldestKey !== void 0) jwksCache.delete(oldestKey);
953
+ }
954
+ }
955
+ function clearJWKSCache() {
956
+ jwksCache.clear();
957
+ }
958
+ function getJWKSCacheSize() {
959
+ return jwksCache.size;
960
+ }
961
+ function canonicalizeIssuerOrigin(issuerUrl) {
962
+ try {
963
+ const origin = new URL(issuerUrl).origin;
964
+ if (origin === "null") {
965
+ return { ok: false, message: `Issuer URL has no valid origin: ${issuerUrl}` };
966
+ }
967
+ return { ok: true, origin };
968
+ } catch {
969
+ return { ok: false, message: `Issuer URL is not a valid URL: ${issuerUrl}` };
970
+ }
971
+ }
972
+ function mapSSRFError(reason, context) {
973
+ let code;
974
+ switch (reason) {
975
+ case "not_https":
976
+ code = "E_VERIFY_INSECURE_SCHEME_BLOCKED";
977
+ break;
978
+ case "private_ip":
979
+ case "loopback":
980
+ case "link_local":
981
+ code = "E_VERIFY_KEY_FETCH_BLOCKED";
982
+ break;
983
+ case "timeout":
984
+ code = "E_VERIFY_KEY_FETCH_TIMEOUT";
985
+ break;
986
+ case "response_too_large":
987
+ code = context === "jwks" ? "E_VERIFY_JWKS_TOO_LARGE" : "E_VERIFY_KEY_FETCH_FAILED";
988
+ break;
989
+ case "dns_failure":
990
+ case "network_error":
991
+ case "too_many_redirects":
992
+ case "scheme_downgrade":
993
+ case "cross_origin_redirect":
994
+ case "invalid_url":
995
+ code = context === "issuer_config" ? "E_VERIFY_ISSUER_CONFIG_MISSING" : "E_VERIFY_KEY_FETCH_FAILED";
996
+ break;
997
+ case "jwks_too_many_keys":
998
+ code = "E_VERIFY_JWKS_TOO_MANY_KEYS";
999
+ break;
1000
+ default:
1001
+ code = "E_VERIFY_KEY_FETCH_FAILED";
1002
+ break;
1003
+ }
1004
+ return { code, reason };
1005
+ }
1006
+ async function resolveJWKS(issuerUrl, options) {
1007
+ const cacheTtlMs = options?.cacheTtlMs ?? DEFAULT_CACHE_TTL_MS;
1008
+ const maxCacheEntries = options?.maxCacheEntries ?? DEFAULT_MAX_CACHE_ENTRIES;
1009
+ const noCache = options?.noCache ?? false;
1010
+ const normalized = canonicalizeIssuerOrigin(issuerUrl);
1011
+ if (!normalized.ok) {
1012
+ return {
1013
+ ok: false,
1014
+ code: "E_VERIFY_ISSUER_CONFIG_INVALID",
1015
+ message: normalized.message,
1016
+ blockedUrl: issuerUrl
1017
+ };
1018
+ }
1019
+ const normalizedIssuer = normalized.origin;
1020
+ const now = Date.now();
1021
+ if (!noCache) {
1022
+ const cached = cacheGet(normalizedIssuer, now);
1023
+ if (cached) {
1024
+ return { ok: true, jwks: cached.jwks, fromCache: true };
1025
+ }
1026
+ }
1027
+ if (!normalizedIssuer.startsWith("https://")) {
1028
+ return {
1029
+ ok: false,
1030
+ code: "E_VERIFY_INSECURE_SCHEME_BLOCKED",
1031
+ message: `Issuer URL must be HTTPS: ${normalizedIssuer}`,
1032
+ blockedUrl: normalizedIssuer
1033
+ };
1034
+ }
1035
+ const configUrl = `${normalizedIssuer}/.well-known/peac-issuer.json`;
1036
+ const configResult = await ssrfSafeFetch(configUrl, {
1037
+ maxBytes: schema.PEAC_ISSUER_CONFIG_MAX_BYTES,
1038
+ headers: { Accept: "application/json" }
1039
+ });
1040
+ if (!configResult.ok) {
1041
+ const mapped = mapSSRFError(configResult.reason, "issuer_config");
1042
+ return {
1043
+ ok: false,
1044
+ code: mapped.code,
1045
+ message: `Failed to fetch peac-issuer.json: ${configResult.message}`,
1046
+ reason: mapped.reason,
1047
+ blockedUrl: configResult.blockedUrl
1048
+ };
1049
+ }
1050
+ let issuerConfig;
1051
+ try {
1052
+ issuerConfig = parseIssuerConfig(configResult.body);
1053
+ } catch (err) {
1054
+ return {
1055
+ ok: false,
1056
+ code: "E_VERIFY_ISSUER_CONFIG_INVALID",
1057
+ message: `Invalid peac-issuer.json: ${err instanceof Error ? err.message : String(err)}`
1058
+ };
1059
+ }
1060
+ const configIssuerResult = canonicalizeIssuerOrigin(issuerConfig.issuer);
1061
+ const configIssuer = configIssuerResult.ok ? configIssuerResult.origin : issuerConfig.issuer;
1062
+ if (configIssuer !== normalizedIssuer) {
1063
+ return {
1064
+ ok: false,
1065
+ code: "E_VERIFY_ISSUER_MISMATCH",
1066
+ message: `Issuer mismatch: expected ${normalizedIssuer}, got ${configIssuer}`
1067
+ };
1068
+ }
1069
+ if (!issuerConfig.jwks_uri) {
1070
+ return {
1071
+ ok: false,
1072
+ code: "E_VERIFY_JWKS_URI_INVALID",
1073
+ message: "peac-issuer.json missing required jwks_uri field"
1074
+ };
1075
+ }
1076
+ if (!issuerConfig.jwks_uri.startsWith("https://")) {
1077
+ return {
1078
+ ok: false,
1079
+ code: "E_VERIFY_JWKS_URI_INVALID",
1080
+ message: `jwks_uri must be HTTPS: ${issuerConfig.jwks_uri}`,
1081
+ blockedUrl: issuerConfig.jwks_uri
1082
+ };
1083
+ }
1084
+ const jwksResult = await fetchJWKSSafe(issuerConfig.jwks_uri);
1085
+ if (!jwksResult.ok) {
1086
+ const mapped = mapSSRFError(jwksResult.reason, "jwks");
1087
+ return {
1088
+ ok: false,
1089
+ code: mapped.code,
1090
+ message: `Failed to fetch JWKS from ${issuerConfig.jwks_uri}: ${jwksResult.message}`,
1091
+ reason: mapped.reason,
1092
+ blockedUrl: jwksResult.blockedUrl
1093
+ };
1094
+ }
1095
+ let jwks;
1096
+ try {
1097
+ jwks = JSON.parse(jwksResult.body);
1098
+ } catch {
1099
+ return {
1100
+ ok: false,
1101
+ code: "E_VERIFY_JWKS_INVALID",
1102
+ message: "JWKS response is not valid JSON"
1103
+ };
1104
+ }
1105
+ if (!jwks.keys || !Array.isArray(jwks.keys)) {
1106
+ return {
1107
+ ok: false,
1108
+ code: "E_VERIFY_JWKS_INVALID",
1109
+ message: "JWKS missing required keys array"
1110
+ };
1111
+ }
1112
+ if (jwks.keys.length > kernel.VERIFIER_LIMITS.maxJwksKeys) {
1113
+ return {
1114
+ ok: false,
1115
+ code: "E_VERIFY_JWKS_TOO_MANY_KEYS",
1116
+ message: `JWKS has too many keys: ${jwks.keys.length} > ${kernel.VERIFIER_LIMITS.maxJwksKeys}`
1117
+ };
1118
+ }
1119
+ if (!noCache) {
1120
+ cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs }, maxCacheEntries);
1121
+ }
1122
+ return {
1123
+ ok: true,
1124
+ jwks,
1125
+ fromCache: false,
1126
+ rawBytes: jwksResult.rawBytes
1127
+ };
1128
+ }
1129
+
1130
+ // src/verify.ts
1131
+ function jwkToPublicKey(jwk) {
1132
+ if (jwk.kty !== "OKP" || jwk.crv !== "Ed25519") {
1133
+ throw new Error("Only Ed25519 keys (OKP/Ed25519) are supported");
1134
+ }
1135
+ const xBytes = Buffer.from(jwk.x, "base64url");
1136
+ if (xBytes.length !== 32) {
1137
+ throw new Error("Ed25519 public key must be 32 bytes");
1138
+ }
1139
+ return new Uint8Array(xBytes);
1140
+ }
1141
+ async function verifyReceipt(optionsOrJws) {
1142
+ const receiptJws = typeof optionsOrJws === "string" ? optionsOrJws : optionsOrJws.receiptJws;
1143
+ const inputSnapshot = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.subject_snapshot;
1144
+ const telemetry = typeof optionsOrJws === "string" ? void 0 : optionsOrJws.telemetry;
1145
+ const startTime = performance.now();
1146
+ let jwksFetchTime;
1147
+ try {
1148
+ const { header, payload } = crypto.decode(receiptJws);
1149
+ const constraintResult = schema.validateKernelConstraints(payload);
1150
+ if (!constraintResult.valid) {
1151
+ const v = constraintResult.violations[0];
1152
+ return {
1153
+ ok: false,
1154
+ reason: "constraint_violation",
1155
+ details: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`
1156
+ };
1157
+ }
1158
+ schema.ReceiptClaims.parse(payload);
1159
+ if (payload.exp && payload.exp < Math.floor(Date.now() / 1e3)) {
1160
+ const durationMs = performance.now() - startTime;
1161
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1162
+ receiptHash: hashReceipt(receiptJws),
1163
+ valid: false,
1164
+ reasonCode: "expired",
1165
+ issuer: payload.iss,
1166
+ kid: header.kid,
1167
+ durationMs
1168
+ });
1169
+ return {
1170
+ ok: false,
1171
+ reason: "expired",
1172
+ details: `Receipt expired at ${new Date(payload.exp * 1e3).toISOString()}`
1173
+ };
1174
+ }
1175
+ const jwksFetchStart = performance.now();
1176
+ const jwksResult = await resolveJWKS(payload.iss);
1177
+ if (!jwksResult.ok) {
1178
+ return {
1179
+ ok: false,
1180
+ reason: jwksResult.code,
1181
+ details: jwksResult.message
1182
+ };
1183
+ }
1184
+ if (!jwksResult.fromCache) {
1185
+ jwksFetchTime = performance.now() - jwksFetchStart;
1186
+ }
1187
+ const jwk = jwksResult.jwks.keys.find((k) => k.kid === header.kid);
1188
+ if (!jwk) {
1189
+ const durationMs = performance.now() - startTime;
1190
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1191
+ receiptHash: hashReceipt(receiptJws),
1192
+ valid: false,
1193
+ reasonCode: "unknown_key",
1194
+ issuer: payload.iss,
1195
+ kid: header.kid,
1196
+ durationMs
1197
+ });
1198
+ return {
1199
+ ok: false,
1200
+ reason: "unknown_key",
1201
+ details: `No key found with kid=${header.kid}`
1202
+ };
1203
+ }
1204
+ const publicKey = jwkToPublicKey(jwk);
1205
+ const result = await crypto.verify(receiptJws, publicKey);
1206
+ if (!result.valid) {
1207
+ const durationMs = performance.now() - startTime;
1208
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1209
+ receiptHash: hashReceipt(receiptJws),
1210
+ valid: false,
1211
+ reasonCode: "invalid_signature",
1212
+ issuer: payload.iss,
1213
+ kid: header.kid,
1214
+ durationMs
1215
+ });
1216
+ return {
1217
+ ok: false,
1218
+ reason: "invalid_signature",
1219
+ details: "Ed25519 signature verification failed"
1220
+ };
1221
+ }
1222
+ const validatedSnapshot = schema.validateSubjectSnapshot(inputSnapshot);
1223
+ const verifyTime = performance.now() - startTime;
1224
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1225
+ receiptHash: hashReceipt(receiptJws),
1226
+ valid: true,
1227
+ issuer: payload.iss,
1228
+ kid: header.kid,
1229
+ durationMs: verifyTime
1230
+ });
1231
+ return {
1232
+ ok: true,
1233
+ claims: payload,
1234
+ ...validatedSnapshot && { subject_snapshot: validatedSnapshot },
1235
+ perf: {
1236
+ verify_ms: verifyTime,
1237
+ ...jwksFetchTime && { jwks_fetch_ms: jwksFetchTime }
1238
+ }
1239
+ };
1240
+ } catch (err) {
1241
+ const durationMs = performance.now() - startTime;
1242
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1243
+ receiptHash: hashReceipt(receiptJws),
1244
+ valid: false,
1245
+ reasonCode: "verification_error",
1246
+ durationMs
1247
+ });
1248
+ return {
1249
+ ok: false,
1250
+ reason: "verification_error",
1251
+ details: err instanceof Error ? err.message : String(err)
1252
+ };
1253
+ }
1254
+ }
1255
+ function isCryptoError(err) {
1256
+ return err !== null && typeof err === "object" && "name" in err && err.name === "CryptoError" && "code" in err && typeof err.code === "string" && err.code.startsWith("CRYPTO_") && "message" in err && typeof err.message === "string";
1257
+ }
1258
+ var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
1259
+ "CRYPTO_INVALID_JWS_FORMAT",
1260
+ "CRYPTO_INVALID_TYP",
1261
+ "CRYPTO_INVALID_ALG",
1262
+ "CRYPTO_INVALID_KEY_LENGTH"
1263
+ ]);
1264
+ var MAX_PARSE_ISSUES = 25;
1265
+ function sanitizeParseIssues(issues) {
1266
+ if (!Array.isArray(issues)) return void 0;
1267
+ return issues.slice(0, MAX_PARSE_ISSUES).map((issue2) => ({
1268
+ path: Array.isArray(issue2?.path) ? issue2.path.join(".") : "",
1269
+ message: typeof issue2?.message === "string" ? issue2.message : String(issue2)
1270
+ }));
1271
+ }
1272
+ async function verifyLocal(jws, publicKey, options = {}) {
1273
+ const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
1274
+ const now = options.now ?? Math.floor(Date.now() / 1e3);
1275
+ try {
1276
+ const result = await crypto.verify(jws, publicKey);
1277
+ if (!result.valid) {
1278
+ return {
1279
+ valid: false,
1280
+ code: "E_INVALID_SIGNATURE",
1281
+ message: "Ed25519 signature verification failed"
1282
+ };
1283
+ }
1284
+ const constraintResult = schema.validateKernelConstraints(result.payload);
1285
+ if (!constraintResult.valid) {
1286
+ const v = constraintResult.violations[0];
1287
+ return {
1288
+ valid: false,
1289
+ code: "E_CONSTRAINT_VIOLATION",
1290
+ message: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`
1291
+ };
1292
+ }
1293
+ const pr = schema.parseReceiptClaims(result.payload);
1294
+ if (!pr.ok) {
1295
+ return {
1296
+ valid: false,
1297
+ code: "E_INVALID_FORMAT",
1298
+ message: `Receipt schema validation failed: ${pr.error.message}`,
1299
+ details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
1300
+ };
1301
+ }
1302
+ if (issuer !== void 0 && pr.claims.iss !== issuer) {
1303
+ return {
1304
+ valid: false,
1305
+ code: "E_INVALID_ISSUER",
1306
+ message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
1307
+ };
1308
+ }
1309
+ if (audience !== void 0 && pr.claims.aud !== audience) {
1310
+ return {
1311
+ valid: false,
1312
+ code: "E_INVALID_AUDIENCE",
1313
+ message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
1314
+ };
1315
+ }
1316
+ if (rid !== void 0 && pr.claims.rid !== rid) {
1317
+ return {
1318
+ valid: false,
1319
+ code: "E_INVALID_RECEIPT_ID",
1320
+ message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
1321
+ };
1322
+ }
1323
+ if (requireExp && pr.claims.exp === void 0) {
1324
+ return {
1325
+ valid: false,
1326
+ code: "E_MISSING_EXP",
1327
+ message: "Receipt missing required exp claim"
1328
+ };
1329
+ }
1330
+ if (pr.claims.iat > now + maxClockSkew) {
1331
+ return {
1332
+ valid: false,
1333
+ code: "E_NOT_YET_VALID",
1334
+ message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1335
+ };
1336
+ }
1337
+ if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
1338
+ return {
1339
+ valid: false,
1340
+ code: "E_EXPIRED",
1341
+ message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
1342
+ };
1343
+ }
1344
+ if (pr.variant === "commerce") {
1345
+ const claims = pr.claims;
1346
+ if (subjectUri !== void 0 && claims.subject?.uri !== subjectUri) {
1347
+ return {
1348
+ valid: false,
1349
+ code: "E_INVALID_SUBJECT",
1350
+ message: `Subject mismatch: expected "${subjectUri}", got "${claims.subject?.uri ?? "undefined"}"`
1351
+ };
1352
+ }
1353
+ return {
1354
+ valid: true,
1355
+ variant: "commerce",
1356
+ claims,
1357
+ kid: result.header.kid,
1358
+ policy_binding: "unavailable"
1359
+ };
1360
+ } else {
1361
+ const claims = pr.claims;
1362
+ if (subjectUri !== void 0 && claims.sub !== subjectUri) {
1363
+ return {
1364
+ valid: false,
1365
+ code: "E_INVALID_SUBJECT",
1366
+ message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
1367
+ };
1368
+ }
1369
+ return {
1370
+ valid: true,
1371
+ variant: "attestation",
1372
+ claims,
1373
+ kid: result.header.kid,
1374
+ policy_binding: "unavailable"
1347
1375
  };
1348
- } catch (err) {
1349
- if (err instanceof Error) {
1350
- if (err.name === "AbortError" || err.message.includes("timeout")) {
1351
- return {
1352
- ok: false,
1353
- reason: "timeout",
1354
- message: `Fetch timeout after ${timeoutMs}ms: ${currentUrl}`
1355
- };
1356
- }
1376
+ }
1377
+ } catch (err) {
1378
+ if (isCryptoError(err)) {
1379
+ if (FORMAT_ERROR_CODES.has(err.code)) {
1380
+ return {
1381
+ valid: false,
1382
+ code: "E_INVALID_FORMAT",
1383
+ message: err.message
1384
+ };
1385
+ }
1386
+ if (err.code === "CRYPTO_INVALID_SIGNATURE") {
1387
+ return {
1388
+ valid: false,
1389
+ code: "E_INVALID_SIGNATURE",
1390
+ message: err.message
1391
+ };
1357
1392
  }
1393
+ }
1394
+ if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
1395
+ const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
1358
1396
  return {
1359
- ok: false,
1360
- reason: "network_error",
1361
- message: `Network error: ${err instanceof Error ? err.message : String(err)}`
1397
+ valid: false,
1398
+ code: "E_INVALID_FORMAT",
1399
+ message: `Invalid receipt payload: ${syntaxMessage}`
1362
1400
  };
1363
1401
  }
1402
+ const message = err instanceof Error ? err.message : String(err);
1403
+ return {
1404
+ valid: false,
1405
+ code: "E_INTERNAL",
1406
+ message: `Unexpected verification error: ${message}`
1407
+ };
1364
1408
  }
1365
1409
  }
1366
- async function fetchJWKSSafe(jwksUrl, options) {
1367
- return ssrfSafeFetch(jwksUrl, {
1368
- ...options,
1369
- maxBytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
1370
- headers: {
1371
- Accept: "application/json",
1372
- ...options?.headers
1410
+ function isCommerceResult(r) {
1411
+ return r.valid === true && r.variant === "commerce";
1412
+ }
1413
+ function isAttestationResult(r) {
1414
+ return r.valid === true && r.variant === "attestation";
1415
+ }
1416
+ function setReceiptHeader(headers, receiptJws) {
1417
+ headers.set(schema.PEAC_RECEIPT_HEADER, receiptJws);
1418
+ }
1419
+ function getReceiptHeader(headers) {
1420
+ return headers.get(schema.PEAC_RECEIPT_HEADER);
1421
+ }
1422
+ function setVaryHeader(headers) {
1423
+ const existing = headers.get("Vary");
1424
+ if (existing) {
1425
+ const varies = existing.split(",").map((v) => v.trim());
1426
+ if (!varies.includes(schema.PEAC_RECEIPT_HEADER)) {
1427
+ headers.set("Vary", `${existing}, ${schema.PEAC_RECEIPT_HEADER}`);
1373
1428
  }
1374
- });
1429
+ } else {
1430
+ headers.set("Vary", schema.PEAC_RECEIPT_HEADER);
1431
+ }
1375
1432
  }
1376
- async function fetchPointerSafe(pointerUrl, options) {
1377
- return ssrfSafeFetch(pointerUrl, {
1378
- ...options,
1379
- maxBytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
1380
- headers: {
1381
- Accept: "application/jose, application/json",
1382
- ...options?.headers
1433
+ function getPurposeHeader(headers) {
1434
+ const value = headers.get(schema.PEAC_PURPOSE_HEADER);
1435
+ if (!value) {
1436
+ return [];
1437
+ }
1438
+ return schema.parsePurposeHeader(value);
1439
+ }
1440
+ function setPurposeAppliedHeader(headers, purpose) {
1441
+ headers.set(schema.PEAC_PURPOSE_APPLIED_HEADER, purpose);
1442
+ }
1443
+ function setPurposeReasonHeader(headers, reason) {
1444
+ headers.set(schema.PEAC_PURPOSE_REASON_HEADER, reason);
1445
+ }
1446
+ function setVaryPurposeHeader(headers) {
1447
+ const existing = headers.get("Vary");
1448
+ if (existing) {
1449
+ const varies = existing.split(",").map((v) => v.trim());
1450
+ if (!varies.includes(schema.PEAC_PURPOSE_HEADER)) {
1451
+ headers.set("Vary", `${existing}, ${schema.PEAC_PURPOSE_HEADER}`);
1383
1452
  }
1384
- });
1453
+ } else {
1454
+ headers.set("Vary", schema.PEAC_PURPOSE_HEADER);
1455
+ }
1456
+ }
1457
+ var DEFAULT_VERIFIER_LIMITS = {
1458
+ max_receipt_bytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
1459
+ max_jwks_bytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
1460
+ max_jwks_keys: kernel.VERIFIER_LIMITS.maxJwksKeys,
1461
+ max_redirects: kernel.VERIFIER_LIMITS.maxRedirects,
1462
+ fetch_timeout_ms: kernel.VERIFIER_LIMITS.fetchTimeoutMs,
1463
+ max_extension_bytes: kernel.VERIFIER_LIMITS.maxExtensionBytes
1464
+ };
1465
+ var DEFAULT_NETWORK_SECURITY = {
1466
+ https_only: kernel.VERIFIER_NETWORK.httpsOnly,
1467
+ block_private_ips: kernel.VERIFIER_NETWORK.blockPrivateIps,
1468
+ allow_redirects: kernel.VERIFIER_NETWORK.allowRedirects,
1469
+ allow_cross_origin_redirects: true,
1470
+ // Allow for CDN compatibility
1471
+ dns_failure_behavior: "block"
1472
+ // Fail-closed by default
1473
+ };
1474
+ function createDefaultPolicy(mode) {
1475
+ return {
1476
+ policy_version: kernel.VERIFIER_POLICY_VERSION,
1477
+ mode,
1478
+ limits: { ...DEFAULT_VERIFIER_LIMITS },
1479
+ network: { ...DEFAULT_NETWORK_SECURITY }
1480
+ };
1481
+ }
1482
+ var CHECK_IDS = [
1483
+ "jws.parse",
1484
+ "limits.receipt_bytes",
1485
+ "jws.protected_header",
1486
+ "claims.schema_unverified",
1487
+ "issuer.trust_policy",
1488
+ "issuer.discovery",
1489
+ "key.resolve",
1490
+ "jws.signature",
1491
+ "claims.time_window",
1492
+ "extensions.limits",
1493
+ "transport.profile_binding",
1494
+ "policy.binding"
1495
+ ];
1496
+ var NON_DETERMINISTIC_ARTIFACT_KEYS = [
1497
+ "issuer_jwks_digest"
1498
+ ];
1499
+ function createDigest(hexValue) {
1500
+ return {
1501
+ alg: "sha-256",
1502
+ value: hexValue.toLowerCase()
1503
+ };
1504
+ }
1505
+ function createEmptyReport(policy) {
1506
+ return {
1507
+ report_version: kernel.VERIFICATION_REPORT_VERSION,
1508
+ policy
1509
+ };
1510
+ }
1511
+ function ssrfErrorToReasonCode(ssrfReason, fetchType) {
1512
+ const prefix = fetchType === "key" ? "key_fetch" : "pointer_fetch";
1513
+ switch (ssrfReason) {
1514
+ case "not_https":
1515
+ case "private_ip":
1516
+ case "loopback":
1517
+ case "link_local":
1518
+ case "cross_origin_redirect":
1519
+ case "dns_failure":
1520
+ return `${prefix}_blocked`;
1521
+ case "timeout":
1522
+ return `${prefix}_timeout`;
1523
+ case "response_too_large":
1524
+ return fetchType === "pointer" ? "pointer_fetch_too_large" : "jwks_too_large";
1525
+ case "jwks_too_many_keys":
1526
+ return "jwks_too_many_keys";
1527
+ case "too_many_redirects":
1528
+ case "scheme_downgrade":
1529
+ case "network_error":
1530
+ case "invalid_url":
1531
+ default:
1532
+ return `${prefix}_failed`;
1533
+ }
1534
+ }
1535
+ function reasonCodeToSeverity(reason) {
1536
+ if (reason === "ok") return "info";
1537
+ return "error";
1538
+ }
1539
+ function reasonCodeToErrorCode(reason) {
1540
+ const mapping = {
1541
+ ok: "",
1542
+ receipt_too_large: "E_VERIFY_RECEIPT_TOO_LARGE",
1543
+ malformed_receipt: "E_VERIFY_MALFORMED_RECEIPT",
1544
+ signature_invalid: "E_VERIFY_SIGNATURE_INVALID",
1545
+ issuer_not_allowed: "E_VERIFY_ISSUER_NOT_ALLOWED",
1546
+ key_not_found: "E_VERIFY_KEY_NOT_FOUND",
1547
+ key_fetch_blocked: "E_VERIFY_KEY_FETCH_BLOCKED",
1548
+ key_fetch_failed: "E_VERIFY_KEY_FETCH_FAILED",
1549
+ key_fetch_timeout: "E_VERIFY_KEY_FETCH_TIMEOUT",
1550
+ pointer_fetch_blocked: "E_VERIFY_POINTER_FETCH_BLOCKED",
1551
+ pointer_fetch_failed: "E_VERIFY_POINTER_FETCH_FAILED",
1552
+ pointer_fetch_timeout: "E_VERIFY_POINTER_FETCH_TIMEOUT",
1553
+ pointer_fetch_too_large: "E_VERIFY_POINTER_FETCH_TOO_LARGE",
1554
+ pointer_digest_mismatch: "E_VERIFY_POINTER_DIGEST_MISMATCH",
1555
+ jwks_too_large: "E_VERIFY_JWKS_TOO_LARGE",
1556
+ jwks_too_many_keys: "E_VERIFY_JWKS_TOO_MANY_KEYS",
1557
+ expired: "E_VERIFY_EXPIRED",
1558
+ not_yet_valid: "E_VERIFY_NOT_YET_VALID",
1559
+ audience_mismatch: "E_VERIFY_AUDIENCE_MISMATCH",
1560
+ schema_invalid: "E_VERIFY_SCHEMA_INVALID",
1561
+ policy_violation: "E_VERIFY_POLICY_VIOLATION",
1562
+ extension_too_large: "E_VERIFY_EXTENSION_TOO_LARGE",
1563
+ invalid_transport: "E_VERIFY_INVALID_TRANSPORT"
1564
+ };
1565
+ return mapping[reason] || "E_VERIFY_POLICY_VIOLATION";
1385
1566
  }
1386
1567
  var VerificationReportBuilder = class {
1387
1568
  state;
@@ -1650,8 +1831,6 @@ async function buildSuccessReport(policy, receiptBytes, issuer, kid, checkDetail
1650
1831
  }
1651
1832
 
1652
1833
  // src/verifier-core.ts
1653
- var jwksCache2 = /* @__PURE__ */ new Map();
1654
- var CACHE_TTL_MS2 = 5 * 60 * 1e3;
1655
1834
  function normalizeIssuer(issuer) {
1656
1835
  try {
1657
1836
  const url = new URL(issuer);
@@ -1677,75 +1856,23 @@ function findPinnedKey(issuer, kid, pinnedKeys) {
1677
1856
  const normalizedIssuer = normalizeIssuer(issuer);
1678
1857
  return pinnedKeys.find((pk) => normalizeIssuer(pk.issuer) === normalizedIssuer && pk.kid === kid);
1679
1858
  }
1680
- async function fetchIssuerConfig2(issuerOrigin) {
1681
- const configUrl = `${issuerOrigin}/.well-known/peac-issuer.json`;
1682
- const result = await ssrfSafeFetch(configUrl, {
1683
- maxBytes: 65536,
1684
- // 64 KB
1685
- headers: { Accept: "application/json" }
1686
- });
1687
- if (!result.ok) {
1688
- return null;
1689
- }
1690
- try {
1691
- return JSON.parse(result.body);
1692
- } catch {
1693
- return null;
1694
- }
1695
- }
1696
1859
  async function fetchIssuerJWKS(issuerOrigin) {
1697
- const now = Date.now();
1698
- const cached = jwksCache2.get(issuerOrigin);
1699
- if (cached && cached.expiresAt > now) {
1700
- return { jwks: cached.jwks, fromCache: true };
1701
- }
1702
- const config = await fetchIssuerConfig2(issuerOrigin);
1703
- if (!config?.jwks_uri) {
1704
- const fallbackUrl = `${issuerOrigin}/.well-known/jwks.json`;
1705
- const result2 = await fetchJWKSSafe(fallbackUrl);
1706
- if (!result2.ok) {
1707
- return { error: result2 };
1708
- }
1709
- try {
1710
- const jwks = JSON.parse(result2.body);
1711
- jwksCache2.set(issuerOrigin, { jwks, expiresAt: now + CACHE_TTL_MS2 });
1712
- return { jwks, fromCache: false, rawBytes: result2.rawBytes };
1713
- } catch {
1714
- return {
1715
- error: {
1716
- ok: false,
1717
- reason: "network_error",
1718
- message: "Invalid JWKS JSON"
1719
- }
1720
- };
1721
- }
1722
- }
1723
- const result = await fetchJWKSSafe(config.jwks_uri);
1860
+ const result = await resolveJWKS(issuerOrigin);
1724
1861
  if (!result.ok) {
1725
- return { error: result };
1726
- }
1727
- try {
1728
- const jwks = JSON.parse(result.body);
1729
- if (jwks.keys.length > kernel.VERIFIER_LIMITS.maxJwksKeys) {
1730
- return {
1731
- error: {
1732
- ok: false,
1733
- reason: "jwks_too_many_keys",
1734
- message: `JWKS has too many keys: ${jwks.keys.length} > ${kernel.VERIFIER_LIMITS.maxJwksKeys}`
1735
- }
1736
- };
1737
- }
1738
- jwksCache2.set(issuerOrigin, { jwks, expiresAt: now + CACHE_TTL_MS2 });
1739
- return { jwks, fromCache: false, rawBytes: result.rawBytes };
1740
- } catch {
1741
1862
  return {
1742
1863
  error: {
1743
1864
  ok: false,
1744
- reason: "network_error",
1745
- message: "Invalid JWKS JSON"
1865
+ reason: result.reason ?? "network_error",
1866
+ message: result.message,
1867
+ blockedUrl: result.blockedUrl
1746
1868
  }
1747
1869
  };
1748
1870
  }
1871
+ return {
1872
+ jwks: result.jwks,
1873
+ fromCache: result.fromCache,
1874
+ rawBytes: result.rawBytes
1875
+ };
1749
1876
  }
1750
1877
  async function verifyReceiptCore(options) {
1751
1878
  const {
@@ -2082,12 +2209,6 @@ async function verifyReceiptCore(options) {
2082
2209
  claims: parsedClaims
2083
2210
  };
2084
2211
  }
2085
- function clearJWKSCache() {
2086
- jwksCache2.clear();
2087
- }
2088
- function getJWKSCacheSize() {
2089
- return jwksCache2.size;
2090
- }
2091
2212
 
2092
2213
  // src/transport-profiles.ts
2093
2214
  function parseHeaderProfile(headerValue) {
@@ -2679,6 +2800,7 @@ exports.parseTransportProfile = parseTransportProfile;
2679
2800
  exports.reasonCodeToErrorCode = reasonCodeToErrorCode;
2680
2801
  exports.reasonCodeToSeverity = reasonCodeToSeverity;
2681
2802
  exports.resetSSRFCapabilitiesCache = resetSSRFCapabilitiesCache;
2803
+ exports.resolveJWKS = resolveJWKS;
2682
2804
  exports.setPurposeAppliedHeader = setPurposeAppliedHeader;
2683
2805
  exports.setPurposeReasonHeader = setPurposeReasonHeader;
2684
2806
  exports.setReceiptHeader = setReceiptHeader;