@bolyra/receipts 0.6.0 → 0.7.0

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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  export { canonicalize } from './canonical';
2
- export { createAuthReceipt } from './receipt';
2
+ export { createAuthReceipt, createCommerceReceipt } from './receipt';
3
3
  export { signReceipt, verifyReceipt, hashPayload } from './sign';
4
- export type { ReceiptPayload, SignedReceipt, ReceiptSignerConfig, AuthReceiptInput, } from './types';
4
+ export type { ReceiptPayload, SignedReceipt, ReceiptSignerConfig, AuthReceiptInput, CommerceReceiptInput, CommerceFields, } from './types';
package/dist/index.js CHANGED
@@ -1,10 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hashPayload = exports.verifyReceipt = exports.signReceipt = exports.createAuthReceipt = exports.canonicalize = void 0;
3
+ exports.hashPayload = exports.verifyReceipt = exports.signReceipt = exports.createCommerceReceipt = exports.createAuthReceipt = exports.canonicalize = void 0;
4
4
  var canonical_1 = require("./canonical");
5
5
  Object.defineProperty(exports, "canonicalize", { enumerable: true, get: function () { return canonical_1.canonicalize; } });
6
6
  var receipt_1 = require("./receipt");
7
7
  Object.defineProperty(exports, "createAuthReceipt", { enumerable: true, get: function () { return receipt_1.createAuthReceipt; } });
8
+ Object.defineProperty(exports, "createCommerceReceipt", { enumerable: true, get: function () { return receipt_1.createCommerceReceipt; } });
8
9
  var sign_1 = require("./sign");
9
10
  Object.defineProperty(exports, "signReceipt", { enumerable: true, get: function () { return sign_1.signReceipt; } });
10
11
  Object.defineProperty(exports, "verifyReceipt", { enumerable: true, get: function () { return sign_1.verifyReceipt; } });
package/dist/receipt.d.ts CHANGED
@@ -1,5 +1,10 @@
1
- import type { AuthReceiptInput, ReceiptPayload } from './types';
1
+ import type { AuthReceiptInput, CommerceReceiptInput, ReceiptPayload } from './types';
2
2
  export declare function createAuthReceipt(input: AuthReceiptInput, config: {
3
3
  issuer: string;
4
4
  keyId: string;
5
+ issuedAt?: number;
6
+ }): ReceiptPayload;
7
+ export declare function createCommerceReceipt(input: CommerceReceiptInput, config: {
8
+ issuer: string;
9
+ keyId: string;
5
10
  }): ReceiptPayload;
package/dist/receipt.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createAuthReceipt = createAuthReceipt;
4
+ exports.createCommerceReceipt = createCommerceReceipt;
4
5
  const sha256_1 = require("@noble/hashes/sha256");
5
6
  const utils_1 = require("@noble/hashes/utils");
6
7
  const canonical_1 = require("./canonical");
