@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.
@@ -4,14 +4,18 @@
4
4
  * Use this for verifying receipts when you have the public key locally,
5
5
  * without JWKS discovery.
6
6
  */
7
- import { type ReceiptClaimsType, type AttestationReceiptClaims } from '@peac/schema';
7
+ import { type VerificationStrictness, type VerificationWarning } from '@peac/kernel';
8
+ import { type ReceiptClaimsType, type AttestationReceiptClaims, type Wire02Claims } from '@peac/schema';
8
9
  import type { PolicyBindingStatus } from './verifier-types';
9
10
  /**
10
11
  * Canonical error codes for local verification
11
12
  *
12
- * These map to E_* codes in specs/kernel/errors.json
13
+ * These map to E_* codes in specs/kernel/errors.json.
14
+ * JOSE hardening codes (E_JWS_*) are distinct from generic E_INVALID_FORMAT
15
+ * so callers can distinguish key-injection, compression, and crit attacks from
16
+ * ordinary format errors (v0.12.0-preview.1, DD-156).
13
17
  */
14
- export type VerifyLocalErrorCode = 'E_INVALID_SIGNATURE' | 'E_INVALID_FORMAT' | 'E_CONSTRAINT_VIOLATION' | 'E_EXPIRED' | 'E_NOT_YET_VALID' | 'E_INVALID_ISSUER' | 'E_INVALID_AUDIENCE' | 'E_INVALID_SUBJECT' | 'E_INVALID_RECEIPT_ID' | 'E_MISSING_EXP' | 'E_INTERNAL';
18
+ export type VerifyLocalErrorCode = 'E_INVALID_SIGNATURE' | 'E_INVALID_FORMAT' | 'E_CONSTRAINT_VIOLATION' | 'E_EXPIRED' | 'E_NOT_YET_VALID' | 'E_INVALID_ISSUER' | 'E_INVALID_AUDIENCE' | 'E_INVALID_SUBJECT' | 'E_INVALID_RECEIPT_ID' | 'E_MISSING_EXP' | 'E_WIRE_VERSION_MISMATCH' | 'E_UNSUPPORTED_WIRE_VERSION' | 'E_OCCURRED_AT_FUTURE' | 'E_JWS_EMBEDDED_KEY' | 'E_JWS_CRIT_REJECTED' | 'E_JWS_MISSING_KID' | 'E_JWS_B64_REJECTED' | 'E_JWS_ZIP_REJECTED' | 'E_POLICY_BINDING_FAILED' | 'E_INTERNAL';
15
19
  /**
16
20
  * Options for local verification
17
21
  */
@@ -62,43 +66,96 @@ export interface VerifyLocalOptions {
62
66
  * Defaults to 300 (5 minutes).
63
67
  */
64
68
  maxClockSkew?: number;
69
+ /**
70
+ * Verification strictness profile (v0.12.0-preview.1, DD-156).
71
+ *
72
+ * - 'strict' (default): missing typ is a hard error before schema validation.
73
+ * - 'interop': missing typ emits a 'typ_missing' warning and routes by payload content.
74
+ *
75
+ * Strictness is EXCLUSIVELY controlled here (@peac/protocol). @peac/crypto has no strictness param.
76
+ */
77
+ strictness?: VerificationStrictness;
78
+ /**
79
+ * Pre-computed local policy digest for policy binding (Wire 0.2, v0.12.0-preview.1, DD-151).
80
+ *
81
+ * Must be in 'sha256:<64 lowercase hex>' format, computed via computePolicyDigestJcs()
82
+ * from @peac/protocol. When provided alongside a receipt that contains a policy block
83
+ * (policy.digest), the binding check is performed:
84
+ * - Match: policy_binding = 'verified'
85
+ * - Mismatch: hard fail with E_POLICY_BINDING_FAILED
86
+ * - Either absent: policy_binding = 'unavailable'
87
+ *
88
+ * Always 'unavailable' for Wire 0.1 receipts regardless of this option.
89
+ */
90
+ policyDigest?: string;
65
91
  }
66
92
  /**
67
93
  * Result of successful local verification
68
94
  *
69
95
  * Discriminated union on `variant` -- callers narrow claims type via variant check:
70
96
  * if (result.valid && result.variant === 'commerce') { result.claims.amt }
97
+ * if (result.valid && result.variant === 'wire-02') { result.claims.kind }
71
98
  */
72
99
  export type VerifyLocalSuccess = {
73
100
  /** Verification succeeded */
74
101
  valid: true;
75
- /** Receipt variant (commerce = payment receipt, attestation = non-payment) */
102
+ /** Receipt variant (commerce = payment receipt) */
76
103
  variant: 'commerce';
77
104
  /** Validated commerce receipt claims */
78
105
  claims: ReceiptClaimsType;
79
106
  /** Key ID from JWS header (for logging/indexing) */
80
107
  kid: string;
108
+ /** Wire format version */
109
+ wireVersion: '0.1';
110
+ /** Verification warnings (always empty for Wire 0.1) */
111
+ warnings: VerificationWarning[];
81
112
  /**
82
113
  * Policy binding status (DD-49).
83
114
  *
84
115
  * Always 'unavailable' for Wire 0.1 receipts (no policy digest on wire).
85
- * Wire 0.2 receipts with `peac.policy.digest` will report 'verified' or 'failed'.
86
116
  */
87
117
  policy_binding: PolicyBindingStatus;
88
118
  } | {
89
119
  /** Verification succeeded */
90
120
  valid: true;
91
- /** Receipt variant (commerce = payment receipt, attestation = non-payment) */
121
+ /** Receipt variant (attestation = non-payment) */
92
122
  variant: 'attestation';
93
123
  /** Validated attestation receipt claims */
94
124
  claims: AttestationReceiptClaims;
95
125
  /** Key ID from JWS header (for logging/indexing) */
96
126
  kid: string;
127
+ /** Wire format version */
128
+ wireVersion: '0.1';
129
+ /** Verification warnings (always empty for Wire 0.1) */
130
+ warnings: VerificationWarning[];
97
131
  /**
98
132
  * Policy binding status (DD-49).
99
133
  *
100
- * Always 'unavailable' for Wire 0.1 receipts (no policy digest on wire).
101
- * Wire 0.2 receipts with `peac.policy.digest` will report 'verified' or 'failed'.
134
+ * Always 'unavailable' for Wire 0.1 receipts.
135
+ */
136
+ policy_binding: PolicyBindingStatus;
137
+ } | {
138
+ /** Verification succeeded */
139
+ valid: true;
140
+ /** Receipt variant (wire-02 = Wire 0.2 evidence or challenge) */
141
+ variant: 'wire-02';
142
+ /** Validated Wire 0.2 receipt claims */
143
+ claims: Wire02Claims;
144
+ /** Key ID from JWS header (for logging/indexing) */
145
+ kid: string;
146
+ /** Wire format version */
147
+ wireVersion: '0.2';
148
+ /** Verification warnings from schema parsing and strictness routing */
149
+ warnings: VerificationWarning[];
150
+ /**
151
+ * Policy binding status (DD-49, DD-151).
152
+ *
153
+ * Three-state result:
154
+ * - 'unavailable': either the receipt contains no policy block, or the
155
+ * caller did not pass a policyDigest option to verifyLocal(). No check.
156
+ * - 'verified': both digests present and match exactly.
157
+ * - 'failed': not returned on success; verifyLocal() returns
158
+ * E_POLICY_BINDING_FAILED (valid: false) before reaching this field.
102
159
  */
103
160
  policy_binding: PolicyBindingStatus;
104
161
  };
@@ -116,11 +173,20 @@ export interface VerifyLocalFailure {
116
173
  details?: {
117
174
  /** Precise parse error code from unified parser (e.g. E_PARSE_COMMERCE_INVALID) */
118
175
  parse_code?: string;
119
- /** Zod validation issues (bounded, stable shape -- non-normative, may change) */
176
+ /** Zod validation issues (bounded, stable shape; non-normative, may change) */
120
177
  issues?: ReadonlyArray<{
121
178
  path: string;
122
179
  message: string;
123
180
  }>;
181
+ /**
182
+ * Policy digest from the receipt (present when code is E_POLICY_BINDING_FAILED).
183
+ * Both are SHA-256 hashes; safe to log without leaking policy content.
184
+ */
185
+ receipt_policy_digest?: string;
186
+ /** Caller-supplied policy digest (present when code is E_POLICY_BINDING_FAILED). */
187
+ local_policy_digest?: string;
188
+ /** policy.uri hint from the receipt (present when code is E_POLICY_BINDING_FAILED and uri set). */
189
+ policy_uri?: string;
124
190
  };
125
191
  }
