@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/README.md +3 -3
- package/dist/commands/samples.d.ts +3 -3
- package/dist/commands/samples.d.ts.map +1 -1
- package/dist/index.cjs +350 -122
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +352 -124
- package/dist/index.mjs.map +1 -1
- package/dist/lib/public-key.d.ts +21 -0
- package/dist/lib/public-key.d.ts.map +1 -0
- package/dist/lib/samples-loader.d.ts +31 -6
- package/dist/lib/samples-loader.d.ts.map +1 -1
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command, CommanderError, Option } from 'commander';
|
|
3
|
-
import { verifyReceipt, fetchIssuerConfig, parseIssuerConfig,
|
|
4
|
-
import { generateKeypair, base64urlEncode, sign,
|
|
3
|
+
import { issue, verifyLocal, verifyReceipt, fetchIssuerConfig, parseIssuerConfig, IssueError } from '@peac/protocol';
|
|
4
|
+
import { generateKeypair, base64urlEncode, decode, sign, jwkToPublicKeyBytes, canonicalize, sha256Hex as sha256Hex$1, validateKeypair, base64urlDecode, derivePublicKey } from '@peac/crypto';
|
|
5
5
|
import * as fs6 from 'fs';
|
|
6
6
|
import { writeFileSync, existsSync, readFileSync, openSync, constants, fstatSync, closeSync, statSync, unlinkSync, createReadStream } from 'fs';
|
|
7
7
|
import * as path6 from 'path';
|
|
@@ -253,8 +253,8 @@ policy.command("validate").description("Validate a PEAC policy file (YAML or JSO
|
|
|
253
253
|
);
|
|
254
254
|
} else {
|
|
255
255
|
console.error("Policy validation failed:\n");
|
|
256
|
-
for (const
|
|
257
|
-
console.error(` - ${
|
|
256
|
+
for (const issue4 of err.issues) {
|
|
257
|
+
console.error(` - ${issue4.path.join(".")}: ${issue4.message}`);
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
260
|
process.exit(1);
|
|
@@ -1886,95 +1886,79 @@ Use 'peac conformance list -c <category>' to see fixtures in a category.`);
|
|
|
1886
1886
|
});
|
|
1887
1887
|
var EMBEDDED_SAMPLES = [
|
|
1888
1888
|
{
|
|
1889
|
-
id: "basic-
|
|
1890
|
-
name: "Basic
|
|
1891
|
-
description: "Minimal valid PEAC
|
|
1889
|
+
id: "basic-record",
|
|
1890
|
+
name: "Basic Record",
|
|
1891
|
+
description: "Minimal valid PEAC signed interaction record",
|
|
1892
1892
|
category: "valid",
|
|
1893
|
-
|
|
1893
|
+
format: "issue-options",
|
|
1894
|
+
input: {
|
|
1894
1895
|
iss: "https://sandbox.peacprotocol.org",
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
// Placeholder - will be set at generation time
|
|
1898
|
-
exp: 0,
|
|
1899
|
-
// Placeholder - will be set at generation time
|
|
1900
|
-
rid: "sample-basic-001"
|
|
1896
|
+
kind: "evidence",
|
|
1897
|
+
type: "org.peacprotocol/access"
|
|
1901
1898
|
}
|
|
1902
1899
|
},
|
|
1903
1900
|
{
|
|
1904
|
-
id: "full-
|
|
1905
|
-
name: "Full
|
|
1906
|
-
description: "PEAC
|
|
1901
|
+
id: "full-record",
|
|
1902
|
+
name: "Full Record",
|
|
1903
|
+
description: "Valid PEAC signed interaction record with optional fields",
|
|
1907
1904
|
category: "valid",
|
|
1908
|
-
|
|
1905
|
+
format: "issue-options",
|
|
1906
|
+
input: {
|
|
1909
1907
|
iss: "https://sandbox.peacprotocol.org",
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
rid: "sample-full-001",
|
|
1915
|
-
purpose_declared: ["search", "index"],
|
|
1916
|
-
purpose_enforced: "search",
|
|
1917
|
-
purpose_reason: "allowed"
|
|
1908
|
+
kind: "evidence",
|
|
1909
|
+
type: "org.peacprotocol/access",
|
|
1910
|
+
sub: "agent:demo-agent",
|
|
1911
|
+
purpose_declared: "search"
|
|
1918
1912
|
}
|
|
1919
1913
|
},
|
|
1920
1914
|
{
|
|
1921
|
-
id: "
|
|
1922
|
-
name: "
|
|
1923
|
-
description: "
|
|
1915
|
+
id: "mcp-tool-run",
|
|
1916
|
+
name: "Mcp Tool Run",
|
|
1917
|
+
description: "Valid PEAC signed interaction record for an MCP tool run",
|
|
1924
1918
|
category: "valid",
|
|
1925
|
-
|
|
1919
|
+
format: "issue-options",
|
|
1920
|
+
input: {
|
|
1926
1921
|
iss: "https://sandbox.peacprotocol.org",
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
"org.peacprotocol/interaction@0.1": {
|
|
1934
|
-
version: "0.1",
|
|
1935
|
-
interaction_id: "int_sample_001",
|
|
1936
|
-
started_at: "2026-01-01T00:00:00.000Z",
|
|
1937
|
-
completed_at: "2026-01-01T00:00:01.000Z",
|
|
1938
|
-
outcome: { kind: "success" },
|
|
1939
|
-
input: { hash: "sha256:abc123...", byte_length: 1024 },
|
|
1940
|
-
output: { hash: "sha256:def456...", byte_length: 2048 }
|
|
1922
|
+
kind: "evidence",
|
|
1923
|
+
type: "org.peacprotocol/mcp",
|
|
1924
|
+
extensions: {
|
|
1925
|
+
"org.peacprotocol/mcp": {
|
|
1926
|
+
server: "demo",
|
|
1927
|
+
tool: "search"
|
|
1941
1928
|
}
|
|
1942
1929
|
}
|
|
1943
1930
|
}
|
|
1944
1931
|
},
|
|
1945
1932
|
{
|
|
1946
|
-
id: "payment-
|
|
1947
|
-
name: "Payment
|
|
1948
|
-
description: "
|
|
1933
|
+
id: "payment-event",
|
|
1934
|
+
name: "Payment Event",
|
|
1935
|
+
description: "Valid PEAC signed interaction record for a payment event",
|
|
1949
1936
|
category: "valid",
|
|
1950
|
-
|
|
1937
|
+
format: "issue-options",
|
|
1938
|
+
input: {
|
|
1951
1939
|
iss: "https://sandbox.peacprotocol.org",
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
reference: "pay_sample_001",
|
|
1961
|
-
amount: "100",
|
|
1962
|
-
currency: "USD"
|
|
1940
|
+
kind: "evidence",
|
|
1941
|
+
type: "org.peacprotocol/payment",
|
|
1942
|
+
extensions: {
|
|
1943
|
+
"org.peacprotocol/commerce": {
|
|
1944
|
+
payment_rail: "stripe",
|
|
1945
|
+
amount_minor: "1000",
|
|
1946
|
+
currency: "USD"
|
|
1947
|
+
}
|
|
1963
1948
|
}
|
|
1964
1949
|
}
|
|
1965
1950
|
},
|
|
1966
1951
|
{
|
|
1967
|
-
id: "
|
|
1968
|
-
name: "
|
|
1969
|
-
description: "
|
|
1952
|
+
id: "event-time-record",
|
|
1953
|
+
name: "Event Time Record",
|
|
1954
|
+
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.",
|
|
1970
1955
|
category: "valid",
|
|
1971
|
-
|
|
1956
|
+
format: "issue-options",
|
|
1957
|
+
input: {
|
|
1972
1958
|
iss: "https://sandbox.peacprotocol.org",
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
// Will be set to iat + 86400 at generation time
|
|
1977
|
-
rid: "sample-long-expiry-001"
|
|
1959
|
+
kind: "evidence",
|
|
1960
|
+
type: "org.peacprotocol/access",
|
|
1961
|
+
occurred_at: "2026-01-01T00:00:00.000Z"
|
|
1978
1962
|
}
|
|
1979
1963
|
},
|
|
1980
1964
|
{
|
|
@@ -1982,6 +1966,7 @@ var EMBEDDED_SAMPLES = [
|
|
|
1982
1966
|
name: "Expired Receipt",
|
|
1983
1967
|
description: "Receipt that has already expired (for testing rejection)",
|
|
1984
1968
|
category: "invalid",
|
|
1969
|
+
format: "legacy-claims",
|
|
1985
1970
|
claims: {
|
|
1986
1971
|
iss: "https://sandbox.peacprotocol.org",
|
|
1987
1972
|
aud: "https://example.com",
|
|
@@ -1998,6 +1983,7 @@ var EMBEDDED_SAMPLES = [
|
|
|
1998
1983
|
name: "Future IAT",
|
|
1999
1984
|
description: "Receipt with iat in the future (should be rejected)",
|
|
2000
1985
|
category: "invalid",
|
|
1986
|
+
format: "legacy-claims",
|
|
2001
1987
|
claims: {
|
|
2002
1988
|
iss: "https://sandbox.peacprotocol.org",
|
|
2003
1989
|
aud: "https://example.com",
|
|
@@ -2014,6 +2000,7 @@ var EMBEDDED_SAMPLES = [
|
|
|
2014
2000
|
name: "Missing Issuer",
|
|
2015
2001
|
description: "Receipt missing required iss claim (for testing validation)",
|
|
2016
2002
|
category: "invalid",
|
|
2003
|
+
format: "legacy-claims",
|
|
2017
2004
|
claims: {
|
|
2018
2005
|
aud: "https://example.com",
|
|
2019
2006
|
iat: 0,
|
|
@@ -2024,12 +2011,6 @@ var EMBEDDED_SAMPLES = [
|
|
|
2024
2011
|
}
|
|
2025
2012
|
];
|
|
2026
2013
|
function findSamplesDir(customPath) {
|
|
2027
|
-
if (customPath) {
|
|
2028
|
-
if (fs6.existsSync(customPath)) {
|
|
2029
|
-
return customPath;
|
|
2030
|
-
}
|
|
2031
|
-
return null;
|
|
2032
|
-
}
|
|
2033
2014
|
const cliPath = path6.resolve(__dirname, "../../../../specs/conformance/samples");
|
|
2034
2015
|
if (fs6.existsSync(cliPath)) {
|
|
2035
2016
|
return cliPath;
|
|
@@ -2047,13 +2028,46 @@ function findSamplesDir(customPath) {
|
|
|
2047
2028
|
function loadSampleFromFile(filePath, id, category) {
|
|
2048
2029
|
try {
|
|
2049
2030
|
const content = JSON.parse(fs6.readFileSync(filePath, "utf8"));
|
|
2050
|
-
const claims = content.payload ?? content.claims ?? content;
|
|
2051
2031
|
const description = content.$comment ?? content.description ?? `Sample ${id}`;
|
|
2032
|
+
const name = id.split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
|
|
2033
|
+
if (category === "valid") {
|
|
2034
|
+
if (content.format !== "issue-options" || content.input === null || typeof content.input !== "object" || Array.isArray(content.input)) {
|
|
2035
|
+
process.stderr.write(
|
|
2036
|
+
`samples-loader: skipping malformed valid sample '${id}' (expected format "issue-options" with an input object)
|
|
2037
|
+
`
|
|
2038
|
+
);
|
|
2039
|
+
return null;
|
|
2040
|
+
}
|
|
2041
|
+
return {
|
|
2042
|
+
id,
|
|
2043
|
+
name,
|
|
2044
|
+
description,
|
|
2045
|
+
category: "valid",
|
|
2046
|
+
format: "issue-options",
|
|
2047
|
+
input: content.input
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
if (content.format !== void 0 && content.format !== "legacy-claims") {
|
|
2051
|
+
process.stderr.write(
|
|
2052
|
+
`samples-loader: skipping '${id}' (unexpected format "${content.format}" for ${category} sample)
|
|
2053
|
+
`
|
|
2054
|
+
);
|
|
2055
|
+
return null;
|
|
2056
|
+
}
|
|
2057
|
+
const claims = content.payload ?? content.claims ?? content;
|
|
2058
|
+
if (claims === null || typeof claims !== "object" || Array.isArray(claims)) {
|
|
2059
|
+
process.stderr.write(
|
|
2060
|
+
`samples-loader: skipping malformed ${category} sample '${id}' (claims must be an object)
|
|
2061
|
+
`
|
|
2062
|
+
);
|
|
2063
|
+
return null;
|
|
2064
|
+
}
|
|
2052
2065
|
return {
|
|
2053
2066
|
id,
|
|
2054
|
-
name
|
|
2067
|
+
name,
|
|
2055
2068
|
description,
|
|
2056
2069
|
category,
|
|
2070
|
+
format: "legacy-claims",
|
|
2057
2071
|
claims,
|
|
2058
2072
|
header: content.header,
|
|
2059
2073
|
expectedError: content.expected_error
|
|
@@ -2094,12 +2108,15 @@ function loadSamplesFromDir(samplesDir) {
|
|
|
2094
2108
|
return samples2;
|
|
2095
2109
|
}
|
|
2096
2110
|
function getSamples(customSamplesPath) {
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
if (dirSamples.length > 0) {
|
|
2101
|
-
return dirSamples;
|
|
2111
|
+
if (customSamplesPath !== void 0) {
|
|
2112
|
+
if (!fs6.existsSync(customSamplesPath)) {
|
|
2113
|
+
throw new Error(`Samples directory not found: ${customSamplesPath}`);
|
|
2102
2114
|
}
|
|
2115
|
+
return loadSamplesFromDir(customSamplesPath);
|
|
2116
|
+
}
|
|
2117
|
+
const samplesDir = findSamplesDir();
|
|
2118
|
+
if (samplesDir) {
|
|
2119
|
+
return loadSamplesFromDir(samplesDir);
|
|
2103
2120
|
}
|
|
2104
2121
|
return EMBEDDED_SAMPLES;
|
|
2105
2122
|
}
|
|
@@ -2127,7 +2144,6 @@ function outputError3(error, details, opts) {
|
|
|
2127
2144
|
function applyTimeAdjustments(claims, sampleId, now) {
|
|
2128
2145
|
const adjusted = { ...claims };
|
|
2129
2146
|
const standardExpiry = 3600;
|
|
2130
|
-
const longExpiry = 86400;
|
|
2131
2147
|
switch (sampleId) {
|
|
2132
2148
|
case "expired":
|
|
2133
2149
|
adjusted.iat = now - 7200;
|
|
@@ -2137,10 +2153,6 @@ function applyTimeAdjustments(claims, sampleId, now) {
|
|
|
2137
2153
|
adjusted.iat = now + 3600;
|
|
2138
2154
|
adjusted.exp = now + 7200;
|
|
2139
2155
|
break;
|
|
2140
|
-
case "long-expiry":
|
|
2141
|
-
adjusted.iat = now;
|
|
2142
|
-
adjusted.exp = now + longExpiry;
|
|
2143
|
-
break;
|
|
2144
2156
|
default:
|
|
2145
2157
|
if (adjusted.iat === 0 || !adjusted.iat) {
|
|
2146
2158
|
adjusted.iat = now;
|
|
@@ -2151,10 +2163,19 @@ function applyTimeAdjustments(claims, sampleId, now) {
|
|
|
2151
2163
|
}
|
|
2152
2164
|
return adjusted;
|
|
2153
2165
|
}
|
|
2154
|
-
var samples = new Command("samples").description("Work with PEAC sample
|
|
2155
|
-
samples.command("list").description("List available sample
|
|
2166
|
+
var samples = new Command("samples").description("Work with PEAC sample records (v0.10.8+)").option("--json", "Output in JSON format");
|
|
2167
|
+
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) => {
|
|
2156
2168
|
const globalOpts = getGlobalOptions3(cmd);
|
|
2157
2169
|
try {
|
|
2170
|
+
if (options.category !== void 0 && !["valid", "invalid", "edge"].includes(options.category)) {
|
|
2171
|
+
outputError3(
|
|
2172
|
+
"Unknown sample category",
|
|
2173
|
+
{ category: options.category, supported: ["valid", "invalid", "edge"] },
|
|
2174
|
+
globalOpts
|
|
2175
|
+
);
|
|
2176
|
+
process.exitCode = 1;
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2158
2179
|
const allSamples = getSamples(options.samples);
|
|
2159
2180
|
let filteredSamples = allSamples;
|
|
2160
2181
|
if (options.category) {
|
|
@@ -2174,7 +2195,7 @@ samples.command("list").description("List available sample receipts").option("-c
|
|
|
2174
2195
|
}));
|
|
2175
2196
|
console.log(JSON.stringify({ samples: data }, null, 2));
|
|
2176
2197
|
} else {
|
|
2177
|
-
console.log("Available PEAC Sample
|
|
2198
|
+
console.log("Available PEAC Sample Records\n");
|
|
2178
2199
|
console.log("VALID SAMPLES:");
|
|
2179
2200
|
for (const s of filteredSamples.filter((x) => x.category === "valid")) {
|
|
2180
2201
|
console.log(` ${s.id}`);
|
|
@@ -2228,11 +2249,16 @@ samples.command("show").description("Show details of a specific sample").argumen
|
|
|
2228
2249
|
console.log(`Category: ${sample.category}`);
|
|
2229
2250
|
console.log(`Description: ${sample.description}
|
|
2230
2251
|
`);
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2252
|
+
if (sample.category === "valid") {
|
|
2253
|
+
console.log("Issue input (current PEAC signed interaction record):");
|
|
2254
|
+
console.log(JSON.stringify(sample.input, null, 2));
|
|
2255
|
+
} else {
|
|
2256
|
+
console.log("Claims (legacy rejection fixture):");
|
|
2257
|
+
console.log(JSON.stringify(sample.claims, null, 2));
|
|
2258
|
+
if (sample.expectedError) {
|
|
2259
|
+
console.log(`
|
|
2235
2260
|
Expected Error: ${sample.expectedError}`);
|
|
2261
|
+
}
|
|
2236
2262
|
}
|
|
2237
2263
|
}
|
|
2238
2264
|
process.exitCode = 0;
|
|
@@ -2241,10 +2267,81 @@ Expected Error: ${sample.expectedError}`);
|
|
|
2241
2267
|
process.exitCode = 1;
|
|
2242
2268
|
}
|
|
2243
2269
|
});
|
|
2244
|
-
samples.command("generate").description("Generate sample
|
|
2270
|
+
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(
|
|
2271
|
+
"--now <timestamp>",
|
|
2272
|
+
"Unix event time (seconds) for valid samples; sets occurred_at (must not be in the future)"
|
|
2273
|
+
).option("--kid <kid>", "Key ID to use").action(async (options, cmd) => {
|
|
2245
2274
|
const globalOpts = getGlobalOptions3(cmd);
|
|
2246
2275
|
try {
|
|
2247
2276
|
const outputDir = options.output;
|
|
2277
|
+
const allowedFormats = /* @__PURE__ */ new Set(["jws", "json"]);
|
|
2278
|
+
if (!allowedFormats.has(options.format)) {
|
|
2279
|
+
outputError3(
|
|
2280
|
+
"Unsupported sample format",
|
|
2281
|
+
{ format: options.format, supported: ["jws", "json"] },
|
|
2282
|
+
globalOpts
|
|
2283
|
+
);
|
|
2284
|
+
process.exitCode = 1;
|
|
2285
|
+
return;
|
|
2286
|
+
}
|
|
2287
|
+
const allowedCategories = /* @__PURE__ */ new Set(["valid", "invalid", "edge"]);
|
|
2288
|
+
if (options.category !== void 0 && !allowedCategories.has(options.category)) {
|
|
2289
|
+
outputError3(
|
|
2290
|
+
"Unknown sample category",
|
|
2291
|
+
{ category: options.category, supported: ["valid", "invalid", "edge"] },
|
|
2292
|
+
globalOpts
|
|
2293
|
+
);
|
|
2294
|
+
process.exitCode = 1;
|
|
2295
|
+
return;
|
|
2296
|
+
}
|
|
2297
|
+
let samplesToGenerate = getSamples(options.samples);
|
|
2298
|
+
if (options.category) {
|
|
2299
|
+
samplesToGenerate = samplesToGenerate.filter((s) => s.category === options.category);
|
|
2300
|
+
}
|
|
2301
|
+
if (samplesToGenerate.length === 0) {
|
|
2302
|
+
outputError3(
|
|
2303
|
+
"No samples selected to generate",
|
|
2304
|
+
{ category: options.category ?? null },
|
|
2305
|
+
globalOpts
|
|
2306
|
+
);
|
|
2307
|
+
process.exitCode = 1;
|
|
2308
|
+
return;
|
|
2309
|
+
}
|
|
2310
|
+
let eventTime;
|
|
2311
|
+
if (options.now !== void 0) {
|
|
2312
|
+
const parsed = Number(options.now);
|
|
2313
|
+
if (!Number.isInteger(parsed)) {
|
|
2314
|
+
outputError3(
|
|
2315
|
+
"--now must be an integer Unix timestamp (seconds)",
|
|
2316
|
+
{ now: options.now },
|
|
2317
|
+
globalOpts
|
|
2318
|
+
);
|
|
2319
|
+
process.exitCode = 1;
|
|
2320
|
+
return;
|
|
2321
|
+
}
|
|
2322
|
+
if (!Number.isFinite(new Date(parsed * 1e3).getTime())) {
|
|
2323
|
+
outputError3(
|
|
2324
|
+
"--now is outside the supported date range",
|
|
2325
|
+
{ now: options.now },
|
|
2326
|
+
globalOpts
|
|
2327
|
+
);
|
|
2328
|
+
process.exitCode = 1;
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
eventTime = parsed;
|
|
2332
|
+
}
|
|
2333
|
+
const generationNow = Math.floor(Date.now() / 1e3);
|
|
2334
|
+
const SKEW_SECONDS = 300;
|
|
2335
|
+
const generatesValid = samplesToGenerate.some((s) => s.category === "valid");
|
|
2336
|
+
if (eventTime !== void 0 && generatesValid && eventTime > generationNow + SKEW_SECONDS) {
|
|
2337
|
+
outputError3(
|
|
2338
|
+
"--now sets occurred_at (event time) for valid samples and must not be in the future",
|
|
2339
|
+
{ now: eventTime, wall_clock: generationNow, skew_seconds: SKEW_SECONDS },
|
|
2340
|
+
globalOpts
|
|
2341
|
+
);
|
|
2342
|
+
process.exitCode = 1;
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2248
2345
|
const validDir = path6.join(outputDir, "valid");
|
|
2249
2346
|
const invalidDir = path6.join(outputDir, "invalid");
|
|
2250
2347
|
const edgeDir = path6.join(outputDir, "edge");
|
|
@@ -2253,11 +2350,10 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2253
2350
|
fs6.mkdirSync(invalidDir, { recursive: true });
|
|
2254
2351
|
fs6.mkdirSync(edgeDir, { recursive: true });
|
|
2255
2352
|
fs6.mkdirSync(bundlesDir, { recursive: true });
|
|
2256
|
-
const now = options.now ? parseInt(options.now, 10) : Math.floor(Date.now() / 1e3);
|
|
2257
2353
|
const keyPair = await generateKeypair();
|
|
2258
2354
|
const publicKeyBytes = keyPair.publicKey;
|
|
2259
2355
|
const privateKeyBytes = keyPair.privateKey;
|
|
2260
|
-
const kid = options.kid || `sandbox-${new Date(
|
|
2356
|
+
const kid = options.kid || `sandbox-${new Date(generationNow * 1e3).toISOString().slice(0, 7)}`;
|
|
2261
2357
|
const publicJwk = {
|
|
2262
2358
|
kty: "OKP",
|
|
2263
2359
|
crv: "Ed25519",
|
|
@@ -2267,30 +2363,41 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2267
2363
|
alg: "EdDSA"
|
|
2268
2364
|
};
|
|
2269
2365
|
const generatedFiles = [];
|
|
2270
|
-
let samplesToGenerate = getSamples(options.samples);
|
|
2271
|
-
if (options.category) {
|
|
2272
|
-
samplesToGenerate = samplesToGenerate.filter((s) => s.category === options.category);
|
|
2273
|
-
}
|
|
2274
2366
|
for (const sample of samplesToGenerate) {
|
|
2275
2367
|
const targetDir = sample.category === "valid" ? validDir : sample.category === "edge" ? edgeDir : invalidDir;
|
|
2276
2368
|
const filename = `${sample.id}.${options.format === "json" ? "json" : "jws"}`;
|
|
2277
2369
|
const filepath = path6.join(targetDir, filename);
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
typ: "interaction-record+jwt",
|
|
2285
|
-
kid
|
|
2286
|
-
},
|
|
2287
|
-
payload: adjustedClaims,
|
|
2288
|
-
...sample.expectedError ? { expected_error: sample.expectedError } : {}
|
|
2370
|
+
if (sample.category === "valid") {
|
|
2371
|
+
const issueOptions = {
|
|
2372
|
+
...sample.input,
|
|
2373
|
+
privateKey: privateKeyBytes,
|
|
2374
|
+
kid,
|
|
2375
|
+
...eventTime !== void 0 ? { occurred_at: new Date(eventTime * 1e3).toISOString() } : {}
|
|
2289
2376
|
};
|
|
2290
|
-
|
|
2377
|
+
const { jws } = await issue(issueOptions);
|
|
2378
|
+
if (options.format === "json") {
|
|
2379
|
+
const { header, payload } = decode(jws);
|
|
2380
|
+
fs6.writeFileSync(
|
|
2381
|
+
filepath,
|
|
2382
|
+
JSON.stringify({ $comment: sample.description, header, payload }, null, 2)
|
|
2383
|
+
);
|
|
2384
|
+
} else {
|
|
2385
|
+
fs6.writeFileSync(filepath, jws);
|
|
2386
|
+
}
|
|
2291
2387
|
} else {
|
|
2292
|
-
const
|
|
2293
|
-
|
|
2388
|
+
const adjustedClaims = applyTimeAdjustments(sample.claims, sample.id, generationNow);
|
|
2389
|
+
if (options.format === "json") {
|
|
2390
|
+
const output2 = {
|
|
2391
|
+
$comment: sample.description,
|
|
2392
|
+
header: { alg: "EdDSA", typ: "interaction-record+jwt", kid },
|
|
2393
|
+
payload: adjustedClaims,
|
|
2394
|
+
...sample.expectedError ? { expected_error: sample.expectedError } : {}
|
|
2395
|
+
};
|
|
2396
|
+
fs6.writeFileSync(filepath, JSON.stringify(output2, null, 2));
|
|
2397
|
+
} else {
|
|
2398
|
+
const jws = await sign(adjustedClaims, privateKeyBytes, kid);
|
|
2399
|
+
fs6.writeFileSync(filepath, jws);
|
|
2400
|
+
}
|
|
2294
2401
|
}
|
|
2295
2402
|
generatedFiles.push(filepath);
|
|
2296
2403
|
}
|
|
@@ -2302,9 +2409,9 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2302
2409
|
generatedFiles.push(jwksPath);
|
|
2303
2410
|
const bundlePath = path6.join(bundlesDir, "offline-verification.json");
|
|
2304
2411
|
const bundle = {
|
|
2305
|
-
$comment: "Offline verification bundle with sample
|
|
2306
|
-
description: "
|
|
2307
|
-
generated_at: new Date(
|
|
2412
|
+
$comment: "Offline verification bundle with sample records and JWKS",
|
|
2413
|
+
description: "Sample record metadata and the sandbox public verification keys.",
|
|
2414
|
+
generated_at: new Date(generationNow * 1e3).toISOString(),
|
|
2308
2415
|
generator: {
|
|
2309
2416
|
name: "peac-cli",
|
|
2310
2417
|
version: getVersion()
|
|
@@ -2316,8 +2423,10 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2316
2423
|
category: s.category
|
|
2317
2424
|
})),
|
|
2318
2425
|
notes: [
|
|
2319
|
-
"
|
|
2320
|
-
"
|
|
2426
|
+
"Valid samples are issued through issue() and pass local verification (verifyLocal).",
|
|
2427
|
+
"Invalid samples are intentionally invalid rejection fixtures.",
|
|
2428
|
+
"For valid samples, --now sets the event time (occurred_at); iat is issuance time.",
|
|
2429
|
+
"Sandbox samples are for local testing and demonstration; do NOT use them in production."
|
|
2321
2430
|
]
|
|
2322
2431
|
};
|
|
2323
2432
|
fs6.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2));
|
|
@@ -2330,7 +2439,8 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2330
2439
|
output_dir: outputDir,
|
|
2331
2440
|
files_generated: generatedFiles.length,
|
|
2332
2441
|
files: generatedFiles,
|
|
2333
|
-
|
|
2442
|
+
generation_time: generationNow,
|
|
2443
|
+
event_time: eventTime ?? null,
|
|
2334
2444
|
kid
|
|
2335
2445
|
},
|
|
2336
2446
|
null,
|
|
@@ -2338,11 +2448,16 @@ samples.command("generate").description("Generate sample receipt files").require
|
|
|
2338
2448
|
)
|
|
2339
2449
|
);
|
|
2340
2450
|
} else {
|
|
2341
|
-
console.log(`Sample
|
|
2451
|
+
console.log(`Sample records generated successfully!
|
|
2342
2452
|
`);
|
|
2343
2453
|
console.log(`Output directory: ${outputDir}`);
|
|
2344
2454
|
console.log(`Files generated: ${generatedFiles.length}`);
|
|
2345
|
-
console.log(
|
|
2455
|
+
console.log(
|
|
2456
|
+
`Generation time: ${generationNow} (${new Date(generationNow * 1e3).toISOString()})`
|
|
2457
|
+
);
|
|
2458
|
+
if (eventTime !== void 0) {
|
|
2459
|
+
console.log(`Event time: ${eventTime} (${new Date(eventTime * 1e3).toISOString()})`);
|
|
2460
|
+
}
|
|
2346
2461
|
console.log(`Key ID: ${kid}
|
|
2347
2462
|
`);
|
|
2348
2463
|
console.log("Generated files:");
|
|
@@ -4794,15 +4909,128 @@ function emitCommand() {
|
|
|
4794
4909
|
emit.addCommand(emitLifecycleSubcommand());
|
|
4795
4910
|
return emit;
|
|
4796
4911
|
}
|
|
4912
|
+
function parsePublicKey(content) {
|
|
4913
|
+
let parsed;
|
|
4914
|
+
try {
|
|
4915
|
+
parsed = JSON.parse(content);
|
|
4916
|
+
} catch {
|
|
4917
|
+
throw new Error("public key file is not valid JSON");
|
|
4918
|
+
}
|
|
4919
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
4920
|
+
throw new Error("public key file must be a JWK or single-key JWKS object");
|
|
4921
|
+
}
|
|
4922
|
+
const obj = parsed;
|
|
4923
|
+
let jwk;
|
|
4924
|
+
if ("keys" in obj) {
|
|
4925
|
+
const keys = obj.keys;
|
|
4926
|
+
if (!Array.isArray(keys) || keys.length === 0) {
|
|
4927
|
+
throw new Error("JWKS contains no keys");
|
|
4928
|
+
}
|
|
4929
|
+
if (keys.length > 1) {
|
|
4930
|
+
throw new Error(
|
|
4931
|
+
"JWKS contains multiple keys; a single-key JWKS is required (multi-key JWKS is not supported)"
|
|
4932
|
+
);
|
|
4933
|
+
}
|
|
4934
|
+
const first = keys[0];
|
|
4935
|
+
if (first === null || typeof first !== "object" || Array.isArray(first)) {
|
|
4936
|
+
throw new Error("JWKS key entry is not a JWK object");
|
|
4937
|
+
}
|
|
4938
|
+
jwk = first;
|
|
4939
|
+
} else {
|
|
4940
|
+
jwk = obj;
|
|
4941
|
+
}
|
|
4942
|
+
if ("d" in jwk) {
|
|
4943
|
+
throw new Error("public key required: the file contains private key material");
|
|
4944
|
+
}
|
|
4945
|
+
const { kty, crv, x } = jwk;
|
|
4946
|
+
if (kty !== "OKP" || crv !== "Ed25519") {
|
|
4947
|
+
throw new Error('unsupported key: expected a public Ed25519 JWK (kty "OKP", crv "Ed25519")');
|
|
4948
|
+
}
|
|
4949
|
+
if (typeof x !== "string" || x.length === 0) {
|
|
4950
|
+
throw new Error('invalid JWK: missing public key value "x"');
|
|
4951
|
+
}
|
|
4952
|
+
try {
|
|
4953
|
+
return jwkToPublicKeyBytes({ kty: "OKP", crv: "Ed25519", x });
|
|
4954
|
+
} catch {
|
|
4955
|
+
throw new Error('invalid JWK: "x" is not a 32-byte base64url Ed25519 public key');
|
|
4956
|
+
}
|
|
4957
|
+
}
|
|
4797
4958
|
|
|
4798
4959
|
// src/index.ts
|
|
4960
|
+
var MAX_PUBLIC_KEY_FILE_BYTES = 16384;
|
|
4961
|
+
var MAX_VERIFY_JWS_FILE_BYTES = 512 * 1024;
|
|
4799
4962
|
var program = new Command();
|
|
4800
4963
|
program.name("peac").description("PEAC protocol command-line tools").version(getVersion()).exitOverride();
|
|
4801
|
-
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").
|
|
4964
|
+
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(
|
|
4965
|
+
"--public-key <path>",
|
|
4966
|
+
"Verify offline with a public Ed25519 JWK or single-key JWKS file (no network)"
|
|
4967
|
+
).action(async (jwsInput, options) => {
|
|
4802
4968
|
try {
|
|
4803
4969
|
let jws = jwsInput;
|
|
4804
4970
|
if (fs6.existsSync(jwsInput)) {
|
|
4805
|
-
|
|
4971
|
+
try {
|
|
4972
|
+
jws = readFileBufferSnapshot(jwsInput, { maxBytes: MAX_VERIFY_JWS_FILE_BYTES }).toString("utf8").trim();
|
|
4973
|
+
} catch (readErr) {
|
|
4974
|
+
const code = readErr.code;
|
|
4975
|
+
if (code === "E_PEAC_FILE_TOO_LARGE") {
|
|
4976
|
+
console.log(
|
|
4977
|
+
`Verification failed: receipt file exceeds ${MAX_VERIFY_JWS_FILE_BYTES} bytes`
|
|
4978
|
+
);
|
|
4979
|
+
} else if (code === "EISDIR") {
|
|
4980
|
+
console.log("Verification failed: receipt path is a directory");
|
|
4981
|
+
} else {
|
|
4982
|
+
console.log("Verification failed: could not read receipt file");
|
|
4983
|
+
}
|
|
4984
|
+
process.exitCode = 1;
|
|
4985
|
+
return;
|
|
4986
|
+
}
|
|
4987
|
+
}
|
|
4988
|
+
if (options.publicKey) {
|
|
4989
|
+
console.log("Verifying PEAC receipt offline...\n");
|
|
4990
|
+
let keyContent;
|
|
4991
|
+
try {
|
|
4992
|
+
keyContent = readFileBufferSnapshot(options.publicKey, {
|
|
4993
|
+
maxBytes: MAX_PUBLIC_KEY_FILE_BYTES
|
|
4994
|
+
}).toString("utf8");
|
|
4995
|
+
} catch (readErr) {
|
|
4996
|
+
const code = readErr.code;
|
|
4997
|
+
if (code === "E_PEAC_FILE_TOO_LARGE") {
|
|
4998
|
+
console.log(
|
|
4999
|
+
`Verification failed: public key file exceeds ${MAX_PUBLIC_KEY_FILE_BYTES} bytes`
|
|
5000
|
+
);
|
|
5001
|
+
} else if (code === "EISDIR") {
|
|
5002
|
+
console.log("Verification failed: public key path is a directory");
|
|
5003
|
+
} else {
|
|
5004
|
+
console.log("Verification failed: could not read public key file");
|
|
5005
|
+
}
|
|
5006
|
+
process.exitCode = 1;
|
|
5007
|
+
return;
|
|
5008
|
+
}
|
|
5009
|
+
let publicKey;
|
|
5010
|
+
try {
|
|
5011
|
+
publicKey = parsePublicKey(keyContent);
|
|
5012
|
+
} catch (keyErr) {
|
|
5013
|
+
console.log(
|
|
5014
|
+
`Verification failed: ${keyErr instanceof Error ? keyErr.message : "invalid public key"}`
|
|
5015
|
+
);
|
|
5016
|
+
process.exitCode = 1;
|
|
5017
|
+
return;
|
|
5018
|
+
}
|
|
5019
|
+
const localResult = await verifyLocal(jws, publicKey);
|
|
5020
|
+
if (localResult.valid) {
|
|
5021
|
+
console.log("Signature valid (offline).");
|
|
5022
|
+
console.log(
|
|
5023
|
+
" Verified the receipt signature and declared receipt structure against the supplied public key."
|
|
5024
|
+
);
|
|
5025
|
+
process.exitCode = 0;
|
|
5026
|
+
} else {
|
|
5027
|
+
console.log(`Verification failed: ${localResult.message}`);
|
|
5028
|
+
if (localResult.code) {
|
|
5029
|
+
console.log(` Code: ${localResult.code}`);
|
|
5030
|
+
}
|
|
5031
|
+
process.exitCode = 1;
|
|
5032
|
+
}
|
|
5033
|
+
return;
|
|
4806
5034
|
}
|
|
4807
5035
|
console.log("Verifying PEAC receipt...\n");
|
|
4808
5036
|
const { payload } = decode(jws);
|