@@ -18,7 +19,7 @@ function createAuthReceipt(input, config) {
18
19
  return {
19
20
  v: 1,
20
21
  kind: 'bolyra.auth',
21
- issuedAt: Math.floor(Date.now() / 1000),
22
+ issuedAt: config.issuedAt ?? Math.floor(Date.now() / 1000),
22
23
  issuer: config.issuer,
23
24
  keyId: config.keyId,
24
25
  subject: {
@@ -44,3 +45,11 @@ function createAuthReceipt(input, config) {
44
45
  },
45
46
  };
46
47
  }
48
+ function createCommerceReceipt(input, config) {
49
+ const base = createAuthReceipt(input, config);
50
+ return {
51
+ ...base,
52
+ kind: 'bolyra.commerce',
53
+ commerce: input.commerce,
54
+ };
55
+ }
package/dist/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export interface ReceiptPayload {
2
2
  v: 1;
3
- kind: 'bolyra.auth';
3
+ kind: 'bolyra.auth' | 'bolyra.commerce';
4
4
  /** Unix seconds when the decision was made. */
5
5
  issuedAt: number;
6
6
  /** Server identifier. */
@@ -36,6 +36,8 @@ export interface ReceiptPayload {
36
36
  /** SHA-256 of canonical JSON of delegationChain (if v=2). */
37
37
  delegationChainHash?: string;
38
38
  };
39
+ /** Present only when kind === 'bolyra.commerce'. */
40
+ commerce?: CommerceFields;
39
41
  }
40
42
  export interface SignedReceipt {
41
43
  /** First 16 hex chars of payloadHash. */
@@ -58,6 +60,13 @@ export interface ReceiptSignerConfig {
58
60
  /** secp256k1 private key, 32 bytes, hex-encoded, 0x-prefixed. */
59
61
  privateKey: string;
60
62
  }
63
+ export interface CommerceFields {
64
+ rail: string;
65
+ amount: number;
66
+ currency: string;
67
+ merchant: string;
68
+ intentHash: string;
69
+ }
61
70
  export interface AuthReceiptInput {
62
71
  /** From BolyraAuthContext or equivalent. */
63
72
  rootDid: string;
@@ -82,3 +91,6 @@ export interface AuthReceiptInput {
82
91
  nonce: string;
83
92
  delegationChain?: unknown[];
84
93
  }
94
+ export interface CommerceReceiptInput extends AuthReceiptInput {
95
+ commerce: CommerceFields;
96
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,295 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
4
+ if (k2 === undefined) k2 = k;
5
+ var desc = Object.getOwnPropertyDescriptor(m, k);
6
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
7
+ desc = { enumerable: true, get: function() { return m[k]; } };
8
+ }
9
+ Object.defineProperty(o, k2, desc);
10
+ }) : (function(o, m, k, k2) {
11
+ if (k2 === undefined) k2 = k;
12
+ o[k2] = m[k];
13
+ }));
14
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
15
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
16
+ }) : function(o, v) {
17
+ o["default"] = v;
18
+ });
19
+ var __importStar = (this && this.__importStar) || (function () {
20
+ var ownKeys = function(o) {
21
+ ownKeys = Object.getOwnPropertyNames || function (o) {
22
+ var ar = [];
23
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
24
+ return ar;
25
+ };
26
+ return ownKeys(o);
27
+ };
28
+ return function (mod) {
29
+ if (mod && mod.__esModule) return mod;
30
+ var result = {};
31
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
32
+ __setModuleDefault(result, mod);
33
+ return result;
34
+ };
35
+ })();
36
+ Object.defineProperty(exports, "__esModule", { value: true });
37
+ const fs = __importStar(require("fs"));
38
+ const process = __importStar(require("process"));
39
+ const sign_1 = require("./sign");
40
+ // --- Helpers ---
41
+ function usage() {
42
+ return `Usage: bolyra-receipt-verify <receipt.json> [options]
43
+
44
+ Options:
45
+ --stdin Read receipt JSON from stdin instead of a file
46
+ --signer <address> Expected signer address (optional)
47
+ --max-age <seconds> Maximum receipt age in seconds (default: 86400)
48
+ --help Show this help`;
49
+ }
50
+ function fail(msg) {
51
+ process.stderr.write(msg + '\n');
52
+ process.exit(2);
53
+ }
54
+ function truncate(hex) {
55
+ if (hex.length <= 14)
56
+ return hex;
57
+ return hex.slice(0, 8) + '...' + hex.slice(-4);
58
+ }
59
+ // --- Arg parsing ---
60
+ const args = process.argv.slice(2);
61
+ if (args.includes('--help') || args.includes('-h')) {
62
+ console.log(usage());
63
+ process.exit(0);
64
+ }
65
+ let filePath;
66
+ let expectedSigner;
67
+ let maxAgeSeconds = 86400;
68
+ let useStdin = false;
69
+ for (let i = 0; i < args.length; i++) {
70
+ const arg = args[i];
71
+ if (arg === '--stdin') {
72
+ useStdin = true;
73
+ }
74
+ else if (arg === '--signer') {
75
+ expectedSigner = args[++i];
76
+ if (!expectedSigner)
77
+ fail('--signer requires an address argument');
78
+ }
79
+ else if (arg === '--max-age') {
80
+ const val = args[++i];
81
+ if (!val)
82
+ fail('--max-age requires a number argument');
83
+ maxAgeSeconds = parseInt(val, 10);
84
+ if (Number.isNaN(maxAgeSeconds) || maxAgeSeconds < 0) {
85
+ fail('--max-age must be a non-negative integer');
86
+ }
87
+ }
88
+ else if (arg.startsWith('-')) {
89
+ fail(`Unknown option: ${arg}\n\n${usage()}`);
90
+ }
91
+ else {
92
+ filePath = arg;
93
+ }
94
+ }
95
+ if (!filePath && !useStdin) {
96
+ fail(`No input specified. Provide a file path or use --stdin.\n\n${usage()}`);
97
+ }
98
+ // --- Read input ---
99
+ let rawJson;
100
+ try {
101
+ if (useStdin) {
102
+ rawJson = fs.readFileSync(0, 'utf-8');
103
+ }
104
+ else {
105
+ rawJson = fs.readFileSync(filePath, 'utf-8');
106
+ }
107
+ }
108
+ catch (err) {
109
+ const msg = err instanceof Error ? err.message : String(err);
110
+ fail(`Cannot read input: ${msg}`);
111
+ }
112
+ let receipt;
113
+ try {
114
+ receipt = JSON.parse(rawJson);
115
+ }
116
+ catch {
117
+ fail('Invalid JSON');
118
+ }
119
+ // --- Checks ---
120
+ const checks = [];
121
+ let failed = false;
122
+ function pass(msg) {
123
+ checks.push(`\u2713 ${msg}`);
124
+ }
125
+ function failCheck(msg) {
126
+ checks.push(`\u2717 ${msg}`);
127
+ failed = true;
128
+ }
129
+ // 1. Schema validation
130
+ function validateSchema() {
131
+ const p = receipt.payload;
132
+ const s = receipt.signature;
133
+ const errors = [];
134
+ if (!p) {
135
+ errors.push('missing payload');
136
+ }
137
+ if (!s) {
138
+ errors.push('missing signature');
139
+ }
140
+ if (errors.length) {
141
+ failCheck(`Schema: ${errors.join(', ')}`);
142
+ return false;
143
+ }
144
+ if (p.v !== 1)
145
+ errors.push('payload.v !== 1');
146
+ if (p.kind !== 'bolyra.auth' && p.kind !== 'bolyra.commerce') {
147
+ errors.push('payload.kind must be "bolyra.auth" or "bolyra.commerce"');
148
+ }
149
+ if (typeof p.issuedAt !== 'number')
150
+ errors.push('payload.issuedAt not a number');
151
+ if (typeof p.issuer !== 'string' || !p.issuer)
152
+ errors.push('payload.issuer missing');
153
+ if (typeof p.keyId !== 'string' || !p.keyId)
154
+ errors.push('payload.keyId missing');
155
+ // subject
156
+ if (!p.subject) {
157
+ errors.push('payload.subject missing');
158
+ }
159
+ else {
160
+ for (const k of ['rootDid', 'actingDid', 'credentialCommitment', 'effectiveCommitment']) {
161
+ if (typeof p.subject[k] !== 'string' || !p.subject[k]) {
162
+ errors.push(`payload.subject.${k} missing or not a string`);
163
+ }
164
+ }
165
+ }
166
+ // decision
167
+ if (!p.decision) {
168
+ errors.push('payload.decision missing');
169
+ }
170
+ else {
171
+ if (typeof p.decision.allowed !== 'boolean')
172
+ errors.push('decision.allowed not boolean');
173
+ if (typeof p.decision.score !== 'number')
174
+ errors.push('decision.score not a number');
175
+ if (typeof p.decision.permissionBitmask !== 'string' || !p.decision.permissionBitmask)
176
+ errors.push('decision.permissionBitmask missing or not a string');
177
+ if (typeof p.decision.chainDepth !== 'number')
178
+ errors.push('decision.chainDepth not a number');
179
+ }
180
+ // proof
181
+ if (!p.proof) {
182
+ errors.push('payload.proof missing');
183
+ }
184
+ else {
185
+ for (const k of ['bundleVersion', 'nonce', 'humanProofHash', 'agentProofHash', 'publicSignalsHash']) {
186
+ if (!(k in p.proof))
187
+ errors.push(`proof.${k} missing`);
188
+ }
189
+ }
190
+ // commerce fields: required for bolyra.commerce, forbidden for bolyra.auth
191
+ if (p.kind === 'bolyra.auth' && p.commerce) {
192
+ errors.push('auth receipt must not contain commerce fields');
193
+ }
194
+ if (p.kind === 'bolyra.commerce') {
195
+ const c = p.commerce;
196
+ if (!c) {
197
+ errors.push('commerce fields missing for bolyra.commerce receipt');
198
+ }
199
+ else {
200
+ if (typeof c.rail !== 'string' || !c.rail)
201
+ errors.push('commerce.rail missing or not a string');
202
+ if (typeof c.amount !== 'number')
203
+ errors.push('commerce.amount not a number');
204
+ if (typeof c.currency !== 'string' || !c.currency)
205
+ errors.push('commerce.currency missing or not a string');
206
+ if (typeof c.merchant !== 'string')
207
+ errors.push('commerce.merchant not a string');
208
+ if (typeof c.intentHash !== 'string' || !/^[0-9a-fA-F]{64}$/.test(c.intentHash)) {
209
+ errors.push('commerce.intentHash must be a 64-char hex string');
210
+ }
211
+ }
212
+ }
213
+ // signature fields
214
+ if (s.alg !== 'ES256K')
215
+ errors.push('signature.alg !== "ES256K"');
216
+ if (s.keyId !== p.keyId)
217
+ errors.push('signature.keyId does not match payload.keyId');
218
+ if (!/^0x[0-9a-fA-F]{40}$/.test(s.signer ?? ''))
219
+ errors.push('signature.signer invalid format');
220
+ if (!/^0x[0-9a-fA-F]{64}$/.test(s.payloadHash ?? ''))
221
+ errors.push('signature.payloadHash invalid format');
222
+ if (!/^0x[0-9a-fA-F]{130}$/.test(s.value ?? ''))
223
+ errors.push('signature.value invalid format');
224
+ if (errors.length) {
225
+ failCheck(`Schema: ${errors.join('; ')}`);
226
+ return false;
227
+ }
228
+ pass('Schema valid');
229
+ return true;
230
+ }
231
+ // 2. Timestamp check
232
+ function validateTimestamp() {
233
+ const nowSec = Math.floor(Date.now() / 1000);
234
+ const issuedAt = receipt.payload.issuedAt;
235
+ const allowFutureSeconds = 300;
236
+ const ageSec = nowSec - issuedAt;
237
+ const isoStr = new Date(issuedAt * 1000).toISOString();
238
+ if (issuedAt > nowSec + allowFutureSeconds) {
239
+ failCheck(`Timestamp: ${isoStr} is ${issuedAt - nowSec}s in the future (max skew: ${allowFutureSeconds}s)`);
240
+ return;
241
+ }
242
+ if (ageSec > maxAgeSeconds) {
243
+ failCheck(`Timestamp: ${isoStr} (${ageSec}s ago, max-age: ${maxAgeSeconds}s)`);
244
+ return;
245
+ }
246
+ pass(`Timestamp: ${isoStr} (${ageSec}s ago)`);
247
+ }
248
+ // 3. Hash check
249
+ function validateHash() {
250
+ const computed = (0, sign_1.hashPayload)(receipt.payload);
251
+ if (computed !== receipt.signature.payloadHash) {
252
+ failCheck(`Payload hash mismatch (expected ${truncate(receipt.signature.payloadHash)}, got ${truncate(computed)})`);
253
+ return false;
254
+ }
255
+ pass('Payload hash matches');
256
+ return true;
257
+ }
258
+ // 4. ID check
259
+ function validateId() {
260
+ const expected = '0x' + receipt.signature.payloadHash.slice(2, 18);
261
+ if (receipt.id !== expected) {
262
+ failCheck(`Receipt ID mismatch (expected ${expected}, got ${receipt.id})`);
263
+ return;
264
+ }
265
+ pass('Receipt ID matches');
266
+ }
267
+ // 5. Signature check
268
+ function validateSignature() {
269
+ const ok = (0, sign_1.verifyReceipt)(receipt, expectedSigner);
270
+ if (!ok) {
271
+ failCheck(`Signature invalid${expectedSigner ? ` (expected signer: ${truncate(expectedSigner)})` : ''}`);
272
+ return;
273
+ }
274
+ pass(`Signature valid (signer: ${truncate(receipt.signature.signer)})`);
275
+ }
276
+ // --- Run checks ---
277
+ if (validateSchema()) {
278
+ validateTimestamp();
279
+ if (validateHash()) {
280
+ validateId();
281
+ validateSignature();
282
+ }
283
+ }
284
+ // --- Output ---
285
+ for (const line of checks) {
286
+ console.log(line);
287
+ }
288
+ if (failed) {
289
+ console.log('FAIL \u2014 receipt is invalid');
290
+ process.exit(1);
291
+ }
292
+ else {
293
+ console.log('PASS \u2014 receipt is valid');
294
+ process.exit(0);
295
+ }
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "@bolyra/receipts",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Signed auth receipts for Bolyra ZKP verification decisions — canonical JSON, secp256k1 sign/verify, EVM-compatible r||s||v signatures.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
+ "bin": {
8
+ "bolyra-receipt-verify": "dist/verify-cli.js"
9
+ },
7
10
  "scripts": {
8
11
  "build": "tsc",
12
+ "prepare": "tsc",
9
13
  "test": "jest --passWithNoTests",
10
14
  "typecheck": "tsc --noEmit"
11
15
  },
