@peac/protocol 0.11.3 → 0.12.0-preview.2

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
@@ -179,6 +179,52 @@ async function issueJws(options) {
179
179
  const result = await issue(options);
180
180
  return result.jws;
181
181
  }
182
+ async function issueWire02(options) {
183
+ if (!schema.isCanonicalIss(options.iss)) {
184
+ throw new IssueError({
185
+ code: "E_ISS_NOT_CANONICAL",
186
+ category: "validation",
187
+ severity: "error",
188
+ retryable: false,
189
+ http_status: 400,
190
+ details: {
191
+ message: `iss is not in canonical form: "${options.iss}". Use an https://<origin> or did:<method> identifier.`
192
+ }
193
+ });
194
+ }
195
+ const jti = options.jti ?? uuidv7.uuidv7();
196
+ const iat = Math.floor(Date.now() / 1e3);
197
+ const claims = {
198
+ peac_version: "0.2",
199
+ kind: options.kind,
200
+ type: options.type,
201
+ iss: options.iss,
202
+ iat,
203
+ jti,
204
+ ...options.sub !== void 0 && { sub: options.sub },
205
+ ...options.pillars !== void 0 && { pillars: options.pillars },
206
+ ...options.occurred_at !== void 0 && { occurred_at: options.occurred_at },
207
+ ...options.purpose_declared !== void 0 && { purpose_declared: options.purpose_declared },
208
+ ...options.policy !== void 0 && { policy: options.policy },
209
+ ...options.extensions !== void 0 && { extensions: options.extensions }
210
+ };
211
+ const parseResult = schema.Wire02ClaimsSchema.safeParse(claims);
212
+ if (!parseResult.success) {
213
+ const firstIssue = parseResult.error.issues[0];
214
+ throw new IssueError({
215
+ code: "E_INVALID_FORMAT",
216
+ category: "validation",
217
+ severity: "error",
218
+ retryable: false,
219
+ http_status: 400,
220
+ details: {
221
+ message: `Wire 0.2 claims schema validation failed: ${firstIssue?.message ?? "unknown"}`
222
+ }
223
+ });
224
+ }
225
+ const jws = await crypto.signWire02(claims, options.privateKey, options.kid);
226
+ return { jws };
227
+ }
182
228
  function parseIssuerConfig(json) {
183
229
  let config;
184
230
  if (typeof json === "string") {
@@ -1353,6 +1399,13 @@ var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
1353
1399
  "CRYPTO_INVALID_ALG",
1354
1400
  "CRYPTO_INVALID_KEY_LENGTH"
1355
1401
  ]);
1402
+ var JOSE_CODE_MAP = {
1403
+ CRYPTO_JWS_EMBEDDED_KEY: "E_JWS_EMBEDDED_KEY",
1404
+ CRYPTO_JWS_CRIT_REJECTED: "E_JWS_CRIT_REJECTED",
1405
+ CRYPTO_JWS_MISSING_KID: "E_JWS_MISSING_KID",
1406
+ CRYPTO_JWS_B64_REJECTED: "E_JWS_B64_REJECTED",
1407
+ CRYPTO_JWS_ZIP_REJECTED: "E_JWS_ZIP_REJECTED"
1408
+ };
1356
1409
  var MAX_PARSE_ISSUES = 25;
1357
1410
  function sanitizeParseIssues(issues) {
1358
1411
  if (!Array.isArray(issues)) return void 0;
@@ -1362,7 +1415,7 @@ function sanitizeParseIssues(issues) {
1362
1415
  }));
1363
1416
  }
1364
1417
  async function verifyLocal(jws, publicKey, options = {}) {
1365
- const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
1418
+ const { issuer, subjectUri, maxClockSkew = 300, strictness = "strict", policyDigest } = options;
1366
1419
  const now = options.now ?? Math.floor(Date.now() / 1e3);
1367
1420
  try {
1368
1421
  const result = await crypto.verify(jws, publicKey);
@@ -1373,6 +1426,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
1373
1426
  message: "Ed25519 signature verification failed"
1374
1427
  };
1375
1428
  }
1429
+ const accumulatedWarnings = [];
1430
+ if (result.header.typ === void 0) {
1431
+ if (strictness === "strict") {
1432
+ return {
1433
+ valid: false,
1434
+ code: "E_INVALID_FORMAT",
1435
+ message: "Missing JWS typ header: strict mode requires typ to be present"
1436
+ };
1437
+ }
1438
+ accumulatedWarnings.push({
1439
+ code: schema.WARNING_TYP_MISSING,
1440
+ message: "JWS typ header is absent; accepted in interop mode"
1441
+ });
1442
+ }
1376
1443
  const constraintResult = schema.validateKernelConstraints(result.payload);
1377
1444
  if (!constraintResult.valid) {
1378
1445
  const v = constraintResult.violations[0];
@@ -1391,66 +1458,18 @@ async function verifyLocal(jws, publicKey, options = {}) {
1391
1458
  details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
1392
1459
  };
1393
1460
  }
