@peac/cli 0.15.0 → 0.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -277,8 +277,8 @@ policy.command("validate").description("Validate a PEAC policy file (YAML or JSO
277
277
  );
278
278
  } else {
279
279
  console.error("Policy validation failed:\n");
280
- for (const issue3 of err.issues) {
281
- console.error(` - ${issue3.path.join(".")}: ${issue3.message}`);
280
+ for (const issue4 of err.issues) {
281
+ console.error(` - ${issue4.path.join(".")}: ${issue4.message}`);
282
282
  }
283
283
  }
284
284
  process.exit(1);
@@ -1910,95 +1910,79 @@ Use 'peac conformance list -c <category>' to see fixtures in a category.`);
1910
1910
  });
1911
1911
  var EMBEDDED_SAMPLES = [
1912
1912
  {
1913
- id: "basic-receipt",
1914
- name: "Basic Receipt",
1915
- description: "Minimal valid PEAC receipt with only required fields",
1913
+ id: "basic-record",
1914
+ name: "Basic Record",
1915
+ description: "Minimal valid PEAC signed interaction record",
1916
1916
  category: "valid",
1917
- claims: {
1917
+ format: "issue-options",
1918
+ input: {
1918
1919
  iss: "https://sandbox.peacprotocol.org",
1919
- aud: "https://example.com",
1920
- iat: 0,
1921
- // Placeholder - will be set at generation time
1922
- exp: 0,
1923
- // Placeholder - will be set at generation time
1924
- rid: "sample-basic-001"
1920
+ kind: "evidence",
1921
+ type: "org.peacprotocol/access"
1925
1922
  }
1926
1923
  },
1927
1924
  {
1928
- id: "full-receipt",
1929
- name: "Full Receipt",
1930
- description: "PEAC receipt with all optional claims populated",
1925
+ id: "full-record",
1926
+ name: "Full Record",
1927
+ description: "Valid PEAC signed interaction record with optional fields",
1931
1928
  category: "valid",
1932
- claims: {
1929
+ format: "issue-options",
1930
+ input: {
1933
1931
  iss: "https://sandbox.peacprotocol.org",
1934
- aud: "https://api.example.com",
1935
- sub: "user:demo-user",
1936
- iat: 0,
1937
- exp: 0,
1938
- rid: "sample-full-001",
1939
- purpose_declared: ["search", "index"],
1940
- purpose_enforced: "search",
1941
- purpose_reason: "allowed"
1932
+ kind: "evidence",
1933
+ type: "org.peacprotocol/access",
1934
+ sub: "agent:demo-agent",
1935
+ purpose_declared: "search"
1942
1936
  }
1943
1937
  },
1944
1938
  {
1945
- id: "interaction-evidence",
1946
- name: "Interaction Evidence",
1947
- description: "Receipt with InteractionEvidence extension for AI agent calls",
1939
+ id: "mcp-tool-run",
1940
+ name: "Mcp Tool Run",
1941
+ description: "Valid PEAC signed interaction record for an MCP tool run",
1948
1942
  category: "valid",
1949
- claims: {
1943
+ format: "issue-options",
1944
+ input: {
1950
1945
  iss: "https://sandbox.peacprotocol.org",
1951
- aud: "https://agent.example.com",
1952
- sub: "agent:demo-agent-v1",
1953
- iat: 0,
1954
- exp: 0,
1955
- rid: "sample-ie-001",
1956
- ext: {
1957
- "org.peacprotocol/interaction@0.1": {
1958
- version: "0.1",
1959
- interaction_id: "int_sample_001",
1960
- started_at: "2026-01-01T00:00:00.000Z",
1961
- completed_at: "2026-01-01T00:00:01.000Z",
1962
- outcome: { kind: "success" },
1963
- input: { hash: "sha256:abc123...", byte_length: 1024 },
1964
- output: { hash: "sha256:def456...", byte_length: 2048 }
1946
+ kind: "evidence",
1947
+ type: "org.peacprotocol/mcp",
1948
+ extensions: {
1949
+ "org.peacprotocol/mcp": {
1950
+ server: "demo",
1951
+ tool: "search"
1965
1952
  }
1966
1953
  }
1967
1954
  }
1968
1955
  },
1969
1956
  {
1970
- id: "payment-evidence",
1971
- name: "Payment Evidence",
1972
- description: "Receipt with payment evidence (402 flow)",
1957
+ id: "payment-event",
1958
+ name: "Payment Event",
1959
+ description: "Valid PEAC signed interaction record for a payment event",
1973
1960
  category: "valid",
1974
- claims: {
1961
+ format: "issue-options",
1962
+ input: {
1975
1963
  iss: "https://sandbox.peacprotocol.org",
1976
- aud: "https://api.example.com",
1977
- iat: 0,
1978
- exp: 0,
1979
- rid: "sample-payment-001",
1980
- amt: "100",
1981
- cur: "USD",
1982
- payment: {
1983
- rail: "x402",
1984
- reference: "pay_sample_001",
1985
- amount: "100",
1986
- currency: "USD"
1964
+ kind: "evidence",
1965
+ type: "org.peacprotocol/payment",
1966
+ extensions: {
1967
+ "org.peacprotocol/commerce": {
1968
+ payment_rail: "stripe",
1969
+ amount_minor: "1000",
1970
+ currency: "USD"
1971
+ }
1987
1972
  }
1988
1973
  }
1989
1974
  },
1990
1975
  {
1991
- id: "long-expiry",
1992
- name: "Long Expiry",
1993
- description: "Receipt with 24-hour expiration",
1976
+ id: "event-time-record",
1977
+ name: "Event Time Record",
1978
+ description: "Valid PEAC signed interaction record with an event time. occurred_at is the interaction time; iat remains issuance time. Generation maps --now to occurred_at.",
1994
1979
  category: "valid",
1995
- claims: {
1980
+ format: "issue-options",
1981
+ input: {
1996
1982
  iss: "https://sandbox.peacprotocol.org",
1997
- aud: "https://example.com",
1998
- iat: 0,
1999
- exp: 0,
2000
- // Will be set to iat + 86400 at generation time
2001
- rid: "sample-long-expiry-001"
1983
+ kind: "evidence",
1984
+ type: "org.peacprotocol/access",
1985
+ occurred_at: "2026-01-01T00:00:00.000Z"
2002
1986
  }
2003
1987
  },
2004
1988
  {
@@ -2006,6 +1990,7 @@ var EMBEDDED_SAMPLES = [
2006
1990
  name: "Expired Receipt",
2007
1991
  description: "Receipt that has already expired (for testing rejection)",
2008
1992
  category: "invalid",
1993
+ format: "legacy-claims",
2009
1994
  claims: {
2010
1995
  iss: "https://sandbox.peacprotocol.org",
2011
1996
  aud: "https://example.com",
@@ -2022,6 +2007,7 @@ var EMBEDDED_SAMPLES = [
2022
2007
  name: "Future IAT",
2023
2008
  description: "Receipt with iat in the future (should be rejected)",
2024
2009
  category: "invalid",
2010
+ format: "legacy-claims",
2025
2011
  claims: {
2026
2012
  iss: "https://sandbox.peacprotocol.org",
2027
2013
  aud: "https://example.com",
@@ -2038,6 +2024,7 @@ var EMBEDDED_SAMPLES = [
2038
2024
  name: "Missing Issuer",
2039
2025
  description: "Receipt missing required iss claim (for testing validation)",
2040
2026
  category: "invalid",
2027
+ format: "legacy-claims",
2041
2028
  claims: {
2042
2029
  aud: "https://example.com",
2043
2030
  iat: 0,
@@ -2048,12 +2035,6 @@ var EMBEDDED_SAMPLES = [
2048
2035
  }
2049
2036
  ];
2050
2037
  function findSamplesDir(customPath) {
2051
- if (customPath) {
2052
- if (fs6__namespace.existsSync(customPath)) {
2053
- return customPath;
2054
- }
2055
- return null;
2056
- }
2057
2038
  const cliPath = path6__namespace.resolve(__dirname, "../../../../specs/conformance/samples");
2058
2039
  if (fs6__namespace.existsSync(cliPath)) {
2059
2040
  return cliPath;
@@ -2071,13 +2052,46 @@ function findSamplesDir(customPath) {
2071
2052
  function loadSampleFromFile(filePath, id, category) {
2072
2053
  try {
2073
2054
  const content = JSON.parse(fs6__namespace.readFileSync(filePath, "utf8"));
2074
- const claims = content.payload ?? content.claims ?? content;
2075
2055
  const description = content.$comment ?? content.description ?? `Sample ${id}`;
2056
+ const name = id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
2057
+ if (category === "valid") {
2058
+ if (content.format !== "issue-options" || content.input === null || typeof content.input !== "object" || Array.isArray(content.input)) {
2059
+ process.stderr.write(
2060
+ `samples-loader: skipping malformed valid sample '${id}' (expected format "issue-options" with an input object)
2061
+ `
2062
+ );
2063
+ return null;
2064
+ }
2065
+ return {
2066
+ id,
2067
+ name,
2068
+ description,
2069
+ category: "valid",
2070
+ format: "issue-options",
2071
+ input: content.input
2072
+ };
2073
+ }
2074
+ if (content.format !== void 0 && content.format !== "legacy-claims") {
2075
+ process.stderr.write(
2076
+ `samples-loader: skipping '${id}' (unexpected format "${content.format}" for ${category} sample)
2077
+ `
2078
+ );
2079
+ return null;
2080
+ }
2081
+ const claims = content.payload ?? content.claims ?? content;
2082
+ if (claims === null || typeof claims !== "object" || Array.isArray(claims)) {
2083
+ process.stderr.write(
2084
+ `samples-loader: skipping malformed ${category} sample '${id}' (claims must be an object)
2085
+ `
2086
+ );
2087
+ return null;
2088
+ }
2076
2089
  return {
2077
2090
  id,
2078
- name: id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" "),
2091
+ name,
2079
2092
  description,
2080
2093
  category,
2094
+ format: "legacy-claims",
2081
2095
  claims,
2082
2096
  header: content.header,
2083
2097
  expectedError: content.expected_error
@@ -2118,12 +2132,15 @@ function loadSamplesFromDir(samplesDir) {
2118
2132
  return samples2;
2119
2133
  }
2120
2134
  function getSamples(customSamplesPath) {
2121
- const samplesDir = findSamplesDir(customSamplesPath);
2122
- if (samplesDir) {
2123
- const dirSamples = loadSamplesFromDir(samplesDir);
2124
- if (dirSamples.length > 0) {
2125
- return dirSamples;
2135
+ if (customSamplesPath !== void 0) {
2136
+ if (!fs6__namespace.existsSync(customSamplesPath)) {
2137
+ throw new Error(`Samples directory not found: ${customSamplesPath}`);
2126
2138
  }
2139
+ return loadSamplesFromDir(customSamplesPath);
2140
+ }
2141
+ const samplesDir = findSamplesDir();
2142
+ if (samplesDir) {
2143
+ return loadSamplesFromDir(samplesDir);
2127
2144
  }
2128
2145
  return EMBEDDED_SAMPLES;
2129
2146
  }
@@ -2151,7 +2168,6 @@ function outputError3(error, details, opts) {
2151
2168
  function applyTimeAdjustments(claims, sampleId, now) {
2152
2169
  const adjusted = { ...claims };
2153
2170
  const standardExpiry = 3600;
2154
- const longExpiry = 86400;
2155
2171
  switch (sampleId) {
2156
2172
  case "expired":
2157
2173
  adjusted.iat = now - 7200;
@@ -2161,10 +2177,6 @@ function applyTimeAdjustments(claims, sampleId, now) {
2161
2177
  adjusted.iat = now + 3600;
2162
2178
  adjusted.exp = now + 7200;
2163
2179
  break;
2164
- case "long-expiry":
2165
- adjusted.iat = now;
2166
- adjusted.exp = now + longExpiry;
2167
- break;
2168
2180
  default:
2169
2181
  if (adjusted.iat === 0 || !adjusted.iat) {
2170
2182
  adjusted.iat = now;
@@ -2175,10 +2187,19 @@ function applyTimeAdjustments(claims, sampleId, now) {
2175
2187
  }
2176
2188
  return adjusted;
2177
2189
  }
2178
- var samples = new commander.Command("samples").description("Work with PEAC sample receipts (v0.10.8+)").option("--json", "Output in JSON format");
2179
- 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) => {
2190
+ var samples = new commander.Command("samples").description("Work with PEAC sample records (v0.10.8+)").option("--json", "Output in JSON format");
2191
+ samples.command("list").description("List available sample records").option("-c, --category <category>", "Filter by category: valid, invalid, edge").option("--samples <path>", "Path to samples directory").action((options, cmd) => {
2180
2192
  const globalOpts = getGlobalOptions3(cmd);
2181
2193
  try {
2194
+ if (options.category !== void 0 && !["valid", "invalid", "edge"].includes(options.category)) {
2195
+ outputError3(
2196
+ "Unknown sample category",
2197
+ { category: options.category, supported: ["valid", "invalid", "edge"] },
2198
+ globalOpts
2199
+ );
2200
+ process.exitCode = 1;
2201
+ return;
2202
+ }
2182
2203
  const allSamples = getSamples(options.samples);
2183
2204
  let filteredSamples = allSamples;
2184
2205
  if (options.category) {
@@ -2198,7 +2219,7 @@ samples.command("list").description("List available sample receipts").option("-c
2198
2219
  }));
2199
2220
  console.log(JSON.stringify({ samples: data }, null, 2));
2200
2221
  } else {
2201
- console.log("Available PEAC Sample Receipts\n");
2222
+ console.log("Available PEAC Sample Records\n");
2202
2223
  console.log("VALID SAMPLES:");
2203
2224
  for (const s of filteredSamples.filter((x) => x.category === "valid")) {
2204
2225
  console.log(` ${s.id}`);
@@ -2252,11 +2273,16 @@ samples.command("show").description("Show details of a specific sample").argumen
2252
2273
  console.log(`Category: ${sample.category}`);
2253
2274
  console.log(`Description: ${sample.description}
2254
2275
  `);
2255
- console.log("Claims:");
2256
- console.log(JSON.stringify(sample.claims, null, 2));
2257
- if (sample.expectedError) {
2258
- console.log(`
2276
+ if (sample.category === "valid") {
2277
+ console.log("Issue input (current PEAC signed interaction record):");
2278
+ console.log(JSON.stringify(sample.input, null, 2));
2279
+ } else {
2280
+ console.log("Claims (legacy rejection fixture):");
2281
+ console.log(JSON.stringify(sample.claims, null, 2));
2282
+ if (sample.expectedError) {
2283
+ console.log(`
2259
2284
  Expected Error: ${sample.expectedError}`);
2285
+ }
2260
2286
  }
2261
2287
  }
