@peac/protocol 0.11.3 → 0.12.0-preview.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
@@ -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 https:// origin or did: 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,16 @@ 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 {
1419
+ issuer,
1420
+ audience,
1421
+ subjectUri,
1422
+ rid,
1423
+ requireExp = false,
1424
+ maxClockSkew = 300,
1425
+ strictness = "strict",
1426
+ policyDigest
1427
+ } = options;
1366
1428
  const now = options.now ?? Math.floor(Date.now() / 1e3);
1367
1429
  try {
1368
1430
  const result = await crypto.verify(jws, publicKey);
@@ -1373,6 +1435,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
1373
1435
  message: "Ed25519 signature verification failed"
1374
1436
  };
1375
1437
  }
1438
+ const accumulatedWarnings = [];
1439
+ if (result.header.typ === void 0) {
1440
+ if (strictness === "strict") {
1441
+ return {
1442
+ valid: false,
1443
+ code: "E_INVALID_FORMAT",
1444
+ message: "Missing JWS typ header: strict mode requires typ to be present"
1445
+ };
1446
+ }
1447
+ accumulatedWarnings.push({
1448
+ code: schema.WARNING_TYP_MISSING,
1449
+ message: "JWS typ header is absent; accepted in interop mode"
1450
+ });
1451
+ }
1376
1452
  const constraintResult = schema.validateKernelConstraints(result.payload);
1377
1453
  if (!constraintResult.valid) {
1378
1454
  const v = constraintResult.violations[0];
@@ -1391,46 +1467,136 @@ async function verifyLocal(jws, publicKey, options = {}) {
1391
1467
  details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
1392
1468
  };
1393
1469
  }