126
192
  /**
@@ -132,31 +198,29 @@ export type VerifyLocalResult = VerifyLocalSuccess | VerifyLocalFailure;
132
198
  *
133
199
  * This function:
134
200
  * 1. Verifies the Ed25519 signature and header (typ, alg)
135
- * 2. Validates the receipt schema with Zod
136
- * 3. Checks issuer/audience/subject binding (if options provided)
137
- * 4. Checks time validity (exp/iat with clock skew tolerance)
201
+ * 2. Applies strictness routing for missing typ (strict: hard error; interop: warning)
202
+ * 3. Validates the receipt schema with Zod (Wire 0.1 or Wire 0.2)
203
+ * 4. Checks issuer/audience/subject binding (if options provided)
204
+ * 5. Checks time validity (exp/iat with clock skew tolerance)
205
+ * 6. For Wire 0.2: checks occurred_at skew and collects parse warnings
138
206
  *
139
207
  * Use this when you have the issuer's public key and don't need JWKS discovery.
140
208
  * For JWKS-based verification, use `verifyReceipt()` instead.
141
209
  *
142
210
  * @param jws - JWS compact serialization
143
211
  * @param publicKey - Ed25519 public key (32 bytes)
144
- * @param options - Optional verification options (issuer, audience, subject, clock skew)
212
+ * @param options - Optional verification options (issuer, audience, subject, clock skew, strictness)
145
213
  * @returns Typed verification result
146
214
  *
147
215
  * @example
148
216
  * ```typescript
149
217
  * const result = await verifyLocal(jws, publicKey, {
150
218
  * issuer: 'https://api.example.com',
151
- * audience: 'https://client.example.com',
152
- * subjectUri: 'https://api.example.com/inference/v1',
219
+ * strictness: 'strict',
153
220
  * });
154
- * if (result.valid) {
155
- * console.log('Issuer:', result.claims.iss);
156
- * console.log('Amount:', result.claims.amt, result.claims.cur);
157
- * console.log('Key ID:', result.kid);
158
- * } else {
159
- * console.error('Verification failed:', result.code, result.message);
221
+ * if (result.valid && result.variant === 'wire-02') {
222
+ * console.log('Kind:', result.claims.kind);
223
+ * console.log('Warnings:', result.warnings);
160
224
  * }
161
225
  * ```
162
226
  */
@@ -179,4 +243,13 @@ export declare function isCommerceResult(r: VerifyLocalResult): r is VerifyLocal
179
243
  export declare function isAttestationResult(r: VerifyLocalResult): r is VerifyLocalSuccess & {
180
244
  variant: 'attestation';
181
245
  };
246
+ /**
247
+ * Type guard: narrows a VerifyLocalResult to a Wire 0.2 success (v0.12.0-preview.1).
248
+ *
249
+ * Use instead of manual `result.valid && result.variant === 'wire-02'` checks
250
+ * to get proper claims narrowing to Wire02Claims.
251
+ */
252
+ export declare function isWire02Result(r: VerifyLocalResult): r is VerifyLocalSuccess & {
253
+ variant: 'wire-02';
254
+ };
182
255
  //# sourceMappingURL=verify-local.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"verify-local.d.ts","sourceRoot":"","sources":["../src/verify-local.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,wBAAwB,EAC9B,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC;AA8B5D;;;;GAIG;AACH,MAAM,MAAM,oBAAoB,GAC5B,qBAAqB,GACrB,kBAAkB,GAClB,wBAAwB,GACxB,WAAW,GACX,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,mBAAmB,GACnB,sBAAsB,GACtB,eAAe,GACf,YAAY,CAAC;AAEjB;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;IAErB;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED;;;;;GAKG;AACH,MAAM,MAAM,kBAAkB,GAC1B;IACE,6BAA6B;IAC7B,KAAK,EAAE,IAAI,CAAC;IACZ,8EAA8E;IAC9E,OAAO,EAAE,UAAU,CAAC;IACpB,wCAAwC;IACxC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,cAAc,EAAE,mBAAmB,CAAC;CACrC,GACD;IACE,6BAA6B;IAC7B,KAAK,EAAE,IAAI,CAAC;IACZ,8EAA8E;IAC9E,OAAO,EAAE,aAAa,CAAC;IACvB,2CAA2C;IAC3C,MAAM,EAAE,wBAAwB,CAAC;IACjC,oDAAoD;IACpD,GAAG,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,cAAc,EAAE,mBAAmB,CAAC;CACrC,CAAC;AAEN;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,0BAA0B;IAC1B,KAAK,EAAE,KAAK,CAAC;IAEb,8DAA8D;IAC9D,IAAI,EAAE,oBAAoB,CAAC;IAE3B,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAEhB,+EAA+E;IAC/E,OAAO,CAAC,EAAE;QACR,mFAAmF;QACnF,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,iFAAiF;QACjF,MAAM,CAAC,EAAE,aAAa,CAAC;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,OAAO,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC3D,CAAC;CACH;AAED;;GAEG;AACH,MAAM,MAAM,iBAAiB,GAAG,kBAAkB,GAAG,kBAAkB,CAAC;AA8BxE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,UAAU,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC,CAmL5B;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAC9B,CAAC,EAAE,iBAAiB,GACnB,CAAC,IAAI,kBAAkB,GAAG;IAAE,OAAO,EAAE,UAAU,CAAA;CAAE,CAEnD;AAED;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,CAAC,EAAE,iBAAiB,GACnB,CAAC,IAAI,kBAAkB,GAAG;IAAE,OAAO,EAAE,aAAa,CAAA;CAAE,CAEtD"}
1
+ {"version":3,"file":"verify-local.d.ts","sourceRoot":"","sources":["../src/verify-local.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,6 @@
1
1
  import { verify } from '@peac/crypto';
2
- import { validateKernelConstraints, parseReceiptClaims } from '@peac/schema';
2
+ import { HASH } from '@peac/kernel';
3
+ import { WARNING_TYP_MISSING, validateKernelConstraints, parseReceiptClaims, checkOccurredAtSkew, REGISTERED_RECEIPT_TYPES, WARNING_TYPE_UNREGISTERED, REGISTERED_EXTENSION_GROUP_KEYS, isValidExtensionKey, WARNING_UNKNOWN_EXTENSION, verifyPolicyBinding, sortWarnings } from '@peac/schema';
3
4
 
4
5
  // src/verify-local.ts
5
6
  function isCryptoError(err) {
@@ -11,6 +12,13 @@ var FORMAT_ERROR_CODES = /* @__PURE__ */ new Set([
11
12
  "CRYPTO_INVALID_ALG",
12
13
  "CRYPTO_INVALID_KEY_LENGTH"
13
14
  ]);
15
+ var JOSE_CODE_MAP = {
16
+ CRYPTO_JWS_EMBEDDED_KEY: "E_JWS_EMBEDDED_KEY",
17
+ CRYPTO_JWS_CRIT_REJECTED: "E_JWS_CRIT_REJECTED",
18
+ CRYPTO_JWS_MISSING_KID: "E_JWS_MISSING_KID",
19
+ CRYPTO_JWS_B64_REJECTED: "E_JWS_B64_REJECTED",
20
+ CRYPTO_JWS_ZIP_REJECTED: "E_JWS_ZIP_REJECTED"
21
+ };
14
22
  var MAX_PARSE_ISSUES = 25;
15
23
  function sanitizeParseIssues(issues) {
16
24
  if (!Array.isArray(issues)) return void 0;
@@ -20,7 +28,16 @@ function sanitizeParseIssues(issues) {
20
28
  }));
21
29
  }
22
30
  async function verifyLocal(jws, publicKey, options = {}) {
23
- const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;
31
+ const {
32
+ issuer,
33
+ audience,
34
+ subjectUri,
35
+ rid,
36
+ requireExp = false,
37
+ maxClockSkew = 300,
38
+ strictness = "strict",
39
+ policyDigest
40
+ } = options;
24
41
  const now = options.now ?? Math.floor(Date.now() / 1e3);
25
42
  try {
26
43
  const result = await verify(jws, publicKey);
@@ -31,6 +48,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
31
48
  message: "Ed25519 signature verification failed"
32
49
  };
33
50
  }
51
+ const accumulatedWarnings = [];
52
+ if (result.header.typ === void 0) {
53
+ if (strictness === "strict") {
54
+ return {
55
+ valid: false,
56
+ code: "E_INVALID_FORMAT",
57
+ message: "Missing JWS typ header: strict mode requires typ to be present"
58
+ };
59
+ }
60
+ accumulatedWarnings.push({
61
+ code: WARNING_TYP_MISSING,
62
+ message: "JWS typ header is absent; accepted in interop mode"
63
+ });
64
+ }
34
65
  const constraintResult = validateKernelConstraints(result.payload);
35
66
  if (!constraintResult.valid) {
36
67
  const v = constraintResult.violations[0];
@@ -49,46 +80,136 @@ async function verifyLocal(jws, publicKey, options = {}) {
49
80
  details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) }
50
81
  };