2262
2288
  process.exitCode = 0;
@@ -2265,10 +2291,81 @@ Expected Error: ${sample.expectedError}`);
2265
2291
  process.exitCode = 1;
2266
2292
  }
2267
2293
  });
2268
- 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) => {
2294
+ samples.command("generate").description("Generate sample record files").requiredOption("-o, --output <dir>", "Output directory").option("-f, --format <format>", "Output format: jws, json", "jws").option("--category <category>", "Generate only specific category").option("--samples <path>", "Path to samples directory").option(
2295
+ "--now <timestamp>",
2296
+ "Unix event time (seconds) for valid samples; sets occurred_at (must not be in the future)"
2297
+ ).option("--kid <kid>", "Key ID to use").action(async (options, cmd) => {
2269
2298
  const globalOpts = getGlobalOptions3(cmd);
2270
2299
  try {
2271
2300
  const outputDir = options.output;
2301
+ const allowedFormats = /* @__PURE__ */ new Set(["jws", "json"]);
2302
+ if (!allowedFormats.has(options.format)) {
2303
+ outputError3(
2304
+ "Unsupported sample format",
2305
+ { format: options.format, supported: ["jws", "json"] },
2306
+ globalOpts
2307
+ );
2308
+ process.exitCode = 1;
2309
+ return;
2310
+ }
2311
+ const allowedCategories = /* @__PURE__ */ new Set(["valid", "invalid", "edge"]);
2312
+ if (options.category !== void 0 && !allowedCategories.has(options.category)) {
2313
+ outputError3(
2314
+ "Unknown sample category",
2315
+ { category: options.category, supported: ["valid", "invalid", "edge"] },
2316
+ globalOpts
2317
+ );
2318
+ process.exitCode = 1;
2319
+ return;
2320
+ }
2321
+ let samplesToGenerate = getSamples(options.samples);
2322
+ if (options.category) {
2323
+ samplesToGenerate = samplesToGenerate.filter((s) => s.category === options.category);
2324
+ }
2325
+ if (samplesToGenerate.length === 0) {
2326
+ outputError3(
2327
+ "No samples selected to generate",
2328
+ { category: options.category ?? null },
2329
+ globalOpts
2330
+ );
2331
+ process.exitCode = 1;
2332
+ return;
2333
+ }
2334
+ let eventTime;
2335
+ if (options.now !== void 0) {
2336
+ const parsed = Number(options.now);
2337
+ if (!Number.isInteger(parsed)) {
2338
+ outputError3(
2339
+ "--now must be an integer Unix timestamp (seconds)",
2340
+ { now: options.now },
2341
+ globalOpts
2342
+ );
2343
+ process.exitCode = 1;
2344
+ return;
2345
+ }
2346
+ if (!Number.isFinite(new Date(parsed * 1e3).getTime())) {
2347
+ outputError3(
2348
+ "--now is outside the supported date range",
2349
+ { now: options.now },
2350
+ globalOpts
2351
+ );
2352
+ process.exitCode = 1;
2353
+ return;
2354
+ }
2355
+ eventTime = parsed;
2356
+ }
2357
+ const generationNow = Math.floor(Date.now() / 1e3);
2358
+ const SKEW_SECONDS = 300;
2359
+ const generatesValid = samplesToGenerate.some((s) => s.category === "valid");
2360
+ if (eventTime !== void 0 && generatesValid && eventTime > generationNow + SKEW_SECONDS) {
2361
+ outputError3(
2362
+ "--now sets occurred_at (event time) for valid samples and must not be in the future",
2363
+ { now: eventTime, wall_clock: generationNow, skew_seconds: SKEW_SECONDS },
2364
+ globalOpts
2365
+ );
2366
+ process.exitCode = 1;
2367
+ return;
2368
+ }
2272
2369
  const validDir = path6__namespace.join(outputDir, "valid");
2273
2370
  const invalidDir = path6__namespace.join(outputDir, "invalid");
2274
2371
  const edgeDir = path6__namespace.join(outputDir, "edge");
@@ -2277,11 +2374,10 @@ samples.command("generate").description("Generate sample receipt files").require
2277
2374
  fs6__namespace.mkdirSync(invalidDir, { recursive: true });
2278
2375
  fs6__namespace.mkdirSync(edgeDir, { recursive: true });
2279
2376
  fs6__namespace.mkdirSync(bundlesDir, { recursive: true });
2280
- const now = options.now ? parseInt(options.now, 10) : Math.floor(Date.now() / 1e3);
2281
2377
  const keyPair = await crypto.generateKeypair();
2282
2378
  const publicKeyBytes = keyPair.publicKey;
2283
2379
  const privateKeyBytes = keyPair.privateKey;
2284
- const kid = options.kid || `sandbox-${new Date(now * 1e3).toISOString().slice(0, 7)}`;
2380
+ const kid = options.kid || `sandbox-${new Date(generationNow * 1e3).toISOString().slice(0, 7)}`;
2285
2381
  const publicJwk = {
2286
2382
  kty: "OKP",
2287
2383
  crv: "Ed25519",
@@ -2291,30 +2387,41 @@ samples.command("generate").description("Generate sample receipt files").require
2291
2387
  alg: "EdDSA"
2292
2388
  };
2293
2389
  const generatedFiles = [];
2294
- let samplesToGenerate = getSamples(options.samples);
2295
- if (options.category) {
2296
- samplesToGenerate = samplesToGenerate.filter((s) => s.category === options.category);
2297
- }
2298
2390
  for (const sample of samplesToGenerate) {
2299
2391
  const targetDir = sample.category === "valid" ? validDir : sample.category === "edge" ? edgeDir : invalidDir;
2300
2392
  const filename = `${sample.id}.${options.format === "json" ? "json" : "jws"}`;
2301
2393
  const filepath = path6__namespace.join(targetDir, filename);
2302
- const adjustedClaims = applyTimeAdjustments(sample.claims, sample.id, now);
2303
- if (options.format === "json") {
2304
- const output2 = {
2305
- $comment: sample.description,
2306
- header: {
2307
- alg: "EdDSA",
2308
- typ: "interaction-record+jwt",
2309
- kid
2310
- },
2311
- payload: adjustedClaims,
2312
- ...sample.expectedError ? { expected_error: sample.expectedError } : {}
2394
+ if (sample.category === "valid") {
2395
+ const issueOptions = {
2396
+ ...sample.input,
2397
+ privateKey: privateKeyBytes,
2398
+ kid,
2399
+ ...eventTime !== void 0 ? { occurred_at: new Date(eventTime * 1e3).toISOString() } : {}
2313
2400
  };
2314
- fs6__namespace.writeFileSync(filepath, JSON.stringify(output2, null, 2));
2401
+ const { jws } = await protocol.issue(issueOptions);
2402
+ if (options.format === "json") {
2403
+ const { header, payload } = crypto.decode(jws);
2404
+ fs6__namespace.writeFileSync(
2405
+ filepath,
2406
+ JSON.stringify({ $comment: sample.description, header, payload }, null, 2)
2407
+ );
2408
+ } else {
2409
+ fs6__namespace.writeFileSync(filepath, jws);
2410
+ }
2315
2411
  } else {
2316
- const jws = await crypto.sign(adjustedClaims, privateKeyBytes, kid);
2317
- fs6__namespace.writeFileSync(filepath, jws);
2412
+ const adjustedClaims = applyTimeAdjustments(sample.claims, sample.id, generationNow);
2413
+ if (options.format === "json") {
2414
+ const output2 = {
2415
+ $comment: sample.description,
2416
+ header: { alg: "EdDSA", typ: "interaction-record+jwt", kid },
2417
+ payload: adjustedClaims,
2418
+ ...sample.expectedError ? { expected_error: sample.expectedError } : {}
2419
+ };
2420
+ fs6__namespace.writeFileSync(filepath, JSON.stringify(output2, null, 2));
2421
+ } else {
2422
+ const jws = await crypto.sign(adjustedClaims, privateKeyBytes, kid);
2423
+ fs6__namespace.writeFileSync(filepath, jws);
2424
+ }
2318
2425
  }
2319
2426
  generatedFiles.push(filepath);
2320
2427
  }
@@ -2326,9 +2433,9 @@ samples.command("generate").description("Generate sample receipt files").require
2326
2433
  generatedFiles.push(jwksPath);
2327
2434
  const bundlePath = path6__namespace.join(bundlesDir, "offline-verification.json");
2328
2435
  const bundle = {
2329
- $comment: "Offline verification bundle with sample receipts and JWKS",
2330
- description: "Bundle for offline verification testing. Contains test JWKS and sample metadata.",
2331
- generated_at: new Date(now * 1e3).toISOString(),
2436
+ $comment: "Offline verification bundle with sample records and JWKS",
2437
+ description: "Sample record metadata and the sandbox public verification keys.",
2438
+ generated_at: new Date(generationNow * 1e3).toISOString(),
2332
2439
  generator: {
2333
2440
  name: "peac-cli",
2334
2441
  version: getVersion()
@@ -2340,8 +2447,10 @@ samples.command("generate").description("Generate sample receipt files").require
2340
2447
  category: s.category
2341
2448
  })),
2342
2449
  notes: [
2343
- "Sample timestamps are based on --now parameter or generation time.",
2344
- "Do NOT use sandbox samples in production."
2450
+ "Valid samples are issued through issue() and pass local verification (verifyLocal).",
2451
+ "Invalid samples are intentionally invalid rejection fixtures.",
2452
+ "For valid samples, --now sets the event time (occurred_at); iat is issuance time.",
2453
+ "Sandbox samples are for local testing and demonstration; do NOT use them in production."
2345
2454
  ]
2346
2455
  };
2347
2456
  fs6__namespace.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
@@ -2354,7 +2463,8 @@ samples.command("generate").description("Generate sample receipt files").require
2354
2463
  output_dir: outputDir,
2355
2464
  files_generated: generatedFiles.length,
2356
2465
  files: generatedFiles,
2357
- timestamp: now,
2466
+ generation_time: generationNow,
2467
+ event_time: eventTime ?? null,
2358
2468
  kid
2359
2469
  },
2360
2470
  null,
@@ -2362,11 +2472,16 @@ samples.command("generate").description("Generate sample receipt files").require
2362
2472
  )
2363
2473
  );
2364
2474
  } else {
2365
- console.log(`Sample receipts generated successfully!
2475
+ console.log(`Sample records generated successfully!
2366
2476
  `);