1394
- if (issuer !== void 0 && pr.claims.iss !== issuer) {
1470
+ if (pr.wireVersion === "0.2") {
1471
+ accumulatedWarnings.push(...pr.warnings);
1472
+ }
1473
+ if (pr.wireVersion === "0.2") {
1474
+ const claims = pr.claims;
1475
+ if (issuer !== void 0 && claims.iss !== issuer) {
1476
+ return {
1477
+ valid: false,
1478
+ code: "E_INVALID_ISSUER",
1479
+ message: `Issuer mismatch: expected "${issuer}", got "${claims.iss}"`
1480
+ };
1481
+ }
1482
+ if (subjectUri !== void 0 && claims.sub !== subjectUri) {
1483
+ return {
1484
+ valid: false,
1485
+ code: "E_INVALID_SUBJECT",
1486
+ message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
1487
+ };
1488
+ }
1489
+ if (claims.iat > now + maxClockSkew) {
1490
+ return {
1491
+ valid: false,
1492
+ code: "E_NOT_YET_VALID",
1493
+ message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1494
+ };
1495
+ }
1496
+ if (claims.kind === "evidence") {
1497
+ const skewResult = schema.checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);
1498
+ if (skewResult === "future_error") {
1499
+ return {
1500
+ valid: false,
1501
+ code: "E_OCCURRED_AT_FUTURE",
1502
+ message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`
1503
+ };
1504
+ }
1505
+ if (skewResult !== null) {
1506
+ accumulatedWarnings.push(skewResult);
1507
+ }
1508
+ }
1509
+ if (!schema.REGISTERED_RECEIPT_TYPES.has(claims.type)) {
1510
+ accumulatedWarnings.push({
1511
+ code: schema.WARNING_TYPE_UNREGISTERED,
1512
+ message: "Receipt type is not in the recommended type registry",
1513
+ pointer: "/type"
1514
+ });
1515
+ }
1516
+ if (claims.extensions !== void 0) {
1517
+ for (const key of Object.keys(claims.extensions)) {
1518
+ if (!schema.REGISTERED_EXTENSION_GROUP_KEYS.has(key) && schema.isValidExtensionKey(key)) {
1519
+ const escapedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
1520
+ accumulatedWarnings.push({
1521
+ code: schema.WARNING_UNKNOWN_EXTENSION,
1522
+ message: "Unknown extension key preserved without schema validation",
1523
+ pointer: `/extensions/${escapedKey}`
1524
+ });
1525
+ }
1526
+ }
1527
+ }
1528
+ if (policyDigest !== void 0 && !kernel.HASH.pattern.test(policyDigest)) {
1529
+ return {
1530
+ valid: false,
1531
+ code: "E_INVALID_FORMAT",
1532
+ message: "policyDigest option must be in sha256:<64 lowercase hex> format"
1533
+ };
1534
+ }
1535
+ const receiptPolicyDigest = claims.policy?.digest;
1536
+ const bindingStatus = receiptPolicyDigest === void 0 || policyDigest === void 0 ? "unavailable" : schema.verifyPolicyBinding(receiptPolicyDigest, policyDigest);
1537
+ if (bindingStatus === "failed") {
1538
+ return {
1539
+ valid: false,
1540
+ code: "E_POLICY_BINDING_FAILED",
1541
+ message: "Policy binding check failed: receipt policy digest does not match local policy",
1542
+ details: {
1543
+ receipt_policy_digest: receiptPolicyDigest,
1544
+ local_policy_digest: policyDigest,
1545
+ ...claims.policy?.uri !== void 0 && { policy_uri: claims.policy.uri }
1546
+ }
1547
+ };
1548
+ }
1549
+ return {
1550
+ valid: true,
1551
+ variant: "wire-02",
1552
+ claims,
1553
+ kid: result.header.kid,
1554
+ wireVersion: "0.2",
1555
+ warnings: schema.sortWarnings(accumulatedWarnings),
1556
+ policy_binding: bindingStatus
1557
+ };
1558
+ }
1559
+ const w01 = pr.claims;
1560
+ if (issuer !== void 0 && w01.iss !== issuer) {
1395
1561
  return {
1396
1562
  valid: false,
1397
1563
  code: "E_INVALID_ISSUER",
1398
- message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
1564
+ message: `Issuer mismatch: expected "${issuer}", got "${w01.iss}"`
1399
1565
  };
1400
1566
  }
1401
- if (audience !== void 0 && pr.claims.aud !== audience) {
1567
+ if (audience !== void 0 && w01.aud !== audience) {
1402
1568
  return {
1403
1569
  valid: false,
1404
1570
  code: "E_INVALID_AUDIENCE",
1405
- message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
1571
+ message: `Audience mismatch: expected "${audience}", got "${w01.aud}"`
1406
1572
  };
1407
1573
  }
1408
- if (rid !== void 0 && pr.claims.rid !== rid) {
1574
+ if (rid !== void 0 && w01.rid !== rid) {
1409
1575
  return {
1410
1576
  valid: false,
1411
1577
  code: "E_INVALID_RECEIPT_ID",
1412
- message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
1578
+ message: `Receipt ID mismatch: expected "${rid}", got "${w01.rid}"`
1413
1579
  };
1414
1580
  }
1415
- if (requireExp && pr.claims.exp === void 0) {
1581
+ if (requireExp && w01.exp === void 0) {
1416
1582
  return {
1417
1583
  valid: false,
1418
1584
  code: "E_MISSING_EXP",
1419
1585
  message: "Receipt missing required exp claim"
1420
1586
  };
1421
1587
  }
1422
- if (pr.claims.iat > now + maxClockSkew) {
1588
+ if (w01.iat > now + maxClockSkew) {
1423
1589
  return {
1424
1590
  valid: false,
1425
1591
  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()}`
1592
+ message: `Receipt not yet valid: issued at ${new Date(w01.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
1427
1593
  };
1428
1594
  }
1429
- if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
1595
+ if (w01.exp !== void 0 && w01.exp < now - maxClockSkew) {
1430
1596
  return {
1431
1597
  valid: false,
1432
1598
  code: "E_EXPIRED",
1433
- message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
1599
+ message: `Receipt expired at ${new Date(w01.exp * 1e3).toISOString()}`
1434
1600
  };
1435
1601
  }
1436
1602
  if (pr.variant === "commerce") {
@@ -1447,6 +1613,8 @@ async function verifyLocal(jws, publicKey, options = {}) {
1447
1613
  variant: "commerce",
1448
1614
  claims,
1449
1615
  kid: result.header.kid,
1616
+ wireVersion: "0.1",
1617
+ warnings: [],
1450
1618
  policy_binding: "unavailable"
1451
1619
  };
1452
1620
  } else {
@@ -1463,11 +1631,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
1463
1631
  variant: "attestation",
1464
1632
  claims,
1465
1633
  kid: result.header.kid,
1634
+ wireVersion: "0.1",
1635
+ warnings: [],
1466
1636
  policy_binding: "unavailable"
1467
1637
  };
1468
1638
  }
1469
1639
  } catch (err) {
1470
1640
  if (isCryptoError(err)) {
1641
+ if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {
1642
+ return {
1643
+ valid: false,
1644
+ code: JOSE_CODE_MAP[err.code],
1645
+ message: err.message
1646
+ };
1647
+ }
1471
1648
  if (FORMAT_ERROR_CODES.has(err.code)) {
1472
1649
  return {
1473
1650
  valid: false,
@@ -1482,6 +1659,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
1482
1659
  message: err.message
1483
1660
  };
1484
1661
  }
1662
+ if (err.code === "CRYPTO_WIRE_VERSION_MISMATCH") {
1663
+ return {
1664
+ valid: false,
1665
+ code: "E_WIRE_VERSION_MISMATCH",
1666
+ message: err.message
1667
+ };
1668
+ }
1485
1669
  }
1486
1670
  if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
1487
1671
  const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
@@ -1505,6 +1689,9 @@ function isCommerceResult(r) {
1505
1689
  function isAttestationResult(r) {
1506
1690
  return r.valid === true && r.variant === "attestation";
1507
1691
  }
1692
+ function isWire02Result(r) {
1693
+ return r.valid === true && r.variant === "wire-02";
1694
+ }
1508
1695
  function setReceiptHeader(headers, receiptJws) {
1509
1696
  headers.set(schema.PEAC_RECEIPT_HEADER, receiptJws);
1510
1697
  }
@@ -1546,6 +1733,16 @@ function setVaryPurposeHeader(headers) {
1546
1733
  headers.set("Vary", schema.PEAC_PURPOSE_HEADER);
1547
1734
  }
1548
1735
  }
1736
+ async function computePolicyDigestJcs(policy) {
1737
+ const hex = await crypto.jcsHash(policy);
1738
+ return `${kernel.HASH.prefix}${hex}`;
1739
+ }
1740
+ function checkPolicyBinding(receiptDigest, localDigest) {
1741
+ if (receiptDigest === void 0 || localDigest === void 0) {
1742
+ return "unavailable";
1743
+ }
1744
+ return schema.verifyPolicyBinding(receiptDigest, localDigest);
1745
+ }
1549
1746
  var DEFAULT_VERIFIER_LIMITS = {
1550
1747
  max_receipt_bytes: kernel.VERIFIER_LIMITS.maxReceiptBytes,
1551
1748
  max_jwks_bytes: kernel.VERIFIER_LIMITS.maxJwksBytes,
@@ -2861,8 +3058,10 @@ exports.NON_DETERMINISTIC_ARTIFACT_KEYS = NON_DETERMINISTIC_ARTIFACT_KEYS;
2861
3058
  exports.VerificationReportBuilder = VerificationReportBuilder;
2862
3059
  exports.buildFailureReport = buildFailureReport;
2863
3060
  exports.buildSuccessReport = buildSuccessReport;
3061
+ exports.checkPolicyBinding = checkPolicyBinding;
2864
3062
  exports.clearJWKSCache = clearJWKSCache;
2865
3063
  exports.clearKidThumbprints = clearKidThumbprints;
3064
+ exports.computePolicyDigestJcs = computePolicyDigestJcs;
2866
3065
  exports.computeReceiptDigest = computeReceiptDigest;
2867
3066
  exports.createDefaultPolicy = createDefaultPolicy;
2868
3067
  exports.createDigest = createDigest;
@@ -2882,8 +3081,10 @@ exports.getSSRFCapabilities = getSSRFCapabilities;
2882
3081
  exports.isAttestationResult = isAttestationResult;
2883
3082
  exports.isBlockedIP = isBlockedIP;
2884
3083
  exports.isCommerceResult = isCommerceResult;
3084
+ exports.isWire02Result = isWire02Result;
2885
3085
  exports.issue = issue;
2886
3086
  exports.issueJws = issueJws;
3087
+ exports.issueWire02 = issueWire02;
2887
3088
  exports.parseBodyProfile = parseBodyProfile;
2888
3089
  exports.parseDiscovery = parseDiscovery;
2889
3090
  exports.parseHeaderProfile = parseHeaderProfile;