@peac/protocol 0.11.2 → 0.11.3

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @peac/protocol
2
2
 
3
- PEAC protocol implementation - receipt issuance and verification
3
+ PEAC protocol implementation: receipt issuance, offline verification, and JWKS resolution.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,9 +8,74 @@ PEAC protocol implementation - receipt issuance and verification
8
8
  pnpm add @peac/protocol
9
9
  ```
10
10
 
11
- ## Documentation
11
+ ## What It Does
12
12
 
13
- See [peacprotocol.org](https://www.peacprotocol.org) for full documentation.
13
+ `@peac/protocol` is Layer 3 of the PEAC stack. It provides `issue()` for signing receipts and `verifyLocal()` for offline verification with Ed25519 public keys. No network calls needed for verification.
14
+
15
+ ## How Do I Issue a Receipt?
16
+
17
+ ```typescript
18
+ import { generateKeypair } from '@peac/crypto';
19
+ import { issue } from '@peac/protocol';
20
+
21
+ const { publicKey, privateKey } = await generateKeypair();
22
+
23
+ const { jws } = await issue({
24
+ iss: 'https://api.example.com',
25
+ aud: 'https://client.example.com',
26
+ amt: 100,
27
+ cur: 'USD',
28
+ rail: 'stripe',
29
+ reference: 'pi_abc123',
30
+ privateKey,
31
+ kid: 'key-2026-02',
32
+ });
33
+ ```
34
+
35
+ ## How Do I Verify a Receipt?
36
+
37
+ ```typescript
38
+ import { verifyLocal } from '@peac/protocol';
39
+
40
+ const result = await verifyLocal(jws, publicKey);
41
+
42
+ if (result.valid && result.variant === 'commerce') {
43
+ console.log(result.claims.iss); // issuer
44
+ console.log(result.claims.amt); // amount
45
+ console.log(result.claims.cur); // currency
46
+ } else if (!result.valid) {
47
+ console.log(result.code, result.message);
48
+ }
49
+ ```
50
+
51
+ ## How Do I Verify with JWKS Discovery?
52
+
53
+ ```typescript
54
+ import { verifyReceipt } from '@peac/protocol';
55
+
56
+ // Resolves issuer's /.well-known/peac-issuer.json -> jwks_uri -> public key
57
+ const result = await verifyReceipt(jws);
58
+
59
+ if (result.ok) {
60
+ console.log('Issuer:', result.claims.iss);
61
+ } else {
62
+ console.log(result.reason, result.details);
63
+ }
64
+ ```
65
+
66
+ ## Integrates With
67
+
68
+ - `@peac/crypto` (Layer 2): Ed25519 key generation and JWS encoding
69
+ - `@peac/kernel` (Layer 0): Error codes and wire format constants
70
+ - `@peac/schema` (Layer 1): Receipt claim validation
71
+ - `@peac/mcp-server` (Layer 5): MCP tool server using protocol functions
72
+ - `@peac/middleware-express` (Layer 3.5): Express middleware for automatic receipt issuance
73
+
74
+ ## Security
75
+
76
+ - Verification is offline and deterministic: no network calls for `verifyLocal()`
77
+ - Fail-closed: invalid or missing evidence always produces a verification failure
78
+ - JWKS resolution (when used) is SSRF-hardened with HTTPS-only, private IP denial
14
79
 
15
80
  ## License
16
81
 
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EAMd,MAAM,cAAc,CAAC;AAMtB;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAsFzE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsCpF;AAiBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAoD1F;AAkFD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+CtF;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAkD1D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B9E"}
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../src/discovery.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EACL,gBAAgB,EAChB,kBAAkB,EAClB,aAAa,EAOd,MAAM,cAAc,CAAC;AAMtB;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,gBAAgB,CAoGzE;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAsCpF;AAiBD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM,GAAG,kBAAkB,CAoD1F;AAkFD;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CA+CtF;AAMD;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAkD1D;AAED;;;;;GAKG;AACH,wBAAsB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B9E"}
package/dist/index.cjs CHANGED
@@ -238,6 +238,17 @@ function parseIssuerConfig(json) {
238
238
  throw new Error("payment_rails must be an array");
239
239
  }
240
240
  }
241
+ let revokedKeys;
242
+ if (obj.revoked_keys !== void 0) {
243
+ if (!Array.isArray(obj.revoked_keys)) {
244
+ throw new Error("revoked_keys must be an array");
245
+ }
246
+ const result = schema.validateRevokedKeys(obj.revoked_keys);
247
+ if (!result.ok) {
248
+ throw new Error(`Invalid revoked_keys: ${result.error}`);
249
+ }
250
+ revokedKeys = result.value;
251
+ }
241
252
  return {
242
253
  version: obj.version,
243
254
  issuer: obj.issuer,
@@ -246,7 +257,8 @@ function parseIssuerConfig(json) {
246
257
  receipt_versions: obj.receipt_versions,
247
258
  algorithms: obj.algorithms,
248
259
  payment_rails: obj.payment_rails,
249
- security_contact: obj.security_contact
260
+ security_contact: obj.security_contact,
261
+ revoked_keys: revokedKeys
250
262
  };
251
263
  }
252
264
  async function fetchIssuerConfig(issuerUrl) {
@@ -933,6 +945,53 @@ async function fetchPointerSafe(pointerUrl, options) {
933
945
  var DEFAULT_CACHE_TTL_MS = 5 * 60 * 1e3;
934
946
  var DEFAULT_MAX_CACHE_ENTRIES = 1e3;
935
947
  var jwksCache = /* @__PURE__ */ new Map();
948
+ var KID_RETENTION_MS = 30 * 24 * 60 * 60 * 1e3;
949
+ var MAX_KID_THUMBPRINT_ENTRIES = 1e4;
950
+ var kidThumbprints = /* @__PURE__ */ new Map();
951
+ function pruneExpiredKidEntries(now) {
952
+ for (const [key, entry] of kidThumbprints) {
953
+ if (now - entry.firstSeen >= KID_RETENTION_MS) {
954
+ kidThumbprints.delete(key);
955
+ }
956
+ }
957
+ }
958
+ function evictOldestKidEntries() {
959
+ if (kidThumbprints.size <= MAX_KID_THUMBPRINT_ENTRIES) return;
960
+ const entries = Array.from(kidThumbprints.entries()).sort(
961
+ (a, b) => a[1].firstSeen - b[1].firstSeen
962
+ );
963
+ const toRemove = kidThumbprints.size - MAX_KID_THUMBPRINT_ENTRIES;
964
+ for (let i = 0; i < toRemove; i++) {
965
+ kidThumbprints.delete(entries[i][0]);
966
+ }
967
+ }
968
+ function checkKidReuse(issuer, jwks, now) {
969
+ pruneExpiredKidEntries(now);
970
+ for (const key of jwks.keys) {
971
+ if (!key.kid || !key.x) continue;
972
+ const mapKey = `${issuer}|${key.kid}`;
973
+ const existing = kidThumbprints.get(mapKey);
974
+ if (existing) {
975
+ if (now - existing.firstSeen < KID_RETENTION_MS) {
976
+ if (existing.thumbprint !== key.x) {
977
+ return `Kid reuse detected: kid=${key.kid} for issuer ${issuer} maps to different key material`;
978
+ }
979
+ } else {
980
+ kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
981
+ }
982
+ } else {
983
+ kidThumbprints.set(mapKey, { thumbprint: key.x, firstSeen: now });
984
+ }
985
+ }
986
+ evictOldestKidEntries();
987
+ return null;
988
+ }
989
+ function clearKidThumbprints() {
990
+ kidThumbprints.clear();
991
+ }
992
+ function getKidThumbprintSize() {
993
+ return kidThumbprints.size;
994
+ }
936
995
  function cacheGet(key, now) {
937
996
  const entry = jwksCache.get(key);
938
997
  if (!entry) return void 0;
@@ -1021,7 +1080,7 @@ async function resolveJWKS(issuerUrl, options) {
1021
1080
  if (!noCache) {
1022
1081
  const cached = cacheGet(normalizedIssuer, now);
1023
1082
  if (cached) {
1024
- return { ok: true, jwks: cached.jwks, fromCache: true };
1083
+ return { ok: true, jwks: cached.jwks, fromCache: true, revokedKeys: cached.revokedKeys };
1025
1084
  }
1026
1085
  }
1027
1086
  if (!normalizedIssuer.startsWith("https://")) {
@@ -1116,14 +1175,28 @@ async function resolveJWKS(issuerUrl, options) {
1116
1175
  message: `JWKS has too many keys: ${jwks.keys.length} > ${kernel.VERIFIER_LIMITS.maxJwksKeys}`
1117
1176
  };
1118
1177
  }
1178
+ const kidReuseError = checkKidReuse(normalizedIssuer, jwks, now);
1179
+ if (kidReuseError) {
1180
+ return {
1181
+ ok: false,
1182
+ code: "E_KID_REUSE_DETECTED",
1183
+ message: kidReuseError
1184
+ };
1185
+ }
1186
+ const revokedKeys = issuerConfig.revoked_keys?.map((entry) => ({
1187
+ kid: entry.kid,
1188
+ revoked_at: entry.revoked_at,
1189
+ reason: entry.reason
1190
+ }));
1119
1191
  if (!noCache) {
1120
- cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs }, maxCacheEntries);
1192
+ cacheSet(normalizedIssuer, { jwks, expiresAt: now + cacheTtlMs, revokedKeys }, maxCacheEntries);
1121
1193
  }
1122
1194
  return {
1123
1195
  ok: true,
1124
1196
  jwks,
1125
1197
  fromCache: false,
1126
- rawBytes: jwksResult.rawBytes
1198
+ rawBytes: jwksResult.rawBytes,
1199
+ revokedKeys
1127
1200
  };
1128
1201
  }
1129
1202
 
@@ -1184,6 +1257,25 @@ async function verifyReceipt(optionsOrJws) {
1184
1257
  if (!jwksResult.fromCache) {
1185
1258
  jwksFetchTime = performance.now() - jwksFetchStart;
1186
1259
  }
1260
+ if (jwksResult.revokedKeys) {
1261
+ const revokedEntry = jwksResult.revokedKeys.find((rk) => rk.kid === header.kid);
1262
+ if (revokedEntry) {
1263
+ const durationMs = performance.now() - startTime;
1264
+ fireTelemetryHook(telemetry?.onReceiptVerified, {
1265
+ receiptHash: hashReceipt(receiptJws),
1266
+ valid: false,
1267
+ reasonCode: "key_revoked",
1268
+ issuer: payload.iss,
1269
+ kid: header.kid,
1270
+ durationMs
1271
+ });
1272
+ return {
1273
+ ok: false,
1274
+ reason: "key_revoked",
1275
+ details: `Key kid=${header.kid} was revoked at ${revokedEntry.revoked_at}${revokedEntry.reason ? ` (reason: ${revokedEntry.reason})` : ""}`
1276
+ };
1277
+ }
1278
+ }
1187
1279
  const jwk = jwksResult.jwks.keys.find((k) => k.kid === header.kid);
1188
1280
  if (!jwk) {
1189
1281
  const durationMs = performance.now() - startTime;
@@ -2770,6 +2862,7 @@ exports.VerificationReportBuilder = VerificationReportBuilder;
2770
2862
  exports.buildFailureReport = buildFailureReport;
2771
2863
  exports.buildSuccessReport = buildSuccessReport;
2772
2864
  exports.clearJWKSCache = clearJWKSCache;
2865
+ exports.clearKidThumbprints = clearKidThumbprints;
2773
2866
  exports.computeReceiptDigest = computeReceiptDigest;
2774
2867
  exports.createDefaultPolicy = createDefaultPolicy;
2775
2868
  exports.createDigest = createDigest;
@@ -2782,6 +2875,7 @@ exports.fetchPointerSafe = fetchPointerSafe;
2782
2875
  exports.fetchPointerWithDigest = fetchPointerWithDigest;
2783
2876
  exports.fetchPolicyManifest = fetchPolicyManifest;
2784
2877
  exports.getJWKSCacheSize = getJWKSCacheSize;
2878
+ exports.getKidThumbprintSize = getKidThumbprintSize;
2785
2879
  exports.getPurposeHeader = getPurposeHeader;
2786
2880
  exports.getReceiptHeader = getReceiptHeader;
2787
2881
  exports.getSSRFCapabilities = getSSRFCapabilities;