2367
2477
  console.log(`Output directory: ${outputDir}`);
2368
2478
  console.log(`Files generated: ${generatedFiles.length}`);
2369
- console.log(`Timestamp: ${now} (${new Date(now * 1e3).toISOString()})`);
2479
+ console.log(
2480
+ `Generation time: ${generationNow} (${new Date(generationNow * 1e3).toISOString()})`
2481
+ );
2482
+ if (eventTime !== void 0) {
2483
+ console.log(`Event time: ${eventTime} (${new Date(eventTime * 1e3).toISOString()})`);
2484
+ }
2370
2485
  console.log(`Key ID: ${kid}
2371
2486
  `);
2372
2487
  console.log("Generated files:");
@@ -4818,15 +4933,128 @@ function emitCommand() {
4818
4933
  emit.addCommand(emitLifecycleSubcommand());
4819
4934
  return emit;
4820
4935
  }
4936
+ function parsePublicKey(content) {
4937
+ let parsed;
4938
+ try {
4939
+ parsed = JSON.parse(content);
4940
+ } catch {
4941
+ throw new Error("public key file is not valid JSON");
4942
+ }
4943
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
4944
+ throw new Error("public key file must be a JWK or single-key JWKS object");
4945
+ }
4946
+ const obj = parsed;
4947
+ let jwk;
4948
+ if ("keys" in obj) {
4949
+ const keys = obj.keys;
4950
+ if (!Array.isArray(keys) || keys.length === 0) {
4951
+ throw new Error("JWKS contains no keys");
4952
+ }
4953
+ if (keys.length > 1) {
4954
+ throw new Error(
4955
+ "JWKS contains multiple keys; a single-key JWKS is required (multi-key JWKS is not supported)"
4956
+ );
4957
+ }
4958
+ const first = keys[0];
4959
+ if (first === null || typeof first !== "object" || Array.isArray(first)) {
4960
+ throw new Error("JWKS key entry is not a JWK object");
4961
+ }
4962
+ jwk = first;
4963
+ } else {
4964
+ jwk = obj;
4965
+ }
4966
+ if ("d" in jwk) {
4967
+ throw new Error("public key required: the file contains private key material");
4968
+ }
4969
+ const { kty, crv, x } = jwk;
4970
+ if (kty !== "OKP" || crv !== "Ed25519") {
4971
+ throw new Error('unsupported key: expected a public Ed25519 JWK (kty "OKP", crv "Ed25519")');
4972
+ }
4973
+ if (typeof x !== "string" || x.length === 0) {
4974
+ throw new Error('invalid JWK: missing public key value "x"');
4975
+ }
4976
+ try {
4977
+ return crypto.jwkToPublicKeyBytes({ kty: "OKP", crv: "Ed25519", x });
4978
+ } catch {
4979
+ throw new Error('invalid JWK: "x" is not a 32-byte base64url Ed25519 public key');
4980
+ }
4981
+ }
4821
4982
 