51
82
  }
52
- if (issuer !== void 0 && pr.claims.iss !== issuer) {
83
+ if (pr.wireVersion === "0.2") {
84
+ accumulatedWarnings.push(...pr.warnings);
85
+ }
86
+ if (pr.wireVersion === "0.2") {
87
+ const claims = pr.claims;
88
+ if (issuer !== void 0 && claims.iss !== issuer) {
89
+ return {
90
+ valid: false,
91
+ code: "E_INVALID_ISSUER",
92
+ message: `Issuer mismatch: expected "${issuer}", got "${claims.iss}"`
93
+ };
94
+ }
95
+ if (subjectUri !== void 0 && claims.sub !== subjectUri) {
96
+ return {
97
+ valid: false,
98
+ code: "E_INVALID_SUBJECT",
99
+ message: `Subject mismatch: expected "${subjectUri}", got "${claims.sub ?? "undefined"}"`
100
+ };
101
+ }
102
+ if (claims.iat > now + maxClockSkew) {
103
+ return {
104
+ valid: false,
105
+ code: "E_NOT_YET_VALID",
106
+ message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
107
+ };
108
+ }
109
+ if (claims.kind === "evidence") {
110
+ const skewResult = checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);
111
+ if (skewResult === "future_error") {
112
+ return {
113
+ valid: false,
114
+ code: "E_OCCURRED_AT_FUTURE",
115
+ message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`
116
+ };
117
+ }
118
+ if (skewResult !== null) {
119
+ accumulatedWarnings.push(skewResult);
120
+ }
121
+ }
122
+ if (!REGISTERED_RECEIPT_TYPES.has(claims.type)) {
123
+ accumulatedWarnings.push({
124
+ code: WARNING_TYPE_UNREGISTERED,
125
+ message: "Receipt type is not in the recommended type registry",
126
+ pointer: "/type"
127
+ });
128
+ }
129
+ if (claims.extensions !== void 0) {
130
+ for (const key of Object.keys(claims.extensions)) {
131
+ if (!REGISTERED_EXTENSION_GROUP_KEYS.has(key) && isValidExtensionKey(key)) {
132
+ const escapedKey = key.replace(/~/g, "~0").replace(/\//g, "~1");
133
+ accumulatedWarnings.push({
134
+ code: WARNING_UNKNOWN_EXTENSION,
135
+ message: "Unknown extension key preserved without schema validation",
136
+ pointer: `/extensions/${escapedKey}`
137
+ });
138
+ }
139
+ }
140
+ }
141
+ if (policyDigest !== void 0 && !HASH.pattern.test(policyDigest)) {
142
+ return {
143
+ valid: false,
144
+ code: "E_INVALID_FORMAT",
145
+ message: "policyDigest option must be in sha256:<64 lowercase hex> format"
146
+ };
147
+ }
148
+ const receiptPolicyDigest = claims.policy?.digest;
149
+ const bindingStatus = receiptPolicyDigest === void 0 || policyDigest === void 0 ? "unavailable" : verifyPolicyBinding(receiptPolicyDigest, policyDigest);
150
+ if (bindingStatus === "failed") {
151
+ return {
152
+ valid: false,
153
+ code: "E_POLICY_BINDING_FAILED",
154
+ message: "Policy binding check failed: receipt policy digest does not match local policy",
155
+ details: {
156
+ receipt_policy_digest: receiptPolicyDigest,
157
+ local_policy_digest: policyDigest,
158
+ ...claims.policy?.uri !== void 0 && { policy_uri: claims.policy.uri }
159
+ }
160
+ };
161
+ }
162
+ return {
163
+ valid: true,
164
+ variant: "wire-02",
165
+ claims,
166
+ kid: result.header.kid,
167
+ wireVersion: "0.2",
168
+ warnings: sortWarnings(accumulatedWarnings),
169
+ policy_binding: bindingStatus
170
+ };
171
+ }
172
+ const w01 = pr.claims;
173
+ if (issuer !== void 0 && w01.iss !== issuer) {
53
174
  return {
54
175
  valid: false,
55
176
  code: "E_INVALID_ISSUER",
56
- message: `Issuer mismatch: expected "${issuer}", got "${pr.claims.iss}"`
177
+ message: `Issuer mismatch: expected "${issuer}", got "${w01.iss}"`
57
178
  };
58
179
  }
59
- if (audience !== void 0 && pr.claims.aud !== audience) {
180
+ if (audience !== void 0 && w01.aud !== audience) {
60
181
  return {
61
182
  valid: false,
62
183
  code: "E_INVALID_AUDIENCE",
63
- message: `Audience mismatch: expected "${audience}", got "${pr.claims.aud}"`
184
+ message: `Audience mismatch: expected "${audience}", got "${w01.aud}"`
64
185
  };
65
186
  }
66
- if (rid !== void 0 && pr.claims.rid !== rid) {
187
+ if (rid !== void 0 && w01.rid !== rid) {
67
188
  return {
68
189
  valid: false,
69
190
  code: "E_INVALID_RECEIPT_ID",
70
- message: `Receipt ID mismatch: expected "${rid}", got "${pr.claims.rid}"`
191
+ message: `Receipt ID mismatch: expected "${rid}", got "${w01.rid}"`
71
192
  };
72
193
  }
73
- if (requireExp && pr.claims.exp === void 0) {
194
+ if (requireExp && w01.exp === void 0) {
74
195
  return {
75
196
  valid: false,
76
197
  code: "E_MISSING_EXP",
77
198
  message: "Receipt missing required exp claim"
78
199
  };
79
200
  }
80
- if (pr.claims.iat > now + maxClockSkew) {
201
+ if (w01.iat > now + maxClockSkew) {
81
202
  return {
82
203
  valid: false,
83
204
  code: "E_NOT_YET_VALID",
84
- message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
205
+ message: `Receipt not yet valid: issued at ${new Date(w01.iat * 1e3).toISOString()}, now is ${new Date(now * 1e3).toISOString()}`
85
206
  };
86
207
  }
87
- if (pr.claims.exp !== void 0 && pr.claims.exp < now - maxClockSkew) {
208
+ if (w01.exp !== void 0 && w01.exp < now - maxClockSkew) {
88
209
  return {
89
210
  valid: false,
90
211
  code: "E_EXPIRED",
91
- message: `Receipt expired at ${new Date(pr.claims.exp * 1e3).toISOString()}`
212
+ message: `Receipt expired at ${new Date(w01.exp * 1e3).toISOString()}`
92
213
  };
93
214
  }
94
215
  if (pr.variant === "commerce") {
@@ -105,6 +226,8 @@ async function verifyLocal(jws, publicKey, options = {}) {
105
226
  variant: "commerce",
106
227
  claims,
107
228
  kid: result.header.kid,
229
+ wireVersion: "0.1",
230
+ warnings: [],
108
231
  policy_binding: "unavailable"
109
232
  };
110
233
  } else {
@@ -121,11 +244,20 @@ async function verifyLocal(jws, publicKey, options = {}) {
121
244
  variant: "attestation",
122
245
  claims,
123
246
  kid: result.header.kid,
247
+ wireVersion: "0.1",
248
+ warnings: [],
124
249
  policy_binding: "unavailable"
125
250
  };
126
251
  }
127
252
  } catch (err) {
128
253
  if (isCryptoError(err)) {
254
+ if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {
255
+ return {
256
+ valid: false,
257
+ code: JOSE_CODE_MAP[err.code],
258
+ message: err.message
259
+ };
260
+ }
129
261
  if (FORMAT_ERROR_CODES.has(err.code)) {
130
262
  return {
131
263
  valid: false,
@@ -140,6 +272,13 @@ async function verifyLocal(jws, publicKey, options = {}) {
140
272
  message: err.message
141
273
  };
142
274
  }
275
+ if (err.code === "CRYPTO_WIRE_VERSION_MISMATCH") {
276
+ return {
277
+ valid: false,
278
+ code: "E_WIRE_VERSION_MISMATCH",
279
+ message: err.message
280
+ };
281
+ }
143
282
  }
144
283
  if (err !== null && typeof err === "object" && "name" in err && err.name === "SyntaxError") {
145
284
  const syntaxMessage = "message" in err && typeof err.message === "string" ? err.message : "Invalid JSON";
@@ -163,7 +302,10 @@ function isCommerceResult(r) {
163
302
  function isAttestationResult(r) {
164
303
  return r.valid === true && r.variant === "attestation";
165
304
  }
305
+ function isWire02Result(r) {
306
+ return r.valid === true && r.variant === "wire-02";
307
+ }
166
308
 
167
- export { isAttestationResult, isCommerceResult, verifyLocal };
309
+ export { isAttestationResult, isCommerceResult, isWire02Result, verifyLocal };
168
310
  //# sourceMappingURL=verify-local.mjs.map
169
311
  //# sourceMappingURL=verify-local.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/verify-local.ts"],"names":["jwsVerify"],"mappings":"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","file":"verify-local.mjs","sourcesContent":["/**\n * Local receipt verification with schema validation\n *\n * Use this for verifying receipts when you have the public key locally,\n * without JWKS discovery.\n */\n\nimport { verify as jwsVerify } from '@peac/crypto';\nimport {\n parseReceiptClaims,\n validateKernelConstraints,\n type ReceiptClaimsType,\n type AttestationReceiptClaims,\n} from '@peac/schema';\nimport type { PolicyBindingStatus } from './verifier-types';\n\n/**\n * Structural type for CryptoError\n * Used instead of instanceof for robustness across ESM/CJS boundaries\n */\ninterface CryptoErrorLike {\n name: 'CryptoError';\n code: string;\n message: string;\n}\n\n/**\n * Structural check for CryptoError\n * More robust than instanceof across module boundaries (ESM/CJS, duplicate packages)\n */\nfunction isCryptoError(err: unknown): err is CryptoErrorLike {\n return (\n err !== null &&\n typeof err === 'object' &&\n 'name' in err &&\n err.name === 'CryptoError' &&\n 'code' in err &&\n typeof err.code === 'string' &&\n err.code.startsWith('CRYPTO_') &&\n 'message' in err &&\n typeof err.message === 'string'\n );\n}\n\n/**\n * Canonical error codes for local verification\n *\n * These map to E_* codes in specs/kernel/errors.json\n */\nexport type VerifyLocalErrorCode =\n | 'E_INVALID_SIGNATURE'\n | 'E_INVALID_FORMAT'\n | 'E_CONSTRAINT_VIOLATION'\n | 'E_EXPIRED'\n | 'E_NOT_YET_VALID'\n | 'E_INVALID_ISSUER'\n | 'E_INVALID_AUDIENCE'\n | 'E_INVALID_SUBJECT'\n | 'E_INVALID_RECEIPT_ID'\n | 'E_MISSING_EXP'\n | 'E_INTERNAL';\n\n/**\n * Options for local verification\n */\nexport interface VerifyLocalOptions {\n /**\n * Expected issuer URL\n *\n * If provided, verification fails if receipt.iss does not match.\n */\n issuer?: string;\n\n /**\n * Expected audience URL\n *\n * If provided, verification fails if receipt.aud does not match.\n */\n audience?: string;\n\n /**\n * Expected subject URI\n *\n * If provided, verification fails if receipt.subject.uri does not match.\n * Binds the receipt to a specific resource/interaction target.\n */\n subjectUri?: string;\n\n /**\n * Expected receipt ID (rid)\n *\n * If provided, verification fails if receipt.rid does not match.\n * Useful for idempotency checks or correlating with prior receipts.\n */\n rid?: string;\n\n /**\n * Require expiration claim\n *\n * If true, receipts without exp claim are rejected.\n * Defaults to false.\n */\n requireExp?: boolean;\n\n /**\n * Current timestamp (Unix seconds)\n *\n * Defaults to Date.now() / 1000. Override for testing.\n */\n now?: number;\n\n /**\n * Maximum clock skew tolerance (seconds)\n *\n * Allows for clock drift between issuer and verifier.\n * Defaults to 300 (5 minutes).\n */\n maxClockSkew?: number;\n}\n\n/**\n * Result of successful local verification\n *\n * Discriminated union on `variant` -- callers narrow claims type via variant check:\n * if (result.valid && result.variant === 'commerce') { result.claims.amt }\n */\nexport type VerifyLocalSuccess =\n | {\n /** Verification succeeded */\n valid: true;\n /** Receipt variant (commerce = payment receipt, attestation = non-payment) */\n variant: 'commerce';\n /** Validated commerce receipt claims */\n claims: ReceiptClaimsType;\n /** Key ID from JWS header (for logging/indexing) */\n kid: string;\n /**\n * Policy binding status (DD-49).\n *\n * Always 'unavailable' for Wire 0.1 receipts (no policy digest on wire).\n * Wire 0.2 receipts with `peac.policy.digest` will report 'verified' or 'failed'.\n */\n policy_binding: PolicyBindingStatus;\n }\n | {\n /** Verification succeeded */\n valid: true;\n /** Receipt variant (commerce = payment receipt, attestation = non-payment) */\n variant: 'attestation';\n /** Validated attestation receipt claims */\n claims: AttestationReceiptClaims;\n /** Key ID from JWS header (for logging/indexing) */\n kid: string;\n /**\n * Policy binding status (DD-49).\n *\n * Always 'unavailable' for Wire 0.1 receipts (no policy digest on wire).\n * Wire 0.2 receipts with `peac.policy.digest` will report 'verified' or 'failed'.\n */\n policy_binding: PolicyBindingStatus;\n };\n\n/**\n * Result of failed local verification\n */\nexport interface VerifyLocalFailure {\n /** Verification failed */\n valid: false;\n\n /** Canonical error code (maps to specs/kernel/errors.json) */\n code: VerifyLocalErrorCode;\n\n /** Human-readable error message */\n message: string;\n\n /** Structured details for debugging (stable error code preserved in `code`) */\n details?: {\n /** Precise parse error code from unified parser (e.g. E_PARSE_COMMERCE_INVALID) */\n parse_code?: string;\n /** Zod validation issues (bounded, stable shape -- non-normative, may change) */\n issues?: ReadonlyArray<{ path: string; message: string }>;\n };\n}\n\n/**\n * Union type for local verification result\n */\nexport type VerifyLocalResult = VerifyLocalSuccess | VerifyLocalFailure;\n\n/**\n * Crypto error codes that indicate format/validation issues\n * These are CRYPTO_* internal codes from @peac/crypto, mapped to canonical E_* codes\n */\nconst FORMAT_ERROR_CODES = new Set([\n 'CRYPTO_INVALID_JWS_FORMAT',\n 'CRYPTO_INVALID_TYP',\n 'CRYPTO_INVALID_ALG',\n 'CRYPTO_INVALID_KEY_LENGTH',\n]);\n\n/** Max parse issues to include in details (prevents log bloat) */\nconst MAX_PARSE_ISSUES = 25;\n\n/**\n * Sanitize Zod issues into a bounded, stable structure.\n * Avoids exposing raw Zod internals or unbounded arrays in the public API.\n */\nfunction sanitizeParseIssues(\n issues: unknown\n): ReadonlyArray<{ path: string; message: string }> | undefined {\n if (!Array.isArray(issues)) return undefined;\n return issues.slice(0, MAX_PARSE_ISSUES).map((issue) => ({\n path: Array.isArray(issue?.path) ? issue.path.join('.') : '',\n message: typeof issue?.message === 'string' ? issue.message : String(issue),\n }));\n}\n\n/**\n * Verify a PEAC receipt locally with a known public key\n *\n * This function:\n * 1. Verifies the Ed25519 signature and header (typ, alg)\n * 2. Validates the receipt schema with Zod\n * 3. Checks issuer/audience/subject binding (if options provided)\n * 4. Checks time validity (exp/iat with clock skew tolerance)\n *\n * Use this when you have the issuer's public key and don't need JWKS discovery.\n * For JWKS-based verification, use `verifyReceipt()` instead.\n *\n * @param jws - JWS compact serialization\n * @param publicKey - Ed25519 public key (32 bytes)\n * @param options - Optional verification options (issuer, audience, subject, clock skew)\n * @returns Typed verification result\n *\n * @example\n * ```typescript\n * const result = await verifyLocal(jws, publicKey, {\n * issuer: 'https://api.example.com',\n * audience: 'https://client.example.com',\n * subjectUri: 'https://api.example.com/inference/v1',\n * });\n * if (result.valid) {\n * console.log('Issuer:', result.claims.iss);\n * console.log('Amount:', result.claims.amt, result.claims.cur);\n * console.log('Key ID:', result.kid);\n * } else {\n * console.error('Verification failed:', result.code, result.message);\n * }\n * ```\n */\nexport async function verifyLocal(\n jws: string,\n publicKey: Uint8Array,\n options: VerifyLocalOptions = {}\n): Promise<VerifyLocalResult> {\n const { issuer, audience, subjectUri, rid, requireExp = false, maxClockSkew = 300 } = options;\n const now = options.now ?? Math.floor(Date.now() / 1000);\n\n try {\n // 1. Verify signature and header (typ, alg validated by @peac/crypto)\n const result = await jwsVerify<unknown>(jws, publicKey);\n\n if (!result.valid) {\n return {\n valid: false,\n code: 'E_INVALID_SIGNATURE',\n message: 'Ed25519 signature verification failed',\n };\n }\n\n // 2. Validate structural kernel constraints (DD-121, fail-closed)\n const constraintResult = validateKernelConstraints(result.payload);\n if (!constraintResult.valid) {\n const v = constraintResult.violations[0];\n return {\n valid: false,\n code: 'E_CONSTRAINT_VIOLATION',\n message: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`,\n };\n }\n\n // 3. Validate schema (unified parser supports both commerce and attestation)\n const pr = parseReceiptClaims(result.payload);\n\n if (!pr.ok) {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: `Receipt schema validation failed: ${pr.error.message}`,\n details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) },\n };\n }\n\n // Shared binding checks (iss, aud, rid, iat, exp exist on both receipt types)\n // 3. Check issuer binding\n if (issuer !== undefined && pr.claims.iss !== issuer) {\n return {\n valid: false,\n code: 'E_INVALID_ISSUER',\n message: `Issuer mismatch: expected \"${issuer}\", got \"${pr.claims.iss}\"`,\n };\n }\n\n // 4. Check audience binding\n if (audience !== undefined && pr.claims.aud !== audience) {\n return {\n valid: false,\n code: 'E_INVALID_AUDIENCE',\n message: `Audience mismatch: expected \"${audience}\", got \"${pr.claims.aud}\"`,\n };\n }\n\n // 5. Check receipt ID binding\n if (rid !== undefined && pr.claims.rid !== rid) {\n return {\n valid: false,\n code: 'E_INVALID_RECEIPT_ID',\n message: `Receipt ID mismatch: expected \"${rid}\", got \"${pr.claims.rid}\"`,\n };\n }\n\n // 6. Check requireExp\n if (requireExp && pr.claims.exp === undefined) {\n return {\n valid: false,\n code: 'E_MISSING_EXP',\n message: 'Receipt missing required exp claim',\n };\n }\n\n // 7. Check not-yet-valid (iat with clock skew)\n if (pr.claims.iat > now + maxClockSkew) {\n return {\n valid: false,\n code: 'E_NOT_YET_VALID',\n message: `Receipt not yet valid: issued at ${new Date(pr.claims.iat * 1000).toISOString()}, now is ${new Date(now * 1000).toISOString()}`,\n };\n }\n\n // 8. Check expiry (with clock skew tolerance)\n if (pr.claims.exp !== undefined && pr.claims.exp < now - maxClockSkew) {\n return {\n valid: false,\n code: 'E_EXPIRED',\n message: `Receipt expired at ${new Date(pr.claims.exp * 1000).toISOString()}`,\n };\n }\n\n // 9. Subject binding + typed return (variant-branched, no unsafe casts)\n if (pr.variant === 'commerce') {\n const claims = pr.claims as ReceiptClaimsType;\n if (subjectUri !== undefined && claims.subject?.uri !== subjectUri) {\n return {\n valid: false,\n code: 'E_INVALID_SUBJECT',\n message: `Subject mismatch: expected \"${subjectUri}\", got \"${claims.subject?.uri ?? 'undefined'}\"`,\n };\n }\n // Wire 0.1: no policy digest on wire, always 'unavailable' (DD-49)\n return {\n valid: true,\n variant: 'commerce',\n claims,\n kid: result.header.kid,\n policy_binding: 'unavailable',\n };\n } else {\n const claims = pr.claims as AttestationReceiptClaims;\n if (subjectUri !== undefined && claims.sub !== subjectUri) {\n return {\n valid: false,\n code: 'E_INVALID_SUBJECT',\n message: `Subject mismatch: expected \"${subjectUri}\", got \"${claims.sub ?? 'undefined'}\"`,\n };\n }\n // Wire 0.1: no policy digest on wire, always 'unavailable' (DD-49)\n return {\n valid: true,\n variant: 'attestation',\n claims,\n kid: result.header.kid,\n policy_binding: 'unavailable',\n };\n }\n } catch (err) {\n // Handle typed CryptoError from @peac/crypto\n // Use structural check instead of instanceof for robustness across ESM/CJS boundaries\n // Map internal CRYPTO_* codes to canonical E_* codes\n if (isCryptoError(err)) {\n if (FORMAT_ERROR_CODES.has(err.code)) {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: err.message,\n };\n }\n if (err.code === 'CRYPTO_INVALID_SIGNATURE') {\n return {\n valid: false,\n code: 'E_INVALID_SIGNATURE',\n message: err.message,\n };\n }\n }\n\n // Handle JSON parse errors from malformed payloads\n // Use structural check for cross-boundary robustness (consistent with isCryptoError pattern)\n if (\n err !== null &&\n typeof err === 'object' &&\n 'name' in err &&\n (err as { name: unknown }).name === 'SyntaxError'\n ) {\n const syntaxMessage =\n 'message' in err && typeof (err as { message: unknown }).message === 'string'\n ? (err as { message: string }).message\n : 'Invalid JSON';\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: `Invalid receipt payload: ${syntaxMessage}`,\n };\n }\n\n // All other errors -> E_INTERNAL\n // No message parsing - code-based mapping only\n const message = err instanceof Error ? err.message : String(err);\n return {\n valid: false,\n code: 'E_INTERNAL',\n message: `Unexpected verification error: ${message}`,\n };\n }\n}\n\n/**\n * Type guard: narrows a VerifyLocalResult to a commerce success.\n *\n * Use instead of manual `result.valid && result.variant === 'commerce'` checks\n * to get proper claims narrowing to ReceiptClaimsType.\n */\nexport function isCommerceResult(\n r: VerifyLocalResult\n): r is VerifyLocalSuccess & { variant: 'commerce' } {\n return r.valid === true && r.variant === 'commerce';\n}\n\n/**\n * Type guard: narrows a VerifyLocalResult to an attestation success.\n *\n * Use instead of manual `result.valid && result.variant === 'attestation'` checks\n * to get proper claims narrowing to AttestationReceiptClaims.\n */\nexport function isAttestationResult(\n r: VerifyLocalResult\n): r is VerifyLocalSuccess & { variant: 'attestation' } {\n return r.valid === true && r.variant === 'attestation';\n}\n"]}
1
+ {"version":3,"sources":["../src/verify-local.ts"],"names":["jwsVerify"],"mappings":"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","file":"verify-local.mjs","sourcesContent":["/**\n * Local receipt verification with schema validation\n *\n * Use this for verifying receipts when you have the public key locally,\n * without JWKS discovery.\n */\n\nimport { verify as jwsVerify } from '@peac/crypto';\nimport { type VerificationStrictness, type VerificationWarning, HASH } from '@peac/kernel';\nimport {\n parseReceiptClaims,\n validateKernelConstraints,\n type ReceiptClaimsType,\n type AttestationReceiptClaims,\n type Wire02Claims,\n checkOccurredAtSkew,\n sortWarnings,\n WARNING_TYP_MISSING,\n WARNING_TYPE_UNREGISTERED,\n WARNING_UNKNOWN_EXTENSION,\n REGISTERED_RECEIPT_TYPES,\n REGISTERED_EXTENSION_GROUP_KEYS,\n isValidExtensionKey,\n verifyPolicyBinding,\n} from '@peac/schema';\nimport type { PolicyBindingStatus } from './verifier-types';\n\n/**\n * Structural type for CryptoError\n * Used instead of instanceof for robustness across ESM/CJS boundaries\n */\ninterface CryptoErrorLike {\n name: 'CryptoError';\n code: string;\n message: string;\n}\n\n/**\n * Structural check for CryptoError\n * More robust than instanceof across module boundaries (ESM/CJS, duplicate packages)\n */\nfunction isCryptoError(err: unknown): err is CryptoErrorLike {\n return (\n err !== null &&\n typeof err === 'object' &&\n 'name' in err &&\n err.name === 'CryptoError' &&\n 'code' in err &&\n typeof err.code === 'string' &&\n err.code.startsWith('CRYPTO_') &&\n 'message' in err &&\n typeof err.message === 'string'\n );\n}\n\n/**\n * Canonical error codes for local verification\n *\n * These map to E_* codes in specs/kernel/errors.json.\n * JOSE hardening codes (E_JWS_*) are distinct from generic E_INVALID_FORMAT\n * so callers can distinguish key-injection, compression, and crit attacks from\n * ordinary format errors (v0.12.0-preview.1, DD-156).\n */\nexport type VerifyLocalErrorCode =\n | 'E_INVALID_SIGNATURE'\n | 'E_INVALID_FORMAT'\n | 'E_CONSTRAINT_VIOLATION'\n | 'E_EXPIRED'\n | 'E_NOT_YET_VALID'\n | 'E_INVALID_ISSUER'\n | 'E_INVALID_AUDIENCE'\n | 'E_INVALID_SUBJECT'\n | 'E_INVALID_RECEIPT_ID'\n | 'E_MISSING_EXP'\n | 'E_WIRE_VERSION_MISMATCH'\n | 'E_UNSUPPORTED_WIRE_VERSION'\n | 'E_OCCURRED_AT_FUTURE'\n // JOSE hardening codes (Wire 0.2, v0.12.0-preview.1, DD-156)\n | 'E_JWS_EMBEDDED_KEY'\n | 'E_JWS_CRIT_REJECTED'\n | 'E_JWS_MISSING_KID'\n | 'E_JWS_B64_REJECTED'\n | 'E_JWS_ZIP_REJECTED'\n // Policy binding (Wire 0.2, v0.12.0-preview.1, DD-151)\n | 'E_POLICY_BINDING_FAILED'\n | 'E_INTERNAL';\n\n/**\n * Options for local verification\n */\nexport interface VerifyLocalOptions {\n /**\n * Expected issuer URL\n *\n * If provided, verification fails if receipt.iss does not match.\n */\n issuer?: string;\n\n /**\n * Expected audience URL\n *\n * If provided, verification fails if receipt.aud does not match.\n */\n audience?: string;\n\n /**\n * Expected subject URI\n *\n * If provided, verification fails if receipt.subject.uri does not match.\n * Binds the receipt to a specific resource/interaction target.\n */\n subjectUri?: string;\n\n /**\n * Expected receipt ID (rid)\n *\n * If provided, verification fails if receipt.rid does not match.\n * Useful for idempotency checks or correlating with prior receipts.\n */\n rid?: string;\n\n /**\n * Require expiration claim\n *\n * If true, receipts without exp claim are rejected.\n * Defaults to false.\n */\n requireExp?: boolean;\n\n /**\n * Current timestamp (Unix seconds)\n *\n * Defaults to Date.now() / 1000. Override for testing.\n */\n now?: number;\n\n /**\n * Maximum clock skew tolerance (seconds)\n *\n * Allows for clock drift between issuer and verifier.\n * Defaults to 300 (5 minutes).\n */\n maxClockSkew?: number;\n\n /**\n * Verification strictness profile (v0.12.0-preview.1, DD-156).\n *\n * - 'strict' (default): missing typ is a hard error before schema validation.\n * - 'interop': missing typ emits a 'typ_missing' warning and routes by payload content.\n *\n * Strictness is EXCLUSIVELY controlled here (@peac/protocol). @peac/crypto has no strictness param.\n */\n strictness?: VerificationStrictness;\n\n /**\n * Pre-computed local policy digest for policy binding (Wire 0.2, v0.12.0-preview.1, DD-151).\n *\n * Must be in 'sha256:<64 lowercase hex>' format, computed via computePolicyDigestJcs()\n * from @peac/protocol. When provided alongside a receipt that contains a policy block\n * (policy.digest), the binding check is performed:\n * - Match: policy_binding = 'verified'\n * - Mismatch: hard fail with E_POLICY_BINDING_FAILED\n * - Either absent: policy_binding = 'unavailable'\n *\n * Always 'unavailable' for Wire 0.1 receipts regardless of this option.\n */\n policyDigest?: string;\n}\n\n/**\n * Result of successful local verification\n *\n * Discriminated union on `variant` -- callers narrow claims type via variant check:\n * if (result.valid && result.variant === 'commerce') { result.claims.amt }\n * if (result.valid && result.variant === 'wire-02') { result.claims.kind }\n */\nexport type VerifyLocalSuccess =\n | {\n /** Verification succeeded */\n valid: true;\n /** Receipt variant (commerce = payment receipt) */\n variant: 'commerce';\n /** Validated commerce receipt claims */\n claims: ReceiptClaimsType;\n /** Key ID from JWS header (for logging/indexing) */\n kid: string;\n /** Wire format version */\n wireVersion: '0.1';\n /** Verification warnings (always empty for Wire 0.1) */\n warnings: VerificationWarning[];\n /**\n * Policy binding status (DD-49).\n *\n * Always 'unavailable' for Wire 0.1 receipts (no policy digest on wire).\n */\n policy_binding: PolicyBindingStatus;\n }\n | {\n /** Verification succeeded */\n valid: true;\n /** Receipt variant (attestation = non-payment) */\n variant: 'attestation';\n /** Validated attestation receipt claims */\n claims: AttestationReceiptClaims;\n /** Key ID from JWS header (for logging/indexing) */\n kid: string;\n /** Wire format version */\n wireVersion: '0.1';\n /** Verification warnings (always empty for Wire 0.1) */\n warnings: VerificationWarning[];\n /**\n * Policy binding status (DD-49).\n *\n * Always 'unavailable' for Wire 0.1 receipts.\n */\n policy_binding: PolicyBindingStatus;\n }\n | {\n /** Verification succeeded */\n valid: true;\n /** Receipt variant (wire-02 = Wire 0.2 evidence or challenge) */\n variant: 'wire-02';\n /** Validated Wire 0.2 receipt claims */\n claims: Wire02Claims;\n /** Key ID from JWS header (for logging/indexing) */\n kid: string;\n /** Wire format version */\n wireVersion: '0.2';\n /** Verification warnings from schema parsing and strictness routing */\n warnings: VerificationWarning[];\n /**\n * Policy binding status (DD-49, DD-151).\n *\n * Three-state result:\n * - 'unavailable': either the receipt contains no policy block, or the\n * caller did not pass a policyDigest option to verifyLocal(). No check.\n * - 'verified': both digests present and match exactly.\n * - 'failed': not returned on success; verifyLocal() returns\n * E_POLICY_BINDING_FAILED (valid: false) before reaching this field.\n */\n policy_binding: PolicyBindingStatus;\n };\n\n/**\n * Result of failed local verification\n */\nexport interface VerifyLocalFailure {\n /** Verification failed */\n valid: false;\n\n /** Canonical error code (maps to specs/kernel/errors.json) */\n code: VerifyLocalErrorCode;\n\n /** Human-readable error message */\n message: string;\n\n /** Structured details for debugging (stable error code preserved in `code`) */\n details?: {\n /** Precise parse error code from unified parser (e.g. E_PARSE_COMMERCE_INVALID) */\n parse_code?: string;\n /** Zod validation issues (bounded, stable shape; non-normative, may change) */\n issues?: ReadonlyArray<{ path: string; message: string }>;\n /**\n * Policy digest from the receipt (present when code is E_POLICY_BINDING_FAILED).\n * Both are SHA-256 hashes; safe to log without leaking policy content.\n */\n receipt_policy_digest?: string;\n /** Caller-supplied policy digest (present when code is E_POLICY_BINDING_FAILED). */\n local_policy_digest?: string;\n /** policy.uri hint from the receipt (present when code is E_POLICY_BINDING_FAILED and uri set). */\n policy_uri?: string;\n };\n}\n\n/**\n * Union type for local verification result\n */\nexport type VerifyLocalResult = VerifyLocalSuccess | VerifyLocalFailure;\n\n/**\n * Internal CRYPTO_* codes that map to generic E_INVALID_FORMAT.\n * These are format/encoding errors not security-specific.\n */\nconst FORMAT_ERROR_CODES = new Set([\n 'CRYPTO_INVALID_JWS_FORMAT',\n 'CRYPTO_INVALID_TYP',\n 'CRYPTO_INVALID_ALG',\n 'CRYPTO_INVALID_KEY_LENGTH',\n]);\n\n/**\n * JOSE hardening code mapping: CRYPTO_JWS_* → specific E_JWS_* (v0.12.0-preview.1, DD-156).\n *\n * Each JOSE hazard code maps to its specific public E_JWS_* counterpart rather than\n * collapsing into the generic E_INVALID_FORMAT. This lets callers distinguish embedded-key\n * injection, crit-header abuse, and unencoded-payload attacks from ordinary format errors.\n */\nconst JOSE_CODE_MAP: Record<string, VerifyLocalErrorCode> = {\n CRYPTO_JWS_EMBEDDED_KEY: 'E_JWS_EMBEDDED_KEY',\n CRYPTO_JWS_CRIT_REJECTED: 'E_JWS_CRIT_REJECTED',\n CRYPTO_JWS_MISSING_KID: 'E_JWS_MISSING_KID',\n CRYPTO_JWS_B64_REJECTED: 'E_JWS_B64_REJECTED',\n CRYPTO_JWS_ZIP_REJECTED: 'E_JWS_ZIP_REJECTED',\n};\n\n/** Max parse issues to include in details (prevents log bloat) */\nconst MAX_PARSE_ISSUES = 25;\n\n/**\n * Sanitize Zod issues into a bounded, stable structure.\n * Avoids exposing raw Zod internals or unbounded arrays in the public API.\n */\nfunction sanitizeParseIssues(\n issues: unknown\n): ReadonlyArray<{ path: string; message: string }> | undefined {\n if (!Array.isArray(issues)) return undefined;\n return issues.slice(0, MAX_PARSE_ISSUES).map((issue) => ({\n path: Array.isArray(issue?.path) ? issue.path.join('.') : '',\n message: typeof issue?.message === 'string' ? issue.message : String(issue),\n }));\n}\n\n/**\n * Verify a PEAC receipt locally with a known public key\n *\n * This function:\n * 1. Verifies the Ed25519 signature and header (typ, alg)\n * 2. Applies strictness routing for missing typ (strict: hard error; interop: warning)\n * 3. Validates the receipt schema with Zod (Wire 0.1 or Wire 0.2)\n * 4. Checks issuer/audience/subject binding (if options provided)\n * 5. Checks time validity (exp/iat with clock skew tolerance)\n * 6. For Wire 0.2: checks occurred_at skew and collects parse warnings\n *\n * Use this when you have the issuer's public key and don't need JWKS discovery.\n * For JWKS-based verification, use `verifyReceipt()` instead.\n *\n * @param jws - JWS compact serialization\n * @param publicKey - Ed25519 public key (32 bytes)\n * @param options - Optional verification options (issuer, audience, subject, clock skew, strictness)\n * @returns Typed verification result\n *\n * @example\n * ```typescript\n * const result = await verifyLocal(jws, publicKey, {\n * issuer: 'https://api.example.com',\n * strictness: 'strict',\n * });\n * if (result.valid && result.variant === 'wire-02') {\n * console.log('Kind:', result.claims.kind);\n * console.log('Warnings:', result.warnings);\n * }\n * ```\n */\nexport async function verifyLocal(\n jws: string,\n publicKey: Uint8Array,\n options: VerifyLocalOptions = {}\n): Promise<VerifyLocalResult> {\n const {\n issuer,\n audience,\n subjectUri,\n rid,\n requireExp = false,\n maxClockSkew = 300,\n strictness = 'strict',\n policyDigest,\n } = options;\n const now = options.now ?? Math.floor(Date.now() / 1000);\n\n try {\n // 1. Verify signature and header (typ, alg validated by @peac/crypto)\n const result = await jwsVerify<unknown>(jws, publicKey);\n\n if (!result.valid) {\n return {\n valid: false,\n code: 'E_INVALID_SIGNATURE',\n message: 'Ed25519 signature verification failed',\n };\n }\n\n // Accumulated warnings for Wire 0.2 path\n const accumulatedWarnings: VerificationWarning[] = [];\n\n // 2. Strictness routing for missing typ (Correction 1, DD-156)\n if (result.header.typ === undefined) {\n if (strictness === 'strict') {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: 'Missing JWS typ header: strict mode requires typ to be present',\n };\n }\n // interop mode: emit warning and continue\n accumulatedWarnings.push({\n code: WARNING_TYP_MISSING,\n message: 'JWS typ header is absent; accepted in interop mode',\n });\n }\n\n // 3. Validate structural kernel constraints (DD-121, fail-closed)\n const constraintResult = validateKernelConstraints(result.payload);\n if (!constraintResult.valid) {\n const v = constraintResult.violations[0];\n return {\n valid: false,\n code: 'E_CONSTRAINT_VIOLATION',\n message: `Kernel constraint violated: ${v.constraint} (actual: ${v.actual}, limit: ${v.limit})`,\n };\n }\n\n // 4. Validate schema (unified parser supports Wire 0.1 and Wire 0.2)\n const pr = parseReceiptClaims(result.payload);\n\n if (!pr.ok) {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: `Receipt schema validation failed: ${pr.error.message}`,\n details: { parse_code: pr.error.code, issues: sanitizeParseIssues(pr.error.issues) },\n };\n }\n\n // 5. Collect parser warnings (Wire 0.2 parser may emit type/extension warnings)\n if (pr.wireVersion === '0.2') {\n accumulatedWarnings.push(...pr.warnings);\n }\n\n // Wire 0.2 path\n if (pr.wireVersion === '0.2') {\n const claims = pr.claims as Wire02Claims;\n\n // Issuer check\n if (issuer !== undefined && claims.iss !== issuer) {\n return {\n valid: false,\n code: 'E_INVALID_ISSUER',\n message: `Issuer mismatch: expected \"${issuer}\", got \"${claims.iss}\"`,\n };\n }\n\n // Subject check\n if (subjectUri !== undefined && claims.sub !== subjectUri) {\n return {\n valid: false,\n code: 'E_INVALID_SUBJECT',\n message: `Subject mismatch: expected \"${subjectUri}\", got \"${claims.sub ?? 'undefined'}\"`,\n };\n }\n\n // iat: not-yet-valid check (with clock skew)\n if (claims.iat > now + maxClockSkew) {\n return {\n valid: false,\n code: 'E_NOT_YET_VALID',\n message: `Receipt not yet valid: issued at ${new Date(claims.iat * 1000).toISOString()}, now is ${new Date(now * 1000).toISOString()}`,\n };\n }\n\n // occurred_at skew check (evidence kind only)\n if (claims.kind === 'evidence') {\n const skewResult = checkOccurredAtSkew(claims.occurred_at, claims.iat, now, maxClockSkew);\n if (skewResult === 'future_error') {\n return {\n valid: false,\n code: 'E_OCCURRED_AT_FUTURE',\n message: `occurred_at is in the future beyond tolerance (${maxClockSkew}s)`,\n };\n }\n if (skewResult !== null) {\n accumulatedWarnings.push(skewResult);\n }\n }\n\n // Emit type_unregistered warning for valid-but-unregistered type values (DD-155)\n if (!REGISTERED_RECEIPT_TYPES.has(claims.type)) {\n accumulatedWarnings.push({\n code: WARNING_TYPE_UNREGISTERED,\n message: 'Receipt type is not in the recommended type registry',\n pointer: '/type',\n });\n }\n\n // Emit unknown_extension_preserved warnings for unrecognized-but-well-formed keys (DD-155)\n // Malformed keys are already hard errors (E_INVALID_EXTENSION_KEY) at schema layer.\n if (claims.extensions !== undefined) {\n for (const key of Object.keys(claims.extensions)) {\n if (!REGISTERED_EXTENSION_GROUP_KEYS.has(key) && isValidExtensionKey(key)) {\n // RFC 6901: '~' -> '~0', '/' -> '~1'\n const escapedKey = key.replace(/~/g, '~0').replace(/\\//g, '~1');\n accumulatedWarnings.push({\n code: WARNING_UNKNOWN_EXTENSION,\n message: 'Unknown extension key preserved without schema validation',\n pointer: `/extensions/${escapedKey}`,\n });\n }\n }\n }\n\n // Validate policyDigest option format (DD-151): must be sha256:<64 lowercase hex> if provided.\n if (policyDigest !== undefined && !HASH.pattern.test(policyDigest)) {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: 'policyDigest option must be in sha256:<64 lowercase hex> format',\n };\n }\n\n // Policy binding check (DD-151): 3-state result.\n // 'unavailable' when either receipt has no policy block or caller omitted policyDigest.\n // 'verified' / 'failed' when both are present; 'failed' is a hard verification error.\n const receiptPolicyDigest = claims.policy?.digest;\n const bindingStatus: PolicyBindingStatus =\n receiptPolicyDigest === undefined || policyDigest === undefined\n ? 'unavailable'\n : verifyPolicyBinding(receiptPolicyDigest, policyDigest);\n if (bindingStatus === 'failed') {\n return {\n valid: false,\n code: 'E_POLICY_BINDING_FAILED',\n message: 'Policy binding check failed: receipt policy digest does not match local policy',\n details: {\n receipt_policy_digest: receiptPolicyDigest,\n local_policy_digest: policyDigest,\n ...(claims.policy?.uri !== undefined && { policy_uri: claims.policy.uri }),\n },\n };\n }\n\n return {\n valid: true,\n variant: 'wire-02',\n claims,\n kid: result.header.kid,\n wireVersion: '0.2',\n warnings: sortWarnings(accumulatedWarnings),\n policy_binding: bindingStatus,\n };\n }\n\n // Wire 0.1 path (commerce or attestation)\n // Wire 0.2 receipts returned early above.\n // Both ReceiptClaimsType and AttestationReceiptClaims have: iss, aud, rid, iat, exp\n // TypeScript cannot narrow the union via wireVersion so we use a typed assertion.\n type Wire01CommonClaims = { iss: string; aud: string; rid: string; iat: number; exp?: number };\n const w01 = pr.claims as Wire01CommonClaims;\n\n // Shared binding checks (iss, aud, rid, iat, exp exist on both receipt types)\n if (issuer !== undefined && w01.iss !== issuer) {\n return {\n valid: false,\n code: 'E_INVALID_ISSUER',\n message: `Issuer mismatch: expected \"${issuer}\", got \"${w01.iss}\"`,\n };\n }\n\n if (audience !== undefined && w01.aud !== audience) {\n return {\n valid: false,\n code: 'E_INVALID_AUDIENCE',\n message: `Audience mismatch: expected \"${audience}\", got \"${w01.aud}\"`,\n };\n }\n\n if (rid !== undefined && w01.rid !== rid) {\n return {\n valid: false,\n code: 'E_INVALID_RECEIPT_ID',\n message: `Receipt ID mismatch: expected \"${rid}\", got \"${w01.rid}\"`,\n };\n }\n\n if (requireExp && w01.exp === undefined) {\n return {\n valid: false,\n code: 'E_MISSING_EXP',\n message: 'Receipt missing required exp claim',\n };\n }\n\n if (w01.iat > now + maxClockSkew) {\n return {\n valid: false,\n code: 'E_NOT_YET_VALID',\n message: `Receipt not yet valid: issued at ${new Date(w01.iat * 1000).toISOString()}, now is ${new Date(now * 1000).toISOString()}`,\n };\n }\n\n if (w01.exp !== undefined && w01.exp < now - maxClockSkew) {\n return {\n valid: false,\n code: 'E_EXPIRED',\n message: `Receipt expired at ${new Date(w01.exp * 1000).toISOString()}`,\n };\n }\n\n // Subject binding + typed return (variant-branched, no unsafe casts)\n if (pr.variant === 'commerce') {\n const claims = pr.claims as ReceiptClaimsType;\n if (subjectUri !== undefined && claims.subject?.uri !== subjectUri) {\n return {\n valid: false,\n code: 'E_INVALID_SUBJECT',\n message: `Subject mismatch: expected \"${subjectUri}\", got \"${claims.subject?.uri ?? 'undefined'}\"`,\n };\n }\n return {\n valid: true,\n variant: 'commerce',\n claims,\n kid: result.header.kid,\n wireVersion: '0.1',\n warnings: [],\n policy_binding: 'unavailable',\n };\n } else {\n const claims = pr.claims as AttestationReceiptClaims;\n if (subjectUri !== undefined && claims.sub !== subjectUri) {\n return {\n valid: false,\n code: 'E_INVALID_SUBJECT',\n message: `Subject mismatch: expected \"${subjectUri}\", got \"${claims.sub ?? 'undefined'}\"`,\n };\n }\n return {\n valid: true,\n variant: 'attestation',\n claims,\n kid: result.header.kid,\n wireVersion: '0.1',\n warnings: [],\n policy_binding: 'unavailable',\n };\n }\n } catch (err) {\n // Handle typed CryptoError from @peac/crypto\n // Use structural check instead of instanceof for robustness across ESM/CJS boundaries\n // Map internal CRYPTO_* codes to canonical E_* codes.\n // JOSE hardening codes get specific E_JWS_* (not generic E_INVALID_FORMAT) so callers\n // can distinguish key-injection attacks from ordinary encoding errors.\n if (isCryptoError(err)) {\n // 1. JOSE hardening: specific E_JWS_* codes (checked first)\n if (Object.prototype.hasOwnProperty.call(JOSE_CODE_MAP, err.code)) {\n return {\n valid: false,\n code: JOSE_CODE_MAP[err.code]!,\n message: err.message,\n };\n }\n // 2. Generic format errors\n if (FORMAT_ERROR_CODES.has(err.code)) {\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: err.message,\n };\n }\n if (err.code === 'CRYPTO_INVALID_SIGNATURE') {\n return {\n valid: false,\n code: 'E_INVALID_SIGNATURE',\n message: err.message,\n };\n }\n if (err.code === 'CRYPTO_WIRE_VERSION_MISMATCH') {\n return {\n valid: false,\n code: 'E_WIRE_VERSION_MISMATCH',\n message: err.message,\n };\n }\n }\n\n // Handle JSON parse errors from malformed payloads\n // Use structural check for cross-boundary robustness (consistent with isCryptoError pattern)\n if (\n err !== null &&\n typeof err === 'object' &&\n 'name' in err &&\n (err as { name: unknown }).name === 'SyntaxError'\n ) {\n const syntaxMessage =\n 'message' in err && typeof (err as { message: unknown }).message === 'string'\n ? (err as { message: string }).message\n : 'Invalid JSON';\n return {\n valid: false,\n code: 'E_INVALID_FORMAT',\n message: `Invalid receipt payload: ${syntaxMessage}`,\n };\n }\n\n // All other errors -> E_INTERNAL\n // No message parsing - code-based mapping only\n const message = err instanceof Error ? err.message : String(err);\n return {\n valid: false,\n code: 'E_INTERNAL',\n message: `Unexpected verification error: ${message}`,\n };\n }\n}\n\n/**\n * Type guard: narrows a VerifyLocalResult to a commerce success.\n *\n * Use instead of manual `result.valid && result.variant === 'commerce'` checks\n * to get proper claims narrowing to ReceiptClaimsType.\n */\nexport function isCommerceResult(\n r: VerifyLocalResult\n): r is VerifyLocalSuccess & { variant: 'commerce' } {\n return r.valid === true && r.variant === 'commerce';\n}\n\n/**\n * Type guard: narrows a VerifyLocalResult to an attestation success.\n *\n * Use instead of manual `result.valid && result.variant === 'attestation'` checks\n * to get proper claims narrowing to AttestationReceiptClaims.\n */\nexport function isAttestationResult(\n r: VerifyLocalResult\n): r is VerifyLocalSuccess & { variant: 'attestation' } {\n return r.valid === true && r.variant === 'attestation';\n}\n\n/**\n * Type guard: narrows a VerifyLocalResult to a Wire 0.2 success (v0.12.0-preview.1).\n *\n * Use instead of manual `result.valid && result.variant === 'wire-02'` checks\n * to get proper claims narrowing to Wire02Claims.\n */\nexport function isWire02Result(\n r: VerifyLocalResult\n): r is VerifyLocalSuccess & { variant: 'wire-02' } {\n return r.valid === true && r.variant === 'wire-02';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peac/protocol",
3
- "version": "0.11.3",
3
+ "version": "0.12.0-preview.1",
4
4
  "description": "PEAC protocol implementation - receipt issuance and verification",
5
5
  "main": "dist/index.cjs",
6
6
  "types": "dist/index.d.ts",
@@ -39,9 +39,9 @@
39
39
  "dependencies": {
40
40
  "uuidv7": "^0.6.3",
41
41
  "zod": "^4.3.6",
42
- "@peac/schema": "0.11.3",
43
- "@peac/crypto": "0.11.3",
44
- "@peac/kernel": "0.11.3"
42
+ "@peac/kernel": "0.12.0-preview.1",
43
+ "@peac/schema": "0.12.0-preview.1",
44
+ "@peac/crypto": "0.12.0-preview.1"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^22.19.11",