@peac/cli 0.15.0 → 0.15.2
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.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
|
|
281
|
-
console.error(` - ${
|
|
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-
|
|
1914
|
-
name: "Basic
|
|
1915
|
-
description: "Minimal valid PEAC
|
|
1913
|
+
id: "basic-record",
|
|
1914
|
+
name: "Basic Record",
|
|
1915
|
+
description: "Minimal valid PEAC signed interaction record",
|
|
1916
1916
|
category: "valid",
|
|
1917
|
-
|
|
1917
|
+
format: "issue-options",
|
|
1918
|
+
input: {
|
|
1918
1919
|
iss: "https://sandbox.peacprotocol.org",
|
|
1919
|
-
|
|
1920
|
-
|
|
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-
|
|
1929
|
-
name: "Full
|
|
1930
|
-
description: "PEAC
|
|
1925
|
+
id: "full-record",
|
|
1926
|
+
name: "Full Record",
|
|
1927
|
+
description: "Valid PEAC signed interaction record with optional fields",
|
|
1931
1928
|
category: "valid",
|
|
1932
|
-
|
|
1929
|
+
format: "issue-options",
|
|
1930
|
+
input: {
|
|
1933
1931
|
iss: "https://sandbox.peacprotocol.org",
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
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: "
|
|
1946
|
-
name: "
|
|
1947
|
-
description: "
|
|
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
|
-
|
|
1943
|
+
format: "issue-options",
|
|
1944
|
+
input: {
|
|
1950
1945
|
iss: "https://sandbox.peacprotocol.org",
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
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-
|
|
1971
|
-
name: "Payment
|
|
1972
|
-
description: "
|
|
1957
|
+
id: "payment-event",
|
|
1958
|
+
name: "Payment Event",
|
|
1959
|
+
description: "Valid PEAC signed interaction record for a payment event",
|
|
1973
1960
|
category: "valid",
|
|
1974
|
-
|
|
1961
|
+
format: "issue-options",
|
|
1962
|
+
input: {
|
|
1975
1963
|
iss: "https://sandbox.peacprotocol.org",
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
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: "
|
|
1992
|
-
name: "
|
|
1993
|
-
description: "
|
|
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
|
-
|
|
1980
|
+
format: "issue-options",
|
|
1981
|
+
input: {
|
|
1996
1982
|
iss: "https://sandbox.peacprotocol.org",
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
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
|
|
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
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
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
|
|
2179
|
-
samples.command("list").description("List available sample
|
|
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
|
|
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
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
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
|
-
|
|
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
|
|
2317
|
-
|
|
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
|
|
2330
|
-
description: "
|
|
2331
|
-
generated_at: new Date(
|
|
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
|
-
"
|
|
2344
|
-
"
|
|
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
|
-
|
|
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
|
|
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(
|
|
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").
|
|
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
|
-
|
|
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);
|