4822
4983
  // src/index.ts
4984
+ var MAX_PUBLIC_KEY_FILE_BYTES = 16384;
4985
+ var MAX_VERIFY_JWS_FILE_BYTES = 512 * 1024;
4823
4986
  var program = new commander.Command();
4824
4987
  program.name("peac").description("PEAC protocol command-line tools").version(getVersion()).exitOverride();
4825
- 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) => {
4988
+ 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").option(
4989
+ "--public-key <path>",
4990
+ "Verify offline with a public Ed25519 JWK or single-key JWKS file (no network)"
4991
+ ).action(async (jwsInput, options) => {
4826
4992
  try {
4827
4993
  let jws = jwsInput;
4828
4994
  if (fs6__namespace.existsSync(jwsInput)) {
4829
- jws = fs6__namespace.readFileSync(jwsInput, "utf-8").trim();
4995
+ try {
4996
+ jws = readFileBufferSnapshot(jwsInput, { maxBytes: MAX_VERIFY_JWS_FILE_BYTES }).toString("utf8").trim();
4997
+ } catch (readErr) {
4998
+ const code = readErr.code;
4999
+ if (code === "E_PEAC_FILE_TOO_LARGE") {
5000
+ console.log(
5001
+ `Verification failed: receipt file exceeds ${MAX_VERIFY_JWS_FILE_BYTES} bytes`
5002
+ );
5003
+ } else if (code === "EISDIR") {
5004
+ console.log("Verification failed: receipt path is a directory");
5005
+ } else {
5006
+ console.log("Verification failed: could not read receipt file");
5007
+ }
5008
+ process.exitCode = 1;
5009
+ return;
5010
+ }
5011
+ }
5012
+ if (options.publicKey) {
5013
+ console.log("Verifying PEAC receipt offline...\n");
5014
+ let keyContent;
5015
+ try {
5016
+ keyContent = readFileBufferSnapshot(options.publicKey, {
5017
+ maxBytes: MAX_PUBLIC_KEY_FILE_BYTES
5018
+ }).toString("utf8");
5019
+ } catch (readErr) {
5020
+ const code = readErr.code;
5021
+ if (code === "E_PEAC_FILE_TOO_LARGE") {
5022
+ console.log(
5023
+ `Verification failed: public key file exceeds ${MAX_PUBLIC_KEY_FILE_BYTES} bytes`
5024
+ );
5025
+ } else if (code === "EISDIR") {
5026
+ console.log("Verification failed: public key path is a directory");
5027
+ } else {
5028
+ console.log("Verification failed: could not read public key file");
5029
+ }
5030
+ process.exitCode = 1;
5031
+ return;
5032
+ }
5033
+ let publicKey;
5034
+ try {
5035
+ publicKey = parsePublicKey(keyContent);
5036
+ } catch (keyErr) {
5037
+ console.log(
5038
+ `Verification failed: ${keyErr instanceof Error ? keyErr.message : "invalid public key"}`
5039
+ );
5040
+ process.exitCode = 1;
5041
+ return;
5042
+ }
5043
+ const localResult = await protocol.verifyLocal(jws, publicKey);
5044
+ if (localResult.valid) {
5045
+ console.log("Signature valid (offline).");
5046
+ console.log(
5047
+ " Verified the receipt signature and declared receipt structure against the supplied public key."
5048
+ );
5049
+ process.exitCode = 0;
5050
+ } else {
5051
+ console.log(`Verification failed: ${localResult.message}`);
5052
+ if (localResult.code) {
5053
+ console.log(` Code: ${localResult.code}`);
5054
+ }
5055
+ process.exitCode = 1;
5056
+ }
5057
+ return;
4830
5058
  }
4831
5059
  console.log("Verifying PEAC receipt...\n");
4832
5060
  const { payload } = crypto.decode(jws);