@peac/cli 0.10.9 → 0.10.11
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/LICENSE +1 -1
- package/dist/index.cjs +2257 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +2236 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +21 -16
- package/dist/cli.js +0 -90
- package/dist/cli.js.map +0 -1
- package/dist/commands/bridge/index.js +0 -20
- package/dist/commands/bridge/index.js.map +0 -1
- package/dist/commands/bridge/install.js +0 -27
- package/dist/commands/bridge/install.js.map +0 -1
- package/dist/commands/bridge/start.js +0 -123
- package/dist/commands/bridge/start.js.map +0 -1
- package/dist/commands/bridge/status.js +0 -192
- package/dist/commands/bridge/status.js.map +0 -1
- package/dist/commands/bridge/stop.js +0 -130
- package/dist/commands/bridge/stop.js.map +0 -1
- package/dist/commands/bundle.js +0 -363
- package/dist/commands/bundle.js.map +0 -1
- package/dist/commands/conformance.js +0 -301
- package/dist/commands/conformance.js.map +0 -1
- package/dist/commands/discover.js +0 -39
- package/dist/commands/discover.js.map +0 -1
- package/dist/commands/hash.js +0 -106
- package/dist/commands/hash.js.map +0 -1
- package/dist/commands/policy.js +0 -712
- package/dist/commands/policy.js.map +0 -1
- package/dist/commands/samples.js +0 -360
- package/dist/commands/samples.js.map +0 -1
- package/dist/commands/verify.js +0 -50
- package/dist/commands/verify.js.map +0 -1
- package/dist/commands/workflow.d.ts +0 -11
- package/dist/commands/workflow.d.ts.map +0 -1
- package/dist/commands/workflow.js +0 -214
- package/dist/commands/workflow.js.map +0 -1
- package/dist/index.js +0 -240
- package/dist/index.js.map +0 -1
- package/dist/lib/conformance/digest.js +0 -202
- package/dist/lib/conformance/digest.js.map +0 -1
- package/dist/lib/conformance/index.js +0 -30
- package/dist/lib/conformance/index.js.map +0 -1
- package/dist/lib/conformance/manifest.js +0 -66
- package/dist/lib/conformance/manifest.js.map +0 -1
- package/dist/lib/conformance/profiles.js +0 -138
- package/dist/lib/conformance/profiles.js.map +0 -1
- package/dist/lib/conformance/types.js +0 -8
- package/dist/lib/conformance/types.js.map +0 -1
- package/dist/lib/conformance/validators.js +0 -415
- package/dist/lib/conformance/validators.js.map +0 -1
- package/dist/lib/conformance-runner.js +0 -433
- package/dist/lib/conformance-runner.js.map +0 -1
- package/dist/lib/samples-loader.js +0 -311
- package/dist/lib/samples-loader.js.map +0 -1
- package/dist/lib/version.js +0 -61
- package/dist/lib/version.js.map +0 -1
- package/dist/types.js +0 -6
- package/dist/types.js.map +0 -1
- package/dist/utils.js +0 -101
- package/dist/utils.js.map +0 -1
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2257 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var commander = require('commander');
|
|
5
|
+
var protocol = require('@peac/protocol');
|
|
6
|
+
var crypto = require('@peac/crypto');
|
|
7
|
+
var fs6 = require('fs');
|
|
8
|
+
var path6 = require('path');
|
|
9
|
+
var policyKit = require('@peac/policy-kit');
|
|
10
|
+
var child_process = require('child_process');
|
|
11
|
+
var crypto$1 = require('crypto');
|
|
12
|
+
var schema = require('@peac/schema');
|
|
13
|
+
var disc = require('@peac/disc');
|
|
14
|
+
|
|
15
|
+
function _interopNamespace(e) {
|
|
16
|
+
if (e && e.__esModule) return e;
|
|
17
|
+
var n = Object.create(null);
|
|
18
|
+
if (e) {
|
|
19
|
+
Object.keys(e).forEach(function (k) {
|
|
20
|
+
if (k !== 'default') {
|
|
21
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
22
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
get: function () { return e[k]; }
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
n.default = e;
|
|
30
|
+
return Object.freeze(n);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
var fs6__namespace = /*#__PURE__*/_interopNamespace(fs6);
|
|
34
|
+
var path6__namespace = /*#__PURE__*/_interopNamespace(path6);
|
|
35
|
+
|
|
36
|
+
function getGlobalOptions(cmd) {
|
|
37
|
+
const parent = cmd.parent;
|
|
38
|
+
if (!parent) return {};
|
|
39
|
+
return parent.opts();
|
|
40
|
+
}
|
|
41
|
+
function output(data, opts, humanMessage) {
|
|
42
|
+
if (opts.json) {
|
|
43
|
+
console.log(JSON.stringify(data, null, 2));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function outputError(error, details, opts) {
|
|
47
|
+
if (opts.json) {
|
|
48
|
+
console.log(JSON.stringify({ success: false, error, ...details }, null, 2));
|
|
49
|
+
} else {
|
|
50
|
+
console.error(`Error: ${error}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var policy = new commander.Command("policy").description("Policy file operations").option("--json", "Output in machine-readable JSON format").option("--yes", "Skip confirmation prompts (auto-confirm)").option("--strict", "Exit non-zero on warnings");
|
|
54
|
+
policy.command("init").description("Create a new PEAC policy file").option("-f, --format <format>", "Output format (yaml or json)", "yaml").option("-o, --output <file>", "Output file path").option("--force", "Overwrite existing file").option("--profile <id>", "Use a pre-built profile as template").action(
|
|
55
|
+
(options, cmd) => {
|
|
56
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
57
|
+
try {
|
|
58
|
+
const format = options.format?.toLowerCase() || "yaml";
|
|
59
|
+
const outputPath = options.output || (format === "json" ? "peac-policy.json" : "peac-policy.yaml");
|
|
60
|
+
if (fs6__namespace.existsSync(outputPath) && !options.force && !globalOpts.yes) {
|
|
61
|
+
outputError(
|
|
62
|
+
`File already exists: ${outputPath}`,
|
|
63
|
+
{ path: outputPath, hint: "Use --force or --yes to overwrite" },
|
|
64
|
+
globalOpts
|
|
65
|
+
);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
let content;
|
|
69
|
+
let policyName;
|
|
70
|
+
if (options.profile) {
|
|
71
|
+
try {
|
|
72
|
+
const profile = policyKit.loadProfile(options.profile);
|
|
73
|
+
const policyDoc = profile.policy;
|
|
74
|
+
content = format === "json" ? policyKit.serializePolicyJson(policyDoc) : policyKit.serializePolicyYaml(policyDoc);
|
|
75
|
+
policyName = profile.name;
|
|
76
|
+
} catch (err) {
|
|
77
|
+
if (err instanceof policyKit.ProfileError) {
|
|
78
|
+
outputError(err.message, { code: err.code, profile: options.profile }, globalOpts);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const example = policyKit.createExamplePolicy();
|
|
85
|
+
content = format === "json" ? policyKit.serializePolicyJson(example) : policyKit.serializePolicyYaml(example);
|
|
86
|
+
policyName = "Example Policy";
|
|
87
|
+
}
|
|
88
|
+
fs6__namespace.writeFileSync(outputPath, content, "utf-8");
|
|
89
|
+
if (globalOpts.json) {
|
|
90
|
+
output(
|
|
91
|
+
{
|
|
92
|
+
success: true,
|
|
93
|
+
file: outputPath,
|
|
94
|
+
format,
|
|
95
|
+
profile: options.profile || null,
|
|
96
|
+
policyName
|
|
97
|
+
},
|
|
98
|
+
globalOpts
|
|
99
|
+
);
|
|
100
|
+
} else {
|
|
101
|
+
console.log(`Created policy file: ${outputPath}`);
|
|
102
|
+
if (options.profile) {
|
|
103
|
+
console.log(`Based on profile: ${options.profile}`);
|
|
104
|
+
}
|
|
105
|
+
console.log("");
|
|
106
|
+
console.log("Next steps:");
|
|
107
|
+
console.log(" 1. Edit the policy file to define your access rules");
|
|
108
|
+
console.log(" 2. Validate with: peac policy validate " + outputPath);
|
|
109
|
+
console.log(" 3. Generate artifacts with: peac policy generate " + outputPath);
|
|
110
|
+
}
|
|
111
|
+
process.exit(0);
|
|
112
|
+
} catch (err) {
|
|
113
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
policy.command("validate").description("Validate a PEAC policy file (YAML or JSON)").argument("<file>", "Path to policy file").option("-v, --verbose", "Show detailed validation output").action((file, options, cmd) => {
|
|
119
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
120
|
+
const warnings = [];
|
|
121
|
+
try {
|
|
122
|
+
const policyDoc = policyKit.loadPolicy(file);
|
|
123
|
+
const result = {
|
|
124
|
+
success: true,
|
|
125
|
+
file,
|
|
126
|
+
valid: true,
|
|
127
|
+
warnings,
|
|
128
|
+
policy: {
|
|
129
|
+
version: policyDoc.version,
|
|
130
|
+
name: policyDoc.name || null,
|
|
131
|
+
defaultDecision: policyDoc.defaults.decision,
|
|
132
|
+
ruleCount: policyDoc.rules.length,
|
|
133
|
+
rules: options.verbose ? policyDoc.rules.map((r) => ({
|
|
134
|
+
name: r.name,
|
|
135
|
+
decision: r.decision,
|
|
136
|
+
subject: r.subject || null,
|
|
137
|
+
purpose: r.purpose || null,
|
|
138
|
+
licensingMode: r.licensing_mode || null
|
|
139
|
+
})) : void 0
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
if (globalOpts.json) {
|
|
143
|
+
output(result, globalOpts);
|
|
144
|
+
} else {
|
|
145
|
+
console.log(`Validating policy file: ${file}
|
|
146
|
+
`);
|
|
147
|
+
console.log("Policy is valid!\n");
|
|
148
|
+
console.log("Policy Information:");
|
|
149
|
+
console.log(` Version: ${policyDoc.version}`);
|
|
150
|
+
if (policyDoc.name) {
|
|
151
|
+
console.log(` Name: ${policyDoc.name}`);
|
|
152
|
+
}
|
|
153
|
+
console.log(` Default: ${policyDoc.defaults.decision}`);
|
|
154
|
+
console.log(` Rules: ${policyDoc.rules.length}`);
|
|
155
|
+
if (options.verbose && policyDoc.rules.length > 0) {
|
|
156
|
+
console.log("\nRules:");
|
|
157
|
+
for (const rule of policyDoc.rules) {
|
|
158
|
+
console.log(` - ${rule.name}: ${rule.decision}`);
|
|
159
|
+
if (rule.subject) {
|
|
160
|
+
const parts = [];
|
|
161
|
+
if (rule.subject.type) {
|
|
162
|
+
const types = Array.isArray(rule.subject.type) ? rule.subject.type.join("|") : rule.subject.type;
|
|
163
|
+
parts.push(`type=${types}`);
|
|
164
|
+
}
|
|
165
|
+
if (rule.subject.labels) {
|
|
166
|
+
parts.push(`labels=[${rule.subject.labels.join(",")}]`);
|
|
167
|
+
}
|
|
168
|
+
if (rule.subject.id) {
|
|
169
|
+
parts.push(`id=${rule.subject.id}`);
|
|
170
|
+
}
|
|
171
|
+
if (parts.length > 0) {
|
|
172
|
+
console.log(` subject: ${parts.join(", ")}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (rule.purpose) {
|
|
176
|
+
const purposes = Array.isArray(rule.purpose) ? rule.purpose.join("|") : rule.purpose;
|
|
177
|
+
console.log(` purpose: ${purposes}`);
|
|
178
|
+
}
|
|
179
|
+
if (rule.licensing_mode) {
|
|
180
|
+
const modes = Array.isArray(rule.licensing_mode) ? rule.licensing_mode.join("|") : rule.licensing_mode;
|
|
181
|
+
console.log(` licensing_mode: ${modes}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
if (warnings.length > 0) {
|
|
186
|
+
console.log("\nWarnings:");
|
|
187
|
+
for (const warning of warnings) {
|
|
188
|
+
console.log(` - ${warning}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (globalOpts.strict && warnings.length > 0) {
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
process.exit(0);
|
|
196
|
+
} catch (err) {
|
|
197
|
+
if (err instanceof policyKit.PolicyValidationError) {
|
|
198
|
+
const issues = err.issues.map((i) => ({
|
|
199
|
+
path: i.path.join("."),
|
|
200
|
+
message: i.message
|
|
201
|
+
}));
|
|
202
|
+
if (globalOpts.json) {
|
|
203
|
+
output(
|
|
204
|
+
{
|
|
205
|
+
success: false,
|
|
206
|
+
file,
|
|
207
|
+
valid: false,
|
|
208
|
+
error: "Validation failed",
|
|
209
|
+
issues
|
|
210
|
+
},
|
|
211
|
+
globalOpts
|
|
212
|
+
);
|
|
213
|
+
} else {
|
|
214
|
+
console.error("Policy validation failed:\n");
|
|
215
|
+
for (const issue of err.issues) {
|
|
216
|
+
console.error(` - ${issue.path.join(".")}: ${issue.message}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
process.exit(1);
|
|
220
|
+
} else if (err instanceof policyKit.PolicyLoadError) {
|
|
221
|
+
outputError(`Failed to load policy: ${err.message}`, { file }, globalOpts);
|
|
222
|
+
process.exit(1);
|
|
223
|
+
} else {
|
|
224
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
policy.command("generate").description("Generate deployment artifacts from a policy file").argument("<file>", "Path to policy file").option("-o, --out <dir>", 'Output directory (use "-" for stdout)', "dist").option("--well-known", "Output peac.txt to .well-known/ subdirectory").option("--dry-run", "Show what would be generated without writing files").option("--peac-version <version>", "PEAC protocol version (default: 0.9)").option("--site-url <url>", "Site URL for peac.txt").option("--contact <email>", "Contact email for policy questions").option("--attribution <mode>", "Attribution requirement (required, optional, none)").option("--receipts <mode>", "Receipts requirement (required, optional, omit)").option("--rate-limit <limit>", 'Rate limit string (e.g., "100/hour", "unlimited")').option("--negotiate <url>", "Negotiate endpoint URL").option("--no-comments", "Omit comments from generated files").action(
|
|
230
|
+
(file, options, cmd) => {
|
|
231
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
232
|
+
const isStdout = options.out === "-";
|
|
233
|
+
try {
|
|
234
|
+
if (!globalOpts.json && !isStdout) {
|
|
235
|
+
console.log(`Loading policy: ${file}
|
|
236
|
+
`);
|
|
237
|
+
}
|
|
238
|
+
const policyDoc = policyKit.loadPolicy(file);
|
|
239
|
+
const compileOptions = {
|
|
240
|
+
peacVersion: options.peacVersion,
|
|
241
|
+
siteUrl: options.siteUrl,
|
|
242
|
+
contact: options.contact,
|
|
243
|
+
attribution: options.attribution,
|
|
244
|
+
receipts: options.receipts,
|
|
245
|
+
rateLimit: options.rateLimit,
|
|
246
|
+
negotiateUrl: options.negotiate,
|
|
247
|
+
includeComments: options.comments !== false
|
|
248
|
+
};
|
|
249
|
+
const peacTxt = policyKit.compilePeacTxt(policyDoc, compileOptions);
|
|
250
|
+
const robotsTxt = policyKit.compileRobotsSnippet(policyDoc, compileOptions);
|
|
251
|
+
const aiprefTemplates = policyKit.compileAiprefTemplates(policyDoc, compileOptions);
|
|
252
|
+
const markdown = policyKit.renderPolicyMarkdown(policyDoc, compileOptions);
|
|
253
|
+
if (isStdout || globalOpts.json) {
|
|
254
|
+
const result = {
|
|
255
|
+
success: true,
|
|
256
|
+
source: file,
|
|
257
|
+
artifacts: {
|
|
258
|
+
"peac.txt": peacTxt,
|
|
259
|
+
"robots-ai-snippet.txt": robotsTxt,
|
|
260
|
+
"aipref-headers.json": aiprefTemplates,
|
|
261
|
+
"ai-policy.md": markdown
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
if (isStdout && !globalOpts.json) {
|
|
265
|
+
console.log(peacTxt);
|
|
266
|
+
} else {
|
|
267
|
+
output(result, { json: true });
|
|
268
|
+
}
|
|
269
|
+
process.exit(0);
|
|
270
|
+
}
|
|
271
|
+
const outDir = options.out;
|
|
272
|
+
let peacTxtPath;
|
|
273
|
+
if (options.wellKnown) {
|
|
274
|
+
peacTxtPath = path6__namespace.join(outDir, ".well-known", "peac.txt");
|
|
275
|
+
} else {
|
|
276
|
+
peacTxtPath = path6__namespace.join(outDir, "peac.txt");
|
|
277
|
+
}
|
|
278
|
+
const robotsPath = path6__namespace.join(outDir, "robots-ai-snippet.txt");
|
|
279
|
+
const aiprefPath = path6__namespace.join(outDir, "aipref-headers.json");
|
|
280
|
+
const mdPath = path6__namespace.join(outDir, "ai-policy.md");
|
|
281
|
+
if (options.dryRun) {
|
|
282
|
+
console.log("Dry run - files that would be generated:\n");
|
|
283
|
+
console.log(`--- ${peacTxtPath} ---`);
|
|
284
|
+
console.log(peacTxt);
|
|
285
|
+
console.log(`--- ${robotsPath} ---`);
|
|
286
|
+
console.log(robotsTxt);
|
|
287
|
+
console.log(`--- ${aiprefPath} ---`);
|
|
288
|
+
console.log(JSON.stringify(aiprefTemplates, null, 2));
|
|
289
|
+
console.log(`
|
|
290
|
+
--- ${mdPath} ---`);
|
|
291
|
+
console.log(markdown);
|
|
292
|
+
process.exit(0);
|
|
293
|
+
}
|
|
294
|
+
if (!fs6__namespace.existsSync(outDir)) {
|
|
295
|
+
fs6__namespace.mkdirSync(outDir, { recursive: true });
|
|
296
|
+
}
|
|
297
|
+
if (options.wellKnown) {
|
|
298
|
+
const wellKnownDir = path6__namespace.join(outDir, ".well-known");
|
|
299
|
+
if (!fs6__namespace.existsSync(wellKnownDir)) {
|
|
300
|
+
fs6__namespace.mkdirSync(wellKnownDir, { recursive: true });
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
fs6__namespace.writeFileSync(peacTxtPath, peacTxt, "utf-8");
|
|
304
|
+
console.log(`Generated: ${peacTxtPath}`);
|
|
305
|
+
fs6__namespace.writeFileSync(robotsPath, robotsTxt, "utf-8");
|
|
306
|
+
console.log(`Generated: ${robotsPath}`);
|
|
307
|
+
fs6__namespace.writeFileSync(aiprefPath, JSON.stringify(aiprefTemplates, null, 2), "utf-8");
|
|
308
|
+
console.log(`Generated: ${aiprefPath}`);
|
|
309
|
+
fs6__namespace.writeFileSync(mdPath, markdown, "utf-8");
|
|
310
|
+
console.log(`Generated: ${mdPath}`);
|
|
311
|
+
console.log("");
|
|
312
|
+
console.log("Deployment instructions:");
|
|
313
|
+
if (options.wellKnown) {
|
|
314
|
+
console.log(` 1. Deploy ${peacTxtPath} to serve at /.well-known/peac.txt`);
|
|
315
|
+
} else {
|
|
316
|
+
console.log(` 1. Copy ${peacTxtPath} to /.well-known/peac.txt`);
|
|
317
|
+
}
|
|
318
|
+
console.log(` 2. Append ${robotsPath} to your robots.txt`);
|
|
319
|
+
console.log(` 3. Add headers from ${aiprefPath} to your server config`);
|
|
320
|
+
console.log(` 4. Publish ${mdPath} for human reference`);
|
|
321
|
+
process.exit(0);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
if (err instanceof policyKit.PolicyLoadError) {
|
|
324
|
+
outputError(`Failed to load policy: ${err.message}`, { file }, globalOpts);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
} else {
|
|
327
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
328
|
+
process.exit(1);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
);
|
|
333
|
+
policy.command("explain").description("Explain which rule would apply for a given context").argument("<file>", "Path to policy file").option("-t, --type <type>", "Subject type (human, org, agent)").option("-l, --labels <labels>", "Subject labels (comma-separated)").option("-i, --id <id>", "Subject ID").option(
|
|
334
|
+
"-p, --purpose <purpose>",
|
|
335
|
+
"Access purpose (crawl, index, train, inference, ai_input, ai_index, search)"
|
|
336
|
+
).option(
|
|
337
|
+
"-m, --licensing-mode <mode>",
|
|
338
|
+
"Licensing mode (subscription, pay_per_crawl, pay_per_inference)"
|
|
339
|
+
).option("--all-matches", "Show all matching rules, not just the first").action(
|
|
340
|
+
(file, options, cmd) => {
|
|
341
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
342
|
+
try {
|
|
343
|
+
const policyDoc = policyKit.loadPolicy(file);
|
|
344
|
+
const context = {};
|
|
345
|
+
if (options.type || options.labels || options.id) {
|
|
346
|
+
context.subject = {};
|
|
347
|
+
if (options.type) {
|
|
348
|
+
context.subject.type = options.type;
|
|
349
|
+
}
|
|
350
|
+
if (options.labels) {
|
|
351
|
+
context.subject.labels = options.labels.split(",").map((l) => l.trim());
|
|
352
|
+
}
|
|
353
|
+
if (options.id) {
|
|
354
|
+
context.subject.id = options.id;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (options.purpose) {
|
|
358
|
+
context.purpose = options.purpose;
|
|
359
|
+
}
|
|
360
|
+
if (options.licensingMode) {
|
|
361
|
+
context.licensing_mode = options.licensingMode;
|
|
362
|
+
}
|
|
363
|
+
if (options.allMatches) {
|
|
364
|
+
const matches = policyKit.explainMatches(policyDoc, context);
|
|
365
|
+
if (globalOpts.json) {
|
|
366
|
+
output(
|
|
367
|
+
{
|
|
368
|
+
success: true,
|
|
369
|
+
context,
|
|
370
|
+
matches: matches.map((m) => {
|
|
371
|
+
if (m === "[default]") {
|
|
372
|
+
return {
|
|
373
|
+
rule: "[default]",
|
|
374
|
+
decision: policyDoc.defaults.decision
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const rule = policyDoc.rules.find((r) => r.name === m);
|
|
378
|
+
return rule ? { rule: rule.name, decision: rule.decision } : { rule: m, decision: null };
|
|
379
|
+
})
|
|
380
|
+
},
|
|
381
|
+
globalOpts
|
|
382
|
+
);
|
|
383
|
+
} else {
|
|
384
|
+
console.log("Evaluation Context:");
|
|
385
|
+
if (context.subject) {
|
|
386
|
+
console.log(` Subject: ${JSON.stringify(context.subject)}`);
|
|
387
|
+
} else {
|
|
388
|
+
console.log(" Subject: (any)");
|
|
389
|
+
}
|
|
390
|
+
console.log(` Purpose: ${context.purpose || "(any)"}`);
|
|
391
|
+
console.log(` Licensing Mode: ${context.licensing_mode || "(any)"}`);
|
|
392
|
+
console.log();
|
|
393
|
+
console.log(`Matching Rules (${matches.length}):`);
|
|
394
|
+
for (const match of matches) {
|
|
395
|
+
if (match === "[default]") {
|
|
396
|
+
console.log(` - [default] -> ${policyDoc.defaults.decision}`);
|
|
397
|
+
} else {
|
|
398
|
+
const rule = policyDoc.rules.find((r) => r.name === match);
|
|
399
|
+
if (rule) {
|
|
400
|
+
console.log(` - ${rule.name} -> ${rule.decision}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
} else {
|
|
406
|
+
const result = policyKit.evaluate(policyDoc, context);
|
|
407
|
+
if (globalOpts.json) {
|
|
408
|
+
output(
|
|
409
|
+
{
|
|
410
|
+
success: true,
|
|
411
|
+
context,
|
|
412
|
+
result: {
|
|
413
|
+
decision: result.decision,
|
|
414
|
+
matchedRule: result.matched_rule || "[default]",
|
|
415
|
+
reason: result.reason || null
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
globalOpts
|
|
419
|
+
);
|
|
420
|
+
} else {
|
|
421
|
+
console.log("Evaluation Context:");
|
|
422
|
+
if (context.subject) {
|
|
423
|
+
console.log(` Subject: ${JSON.stringify(context.subject)}`);
|
|
424
|
+
} else {
|
|
425
|
+
console.log(" Subject: (any)");
|
|
426
|
+
}
|
|
427
|
+
console.log(` Purpose: ${context.purpose || "(any)"}`);
|
|
428
|
+
console.log(` Licensing Mode: ${context.licensing_mode || "(any)"}`);
|
|
429
|
+
console.log();
|
|
430
|
+
console.log("Result:");
|
|
431
|
+
console.log(` Decision: ${result.decision}`);
|
|
432
|
+
if (result.matched_rule) {
|
|
433
|
+
console.log(` Matched Rule: ${result.matched_rule}`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(" Matched Rule: (default)");
|
|
436
|
+
}
|
|
437
|
+
if (result.reason) {
|
|
438
|
+
console.log(` Reason: ${result.reason}`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
process.exit(0);
|
|
443
|
+
} catch (err) {
|
|
444
|
+
if (err instanceof policyKit.PolicyLoadError) {
|
|
445
|
+
outputError(`Failed to load policy: ${err.message}`, { file }, globalOpts);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
} else {
|
|
448
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
);
|
|
454
|
+
policy.command("list-profiles").description("List available pre-built policy profiles").action((options, cmd) => {
|
|
455
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
456
|
+
try {
|
|
457
|
+
const profileIds = policyKit.listProfiles();
|
|
458
|
+
const profiles = profileIds.map((id) => policyKit.getProfileSummary(id));
|
|
459
|
+
if (globalOpts.json) {
|
|
460
|
+
output(
|
|
461
|
+
{
|
|
462
|
+
success: true,
|
|
463
|
+
profiles: profiles.map((p) => ({
|
|
464
|
+
id: p.id,
|
|
465
|
+
name: p.name,
|
|
466
|
+
description: p.description,
|
|
467
|
+
defaultDecision: p.defaultDecision,
|
|
468
|
+
ruleCount: p.ruleCount,
|
|
469
|
+
requiresReceipt: p.requiresReceipt,
|
|
470
|
+
requiredParams: p.requiredParams,
|
|
471
|
+
optionalParams: p.optionalParams
|
|
472
|
+
}))
|
|
473
|
+
},
|
|
474
|
+
globalOpts
|
|
475
|
+
);
|
|
476
|
+
} else {
|
|
477
|
+
console.log("Available Policy Profiles:\n");
|
|
478
|
+
for (const profile of profiles) {
|
|
479
|
+
console.log(` ${profile.id}`);
|
|
480
|
+
console.log(` Name: ${profile.name}`);
|
|
481
|
+
console.log(` Default: ${profile.defaultDecision}`);
|
|
482
|
+
console.log(` Rules: ${profile.ruleCount}`);
|
|
483
|
+
if (profile.requiresReceipt) {
|
|
484
|
+
console.log(" Requires receipt: yes");
|
|
485
|
+
}
|
|
486
|
+
if (profile.requiredParams.length > 0) {
|
|
487
|
+
console.log(` Required params: ${profile.requiredParams.join(", ")}`);
|
|
488
|
+
}
|
|
489
|
+
console.log();
|
|
490
|
+
}
|
|
491
|
+
console.log('Use "peac policy show-profile <id>" for full details');
|
|
492
|
+
console.log('Use "peac policy init --profile <id>" to create a policy from a profile');
|
|
493
|
+
}
|
|
494
|
+
process.exit(0);
|
|
495
|
+
} catch (err) {
|
|
496
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
policy.command("show-profile").description("Show details of a specific policy profile").argument("<id>", "Profile ID").action((id, options, cmd) => {
|
|
501
|
+
const globalOpts = getGlobalOptions(cmd);
|
|
502
|
+
try {
|
|
503
|
+
const profile = policyKit.loadProfile(id);
|
|
504
|
+
const summary = policyKit.getProfileSummary(id);
|
|
505
|
+
if (globalOpts.json) {
|
|
506
|
+
output(
|
|
507
|
+
{
|
|
508
|
+
success: true,
|
|
509
|
+
profile: {
|
|
510
|
+
id: profile.id,
|
|
511
|
+
name: profile.name,
|
|
512
|
+
description: profile.description,
|
|
513
|
+
policy: profile.policy,
|
|
514
|
+
parameters: profile.parameters,
|
|
515
|
+
defaults: profile.defaults
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
globalOpts
|
|
519
|
+
);
|
|
520
|
+
} else {
|
|
521
|
+
console.log(`Profile: ${profile.name}`);
|
|
522
|
+
console.log(`ID: ${profile.id}`);
|
|
523
|
+
console.log();
|
|
524
|
+
console.log("Description:");
|
|
525
|
+
console.log(
|
|
526
|
+
profile.description.split("\n").map((l) => ` ${l}`).join("\n")
|
|
527
|
+
);
|
|
528
|
+
console.log();
|
|
529
|
+
console.log("Policy:");
|
|
530
|
+
console.log(` Version: ${profile.policy.version}`);
|
|
531
|
+
console.log(` Default decision: ${profile.policy.defaults.decision}`);
|
|
532
|
+
console.log(` Rules: ${profile.policy.rules?.length || 0}`);
|
|
533
|
+
if (profile.policy.rules && profile.policy.rules.length > 0) {
|
|
534
|
+
console.log();
|
|
535
|
+
console.log("Rules:");
|
|
536
|
+
for (const rule of profile.policy.rules) {
|
|
537
|
+
console.log(` - ${rule.name}: ${rule.decision}`);
|
|
538
|
+
if (rule.purpose) {
|
|
539
|
+
const purposes = Array.isArray(rule.purpose) ? rule.purpose.join(", ") : rule.purpose;
|
|
540
|
+
console.log(` purpose: ${purposes}`);
|
|
541
|
+
}
|
|
542
|
+
if (rule.reason) {
|
|
543
|
+
console.log(` reason: ${rule.reason}`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (Object.keys(profile.parameters || {}).length > 0) {
|
|
548
|
+
console.log();
|
|
549
|
+
console.log("Parameters:");
|
|
550
|
+
for (const [key, param] of Object.entries(profile.parameters || {})) {
|
|
551
|
+
const required = param.required ? " (required)" : "";
|
|
552
|
+
console.log(` ${key}${required}`);
|
|
553
|
+
console.log(` ${param.description}`);
|
|
554
|
+
if (param.default) {
|
|
555
|
+
console.log(` Default: ${param.default}`);
|
|
556
|
+
}
|
|
557
|
+
if (param.example) {
|
|
558
|
+
console.log(` Example: ${param.example}`);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (profile.defaults) {
|
|
563
|
+
console.log();
|
|
564
|
+
console.log("Defaults:");
|
|
565
|
+
if (profile.defaults.requirements?.receipt) {
|
|
566
|
+
console.log(" Receipt required: yes");
|
|
567
|
+
}
|
|
568
|
+
if (profile.defaults.rate_limit) {
|
|
569
|
+
console.log(
|
|
570
|
+
` Rate limit: ${profile.defaults.rate_limit.limit}/${profile.defaults.rate_limit.window_seconds}s`
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
process.exit(0);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
if (err instanceof policyKit.ProfileError) {
|
|
578
|
+
outputError(err.message, { code: err.code, profile: id }, globalOpts);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
outputError(err instanceof Error ? err.message : String(err), {}, globalOpts);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
function sha256(content) {
|
|
586
|
+
return crypto$1.createHash("sha256").update(content, "utf8").digest("hex");
|
|
587
|
+
}
|
|
588
|
+
function zodPathToJsonPointer(path8) {
|
|
589
|
+
if (path8.length === 0) return "";
|
|
590
|
+
return "/" + path8.map((segment) => {
|
|
591
|
+
const str = String(segment);
|
|
592
|
+
return str.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
593
|
+
}).join("/");
|
|
594
|
+
}
|
|
595
|
+
function computeCanonicalDigest(input) {
|
|
596
|
+
try {
|
|
597
|
+
const canonical = crypto.canonicalize(input);
|
|
598
|
+
return { alg: "sha-256+jcs", value: sha256(canonical) };
|
|
599
|
+
} catch {
|
|
600
|
+
return { alg: "sha-256+json", value: sha256(JSON.stringify(input)) };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
var VECTORS_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".json", ".txt", ".jwks", ".pem", ".zip"]);
|
|
604
|
+
var VECTORS_EXCLUDE_FILES = /* @__PURE__ */ new Set([".DS_Store", ".gitkeep", "Thumbs.db"]);
|
|
605
|
+
function collectFilesRecursive(dir, basePath = "") {
|
|
606
|
+
const files = [];
|
|
607
|
+
if (!fs6__namespace.existsSync(dir)) return files;
|
|
608
|
+
const entries = fs6__namespace.readdirSync(dir, { withFileTypes: true });
|
|
609
|
+
for (const entry of entries) {
|
|
610
|
+
if (VECTORS_EXCLUDE_FILES.has(entry.name)) continue;
|
|
611
|
+
const relativePath = basePath ? `${basePath}/${entry.name}` : entry.name;
|
|
612
|
+
const fullPath = path6__namespace.join(dir, entry.name);
|
|
613
|
+
if (entry.isDirectory()) {
|
|
614
|
+
files.push(...collectFilesRecursive(fullPath, relativePath));
|
|
615
|
+
} else {
|
|
616
|
+
const ext = path6__namespace.extname(entry.name).toLowerCase();
|
|
617
|
+
if (VECTORS_FILE_EXTENSIONS.has(ext)) {
|
|
618
|
+
files.push({
|
|
619
|
+
relativePath,
|
|
620
|
+
content: fs6__namespace.readFileSync(fullPath, "utf8")
|
|
621
|
+
});
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
return files;
|
|
626
|
+
}
|
|
627
|
+
function computeVectorsDigest(fixturesDir, categories) {
|
|
628
|
+
const hash = crypto$1.createHash("sha256");
|
|
629
|
+
const allFiles = [];
|
|
630
|
+
const manifestPath = path6__namespace.join(fixturesDir, "manifest.json");
|
|
631
|
+
if (fs6__namespace.existsSync(manifestPath)) {
|
|
632
|
+
allFiles.push({
|
|
633
|
+
relativePath: "manifest.json",
|
|
634
|
+
content: fs6__namespace.readFileSync(manifestPath, "utf8")
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
const sortedCategories = [...categories].sort();
|
|
638
|
+
for (const cat of sortedCategories) {
|
|
639
|
+
const categoryPath = path6__namespace.join(fixturesDir, cat);
|
|
640
|
+
const categoryFiles = collectFilesRecursive(categoryPath, cat);
|
|
641
|
+
allFiles.push(...categoryFiles);
|
|
642
|
+
}
|
|
643
|
+
allFiles.sort((a, b) => a.relativePath.localeCompare(b.relativePath));
|
|
644
|
+
for (const file of allFiles) {
|
|
645
|
+
const fileHash = sha256(file.content);
|
|
646
|
+
hash.update(file.relativePath);
|
|
647
|
+
hash.update("\0");
|
|
648
|
+
hash.update(fileHash);
|
|
649
|
+
}
|
|
650
|
+
return hash.digest("hex");
|
|
651
|
+
}
|
|
652
|
+
function loadManifest(fixturesDir) {
|
|
653
|
+
const manifestPath = path6__namespace.join(fixturesDir, "manifest.json");
|
|
654
|
+
if (!fs6__namespace.existsSync(manifestPath)) {
|
|
655
|
+
return {};
|
|
656
|
+
}
|
|
657
|
+
try {
|
|
658
|
+
return JSON.parse(fs6__namespace.readFileSync(manifestPath, "utf8"));
|
|
659
|
+
} catch {
|
|
660
|
+
return {};
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
function getManifestEntry(manifest, category, filename) {
|
|
664
|
+
return manifest[category]?.[filename];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
// src/lib/conformance/profiles.ts
|
|
668
|
+
var PROFILE_CAPABILITIES = {
|
|
669
|
+
valid: {
|
|
670
|
+
profile: "receipt.verify.claims",
|
|
671
|
+
level: "semantic",
|
|
672
|
+
validator: "@peac/schema:ReceiptClaimsSchema"
|
|
673
|
+
},
|
|
674
|
+
invalid: {
|
|
675
|
+
profile: "receipt.verify.claims",
|
|
676
|
+
level: "semantic",
|
|
677
|
+
validator: "@peac/schema:ReceiptClaimsSchema"
|
|
678
|
+
},
|
|
679
|
+
edge: {
|
|
680
|
+
profile: "receipt.verify.claims",
|
|
681
|
+
level: "semantic",
|
|
682
|
+
validator: "@peac/schema:ReceiptClaimsSchema"
|
|
683
|
+
},
|
|
684
|
+
"agent-identity": {
|
|
685
|
+
profile: "receipt.verify.agent-identity",
|
|
686
|
+
level: "semantic",
|
|
687
|
+
validator: "@peac/schema:validateAgentIdentityAttestation"
|
|
688
|
+
},
|
|
689
|
+
attribution: {
|
|
690
|
+
profile: "receipt.verify.attribution",
|
|
691
|
+
level: "semantic",
|
|
692
|
+
validator: "@peac/schema:validateAttributionAttestation"
|
|
693
|
+
},
|
|
694
|
+
bundle: {
|
|
695
|
+
profile: "bundle.shape",
|
|
696
|
+
level: "shape",
|
|
697
|
+
validator: "structural-check",
|
|
698
|
+
notes: "Structure-only; no cryptographic integrity verification"
|
|
699
|
+
},
|
|
700
|
+
discovery: {
|
|
701
|
+
profile: "discovery.config",
|
|
702
|
+
level: "semantic",
|
|
703
|
+
validator: "@peac/disc:parse"
|
|
704
|
+
},
|
|
705
|
+
dispute: {
|
|
706
|
+
profile: "receipt.verify.dispute",
|
|
707
|
+
level: "semantic",
|
|
708
|
+
validator: "@peac/schema:validateDisputeAttestation"
|
|
709
|
+
},
|
|
710
|
+
interaction: {
|
|
711
|
+
profile: "receipt.verify.interaction",
|
|
712
|
+
level: "semantic",
|
|
713
|
+
validator: "@peac/schema:validateInteractionOrdered"
|
|
714
|
+
},
|
|
715
|
+
issue: {
|
|
716
|
+
profile: "receipt.issue",
|
|
717
|
+
level: "semantic",
|
|
718
|
+
validator: "@peac/schema:ReceiptClaimsSchema"
|
|
719
|
+
},
|
|
720
|
+
obligations: {
|
|
721
|
+
profile: "receipt.verify.obligations",
|
|
722
|
+
level: "semantic",
|
|
723
|
+
validator: "@peac/schema:validateObligationsExtension"
|
|
724
|
+
},
|
|
725
|
+
policy: {
|
|
726
|
+
profile: "policy.evaluate",
|
|
727
|
+
level: "shape",
|
|
728
|
+
validator: "structural-check",
|
|
729
|
+
notes: "Structure-only; no policy evaluation logic"
|
|
730
|
+
},
|
|
731
|
+
purpose: {
|
|
732
|
+
profile: "receipt.verify.purpose",
|
|
733
|
+
level: "semantic",
|
|
734
|
+
validator: "@peac/schema:isValidPurposeToken"
|
|
735
|
+
},
|
|
736
|
+
verifier: {
|
|
737
|
+
profile: "receipt.verify",
|
|
738
|
+
level: "semantic",
|
|
739
|
+
validator: "@peac/schema:ReceiptClaimsSchema"
|
|
740
|
+
},
|
|
741
|
+
workflow: {
|
|
742
|
+
profile: "workflow.validate",
|
|
743
|
+
level: "semantic",
|
|
744
|
+
validator: "@peac/schema:validateWorkflowContextOrdered"
|
|
745
|
+
},
|
|
746
|
+
x402: {
|
|
747
|
+
profile: "transport.x402.shape",
|
|
748
|
+
level: "shape",
|
|
749
|
+
validator: "structural-check",
|
|
750
|
+
notes: "Structure-only; no offer/receipt verification or term matching"
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
function getCategoryCapability(category) {
|
|
754
|
+
return PROFILE_CAPABILITIES[category] ?? {
|
|
755
|
+
profile: `receipt.verify.${category}`,
|
|
756
|
+
level: "semantic",
|
|
757
|
+
validator: "@peac/schema:ReceiptClaimsSchema (heuristic)",
|
|
758
|
+
notes: "Unknown category - uses semantic receipt validation when input looks like claims"
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
function getCategoryProfile(category) {
|
|
762
|
+
return getCategoryCapability(category).profile;
|
|
763
|
+
}
|
|
764
|
+
function shouldRunAtLevel(fixtureVersion, level) {
|
|
765
|
+
if (!fixtureVersion) return true;
|
|
766
|
+
const [major, minor] = fixtureVersion.split(".").map(Number);
|
|
767
|
+
const fixtureLevel = major > 0 || minor >= 10 ? "full" : minor >= 9 ? "standard" : "basic";
|
|
768
|
+
const levelOrder = ["basic", "standard", "full"];
|
|
769
|
+
return levelOrder.indexOf(fixtureLevel) <= levelOrder.indexOf(level);
|
|
770
|
+
}
|
|
771
|
+
function zodCodeToKeyword(code) {
|
|
772
|
+
const mapping = {
|
|
773
|
+
invalid_type: "type",
|
|
774
|
+
invalid_literal: "const",
|
|
775
|
+
unrecognized_keys: "additionalProperties",
|
|
776
|
+
invalid_union: "anyOf",
|
|
777
|
+
invalid_enum_value: "enum",
|
|
778
|
+
invalid_string: "format",
|
|
779
|
+
too_small: "minLength",
|
|
780
|
+
too_big: "maxLength",
|
|
781
|
+
custom: "custom"
|
|
782
|
+
};
|
|
783
|
+
return mapping[code] ?? code;
|
|
784
|
+
}
|
|
785
|
+
function validateReceiptPayload(payload) {
|
|
786
|
+
const result = schema.ReceiptClaimsSchema.safeParse(payload);
|
|
787
|
+
if (result.success) {
|
|
788
|
+
return { valid: true };
|
|
789
|
+
}
|
|
790
|
+
const firstIssue = result.error.issues[0];
|
|
791
|
+
const zodPath = firstIssue?.path ?? [];
|
|
792
|
+
const errorPath = zodPathToJsonPointer(zodPath);
|
|
793
|
+
const errorKeyword = zodCodeToKeyword(firstIssue?.code ?? "unknown");
|
|
794
|
+
let errorCode = "E_INVALID_FORMAT";
|
|
795
|
+
const pathStr = zodPath.join(".");
|
|
796
|
+
if (pathStr === "iss" || pathStr.startsWith("iss.")) errorCode = "E_INVALID_ISSUER";
|
|
797
|
+
else if (pathStr === "aud" || pathStr.startsWith("aud.")) errorCode = "E_INVALID_AUDIENCE";
|
|
798
|
+
else if (pathStr === "iat") errorCode = "E_INVALID_IAT";
|
|
799
|
+
else if (pathStr === "exp") errorCode = "E_INVALID_EXP";
|
|
800
|
+
else if (pathStr === "rid") errorCode = "E_INVALID_RID";
|
|
801
|
+
else if (firstIssue?.code === "invalid_type" && zodPath.length > 0) {
|
|
802
|
+
errorCode = "E_REQUIRED";
|
|
803
|
+
}
|
|
804
|
+
return {
|
|
805
|
+
valid: false,
|
|
806
|
+
error_code: errorCode,
|
|
807
|
+
error_message: firstIssue?.message ?? "Unknown validation error",
|
|
808
|
+
error_path: errorPath,
|
|
809
|
+
error_keyword: errorKeyword
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function validateParseReceiptClaims(payload) {
|
|
813
|
+
const pr = schema.parseReceiptClaims(payload);
|
|
814
|
+
if (pr.ok) {
|
|
815
|
+
return { valid: true };
|
|
816
|
+
}
|
|
817
|
+
return {
|
|
818
|
+
valid: false,
|
|
819
|
+
error_code: pr.error.code,
|
|
820
|
+
error_message: pr.error.message
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function validateDiscoveryInput(input) {
|
|
824
|
+
if (typeof input === "string") {
|
|
825
|
+
const result = disc.parse(input);
|
|
826
|
+
if (result.valid) {
|
|
827
|
+
return { valid: true };
|
|
828
|
+
}
|
|
829
|
+
return {
|
|
830
|
+
valid: false,
|
|
831
|
+
error_code: "E_DISCOVERY_PARSE_ERROR",
|
|
832
|
+
error_message: result.errors?.join("; ") ?? "Discovery parse error"
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
if (typeof input === "object" && input !== null) {
|
|
836
|
+
const obj = input;
|
|
837
|
+
if (!obj.issuer || typeof obj.issuer !== "string") {
|
|
838
|
+
return {
|
|
839
|
+
valid: false,
|
|
840
|
+
error_code: "E_MISSING_ISSUER",
|
|
841
|
+
error_message: "Discovery document missing required issuer field"
|
|
842
|
+
};
|
|
843
|
+
}
|
|
844
|
+
if (!obj.jwks_uri || typeof obj.jwks_uri !== "string") {
|
|
845
|
+
return {
|
|
846
|
+
valid: false,
|
|
847
|
+
error_code: "E_MISSING_JWKS_URI",
|
|
848
|
+
error_message: "Discovery document missing required jwks_uri field"
|
|
849
|
+
};
|
|
850
|
+
}
|
|
851
|
+
return { valid: true };
|
|
852
|
+
}
|
|
853
|
+
return {
|
|
854
|
+
valid: false,
|
|
855
|
+
error_code: "E_INVALID_DISCOVERY",
|
|
856
|
+
error_message: "Discovery input must be string or object"
|
|
857
|
+
};
|
|
858
|
+
}
|
|
859
|
+
function validateAgentIdentityInput(input) {
|
|
860
|
+
const result = schema.validateAgentIdentityAttestation(input);
|
|
861
|
+
if (result.ok) {
|
|
862
|
+
return { valid: true };
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
valid: false,
|
|
866
|
+
error_code: "E_INVALID_AGENT_IDENTITY",
|
|
867
|
+
error_message: result.error
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
function validateAttributionInput(input) {
|
|
871
|
+
const result = schema.validateAttributionAttestation(input);
|
|
872
|
+
if (result.ok) {
|
|
873
|
+
return { valid: true };
|
|
874
|
+
}
|
|
875
|
+
return {
|
|
876
|
+
valid: false,
|
|
877
|
+
error_code: "E_INVALID_ATTRIBUTION",
|
|
878
|
+
error_message: result.error
|
|
879
|
+
};
|
|
880
|
+
}
|
|
881
|
+
function validateDisputeInput(input) {
|
|
882
|
+
const result = schema.validateDisputeAttestation(input);
|
|
883
|
+
if (result.ok) {
|
|
884
|
+
return { valid: true };
|
|
885
|
+
}
|
|
886
|
+
return {
|
|
887
|
+
valid: false,
|
|
888
|
+
error_code: "E_INVALID_DISPUTE",
|
|
889
|
+
error_message: result.error
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
function validateInteractionInput(input) {
|
|
893
|
+
const result = schema.validateInteractionOrdered(input);
|
|
894
|
+
if (result.valid) {
|
|
895
|
+
return {
|
|
896
|
+
valid: true,
|
|
897
|
+
warnings: result.warnings.map((w) => w.message)
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
const firstError = result.errors[0];
|
|
901
|
+
return {
|
|
902
|
+
valid: false,
|
|
903
|
+
error_code: firstError?.code ?? "E_INVALID_INTERACTION",
|
|
904
|
+
error_message: firstError?.message ?? "Interaction validation failed"
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
function validateWorkflowInput(input) {
|
|
908
|
+
const result = schema.validateWorkflowContextOrdered(input);
|
|
909
|
+
if (result.valid) {
|
|
910
|
+
return { valid: true };
|
|
911
|
+
}
|
|
912
|
+
return {
|
|
913
|
+
valid: false,
|
|
914
|
+
error_code: result.error_code,
|
|
915
|
+
error_message: result.error
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
function validateObligationsInput(input) {
|
|
919
|
+
const result = schema.validateObligationsExtension(input);
|
|
920
|
+
if (result.ok) {
|
|
921
|
+
return { valid: true };
|
|
922
|
+
}
|
|
923
|
+
return {
|
|
924
|
+
valid: false,
|
|
925
|
+
error_code: "E_INVALID_OBLIGATIONS",
|
|
926
|
+
error_message: result.error
|
|
927
|
+
};
|
|
928
|
+
}
|
|
929
|
+
function validatePurposeInput(input) {
|
|
930
|
+
if (typeof input === "object" && input !== null) {
|
|
931
|
+
const obj = input;
|
|
932
|
+
if (obj.purpose_declared && Array.isArray(obj.purpose_declared)) {
|
|
933
|
+
for (const p of obj.purpose_declared) {
|
|
934
|
+
if (typeof p === "string" && !schema.isValidPurposeToken(p) && !schema.isCanonicalPurpose(p)) {
|
|
935
|
+
return {
|
|
936
|
+
valid: false,
|
|
937
|
+
error_code: "E_INVALID_PURPOSE_TOKEN",
|
|
938
|
+
error_message: `Invalid purpose token: ${p}`
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
if (obj.purpose_enforced && typeof obj.purpose_enforced === "string") {
|
|
944
|
+
if (!schema.isValidPurposeToken(obj.purpose_enforced) && !schema.isCanonicalPurpose(obj.purpose_enforced)) {
|
|
945
|
+
return {
|
|
946
|
+
valid: false,
|
|
947
|
+
error_code: "E_INVALID_PURPOSE_ENFORCED",
|
|
948
|
+
error_message: `Invalid enforced purpose: ${obj.purpose_enforced}`
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
return { valid: true };
|
|
953
|
+
}
|
|
954
|
+
return {
|
|
955
|
+
valid: false,
|
|
956
|
+
error_code: "E_INVALID_PURPOSE",
|
|
957
|
+
error_message: "Purpose input must be object with purpose_declared/purpose_enforced"
|
|
958
|
+
};
|
|
959
|
+
}
|
|
960
|
+
function validateBundleInput(input) {
|
|
961
|
+
if (typeof input !== "object" || input === null) {
|
|
962
|
+
return {
|
|
963
|
+
valid: false,
|
|
964
|
+
error_code: "E_INVALID_BUNDLE",
|
|
965
|
+
error_message: "Bundle must be an object"
|
|
966
|
+
};
|
|
967
|
+
}
|
|
968
|
+
const obj = input;
|
|
969
|
+
if (!obj.version || typeof obj.version !== "string") {
|
|
970
|
+
return {
|
|
971
|
+
valid: false,
|
|
972
|
+
error_code: "E_MISSING_VERSION",
|
|
973
|
+
error_message: "Bundle missing required version field"
|
|
974
|
+
};
|
|
975
|
+
}
|
|
976
|
+
if (!Array.isArray(obj.entries) && !Array.isArray(obj.receipts)) {
|
|
977
|
+
return {
|
|
978
|
+
valid: false,
|
|
979
|
+
error_code: "E_MISSING_ENTRIES",
|
|
980
|
+
error_message: "Bundle missing entries or receipts array"
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
return { valid: true };
|
|
984
|
+
}
|
|
985
|
+
function validateX402Input(input) {
|
|
986
|
+
if (typeof input !== "object" || input === null) {
|
|
987
|
+
return {
|
|
988
|
+
valid: false,
|
|
989
|
+
error_code: "E_INVALID_X402",
|
|
990
|
+
error_message: "x402 input must be an object"
|
|
991
|
+
};
|
|
992
|
+
}
|
|
993
|
+
const obj = input;
|
|
994
|
+
if (obj.accepts && Array.isArray(obj.accepts)) {
|
|
995
|
+
for (const accept of obj.accepts) {
|
|
996
|
+
if (typeof accept !== "object" || accept === null) {
|
|
997
|
+
return {
|
|
998
|
+
valid: false,
|
|
999
|
+
error_code: "E_INVALID_ACCEPTS",
|
|
1000
|
+
error_message: "x402 accepts must contain payment option objects"
|
|
1001
|
+
};
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
return { valid: true };
|
|
1005
|
+
}
|
|
1006
|
+
if (obj.rail && obj.reference) {
|
|
1007
|
+
return { valid: true };
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
valid: false,
|
|
1011
|
+
error_code: "E_INVALID_X402_STRUCTURE",
|
|
1012
|
+
error_message: "x402 input missing required fields (accepts or rail+reference)"
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
function validatePolicyInput(input) {
|
|
1016
|
+
if (typeof input !== "object" || input === null) {
|
|
1017
|
+
return {
|
|
1018
|
+
valid: false,
|
|
1019
|
+
error_code: "E_INVALID_POLICY",
|
|
1020
|
+
error_message: "Policy input must be an object"
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
return { valid: true };
|
|
1024
|
+
}
|
|
1025
|
+
function validateIssueInput(input) {
|
|
1026
|
+
if (typeof input === "object" && input !== null) {
|
|
1027
|
+
const obj = input;
|
|
1028
|
+
if (obj.claims) {
|
|
1029
|
+
return validateReceiptPayload(obj.claims);
|
|
1030
|
+
}
|
|
1031
|
+
if (obj.payload) {
|
|
1032
|
+
return validateReceiptPayload(obj.payload);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
return { valid: true };
|
|
1036
|
+
}
|
|
1037
|
+
var CATEGORY_VALIDATORS = {
|
|
1038
|
+
// Receipt claim validation
|
|
1039
|
+
valid: (input) => {
|
|
1040
|
+
const obj = input;
|
|
1041
|
+
return validateReceiptPayload(obj.payload ?? obj.claims ?? input);
|
|
1042
|
+
},
|
|
1043
|
+
invalid: (input) => {
|
|
1044
|
+
const obj = input;
|
|
1045
|
+
return validateReceiptPayload(obj.payload ?? obj.claims ?? input);
|
|
1046
|
+
},
|
|
1047
|
+
edge: (input) => {
|
|
1048
|
+
const obj = input;
|
|
1049
|
+
return validateReceiptPayload(obj.payload ?? obj.claims ?? input);
|
|
1050
|
+
},
|
|
1051
|
+
// Parse-level validation (unified parser, parse-level error codes).
|
|
1052
|
+
// The claims/payload ambiguity guard lives in the runner (test-harness concern).
|
|
1053
|
+
parse: (input) => {
|
|
1054
|
+
const obj = input;
|
|
1055
|
+
return validateParseReceiptClaims(obj.claims ?? obj.payload ?? input);
|
|
1056
|
+
},
|
|
1057
|
+
// Category-specific validators
|
|
1058
|
+
"agent-identity": validateAgentIdentityInput,
|
|
1059
|
+
attribution: validateAttributionInput,
|
|
1060
|
+
bundle: validateBundleInput,
|
|
1061
|
+
discovery: validateDiscoveryInput,
|
|
1062
|
+
dispute: validateDisputeInput,
|
|
1063
|
+
interaction: validateInteractionInput,
|
|
1064
|
+
issue: validateIssueInput,
|
|
1065
|
+
obligations: validateObligationsInput,
|
|
1066
|
+
policy: validatePolicyInput,
|
|
1067
|
+
purpose: validatePurposeInput,
|
|
1068
|
+
verifier: (input) => {
|
|
1069
|
+
const obj = input;
|
|
1070
|
+
return validateReceiptPayload(obj.payload ?? obj.claims ?? input);
|
|
1071
|
+
},
|
|
1072
|
+
workflow: validateWorkflowInput,
|
|
1073
|
+
x402: validateX402Input
|
|
1074
|
+
};
|
|
1075
|
+
function getValidator(category) {
|
|
1076
|
+
return CATEGORY_VALIDATORS[category] ?? ((input) => {
|
|
1077
|
+
if (typeof input === "object" && input !== null) {
|
|
1078
|
+
const obj = input;
|
|
1079
|
+
if (obj.payload || obj.claims) {
|
|
1080
|
+
return validateReceiptPayload(obj.payload ?? obj.claims);
|
|
1081
|
+
}
|
|
1082
|
+
if ("iss" in obj || "aud" in obj || "iat" in obj || "rid" in obj) {
|
|
1083
|
+
return validateReceiptPayload(input);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
return { valid: true };
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
// src/lib/conformance-runner.ts
|
|
1091
|
+
function getGitSha() {
|
|
1092
|
+
try {
|
|
1093
|
+
return child_process.execSync("git rev-parse --short HEAD", { encoding: "utf-8" }).trim();
|
|
1094
|
+
} catch {
|
|
1095
|
+
return void 0;
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
function runConformance(options, callbacks) {
|
|
1099
|
+
const startTime = Date.now();
|
|
1100
|
+
const { fixturesDir, level, category, implementationName, implementationVersion } = options;
|
|
1101
|
+
const manifest = loadManifest(fixturesDir);
|
|
1102
|
+
const entries = fs6__namespace.readdirSync(fixturesDir, { withFileTypes: true });
|
|
1103
|
+
let categories = entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name).sort();
|
|
1104
|
+
if (category) {
|
|
1105
|
+
if (!categories.includes(category)) {
|
|
1106
|
+
throw new Error(`Category not found: ${category}`);
|
|
1107
|
+
}
|
|
1108
|
+
categories = [category];
|
|
1109
|
+
}
|
|
1110
|
+
const vectorsDigest = computeVectorsDigest(fixturesDir, categories);
|
|
1111
|
+
const profiles = /* @__PURE__ */ new Set();
|
|
1112
|
+
const results = [];
|
|
1113
|
+
let passed = 0;
|
|
1114
|
+
let failed = 0;
|
|
1115
|
+
let skipped = 0;
|
|
1116
|
+
for (const cat of categories) {
|
|
1117
|
+
const categoryPath = path6__namespace.join(fixturesDir, cat);
|
|
1118
|
+
const profile = getCategoryProfile(cat);
|
|
1119
|
+
profiles.add(profile);
|
|
1120
|
+
const validator = getValidator(cat);
|
|
1121
|
+
const files = fs6__namespace.readdirSync(categoryPath).filter((f) => f.endsWith(".json")).sort();
|
|
1122
|
+
for (const file of files) {
|
|
1123
|
+
const filePath = path6__namespace.join(categoryPath, file);
|
|
1124
|
+
const content = fs6__namespace.readFileSync(filePath, "utf8");
|
|
1125
|
+
let fixture;
|
|
1126
|
+
const baseTestId = `${cat}.${file.replace(".json", "")}`;
|
|
1127
|
+
callbacks?.onTestStart?.(baseTestId);
|
|
1128
|
+
try {
|
|
1129
|
+
fixture = JSON.parse(content);
|
|
1130
|
+
} catch (err) {
|
|
1131
|
+
failed++;
|
|
1132
|
+
const result = {
|
|
1133
|
+
id: `${baseTestId}.parse`,
|
|
1134
|
+
category: profile,
|
|
1135
|
+
status: "fail",
|
|
1136
|
+
observed: {
|
|
1137
|
+
valid: false,
|
|
1138
|
+
error_code: "E_INVALID_JSON",
|
|
1139
|
+
error_message: err.message
|
|
1140
|
+
},
|
|
1141
|
+
diagnostics: {
|
|
1142
|
+
error_code: "E_INVALID_JSON",
|
|
1143
|
+
error_message: err.message
|
|
1144
|
+
}
|
|
1145
|
+
};
|
|
1146
|
+
results.push(result);
|
|
1147
|
+
callbacks?.onTestComplete?.(result);
|
|
1148
|
+
continue;
|
|
1149
|
+
}
|
|
1150
|
+
const fixtureObj = fixture;
|
|
1151
|
+
const isFixturePack = Array.isArray(fixtureObj.fixtures);
|
|
1152
|
+
if (isFixturePack) {
|
|
1153
|
+
const pack = fixture;
|
|
1154
|
+
if (!shouldRunAtLevel(pack.version, level)) {
|
|
1155
|
+
skipped++;
|
|
1156
|
+
const result = {
|
|
1157
|
+
id: baseTestId,
|
|
1158
|
+
category: profile,
|
|
1159
|
+
status: "skip",
|
|
1160
|
+
diagnostics: {
|
|
1161
|
+
skip_reason: `Fixture version ${pack.version} exceeds level ${level}`
|
|
1162
|
+
}
|
|
1163
|
+
};
|
|
1164
|
+
results.push(result);
|
|
1165
|
+
callbacks?.onTestComplete?.(result);
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
for (let i = 0; i < pack.fixtures.length; i++) {
|
|
1169
|
+
const f = pack.fixtures[i];
|
|
1170
|
+
const fixtureName = f.name ?? `fixture_${i}`;
|
|
1171
|
+
const testId = `${baseTestId}.${fixtureName.replace(/\s+/g, "_")}`;
|
|
1172
|
+
const inputDigest = computeCanonicalDigest(f.input);
|
|
1173
|
+
callbacks?.onTestStart?.(testId);
|
|
1174
|
+
if (typeof f.input === "object" && f.input !== null) {
|
|
1175
|
+
const inputObj = f.input;
|
|
1176
|
+
if (inputObj.claims !== void 0 && inputObj.payload !== void 0) {
|
|
1177
|
+
failed++;
|
|
1178
|
+
const result2 = {
|
|
1179
|
+
id: testId,
|
|
1180
|
+
category: profile,
|
|
1181
|
+
status: "fail",
|
|
1182
|
+
diagnostics: {
|
|
1183
|
+
error_message: 'Fixture has both "claims" and "payload" -- ambiguous; use one'
|
|
1184
|
+
}
|
|
1185
|
+
};
|
|
1186
|
+
results.push(result2);
|
|
1187
|
+
callbacks?.onTestComplete?.(result2);
|
|
1188
|
+
continue;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
const observed = validator(f.input);
|
|
1192
|
+
const expectedValid = f.expected.valid;
|
|
1193
|
+
const actualValid = observed.valid;
|
|
1194
|
+
const expectedError = f.expected.error_code ?? f.expected.error;
|
|
1195
|
+
const actualError = observed.error_code;
|
|
1196
|
+
let status;
|
|
1197
|
+
if (expectedValid && actualValid) {
|
|
1198
|
+
status = "pass";
|
|
1199
|
+
passed++;
|
|
1200
|
+
} else if (!expectedValid && !actualValid) {
|
|
1201
|
+
if (expectedError && actualError && expectedError !== actualError) {
|
|
1202
|
+
status = "fail";
|
|
1203
|
+
failed++;
|
|
1204
|
+
} else {
|
|
1205
|
+
status = "pass";
|
|
1206
|
+
passed++;
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
status = "fail";
|
|
1210
|
+
failed++;
|
|
1211
|
+
}
|
|
1212
|
+
const result = {
|
|
1213
|
+
id: testId,
|
|
1214
|
+
category: profile,
|
|
1215
|
+
status,
|
|
1216
|
+
expected: {
|
|
1217
|
+
valid: expectedValid,
|
|
1218
|
+
error_code: expectedError
|
|
1219
|
+
},
|
|
1220
|
+
observed: {
|
|
1221
|
+
valid: actualValid,
|
|
1222
|
+
error_code: actualError,
|
|
1223
|
+
error_message: observed.error_message
|
|
1224
|
+
},
|
|
1225
|
+
diagnostics: {
|
|
1226
|
+
input_digest: inputDigest,
|
|
1227
|
+
warnings: observed.warnings
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
results.push(result);
|
|
1231
|
+
callbacks?.onTestComplete?.(result);
|
|
1232
|
+
}
|
|
1233
|
+
} else {
|
|
1234
|
+
const singleFixture = fixture;
|
|
1235
|
+
const testId = baseTestId;
|
|
1236
|
+
const manifestEntry = getManifestEntry(manifest, cat, file);
|
|
1237
|
+
const inputToValidate = singleFixture.payload ?? fixture;
|
|
1238
|
+
const inputDigest = singleFixture.payload ? computeCanonicalDigest(inputToValidate) : { alg: "sha-256", value: sha256(content) };
|
|
1239
|
+
const expectedValid = singleFixture.expected_valid ?? manifestEntry?.expected_valid ?? (cat === "valid" || cat === "edge");
|
|
1240
|
+
const expectedError = singleFixture.expected_error ?? manifestEntry?.expected_error_code;
|
|
1241
|
+
const expectedPath = manifestEntry?.expected_path;
|
|
1242
|
+
const expectedKeyword = manifestEntry?.expected_keyword;
|
|
1243
|
+
const observed = validator(inputToValidate);
|
|
1244
|
+
let status;
|
|
1245
|
+
if (expectedValid && observed.valid) {
|
|
1246
|
+
status = "pass";
|
|
1247
|
+
passed++;
|
|
1248
|
+
} else if (!expectedValid && !observed.valid) {
|
|
1249
|
+
let mismatch = false;
|
|
1250
|
+
if (expectedError && observed.error_code && expectedError !== observed.error_code) {
|
|
1251
|
+
mismatch = true;
|
|
1252
|
+
}
|
|
1253
|
+
if (expectedPath && observed.error_path && expectedPath !== observed.error_path) {
|
|
1254
|
+
mismatch = true;
|
|
1255
|
+
}
|
|
1256
|
+
if (expectedKeyword && observed.error_keyword && expectedKeyword !== observed.error_keyword) {
|
|
1257
|
+
mismatch = true;
|
|
1258
|
+
}
|
|
1259
|
+
if (mismatch) {
|
|
1260
|
+
status = "fail";
|
|
1261
|
+
failed++;
|
|
1262
|
+
} else {
|
|
1263
|
+
status = "pass";
|
|
1264
|
+
passed++;
|
|
1265
|
+
}
|
|
1266
|
+
} else {
|
|
1267
|
+
status = "fail";
|
|
1268
|
+
failed++;
|
|
1269
|
+
}
|
|
1270
|
+
const result = {
|
|
1271
|
+
id: testId,
|
|
1272
|
+
category: profile,
|
|
1273
|
+
status,
|
|
1274
|
+
expected: {
|
|
1275
|
+
valid: expectedValid,
|
|
1276
|
+
error_code: expectedError,
|
|
1277
|
+
error_path: expectedPath,
|
|
1278
|
+
error_keyword: expectedKeyword
|
|
1279
|
+
},
|
|
1280
|
+
observed: {
|
|
1281
|
+
valid: observed.valid,
|
|
1282
|
+
error_code: observed.error_code,
|
|
1283
|
+
error_path: observed.error_path,
|
|
1284
|
+
error_keyword: observed.error_keyword,
|
|
1285
|
+
error_message: observed.error_message
|
|
1286
|
+
},
|
|
1287
|
+
diagnostics: {
|
|
1288
|
+
input_digest: inputDigest,
|
|
1289
|
+
warnings: observed.warnings
|
|
1290
|
+
}
|
|
1291
|
+
};
|
|
1292
|
+
results.push(result);
|
|
1293
|
+
callbacks?.onTestComplete?.(result);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
const capabilities = [];
|
|
1298
|
+
for (const cat of categories) {
|
|
1299
|
+
const capability = getCategoryCapability(cat);
|
|
1300
|
+
if (!capabilities.some((c) => c.profile === capability.profile)) {
|
|
1301
|
+
capabilities.push(capability);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
capabilities.sort((a, b) => a.profile.localeCompare(b.profile));
|
|
1305
|
+
const endTime = Date.now();
|
|
1306
|
+
const report = {
|
|
1307
|
+
report_version: "peac-conformance-report/0.1",
|
|
1308
|
+
suite: {
|
|
1309
|
+
name: "peac-core-conformance",
|
|
1310
|
+
version: "0.1.0",
|
|
1311
|
+
vectors_digest: {
|
|
1312
|
+
alg: "sha-256",
|
|
1313
|
+
value: vectorsDigest
|
|
1314
|
+
},
|
|
1315
|
+
profiles: Array.from(profiles).sort(),
|
|
1316
|
+
capabilities
|
|
1317
|
+
},
|
|
1318
|
+
implementation: {
|
|
1319
|
+
name: implementationName ?? "@peac/cli",
|
|
1320
|
+
version: implementationVersion ?? "0.0.0"
|
|
1321
|
+
},
|
|
1322
|
+
summary: {
|
|
1323
|
+
passed,
|
|
1324
|
+
failed,
|
|
1325
|
+
skipped,
|
|
1326
|
+
total: passed + failed + skipped,
|
|
1327
|
+
status: failed === 0 ? "pass" : "fail"
|
|
1328
|
+
},
|
|
1329
|
+
results,
|
|
1330
|
+
meta: {
|
|
1331
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1332
|
+
duration_ms: endTime - startTime,
|
|
1333
|
+
runner: {
|
|
1334
|
+
name: "@peac/cli",
|
|
1335
|
+
version: implementationVersion ?? "0.0.0",
|
|
1336
|
+
git_sha: getGitSha()
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
};
|
|
1340
|
+
return report;
|
|
1341
|
+
}
|
|
1342
|
+
function formatReportText(report) {
|
|
1343
|
+
const lines = [];
|
|
1344
|
+
lines.push("PEAC Conformance Test Results");
|
|
1345
|
+
lines.push("=============================");
|
|
1346
|
+
lines.push("");
|
|
1347
|
+
lines.push(`Status: ${report.summary.status.toUpperCase()}`);
|
|
1348
|
+
lines.push(`Profiles: ${report.suite.profiles.join(", ")}`);
|
|
1349
|
+
lines.push("");
|
|
1350
|
+
lines.push("Summary:");
|
|
1351
|
+
lines.push(` Passed: ${report.summary.passed}`);
|
|
1352
|
+
lines.push(` Failed: ${report.summary.failed}`);
|
|
1353
|
+
lines.push(` Skipped: ${report.summary.skipped}`);
|
|
1354
|
+
lines.push(` Total: ${report.summary.total}`);
|
|
1355
|
+
const failures = report.results.filter((r) => r.status === "fail");
|
|
1356
|
+
if (failures.length > 0) {
|
|
1357
|
+
lines.push("");
|
|
1358
|
+
lines.push("Failures:");
|
|
1359
|
+
for (const f of failures) {
|
|
1360
|
+
lines.push(` - ${f.id}`);
|
|
1361
|
+
if (f.expected) {
|
|
1362
|
+
lines.push(
|
|
1363
|
+
` Expected: valid=${f.expected.valid}${f.expected.error_code ? `, error=${f.expected.error_code}` : ""}`
|
|
1364
|
+
);
|
|
1365
|
+
}
|
|
1366
|
+
if (f.observed) {
|
|
1367
|
+
lines.push(
|
|
1368
|
+
` Observed: valid=${f.observed.valid}${f.observed.error_code ? `, error=${f.observed.error_code}` : ""}`
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
return lines.join("\n");
|
|
1374
|
+
}
|
|
1375
|
+
function formatReportMarkdown(report) {
|
|
1376
|
+
const lines = [];
|
|
1377
|
+
lines.push("# PEAC Conformance Report");
|
|
1378
|
+
lines.push("");
|
|
1379
|
+
lines.push(`**Status:** ${report.summary.status.toUpperCase()}`);
|
|
1380
|
+
lines.push(`**Profiles:** ${report.suite.profiles.join(", ")}`);
|
|
1381
|
+
lines.push("");
|
|
1382
|
+
lines.push("## Summary");
|
|
1383
|
+
lines.push("");
|
|
1384
|
+
lines.push("| Metric | Count |");
|
|
1385
|
+
lines.push("|--------|-------|");
|
|
1386
|
+
lines.push(`| Passed | ${report.summary.passed} |`);
|
|
1387
|
+
lines.push(`| Failed | ${report.summary.failed} |`);
|
|
1388
|
+
lines.push(`| Skipped | ${report.summary.skipped} |`);
|
|
1389
|
+
lines.push(`| Total | ${report.summary.total} |`);
|
|
1390
|
+
const failures = report.results.filter((r) => r.status === "fail");
|
|
1391
|
+
if (failures.length > 0) {
|
|
1392
|
+
lines.push("");
|
|
1393
|
+
lines.push("## Failures");
|
|
1394
|
+
lines.push("");
|
|
1395
|
+
for (const f of failures) {
|
|
1396
|
+
lines.push(`### ${f.id}`);
|
|
1397
|
+
lines.push("");
|
|
1398
|
+
if (f.expected) {
|
|
1399
|
+
lines.push(
|
|
1400
|
+
`- **Expected:** valid=${f.expected.valid}${f.expected.error_code ? `, error=${f.expected.error_code}` : ""}`
|
|
1401
|
+
);
|
|
1402
|
+
}
|
|
1403
|
+
if (f.observed) {
|
|
1404
|
+
lines.push(
|
|
1405
|
+
`- **Observed:** valid=${f.observed.valid}${f.observed.error_code ? `, error=${f.observed.error_code}` : ""}`
|
|
1406
|
+
);
|
|
1407
|
+
}
|
|
1408
|
+
lines.push("");
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
return lines.join("\n");
|
|
1412
|
+
}
|
|
1413
|
+
function getVersion() {
|
|
1414
|
+
try {
|
|
1415
|
+
const pkgPath = path6.join(__dirname, "..", "..", "package.json");
|
|
1416
|
+
if (fs6.existsSync(pkgPath)) {
|
|
1417
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
1418
|
+
if (pkg.version) return pkg.version;
|
|
1419
|
+
}
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
try {
|
|
1423
|
+
const pkgPath = path6.join(process.cwd(), "packages", "cli", "package.json");
|
|
1424
|
+
if (fs6.existsSync(pkgPath)) {
|
|
1425
|
+
const pkg = JSON.parse(fs6.readFileSync(pkgPath, "utf8"));
|
|
1426
|
+
if (pkg.version) return pkg.version;
|
|
1427
|
+
}
|
|
1428
|
+
} catch {
|
|
1429
|
+
}
|
|
1430
|
+
return "0.0.0-unknown";
|
|
1431
|
+
}
|
|
1432
|
+
function getRuntime() {
|
|
1433
|
+
return `node-${process.version.slice(1)}`;
|
|
1434
|
+
}
|
|
1435
|
+
function getCommit() {
|
|
1436
|
+
return process.env.GIT_COMMIT ?? process.env.GITHUB_SHA;
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// src/commands/conformance.ts
|
|
1440
|
+
function getGlobalOptions2(cmd) {
|
|
1441
|
+
const parent = cmd.parent;
|
|
1442
|
+
if (!parent) return {};
|
|
1443
|
+
return parent.opts();
|
|
1444
|
+
}
|
|
1445
|
+
function outputError2(error, details, opts) {
|
|
1446
|
+
if (opts.json) {
|
|
1447
|
+
console.log(JSON.stringify({ success: false, error, ...details }, null, 2));
|
|
1448
|
+
} else {
|
|
1449
|
+
console.error(`Error: ${error}`);
|
|
1450
|
+
if (details.code) {
|
|
1451
|
+
console.error(`Code: ${details.code}`);
|
|
1452
|
+
}
|
|
1453
|
+
if (details.hint) {
|
|
1454
|
+
console.error(`Hint: ${details.hint}`);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
function findFixturesDir(customPath) {
|
|
1459
|
+
if (customPath) {
|
|
1460
|
+
if (fs6__namespace.existsSync(customPath)) {
|
|
1461
|
+
return customPath;
|
|
1462
|
+
}
|
|
1463
|
+
return null;
|
|
1464
|
+
}
|
|
1465
|
+
const cliPath = path6__namespace.resolve(__dirname, "../../../../specs/conformance/fixtures");
|
|
1466
|
+
if (fs6__namespace.existsSync(cliPath)) {
|
|
1467
|
+
return cliPath;
|
|
1468
|
+
}
|
|
1469
|
+
const repoPath = path6__namespace.resolve(process.cwd(), "specs/conformance/fixtures");
|
|
1470
|
+
if (fs6__namespace.existsSync(repoPath)) {
|
|
1471
|
+
return repoPath;
|
|
1472
|
+
}
|
|
1473
|
+
const upPath = path6__namespace.resolve(process.cwd(), "../../specs/conformance/fixtures");
|
|
1474
|
+
if (fs6__namespace.existsSync(upPath)) {
|
|
1475
|
+
return upPath;
|
|
1476
|
+
}
|
|
1477
|
+
return null;
|
|
1478
|
+
}
|
|
1479
|
+
function listCategories(fixturesDir) {
|
|
1480
|
+
const entries = fs6__namespace.readdirSync(fixturesDir, { withFileTypes: true });
|
|
1481
|
+
return entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name);
|
|
1482
|
+
}
|
|
1483
|
+
function countFixtures(categoryPath) {
|
|
1484
|
+
if (!fs6__namespace.existsSync(categoryPath)) return 0;
|
|
1485
|
+
const files = fs6__namespace.readdirSync(categoryPath);
|
|
1486
|
+
return files.filter((f) => f.endsWith(".json")).length;
|
|
1487
|
+
}
|
|
1488
|
+
var conformance = new commander.Command("conformance").description("PEAC conformance testing (v0.10.8+)").option("--json", "Output in JSON format");
|
|
1489
|
+
conformance.command("run").description("Run conformance tests").option("-l, --level <level>", "Conformance level: basic, standard, full", "standard").option("-c, --category <category>", "Test specific category only").option("-o, --output <format>", "Output format: json, text, markdown", "text").option("-v, --verbose", "Show detailed test output").option("--fixtures <path>", "Path to fixtures directory").option("--implementation <name>", "Implementation name for report", "@peac/cli").action(async (options, cmd) => {
|
|
1490
|
+
const globalOpts = getGlobalOptions2(cmd);
|
|
1491
|
+
const startTime = Date.now();
|
|
1492
|
+
try {
|
|
1493
|
+
const fixturesDir = findFixturesDir(options.fixtures);
|
|
1494
|
+
if (!fixturesDir) {
|
|
1495
|
+
outputError2(
|
|
1496
|
+
"Fixtures directory not found",
|
|
1497
|
+
{
|
|
1498
|
+
hint: options.fixtures ? `Specified path does not exist: ${options.fixtures}` : "Run from repo root, specify --fixtures <path>, or ensure specs/conformance/fixtures is available"
|
|
1499
|
+
},
|
|
1500
|
+
globalOpts
|
|
1501
|
+
);
|
|
1502
|
+
process.exitCode = 1;
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const validLevels = ["basic", "standard", "full"];
|
|
1506
|
+
if (!validLevels.includes(options.level)) {
|
|
1507
|
+
outputError2(
|
|
1508
|
+
"Invalid conformance level",
|
|
1509
|
+
{
|
|
1510
|
+
level: options.level,
|
|
1511
|
+
valid: validLevels
|
|
1512
|
+
},
|
|
1513
|
+
globalOpts
|
|
1514
|
+
);
|
|
1515
|
+
process.exitCode = 1;
|
|
1516
|
+
return;
|
|
1517
|
+
}
|
|
1518
|
+
const callbacks = options.verbose ? {
|
|
1519
|
+
onTestStart: (testId) => {
|
|
1520
|
+
console.log(`Running: ${testId}`);
|
|
1521
|
+
},
|
|
1522
|
+
onTestComplete: (result) => {
|
|
1523
|
+
const status = result.status === "pass" ? "PASS" : result.status === "fail" ? "FAIL" : "SKIP";
|
|
1524
|
+
console.log(` ${status}: ${result.id}`);
|
|
1525
|
+
}
|
|
1526
|
+
} : void 0;
|
|
1527
|
+
const report = runConformance(
|
|
1528
|
+
{
|
|
1529
|
+
fixturesDir,
|
|
1530
|
+
level: options.level,
|
|
1531
|
+
category: options.category,
|
|
1532
|
+
implementationName: options.implementation,
|
|
1533
|
+
implementationVersion: getVersion()
|
|
1534
|
+
},
|
|
1535
|
+
callbacks
|
|
1536
|
+
);
|
|
1537
|
+
report.implementation.runtime = getRuntime();
|
|
1538
|
+
const commit = getCommit();
|
|
1539
|
+
if (commit) {
|
|
1540
|
+
report.implementation.commit = commit;
|
|
1541
|
+
}
|
|
1542
|
+
report.meta = {
|
|
1543
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1544
|
+
runner: {
|
|
1545
|
+
name: "peac-cli",
|
|
1546
|
+
version: getVersion()
|
|
1547
|
+
},
|
|
1548
|
+
duration_ms: Date.now() - startTime
|
|
1549
|
+
};
|
|
1550
|
+
if (options.output === "json" || globalOpts.json) {
|
|
1551
|
+
console.log(JSON.stringify(report, null, 2));
|
|
1552
|
+
} else if (options.output === "markdown") {
|
|
1553
|
+
console.log(formatReportMarkdown(report));
|
|
1554
|
+
} else {
|
|
1555
|
+
console.log(formatReportText(report));
|
|
1556
|
+
}
|
|
1557
|
+
process.exitCode = report.summary.failed === 0 ? 0 : 1;
|
|
1558
|
+
} catch (err) {
|
|
1559
|
+
outputError2(err.message, {}, globalOpts);
|
|
1560
|
+
process.exitCode = 1;
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
conformance.command("list").description("List available conformance test fixtures").option("-c, --category <category>", "List fixtures in specific category").option("--fixtures <path>", "Path to fixtures directory").action((options, cmd) => {
|
|
1564
|
+
const globalOpts = getGlobalOptions2(cmd);
|
|
1565
|
+
try {
|
|
1566
|
+
const fixturesDir = findFixturesDir(options.fixtures);
|
|
1567
|
+
if (!fixturesDir) {
|
|
1568
|
+
outputError2(
|
|
1569
|
+
"Fixtures directory not found",
|
|
1570
|
+
{
|
|
1571
|
+
hint: options.fixtures ? `Specified path does not exist: ${options.fixtures}` : "Run from repo root, specify --fixtures <path>, or ensure specs/conformance/fixtures is available"
|
|
1572
|
+
},
|
|
1573
|
+
globalOpts
|
|
1574
|
+
);
|
|
1575
|
+
process.exitCode = 1;
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
const manifestPath = path6__namespace.join(fixturesDir, "manifest.json");
|
|
1579
|
+
const manifest = fs6__namespace.existsSync(manifestPath) ? JSON.parse(fs6__namespace.readFileSync(manifestPath, "utf8")) : {};
|
|
1580
|
+
const categories = listCategories(fixturesDir);
|
|
1581
|
+
if (options.category) {
|
|
1582
|
+
if (!categories.includes(options.category)) {
|
|
1583
|
+
outputError2(
|
|
1584
|
+
"Category not found",
|
|
1585
|
+
{
|
|
1586
|
+
category: options.category,
|
|
1587
|
+
available: categories
|
|
1588
|
+
},
|
|
1589
|
+
globalOpts
|
|
1590
|
+
);
|
|
1591
|
+
process.exitCode = 1;
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
const categoryPath = path6__namespace.join(fixturesDir, options.category);
|
|
1595
|
+
const files = fs6__namespace.readdirSync(categoryPath).filter((f) => f.endsWith(".json"));
|
|
1596
|
+
const categoryManifest = manifest[options.category];
|
|
1597
|
+
if (globalOpts.json) {
|
|
1598
|
+
const fixtures = files.map((f) => ({
|
|
1599
|
+
file: f,
|
|
1600
|
+
description: categoryManifest?.[f]?.description || "No description"
|
|
1601
|
+
}));
|
|
1602
|
+
console.log(JSON.stringify({ category: options.category, fixtures }, null, 2));
|
|
1603
|
+
} else {
|
|
1604
|
+
console.log(`Fixtures in ${options.category}:
|
|
1605
|
+
`);
|
|
1606
|
+
for (const file of files) {
|
|
1607
|
+
const info = categoryManifest?.[file] || {};
|
|
1608
|
+
const desc = info.description || "No description";
|
|
1609
|
+
console.log(` ${file}`);
|
|
1610
|
+
console.log(` ${desc}
|
|
1611
|
+
`);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
} else {
|
|
1615
|
+
if (globalOpts.json) {
|
|
1616
|
+
const categorySummary = categories.map((cat) => ({
|
|
1617
|
+
name: cat,
|
|
1618
|
+
fixture_count: countFixtures(path6__namespace.join(fixturesDir, cat))
|
|
1619
|
+
}));
|
|
1620
|
+
console.log(JSON.stringify({ categories: categorySummary }, null, 2));
|
|
1621
|
+
} else {
|
|
1622
|
+
console.log(`Available conformance test categories:
|
|
1623
|
+
`);
|
|
1624
|
+
for (const cat of categories) {
|
|
1625
|
+
const count = countFixtures(path6__namespace.join(fixturesDir, cat));
|
|
1626
|
+
console.log(` ${cat} (${count} fixtures)`);
|
|
1627
|
+
}
|
|
1628
|
+
console.log(`
|
|
1629
|
+
Use 'peac conformance list -c <category>' to see fixtures in a category.`);
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
process.exitCode = 0;
|
|
1633
|
+
} catch (err) {
|
|
1634
|
+
outputError2(err.message, {}, globalOpts);
|
|
1635
|
+
process.exitCode = 1;
|
|
1636
|
+
}
|
|
1637
|
+
});
|
|
1638
|
+
var EMBEDDED_SAMPLES = [
|
|
1639
|
+
{
|
|
1640
|
+
id: "basic-receipt",
|
|
1641
|
+
name: "Basic Receipt",
|
|
1642
|
+
description: "Minimal valid PEAC receipt with only required fields",
|
|
1643
|
+
category: "valid",
|
|
1644
|
+
claims: {
|
|
1645
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1646
|
+
aud: "https://example.com",
|
|
1647
|
+
iat: 0,
|
|
1648
|
+
// Placeholder - will be set at generation time
|
|
1649
|
+
exp: 0,
|
|
1650
|
+
// Placeholder - will be set at generation time
|
|
1651
|
+
rid: "sample-basic-001"
|
|
1652
|
+
}
|
|
1653
|
+
},
|
|
1654
|
+
{
|
|
1655
|
+
id: "full-receipt",
|
|
1656
|
+
name: "Full Receipt",
|
|
1657
|
+
description: "PEAC receipt with all optional claims populated",
|
|
1658
|
+
category: "valid",
|
|
1659
|
+
claims: {
|
|
1660
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1661
|
+
aud: "https://api.example.com",
|
|
1662
|
+
sub: "user:demo-user",
|
|
1663
|
+
iat: 0,
|
|
1664
|
+
exp: 0,
|
|
1665
|
+
rid: "sample-full-001",
|
|
1666
|
+
purpose_declared: ["search", "index"],
|
|
1667
|
+
purpose_enforced: "search",
|
|
1668
|
+
purpose_reason: "allowed"
|
|
1669
|
+
}
|
|
1670
|
+
},
|
|
1671
|
+
{
|
|
1672
|
+
id: "interaction-evidence",
|
|
1673
|
+
name: "Interaction Evidence",
|
|
1674
|
+
description: "Receipt with InteractionEvidence extension for AI agent calls",
|
|
1675
|
+
category: "valid",
|
|
1676
|
+
claims: {
|
|
1677
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1678
|
+
aud: "https://agent.example.com",
|
|
1679
|
+
sub: "agent:demo-agent-v1",
|
|
1680
|
+
iat: 0,
|
|
1681
|
+
exp: 0,
|
|
1682
|
+
rid: "sample-ie-001",
|
|
1683
|
+
ext: {
|
|
1684
|
+
"org.peacprotocol/interaction@0.1": {
|
|
1685
|
+
version: "0.1",
|
|
1686
|
+
interaction_id: "int_sample_001",
|
|
1687
|
+
started_at: "2026-01-01T00:00:00.000Z",
|
|
1688
|
+
completed_at: "2026-01-01T00:00:01.000Z",
|
|
1689
|
+
outcome: { kind: "success" },
|
|
1690
|
+
input: { hash: "sha256:abc123...", byte_length: 1024 },
|
|
1691
|
+
output: { hash: "sha256:def456...", byte_length: 2048 }
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
},
|
|
1696
|
+
{
|
|
1697
|
+
id: "payment-evidence",
|
|
1698
|
+
name: "Payment Evidence",
|
|
1699
|
+
description: "Receipt with payment evidence (402 flow)",
|
|
1700
|
+
category: "valid",
|
|
1701
|
+
claims: {
|
|
1702
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1703
|
+
aud: "https://api.example.com",
|
|
1704
|
+
iat: 0,
|
|
1705
|
+
exp: 0,
|
|
1706
|
+
rid: "sample-payment-001",
|
|
1707
|
+
amt: "100",
|
|
1708
|
+
cur: "USD",
|
|
1709
|
+
payment: {
|
|
1710
|
+
rail: "x402",
|
|
1711
|
+
reference: "pay_sample_001",
|
|
1712
|
+
amount: "100",
|
|
1713
|
+
currency: "USD"
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
},
|
|
1717
|
+
{
|
|
1718
|
+
id: "long-expiry",
|
|
1719
|
+
name: "Long Expiry",
|
|
1720
|
+
description: "Receipt with 24-hour expiration",
|
|
1721
|
+
category: "valid",
|
|
1722
|
+
claims: {
|
|
1723
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1724
|
+
aud: "https://example.com",
|
|
1725
|
+
iat: 0,
|
|
1726
|
+
exp: 0,
|
|
1727
|
+
// Will be set to iat + 86400 at generation time
|
|
1728
|
+
rid: "sample-long-expiry-001"
|
|
1729
|
+
}
|
|
1730
|
+
},
|
|
1731
|
+
{
|
|
1732
|
+
id: "expired",
|
|
1733
|
+
name: "Expired Receipt",
|
|
1734
|
+
description: "Receipt that has already expired (for testing rejection)",
|
|
1735
|
+
category: "invalid",
|
|
1736
|
+
claims: {
|
|
1737
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1738
|
+
aud: "https://example.com",
|
|
1739
|
+
iat: 0,
|
|
1740
|
+
// Will be set to now - 7200 at generation time
|
|
1741
|
+
exp: 0,
|
|
1742
|
+
// Will be set to now - 3600 at generation time
|
|
1743
|
+
rid: "sample-expired-001"
|
|
1744
|
+
},
|
|
1745
|
+
expectedError: "E_EXPIRED_RECEIPT"
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
id: "future-iat",
|
|
1749
|
+
name: "Future IAT",
|
|
1750
|
+
description: "Receipt with iat in the future (should be rejected)",
|
|
1751
|
+
category: "invalid",
|
|
1752
|
+
claims: {
|
|
1753
|
+
iss: "https://sandbox.peacprotocol.org",
|
|
1754
|
+
aud: "https://example.com",
|
|
1755
|
+
iat: 0,
|
|
1756
|
+
// Will be set to now + 3600 at generation time
|
|
1757
|
+
exp: 0,
|
|
1758
|
+
// Will be set to now + 7200 at generation time
|
|
1759
|
+
rid: "sample-future-iat-001"
|
|
1760
|
+
},
|
|
1761
|
+
expectedError: "E_FUTURE_IAT"
|
|
1762
|
+
},
|
|
1763
|
+
{
|
|
1764
|
+
id: "missing-iss",
|
|
1765
|
+
name: "Missing Issuer",
|
|
1766
|
+
description: "Receipt missing required iss claim (for testing validation)",
|
|
1767
|
+
category: "invalid",
|
|
1768
|
+
claims: {
|
|
1769
|
+
aud: "https://example.com",
|
|
1770
|
+
iat: 0,
|
|
1771
|
+
exp: 0,
|
|
1772
|
+
rid: "sample-no-iss-001"
|
|
1773
|
+
},
|
|
1774
|
+
expectedError: "E_MISSING_CLAIM"
|
|
1775
|
+
}
|
|
1776
|
+
];
|
|
1777
|
+
function findSamplesDir(customPath) {
|
|
1778
|
+
if (customPath) {
|
|
1779
|
+
if (fs6__namespace.existsSync(customPath)) {
|
|
1780
|
+
return customPath;
|
|
1781
|
+
}
|
|
1782
|
+
return null;
|
|
1783
|
+
}
|
|
1784
|
+
const cliPath = path6__namespace.resolve(__dirname, "../../../../specs/conformance/samples");
|
|
1785
|
+
if (fs6__namespace.existsSync(cliPath)) {
|
|
1786
|
+
return cliPath;
|
|
1787
|
+
}
|
|
1788
|
+
const repoPath = path6__namespace.resolve(process.cwd(), "specs/conformance/samples");
|
|
1789
|
+
if (fs6__namespace.existsSync(repoPath)) {
|
|
1790
|
+
return repoPath;
|
|
1791
|
+
}
|
|
1792
|
+
const upPath = path6__namespace.resolve(process.cwd(), "../../specs/conformance/samples");
|
|
1793
|
+
if (fs6__namespace.existsSync(upPath)) {
|
|
1794
|
+
return upPath;
|
|
1795
|
+
}
|
|
1796
|
+
return null;
|
|
1797
|
+
}
|
|
1798
|
+
function loadSampleFromFile(filePath, id, category) {
|
|
1799
|
+
try {
|
|
1800
|
+
const content = JSON.parse(fs6__namespace.readFileSync(filePath, "utf8"));
|
|
1801
|
+
const claims = content.payload ?? content.claims ?? content;
|
|
1802
|
+
const description = content.$comment ?? content.description ?? `Sample ${id}`;
|
|
1803
|
+
return {
|
|
1804
|
+
id,
|
|
1805
|
+
name: id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
|
|
1806
|
+
description,
|
|
1807
|
+
category,
|
|
1808
|
+
claims,
|
|
1809
|
+
header: content.header,
|
|
1810
|
+
expectedError: content.expected_error
|
|
1811
|
+
};
|
|
1812
|
+
} catch {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
function loadSamplesFromDir(samplesDir) {
|
|
1817
|
+
const samples2 = [];
|
|
1818
|
+
const validDir = path6__namespace.join(samplesDir, "valid");
|
|
1819
|
+
if (fs6__namespace.existsSync(validDir)) {
|
|
1820
|
+
const files = fs6__namespace.readdirSync(validDir).filter((f) => f.endsWith(".json"));
|
|
1821
|
+
for (const file of files) {
|
|
1822
|
+
const id = file.replace(".json", "");
|
|
1823
|
+
const sample = loadSampleFromFile(path6__namespace.join(validDir, file), id, "valid");
|
|
1824
|
+
if (sample) samples2.push(sample);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
const invalidDir = path6__namespace.join(samplesDir, "invalid");
|
|
1828
|
+
if (fs6__namespace.existsSync(invalidDir)) {
|
|
1829
|
+
const files = fs6__namespace.readdirSync(invalidDir).filter((f) => f.endsWith(".json"));
|
|
1830
|
+
for (const file of files) {
|
|
1831
|
+
const id = file.replace(".json", "");
|
|
1832
|
+
const sample = loadSampleFromFile(path6__namespace.join(invalidDir, file), id, "invalid");
|
|
1833
|
+
if (sample) samples2.push(sample);
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const edgeDir = path6__namespace.join(samplesDir, "edge");
|
|
1837
|
+
if (fs6__namespace.existsSync(edgeDir)) {
|
|
1838
|
+
const files = fs6__namespace.readdirSync(edgeDir).filter((f) => f.endsWith(".json"));
|
|
1839
|
+
for (const file of files) {
|
|
1840
|
+
const id = file.replace(".json", "");
|
|
1841
|
+
const sample = loadSampleFromFile(path6__namespace.join(edgeDir, file), id, "edge");
|
|
1842
|
+
if (sample) samples2.push(sample);
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
return samples2;
|
|
1846
|
+
}
|
|
1847
|
+
function getSamples(customSamplesPath) {
|
|
1848
|
+
const samplesDir = findSamplesDir(customSamplesPath);
|
|
1849
|
+
if (samplesDir) {
|
|
1850
|
+
const dirSamples = loadSamplesFromDir(samplesDir);
|
|
1851
|
+
if (dirSamples.length > 0) {
|
|
1852
|
+
return dirSamples;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return EMBEDDED_SAMPLES;
|
|
1856
|
+
}
|
|
1857
|
+
function getSampleById(id, customSamplesPath) {
|
|
1858
|
+
const samples2 = getSamples(customSamplesPath);
|
|
1859
|
+
return samples2.find((s) => s.id === id) ?? null;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
// src/commands/samples.ts
|
|
1863
|
+
function getGlobalOptions3(cmd) {
|
|
1864
|
+
const parent = cmd.parent;
|
|
1865
|
+
if (!parent) return {};
|
|
1866
|
+
return parent.opts();
|
|
1867
|
+
}
|
|
1868
|
+
function outputError3(error, details, opts) {
|
|
1869
|
+
if (opts.json) {
|
|
1870
|
+
console.log(JSON.stringify({ success: false, error, ...details }, null, 2));
|
|
1871
|
+
} else {
|
|
1872
|
+
console.error(`Error: ${error}`);
|
|
1873
|
+
if (details.hint) {
|
|
1874
|
+
console.error(`Hint: ${details.hint}`);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
function applyTimeAdjustments(claims, sampleId, now) {
|
|
1879
|
+
const adjusted = { ...claims };
|
|
1880
|
+
const standardExpiry = 3600;
|
|
1881
|
+
const longExpiry = 86400;
|
|
1882
|
+
switch (sampleId) {
|
|
1883
|
+
case "expired":
|
|
1884
|
+
adjusted.iat = now - 7200;
|
|
1885
|
+
adjusted.exp = now - 3600;
|
|
1886
|
+
break;
|
|
1887
|
+
case "future-iat":
|
|
1888
|
+
adjusted.iat = now + 3600;
|
|
1889
|
+
adjusted.exp = now + 7200;
|
|
1890
|
+
break;
|
|
1891
|
+
case "long-expiry":
|
|
1892
|
+
adjusted.iat = now;
|
|
1893
|
+
adjusted.exp = now + longExpiry;
|
|
1894
|
+
break;
|
|
1895
|
+
default:
|
|
1896
|
+
if (adjusted.iat === 0 || !adjusted.iat) {
|
|
1897
|
+
adjusted.iat = now;
|
|
1898
|
+
}
|
|
1899
|
+
if (adjusted.exp === 0 || !adjusted.exp) {
|
|
1900
|
+
adjusted.exp = now + standardExpiry;
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
return adjusted;
|
|
1904
|
+
}
|
|
1905
|
+
var samples = new commander.Command("samples").description("Work with PEAC sample receipts (v0.10.8+)").option("--json", "Output in JSON format");
|
|
1906
|
+
samples.command("list").description("List available sample receipts").option("-c, --category <category>", "Filter by category: valid, invalid, edge").option("--samples <path>", "Path to samples directory").action((options, cmd) => {
|
|
1907
|
+
const globalOpts = getGlobalOptions3(cmd);
|
|
1908
|
+
try {
|
|
1909
|
+
const allSamples = getSamples(options.samples);
|
|
1910
|
+
let filteredSamples = allSamples;
|
|
1911
|
+
if (options.category) {
|
|
1912
|
+
filteredSamples = allSamples.filter((s) => s.category === options.category);
|
|
1913
|
+
if (filteredSamples.length === 0) {
|
|
1914
|
+
outputError3("No samples in category", { category: options.category }, globalOpts);
|
|
1915
|
+
process.exitCode = 1;
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
if (globalOpts.json) {
|
|
1920
|
+
const data = filteredSamples.map((s) => ({
|
|
1921
|
+
id: s.id,
|
|
1922
|
+
name: s.name,
|
|
1923
|
+
description: s.description,
|
|
1924
|
+
category: s.category
|
|
1925
|
+
}));
|
|
1926
|
+
console.log(JSON.stringify({ samples: data }, null, 2));
|
|
1927
|
+
} else {
|
|
1928
|
+
console.log("Available PEAC Sample Receipts\n");
|
|
1929
|
+
console.log("VALID SAMPLES:");
|
|
1930
|
+
for (const s of filteredSamples.filter((x) => x.category === "valid")) {
|
|
1931
|
+
console.log(` ${s.id}`);
|
|
1932
|
+
console.log(` ${s.description}
|
|
1933
|
+
`);
|
|
1934
|
+
}
|
|
1935
|
+
const invalid = filteredSamples.filter((x) => x.category === "invalid");
|
|
1936
|
+
if (invalid.length > 0) {
|
|
1937
|
+
console.log("\nINVALID SAMPLES (for testing rejection):");
|
|
1938
|
+
for (const s of invalid) {
|
|
1939
|
+
console.log(` ${s.id}`);
|
|
1940
|
+
console.log(` ${s.description}
|
|
1941
|
+
`);
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
const edge = filteredSamples.filter((x) => x.category === "edge");
|
|
1945
|
+
if (edge.length > 0) {
|
|
1946
|
+
console.log("\nEDGE CASE SAMPLES:");
|
|
1947
|
+
for (const s of edge) {
|
|
1948
|
+
console.log(` ${s.id}`);
|
|
1949
|
+
console.log(` ${s.description}
|
|
1950
|
+
`);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
console.log('\nUse "peac samples show <id>" to see sample details.');
|
|
1954
|
+
console.log('Use "peac samples generate -o <dir>" to generate sample files.');
|
|
1955
|
+
}
|
|
1956
|
+
process.exitCode = 0;
|
|
1957
|
+
} catch (err) {
|
|
1958
|
+
outputError3(err.message, {}, globalOpts);
|
|
1959
|
+
process.exitCode = 1;
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
samples.command("show").description("Show details of a specific sample").argument("<id>", "Sample ID").option("--samples <path>", "Path to samples directory").action((id, options, cmd) => {
|
|
1963
|
+
const globalOpts = getGlobalOptions3(cmd);
|
|
1964
|
+
try {
|
|
1965
|
+
const sample = getSampleById(id, options.samples);
|
|
1966
|
+
if (!sample) {
|
|
1967
|
+
const allSamples = getSamples(options.samples);
|
|
1968
|
+
const available = allSamples.map((s) => s.id);
|
|
1969
|
+
outputError3("Sample not found", { id, available }, globalOpts);
|
|
1970
|
+
process.exitCode = 1;
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
if (globalOpts.json) {
|
|
1974
|
+
console.log(JSON.stringify(sample, null, 2));
|
|
1975
|
+
} else {
|
|
1976
|
+
console.log(`Sample: ${sample.name}
|
|
1977
|
+
`);
|
|
1978
|
+
console.log(`ID: ${sample.id}`);
|
|
1979
|
+
console.log(`Category: ${sample.category}`);
|
|
1980
|
+
console.log(`Description: ${sample.description}
|
|
1981
|
+
`);
|
|
1982
|
+
console.log("Claims:");
|
|
1983
|
+
console.log(JSON.stringify(sample.claims, null, 2));
|
|
1984
|
+
if (sample.expectedError) {
|
|
1985
|
+
console.log(`
|
|
1986
|
+
Expected Error: ${sample.expectedError}`);
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
process.exitCode = 0;
|
|
1990
|
+
} catch (err) {
|
|
1991
|
+
outputError3(err.message, {}, globalOpts);
|
|
1992
|
+
process.exitCode = 1;
|
|
1993
|
+
}
|
|
1994
|
+
});
|
|
1995
|
+
samples.command("generate").description("Generate sample receipt files").requiredOption("-o, --output <dir>", "Output directory").option("-f, --format <format>", "Output format: jws, json, bundle", "jws").option("--category <category>", "Generate only specific category").option("--samples <path>", "Path to samples directory").option("--now <timestamp>", "Unix timestamp for iat/exp (for deterministic generation)").option("--kid <kid>", "Key ID to use").action(async (options, cmd) => {
|
|
1996
|
+
const globalOpts = getGlobalOptions3(cmd);
|
|
1997
|
+
try {
|
|
1998
|
+
const outputDir = options.output;
|
|
1999
|
+
const validDir = path6__namespace.join(outputDir, "valid");
|
|
2000
|
+
const invalidDir = path6__namespace.join(outputDir, "invalid");
|
|
2001
|
+
const edgeDir = path6__namespace.join(outputDir, "edge");
|
|
2002
|
+
const bundlesDir = path6__namespace.join(outputDir, "bundles");
|
|
2003
|
+
fs6__namespace.mkdirSync(validDir, { recursive: true });
|
|
2004
|
+
fs6__namespace.mkdirSync(invalidDir, { recursive: true });
|
|
2005
|
+
fs6__namespace.mkdirSync(edgeDir, { recursive: true });
|
|
2006
|
+
fs6__namespace.mkdirSync(bundlesDir, { recursive: true });
|
|
2007
|
+
const now = options.now ? parseInt(options.now, 10) : Math.floor(Date.now() / 1e3);
|
|
2008
|
+
const keyPair = await crypto.generateKeypair();
|
|
2009
|
+
const publicKeyBytes = keyPair.publicKey;
|
|
2010
|
+
const privateKeyBytes = keyPair.privateKey;
|
|
2011
|
+
const kid = options.kid || `sandbox-${new Date(now * 1e3).toISOString().slice(0, 7)}`;
|
|
2012
|
+
const publicJwk = {
|
|
2013
|
+
kty: "OKP",
|
|
2014
|
+
crv: "Ed25519",
|
|
2015
|
+
x: crypto.base64urlEncode(publicKeyBytes),
|
|
2016
|
+
kid,
|
|
2017
|
+
use: "sig",
|
|
2018
|
+
alg: "EdDSA"
|
|
2019
|
+
};
|
|
2020
|
+
const generatedFiles = [];
|
|
2021
|
+
let samplesToGenerate = getSamples(options.samples);
|
|
2022
|
+
if (options.category) {
|
|
2023
|
+
samplesToGenerate = samplesToGenerate.filter((s) => s.category === options.category);
|
|
2024
|
+
}
|
|
2025
|
+
for (const sample of samplesToGenerate) {
|
|
2026
|
+
const targetDir = sample.category === "valid" ? validDir : sample.category === "edge" ? edgeDir : invalidDir;
|
|
2027
|
+
const filename = `${sample.id}.${options.format === "json" ? "json" : "jws"}`;
|
|
2028
|
+
const filepath = path6__namespace.join(targetDir, filename);
|
|
2029
|
+
const adjustedClaims = applyTimeAdjustments(sample.claims, sample.id, now);
|
|
2030
|
+
if (options.format === "json") {
|
|
2031
|
+
const output2 = {
|
|
2032
|
+
$comment: sample.description,
|
|
2033
|
+
header: {
|
|
2034
|
+
alg: "EdDSA",
|
|
2035
|
+
typ: "peac-receipt/0.1",
|
|
2036
|
+
kid
|
|
2037
|
+
},
|
|
2038
|
+
payload: adjustedClaims,
|
|
2039
|
+
...sample.expectedError ? { expected_error: sample.expectedError } : {}
|
|
2040
|
+
};
|
|
2041
|
+
fs6__namespace.writeFileSync(filepath, JSON.stringify(output2, null, 2));
|
|
2042
|
+
} else {
|
|
2043
|
+
const jws = await crypto.sign(adjustedClaims, privateKeyBytes, kid);
|
|
2044
|
+
fs6__namespace.writeFileSync(filepath, jws);
|
|
2045
|
+
}
|
|
2046
|
+
generatedFiles.push(filepath);
|
|
2047
|
+
}
|
|
2048
|
+
const jwksPath = path6__namespace.join(bundlesDir, "sandbox-jwks.json");
|
|
2049
|
+
const jwks = {
|
|
2050
|
+
keys: [publicJwk]
|
|
2051
|
+
};
|
|
2052
|
+
fs6__namespace.writeFileSync(jwksPath, JSON.stringify(jwks, null, 2));
|
|
2053
|
+
generatedFiles.push(jwksPath);
|
|
2054
|
+
const bundlePath = path6__namespace.join(bundlesDir, "offline-verification.json");
|
|
2055
|
+
const bundle = {
|
|
2056
|
+
$comment: "Offline verification bundle with sample receipts and JWKS",
|
|
2057
|
+
description: "Bundle for offline verification testing. Contains test JWKS and sample metadata.",
|
|
2058
|
+
generated_at: new Date(now * 1e3).toISOString(),
|
|
2059
|
+
generator: {
|
|
2060
|
+
name: "peac-cli",
|
|
2061
|
+
version: getVersion()
|
|
2062
|
+
},
|
|
2063
|
+
jwks,
|
|
2064
|
+
samples: samplesToGenerate.map((s) => ({
|
|
2065
|
+
id: s.id,
|
|
2066
|
+
description: s.description,
|
|
2067
|
+
category: s.category
|
|
2068
|
+
})),
|
|
2069
|
+
notes: [
|
|
2070
|
+
"Sample timestamps are based on --now parameter or generation time.",
|
|
2071
|
+
"Do NOT use sandbox samples in production."
|
|
2072
|
+
]
|
|
2073
|
+
};
|
|
2074
|
+
fs6__namespace.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
2075
|
+
generatedFiles.push(bundlePath);
|
|
2076
|
+
if (globalOpts.json) {
|
|
2077
|
+
console.log(
|
|
2078
|
+
JSON.stringify(
|
|
2079
|
+
{
|
|
2080
|
+
success: true,
|
|
2081
|
+
output_dir: outputDir,
|
|
2082
|
+
files_generated: generatedFiles.length,
|
|
2083
|
+
files: generatedFiles,
|
|
2084
|
+
timestamp: now,
|
|
2085
|
+
kid
|
|
2086
|
+
},
|
|
2087
|
+
null,
|
|
2088
|
+
2
|
|
2089
|
+
)
|
|
2090
|
+
);
|
|
2091
|
+
} else {
|
|
2092
|
+
console.log(`Sample receipts generated successfully!
|
|
2093
|
+
`);
|
|
2094
|
+
console.log(`Output directory: ${outputDir}`);
|
|
2095
|
+
console.log(`Files generated: ${generatedFiles.length}`);
|
|
2096
|
+
console.log(`Timestamp: ${now} (${new Date(now * 1e3).toISOString()})`);
|
|
2097
|
+
console.log(`Key ID: ${kid}
|
|
2098
|
+
`);
|
|
2099
|
+
console.log("Generated files:");
|
|
2100
|
+
for (const f of generatedFiles) {
|
|
2101
|
+
console.log(` ${path6__namespace.relative(outputDir, f)}`);
|
|
2102
|
+
}
|
|
2103
|
+
console.log(`
|
|
2104
|
+
JWKS for verification: ${path6__namespace.relative(outputDir, jwksPath)}`);
|
|
2105
|
+
}
|
|
2106
|
+
process.exitCode = 0;
|
|
2107
|
+
} catch (err) {
|
|
2108
|
+
outputError3(err.message, {}, globalOpts);
|
|
2109
|
+
process.exitCode = 1;
|
|
2110
|
+
}
|
|
2111
|
+
});
|
|
2112
|
+
|
|
2113
|
+
// src/index.ts
|
|
2114
|
+
var program = new commander.Command();
|
|
2115
|
+
program.name("peac").description("PEAC protocol command-line tools").version(getVersion()).exitOverride();
|
|
2116
|
+
program.command("verify").description("Verify a PEAC receipt JWS").argument("<jws>", "JWS compact serialization or path to file containing JWS").option("-v, --verbose", "Show detailed output").action(async (jwsInput, options) => {
|
|
2117
|
+
try {
|
|
2118
|
+
let jws = jwsInput;
|
|
2119
|
+
if (fs6__namespace.existsSync(jwsInput)) {
|
|
2120
|
+
jws = fs6__namespace.readFileSync(jwsInput, "utf-8").trim();
|
|
2121
|
+
}
|
|
2122
|
+
console.log("Verifying PEAC receipt...\n");
|
|
2123
|
+
const { header, payload } = crypto.decode(jws);
|
|
2124
|
+
console.log("Receipt Information:");
|
|
2125
|
+
console.log(` Receipt ID: ${payload.rid}`);
|
|
2126
|
+
console.log(` Issuer: ${payload.iss}`);
|
|
2127
|
+
console.log(` Audience: ${payload.aud}`);
|
|
2128
|
+
console.log(` Amount: ${payload.amt} ${payload.cur}`);
|
|
2129
|
+
console.log(` Payment: ${payload.payment.rail} (${payload.payment.reference})`);
|
|
2130
|
+
console.log(` Issued: ${new Date(payload.iat * 1e3).toISOString()}`);
|
|
2131
|
+
if (payload.exp) {
|
|
2132
|
+
console.log(` Expires: ${new Date(payload.exp * 1e3).toISOString()}`);
|
|
2133
|
+
}
|
|
2134
|
+
console.log();
|
|
2135
|
+
console.log("Verifying signature...");
|
|
2136
|
+
const result = await protocol.verifyReceipt(jws);
|
|
2137
|
+
if (result.ok) {
|
|
2138
|
+
console.log("Signature valid!");
|
|
2139
|
+
if (result.perf) {
|
|
2140
|
+
console.log(` Verification time: ${result.perf.verify_ms.toFixed(2)}ms`);
|
|
2141
|
+
if (result.perf.jwks_fetch_ms) {
|
|
2142
|
+
console.log(` JWKS fetch time: ${result.perf.jwks_fetch_ms.toFixed(2)}ms`);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
process.exitCode = 0;
|
|
2146
|
+
} else {
|
|
2147
|
+
console.log(`Verification failed: ${result.reason}`);
|
|
2148
|
+
if (result.details) {
|
|
2149
|
+
console.log(` Details: ${result.details}`);
|
|
2150
|
+
}
|
|
2151
|
+
process.exitCode = 1;
|
|
2152
|
+
}
|
|
2153
|
+
} catch (err) {
|
|
2154
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
2155
|
+
process.exitCode = 1;
|
|
2156
|
+
}
|
|
2157
|
+
});
|
|
2158
|
+
program.command("validate-issuer").description("Validate a PEAC issuer configuration").argument("<input>", "Path to peac-issuer.json file or issuer URL").action(async (input) => {
|
|
2159
|
+
try {
|
|
2160
|
+
console.log("Validating issuer configuration...\n");
|
|
2161
|
+
let config;
|
|
2162
|
+
if (input.startsWith("http://") || input.startsWith("https://")) {
|
|
2163
|
+
console.log(`Fetching from ${input}...`);
|
|
2164
|
+
config = await protocol.fetchIssuerConfig(input);
|
|
2165
|
+
} else {
|
|
2166
|
+
const text = fs6__namespace.readFileSync(input, "utf-8");
|
|
2167
|
+
config = protocol.parseIssuerConfig(text);
|
|
2168
|
+
}
|
|
2169
|
+
console.log("Issuer configuration is valid!\n");
|
|
2170
|
+
console.log("Issuer Configuration:");
|
|
2171
|
+
console.log(` Version: ${config.version}`);
|
|
2172
|
+
console.log(` Issuer: ${config.issuer}`);
|
|
2173
|
+
console.log(` JWKS URI: ${config.jwks_uri}`);
|
|
2174
|
+
if (config.verify_endpoint) {
|
|
2175
|
+
console.log(` Verify: ${config.verify_endpoint}`);
|
|
2176
|
+
}
|
|
2177
|
+
if (config.receipt_versions && config.receipt_versions.length > 0) {
|
|
2178
|
+
console.log(` Receipts: ${config.receipt_versions.join(", ")}`);
|
|
2179
|
+
}
|
|
2180
|
+
if (config.algorithms && config.algorithms.length > 0) {
|
|
2181
|
+
console.log(` Algorithms: ${config.algorithms.join(", ")}`);
|
|
2182
|
+
}
|
|
2183
|
+
if (config.payment_rails && config.payment_rails.length > 0) {
|
|
2184
|
+
console.log(` Rails: ${config.payment_rails.join(", ")}`);
|
|
2185
|
+
}
|
|
2186
|
+
if (config.security_contact) {
|
|
2187
|
+
console.log(` Security: ${config.security_contact}`);
|
|
2188
|
+
}
|
|
2189
|
+
process.exitCode = 0;
|
|
2190
|
+
} catch (err) {
|
|
2191
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
2192
|
+
process.exitCode = 1;
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
program.command("decode").description("Decode a PEAC receipt JWS (without verification)").argument("<jws>", "JWS compact serialization or path to file containing JWS").option("--json", "Output as JSON").action((jwsInput, options) => {
|
|
2196
|
+
try {
|
|
2197
|
+
let jws = jwsInput;
|
|
2198
|
+
if (fs6__namespace.existsSync(jwsInput)) {
|
|
2199
|
+
jws = fs6__namespace.readFileSync(jwsInput, "utf-8").trim();
|
|
2200
|
+
}
|
|
2201
|
+
const { header, payload } = crypto.decode(jws);
|
|
2202
|
+
if (options.json) {
|
|
2203
|
+
console.log(JSON.stringify({ header, payload }, null, 2));
|
|
2204
|
+
} else {
|
|
2205
|
+
console.log("PEAC Receipt (Decoded, Not Verified)\n");
|
|
2206
|
+
console.log("Header:");
|
|
2207
|
+
console.log(` typ: ${header.typ}`);
|
|
2208
|
+
console.log(` alg: ${header.alg}`);
|
|
2209
|
+
console.log(` kid: ${header.kid}`);
|
|
2210
|
+
console.log();
|
|
2211
|
+
console.log("Claims:");
|
|
2212
|
+
console.log(` iss: ${payload.iss}`);
|
|
2213
|
+
console.log(` aud: ${payload.aud}`);
|
|
2214
|
+
console.log(` iat: ${payload.iat} (${new Date(payload.iat * 1e3).toISOString()})`);
|
|
2215
|
+
if (payload.exp) {
|
|
2216
|
+
console.log(` exp: ${payload.exp} (${new Date(payload.exp * 1e3).toISOString()})`);
|
|
2217
|
+
}
|
|
2218
|
+
console.log(` rid: ${payload.rid}`);
|
|
2219
|
+
console.log(` amt: ${payload.amt}`);
|
|
2220
|
+
console.log(` cur: ${payload.cur}`);
|
|
2221
|
+
console.log();
|
|
2222
|
+
console.log("Payment:");
|
|
2223
|
+
console.log(` rail: ${payload.payment.rail}`);
|
|
2224
|
+
console.log(` reference: ${payload.payment.reference}`);
|
|
2225
|
+
console.log(` amount: ${payload.payment.amount}`);
|
|
2226
|
+
console.log(` currency: ${payload.payment.currency}`);
|
|
2227
|
+
if (payload.subject) {
|
|
2228
|
+
console.log();
|
|
2229
|
+
console.log("Subject:");
|
|
2230
|
+
console.log(` uri: ${payload.subject.uri}`);
|
|
2231
|
+
}
|
|
2232
|
+
if (payload.ext) {
|
|
2233
|
+
console.log();
|
|
2234
|
+
console.log("Extensions:");
|
|
2235
|
+
console.log(JSON.stringify(payload.ext, null, 2));
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
process.exitCode = 0;
|
|
2239
|
+
} catch (err) {
|
|
2240
|
+
console.error("Error:", err instanceof Error ? err.message : String(err));
|
|
2241
|
+
process.exitCode = 1;
|
|
2242
|
+
}
|
|
2243
|
+
});
|
|
2244
|
+
program.addCommand(policy);
|
|
2245
|
+
program.addCommand(conformance);
|
|
2246
|
+
program.addCommand(samples);
|
|
2247
|
+
try {
|
|
2248
|
+
program.parse();
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
if (err instanceof commander.CommanderError) {
|
|
2251
|
+
process.exitCode = err.exitCode;
|
|
2252
|
+
} else {
|
|
2253
|
+
throw err;
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
//# sourceMappingURL=index.cjs.map
|
|
2257
|
+
//# sourceMappingURL=index.cjs.map
|