@peac/protocol 0.11.1 → 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 +68 -3
- package/dist/discovery.d.ts.map +1 -1
- package/dist/index.cjs +98 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +98 -6
- package/dist/index.mjs.map +1 -1
- package/dist/jwks-resolver.d.ts +20 -0
- package/dist/jwks-resolver.d.ts.map +1 -1
- package/dist/verify.d.ts.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @peac/protocol
|
|
2
2
|
|
|
3
|
-
PEAC protocol implementation
|
|
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
|
-
##
|
|
11
|
+
## What It Does
|
|
12
12
|
|
|
13
|
-
|
|
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
|
|
package/dist/discovery.d.ts.map
CHANGED
|
@@ -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,
|
|
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;
|