1394
- if (issuer !== void 0 && pr.claims.iss !== issuer) {
1395
- return {
1396
- valid: false,
1397
- code: "E_INVALID_ISSUER",
1398
- message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
1399
- };
1461
+ if (pr.wireVersion === "0.2") {
1462
+ accumulatedWarnings.push(...pr.warnings);
1400
1463
  }
1401
- if (audience !== void 0 && pr.claims.aud !== audience) {
1402
- return {
1403
- valid: false,
1404
- code: "E_INVALID_AUDIENCE",
1405
- message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
1406
- };
1407
- }
1408
- if (rid !== void 0 && pr.claims.rid !== rid) {
1409
- return {
1410
- valid: false,
1411
- code: "E_INVALID_RECEIPT_ID",
1412
- message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
1413
- };
1414
- }
1415
- if (requireExp && pr.claims.exp === void 0) {
1416
- return {
1417
- valid: false,
1418
- code: "E_MISSING_EXP",
1419
- message: "Receipt missing required exp claim"
1420
- };
1421
- }
1422
- if (pr.claims.iat > now + maxClockSkew) {
1423
- return {
1424
- valid: false,
1425
- code: "E_NOT_YET_VALID",
1426
- message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1427
- };
1428
- }
1429
- if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
1430
- return {
1431
- valid: false,
1432
- code: "E_EXPIRED",
1433
- message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
1434
- };
1435
- }
1436
- if (pr.variant === "commerce") {
1464
+ if (pr.wireVersion === "0.2") {
1437
1465
  const claims = pr.claims;
1438
- if (subjectUri !== void 0 && claims.subject?.uri !== subjectUri) {
1466
+ if (issuer !== void 0 && claims.iss !== issuer) {
1439
1467
  return {
1440
1468
  valid: false,
1441
- code: "E_INVALID_SUBJECT",
1442
- message: `Subject mismatch: expected "${subjectUri}", got "${claims.subject?.uri ?? "undefined"}"`
1469
+ code: "E_INVALID_ISSUER",
1470
+ message: `Issuer mismatch: expected "${issuer}", got "${claims.iss}"`
1443
1471
  };
1444
1472
  }
1445
- return {
1446
- valid: true,
1447
- variant: "commerce",
1448
- claims,
1449
- kid: result.header.kid,
1450
- policy_binding: "unavailable"
1451
- };
1452
- } else {
1453
- const claims = pr.claims;
1454
1473
  if (subjectUri !== void 0 && claims.sub !== subjectUri) {
1455
1474
  return {
1456
1475
  valid: false,
@@ -1458,16 +1477,90 @@ async function verifyLocal(jws, publicKey, options = {}) {
1458
1477
  message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
1459
1478
  };
1460
1479
  }
1480
+ if (claims.iat > now + maxClockSkew) {
1481
+ return {
1482
+ valid: false,
1483
+ code: "E_NOT_YET_VALID",
1484
+ message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1485
+ };
1486
+ }
1487
+ if (claims.kind === "evidence") {
1488
+ const skewResult = schema.checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);
1489
+ if (skewResult === "future_error") {
1490
+ return {
1491
+ valid: false,
1492
+ code: "E_OCCURRED_AT_FUTURE",
1493
+ message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`
1494
+ };
1495
+ }
1496
+ if (skewResult !== null) {
1497
+ accumulatedWarnings.push(skewResult);
1498
+ }
1499
+ }
1500
+ if (!schema.REGISTERED_RECEIPT_TYPES.has(claims.type)) {
1501
+ accumulatedWarnings.push({
1502
+ code: schema.WARNING_TYPE_UNREGISTERED,
1503
+ message: "Receipt type is not in the recommended type registry",
1504
+ pointer: "/type"
1505
+ });
1506
+ }
1507
+ if (claims.extensions !== void 0) {
1508
+ for (const key of Object.keys(claims.extensions)) {
1509
+ if (!schema.REGISTERED_EXTENSION_GROUP_KEYS.has(key) && schema.isValidExtensionKey(key)) {
1510
+ const escapedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
1511
+ accumulatedWarnings.push({
1512
+ code: schema.WARNING_UNKNOWN_EXTENSION,
1513
+ message: "Unknown extension key preserved without schema validation",
1514
+ pointer: `/extensions/${escapedKey}`
1515
+ });
1516
+ }
1517
+ }
1518
+ }
1519
+ if (policyDigest !== void 0 && !kernel.HASH.pattern.test(policyDigest)) {
1520
+ return {
1521
+ valid: false,
1522
+ code: "E_INVALID_FORMAT",
1523
+ message: "policyDigest option must be in sha256:<64 lowercase hex> format"
1524
+ };
1525
+ }
1526
+ const receiptPolicyDigest = claims.policy?.digest;
1527
+ const bindingStatus = receiptPolicyDigest === void 0 || policyDigest === void 0 ? "unavailable" : schema.verifyPolicyBinding(receiptPolicyDigest, policyDigest);
1528
+ if (bindingStatus === "failed") {
1529
+ return {
1530
+ valid: false,
1531
+ code: "E_POLICY_BINDING_FAILED",
1532
+ message: "Policy binding check failed: receipt policy digest does not match local policy",
1533
+ details: {
1534
+ receipt_policy_digest: receiptPolicyDigest,
1535
+ local_policy_digest: policyDigest,
1536
+ ...claims.policy?.uri !== void 0 && { policy_uri: claims.policy.uri }
1537
+ }
1538
+ };
1539
+ }
1461
1540
  return {
1462
1541
  valid: true,
1463
- variant: "attestation",
1542
+ variant: "wire-02",
1464
1543
  claims,
1465
1544
  kid: result.header.kid,
1466
- policy_binding: "unavailable"
1545
+ wireVersion: "0.2",
1546
+ warnings: schema.sortWarnings(accumulatedWarnings),
1547
+ policy_binding: bindingStatus
1467
1548
  };
1468
1549
  }
1550
+ return {
1551
+ valid: false,
1552
+ code: "E_UNSUPPORTED_WIRE_VERSION",
1553
+ message: "Wire 0.1 receipts are not supported. Re-issue as Wire 0.2 using issueWire02()."
1554
+ };
1469
1555
  } catch (err) {
1470
1556
  if (isCryptoError(err)) {
1557
+ if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {
1558
+ return {
1559
+ valid: false,
1560
+ code: JOSE_CODE_MAP[err.code],
1561
+ message: err.message
1562
+ };
1563
+ }
1471
1564
  if (FORMAT_ERROR_CODES.has(err.code)) {
1472
1565
  return {
1473
1566
  valid: false,
@@ -1482,6 +1575,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
1482
1575
  message: err.message
1483
1576
  };
1484
1577
  }
1578
+ if (err.code === "CRYPTO_WIRE_VERSION_MISMATCH") {
1579
+ return {
1580
+ valid: false,
1581
+ code: "E_WIRE_VERSION_MISMATCH",
1582
+ message: err.message
1583
+ };
1584
+ }
1485
1585
  }
1486
1586
  if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
1487
1587
  const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
@@ -1500,10 +1600,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
1500
1600
  }
1501
1601
  }
1502
1602
  function isCommerceResult(r) {
1503
- return r.valid === true && r.variant === "commerce";
1603
+ return false;
1504
1604
  }
1505
1605
  function isAttestationResult(r) {
1506
- return r.valid === true && r.variant === "attestation";
1606
+ return false;
1607
+ }
1608
+ function isWire02Result(r) {
1609
+ return r.valid === true && r.variant === "wire-02";
1507
1610
  }
1508
1611
  function setReceiptHeader(headers, receiptJws) {
1509
1612
  headers.set(schema.PEAC_RECEIPT_HEADER, receiptJws);
@@ -1546,6 +1649,16 @@ function setVaryPurposeHeader(headers) {
1546
1649
  headers.set("Vary", schema.PEAC_PURPOSE_HEADER);
1547
1650
  }
1548
1651
  }
1652
+ async function computePolicyDigestJcs(policy) {
1653
+ const hex = await crypto.jcsHash(policy);
1654
+ return `${kernel.HASH.prefix}${hex}`;
1655
+ }
1656
+ function checkPolicyBinding(receiptDigest, localDigest) {
1657
+ if (receiptDigest === void 0 || localDigest === void 0) {
1658
+ return "unavailable";
1659
+ }
1660
+ return schema.verifyPolicyBinding(receiptDigest, localDigest);
1661
+ }
1549
1662
  var DEFAULT_VERIFIER_LIMITS = {
1550
1663
  max_receipt_bytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
1551
1664
  max_jwks_bytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
@@ -2861,8 +2974,10 @@ exports.NON_DETERMINISTIC_ARTIFACT_KEYS = NON_DETERMINISTIC_ARTIFACT_KEYS;
2861
2974
  exports.VerificationReportBuilder = VerificationReportBuilder;
2862
2975
  exports.buildFailureReport = buildFailureReport;
2863
2976
  exports.buildSuccessReport = buildSuccessReport;
2977
+ exports.checkPolicyBinding = checkPolicyBinding;
2864
2978
  exports.clearJWKSCache = clearJWKSCache;
2865
2979
  exports.clearKidThumbprints = clearKidThumbprints;
2980
+ exports.computePolicyDigestJcs = computePolicyDigestJcs;
2866
2981
  exports.computeReceiptDigest = computeReceiptDigest;
2867
2982
  exports.createDefaultPolicy = createDefaultPolicy;
2868
2983
  exports.createDigest = createDigest;
@@ -2882,8 +2997,10 @@ exports.getSSRFCapabilities = getSSRFCapabilities;
2882
2997
  exports.isAttestationResult = isAttestationResult;
2883
2998
  exports.isBlockedIP = isBlockedIP;
2884
2999
  exports.isCommerceResult = isCommerceResult;
3000
+ exports.isWire02Result = isWire02Result;
2885
3001
  exports.issue = issue;
2886
3002
  exports.issueJws = issueJws;
3003
+ exports.issueWire02 = issueWire02;
2887
3004
  exports.parseBodyProfile = parseBodyProfile;
2888
3005
  exports.parseDiscovery = parseDiscovery;
2889
3006
  exports.parseHeaderProfile = parseHeaderProfile;