package/src/index.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  export { canonicalize } from './canonical';
2
- export { createAuthReceipt } from './receipt';
2
+ export { createAuthReceipt, createCommerceReceipt } from './receipt';
3
3
  export { signReceipt, verifyReceipt, hashPayload } from './sign';
4
4
  export type {
5
5
  ReceiptPayload,
6
6
  SignedReceipt,
7
7
  ReceiptSignerConfig,
8
8
  AuthReceiptInput,
9
+ CommerceReceiptInput,
10
+ CommerceFields,
9
11
  } from './types';
package/src/receipt.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { sha256 } from '@noble/hashes/sha256';
2
2
  import { bytesToHex } from '@noble/hashes/utils';
3
3
  import { canonicalize } from './canonical';
4
- import type { AuthReceiptInput, ReceiptPayload } from './types';
4
+ import type { AuthReceiptInput, CommerceReceiptInput, ReceiptPayload } from './types';
5
5
 
6
6
  function sha256Hex(data: string): string {
7
7
  const encoder = new TextEncoder();
@@ -10,7 +10,7 @@ function sha256Hex(data: string): string {
10
10
 
11
11
  export function createAuthReceipt(
12
12
  input: AuthReceiptInput,
13
- config: { issuer: string; keyId: string },
13
+ config: { issuer: string; keyId: string; issuedAt?: number },
14
14
  ): ReceiptPayload {
15
15
  const humanProofHash = sha256Hex(canonicalize(input.humanProof.proof));
16
16
  const agentProofHash = sha256Hex(canonicalize(input.agentProof.proof));
@@ -24,7 +24,7 @@ export function createAuthReceipt(
24
24
  return {
25
25
  v: 1,
26
26
  kind: 'bolyra.auth',
27
- issuedAt: Math.floor(Date.now() / 1000),
27
+ issuedAt: config.issuedAt ?? Math.floor(Date.now() / 1000),
28
28
  issuer: config.issuer,
29
29
  keyId: config.keyId,
30
30
  subject: {
@@ -50,3 +50,15 @@ export function createAuthReceipt(
50
50
  },
51
51
  };
52
52
  }
53
+
54
+ export function createCommerceReceipt(
55
+ input: CommerceReceiptInput,
56
+ config: { issuer: string; keyId: string },
57
+ ): ReceiptPayload {
58
+ const base = createAuthReceipt(input, config);
59
+ return {
60
+ ...base,
61
+ kind: 'bolyra.commerce',
62
+ commerce: input.commerce,
63
+ };
64
+ }
package/src/types.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export interface ReceiptPayload {
2
2
  v: 1;
3
- kind: 'bolyra.auth';
3
+ kind: 'bolyra.auth' | 'bolyra.commerce';
4
4
  /** Unix seconds when the decision was made. */
5
5
  issuedAt: number;
6
6
  /** Server identifier. */
@@ -39,6 +39,9 @@ export interface ReceiptPayload {
39
39
  /** SHA-256 of canonical JSON of delegationChain (if v=2). */
40
40
  delegationChainHash?: string;
41
41
  };
42
+
43
+ /** Present only when kind === 'bolyra.commerce'. */
44
+ commerce?: CommerceFields;
42
45
  }
43
46
 
44
47
  export interface SignedReceipt {
@@ -64,6 +67,14 @@ export interface ReceiptSignerConfig {
64
67
  privateKey: string;
65
68
  }
66
69
 
70
+ export interface CommerceFields {
71
+ rail: string;
72
+ amount: number;
73
+ currency: string;
74
+ merchant: string;
75
+ intentHash: string;
76
+ }
77
+
67
78
  export interface AuthReceiptInput {
68
79
  /** From BolyraAuthContext or equivalent. */
69
80
  rootDid: string;
@@ -84,3 +95,7 @@ export interface AuthReceiptInput {
84
95
  nonce: string;
85
96
  delegationChain?: unknown[];
86
97
  }
98
+
99
+ export interface CommerceReceiptInput extends AuthReceiptInput {
100
+ commerce: CommerceFields;
101
+ }
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+
3
+ import * as fs from 'fs';
4
+ import * as process from 'process';
5
+ import { verifyReceipt, hashPayload } from './sign';
6
+ import type { SignedReceipt } from './types';
7
+
8
+ // --- Helpers ---
9
+
10
+ function usage(): string {
11
+ return `Usage: bolyra-receipt-verify <receipt.json> [options]
12
+
13
+ Options:
14
+ --stdin Read receipt JSON from stdin instead of a file
15
+ --signer <address> Expected signer address (optional)
16
+ --max-age <seconds> Maximum receipt age in seconds (default: 86400)
17
+ --help Show this help`;
18
+ }
19
+
20
+ function fail(msg: string): never {
21
+ process.stderr.write(msg + '\n');
22
+ process.exit(2);
23
+ }
24
+
25
+ function truncate(hex: string): string {
26
+ if (hex.length <= 14) return hex;
27
+ return hex.slice(0, 8) + '...' + hex.slice(-4);
28
+ }
29
+
30
+ // --- Arg parsing ---
31
+
32
+ const args = process.argv.slice(2);
33
+
34
+ if (args.includes('--help') || args.includes('-h')) {
35
+ console.log(usage());
36
+ process.exit(0);
37
+ }
38
+
39
+ let filePath: string | undefined;
40
+ let expectedSigner: string | undefined;
41
+ let maxAgeSeconds = 86400;
42
+ let useStdin = false;
43
+
44
+ for (let i = 0; i < args.length; i++) {
45
+ const arg = args[i];
46
+ if (arg === '--stdin') {
47
+ useStdin = true;
48
+ } else if (arg === '--signer') {
49
+ expectedSigner = args[++i];
50
+ if (!expectedSigner) fail('--signer requires an address argument');
51
+ } else if (arg === '--max-age') {
52
+ const val = args[++i];
53
+ if (!val) fail('--max-age requires a number argument');
54
+ maxAgeSeconds = parseInt(val, 10);
55
+ if (Number.isNaN(maxAgeSeconds) || maxAgeSeconds < 0) {
56
+ fail('--max-age must be a non-negative integer');
57
+ }
58
+ } else if (arg.startsWith('-')) {
59
+ fail(`Unknown option: ${arg}\n\n${usage()}`);
60
+ } else {
61
+ filePath = arg;
62
+ }
63
+ }
64
+
65
+ if (!filePath && !useStdin) {
66
+ fail(`No input specified. Provide a file path or use --stdin.\n\n${usage()}`);
67
+ }
68
+
69
+ // --- Read input ---
70
+
71
+ let rawJson: string;
72
+ try {
73
+ if (useStdin) {
74
+ rawJson = fs.readFileSync(0, 'utf-8');
75
+ } else {
76
+ rawJson = fs.readFileSync(filePath!, 'utf-8');
77
+ }
78
+ } catch (err: unknown) {
79
+ const msg = err instanceof Error ? err.message : String(err);
80
+ fail(`Cannot read input: ${msg}`);
81
+ }
82
+
83
+ let receipt: SignedReceipt;
84
+ try {
85
+ receipt = JSON.parse(rawJson);
86
+ } catch {
87
+ fail('Invalid JSON');
88
+ }
89
+
90
+ // --- Checks ---
91
+
92
+ const checks: string[] = [];
93
+ let failed = false;
94
+
95
+ function pass(msg: string): void {
96
+ checks.push(`\u2713 ${msg}`);
97
+ }
98
+
99
+ function failCheck(msg: string): void {
100
+ checks.push(`\u2717 ${msg}`);
101
+ failed = true;
102
+ }
103
+
104
+ // 1. Schema validation
105
+ function validateSchema(): boolean {
106
+ const p = receipt.payload;
107
+ const s = receipt.signature;
108
+ const errors: string[] = [];
109
+
110
+ if (!p) { errors.push('missing payload'); }
111
+ if (!s) { errors.push('missing signature'); }
112
+ if (errors.length) { failCheck(`Schema: ${errors.join(', ')}`); return false; }
113
+
114
+ if (p.v !== 1) errors.push('payload.v !== 1');
115
+ if (p.kind !== 'bolyra.auth' && p.kind !== 'bolyra.commerce') {
116
+ errors.push('payload.kind must be "bolyra.auth" or "bolyra.commerce"');
117
+ }
118
+ if (typeof p.issuedAt !== 'number') errors.push('payload.issuedAt not a number');
119
+ if (typeof p.issuer !== 'string' || !p.issuer) errors.push('payload.issuer missing');
120
+ if (typeof p.keyId !== 'string' || !p.keyId) errors.push('payload.keyId missing');
121
+
122
+ // subject
123
+ if (!p.subject) {
124
+ errors.push('payload.subject missing');
125
+ } else {
126
+ for (const k of ['rootDid', 'actingDid', 'credentialCommitment', 'effectiveCommitment'] as const) {
127
+ if (typeof (p.subject as any)[k] !== 'string' || !(p.subject as any)[k]) {
128
+ errors.push(`payload.subject.${k} missing or not a string`);
129
+ }
130
+ }
131
+ }
132
+
133
+ // decision
134
+ if (!p.decision) {
135
+ errors.push('payload.decision missing');
136
+ } else {
137
+ if (typeof p.decision.allowed !== 'boolean') errors.push('decision.allowed not boolean');
138
+ if (typeof p.decision.score !== 'number') errors.push('decision.score not a number');
139
+ if (typeof p.decision.permissionBitmask !== 'string' || !p.decision.permissionBitmask) errors.push('decision.permissionBitmask missing or not a string');
140
+ if (typeof p.decision.chainDepth !== 'number') errors.push('decision.chainDepth not a number');
141
+ }
142
+
143
+ // proof
144
+ if (!p.proof) {
145
+ errors.push('payload.proof missing');
146
+ } else {
147
+ for (const k of ['bundleVersion', 'nonce', 'humanProofHash', 'agentProofHash', 'publicSignalsHash'] as const) {
148
+ if (!(k in p.proof)) errors.push(`proof.${k} missing`);
149
+ }
150
+ }
151
+
152
+ // commerce fields: required for bolyra.commerce, forbidden for bolyra.auth
153
+ if (p.kind === 'bolyra.auth' && (p as any).commerce) {
154
+ errors.push('auth receipt must not contain commerce fields');
155
+ }
156
+ if (p.kind === 'bolyra.commerce') {
157
+ const c = (p as any).commerce;
158
+ if (!c) {
159
+ errors.push('commerce fields missing for bolyra.commerce receipt');
160
+ } else {
161
+ if (typeof c.rail !== 'string' || !c.rail) errors.push('commerce.rail missing or not a string');
162
+ if (typeof c.amount !== 'number') errors.push('commerce.amount not a number');
163
+ if (typeof c.currency !== 'string' || !c.currency) errors.push('commerce.currency missing or not a string');
164
+ if (typeof c.merchant !== 'string') errors.push('commerce.merchant not a string');
165
+ if (typeof c.intentHash !== 'string' || !/^[0-9a-fA-F]{64}$/.test(c.intentHash)) {
166
+ errors.push('commerce.intentHash must be a 64-char hex string');
167
+ }
168
+ }
169
+ }
170
+
171
+ // signature fields
172
+ if (s.alg !== 'ES256K') errors.push('signature.alg !== "ES256K"');
173
+ if (s.keyId !== p.keyId) errors.push('signature.keyId does not match payload.keyId');
174
+ if (!/^0x[0-9a-fA-F]{40}$/.test(s.signer ?? '')) errors.push('signature.signer invalid format');
175
+ if (!/^0x[0-9a-fA-F]{64}$/.test(s.payloadHash ?? '')) errors.push('signature.payloadHash invalid format');
176
+ if (!/^0x[0-9a-fA-F]{130}$/.test(s.value ?? '')) errors.push('signature.value invalid format');
177
+
178
+ if (errors.length) {
179
+ failCheck(`Schema: ${errors.join('; ')}`);
180
+ return false;
181
+ }
182
+ pass('Schema valid');
183
+ return true;
184
+ }
185
+
186
+ // 2. Timestamp check
187
+ function validateTimestamp(): void {
188
+ const nowSec = Math.floor(Date.now() / 1000);
189
+ const issuedAt = receipt.payload.issuedAt;
190
+ const allowFutureSeconds = 300;
191
+ const ageSec = nowSec - issuedAt;
192
+ const isoStr = new Date(issuedAt * 1000).toISOString();
193
+
194
+ if (issuedAt > nowSec + allowFutureSeconds) {
195
+ failCheck(`Timestamp: ${isoStr} is ${issuedAt - nowSec}s in the future (max skew: ${allowFutureSeconds}s)`);
196
+ return;
197
+ }
198
+ if (ageSec > maxAgeSeconds) {
199
+ failCheck(`Timestamp: ${isoStr} (${ageSec}s ago, max-age: ${maxAgeSeconds}s)`);
200
+ return;
201
+ }
202
+ pass(`Timestamp: ${isoStr} (${ageSec}s ago)`);
203
+ }
204
+
205
+ // 3. Hash check
206
+ function validateHash(): boolean {
207
+ const computed = hashPayload(receipt.payload);
208
+ if (computed !== receipt.signature.payloadHash) {
209
+ failCheck(`Payload hash mismatch (expected ${truncate(receipt.signature.payloadHash)}, got ${truncate(computed)})`);
210
+ return false;
211
+ }
212
+ pass('Payload hash matches');
213
+ return true;
214
+ }
215
+
216
+ // 4. ID check
217
+ function validateId(): void {
218
+ const expected = '0x' + receipt.signature.payloadHash.slice(2, 18);
219
+ if (receipt.id !== expected) {
220
+ failCheck(`Receipt ID mismatch (expected ${expected}, got ${receipt.id})`);
221
+ return;
222
+ }
223
+ pass('Receipt ID matches');
224
+ }
225
+
226
+ // 5. Signature check
227
+ function validateSignature(): void {
228
+ const ok = verifyReceipt(receipt, expectedSigner);
229
+ if (!ok) {
230
+ failCheck(`Signature invalid${expectedSigner ? ` (expected signer: ${truncate(expectedSigner)})` : ''}`);
231
+ return;
232
+ }
233
+ pass(`Signature valid (signer: ${truncate(receipt.signature.signer)})`);
234
+ }
235
+
236
+ // --- Run checks ---
237
+
238
+ if (validateSchema()) {
239
+ validateTimestamp();
240
+ if (validateHash()) {
241
+ validateId();
242
+ validateSignature();
243
+ }
244
+ }
245
+
246
+ // --- Output ---
247
+
248
+ for (const line of checks) {
249
+ console.log(line);
250
+ }
251
+
252
+ if (failed) {
253
+ console.log('FAIL \u2014 receipt is invalid');
254
+ process.exit(1);
255
+ } else {
256
+ console.log('PASS \u2014 receipt is valid');
257
+ process.exit(0);
258
+ }