diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index cf6c04c40..c915a8595 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -133,26 +133,29 @@ jobs: - name: Build all packages run: pnpm -w build - - name: Test core crypto functions + - name: Test @peac/crypto sign/verify runtime smoke run: | - echo "Testing @peac/core crypto functions with Node.js..." + echo "Testing @peac/crypto sign/verify round-trip with Node.js..." node --input-type=module -e " - const m = await import('./packages/core/dist/index.js'); - const { signDetached, verifyDetached, generateEdDSAKeyPair, validateKidFormat } = m; + const m = await import('./packages/crypto/dist/index.mjs'); + const { sign, verify, generateKeypair } = m; console.log('Testing with Node.js...'); - const { privateKey, publicKey, kid } = await generateEdDSAKeyPair(); + const { privateKey, publicKey } = await generateKeypair(); console.log('[OK] Key generation works'); - if (!validateKidFormat(kid)) throw new Error('Generated kid invalid format'); - console.log('[OK] Kid validation works'); - - const payload = 'test payload'; - const jws = await signDetached(payload, privateKey, kid); + const kid = 'nightly-test-key'; + const payload = { msg: 'test payload' }; + const jws = await sign(payload, privateKey, kid); + if (typeof jws !== 'string' || jws.split('.').length !== 3) { + throw new Error('sign() did not produce a compact JWS'); + } console.log('[OK] Signing works'); - const verified = await verifyDetached(payload, jws, publicKey); - if (!verified) throw new Error('Verification failed'); + const result = await verify(jws, publicKey); + if (!result || !result.payload || result.payload.msg !== payload.msg) { + throw new Error('Verification did not return the original payload'); + } console.log('[OK] Verification works'); console.log('[OK] Node.js runtime validation complete'); " diff --git a/AGENTS.md b/AGENTS.md index 604d1bb89..7a21c1f4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,12 +58,18 @@ The `PEAC-Receipt` header carries a compact JWS (never a bare `receipt_ref`). For A2A (Agent-to-Agent Protocol, Linux Foundation) discovery via `/.well-known/agent-card.json`: -**Agent Card (`capabilities.extensions[]` array, A2A v0.3.0 and v1.0.0):** +**Agent Card (`capabilities.extensions[]` array, A2A v1.0.0):** ```json { "name": "Example Agent", - "url": "https://agent.example", + "supportedInterfaces": [ + { + "url": "https://agent.example", + "protocolBinding": "http+json", + "protocolVersion": "1.0.0" + } + ], "capabilities": { "extensions": [ { @@ -93,7 +99,7 @@ For A2A (Agent-to-Agent Protocol, Linux Foundation) discovery via `/.well-known/ } ``` -The extension URI key maps to a nested object containing the carrier array. This follows the A2A metadata convention. Both v0.3.0 and v1.0.0 Agent Card shapes are accepted. +The extension URI key maps to a nested object containing the carrier array. This follows the A2A metadata convention. A2A v1.0.0 Agent Card shape is required; v0.3.0 compatibility was removed in v0.13.0 (DD-186). Cards without a valid `supportedInterfaces[0].url` are rejected. ## Discovery diff --git a/REPO_SURFACE_STATUS.json b/REPO_SURFACE_STATUS.json index 8e1548293..4fffb48c3 100644 --- a/REPO_SURFACE_STATUS.json +++ b/REPO_SURFACE_STATUS.json @@ -425,15 +425,6 @@ "layer": 5, "published": false }, - "packages/core": { - "npm": "@peac/core", - "state": "deprecated", - "wire": "0.9", - "layer": "legacy", - "published": true, - "removal": "v0.13.0", - "note": "DEPRECATED. Archival of packages/core/ is coupled with the legacy /verify handler rewire that eliminates the last active consumer in apps/api/src/verifier.ts. Historical 0.9-series receipt verify-only path; use @peac/protocol + @peac/schema + @peac/crypto + @peac/kernel for current wire." - }, "packages/sdk-js": { "npm": "@peac/sdk", "state": "archived", @@ -574,6 +565,11 @@ "layer": 6, "published": false, "note": "ARCHIVED in v0.13.0. Near-empty scaffold (version constant + doc comment) never published." + }, + "archive/0.9.0-0.9.14/packages-core": { + "state": "archived", + "wire": "0.1", + "note": "ARCHIVED in v0.13.0. Source moved from packages/core/ to archive/0.9.0-0.9.14/packages-core/. Historical 0.9-series peac.receipt/0.9 verify-only path. Not published at v0.13.0 or later. Historical npm versions <=0.9.14 remain installable (deprecate-then-remove discipline)." } } } diff --git a/SECURITY.md b/SECURITY.md index db0b1680a..856271762 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -40,7 +40,7 @@ kept in the loop and the disclosure calendar is coordinated. | `v0.12.x` | Active | Wire 0.2 (`interaction-record+jwt`) | Through the `v0.13.x` line | | `v0.11.x` | Maintenance (security fixes only) | Wire 0.1 (`peac-receipt/0.1`) | 6 months after `v0.13.0` ships | | `v0.10.x` and earlier | End of life | Wire 0.1 and earlier | No further updates | -| `peac.receipt/0.9` archival path | Verify-only through `@peac/core` | `peac.receipt/0.9` | Frozen; removal at `v0.13.0` | +| `peac.receipt/0.9` archival path | Historical verify-only | `peac.receipt/0.9` | Archived at `v0.13.0` | See [Compatibility matrix](docs/COMPATIBILITY_MATRIX.md) for full runtime and wire-format compatibility, and [Security operations](docs/SECURITY-OPERATIONS.md) diff --git a/apps/api/package.json b/apps/api/package.json index d8ded81d9..6399192db 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -30,7 +30,6 @@ }, "dependencies": { "@hono/node-server": "^1.19.13", - "@peac/core": "workspace:*", "@peac/crypto": "workspace:*", "@peac/disc": "workspace:*", "@peac/jwks-cache": "workspace:*", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 58b691fb6..66e8e457c 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,7 +5,6 @@ import { Hono } from 'hono'; import { serve } from '@hono/node-server'; -import { createV13HonoHandler } from './routes.js'; import { createVerifyV1Handler } from './verify-v1.js'; import { createIssueV1Handler } from './hosted-issue.js'; import { createIssuerHealthHandler } from './issuer-health.js'; @@ -34,11 +33,6 @@ export type { HttpStatus, } from './types.js'; -// Legacy enhanced verifier (deprecated -- kept for backwards compat, will be removed) -export { VerifierV13 } from './verifier.js'; -export { createV13ExpressHandler, createV13HonoHandler } from './routes.js'; -export type { V13VerifyRequest, V13VerifyResponse, VerifierOptions } from './verifier.js'; - // v1 verify endpoint export { createVerifyV1Handler, resetVerifyV1RateLimit } from './verify-v1.js'; @@ -68,6 +62,35 @@ export const PROBLEM_TYPES = { MISCONFIGURED_VERIFIER: 'https://www.peacprotocol.org/problems/misconfigured-verifier', } as const; +/** + * Deprecation headers for the legacy `POST /verify` alias and any other + * route that predates `POST /v1/verify`. The alias keeps serving valid + * `/v1/verify`-shaped responses, but every response carries an RFC 9745 + * `Deprecation` marker, an RFC 8594 `Sunset` date, and an RFC 8288 + * `Link` relation pointing at the migration guide. + */ +export const LEGACY_VERIFY_DEPRECATION_HEADERS = { + Deprecation: 'true', + Sunset: 'Sat, 01 Nov 2026 00:00:00 GMT', + Link: '; rel="deprecation"', +} as const; + +/** + * Build a Hono handler that stamps `LEGACY_VERIFY_DEPRECATION_HEADERS` on + * the response and then delegates to the canonical `/v1/verify` handler. + * Used by both the legacy `POST /verify` route and the `POST /api/v1/verify` + * alias. Production routing and tests both go through this helper so the + * two cannot drift on header set, header values, or delegation target. + */ +export function createLegacyVerifyAliasHandler(verifyV1: ReturnType) { + return (c: Parameters[0]) => { + for (const [key, value] of Object.entries(LEGACY_VERIFY_DEPRECATION_HEADERS)) { + c.header(key, value); + } + return verifyV1(c); + }; +} + // HTTP Server (when run as application) if (import.meta.url === `file://${process.argv[1]}`) { const app = new Hono(); @@ -96,20 +119,16 @@ if (import.meta.url === `file://${process.argv[1]}`) { // Health check endpoint app.get('/health', (c) => c.json({ ok: true })); - // Legacy verify endpoint (deprecated: removal target v0.13.0; see Sunset header) - app.post('/verify', createV13HonoHandler()); - // Canonical v1 verify endpoint const verifyV1 = createVerifyV1Handler(); app.post('/v1/verify', verifyV1); - // Deprecated alias (Sunset: Nov 1 2026) - app.post('/api/v1/verify', (c) => { - c.header('Sunset', 'Sat, 01 Nov 2026 00:00:00 GMT'); - c.header('Deprecation', 'true'); - c.header('Link', '; rel="deprecation"'); - return verifyV1(c); - }); + // Legacy verify endpoint. Kept runtime-reachable through the advertised + // Sunset date. The alias delegates in-process to the canonical v1 + // handler and stamps deprecation headers on every response. + const legacyVerifyAlias = createLegacyVerifyAliasHandler(verifyV1); + app.post('/verify', legacyVerifyAlias); + app.post('/api/v1/verify', legacyVerifyAlias); // Provisional v1 issue endpoint (BYO-key, disable via PEAC_HOSTED_ISSUE=false) app.post('/v1/issue', createIssueV1Handler()); diff --git a/apps/api/src/peac-core.d.ts b/apps/api/src/peac-core.d.ts deleted file mode 100644 index 32da4141f..000000000 --- a/apps/api/src/peac-core.d.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Ambient type declarations for @peac/core (deprecated compat-only package). - * - * @peac/core may not emit .d.ts files reliably in CI. This file provides - * the minimal type surface needed by the legacy verifier.ts so that - * typecheck:apps can run as a blocking CI gate. - * - * Remove this file when apps/api/src/verifier.ts is migrated away from - * @peac/core (tracked: @peac/core removal target is v0.13.0). - */ -declare module '@peac/core' { - export interface VerifyKeySet { - [kid: string]: Uint8Array; - } - - export interface VerifyResult { - valid: boolean; - claims?: Record; - payload?: Record; - kid?: string; - error?: string; - } - - export function verifyReceipt( - jws: string, - keys: VerifyKeySet, - options?: Record - ): Promise; - - export function canonicalPolicyHash(policy: unknown): Promise; -} diff --git a/apps/api/src/routes.ts b/apps/api/src/routes.ts deleted file mode 100644 index 1b3e994d1..000000000 --- a/apps/api/src/routes.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * API routes for v0.9.13.1 enhanced verifier (deprecated) - */ - -import { VerifierV13 } from './verifier.js'; - -const verifier = new VerifierV13(); - -/** - * Centralized deprecation header values for the legacy /verify endpoint. - * RFC 8594 (Sunset), RFC 8288 (Link rel="deprecation"). - */ -const LEGACY_DEPRECATION_HEADERS = { - Sunset: 'Sat, 01 Nov 2026 00:00:00 GMT', - Deprecation: 'true', - Link: '; rel="deprecation"', -} as const; - -// Express.js route handler -export function createV13ExpressHandler() { - return async (req: any, res: any) => { - // Extract observability headers - const requestId = req.headers['x-request-id']; - const traceParent = req.headers['traceparent']; - - const result = await verifier.verify(req.body, { - allowPrivateNet: process.env.PEAC_ALLOW_PRIVATE_NET === 'true', - timeout: 250, // Total ≤ 250ms per spec - maxInputSize: 256 * 1024, // 256 KiB - maxRedirects: 3, - requestId, - traceId: traceParent, - }); - - res.status(result.status); - - if (result.status !== 200) { - res.set('Content-Type', 'application/problem+json'); - } else { - res.set('Content-Type', 'application/json'); - } - - // Add required security headers - res.set('X-Content-Type-Options', 'nosniff'); - res.set('Cache-Control', 'no-store'); - res.set('Vary', 'PEAC-Receipt'); - res.set('Referrer-Policy', 'no-referrer'); - - // RFC 8594 deprecation headers - for (const [k, v] of Object.entries(LEGACY_DEPRECATION_HEADERS)) res.set(k, v); - - res.json(result.body); - }; -} - -// Hono route handler -export function createV13HonoHandler() { - return async (c: any) => { - // Extract observability headers - const requestId = c.req.header('x-request-id'); - const traceParent = c.req.header('traceparent'); - - const body = await c.req.json(); - const result = await verifier.verify(body, { - allowPrivateNet: process.env.PEAC_ALLOW_PRIVATE_NET === 'true', - timeout: 250, // Total ≤ 250ms per spec - maxInputSize: 256 * 1024, - maxRedirects: 3, - requestId, - traceId: traceParent, - }); - - const headers = { - 'Content-Type': result.status === 200 ? 'application/json' : 'application/problem+json', - 'X-Content-Type-Options': 'nosniff', - 'Cache-Control': 'no-store', - Vary: 'PEAC-Receipt', - 'Referrer-Policy': 'no-referrer', - // RFC 8594 deprecation headers - ...LEGACY_DEPRECATION_HEADERS, - }; - - return c.json(result.body, result.status, headers); - }; -} diff --git a/apps/api/src/verifier.ts b/apps/api/src/verifier.ts deleted file mode 100644 index 6b3d0b358..000000000 --- a/apps/api/src/verifier.ts +++ /dev/null @@ -1,596 +0,0 @@ -/** - * Enhanced verifier implementation for v0.9.13.1 spec - * POST /verify {receipt, resource} → {valid, claims, policyHash, reconstructed, inputs, timing} - */ - -import { promises as dns } from 'node:dns'; -import { verifyReceipt, canonicalPolicyHash } from '@peac/core'; -import type { VerifyKeySet } from '@peac/core'; -import { discover } from '@peac/disc'; -import type { HttpStatus } from './types.js'; -import { PROBLEM_TYPES } from './index.js'; - -export interface V13VerifyRequest { - receipt: string; - resource?: string; -} - -export interface V13VerifyResponse { - valid: boolean; - claims?: any; - policyHash?: string; - reconstructed?: { - hash?: string; - matches?: boolean; - }; - inputs?: Array<{ - type: 'aipref' | 'agent-permissions' | 'peac.txt'; - url: string; - etag?: string | null; - }>; - timing: { - total_ms: number; - fetch_ms: number; - hash_ms: number; - }; - meta?: { - request_id?: string; - trace_id?: string; - }; -} - -export interface VerifierOptions { - timeout?: number; - allowPrivateNet?: boolean; - maxInputSize?: number; - maxRedirects?: number; - requestId?: string; - traceId?: string; -} - -export class VerifierV13 { - private cache = new Map(); - - async verify( - request: V13VerifyRequest, - options: VerifierOptions = {} - ): Promise<{ status: HttpStatus; body: V13VerifyResponse | any }> { - const startTime = Date.now(); - let fetchTime = 0; - let hashTime = 0; - - const buildTiming = () => ({ - total_ms: Date.now() - startTime, - fetch_ms: fetchTime, - hash_ms: hashTime, - }); - - const buildMeta = () => ({ - ...(options.requestId && { request_id: options.requestId }), - ...(options.traceId && { trace_id: options.traceId }), - }); - - try { - // Validate request - if (!request.receipt || typeof request.receipt !== 'string') { - return { - status: 400, - body: { - type: PROBLEM_TYPES.INVALID_REQUEST, - title: 'Invalid Request', - status: 400, - detail: 'receipt field is required and must be a string', - timing: buildTiming(), - meta: buildMeta(), - }, - }; - } - - // Parse verification keys with fail-closed logic - function parseKeyset(env = process.env.PEAC_VERIFY_KEYS): VerifyKeySet { - if (!env) return {}; - try { - const ks = JSON.parse(env); - return ks && typeof ks === 'object' ? (ks as VerifyKeySet) : {}; - } catch { - return {}; // invalid JSON - } - } - - const keys = parseKeyset(); - if (!keys || Object.keys(keys).length === 0) { - return { - status: 422, - body: { - type: PROBLEM_TYPES.MISCONFIGURED_VERIFIER, - title: 'Missing Verification Keys', - status: 422, - detail: 'PEAC_VERIFY_KEYS is not set or invalid.', - timing: buildTiming(), - meta: buildMeta(), - }, - }; - } - - // Verify receipt signature using v0.9.14 core function - let payload; - try { - ({ payload } = await verifyReceipt(request.receipt, keys)); - } catch (error) { - const errorMsg = error instanceof Error ? error.message : 'Unknown error'; - - // Map specific error types to problem types - if (errorMsg.includes('Expired receipt')) { - return { - status: 422, - body: { - type: PROBLEM_TYPES.EXPIRED_RECEIPT, - title: 'Expired Receipt', - status: 422, - detail: errorMsg, - timing: buildTiming(), - meta: buildMeta(), - }, - }; - } - - return { - status: 422, - body: { - type: PROBLEM_TYPES.INVALID_SIGNATURE, - title: 'Invalid Signature', - status: 422, - detail: errorMsg, - timing: buildTiming(), - meta: buildMeta(), - }, - }; - } - - const response: V13VerifyResponse = { - valid: true, // reaching here means verification passed - claims: payload, - timing: buildTiming(), - meta: buildMeta(), - }; - - // If resource is provided, discover policies and recompute hash - if (request.resource) { - try { - const fetchStart = Date.now(); - await this.addPolicyValidation(request.resource, response, options); - fetchTime = Date.now() - fetchStart; - - // Update timing with fetch time - response.timing = buildTiming(); - } catch (error) { - // Policy validation errors don't invalidate the receipt itself - response.reconstructed = { - hash: '', - matches: false, - }; - response.timing = buildTiming(); - } - } - - return { - status: 200, - body: response, - }; - } catch (error) { - return { - status: 500, - body: { - type: PROBLEM_TYPES.PROCESSING_ERROR, - title: 'Processing Error', - status: 500, - detail: error instanceof Error ? error.message : 'Unknown error', - timing: buildTiming(), - meta: buildMeta(), - }, - }; - } - } - - private async addPolicyValidation( - resource: string, - response: V13VerifyResponse, - options: VerifierOptions - ) { - const inputs: V13VerifyResponse['inputs'] = []; - const totalTimeout = Math.min(options.timeout || 250, 250); // Total ≤ 250ms - const startTime = Date.now(); - - // Apply SSRF guards - if (!(await this.isAllowedUrl(resource, options))) { - throw new Error('URL not allowed by security policy'); - } - - // Helper to check remaining time budget - const checkTimeLimit = () => { - if (Date.now() - startTime > totalTimeout) { - throw new Error('Total time budget exceeded'); - } - }; - - // Discover peac.txt with caching. peac.txt is a policy-document surface - // per docs/specs/PEAC-TXT.md; this verifier only records the observation - // (URL + etag) and does NOT read key-discovery fields from the parsed - // result. Key resolution uses /.well-known/peac-issuer.json -> jwks_uri - // -> JWKS elsewhere in the verify path. - try { - checkTimeLimit(); - const peacUrl = new URL('/.well-known/peac.txt', resource).toString(); - const cached = this.getCachedResult(peacUrl); - - if (cached) { - inputs.push({ - type: 'peac.txt', - url: peacUrl, - etag: cached.etag || null, - }); - } else { - const peacResult = await discover(resource); - this.setCachedResult(peacUrl, peacResult, null); - inputs.push({ - type: 'peac.txt', - url: peacUrl, - etag: null, - }); - } - } catch { - inputs.push({ - type: 'peac.txt', - url: new URL('/.well-known/peac.txt', resource).toString(), - etag: null, - }); - } - - // Check AIPREF headers with caching - try { - checkTimeLimit(); - const cached = this.getCachedResult(resource); - - let aiprefResult; - let etag = null; - - if (cached) { - // Use cached result and send If-None-Match if ETag available - const headers = cached.etag ? { 'If-None-Match': cached.etag } : undefined; - try { - aiprefResult = await this.fetchWithLimits(resource, { - method: 'HEAD', - timeout: 150, - headers, - }); - if (aiprefResult.status === 304) { - // Not modified, use cached data - aiprefResult = cached.data; - etag = cached.etag; - } else { - // Updated, store new result - etag = aiprefResult.headers.get('etag'); - this.setCachedResult(resource, aiprefResult, etag); - } - } catch { - // Fallback to cached data on fetch error - aiprefResult = cached.data; - etag = cached.etag; - } - } else { - aiprefResult = await this.fetchWithLimits(resource, { - method: 'HEAD', - timeout: 150, - }); - etag = aiprefResult.headers.get('etag'); - this.setCachedResult(resource, aiprefResult, etag); - } - - inputs.push({ - type: 'aipref', - url: resource, - etag, - }); - } catch { - inputs.push({ - type: 'aipref', - url: resource, - etag: null, - }); - } - - // Check agent-permissions with caching - try { - checkTimeLimit(); - const htmlCacheKey = `${resource}:html`; - const cached = this.getCachedResult(htmlCacheKey); - - let htmlResponse; - let etag = null; - - if (cached) { - // Use cached result and send If-None-Match if ETag available - const headers = cached.etag ? { 'If-None-Match': cached.etag } : undefined; - try { - htmlResponse = await this.fetchWithLimits(resource, { - timeout: 150, - headers, - }); - if (htmlResponse.status === 304) { - // Not modified, use cached data - htmlResponse = cached.data; - etag = cached.etag; - } else { - // Updated, store new result - etag = htmlResponse.headers.get('etag'); - this.setCachedResult(htmlCacheKey, htmlResponse, etag); - } - } catch { - // Fallback to cached data on fetch error - htmlResponse = cached.data; - etag = cached.etag; - } - } else { - htmlResponse = await this.fetchWithLimits(resource, { - timeout: 150, - }); - etag = htmlResponse.headers.get('etag'); - this.setCachedResult(htmlCacheKey, htmlResponse, etag); - } - - const html = await htmlResponse.text(); - const linkMatch = html.match( - /]*rel=["']agent-permissions["'][^>]*href=["']([^"']+)["']/i - ); - - if (linkMatch) { - const href = linkMatch[1]; - const absoluteUrl = new URL(href, resource).toString(); - inputs.push({ - type: 'agent-permissions', - url: absoluteUrl, - etag, - }); - } else { - inputs.push({ - type: 'agent-permissions', - url: resource, - etag: null, - }); - } - } catch { - inputs.push({ - type: 'agent-permissions', - url: resource, - etag: null, - }); - } - - response.inputs = inputs; - - // Recompute policy hash from discovered inputs with timing - const hashStart = Date.now(); - try { - const mockPolicy = { - resource, - inputs: inputs, - discovered_at: new Date().toISOString(), - }; - - const recomputedHash = await canonicalPolicyHash(mockPolicy); - response.policyHash = recomputedHash; - - // Compare with receipt policy_hash if present - if (response.claims?.policy_hash) { - response.reconstructed = { - hash: recomputedHash, - matches: response.claims.policy_hash === recomputedHash, - }; - } - } catch (error) { - response.reconstructed = { - hash: '', - matches: false, - }; - } - - // Update hash timing in the parent context - if (response.timing) { - response.timing.hash_ms = Date.now() - hashStart; - } - } - - private getCachedResult(key: string) { - const cached = this.cache.get(key); - if (!cached) return null; - if (Date.now() >= cached.expires) { - this.cache.delete(key); - return null; - } - return cached; // { data, etag, expires } - } - - private setCachedResult(key: string, data: any, etag: string | null) { - this.cache.set(key, { data, etag: etag || undefined, expires: Date.now() + 5 * 60 * 1000 }); - } - - private async isAllowedUrl(url: string, options: VerifierOptions): Promise { - try { - const parsed = new URL(url); - - // Scheme allowlist: https only; http only on loopback - if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { - return false; - } - - if (parsed.protocol === 'http:') { - const hostname = parsed.hostname.toLowerCase(); - // Only allow http for loopback addresses - if (hostname !== 'localhost' && hostname !== '127.0.0.1' && hostname !== '::1') { - return false; - } - } - - // Resolve and block private/link-local after DNS - if (!options.allowPrivateNet) { - try { - const addrs = await dns.lookup(parsed.hostname, { all: true }); - for (const addr of addrs) { - if (await this.isIpPrivate(addr.address)) { - return false; - } - } - } catch { - // DNS resolution failed - return false; - } - } - - return true; - } catch { - return false; - } - } - - private async isIpPrivate(addr: string): Promise { - // IPv4 private ranges - const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/; - const ipv4Match = addr.match(ipv4Regex); - - if (ipv4Match) { - const [, a, b, c, d] = ipv4Match.map(Number); - - // Loopback: 127.0.0.0/8 - if (a === 127) return true; - // Private ranges: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - if (a === 10) return true; - if (a === 172 && b >= 16 && b <= 31) return true; - if (a === 192 && b === 168) return true; - // Link-local: 169.254.0.0/16 - if (a === 169 && b === 254) return true; - - return false; - } - - // IPv6 ranges - if (addr.includes(':')) { - const lower = addr.toLowerCase(); - // Loopback: ::1 - if (lower === '::1' || lower === '0:0:0:0:0:0:0:1') return true; - // ULA: fc00::/7 - if (lower.startsWith('fc') || lower.startsWith('fd')) return true; - // Link-local: fe80::/10 - if ( - lower.startsWith('fe8') || - lower.startsWith('fe9') || - lower.startsWith('fea') || - lower.startsWith('feb') - ) - return true; - } - - return false; - } - - private async fetchWithLimits( - url: string, - options: { - method?: string; - timeout?: number; - maxRedirects?: number; - headers?: Record; - } = {} - ) { - const perFetch = Math.min(options.timeout || 150, 150); - const maxSize = 256 * 1024; // 256 KiB - const maxRedirects = Math.min(options.maxRedirects || 3, 3); - - let current = url; - let redirects = 0; - - for (;;) { - // Validate current URL with DNS resolution - if (!(await this.isAllowedUrl(current, {}))) { - throw new Error('URL not allowed'); - } - - const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), perFetch); - - try { - const response = await fetch(current, { - method: options.method || 'GET', - redirect: 'manual', - signal: controller.signal, - headers: { - 'User-Agent': 'PEAC-Verifier/0.9.13.1', - Accept: 'text/html,application/json,text/plain,*/*;q=0.8', - 'Cache-Control': 'no-cache', - ...options.headers, - }, - }); - - clearTimeout(timeoutId); - - // Handle 3xx redirects with same-scheme validation and DNS re-check - if (response.status >= 300 && response.status < 400) { - const location = response.headers.get('location'); - if (!location) throw new Error('Redirect without Location'); - - const next = new URL(location, current).toString(); - const fromScheme = new URL(current).protocol; - const toScheme = new URL(next).protocol; - if (fromScheme !== toScheme) throw new Error('Cross-scheme redirect blocked'); - - if (++redirects > maxRedirects) throw new Error('Too many redirects'); - current = next; - continue; - } - - // Enforce size limit even without Content-Length by streaming - if (response.body) { - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - let totalRead = 0; - - try { - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - - if (value) { - totalRead += value.byteLength; - if (totalRead > maxSize) { - throw new Error('Response too large'); - } - chunks.push(value); - } - } - - // Reconstruct the response with the buffered body - const totalLength = chunks.reduce((acc, chunk) => acc + chunk.byteLength, 0); - const body = new Uint8Array(totalLength); - let offset = 0; - for (const chunk of chunks) { - body.set(chunk, offset); - offset += chunk.byteLength; - } - - return new Response(body, { - status: response.status, - statusText: response.statusText, - headers: response.headers, - }); - } finally { - reader.releaseLock(); - } - } - - return response; - } catch (error) { - clearTimeout(timeoutId); - throw error; - } - } - } -} diff --git a/apps/api/tests/legacy-verify-alias-headers.test.ts b/apps/api/tests/legacy-verify-alias-headers.test.ts new file mode 100644 index 000000000..06e5cc7d6 --- /dev/null +++ b/apps/api/tests/legacy-verify-alias-headers.test.ts @@ -0,0 +1,93 @@ +/** + * Legacy /verify alias: deprecation-header contract. + * + * The reference verifier keeps `POST /verify` runtime-reachable as a + * deprecated compatibility alias. Every response (success or error) + * MUST carry: + * + * - `Deprecation: true` (RFC 9745) + * - `Sunset: Sat, 01 Nov 2026 00:00:00 GMT` (RFC 8594) + * - `Link: ; rel="deprecation"` (RFC 8288) + * + * The same is true for the `/api/v1/verify` alias. This test exercises + * the alias routes directly in a minimal Hono app that wires the alias + * handler the same way the real server does. + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Hono } from 'hono'; +import { + createVerifyV1Handler, + resetVerifyV1RateLimit, + isProblemError, + PROBLEM_MEDIA_TYPE, + LEGACY_VERIFY_DEPRECATION_HEADERS, + createLegacyVerifyAliasHandler, +} from '../src/index.js'; + +function buildApp() { + resetVerifyV1RateLimit(); + const app = new Hono(); + app.onError((err, c) => { + if (isProblemError(err)) { + c.header('Content-Type', PROBLEM_MEDIA_TYPE); + return c.body(JSON.stringify(err.toProblemDetails()), err.status); + } + return c.json({ title: 'Internal Server Error', status: 500 }, 500); + }); + const verifyV1 = createVerifyV1Handler(); + const legacyVerifyAlias = createLegacyVerifyAliasHandler(verifyV1); + app.post('/v1/verify', verifyV1); + app.post('/verify', legacyVerifyAlias); + app.post('/api/v1/verify', legacyVerifyAlias); + return app; +} + +function request(path: string, body: unknown) { + return new Request(`http://localhost${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('legacy /verify alias: deprecation headers', () => { + let app: ReturnType; + + beforeAll(() => { + app = buildApp(); + }); + + it('exposes the full deprecation header set as a named constant', () => { + expect(LEGACY_VERIFY_DEPRECATION_HEADERS.Deprecation).toBe('true'); + expect(LEGACY_VERIFY_DEPRECATION_HEADERS.Sunset).toBe('Sat, 01 Nov 2026 00:00:00 GMT'); + expect(LEGACY_VERIFY_DEPRECATION_HEADERS.Link).toBe( + '; rel="deprecation"' + ); + }); + + it('stamps all three headers on a /verify error response', async () => { + const res = await app.request(request('/verify', { receipt: 'not-a-jws' })); + expect(res.headers.get('Deprecation')).toBe('true'); + expect(res.headers.get('Sunset')).toBe('Sat, 01 Nov 2026 00:00:00 GMT'); + expect(res.headers.get('Link')).toBe( + '; rel="deprecation"' + ); + }); + + it('stamps all three headers on an /api/v1/verify error response', async () => { + const res = await app.request(request('/api/v1/verify', { receipt: 'not-a-jws' })); + expect(res.headers.get('Deprecation')).toBe('true'); + expect(res.headers.get('Sunset')).toBe('Sat, 01 Nov 2026 00:00:00 GMT'); + expect(res.headers.get('Link')).toBe( + '; rel="deprecation"' + ); + }); + + it('canonical /v1/verify response does NOT carry deprecation headers', async () => { + const res = await app.request(request('/v1/verify', { receipt: 'not-a-jws' })); + expect(res.headers.get('Deprecation')).toBeNull(); + expect(res.headers.get('Sunset')).toBeNull(); + expect(res.headers.get('Link')).toBeNull(); + }); +}); diff --git a/apps/api/tests/legacy-verify-alias-pre-sunset.test.ts b/apps/api/tests/legacy-verify-alias-pre-sunset.test.ts new file mode 100644 index 000000000..4231a7bfb --- /dev/null +++ b/apps/api/tests/legacy-verify-alias-pre-sunset.test.ts @@ -0,0 +1,107 @@ +/** + * Legacy /verify alias: pre-Sunset runtime responsiveness. + * + * The alias MUST remain runtime-reachable through the advertised Sunset + * date (Sat, 01 Nov 2026 00:00:00 GMT, RFC 8594). For the same input, + * the alias MUST return the same response shape and status code as the + * canonical `POST /v1/verify`. The alias differs only in the + * deprecation-header set stamped on every response. + * + * This test drives the alias and the canonical route with identical + * inputs and asserts both the status-code parity and the shape of the + * response body, across a success-adjacent path (the request is parsed + * and reaches the verifier layer) and an error path (malformed input). + */ + +import { describe, it, expect, beforeAll } from 'vitest'; +import { Hono } from 'hono'; +import { + createVerifyV1Handler, + resetVerifyV1RateLimit, + isProblemError, + PROBLEM_MEDIA_TYPE, + createLegacyVerifyAliasHandler, +} from '../src/index.js'; + +function buildApp() { + resetVerifyV1RateLimit(); + const app = new Hono(); + app.onError((err, c) => { + if (isProblemError(err)) { + c.header('Content-Type', PROBLEM_MEDIA_TYPE); + return c.body(JSON.stringify(err.toProblemDetails()), err.status); + } + return c.json({ title: 'Internal Server Error', status: 500 }, 500); + }); + const verifyV1 = createVerifyV1Handler(); + app.post('/v1/verify', verifyV1); + app.post('/verify', createLegacyVerifyAliasHandler(verifyV1)); + return app; +} + +function request(path: string, body: unknown) { + return new Request(`http://localhost${path}`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); +} + +describe('legacy /verify alias: pre-Sunset responsiveness', () => { + let app: ReturnType; + + beforeAll(() => { + app = buildApp(); + }); + + it('Sunset header value is exactly the advertised date', async () => { + const res = await app.request(request('/verify', { receipt: 'not-a-jws' })); + expect(res.headers.get('Sunset')).toBe('Sat, 01 Nov 2026 00:00:00 GMT'); + }); + + it('alias and canonical return identical status codes on invalid input', async () => { + const aliasRes = await app.request(request('/verify', { receipt: 'not-a-jws' })); + const canonRes = await app.request(request('/v1/verify', { receipt: 'not-a-jws' })); + expect(aliasRes.status).toBe(canonRes.status); + }); + + it('alias and canonical return identical response shape on invalid input', async () => { + const aliasRes = await app.request(request('/verify', { receipt: 'not-a-jws' })); + const canonRes = await app.request(request('/v1/verify', { receipt: 'not-a-jws' })); + + const aliasBody = await aliasRes.json(); + const canonBody = await canonRes.json(); + + // Both paths return RFC 9457 Problem Details. Compare the set of + // keys (values that would drift per-request, e.g. peac_trace_id, are + // allowed to differ). + expect(Object.keys(aliasBody).sort()).toEqual(Object.keys(canonBody).sort()); + expect(aliasBody.type).toBe(canonBody.type); + expect(aliasBody.title).toBe(canonBody.title); + expect(aliasBody.status).toBe(canonBody.status); + }); + + it('alias and canonical agree on RFC 9457 Content-Type for error bodies', async () => { + const aliasRes = await app.request(request('/verify', { receipt: 'not-a-jws' })); + const canonRes = await app.request(request('/v1/verify', { receipt: 'not-a-jws' })); + const aliasCt = aliasRes.headers.get('Content-Type') ?? ''; + const canonCt = canonRes.headers.get('Content-Type') ?? ''; + // Both should start with application/problem+json (may or may not have charset) + expect(aliasCt.split(';')[0]).toBe(canonCt.split(';')[0]); + }); + + it('alias is reachable with malformed JSON (returns 400 Problem Details)', async () => { + const badReq = new Request('http://localhost/verify', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: '{not-json', + }); + const res = await app.request(badReq); + // We don't assert the exact status; only that the alias responded + // (did not hang, did not throw uncaught) and stamped deprecation. + expect(res.status).toBeGreaterThanOrEqual(400); + expect(res.status).toBeLessThan(500); + expect(res.headers.get('Sunset')).toBe('Sat, 01 Nov 2026 00:00:00 GMT'); + expect(res.headers.get('Deprecation')).toBe('true'); + }); +}); diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts index 59e873d64..3ee8a1512 100644 --- a/apps/api/tsup.config.ts +++ b/apps/api/tsup.config.ts @@ -1,14 +1,7 @@ import { defineConfig } from 'tsup'; export default defineConfig({ - entry: [ - 'src/index.ts', - 'src/handler.ts', - 'src/errors.ts', - 'src/verifier.ts', - 'src/routes.ts', - 'src/verify-v1.ts', - ], + entry: ['src/index.ts', 'src/handler.ts', 'src/errors.ts', 'src/verify-v1.ts'], format: ['cjs', 'esm'], dts: false, // Temporarily disable declaration generation sourcemap: true, @@ -17,5 +10,5 @@ export default defineConfig({ treeshake: true, minify: false, target: 'es2022', - external: ['node:*', '@peac/core', '@peac/disc'], + external: ['node:*', '@peac/disc'], }); diff --git a/packages/core/README.md b/archive/0.9.0-0.9.14/packages-core/README.md similarity index 64% rename from packages/core/README.md rename to archive/0.9.0-0.9.14/packages-core/README.md index abd8d802e..208f381bc 100644 --- a/packages/core/README.md +++ b/archive/0.9.0-0.9.14/packages-core/README.md @@ -1,4 +1,30 @@ -# @peac/core +# @peac/core — ARCHIVED (verify-only history) + +> **ARCHIVED (v0.13.0).** Source moved from `packages/core/` to +> `archive/0.9.0-0.9.14/packages-core/`. `@peac/core` is **not published +> at v0.13.0 or later**. Historical npm versions `<=0.9.14` remain +> installable for verify-only use of historical `peac.receipt/0.9` +> records (deprecate-then-remove discipline; historical tarballs are +> never unpublished). +> +> `@peac/core` was the original monolithic package that bundled signing, +> verification, policy enforcement, and utilities. It has been replaced +> by the kernel-first package architecture. This directory is preserved +> for historical reference only; it is not built, not linted, and not +> included in the workspace. +> +> **Migration:** `@peac/kernel` (constants), `@peac/schema` (types), +> `@peac/crypto` (sign / verify primitives), `@peac/protocol` +> (`issue`, `verifyLocal`, `verify`). See +> [`docs/MIGRATION_CURRENT.md`](../../../docs/MIGRATION_CURRENT.md). +> +> Below this banner is the historical README preserved verbatim for +> archaeology. It describes the (now-archived) `@peac/core` surface and +> is no longer authoritative. + +--- + +## Historical README (pre-archive) > **DEPRECATED. Removal scheduled for v0.13.0.** > Use `@peac/kernel`, `@peac/schema`, `@peac/crypto`, and `@peac/protocol` instead. diff --git a/packages/core/jest.config.js b/archive/0.9.0-0.9.14/packages-core/jest.config.js similarity index 100% rename from packages/core/jest.config.js rename to archive/0.9.0-0.9.14/packages-core/jest.config.js diff --git a/packages/core/package.json b/archive/0.9.0-0.9.14/packages-core/package.json similarity index 91% rename from packages/core/package.json rename to archive/0.9.0-0.9.14/packages-core/package.json index 01ce4b880..d6acb69d4 100644 --- a/packages/core/package.json +++ b/archive/0.9.0-0.9.14/packages-core/package.json @@ -1,7 +1,8 @@ { "name": "@peac/core", "version": "0.12.14", - "description": "DEPRECATED (removal: v0.13.0) - Use @peac/kernel, @peac/schema, @peac/crypto, @peac/protocol instead", + "description": "ARCHIVED in v0.13.0. Historical 0.9-series verify-only path. Use @peac/kernel, @peac/schema, @peac/crypto, @peac/protocol instead.", + "private": true, "type": "module", "homepage": "https://github.com/peacprotocol/peac#readme", "bugs": { diff --git a/packages/core/src/constants.ts b/archive/0.9.0-0.9.14/packages-core/src/constants.ts similarity index 100% rename from packages/core/src/constants.ts rename to archive/0.9.0-0.9.14/packages-core/src/constants.ts diff --git a/packages/core/src/crypto-sanity.test.js b/archive/0.9.0-0.9.14/packages-core/src/crypto-sanity.test.js similarity index 100% rename from packages/core/src/crypto-sanity.test.js rename to archive/0.9.0-0.9.14/packages-core/src/crypto-sanity.test.js diff --git a/packages/core/src/crypto.ts b/archive/0.9.0-0.9.14/packages-core/src/crypto.ts similarity index 100% rename from packages/core/src/crypto.ts rename to archive/0.9.0-0.9.14/packages-core/src/crypto.ts diff --git a/packages/core/src/enforce.ts b/archive/0.9.0-0.9.14/packages-core/src/enforce.ts similarity index 100% rename from packages/core/src/enforce.ts rename to archive/0.9.0-0.9.14/packages-core/src/enforce.ts diff --git a/packages/core/src/hash.test.js b/archive/0.9.0-0.9.14/packages-core/src/hash.test.js similarity index 100% rename from packages/core/src/hash.test.js rename to archive/0.9.0-0.9.14/packages-core/src/hash.test.js diff --git a/packages/core/src/hash.ts b/archive/0.9.0-0.9.14/packages-core/src/hash.ts similarity index 100% rename from packages/core/src/hash.ts rename to archive/0.9.0-0.9.14/packages-core/src/hash.ts diff --git a/packages/core/src/ids/uuidv7.ts b/archive/0.9.0-0.9.14/packages-core/src/ids/uuidv7.ts similarity index 100% rename from packages/core/src/ids/uuidv7.ts rename to archive/0.9.0-0.9.14/packages-core/src/ids/uuidv7.ts diff --git a/packages/core/src/index.ts b/archive/0.9.0-0.9.14/packages-core/src/index.ts similarity index 100% rename from packages/core/src/index.ts rename to archive/0.9.0-0.9.14/packages-core/src/index.ts diff --git a/packages/core/src/sign.ts b/archive/0.9.0-0.9.14/packages-core/src/sign.ts similarity index 100% rename from packages/core/src/sign.ts rename to archive/0.9.0-0.9.14/packages-core/src/sign.ts diff --git a/packages/core/src/types.ts b/archive/0.9.0-0.9.14/packages-core/src/types.ts similarity index 100% rename from packages/core/src/types.ts rename to archive/0.9.0-0.9.14/packages-core/src/types.ts diff --git a/packages/core/src/verify.test.js b/archive/0.9.0-0.9.14/packages-core/src/verify.test.js similarity index 100% rename from packages/core/src/verify.test.js rename to archive/0.9.0-0.9.14/packages-core/src/verify.test.js diff --git a/packages/core/src/verify.ts b/archive/0.9.0-0.9.14/packages-core/src/verify.ts similarity index 100% rename from packages/core/src/verify.ts rename to archive/0.9.0-0.9.14/packages-core/src/verify.ts diff --git a/packages/core/tsconfig.dts.json b/archive/0.9.0-0.9.14/packages-core/tsconfig.dts.json similarity index 100% rename from packages/core/tsconfig.dts.json rename to archive/0.9.0-0.9.14/packages-core/tsconfig.dts.json diff --git a/packages/core/tsconfig.json b/archive/0.9.0-0.9.14/packages-core/tsconfig.json similarity index 100% rename from packages/core/tsconfig.json rename to archive/0.9.0-0.9.14/packages-core/tsconfig.json diff --git a/packages/core/tsconfig.types.json b/archive/0.9.0-0.9.14/packages-core/tsconfig.types.json similarity index 100% rename from packages/core/tsconfig.types.json rename to archive/0.9.0-0.9.14/packages-core/tsconfig.types.json diff --git a/packages/core/tsup.config.ts b/archive/0.9.0-0.9.14/packages-core/tsup.config.ts similarity index 100% rename from packages/core/tsup.config.ts rename to archive/0.9.0-0.9.14/packages-core/tsup.config.ts diff --git a/contracts/api/crypto.json b/contracts/api/crypto.json index 3511fd9c0..d03fcf23c 100644 --- a/contracts/api/crypto.json +++ b/contracts/api/crypto.json @@ -1,7 +1,7 @@ { "package": "@peac/crypto", "version": "0.12.14", - "extracted_at": "2026-04-22", + "extracted_at": "2026-04-24", "node_version": "v24.13.0", "value_exports": [ { diff --git a/contracts/api/kernel.json b/contracts/api/kernel.json index ab904da58..e146483c1 100644 --- a/contracts/api/kernel.json +++ b/contracts/api/kernel.json @@ -1,7 +1,7 @@ { "package": "@peac/kernel", "version": "0.12.14", - "extracted_at": "2026-04-22", + "extracted_at": "2026-04-24", "node_version": "v24.13.0", "value_exports": [ { diff --git a/contracts/api/protocol.json b/contracts/api/protocol.json index 44ea937b5..84c0a8b29 100644 --- a/contracts/api/protocol.json +++ b/contracts/api/protocol.json @@ -1,7 +1,7 @@ { "package": "@peac/protocol", "version": "0.12.14", - "extracted_at": "2026-04-22", + "extracted_at": "2026-04-24", "node_version": "v24.13.0", "value_exports": [ { diff --git a/contracts/api/schema.json b/contracts/api/schema.json index f88dc6f96..1d926d16c 100644 --- a/contracts/api/schema.json +++ b/contracts/api/schema.json @@ -1,7 +1,7 @@ { "package": "@peac/schema", "version": "0.12.14", - "extracted_at": "2026-04-22", + "extracted_at": "2026-04-24", "node_version": "v24.13.0", "value_exports": [ { @@ -676,10 +676,6 @@ "name": "PRIVACY_EXTENSION_KEY", "typeof": "static" }, - { - "name": "PROOF_METHODS", - "typeof": "static" - }, { "name": "PROOF_TYPES", "typeof": "static" @@ -740,10 +736,6 @@ "name": "ProblemDetailsSchema", "typeof": "static" }, - { - "name": "ProofMethodSchema", - "typeof": "static" - }, { "name": "ProofTypeSchema", "typeof": "static" @@ -1681,7 +1673,6 @@ "PolicyContext", "PolicyDecision", "PrivacyExtension", - "ProofMethod", "ProofType", "ProvenanceExtension", "PurposeDecision", @@ -1795,6 +1786,6 @@ "PurposeExtensionSchema", "SafetyExtensionSchema" ], - "total_value_exports": 386, - "total_type_exports": 190 + "total_value_exports": 384, + "total_type_exports": 189 } diff --git a/docs/COMPATIBILITY_MATRIX.md b/docs/COMPATIBILITY_MATRIX.md index c4ae16d37..fba3cf45e 100644 --- a/docs/COMPATIBILITY_MATRIX.md +++ b/docs/COMPATIBILITY_MATRIX.md @@ -14,7 +14,7 @@ Current as of v0.12.13. | `@peac/middleware-express` | Full | - | **default** | | Go SDK (`sdks/go/`) | Full: `Issue()` + `VerifyLocal()` + JCS (22 cross-language vectors) | Legacy verify only | **supported** (core issue/verify); middleware **experimental** | | Python | API-first via reference verifier (httpx examples, `>=3.12`) | - | **examples only** | -| `@peac/core` | - | Full (Wire 0.9 locked) | **deprecated** (removal: v0.13.0) | +| `@peac/core` | - | Full (Wire 0.9 locked) | **archived** (at v0.13.0) | | `@peac/sdk` | - | Full (Wire 0.1) | **archived** (use `@peac/protocol`) | ## Runtime Environments @@ -106,10 +106,10 @@ Informational and regression-oriented. Operator-facing service-level objectives ## Deprecation Schedule -| Surface | Deprecated since | Removal target | Migration | -| ------------------------- | ---------------- | ------------------------ | ---------------------------------------------------------------------------------------------------- | -| `@peac/core` | v0.10.0 | v0.13.0 | Use `@peac/kernel` plus `@peac/schema` plus `@peac/crypto` plus `@peac/protocol`. | -| `@peac/sdk` | v0.12.7 | v0.13.0 | Use `@peac/protocol` directly. | -| API `/verify` endpoint | v0.12.7 | v0.13.0 (or Nov 1, 2026) | Use `/api/v1/verify`. Legacy `/verify` carries RFC 9745 `Deprecation` and RFC 8594 `Sunset` headers. | -| `apps/bridge` | v0.12.7 | v0.13.0 | Use `@peac/protocol` or `/api/v1/verify`. | -| Wire 0.1 default teaching | v0.12.7 | Immediate | All defaults now Wire 0.2. | +| Surface | Deprecated since | Removal target | Migration | +| ------------------------- | ---------------- | ------------------------ | ------------------------------------------------------------------------------------------------ | +| `@peac/core` | v0.10.0 | v0.13.0 (archived) | Use `@peac/kernel` plus `@peac/schema` plus `@peac/crypto` plus `@peac/protocol`. | +| `@peac/sdk` | v0.12.7 | v0.13.0 (archived) | Use `@peac/protocol` directly. | +| API `/verify` endpoint | v0.12.7 | post-Sunset (2026-11-01) | Use `/v1/verify`. Legacy `/verify` delegates in-process; carries RFC 9745 / RFC 8594 / RFC 8288. | +| `apps/bridge` | v0.12.7 | v0.13.0 | Use `@peac/protocol` or `/api/v1/verify`. | +| Wire 0.1 default teaching | v0.12.7 | Immediate | All defaults now Wire 0.2. | diff --git a/docs/DEPRECATION_POLICY.md b/docs/DEPRECATION_POLICY.md index 33e6612d4..13bd7b973 100644 --- a/docs/DEPRECATION_POLICY.md +++ b/docs/DEPRECATION_POLICY.md @@ -51,10 +51,10 @@ No surface may be promoted from `experimental` or `compat-only` to `supported` o | Surface | State | Deprecated since | Removal target | Replacement | | ------------------------- | ---------- | ---------------- | ------------------------ | ------------------------------------------------------------------- | -| `@peac/core` | deprecated | v0.10.0 | v0.13.0 | `@peac/kernel` + `@peac/schema` + `@peac/crypto` + `@peac/protocol` | +| `@peac/core` | archived | v0.10.0 | v0.13.0 | `@peac/kernel` + `@peac/schema` + `@peac/crypto` + `@peac/protocol` | | `@peac/sdk` | archived | v0.12.7 | v0.13.0 | `@peac/protocol` | -| `apps/bridge` | archived | v0.12.7 | v0.13.0 | `@peac/protocol` or `/api/v1/verify` | -| API `/verify` | deprecated | v0.12.7 | v0.13.0 (or Nov 1, 2026) | `/api/v1/verify` | +| `apps/bridge` | archived | v0.12.7 | v0.13.0 | `@peac/protocol` or `/v1/verify` | +| API `/verify` | deprecated | v0.12.7 | post-Sunset (2026-11-01) | `/v1/verify` | | Wire 0.1 default teaching | removed | v0.12.7 | Immediate | All defaults now Wire 0.2 | ## Archive Protocol diff --git a/docs/HOSTED_VERIFY_CONTRACT.md b/docs/HOSTED_VERIFY_CONTRACT.md index 515960b2a..471cdb215 100644 --- a/docs/HOSTED_VERIFY_CONTRACT.md +++ b/docs/HOSTED_VERIFY_CONTRACT.md @@ -45,6 +45,16 @@ Verify a signed interaction record. **Error responses:** RFC 9457 Problem Details. +### Legacy `POST /verify` (deprecated compatibility alias) + +The reference verifier keeps `POST /verify` runtime-reachable for callers that have not yet migrated. The alias is **not** documented in the machine-readable OpenAPI contract (the canonical verify operation is `POST /v1/verify`). At runtime the alias: + +- Delegates in-process to the canonical `POST /v1/verify` handler and returns the same response shape and status codes. +- Stamps the same security and rate-limit headers as `POST /v1/verify`. +- Additionally stamps RFC 9745 `Deprecation: true`, RFC 8594 `Sunset: Sat, 01 Nov 2026 00:00:00 GMT`, and RFC 8288 `Link: ; rel="deprecation"` on every response. + +Runtime removal is scheduled no earlier than the advertised Sunset date (2026-11-01). New integrations MUST target `POST /v1/verify`; the `POST /api/v1/verify` path is covered by the same alias behavior. + ### POST /v1/issue (provisional) Issue a signed interaction record. This endpoint is provisional and uses a BYO-key model (caller provides an Ed25519 private key seed). The contract is defined in [`HOSTED_ISSUE_CONTRACT.md`](HOSTED_ISSUE_CONTRACT.md). The legacy request model shown below is retained for archival purposes only. diff --git a/docs/MIGRATION_CURRENT.md b/docs/MIGRATION_CURRENT.md index c74016775..52a4b96dc 100644 --- a/docs/MIGRATION_CURRENT.md +++ b/docs/MIGRATION_CURRENT.md @@ -2,6 +2,58 @@ This guide covers migration paths for current PEAC Protocol surfaces. +## A2A v0.3.0 compatibility removal (v0.13.0) + +A2A v0.3.0 compatibility was deprecated in v0.12.3 (DD-186) after the A2A v1.0.0 upstream stabilization and **removed in v0.13.0 PR B**. `@peac/mappings-a2a` now validates A2A v1.0.0 shapes only. + +Surfaces removed: + +- **Agent Card top-level `url`.** v0.3.0 cards carried the endpoint URL as `AgentCard.url`. v1.0.0 replaced this with `supportedInterfaces[]`. Cards without a valid `supportedInterfaces[0].url` are rejected at **runtime**: `normalizeAgentCard(card)` returns `null` and `discoverAgentCard(...)` skips them. (The `A2AAgentCard` interface intentionally keeps a `[key: string]: unknown` index signature so incoming JSON with unknown extra fields still typechecks; type-level rejection is NOT claimed. The v0.3.0 removal is enforced by the normalization layer, not the type system.) +- **Kebab-case TaskState strings.** v0.3.0 used `"working"`, `"completed"`, `"input-required"`, etc. v1.0.0 uses SCREAMING_SNAKE_CASE with a type prefix (`"TASK_STATE_WORKING"`, `"TASK_STATE_COMPLETED"`, `"TASK_STATE_INPUT_REQUIRED"`). The `normalizeTaskState` function and the `TASK_STATE_V03_TO_V1` map are removed. Callers MUST supply v1.0.0 TaskState values directly. +- **`/.well-known/agent.json` legacy discovery path.** `discoverAgentCard(baseUrl)` now fetches only the v1.0.0 canonical path `/.well-known/agent-card.json`. Deployers still serving the legacy path should publish the canonical path or upgrade to an A2A v1.0.0 implementation. +- **Deprecation-warning plumbing.** `_resetDeprecationWarning` is gone; no v0.3.0 `DeprecationWarning` is emitted because v0.3.0 inputs are now rejected outright rather than normalized with a warning. + +**Migration:** + +```ts +// Before (v0.3.0 shape; no longer accepted at v0.13.0) +const card = { name: 'agent', url: 'https://agent.example' }; + +// After (v1.0.0 shape) +const card = { + name: 'agent', + supportedInterfaces: [ + { url: 'https://agent.example', protocolBinding: 'http+json', protocolVersion: '1.0.0' }, + ], +}; + +// TaskState values must already be v1.0.0 +const state = 'TASK_STATE_WORKING'; // previously 'working' +``` + +`A2A_MAX_CARRIER_SIZE` (`65_536` bytes) is unchanged. `PEAC_EXTENSION_URI`, the `capabilities.extensions[]` registration pattern, and the `metadata[extensionURI].carriers[]` placement convention are all unchanged. The v0.3.0 removal is scoped strictly to the dual-version acceptance surface; v1.0.0 behavior is byte-stable. See `docs/specs/A2A-RECEIPT-PROFILE.md` for the normative v1.0.0 profile. + +## ProofMethodSchema removal (v0.13.0) + +`ProofMethodSchema`, `PROOF_METHODS`, and the `ProofMethod` type were deprecated in v0.12.2 (DD-185) and **removed in v0.13.0 PR B**. The deprecated standalone schema export retired because transport-binding methods are semantically distinct from trust-root proof models; the two concerns should not share a public surface. + +`AgentProofSchema.method` still accepts the same four transport-binding values — the enum is now inlined on the field definition: + +- `http-message-signature` +- `dpop` +- `mtls` +- `jwk-thumbprint` + +Migration by use site: + +- **If you imported `ProofMethodSchema` to validate a method string in isolation:** inline the enum yourself (`z.enum(['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint'])`) or, preferably, validate the whole proof object through `AgentProofSchema.parse(...)`. +- **If you imported the `ProofMethod` type for a function signature:** replace with `"http-message-signature" | "dpop" | "mtls" | "jwk-thumbprint"` or derive from the schema: `type Method = z.infer['method']`. +- **If you want trust-root proof semantics (how an identity proves itself):** use `ProofTypeSchema` and the `PROOF_TYPES` constant. `ProofType` (8 values: `did-web`, `did-key`, `did-plc`, `did-ion`, `x509-chain`, `ed25519-cert-chain`, `hsm-attestation`, `custom`) was always the canonical trust-root surface; it is unchanged. + +**No wire-format change.** Existing valid `AgentProofSchema.method` values continue to validate. No envelope, no `typ`, no signing change. + +See `docs/PACKAGE_STATUS_V0.13.0_PARITY.md` for the per-export parity table used to scope v0.13.0 schema removals. + ## Package-surface cleanup (v0.13.0) v0.13.0 finishes the scheduled package-surface cleanup. Deprecate-then-remove discipline applies throughout: historical npm versions are never unpublished. @@ -51,11 +103,11 @@ No runtime behavior change: `@peac/pref` v0.12.14 was already a facade over `@pe Summary: migrate parse / validate / emit to `@peac/policy-kit` now if your code only touches policy documents. Keep `@peac/disc@0.13.0` installed if your code needs `discover()` or the associated constants; migrate once the remote-fetch surface ports. -### `@peac/core` — archive coupled with legacy `/verify` handler rewire +### `@peac/core` — archived -**State:** `@peac/core` is the v0.9.x `peac.receipt/0.9` verify-only implementation and remains marked deprecated. Archival is coupled with the legacy `POST /verify` HTTP handler rewire (the only remaining active consumer is `apps/api/src/verifier.ts`). When that rewire lands, `packages/core/` moves to `archive/0.9.0-0.9.14/@peac-core/`, `apps/api/src/verifier.ts` is deleted, and the legacy `/verify` route delegates internally to the canonical `/v1/verify` handler while preserving its advertised `Sunset: Sat, 01 Nov 2026 00:00:00 GMT` (RFC 8594). +**State at v0.13.0:** archived. `@peac/core` was the v0.9.x `peac.receipt/0.9` verify-only implementation. Source moved from `packages/core/` to `archive/0.9.0-0.9.14/packages-core/`. `@peac/core` is **not published at v0.13.0 or later**. Historical npm versions `<=0.9.14` remain installable for verify-only use of historical `peac.receipt/0.9` records. The archive is coupled with the legacy `POST /verify` handler rewire: `apps/api/src/verifier.ts` was deleted (it was the only remaining active consumer), and the legacy `/verify` route now delegates in-process to the canonical `/v1/verify` handler while stamping RFC 9745 `Deprecation: true`, RFC 8594 `Sunset: Sat, 01 Nov 2026 00:00:00 GMT`, and RFC 8288 `Link` headers on every response. -**Migration:** use `@peac/protocol` (`issue`, `verifyLocal`, `verify`), `@peac/schema` (types), `@peac/crypto` (sign / verify primitives), and `@peac/kernel` (wire constants). Historical `@peac/core@<=0.9.14` versions on npm remain installable for verify-only use of historical `peac.receipt/0.9` records. +**Migration:** use `@peac/protocol` (`issue`, `verifyLocal`, `verify`), `@peac/schema` (types), `@peac/crypto` (sign / verify primitives), and `@peac/kernel` (wire constants). ### Empty Layer-6 pillar stubs — archived @@ -80,7 +132,7 @@ Kept in workspace as shipping packages: ### `npm deprecate` dispatch -Staged at [`scripts/release/npm-deprecate-v0.13.0.sh`](../scripts/release/npm-deprecate-v0.13.0.sh). Executed manually after promote. Covers `@peac/pref@<=0.12.14`, `@peac/sdk@<=0.10.2`, `@peac/disc@<=0.12.14`, and `@peac/disc@0.13.0` (explicitly marked as a one-release compatibility bridge). `@peac/core` deprecate line is present but commented until the legacy `/verify` handler rewire lands. +Staged at [`scripts/release/npm-deprecate-v0.13.0.sh`](../scripts/release/npm-deprecate-v0.13.0.sh). Executed manually after promote. Covers `@peac/pref@<=0.12.14`, `@peac/sdk@<=0.10.2`, `@peac/disc@<=0.12.14`, `@peac/disc@0.13.0` (marked as a one-release compatibility bridge), and `@peac/core@<=0.9.14`. --- @@ -151,7 +203,7 @@ const result = await verifyLocal(jws, publicKey); ## From `@peac/core` to `@peac/protocol` -`@peac/core` is deprecated (removal: v0.13.0). Migrate to the kernel-first packages. +`@peac/core` is archived at v0.13.0. Migrate to the kernel-first packages. ### Import changes @@ -177,7 +229,7 @@ import { generateKeypair, verify } from '@peac/crypto'; ## From legacy API `/verify` to `/v1/verify` -The legacy `/verify` endpoint is deprecated (Sunset: Nov 1, 2026). Migrate to the canonical `/v1/verify`. The `/api/v1/verify` path remains wired as a deprecated alias and resolves to the same handler; new code should use `/v1/verify`, which is the path documented in [`packages/schema/openapi/verify.yaml`](../packages/schema/openapi/verify.yaml) and [`docs/HOSTED_VERIFY_CONTRACT.md`](HOSTED_VERIFY_CONTRACT.md). +The legacy `/verify` endpoint is deprecated (Sunset: Nov 1, 2026). Migrate to the canonical `/v1/verify`. The `/verify` and `/api/v1/verify` paths remain runtime-reachable as deprecated compatibility aliases; they delegate in-process to the canonical `/v1/verify` handler, return the same response shape, and stamp RFC 9745 `Deprecation: true`, RFC 8594 `Sunset: Sat, 01 Nov 2026 00:00:00 GMT`, and RFC 8288 `Link: ; rel="deprecation"` on every response. New integrations must target `/v1/verify`, which is the only path documented in the public OpenAPI contract ([`packages/schema/openapi/verify.yaml`](../packages/schema/openapi/verify.yaml), [`docs/HOSTED_VERIFY_CONTRACT.md`](HOSTED_VERIFY_CONTRACT.md)). ### Request diff --git a/docs/PACKAGE_STATUS.md b/docs/PACKAGE_STATUS.md index 8eac13d39..90df0f8a0 100644 --- a/docs/PACKAGE_STATUS.md +++ b/docs/PACKAGE_STATUS.md @@ -85,20 +85,20 @@ Do not edit manually. Source: `REPO_SURFACE_STATUS.json`. Rebuild via `node scri | Package | npm | Removal | Note | | -------------------- | ------------ | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `packages/discovery` | `@peac/disc` | v0.13.1 | Published deprecated compatibility alias. Re-exports tolerant peac.txt parse / emit / validate / discover + constants; barrel emits one-shot PEAC_DISC_DEPRECATED DeprecationWarning. Canonical loader is @peac/policy-kit loadPolicyDocument. Retained at v0.13.0 to preserve publish closure with @peac/cli and apps/api; full retirement deferred until workspace consumers migrate. | -| `packages/core` | `@peac/core` | v0.13.0 | DEPRECATED. Archival of packages/core/ is coupled with the legacy /verify handler rewire that eliminates the last active consumer in apps/api/src/verifier.ts. Historical 0.9-series receipt verify-only path; use @peac/protocol + @peac/schema + @peac/crypto + @peac/kernel for current wire. | ## Archived (non-default, may be removed) -| Surface | Reason | -| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `packages/sdk-js` | Moved to archive/sdk-js/. Uses deprecated @peac/core. Replaced by @peac/protocol. | -| `apps/bridge` | Moved to archive/bridge/. Unused dev sidecar. Imports deprecated @peac/core. | -| `archive/pref` | ARCHIVED in v0.13.0 (moved from packages/aipref/). Migration: @peac/mappings-content-signals (RFC 9651 Structured Fields, full RFC 8785 JCS + SHA-256 digest). Historical npm versions <=0.12.14 remain installable (deprecate-then-remove discipline). | -| `archive/pillars/access` | ARCHIVED in v0.13.0. Empty pillar stub never published. Pillar taxonomy label retained; concrete implementation targeted for v1.0+ watchlist. | -| `archive/pillars/compliance` | ARCHIVED in v0.13.0. Empty pillar stub never published. | -| `archive/pillars/consent` | ARCHIVED in v0.13.0. Empty pillar stub never published. | -| `archive/pillars/intelligence` | ARCHIVED in v0.13.0. Empty pillar stub never published. | -| `archive/pillars/provenance` | ARCHIVED in v0.13.0. Near-empty scaffold (version constant + doc comment) never published. | +| Surface | Reason | +| ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `packages/sdk-js` | Moved to archive/sdk-js/. Uses deprecated @peac/core. Replaced by @peac/protocol. | +| `apps/bridge` | Moved to archive/bridge/. Unused dev sidecar. Imports deprecated @peac/core. | +| `archive/pref` | ARCHIVED in v0.13.0 (moved from packages/aipref/). Migration: @peac/mappings-content-signals (RFC 9651 Structured Fields, full RFC 8785 JCS + SHA-256 digest). Historical npm versions <=0.12.14 remain installable (deprecate-then-remove discipline). | +| `archive/pillars/access` | ARCHIVED in v0.13.0. Empty pillar stub never published. Pillar taxonomy label retained; concrete implementation targeted for v1.0+ watchlist. | +| `archive/pillars/compliance` | ARCHIVED in v0.13.0. Empty pillar stub never published. | +| `archive/pillars/consent` | ARCHIVED in v0.13.0. Empty pillar stub never published. | +| `archive/pillars/intelligence` | ARCHIVED in v0.13.0. Empty pillar stub never published. | +| `archive/pillars/provenance` | ARCHIVED in v0.13.0. Near-empty scaffold (version constant + doc comment) never published. | +| `archive/0.9.0-0.9.14/packages-core` | ARCHIVED in v0.13.0. Source moved from packages/core/ to archive/0.9.0-0.9.14/packages-core/. Historical 0.9-series peac.receipt/0.9 verify-only path. Not published at v0.13.0 or later. Historical npm versions <=0.9.14 remain installable (deprecate-then-remove discipline). | ## Experimental (API may change) diff --git a/docs/PACKAGE_STATUS_V0.13.0_PARITY.md b/docs/PACKAGE_STATUS_V0.13.0_PARITY.md index e61e26d36..2372df286 100644 --- a/docs/PACKAGE_STATUS_V0.13.0_PARITY.md +++ b/docs/PACKAGE_STATUS_V0.13.0_PARITY.md @@ -69,16 +69,14 @@ If step 1 is not completed, `@peac/disc` must remain published as a compatibilit --- -## `@peac/core` → canonical replacement parity +## `@peac/core` → canonical replacement parity (already archived) -| `@peac/core` export | Kind | Replacement | Equivalent? | Action | -| ------------------------------------------------ | --------- | ------------------------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `verifyReceipt(jws, keys)` | function | `@peac/protocol.verifyReceipt` (canonical JWKS-based verify) | yes | Rewire `apps/api/src/verifier.ts` to delegate internally to the canonical `/v1/verify` handler; delete `apps/api/src/verifier.ts`; remove `@peac/core` from `apps/api/package.json`; archive `packages/core/`. | -| `canonicalPolicyHash` | function | Canonical `computePolicyDigestJcs` from `@peac/protocol` | yes | Rewire legacy-verify call sites to the canonical digest path. | -| `VerifyKeySet` | type | Canonical key-set type from `@peac/protocol` | yes | Update type imports in the surviving legacy code paths; confirm byte-identical verify output on parity corpus. | -| Legacy `peac.receipt/0.9` signing / verify paths | functions | No current-wire replacement; archive package is verify-only. | N/A | Already legacy-quarantined. No consumer on active surface should need these. | - -Archive execution is coupled with the legacy `/verify` handler rewire so the retirement is one conceptual change, not two. +| `@peac/core` export | Kind | Replacement | Equivalent? | Status | +| ------------------------------------------------ | --------- | ------------------------------------------------------------ | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `verifyReceipt(jws, keys)` | function | `@peac/protocol.verifyReceipt` (canonical JWKS-based verify) | yes | Archived. `apps/api/src/verifier.ts` deleted; the legacy `POST /verify` route now delegates in-process to `/v1/verify` and stamps RFC 9745 / RFC 8594 / RFC 8288 headers. `@peac/core` removed from `apps/api/package.json`. | +| `canonicalPolicyHash` | function | Canonical `computePolicyDigestJcs` from `@peac/protocol` | yes | Archived. No active consumer. | +| `VerifyKeySet` | type | Canonical key-set type from `@peac/protocol` | yes | Archived. No active consumer. | +| Legacy `peac.receipt/0.9` signing / verify paths | functions | No current-wire replacement; archive is verify-only. | N/A | Archived under `archive/0.9.0-0.9.14/packages-core/`. Historical npm versions `<=0.9.14` remain installable for verify-only use; `@peac/core` is not published at v0.13.0 or later. | --- @@ -96,6 +94,6 @@ No further migration work required; all `@peac/pref` exports have canonical equi ## Change log for this audit -| Date | Change | -| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| 2026-04-24 | Initial audit drafted. `@peac/disc` is a published deprecated compatibility alias; retirement requires the 5 current-compatibility items above to be satisfied. `@peac/core` archive is coupled with the legacy `/verify` handler rewire. `@peac/pref` migration complete as of v0.12.14. | +| Date | Change | +| ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 2026-04-24 | Initial audit drafted. `@peac/disc` is a published deprecated compatibility alias; retirement requires the 5 current-compatibility items above to be satisfied. `@peac/core` archived (source moved to `archive/0.9.0-0.9.14/packages-core/`; legacy `/verify` handler rewired to delegate in-process to `/v1/verify`). `@peac/pref` migration complete as of v0.12.14. | diff --git a/docs/REFERENCE_ARCHITECTURES.md b/docs/REFERENCE_ARCHITECTURES.md index dba15a27c..bf58f52c0 100644 --- a/docs/REFERENCE_ARCHITECTURES.md +++ b/docs/REFERENCE_ARCHITECTURES.md @@ -122,7 +122,7 @@ Agent A (Requester) A2A Gateway Agent B (Provider) - Receipt is carried in A2A task `metadata[extensionURI].carriers[]` (max 64 KB) - Agent B is the issuer; Agent A is the verifier - The A2A gateway routes but does not modify the evidence payload -- Supports A2A v1.0 (Linux Foundation); v0.3.0 backward compatibility via normalizer +- Supports A2A v1.0 (Linux Foundation); v0.3.0 compatibility was deprecated in v0.12.3 and removed in v0.13.0 (DD-186) - OAuth PKCE (S256) for agent authentication; Device Code flow for non-browser agents ### Packages involved diff --git a/docs/STABILITY-CONTRACT.md b/docs/STABILITY-CONTRACT.md index 712b79ac2..d364fc52a 100644 --- a/docs/STABILITY-CONTRACT.md +++ b/docs/STABILITY-CONTRACT.md @@ -20,14 +20,14 @@ maintenance (security and correctness fixes apply to every active line per ## Wire formats -| Surface | Concrete identifier | Classification | Notes | -| -------------------------- | --------------------------------------------------------- | -------------- | -------------------------------------------------------------- | -| Current interaction record | `typ: interaction-record+jwt` (JWS JOSE header; Wire 0.2) | `stable` | Frozen wire identifiers until v1.0 | -| Legacy receipt format | `peac-receipt/0.1` (Wire 0.1) | `stable` | Verify-only path frozen; no new-feature extensions | -| Archival receipt format | `peac.receipt/0.9` | `deprecated` | Verify-only via `@peac/core` archival path; removal at v0.13.0 | -| Cryptographic envelope | JWS Compact Serialization (RFC 7515), Ed25519 (RFC 8032) | `stable` | Algorithm negotiation is not supported | -| Canonical JSON | JCS (RFC 8785) | `stable` | Cross-language parity fixtures in `specs/conformance/` | -| Verifier response bodies | `application/json` (RFC 8259) | `stable` | Error bodies: `application/problem+json` (RFC 9457) | +| Surface | Concrete identifier | Classification | Notes | +| -------------------------- | --------------------------------------------------------- | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | +| Current interaction record | `typ: interaction-record+jwt` (JWS JOSE header; Wire 0.2) | `stable` | Frozen wire identifiers until v1.0 | +| Legacy receipt format | `peac-receipt/0.1` (Wire 0.1) | `stable` | Verify-only path frozen; no new-feature extensions | +| Archival receipt format | `peac.receipt/0.9` | `archived` | Historical verify-only path. Source archived under `archive/0.9.0-0.9.14/packages-core/`; historical npm `@peac/core@<=0.9.14` remains installable. | +| Cryptographic envelope | JWS Compact Serialization (RFC 7515), Ed25519 (RFC 8032) | `stable` | Algorithm negotiation is not supported | +| Canonical JSON | JCS (RFC 8785) | `stable` | Cross-language parity fixtures in `specs/conformance/` | +| Verifier response bodies | `application/json` (RFC 8259) | `stable` | Error bodies: `application/problem+json` (RFC 9457) | ## Public TypeScript APIs @@ -59,8 +59,8 @@ maintenance (security and correctness fixes apply to every active line per | `@peac/contracts` | `stable` | Machine-readable contract exports | | `@peac/pay402`, `@peac/pref`, `@peac/attribution`, `@peac/receipts` | `stable` | Supporting packages on Layer 4 | | `@peac/worker-core` | `stable` | Worker-oriented helpers | -| `@peac/core` | `deprecated` | Archival; Wire 0.9 verify-only path; removal at v0.13.0 | -| `@peac/sdk` | `deprecated` | Workspace stub; removal at v0.13.0 | +| `@peac/core` | `archived` | Archived at v0.13.0; source under `archive/0.9.0-0.9.14/packages-core/`; historical npm `<=0.9.14` remains installable | +| `@peac/sdk` | `archived` | Archived; historical npm `<=0.10.2` remains installable | Consumers: import only from the package's documented public entry points. Subpath imports into internal modules are not a stable surface even when @@ -96,11 +96,11 @@ Package: [`@peac/cli`](../packages/cli). ## Reference verifier (`apps/api`) -| Surface | Path | Classification | -| --------------------- | -------------------------------------------------------------------------------- | --------------------------------------------------------------- | -| `POST /v1/verify` | [`apps/api`](../apps/api) | `stable` | -| `POST /v1/issue` | `apps/api` (provisional; see [Hosted issue contract](HOSTED_VERIFY_CONTRACT.md)) | `experimental` | -| Legacy `POST /verify` | `apps/api` | `deprecated` (Deprecation + Sunset headers; removal at v0.13.0) | +| Surface | Path | Classification | +| --------------------- | -------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `POST /v1/verify` | [`apps/api`](../apps/api) | `stable` | +| `POST /v1/issue` | `apps/api` (provisional; see [Hosted issue contract](HOSTED_VERIFY_CONTRACT.md)) | `experimental` | +| Legacy `POST /verify` | `apps/api` | `deprecated` (delegates in-process to `/v1/verify`; Deprecation + Sunset headers; runtime removal no earlier than 2026-11-01) | Response shapes: @@ -156,14 +156,14 @@ Path: [`surfaces/plugin-pack/`](../surfaces/plugin-pack/). ## Deprecation schedule -| Surface | Deprecated since | Removal target | -| --------------------------------------------------------- | ---------------- | -------------- | -| `ProofMethodSchema` (compat alias) | v0.12.2 | v0.13.0 | -| A2A v0.3.0 compatibility path | v0.12.3 | v0.13.0 | -| Legacy `POST /verify` endpoint (in favor of `/v1/verify`) | v0.12.x | v0.13.0 | -| `packages/sdk-js/` workspace stub | v0.12.x | v0.13.0 | -| `peac.receipt/0.9` archival format | Legacy frozen | v0.13.0 | -| `@peac/core` archival verify-only path | Legacy frozen | v0.13.0 | +| Surface | Deprecated since | Removal target | Status | +| --------------------------------------------------------- | ---------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `ProofMethodSchema` (compat alias) | v0.12.2 | v0.13.0 | **Removed in v0.13.0.** Transport-binding values (`http-message-signature`, `dpop`, `mtls`, `jwk-thumbprint`) inlined on `AgentProofSchema.method`. | +| A2A v0.3.0 compatibility path | v0.12.3 | v0.13.0 | **Removed in v0.13.0.** Agent Cards carrying only a top-level `url`, kebab-case TaskState strings, and the `/.well-known/agent.json` legacy discovery path are no longer accepted. A2A v1.0.0 `supportedInterfaces[]` is required. | +| Legacy `POST /verify` endpoint (in favor of `/v1/verify`) | v0.12.x | post-Sunset (2026-11-01) | Removed from active OpenAPI teaching at v0.13.0. Runtime alias preserved, delegating in-process to `/v1/verify`, until the advertised Sunset date. | +| `packages/sdk-js/` workspace stub | v0.12.x | v0.13.0 | Scheduled | +| `peac.receipt/0.9` archival format | Legacy frozen | v0.13.0 (quarantine) | Quarantined to historical contexts; wire stays frozen | +| `@peac/core` archival verify-only path | Legacy frozen | v0.13.0 | **Archived in v0.13.0** to `archive/0.9.0-0.9.14/packages-core/`; historical npm `<=0.9.14` remains installable. | All status transitions are tracked in [`REPO_SURFACE_STATUS.json`](../REPO_SURFACE_STATUS.json) and mirrored in diff --git a/docs/SURFACE_STATUS.md b/docs/SURFACE_STATUS.md index a884198de..74f436680 100644 --- a/docs/SURFACE_STATUS.md +++ b/docs/SURFACE_STATUS.md @@ -123,11 +123,10 @@ Do not edit manually. Source: `REPO_SURFACE_STATUS.json`. Rebuild via `node scri ## Layer legacy -| Surface | npm | State | Wire | -| ----------------- | ------------ | ---------- | ------ | -| `apps/bridge` | - | archived | 0.9.13 | -| `packages/core` | `@peac/core` | deprecated | 0.9 | -| `packages/sdk-js` | `@peac/sdk` | archived | 0.1 | +| Surface | npm | State | Wire | +| ----------------- | ----------- | -------- | ------ | +| `apps/bridge` | - | archived | 0.9.13 | +| `packages/sdk-js` | `@peac/sdk` | archived | 0.1 | ## Layer sdk @@ -140,3 +139,9 @@ Do not edit manually. Source: `REPO_SURFACE_STATUS.json`. Rebuild via `node scri | Surface | npm | State | Wire | | -------------- | ------------ | -------- | ---- | | `archive/pref` | `@peac/pref` | archived | null | + +## Layer undefined + +| Surface | npm | State | Wire | +| ------------------------------------ | --- | -------- | ---- | +| `archive/0.9.0-0.9.14/packages-core` | - | archived | 0.1 | diff --git a/docs/releases/api-surface/kernel.txt b/docs/releases/api-surface/kernel.txt index f1ee1802c..15582aade 100644 --- a/docs/releases/api-surface/kernel.txt +++ b/docs/releases/api-surface/kernel.txt @@ -15,9 +15,12 @@ DISPUTE_ERRORS ERRORS ERROR_CATEGORIES ERROR_CODES +EXTENSION_BUDGET +EXTENSION_GROUPS ErrorCategory ErrorDefinition EvidencePillar +ExtensionGroupEntry HASH HEADERS ISSUER_CONFIG @@ -34,18 +37,24 @@ PAYMENT_RAILS PEAC_ALG PEAC_RECEIPT_HEADER PEAC_RECEIPT_URL_HEADER +PILLAR_VALUES POLICY POLICY_BLOCK PRIVATE_IP_RANGES +PROOF_TYPES PaymentRailEntry PeacEvidenceCarrier PolicyBlock +ProofTypeEntry RECEIPT +RECEIPT_TYPES REGISTRIES ReceiptRef +ReceiptTypeEntry RepresentationFields TRANSPORT_METHODS TYPE_GRAMMAR +TYPE_TO_EXTENSION_MAP TransportMethodEntry VARY_HEADERS VERIFICATION_MODES @@ -67,7 +76,10 @@ WireVersion applyPurposeVary findAgentProtocol findControlEngine +findExtensionGroup findPaymentRail +findProofType +findReceiptType findTransportMethod formatHash getError diff --git a/docs/releases/api-surface/mappings-a2a.txt b/docs/releases/api-surface/mappings-a2a.txt new file mode 100644 index 000000000..1c798c4c5 --- /dev/null +++ b/docs/releases/api-surface/mappings-a2a.txt @@ -0,0 +1,68 @@ +A2AAgentCard +A2AArtifactLike +A2AAuthErrorCode +A2AAuthEvent +A2AAuthEvidenceResult +A2AAuthMethod +A2ACarrierAdapter +A2ADeviceCodeFlowConfig +A2AExtractAsyncResult +A2AExtractResult +A2AMessageLike +A2AMetadata +A2AOAuthConfig +A2APeacPayload +A2ASupportedInterface +A2ATaskStatusLike +A2A_MAX_CARRIER_SIZE +A2A_V1_MESSAGE_ROLE +A2A_V1_TASK_STATE +AgentCardExtension +AgentCardPeacExtension +AuthorizationRequest +DeviceCodeErrorResponse +DeviceCodePollingError +DeviceCodeRequest +DeviceCodeResponse +DeviceCodeTokenResponse +DiscoveryOptions +FetchFn +GrpcErrorInfo +GrpcStatus +GrpcStatusCode +GrpcStatusCodeValue +NormalizedAgentCard +PEAC_EXTENSION_URI +PKCEChallenge +PeacDiscoveryResult +PeacDiscoverySource +TokenResponse +attachReceiptToArtifact +attachReceiptToMessage +attachReceiptToMetadata +attachReceiptToTaskStatus +buildA2AExtensionsHeader +buildAuthorizationRequest +computeS256Challenge +createA2AAuthStatus +createA2ACarrierMeta +discoverAgentCard +discoverPeacCapabilities +exchangeAuthorizationCode +extractReceiptFromArtifact +extractReceiptFromArtifactAsync +extractReceiptFromMessage +extractReceiptFromMessageAsync +extractReceiptFromMetadata +extractReceiptFromMetadataAsync +extractReceiptFromTaskStatus +extractReceiptFromTaskStatusAsync +fromA2AAuthEvent +generatePKCEChallenge +getPeacExtension +hasPeacExtension +isV1AgentCard +normalizeAgentCard +parseA2AExtensionsHeader +selectBestInterface +validatePKCEVerifier diff --git a/docs/releases/api-surface/protocol.txt b/docs/releases/api-surface/protocol.txt index 62b67c981..9903ae199 100644 --- a/docs/releases/api-surface/protocol.txt +++ b/docs/releases/api-surface/protocol.txt @@ -1,10 +1,26 @@ +JWK +JWKS +JWKSResolveError +JWKSResolveResult +JWKSResolveSuccess +ResolveJWKSOptions +RevokedKeyInfo base64urlDecode base64urlEncode +checkDocumentBinding checkPolicyBinding +clearJWKSCache +clearKidThumbprints +computeDocumentDigest +computeJsonDocumentDigestJcs computeJwkThumbprint computePolicyDigestJcs +computeTextDocumentDigestUtf8 generateKeypair +getJWKSCacheSize +getKidThumbprintSize jwkToPublicKeyBytes +resolveJWKS sha256Bytes sha256Hex verify diff --git a/docs/releases/api-surface/schema.txt b/docs/releases/api-surface/schema.txt index 7beaf319c..9f72e3496 100644 --- a/docs/releases/api-surface/schema.txt +++ b/docs/releases/api-surface/schema.txt @@ -4,6 +4,7 @@ AGENT_IDENTITY_TYPE AIPREFSnapshotSchema ATTESTATION_LIMITS ATTESTATION_RECEIPT_TYPE +ATTRIBUTION_EXTENSION_KEY ATTRIBUTION_LIMITS ATTRIBUTION_TYPE ATTRIBUTION_USAGES @@ -19,6 +20,7 @@ AgentIdentityVerified AgentIdentityVerifiedSchema AgentProof AgentProofSchema +AmountMinorStringSchema AttestationExtensions AttestationExtensionsSchema AttestationReceiptClaims @@ -29,6 +31,8 @@ AttributionAttestation AttributionAttestationSchema AttributionEvidence AttributionEvidenceSchema +AttributionExtension +AttributionExtensionSchema AttributionSource AttributionSourceSchema AttributionUsage @@ -42,6 +46,11 @@ CHALLENGE_EXTENSION_KEY CHALLENGE_TYPES COMMERCE_EXTENSION_KEY COMMITMENT_CLASSES +COMPLIANCE_EXTENSION_KEY +COMPLIANCE_STATUSES +CONSENT_EXTENSION_KEY +CONSENT_STATUSES +CONTENT_SIGNAL_SOURCES CONTRIBUTION_TYPES CONTROL_ACTIONS CONTROL_ACTION_EXTENSION_KEY @@ -69,12 +78,22 @@ CommerceExtensionSchema CommitmentClass CommitmentClassSchema CompactJwsSchema +ComplianceExtension +ComplianceExtensionSchema +ComplianceStatus +ComplianceStatusSchema +ConsentExtension +ConsentExtensionSchema +ConsentStatus +ConsentStatusSchema ConstraintValidationResult ConstraintViolation ContactMethod ContactMethodSchema ContentHash ContentHashSchema +ContentSignalSource +ContentSignalSourceSchema ContextMetadata ContributionObligation ContributionObligationSchema @@ -110,6 +129,8 @@ CreditMethod CreditMethodSchema CreditObligation CreditObligationSchema +CustodyEntry +CustodyEntrySchema DERIVATION_TYPES DIGEST_SIZE_CONSTANTS DIGEST_VALUE_PATTERN @@ -151,6 +172,7 @@ DisputeType DisputeTypeSchema DocumentRef DocumentRefSchema +EXTENSION_BUDGET EXTENSION_KEY_PATTERN EXTENSION_LIMITS EnforcementContext @@ -166,6 +188,7 @@ HashAlgorithm HashAlgorithmSchema HashEncoding HashEncodingSchema +HttpsUriHintSchema IDENTITY_EXTENSION_KEY INTERACTION_EXTENSION_KEY INTERACTION_LIMITS @@ -176,6 +199,10 @@ IdentityExtensionSchema InteractionEvidenceV01 InteractionEvidenceV01Schema InteractionValidationResult +Iso8601DateSchema +Iso8601DateStringSchema +Iso8601DurationSchema +Iso8601OffsetDateTimeSchema JSON_EVIDENCE_LIMITS JWSHeader JsonArraySchema @@ -207,8 +234,10 @@ OrchestrationFrameworkSchema PEACEnvelope PEACParseError POLICY_DECISIONS -PROOF_METHODS +PRIVACY_EXTENSION_KEY PROOF_TYPES +PROVENANCE_EXTENSION_KEY +PURPOSE_EXTENSION_KEY ParseFailure ParseReceiptOptions ParseReceiptResult @@ -225,13 +254,18 @@ PolicyBlockSchema PolicyContext PolicyContextSchema PolicyDecision +PrivacyExtension +PrivacyExtensionSchema ProblemDetailsSchema -ProofMethod -ProofMethodSchema ProofType ProofTypeSchema +ProvenanceExtension +ProvenanceExtensionSchema +PurposeExtension +PurposeExtensionSchema PurposeReasonSchema PurposeTokenSchema +RECIPIENT_SCOPES REDACTION_MODES REGISTERED_EXTENSION_GROUP_KEYS REGISTERED_RECEIPT_TYPES @@ -239,7 +273,10 @@ REMEDIATION_TYPES REPRESENTATION_LIMITS RESERVED_KIND_PREFIXES RESULT_STATUSES +RETENTION_MODES +REVIEW_STATUSES REVOCATION_REASONS +RISK_LEVELS ReceiptClaims ReceiptClaimsSchema ReceiptClaimsType @@ -249,6 +286,8 @@ ReceiptTypeSchema ReceiptUrlSchema ReceiptVariant ReceiptView +RecipientScope +RecipientScopeSchema Refs RefsSchema Remediation @@ -261,13 +300,28 @@ ResourceTargetSchema Result ResultSchema ResultStatus +RetentionMode +RetentionModeSchema +ReviewStatus +ReviewStatusSchema RevocationReason RevokedKeyEntryInput RevokedKeyEntryOutput RevokedKeyEntrySchema RevokedKeysArraySchema +Rfc3339DateTimeSchema +Rfc3339TimestampSchema +RiskLevel +RiskLevelSchema +SAFETY_EXTENSION_KEY STEP_ID_PATTERN +SafetyExtension +SafetyExtensionSchema +Sha256DigestSchema SimpleValidationResult +SlsaLevel +SlsaLevelSchema +SpdxExpressionSchema StepId StepIdSchema SubjectProfileSchema @@ -288,6 +342,8 @@ UNSAFE_JsonEvidenceLimits ValidationError ValidationWarning VerifyRequestSchema +WARNING_EXTENSION_GROUP_MISMATCH +WARNING_EXTENSION_GROUP_MISSING WARNING_OCCURRED_AT_SKEW WARNING_TYPE_UNREGISTERED WARNING_TYP_MISSING @@ -341,11 +397,18 @@ extractObligationsExtension findRevokedKey fingerprintRefToString getAccessExtension +getAttributionExtension getChallengeExtension getCommerceExtension +getComplianceExtension +getConsentExtension getCorrelationExtension getIdentityExtension getInteraction +getPrivacyExtension +getProvenanceExtension +getPurposeExtension +getSafetyExtension getValidTransitions hasInteraction hasValidDagSemantics @@ -370,6 +433,7 @@ isPaymentReceipt isReservedKindPrefix isTerminalState isTerminalWorkflowStatus +isValidAmountMinor isValidDisputeAttestation isValidExtensionKey isValidInteractionEvidence diff --git a/docs/releases/api-surface/sdk.txt b/docs/releases/api-surface/sdk.txt deleted file mode 100644 index 858ce3e59..000000000 --- a/docs/releases/api-surface/sdk.txt +++ /dev/null @@ -1,13 +0,0 @@ -ClientConfig -ClientError -DiscoverOptions -DiscoveryResult -PeacClient -PublicKeyMap -VerificationResult -VerifyLocalOptions -VerifyRemoteOptions -discover -verify -verifyLocal -verifyRemote diff --git a/docs/releases/facts.json b/docs/releases/facts.json index 72e754612..e2716264e 100644 --- a/docs/releases/facts.json +++ b/docs/releases/facts.json @@ -7,9 +7,9 @@ "release_date": "2026-04-22", "metrics": { "tests": 7672, - "test_files": 309, + "test_files": 310, "published_packages": 37, - "build_targets": 100, + "build_targets": 99, "conformance_requirement_ids": 224, "conformance_sections": 25 }, diff --git a/docs/specs/A2A-RECEIPT-PROFILE.md b/docs/specs/A2A-RECEIPT-PROFILE.md index ec2c8b447..14c5296ff 100644 --- a/docs/specs/A2A-RECEIPT-PROFILE.md +++ b/docs/specs/A2A-RECEIPT-PROFILE.md @@ -3,11 +3,11 @@ **Version:** 0.2 **Status:** Normative **Package:** `@peac/mappings-a2a` -**A2A Spec Version:** v1.0.0 (stable); v0.3.0 (deprecated, supported via normalizer) +**A2A Spec Version:** v1.0.0 (stable). v0.3.0 compatibility was deprecated in v0.12.3 and removed in v0.13.0 (DD-186). **Extension URI:** `https://www.peacprotocol.org/ext/traceability/v1` -**Depends on:** Evidence Carrier Contract (DD-124), A2A v1.0 transition normalizer (DD-186) +**Depends on:** Evidence Carrier Contract (DD-124) -This document specifies how PEAC evidence carriers are placed within A2A (Agent-to-Agent Protocol) messages and metadata. It covers Agent Card declaration, metadata layout, header conventions, and security considerations. Both A2A v1.0.0 and v0.3.0 (deprecated) wire formats are supported via the dual-version normalizer (DD-186). +This document specifies how PEAC evidence carriers are placed within A2A (Agent-to-Agent Protocol) messages and metadata. It covers Agent Card declaration, metadata layout, header conventions, and security considerations. A2A v1.0.0 is the only accepted wire format; the v0.3.0 compatibility path (top-level `url`, kebab-case TaskState normalization, `/.well-known/agent.json` legacy discovery) was removed in v0.13.0. The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 (RFC 2119, RFC 8174) when, and only when, they appear in all capitals, as shown here. diff --git a/docs/specs/AGENT-IDENTITY-PROFILE.md b/docs/specs/AGENT-IDENTITY-PROFILE.md index 44b20d3e9..c9d65f269 100644 --- a/docs/specs/AGENT-IDENTITY-PROFILE.md +++ b/docs/specs/AGENT-IDENTITY-PROFILE.md @@ -22,7 +22,7 @@ This specification extends the base [AGENT-IDENTITY.md](AGENT-IDENTITY.md) speci 2. **Multi-Root Proof Types** (DD-143): An expanded proof type vocabulary supporting 8 cryptographic and identity proof mechanisms 3. **Minimum Viable Identity Set (MVIS)** (DD-144): Five required fields that constitute a complete identity receipt -These additions are additive to Wire 0.1. All identity data flows through `ext[]` extension slots using reverse-DNS keys per PROFILE_RULES.md. The existing `AgentIdentityAttestation` type and `ProofMethod` enum (4 methods) remain unchanged; the new `ProofType` vocabulary (8 types) is separate. +These additions are additive to Wire 0.1. All identity data flows through `ext[]` extension slots using reverse-DNS keys per PROFILE_RULES.md. The existing `AgentIdentityAttestation` type remains unchanged. The four transport-binding values previously exported as `ProofMethodSchema` (`http-message-signature`, `dpop`, `mtls`, `jwk-thumbprint`) are now inlined directly on `AgentProofSchema.method`; the standalone `ProofMethodSchema` / `ProofMethod` / `PROOF_METHODS` exports were removed in v0.13.0 (DD-185). The new `ProofType` vocabulary (8 types) is a separate trust-root taxonomy. ### 1.2 Scope @@ -38,7 +38,7 @@ This specification does NOT cover: - Changes to the base `AgentIdentityAttestation` schema (see [AGENT-IDENTITY.md](AGENT-IDENTITY.md)) - Wire format modifications (Wire 0.1 is frozen) - EAT adapter implementation (deferred to v0.12.0-preview.1; see DD-154) -- Unification of `ProofMethod` and `ProofType` (deferred to v0.12.0) +- Unification of the transport-binding method enum (`AgentProofSchema.method`) and `ProofType` under a single taxonomy (intentionally rejected: transport-binding and trust-root concerns are semantically distinct surfaces and remain separate) ### 1.3 Requirements Notation @@ -267,20 +267,20 @@ Implementations using `custom` SHOULD document: The proof type vocabulary is **extensible via `registries.json`**: new proof types can be added in future versions. Implementations encountering an unknown `proof_type` value SHOULD treat it as equivalent to `custom` (process the receipt, log a warning) rather than rejecting the receipt. -### 3.4 Relationship to ProofMethod +### 3.4 Relationship to the transport-binding method enum -The existing `ProofMethod` enum in [AGENT-IDENTITY.md](AGENT-IDENTITY.md) (4 methods: `http-message-signature`, `dpop`, `mtls`, `jwk-thumbprint`) describes how an agent proves control of a key in real-time HTTP requests. +The `AgentProofSchema.method` field in [AGENT-IDENTITY.md](AGENT-IDENTITY.md) (four transport-binding values: `http-message-signature`, `dpop`, `mtls`, `jwk-thumbprint`) describes how an agent proves control of a key in real-time HTTP requests. In v0.13.0 (DD-185) the four values were inlined directly on the field; the standalone `ProofMethodSchema` / `ProofMethod` / `PROOF_METHODS` exports were removed because transport-binding concerns are semantically distinct from trust-root taxonomy. -The new `ProofType` vocabulary (8 types) describes the trust root mechanism for an actor's identity in receipts. +The `ProofType` vocabulary (8 types) describes the trust-root mechanism for an actor's identity in receipts. These are **separate concerns**: -| Concern | Type | Vocabulary | Schema | -| --------------------------------- | ------------- | ---------- | ------------------------- | -| Request-time proof of key control | `ProofMethod` | 4 methods | `AgentProof.method` | -| Receipt-time identity trust root | `ProofType` | 8 types | `ActorBinding.proof_type` | +| Concern | Surface | Vocabulary | Schema | +| --------------------------------- | ------------------------- | ------------------ | ------------------------- | +| Request-time proof of key control | `AgentProofSchema.method` | 4 transport values | `AgentProof.method` | +| Receipt-time identity trust root | `ProofTypeSchema` | 8 trust-root types | `ActorBinding.proof_type` | -Unification of `ProofMethod` and `ProofType` into a single taxonomy is deferred to v0.12.0 as it would be a breaking change to the existing API. +Unification of the transport-binding method enum with `ProofType` under a single taxonomy was considered and rejected in v0.13.0 (DD-185): the concerns are semantically distinct and unifying them would conflate how a key is proven in transit with how an identity's trust root is established. The two surfaces remain separate by design. --- @@ -626,16 +626,16 @@ All five fields present; MVIS satisfied. Existing systems using `AgentIdentityAttestation` (v0.9.25) can adopt ActorBinding incrementally: 1. **Phase 1**: Add `org.peacprotocol/actor_binding` to receipts alongside existing attestation headers -2. **Phase 2**: Map existing `ProofMethod` to the closest `ProofType`: +2. **Phase 2**: Pick a `proof_type` (trust-root) that matches how the identity is established, independent of how the key is proven in transit. The transport-binding method on `AgentProofSchema.method` and the receipt-time trust root carried by `ProofType` compose rather than conflict: -| ProofMethod (existing) | ProofType (new) | -| ------------------------ | -------------------------------- | -| `http-message-signature` | `ed25519-cert-chain` or `custom` | -| `dpop` | `custom` | -| `mtls` | `x509-pki` | -| `jwk-thumbprint` | `ed25519-cert-chain` | +| Transport-binding method (`AgentProofSchema.method`) | Typical trust root (`ActorBinding.proof_type`) | +| ---------------------------------------------------- | ---------------------------------------------- | +| `http-message-signature` | `ed25519-cert-chain` or `custom` | +| `dpop` | `custom` | +| `mtls` | `x509-pki` | +| `jwk-thumbprint` | `ed25519-cert-chain` | -3. **Phase 3**: At v0.12.0, unified taxonomy replaces both vocabularies +3. **Phase 3 (no longer on the roadmap)**: Unification of the transport-binding method enum with `ProofType` was considered and rejected in v0.13.0. The two surfaces stay separate because they answer different questions (how the key is proven in the moment vs. how the identity's trust root is rooted). Systems should continue to populate both fields when both signals exist. ### 9.2 From No Identity to MVIS diff --git a/docs/specs/AGENT-IDENTITY.md b/docs/specs/AGENT-IDENTITY.md index 1fc4760f6..5248a1cfd 100644 --- a/docs/specs/AGENT-IDENTITY.md +++ b/docs/specs/AGENT-IDENTITY.md @@ -112,7 +112,9 @@ interface AgentIdentityEvidence { ```typescript interface AgentProof { - method: ProofMethod; // REQUIRED: Proof method + // REQUIRED: transport-binding method. Enum inlined on the field + // (previously exported as ProofMethodSchema; removed in v0.13.0). + method: 'http-message-signature' | 'dpop' | 'mtls' | 'jwk-thumbprint'; key_id: string; // REQUIRED: Key identifier (1-256 chars) alg?: string; // OPTIONAL: Algorithm (default: 'EdDSA') signature?: string; // OPTIONAL: Base64url signature diff --git a/docs/specs/DISCOVERY-PROFILE.md b/docs/specs/DISCOVERY-PROFILE.md index 1a41a724c..8f69f1068 100644 --- a/docs/specs/DISCOVERY-PROFILE.md +++ b/docs/specs/DISCOVERY-PROFILE.md @@ -21,11 +21,12 @@ The algorithm tries three steps in sequence, returning the first successful resu Check if the endpoint publishes an A2A Agent Card with a PEAC extension declared. -1. Fetch `{baseUrl}/.well-known/agent-card.json` (A2A v0.3.0 canonical path) -2. If 404, try `{baseUrl}/.well-known/agent.json` (legacy fallback) -3. Parse the Agent Card JSON -4. Look for PEAC extension in `capabilities.extensions[]` with URI `https://www.peacprotocol.org/ext/traceability/v1` -5. If found, return capabilities from the extension's `params` field +1. Fetch `{baseUrl}/.well-known/agent-card.json` (A2A v1.0.0 canonical path) +2. Parse the Agent Card JSON +3. Look for PEAC extension in `capabilities.extensions[]` with URI `https://www.peacprotocol.org/ext/traceability/v1` +4. If found, return capabilities from the extension's `params` field + +The v0.3.0 legacy fallback `{baseUrl}/.well-known/agent.json` was removed in v0.13.0 alongside the rest of the A2A v0.3.0 compatibility surface (DD-186); deployers still serving only the legacy path should publish the canonical path. **Result source:** `agent_card` diff --git a/docs/specs/EVIDENCE-CARRIER-CONTRACT.md b/docs/specs/EVIDENCE-CARRIER-CONTRACT.md index f52290d16..cb8d33bfd 100644 --- a/docs/specs/EVIDENCE-CARRIER-CONTRACT.md +++ b/docs/specs/EVIDENCE-CARRIER-CONTRACT.md @@ -290,7 +290,7 @@ Carriers are placed in the `metadata` map of A2A messages using the PEAC extensi } ``` -The extension URI (`https://www.peacprotocol.org/ext/traceability/v1`) is registered in the A2A Agent Card's `capabilities.extensions[]` array per A2A spec v0.3.0. +The extension URI (`https://www.peacprotocol.org/ext/traceability/v1`) is registered in the A2A Agent Card's `capabilities.extensions[]` array per the A2A v1.0.0 extension registration convention (originally defined in v0.3.0 and preserved on the v1.0.0 wire). ### 7.3 ACP (Agentic Commerce Protocol) diff --git a/docs/specs/INTEROP.md b/docs/specs/INTEROP.md index da882e585..9f7eaeb69 100644 --- a/docs/specs/INTEROP.md +++ b/docs/specs/INTEROP.md @@ -148,7 +148,7 @@ const result = rslToControlPurposes(['ai-all', 'search']); ### 5.3 Package -`@peac/mappings-a2a` provides A2A v1.0.0 artifact embedding, Agent Card discovery, and carrier extraction. A2A v0.3.0 compat shim retained through v0.12.x; removal in v0.13.0. +`@peac/mappings-a2a` provides A2A v1.0.0 artifact embedding, Agent Card discovery, and carrier extraction. The A2A v0.3.0 compatibility shim was deprecated in v0.12.3 and removed in v0.13.0 (DD-186). --- diff --git a/integrator-kits/a2a/README.md b/integrator-kits/a2a/README.md index ab6b853b5..97c7c1055 100644 --- a/integrator-kits/a2a/README.md +++ b/integrator-kits/a2a/README.md @@ -6,7 +6,7 @@ Carry signed receipts across A2A agent flows: declare PEAC support in your Agent PEAC integrates with A2A at the metadata layer. Receipts travel as Evidence Carriers inside A2A TaskStatus metadata, using the reverse-DNS extension URI `org.peacprotocol`. No A2A protocol changes are required; PEAC uses the standard metadata extension mechanism. -**Compatibility:** `@peac/mappings-a2a` targets A2A v1.0.0 (shipped in v0.12.3). A2A v0.3.0 compat shim is retained through v0.12.x; removal scheduled for v0.13.0. +**Compatibility:** `@peac/mappings-a2a` targets A2A v1.0.0 only (shipped in v0.12.3; deprecated v0.3.0 compat shim removed in v0.13.0 per DD-186). ## Prerequisites diff --git a/packages/cli/tsconfig.types.json b/packages/cli/tsconfig.types.json index 30d81005b..1ae35fde5 100644 --- a/packages/cli/tsconfig.types.json +++ b/packages/cli/tsconfig.types.json @@ -18,10 +18,9 @@ "noEmit": false, "paths": { - "@peac/core": ["../core/dist/index.d.ts"], "@peac/disc": ["../discovery/dist/index.d.ts"] } }, "include": ["src/**/*"], - "references": [{ "path": "../core" }, { "path": "../discovery" }] + "references": [{ "path": "../discovery" }] } diff --git a/packages/mappings/a2a/README.md b/packages/mappings/a2a/README.md index 98c24c3af..558d9ce28 100644 --- a/packages/mappings/a2a/README.md +++ b/packages/mappings/a2a/README.md @@ -10,13 +10,13 @@ pnpm add @peac/mappings-a2a ## What It Does -`@peac/mappings-a2a` bridges PEAC signed interaction receipts and the Agent-to-Agent Protocol (A2A). It normalizes A2A v0.3.0 and v1.0.0 structures into a consistent shape, attaches and extracts PEAC evidence carriers from A2A metadata, and discovers PEAC capabilities advertised by remote agents. All carrier operations enforce transport constraints (64 KB embed limit for A2A). +`@peac/mappings-a2a` bridges PEAC signed interaction receipts and the Agent-to-Agent Protocol (A2A). It validates A2A v1.0.0 Agent Card structures, attaches and extracts PEAC evidence carriers from A2A metadata, and discovers PEAC capabilities advertised by remote agents. All carrier operations enforce transport constraints (64 KB embed limit for A2A). -## How Do I Use It? +A2A v0.3.0 compatibility (dual-version Agent Cards, top-level `url`, kebab-case TaskState normalization, `/.well-known/agent.json` legacy discovery path) was deprecated in v0.12.3 and removed in v0.13.0 (DD-186). This package accepts A2A v1.0.0 shapes only; see [`docs/MIGRATION_CURRENT.md`](../../../docs/MIGRATION_CURRENT.md) for the migration guide. -### Normalize an A2A Agent Card +## How Do I Use It? -Accept either v0.3.0 or v1.0.0 Agent Cards and get a consistent interface: +### Validate an A2A v1.0.0 Agent Card ```typescript import { normalizeAgentCard } from '@peac/mappings-a2a'; @@ -30,21 +30,10 @@ const card = { const normalized = normalizeAgentCard(card); if (normalized) { - console.log(normalized.version); // '1.0.0' console.log(normalized.url); // 'https://billing.example.com' } -``` - -### Normalize a task state across versions - -```typescript -import { normalizeTaskState } from '@peac/mappings-a2a'; - -// v0.3.0 value normalized to v1.0.0 canonical form -console.log(normalizeTaskState('working')); // 'TASK_STATE_WORKING' - -// v1.0.0 values pass through unchanged -console.log(normalizeTaskState('TASK_STATE_COMPLETED')); // 'TASK_STATE_COMPLETED' +// Cards without a valid supportedInterfaces[0].url (including legacy +// v0.3.0 cards that used top-level `url`) return null. ``` ### Attach a receipt to A2A TaskStatus metadata @@ -114,7 +103,7 @@ if (result) { If you are building an AI agent that communicates via A2A and needs signed interaction receipts: -- Use `normalizeAgentCard()` and `normalizeTaskState()` to handle both A2A v0.3.0 and v1.0.0 inputs +- Use `normalizeAgentCard()` to validate incoming A2A v1.0.0 Agent Cards (cards without a valid `supportedInterfaces[0].url` return `null`) - Use `attachReceiptToTaskStatus()` to embed evidence in outgoing A2A messages - Use `extractReceiptFromMetadata()` to retrieve evidence from incoming A2A messages - Use `A2ACarrierAdapter` for a transport-agnostic carrier interface diff --git a/packages/mappings/a2a/src/attach.ts b/packages/mappings/a2a/src/attach.ts index a565d4d03..7a7cc8823 100644 --- a/packages/mappings/a2a/src/attach.ts +++ b/packages/mappings/a2a/src/attach.ts @@ -1,9 +1,10 @@ /** * Attach PEAC evidence carriers to A2A metadata. * - * Places carrier data under metadata[PEAC_EXTENSION_URI] per A2A v0.3.0 - * metadata convention. Uses computeReceiptRef() from @peac/schema - * for receipt reference computation (correction item 4). + * Places carrier data under metadata[PEAC_EXTENSION_URI] per the A2A + * extension-URI metadata convention (inherited from v0.3.0 and carried + * forward in v1.0.0). Uses computeReceiptRef() from @peac/schema for + * receipt reference computation. */ import type { PeacEvidenceCarrier, CarrierMeta } from '@peac/kernel'; diff --git a/packages/mappings/a2a/src/discovery.ts b/packages/mappings/a2a/src/discovery.ts index 1f48a2ab4..68f01beb4 100644 --- a/packages/mappings/a2a/src/discovery.ts +++ b/packages/mappings/a2a/src/discovery.ts @@ -129,8 +129,11 @@ async function validateUrlForDiscovery(url: string, options: DiscoveryOptions): /** * Discover A2A Agent Card from a base URL. * - * Tries `/.well-known/agent-card.json` first, then `/.well-known/agent.json` - * as a legacy fallback per A2A v0.3.0. + * Fetches the A2A v1.0.0 canonical path `/.well-known/agent-card.json`. + * The v0.3.0 legacy fallback `/.well-known/agent.json` was removed in + * v0.13.0 (DD-186) alongside the rest of the v0.3.0 compatibility + * surface; deployers still serving the legacy path should publish the + * canonical path or upgrade to an A2A v1.0.0 implementation. * * SSRF protection per Polish C: * 1. Private IP blocking (literal check; DNS resolution when resolveHostname provided) @@ -146,7 +149,7 @@ export async function discoverAgentCard( options: DiscoveryOptions = {} ): Promise { const fetchFn = options.fetch ?? globalThis.fetch; - const paths = ['/.well-known/agent-card.json', '/.well-known/agent.json']; + const paths = ['/.well-known/agent-card.json']; for (const path of paths) { const url = new URL(path, baseUrl).toString(); @@ -186,7 +189,10 @@ export async function discoverAgentCard( const card = JSON.parse(text) as A2AAgentCard; - // Dual-version validation (DD-186): accept v0.3.0 (url) or v1.0.0 (supportedInterfaces) + // v1.0.0 validation (DD-186; v0.3.0 rejected). Cards MUST expose a + // valid `supportedInterfaces[0].url`. Cards carrying only a legacy + // top-level `url` (the v0.3.0 shape) return null from + // normalizeAgentCard and are skipped here. if (typeof card.name !== 'string') { continue; } diff --git a/packages/mappings/a2a/src/index.ts b/packages/mappings/a2a/src/index.ts index b06a32a4f..3b8014cf0 100644 --- a/packages/mappings/a2a/src/index.ts +++ b/packages/mappings/a2a/src/index.ts @@ -3,7 +3,8 @@ * * Agent-to-Agent Protocol (A2A) integration for PEAC. * Maps PEAC evidence carriers to A2A metadata (TaskStatus, Message, Artifact). - * Supports both A2A v0.3.0 and v1.0.0 (DD-186 dual-version transition). + * A2A v1.0.0 only. The v0.3.0 compatibility path (DD-186) was deprecated + * in v0.12.3 and removed in v0.13.0; see `docs/MIGRATION_CURRENT.md`. */ // Types @@ -24,19 +25,12 @@ export { A2A_MAX_CARRIER_SIZE, A2A_V1_TASK_STATE, A2A_V1_MESSAGE_ROLE, - TASK_STATE_V03_TO_V1, } from './types'; -// Normalizers (DD-186: v0.3.0 + v1.0.0 dual-version) +// Normalizers (A2A v1.0.0 only) export type { NormalizedAgentCard } from './normalizers'; -export { - isV1AgentCard, - normalizeAgentCard, - selectBestInterface, - normalizeTaskState, - _resetDeprecationWarning, -} from './normalizers'; +export { isV1AgentCard, normalizeAgentCard, selectBestInterface } from './normalizers'; // Attach export { diff --git a/packages/mappings/a2a/src/normalizers.ts b/packages/mappings/a2a/src/normalizers.ts index 332a7d56a..cac9d6cd8 100644 --- a/packages/mappings/a2a/src/normalizers.ts +++ b/packages/mappings/a2a/src/normalizers.ts @@ -1,100 +1,63 @@ /** - * A2A v1.0.0 transition normalizers (DD-186). + * A2A v1.0.0 Agent Card normalizers. * - * Accept both v0.3.0 and v1.0.0 Agent Card, TaskState, and Part shapes. - * v0.3.0 inputs emit a deprecation warning. v0.3.0 removal at v0.13.0. + * The v0.3.0 compatibility path (DD-186; `url` top-level field plus + * kebab-case TaskState mapping) was deprecated in v0.12.3 and removed + * in v0.13.0. Every accepted Agent Card must expose a non-empty + * `supportedInterfaces[0].url`; cards without it are rejected. The + * v0.3.0-to-v1.0.0 TaskState mapping function was removed because its + * only job was translating kebab-case inputs, which are no longer + * accepted. TaskState values are now v1.0.0 SCREAMING_SNAKE_CASE + * strings carried by the caller. */ import type { A2AAgentCard, A2ASupportedInterface } from './types'; -import { TASK_STATE_V03_TO_V1 } from './types'; - -// --------------------------------------------------------------------------- -// Deprecation warning (fires once per process) -// --------------------------------------------------------------------------- - -let v03DeprecationWarned = false; - -function warnV03Deprecated(context: string): void { - if (!v03DeprecationWarned) { - v03DeprecationWarned = true; - process.emitWarning( - `A2A v0.3.0 ${context} detected. Migrate to v1.0.0. ` + - 'v0.3.0 support will be removed in @peac/mappings-a2a v0.13.0.', - 'DeprecationWarning' - ); - } -} - -/** Reset deprecation warning state (for testing only) */ -export function _resetDeprecationWarning(): void { - v03DeprecationWarned = false; -} // --------------------------------------------------------------------------- // Agent Card normalizer // --------------------------------------------------------------------------- -/** Normalized Agent Card with a resolved URL regardless of version */ +/** Normalized Agent Card with a resolved v1.0.0 URL. */ export interface NormalizedAgentCard { name: string; url: string; - version: '0.3.0' | '1.0.0'; supportedInterfaces: A2ASupportedInterface[]; original: A2AAgentCard; } /** - * Detect whether an Agent Card uses v1.0.0 structure. - * - * v1.0.0 cards have `supportedInterfaces[]` instead of top-level `url`. + * Returns true if the card satisfies the v1.0.0 Agent Card contract: + * `supportedInterfaces[0].url` exists and is a non-empty string. Cards + * that fail this check are rejected by `normalizeAgentCard` (they are + * not v0.3.0 fallbacks, which are no longer accepted). */ export function isV1AgentCard(card: A2AAgentCard): boolean { return ( Array.isArray(card.supportedInterfaces) && card.supportedInterfaces.length > 0 && - typeof card.supportedInterfaces[0]?.url === 'string' + typeof card.supportedInterfaces[0]?.url === 'string' && + card.supportedInterfaces[0].url.length > 0 ); } /** - * Normalize an A2A Agent Card from either v0.3.0 or v1.0.0 format. + * Normalize an A2A v1.0.0 Agent Card. * - * Returns a consistent shape with a resolved URL. v0.3.0 cards emit - * a deprecation warning on first encounter. - * - * Returns null if the card has neither a valid `url` nor valid - * `supportedInterfaces[0].url`. + * Returns a consistent shape with the resolved primary URL. Cards that + * do not satisfy `isV1AgentCard(...)` return `null` (this includes the + * legacy v0.3.0 shape with only a top-level `url`, which is no longer + * supported). Callers receiving `null` should treat the card as invalid + * and surface a structured error rather than falling back to v0.3.0. */ export function normalizeAgentCard(card: A2AAgentCard): NormalizedAgentCard | null { - if (isV1AgentCard(card)) { - const iface = card.supportedInterfaces![0]!; - return { - name: card.name, - url: iface.url, - version: '1.0.0', - supportedInterfaces: card.supportedInterfaces!, - original: card, - }; - } - - if (typeof card.url === 'string') { - warnV03Deprecated('Agent Card (top-level url)'); - return { - name: card.name, - url: card.url, - version: '0.3.0', - supportedInterfaces: [ - { - url: card.url, - protocolBinding: 'http+json', - protocolVersion: '0.3.0', - }, - ], - original: card, - }; - } - - return null; + if (!isV1AgentCard(card)) return null; + const iface = card.supportedInterfaces![0]!; + return { + name: card.name, + url: iface.url, + supportedInterfaces: card.supportedInterfaces!, + original: card, + }; } /** @@ -111,25 +74,3 @@ export function selectBestInterface( b.protocolVersion.localeCompare(a.protocolVersion, undefined, { numeric: true }) )[0]!; } - -// --------------------------------------------------------------------------- -// TaskState normalizer -// --------------------------------------------------------------------------- - -/** - * Normalize a task state string from v0.3.0 or v1.0.0 format. - * - * v0.3.0 uses kebab-case (e.g., "working"), v1.0.0 uses prefixed - * SCREAMING_SNAKE_CASE (e.g., "TASK_STATE_WORKING"). - * - * Returns the v1.0.0 canonical form. Unrecognized values pass through - * unchanged. - */ -export function normalizeTaskState(state: string): string { - const v1 = TASK_STATE_V03_TO_V1[state]; - if (v1) { - warnV03Deprecated('TaskState value'); - return v1; - } - return state; -} diff --git a/packages/mappings/a2a/src/types.ts b/packages/mappings/a2a/src/types.ts index c8cd051e3..1df3d8179 100644 --- a/packages/mappings/a2a/src/types.ts +++ b/packages/mappings/a2a/src/types.ts @@ -1,12 +1,16 @@ /** - * A2A types supporting both v0.3.0 and v1.0.0. + * A2A v1.0.0 types. * * These types are defined locally rather than importing @a2a-js/sdk, * which brings protobuf + gRPC + express peer dependencies that are * not needed for evidence placement. * - * v1.0.0 transition (DD-186): dual-version acceptance with v0.3.0 - * deprecated via process.emitWarning(). v0.3.0 removal at v0.13.0. + * A2A v0.3.0 compatibility (DD-186) was deprecated in v0.12.3 and + * removed in v0.13.0 (DD-186 removal target honored). Agent Cards + * carrying only a top-level `url` (the v0.3.0 shape) are no longer + * accepted; every card must expose `supportedInterfaces[0].url`. + * TaskState values must use the v1.0.0 prefixed SCREAMING_SNAKE_CASE + * form; v0.3.0 kebab-case values are no longer mapped. */ import type { CarrierFormat, PeacEvidenceCarrier, CarrierMeta } from '@peac/kernel'; @@ -22,7 +26,7 @@ export const PEAC_EXTENSION_URI = 'https://www.peacprotocol.org/ext/traceability export const A2A_MAX_CARRIER_SIZE = 65_536; // --------------------------------------------------------------------------- -// A2A Agent Card types (v0.3.0 + v1.0.0) +// A2A Agent Card types (v1.0.0) // --------------------------------------------------------------------------- /** A2A extension entry in capabilities.extensions[] */ @@ -53,16 +57,22 @@ export interface A2ASupportedInterface { } /** - * Minimal A2A Agent Card shape (v0.3.0 + v1.0.0). + * Minimal A2A Agent Card shape (v1.0.0). * - * v0.3.0 cards have top-level `url`. v1.0.0 cards replace it with - * `supportedInterfaces[]`. Both shapes are accepted; v0.3.0 emits - * a deprecation warning. v0.3.0 support removal at v0.13.0. + * v1.0.0 cards expose endpoint bindings via `supportedInterfaces[]`. + * Every accepted card MUST carry a non-empty `supportedInterfaces[0].url`. + * + * The legacy v0.3.0 top-level `url` field is no longer a declared field + * on this interface. Rejection is **runtime** (`normalizeAgentCard` returns + * null and `discoverAgentCard` skips the card), not type-level. The + * interface intentionally keeps a `[key: string]: unknown` index + * signature so consumers may pass incoming JSON with unknown extra + * fields without TypeScript errors, so a literal with a stray `url` + * string property still typechecks. The v0.3.0 removal is enforced by + * the normalization layer, not by the type system. */ export interface A2AAgentCard { name: string; - /** @deprecated v0.3.0 field. Use supportedInterfaces[0].url in v1.0.0. */ - url?: string; supportedInterfaces?: A2ASupportedInterface[]; capabilities?: { extensions?: AgentCardExtension[]; @@ -124,17 +134,9 @@ export const A2A_V1_MESSAGE_ROLE = { AGENT: 'ROLE_AGENT', } as const; -/** Map from v0.3.0 kebab-case task states to v1.0.0 prefixed form */ -export const TASK_STATE_V03_TO_V1: Record = { - submitted: A2A_V1_TASK_STATE.SUBMITTED, - working: A2A_V1_TASK_STATE.WORKING, - completed: A2A_V1_TASK_STATE.COMPLETED, - failed: A2A_V1_TASK_STATE.FAILED, - canceled: A2A_V1_TASK_STATE.CANCELED, - rejected: A2A_V1_TASK_STATE.REJECTED, - 'input-required': A2A_V1_TASK_STATE.INPUT_REQUIRED, - 'auth-required': A2A_V1_TASK_STATE.AUTH_REQUIRED, -}; +// The v0.3.0 → v1.0.0 TaskState mapping (previously TASK_STATE_V03_TO_V1) +// was removed in v0.13.0 alongside the v0.3.0 compat path (DD-186). +// Callers MUST supply v1.0.0 TaskState values directly. // --------------------------------------------------------------------------- // Carrier payload type diff --git a/packages/mappings/a2a/tests/discovery.test.ts b/packages/mappings/a2a/tests/discovery.test.ts index d5741840e..09b0853ed 100644 --- a/packages/mappings/a2a/tests/discovery.test.ts +++ b/packages/mappings/a2a/tests/discovery.test.ts @@ -40,7 +40,13 @@ function createMockFetch( const VALID_AGENT_CARD: A2AAgentCard = { name: 'Test Agent', - url: 'https://agent.example.com', + supportedInterfaces: [ + { + url: 'https://agent.example.com', + protocolBinding: 'http+json', + protocolVersion: '1.0.0', + }, + ], capabilities: { extensions: [ { @@ -54,7 +60,13 @@ const VALID_AGENT_CARD: A2AAgentCard = { const AGENT_CARD_NO_PEAC: A2AAgentCard = { name: 'Other Agent', - url: 'https://other.example.com', + supportedInterfaces: [ + { + url: 'https://other.example.com', + protocolBinding: 'http+json', + protocolVersion: '1.0.0', + }, + ], capabilities: { extensions: [ { @@ -87,7 +99,11 @@ describe('discoverAgentCard', () => { expect(card!.name).toBe('Test Agent'); }); - it('falls back to /.well-known/agent.json', async () => { + it('does NOT fall back to the legacy v0.3.0 /.well-known/agent.json path', async () => { + // v0.3.0 discovery fallback was removed in v0.13.0 (DD-186). A deployer + // serving the card only at /.well-known/agent.json (the v0.3.0 path) is + // no longer discoverable; the canonical v1.0.0 path + // /.well-known/agent-card.json is the only path consulted. const mockFetch = createMockFetch( new Map([ [ @@ -100,8 +116,7 @@ describe('discoverAgentCard', () => { const card = await discoverAgentCard('https://agent.example.com', { fetch: mockFetch, }); - expect(card).not.toBeNull(); - expect(card!.name).toBe('Test Agent'); + expect(card).toBeNull(); }); it('returns null when no agent card found', async () => { @@ -289,7 +304,13 @@ describe('getPeacExtension', () => { const AGENT_CARD_WITH_PARAMS: A2AAgentCard = { name: 'Full Agent', - url: 'https://agent.example.com', + supportedInterfaces: [ + { + url: 'https://agent.example.com', + protocolBinding: 'http+json', + protocolVersion: '1.0.0', + }, + ], capabilities: { extensions: [ { diff --git a/packages/mappings/a2a/tests/normalizers.test.ts b/packages/mappings/a2a/tests/normalizers.test.ts index eb2824f3a..c6ceb58cb 100644 --- a/packages/mappings/a2a/tests/normalizers.test.ts +++ b/packages/mappings/a2a/tests/normalizers.test.ts @@ -1,17 +1,14 @@ -import { describe, it, expect, beforeEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { isV1AgentCard, normalizeAgentCard, selectBestInterface, - normalizeTaskState, - _resetDeprecationWarning, - A2A_V1_TASK_STATE, type A2AAgentCard, type A2ASupportedInterface, } from '../src/index'; describe('isV1AgentCard', () => { - it('returns true for v1.0.0 card with supportedInterfaces', () => { + it('returns true for v1.0.0 card with supportedInterfaces[0].url', () => { const card: A2AAgentCard = { name: 'Test Agent', supportedInterfaces: [ @@ -21,28 +18,35 @@ describe('isV1AgentCard', () => { expect(isV1AgentCard(card)).toBe(true); }); - it('returns false for v0.3.0 card with top-level url', () => { + it('returns false for a card with empty supportedInterfaces', () => { const card: A2AAgentCard = { name: 'Test Agent', - url: 'https://agent.example.com', + supportedInterfaces: [], }; expect(isV1AgentCard(card)).toBe(false); }); - it('returns false for card with empty supportedInterfaces', () => { + it('returns false for a card without supportedInterfaces (legacy v0.3.0 shape, no longer accepted)', () => { + // A card with only a top-level `url` (the v0.3.0 shape) is not a + // valid v1.0.0 Agent Card. v0.3.0 compatibility was removed in + // v0.13.0 (DD-186); isV1AgentCard MUST NOT claim this shape is v1. + const card = { + name: 'Legacy Agent', + url: 'https://legacy.example.com', + } as unknown as A2AAgentCard; + expect(isV1AgentCard(card)).toBe(false); + }); + + it('returns false when supportedInterfaces[0].url is empty', () => { const card: A2AAgentCard = { - name: 'Test Agent', - supportedInterfaces: [], + name: 'Broken Agent', + supportedInterfaces: [{ url: '', protocolBinding: 'http+json', protocolVersion: '1.0' }], }; expect(isV1AgentCard(card)).toBe(false); }); }); describe('normalizeAgentCard', () => { - beforeEach(() => { - _resetDeprecationWarning(); - }); - it('normalizes v1.0.0 card with supportedInterfaces', () => { const card: A2AAgentCard = { name: 'V1 Agent', @@ -52,23 +56,20 @@ describe('normalizeAgentCard', () => { }; const result = normalizeAgentCard(card); expect(result).not.toBeNull(); - expect(result!.version).toBe('1.0.0'); expect(result!.url).toBe('https://v1.example.com'); expect(result!.name).toBe('V1 Agent'); expect(result!.original).toBe(card); }); - it('normalizes v0.3.0 card with top-level url', () => { - const card: A2AAgentCard = { + it('rejects v0.3.0 card with only top-level url (DD-186 removal at v0.13.0)', () => { + // v0.3.0 cards carried `url` at the top level. v0.13.0 removes this + // compatibility path: normalizeAgentCard returns null instead of + // synthesizing a supportedInterfaces entry from the legacy url. + const card = { name: 'Legacy Agent', url: 'https://legacy.example.com', - }; - const result = normalizeAgentCard(card); - expect(result).not.toBeNull(); - expect(result!.version).toBe('0.3.0'); - expect(result!.url).toBe('https://legacy.example.com'); - expect(result!.supportedInterfaces).toHaveLength(1); - expect(result!.supportedInterfaces[0]!.protocolVersion).toBe('0.3.0'); + } as unknown as A2AAgentCard; + expect(normalizeAgentCard(card)).toBeNull(); }); it('returns null for card with neither url nor supportedInterfaces', () => { @@ -76,16 +77,18 @@ describe('normalizeAgentCard', () => { expect(normalizeAgentCard(card)).toBeNull(); }); - it('prefers supportedInterfaces over url when both present', () => { - const card: A2AAgentCard = { - name: 'Both Agent', + it('ignores a legacy top-level url when supportedInterfaces is present', () => { + // A hybrid card (v0.3.0 url plus v1.0.0 supportedInterfaces) is + // treated as v1.0.0 by the normalizer. The top-level url is no + // longer consulted. + const card = { + name: 'Hybrid Agent', url: 'https://old.example.com', supportedInterfaces: [ { url: 'https://new.example.com', protocolBinding: 'http+json', protocolVersion: '1.0' }, ], - }; + } as A2AAgentCard; const result = normalizeAgentCard(card); - expect(result!.version).toBe('1.0.0'); expect(result!.url).toBe('https://new.example.com'); }); @@ -139,29 +142,3 @@ describe('selectBestInterface', () => { expect(selectBestInterface(interfaces)!.url).toBe('https://only.example.com'); }); }); - -describe('normalizeTaskState', () => { - beforeEach(() => { - _resetDeprecationWarning(); - }); - - it('converts v0.3.0 kebab-case to v1.0.0 prefixed form', () => { - expect(normalizeTaskState('submitted')).toBe(A2A_V1_TASK_STATE.SUBMITTED); - expect(normalizeTaskState('working')).toBe(A2A_V1_TASK_STATE.WORKING); - expect(normalizeTaskState('completed')).toBe(A2A_V1_TASK_STATE.COMPLETED); - expect(normalizeTaskState('failed')).toBe(A2A_V1_TASK_STATE.FAILED); - expect(normalizeTaskState('canceled')).toBe(A2A_V1_TASK_STATE.CANCELED); - expect(normalizeTaskState('rejected')).toBe(A2A_V1_TASK_STATE.REJECTED); - expect(normalizeTaskState('input-required')).toBe(A2A_V1_TASK_STATE.INPUT_REQUIRED); - expect(normalizeTaskState('auth-required')).toBe(A2A_V1_TASK_STATE.AUTH_REQUIRED); - }); - - it('passes through v1.0.0 values unchanged', () => { - expect(normalizeTaskState('TASK_STATE_WORKING')).toBe('TASK_STATE_WORKING'); - expect(normalizeTaskState('TASK_STATE_COMPLETED')).toBe('TASK_STATE_COMPLETED'); - }); - - it('passes through unknown values unchanged', () => { - expect(normalizeTaskState('custom-state')).toBe('custom-state'); - }); -}); diff --git a/packages/schema/__tests__/actor-binding.test.ts b/packages/schema/__tests__/actor-binding.test.ts index 7efa83136..67ebd5457 100644 --- a/packages/schema/__tests__/actor-binding.test.ts +++ b/packages/schema/__tests__/actor-binding.test.ts @@ -51,9 +51,10 @@ describe('ProofTypeSchema', () => { expect(PROOF_TYPES).toContain('custom'); }); - it('should be separate from ProofMethodSchema (no overlap with transport methods)', () => { - // ProofTypeSchema covers trust root models, not transport mechanisms - // ProofMethodSchema covers: http-message-signature, dpop, mtls, jwk-thumbprint + it('should be separate from the transport-binding method enum (no overlap)', () => { + // ProofTypeSchema covers trust-root models; transport-binding methods + // (http-message-signature / dpop / mtls / jwk-thumbprint) are inlined + // on AgentProofSchema.method and are NOT valid ProofType values. expect(() => ProofTypeSchema.parse('http-message-signature')).toThrow(); expect(() => ProofTypeSchema.parse('dpop')).toThrow(); expect(() => ProofTypeSchema.parse('mtls')).toThrow(); diff --git a/packages/schema/__tests__/agent-identity.test.ts b/packages/schema/__tests__/agent-identity.test.ts index ed6e91c83..a01a6a606 100644 --- a/packages/schema/__tests__/agent-identity.test.ts +++ b/packages/schema/__tests__/agent-identity.test.ts @@ -4,7 +4,6 @@ import { describe, it, expect } from 'vitest'; import { ControlTypeSchema, - ProofMethodSchema, BindingDetailsSchema, AgentProofSchema, AgentIdentityEvidenceSchema, @@ -13,7 +12,6 @@ import { AgentIdentityVerifiedSchema, AGENT_IDENTITY_TYPE, CONTROL_TYPES, - PROOF_METHODS, validateAgentIdentityAttestation, isAgentIdentityAttestation, createAgentIdentityAttestation, @@ -43,21 +41,28 @@ describe('ControlTypeSchema', () => { }); }); -describe('ProofMethodSchema', () => { - it('should accept valid proof methods', () => { - expect(ProofMethodSchema.parse('http-message-signature')).toBe('http-message-signature'); - expect(ProofMethodSchema.parse('dpop')).toBe('dpop'); - expect(ProofMethodSchema.parse('mtls')).toBe('mtls'); - expect(ProofMethodSchema.parse('jwk-thumbprint')).toBe('jwk-thumbprint'); - }); +describe('AgentProofSchema.method (transport-binding enum; inlined v0.13.0)', () => { + // ProofMethodSchema was removed in v0.13.0 after the DD-185 deprecation + // horizon. The 4 transport-binding values are now inlined on + // AgentProofSchema.method. Runtime validation must still accept the same + // four values and reject everything else. + const validMethods = ['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint'] as const; + const baseProof = { + key_id: 'k1', + alg: 'EdDSA', + signature: 'sig', + }; - it('should reject invalid proof methods', () => { - expect(() => ProofMethodSchema.parse('unknown')).toThrow(); - expect(() => ProofMethodSchema.parse('signature')).toThrow(); + it('accepts the four canonical transport-binding methods', () => { + for (const method of validMethods) { + expect(AgentProofSchema.parse({ ...baseProof, method }).method).toBe(method); + } }); - it('should match PROOF_METHODS constant', () => { - expect(PROOF_METHODS).toEqual(['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint']); + it('rejects values outside the transport-binding enum', () => { + for (const method of ['unknown', 'signature', 'ed25519-cert-chain', '', 'custom']) { + expect(() => AgentProofSchema.parse({ ...baseProof, method })).toThrow(); + } }); }); diff --git a/packages/schema/__tests__/proof-method-deprecation.test.ts b/packages/schema/__tests__/proof-method-deprecation.test.ts deleted file mode 100644 index c7f39b485..000000000 --- a/packages/schema/__tests__/proof-method-deprecation.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -/** - * ProofMethodSchema deprecation contract tests - * - * Validates that the deprecated ProofMethodSchema, ProofMethod type, - * and PROOF_METHODS array remain functional and importable from the - * package barrel through v0.12.x. Imports use the barrel to prove - * public surface availability (not internal module paths). - */ - -import { describe, it, expect } from 'vitest'; -import { ProofMethodSchema, PROOF_METHODS } from '../src/index'; - -describe('ProofMethodSchema deprecation contract', () => { - it('is exported from barrel', () => { - expect(ProofMethodSchema).toBeDefined(); - expect(PROOF_METHODS).toBeDefined(); - }); - - it('parses all 4 valid transport-binding methods at runtime', () => { - const expected = ['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint'] as const; - for (const method of expected) { - expect(ProofMethodSchema.parse(method)).toBe(method); - } - }); - - it('rejects values not in the transport-binding enum', () => { - expect(() => ProofMethodSchema.parse('ed25519-cert-chain')).toThrow(); - expect(() => ProofMethodSchema.parse('custom')).toThrow(); - expect(() => ProofMethodSchema.parse('')).toThrow(); - expect(() => ProofMethodSchema.parse(123)).toThrow(); - expect(() => ProofMethodSchema.parse(null)).toThrow(); - expect(() => ProofMethodSchema.parse(undefined)).toThrow(); - }); - - it('PROOF_METHODS array matches schema enum values exactly', () => { - expect(PROOF_METHODS).toEqual(['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint']); - expect(PROOF_METHODS).toHaveLength(4); - }); - - it('PROOF_METHODS values all round-trip through schema', () => { - for (const method of PROOF_METHODS) { - expect(ProofMethodSchema.parse(method)).toBe(method); - } - }); - - it('safeParse returns success for valid values and failure for invalid', () => { - const valid = ProofMethodSchema.safeParse('dpop'); - expect(valid.success).toBe(true); - if (valid.success) { - expect(valid.data).toBe('dpop'); - } - - const invalid = ProofMethodSchema.safeParse('nonexistent'); - expect(invalid.success).toBe(false); - }); -}); diff --git a/packages/schema/openapi/verify.yaml b/packages/schema/openapi/verify.yaml index bada62427..8bced8117 100644 --- a/packages/schema/openapi/verify.yaml +++ b/packages/schema/openapi/verify.yaml @@ -21,9 +21,13 @@ info: Error responses use RFC 9457 Problem Details (`application/problem+json`) extended with PEAC-canonical fields (`peac_error_code`, `peac_trace_id`). - The legacy `POST /verify` endpoint is retained for compatibility and - carries RFC 9745 `Deprecation` and RFC 8594 `Sunset` response headers; - it is scheduled for removal no earlier than 2026-11-01. + The reference verifier also keeps a runtime-reachable `POST /verify` + alias for callers that have not yet migrated. The alias is omitted + from this contract because the canonical operation is + `POST /v1/verify`. Every response on the alias carries RFC 9745 + `Deprecation`, RFC 8594 `Sunset`, and RFC 8288 `Link` headers; the + alias is scheduled for runtime removal no earlier than 2026-11-01 + and is documented in `docs/HOSTED_VERIFY_CONTRACT.md`. Size caps (enforced by the kernel and verifier, documented here for contract completeness): @@ -43,7 +47,7 @@ servers: tags: - name: Verify - description: Verification endpoints (canonical and legacy). + description: Verification endpoint (canonical). - name: Issue description: Hosted issue endpoints (alpha). - name: Health @@ -196,62 +200,6 @@ paths: schema: $ref: '#/components/schemas/ProblemDetails' - /verify: - post: - operationId: verifyReceiptLegacy - tags: [Verify] - deprecated: true - summary: '[DEPRECATED] Legacy verify endpoint' - description: | - Legacy verification endpoint retained for backward compatibility. - New integrations MUST use `POST /v1/verify`. Every response from - this path carries RFC 9745 `Deprecation` and RFC 8594 `Sunset` - headers plus an RFC 8288 `Link` relation pointing at the migration - guide. Scheduled removal: no earlier than 2026-11-01. - Request body is `application/json`, identical shape to - `POST /v1/verify`. - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/VerifyRequest' - responses: - '200': - description: Verification completed (deprecated response shape). - headers: - Deprecation: - description: RFC 9745 marker indicating the endpoint is deprecated. - schema: - type: string - const: 'true' - Sunset: - description: RFC 8594 HTTP-date at which the endpoint is scheduled for removal. - schema: - type: string - example: 'Sat, 01 Nov 2026 00:00:00 GMT' - Link: - description: RFC 8288 Link header with rel="deprecation" pointing at the migration guide. - schema: - type: string - example: '; rel="deprecation"' - content: - application/json: - schema: - $ref: '#/components/schemas/VerifySuccessResponse' - '400': - description: Invalid request format. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - '422': - description: Verification failure. - content: - application/problem+json: - schema: - $ref: '#/components/schemas/ProblemDetails' - /v1/issue: post: operationId: issueReceipt diff --git a/packages/schema/src/actor-binding.ts b/packages/schema/src/actor-binding.ts index 32218a2af..ebe5da9d4 100644 --- a/packages/schema/src/actor-binding.ts +++ b/packages/schema/src/actor-binding.ts @@ -5,8 +5,13 @@ * and (MVIS) for the Agent Identity Profile. * * ActorBinding lives in ext["org.peacprotocol/actor_binding"] in Wire 0.1. - * ProofTypeSchema is SEPARATE from ProofMethodSchema (agent-identity.ts) - * to avoid breaking the v0.9.25+ API. Unification deferred to v0.12.0. + * ProofTypeSchema (trust-root taxonomy) is SEPARATE from the transport-binding + * method enum on AgentProofSchema.method (agent-identity.ts). Unifying them + * into a single taxonomy was considered and rejected in v0.13.0 (DD-185): + * transport-binding and trust-root concerns are semantically distinct and + * remain separate surfaces. The ProofMethodSchema / PROOF_METHODS / ProofMethod + * standalone exports were removed in v0.13.0; the four transport-binding + * values are now inlined directly on AgentProofSchema.method. * * @see docs/specs/AGENT-IDENTITY-PROFILE.md for normative specification */ @@ -22,9 +27,12 @@ import { z } from 'zod'; * 8 methods covering attestation chains, RATS, keyless signing, * decentralized identity, workload identity, PKI, and vendor-defined. * - * SEPARATE from ProofMethodSchema (4 transport-level methods in agent-identity.ts). - * ProofMethodSchema covers how proof is transported (HTTP sig, DPoP, mTLS, JWK thumbprint). - * ProofTypeSchema covers the trust root model used to establish identity. + * SEPARATE from the transport-binding method enum inlined on + * AgentProofSchema.method (agent-identity.ts). That enum covers how proof + * is transported (HTTP signatures, DPoP, mTLS, JWK thumbprint). + * ProofTypeSchema covers the trust-root model used to establish identity. + * The two concerns compose rather than conflict; see + * docs/specs/AGENT-IDENTITY-PROFILE.md §3.4 for the mapping table. * * The 'custom' type: implementers MUST document their proof semantics externally. * proof_ref SHOULD use a reverse-DNS namespace (e.g., 'com.example.vendor/proof-type-v1'). diff --git a/packages/schema/src/agent-identity.ts b/packages/schema/src/agent-identity.ts index e5f748650..879c27d55 100644 --- a/packages/schema/src/agent-identity.ts +++ b/packages/schema/src/agent-identity.ts @@ -28,33 +28,6 @@ export type ControlType = z.infer; */ export const CONTROL_TYPES = ['operator', 'user-delegated'] as const; -// ============================================================================= -// PROOF METHOD (v0.9.25+) -// ============================================================================= - -/** - * @deprecated ProofMethodSchema is deprecated as of v0.12.2. - * Transport-level binding methods (HTTP signatures, DPoP, mTLS, JWK thumbprint) - * are semantically distinct from trust-root models (ProofTypeSchema). - * This alias remains functional through v0.12.x. No consumer action required now. - * In v0.13.0, AgentProofSchema.method will migrate to either an inline enum - * or a dedicated TransportBindingMethodSchema. Remove-not-before: v0.13.0. - * - * @see ProofTypeSchema for the canonical trust-root model schema - */ -export const ProofMethodSchema = z.enum([ - 'http-message-signature', - 'dpop', - 'mtls', - 'jwk-thumbprint', -]); -export type ProofMethod = z.infer; - -/** - * @deprecated See ProofMethodSchema deprecation note. - */ -export const PROOF_METHODS = ['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint'] as const; - // ============================================================================= // BINDING DETAILS (v0.9.25+) // ============================================================================= @@ -94,10 +67,11 @@ export type BindingDetails = z.infer; export const AgentProofSchema = z .object({ /** - * Proof method used. - * @see ProofMethodSchema - deprecated in v0.12.2; will migrate in v0.13.0 + * Transport-binding proof method. The four values below are the + * canonical transport-layer bindings recognized by PEAC; they are + * semantically distinct from trust-root models (see `ProofTypeSchema`). */ - method: ProofMethodSchema, + method: z.enum(['http-message-signature', 'dpop', 'mtls', 'jwk-thumbprint']), /** Key ID (matches kid in JWS header or JWKS) */ key_id: z.string().min(1).max(256), diff --git a/packages/schema/src/index.ts b/packages/schema/src/index.ts index aea04d7ef..6492a878b 100644 --- a/packages/schema/src/index.ts +++ b/packages/schema/src/index.ts @@ -94,7 +94,6 @@ export type { EvidenceValidationResult, ReceiptClaimsType } from './validators'; // Agent Identity validators (v0.9.25+) export { ControlTypeSchema, - ProofMethodSchema, BindingDetailsSchema, AgentProofSchema, AgentIdentityEvidenceSchema, @@ -104,7 +103,6 @@ export { // Constants AGENT_IDENTITY_TYPE, CONTROL_TYPES, - PROOF_METHODS, // Helpers validateAgentIdentityAttestation, isAgentIdentityAttestation, @@ -115,7 +113,6 @@ export { } from './agent-identity'; export type { ControlType, - ProofMethod, BindingDetails, AgentProof, AgentIdentityEvidence, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b4ac1089..d6cc58d2a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -100,9 +100,6 @@ importers: '@hono/node-server': specifier: '>=1.19.13' version: 1.19.13(hono@4.12.12) - '@peac/core': - specifier: workspace:* - version: link:../../packages/core '@peac/crypto': specifier: workspace:* version: link:../../packages/crypto @@ -1260,40 +1257,6 @@ importers: specifier: ^4.0.0 version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@22.19.11)(tsx@4.21.0)(yaml@2.8.3) - packages/core: - dependencies: - '@peac/kernel': - specifier: workspace:* - version: link:../kernel - jose: - specifier: ^5.2.0 - version: 5.10.0 - lru-cache: - specifier: ^10.0.0 - version: 10.4.3 - zod: - specifier: ^4.3.6 - version: 4.3.6 - devDependencies: - '@types/jest': - specifier: ^30.0.0 - version: 30.0.0 - '@types/node': - specifier: ^22.19.11 - version: 22.19.11 - jest: - specifier: ^30.2.0 - version: 30.2.0(@types/node@22.19.11) - ts-jest: - specifier: ^29.0.0 - version: 29.4.5(@babel/core@7.29.0)(esbuild@0.27.0)(jest@30.2.0)(typescript@5.9.3) - tsup: - specifier: ^8.0.0 - version: 8.5.1(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.3) - typescript: - specifier: ^5.2.0 - version: 5.9.3 - packages/crypto: dependencies: '@noble/ed25519': @@ -8895,6 +8858,7 @@ packages: /lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + dev: true /lru-cache@11.2.5: resolution: {integrity: sha512-vFrFJkWtJvJnD5hg+hJvVE8Lh/TcMzKnTgCWmtBipwI5yLX/iX+5UB2tfuyODF5E7k9xEzMdYgGqaSb1c0c5Yw==} diff --git a/scripts/assert-core-exports.mjs b/scripts/assert-core-exports.mjs deleted file mode 100755 index 0f7119deb..000000000 --- a/scripts/assert-core-exports.mjs +++ /dev/null @@ -1,104 +0,0 @@ -#!/usr/bin/env node -/** - * Assert @peac/core v0.9.14 exports are available - */ - -import { strict as assert } from 'node:assert'; - -async function main() { - console.log('Checking @peac/core exports...'); - - // Import the built module (avoid literal dist path for guard) - const parts = ['..', 'packages', 'core', 'dist', 'index.js']; - const core = await import(new URL(parts.join('/'), import.meta.url).href); - - // Required v0.9.14 exports - const requiredExports = [ - // Core functions - 'signReceipt', - 'createAndSignReceipt', - 'verifyReceipt', - 'enforce', - 'discover', - 'evaluate', - 'settle', - 'prove', - // Utilities - 'canonicalPolicyHash', - 'uuidv7', - 'isUUIDv7', - 'extractTimestamp', - ]; - - const requiredTypes = [ - 'Receipt', - 'Kid', - 'KeySet', - 'VerifyKeySet', - 'VerifyResult', - 'SignOptions', - 'SignReceiptOptions', - 'DiscoveryContext', - 'PolicySource', - 'EvaluationContext', - 'SettlementResult', - 'EnforceResult', - 'EnforceOptions', - 'PolicyInputs', - ]; - - // Check functions - for (const name of requiredExports) { - assert(typeof core[name] === 'function', `Missing export: ${name}`); - console.log(` [OK] ${name}`); - } - - // Check that types are exported (they won't be in the runtime, but build will fail if missing) - console.log('\nType exports (build-time check):'); - for (const type of requiredTypes) { - console.log(` [OK] ${type} (type)`); - } - - // Test basic functionality - console.log('\nTesting basic functionality...'); - - // Test signReceipt and verifyReceipt - try { - const receipt = await core.createAndSignReceipt({ - subject: 'https://example.com/test', - aipref: { status: 'allowed' }, - purpose: 'train-ai', - enforcement: { method: 'none' }, - kid: 'test-key', - privateKey: { - kty: 'OKP', - crv: 'Ed25519', - d: 'nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A', - x: '11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo', - }, - }); - - assert(typeof receipt === 'string', 'signReceipt should return a string'); - console.log(' [OK] createAndSignReceipt works'); - - // Test canonicalPolicyHash - const hash = core.canonicalPolicyHash({ test: 'data' }); - assert(typeof hash === 'string', 'canonicalPolicyHash should return a string'); - console.log(' [OK] canonicalPolicyHash works'); - - // Test uuidv7 - const uuid = core.uuidv7(); - assert(core.isUUIDv7(uuid), 'uuidv7 should generate valid UUIDv7'); - console.log(' [OK] uuidv7 works'); - - console.log('\n[OK] All @peac/core exports verified!'); - } catch (error) { - console.error('\n[FAIL] Functionality test failed:', error); - process.exit(1); - } -} - -main().catch((error) => { - console.error('[FAIL] Failed:', error); - process.exit(1); -}); diff --git a/scripts/bench-verify.ts b/scripts/bench-verify.ts deleted file mode 100644 index eba235a91..000000000 --- a/scripts/bench-verify.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * @peac/core v0.9.14 - Verification performance benchmark - * Measures p95 latency for verifyReceipt() with proper @peac/core imports - * - * @deprecated This benchmark uses @peac/core which is deprecated. See v0.9.15+ benchmarks. - */ - -import { performance } from 'node:perf_hooks'; -import { writeFileSync } from 'node:fs'; -// @ts-expect-error jose types not installed in root devDependencies -import { generateKeyPair, exportJWK } from 'jose'; - -// Robust monorepo-safe module loading -async function loadCore() { - // 1) Normal resolution (if root has dep / after publish) - try { - // @ts-expect-error @peac/core is deprecated, use @peac/protocol - return await import('@peac/core'); - } catch { - /* fallback below */ - } - - // 2) Workspace package root (Node resolves via package.json "exports") - try { - // @ts-expect-error import.meta requires ESM module setting - const pkgRoot = new URL('../packages/core/', import.meta.url); - console.log('Loading @peac/core from workspace package root'); - return await import(pkgRoot.href); - } catch { - /* fallback below */ - } - - // 3) TS source (no build needed; tsx compiles it) - console.log('Fallback: Loading @peac/core from TypeScript source'); - // @ts-expect-error import.meta requires ESM module setting - const srcUrl = new URL('../packages/core/src/index.ts', import.meta.url); - return await import(srcUrl.href); -} - -const ITERATIONS = 300; -const WARMUP_ITERATIONS = 30; - -async function benchmark() { - // Load core module first - const core = await loadCore(); - const { verifyReceipt, createAndSignReceipt } = core; - - // 30s watchdog to ensure P95 is always printed - const kill = setTimeout(() => { - console.log('P95: 999'); // Timeout sentinel - process.exit(1); - }, 30_000); - - // Ensure P95 is always printed even on early exit - process.on('uncaughtException', (err) => { - console.error('Error:', err.message); - clearTimeout(kill); - console.log('P95: 999'); // Failure sentinel - process.exit(1); - }); - - console.log('Setting up test data...'); - - // Generate test key pair - const { privateKey, publicKey } = await generateKeyPair('EdDSA'); - const jwkPriv = await exportJWK(privateKey); - const jwkPub = await exportJWK(publicKey); - - jwkPriv.alg = 'EdDSA'; - jwkPriv.kid = 'bench-1'; - jwkPub.alg = 'EdDSA'; - jwkPub.kid = 'bench-1'; - - const keyId = 'bench-1'; - const keyPair = jwkPriv; - - // Create test receipt - const testReceipt = await createAndSignReceipt({ - subject: 'https://example.com/test-resource', - aipref: { status: 'allowed' }, - purpose: 'train-ai', - enforcement: { method: 'none' }, - kid: keyId, - privateKey: keyPair, - }); - - // Prepare verification keys (public only) - const verifyKeys = { - [keyId]: jwkPub, - }; - - console.log('Warming up...'); - - // Warmup - for (let i = 0; i < WARMUP_ITERATIONS; i++) { - await verifyReceipt(testReceipt, verifyKeys); - } - - console.log(`Running ${ITERATIONS} verification operations...`); - - const timings: number[] = []; - - for (let i = 0; i < ITERATIONS; i++) { - const start = performance.now(); - await verifyReceipt(testReceipt, verifyKeys); - const end = performance.now(); - timings.push(end - start); - } - - // Calculate statistics - timings.sort((a, b) => a - b); - - const min = timings[0]; - const max = timings[timings.length - 1]; - const avg = timings.reduce((sum, t) => sum + t, 0) / timings.length; - const p50 = timings[Math.floor(timings.length * 0.5)]; - const p95 = timings[Math.floor(timings.length * 0.95)]; - const p99 = timings[Math.floor(timings.length * 0.99)]; - - const targetP95 = parseFloat(process.env.P95_MAX || '5.0'); - - const results = { - timestamp: new Date().toISOString(), - version: '0.9.14', - operations: ITERATIONS, - timings_ms: { - min: Number(min.toFixed(3)), - max: Number(max.toFixed(3)), - avg: Number(avg.toFixed(3)), - p50: Number(p50.toFixed(3)), - p95: Number(p95.toFixed(3)), - p99: Number(p99.toFixed(3)), - }, - target_p95_ms: targetP95, - passes_target: p95 < targetP95, - }; - - console.log('\nPerformance Results:'); - console.log(` Min: ${min.toFixed(3)}ms`); - console.log(` Max: ${max.toFixed(3)}ms`); - console.log(` Avg: ${avg.toFixed(3)}ms`); - console.log(` P50: ${p50.toFixed(3)}ms`); - console.log(` P95: ${p95.toFixed(3)}ms (target: <${targetP95}ms)`); - console.log(` P99: ${p99.toFixed(3)}ms`); - console.log(`\nTarget: ${results.passes_target ? 'PASS' : 'FAIL'}`); - - // Output exact P95 for CI parsing - console.log(`P95: ${p95.toFixed(2)}`); - - // Clear watchdog - clearTimeout(kill); - - // Write results to file - writeFileSync('perf-results.json', JSON.stringify(results, null, 2)); - console.log('\nResults saved to perf-results.json'); - - return results; -} - -// @ts-expect-error import.meta requires ESM module setting -if (import.meta.url === `file://${process.argv[1]}`) { - benchmark().catch((err) => { - console.error(err); - console.log('P95: 999'); // Failure sentinel - process.exit(1); - }); -} - -export { benchmark }; diff --git a/scripts/guard.sh b/scripts/guard.sh index 312a09390..c5d3937bc 100755 --- a/scripts/guard.sh +++ b/scripts/guard.sh @@ -183,8 +183,8 @@ echo "== forbid legacy wire format IDs (v0.10.0 break) ==" # peac.receipt/0.9 -> peac-receipt/0.1 # peac.dispute-bundle/0.1 -> peac-bundle/0.1 # wire/0.9/ -> wire/0.1/ -# Allow in: CHANGELOG, guard script, migration docs, versioning doctrine (historical), deprecated packages, tests pending migration, Go SDK, security / stability / threat docs that must name the legacy identifier to describe its deprecated state, v0.13.0 package-status doc (archive-scheduled rows must name the legacy wire they retire), archive/ (excluded by path filter below). -LEGACY_WIRE_ALLOW='^(CHANGELOG\.md|SECURITY\.md|scripts/(guard\.sh|lint-schemas\.mjs|verify-trust-artifacts\.mjs)|docs/(migration/|MIGRATION_CURRENT\.md|PACKAGE_STATUS\.md|PACKAGE_STATUS_V0\.13\.0_PARITY\.md|specs/VERSIONING\.md|STABILITY-CONTRACT\.md|THREAT_MODEL\.md)|packages/(core|sdk-js)/|packages/(crypto|protocol|schema)/(tests|openapi|src/(index|types|envelope))|tests/(golden|vectors)/|specs/(kernel/README|conformance/fixtures/bundle/invalid)|sdks/go/|examples/x402-node-server/)' +# Allow in: CHANGELOG, guard script, migration docs, versioning doctrine (historical), deprecated packages, tests pending migration, Go SDK, security / stability / threat docs that must name the legacy identifier to describe its deprecated state, v0.13.0 package-status doc (archive-scheduled rows must name the legacy wire they retire), machine-readable surface status that records the archive note, the post-release npm deprecate script (must name the legacy receipt format in its deprecation message), archive/ (excluded by path filter below). +LEGACY_WIRE_ALLOW='^(CHANGELOG\.md|SECURITY\.md|REPO_SURFACE_STATUS\.json|scripts/(guard\.sh|lint-schemas\.mjs|verify-trust-artifacts\.mjs|release/npm-deprecate-v0\.13\.0\.sh)|docs/(migration/|MIGRATION_CURRENT\.md|PACKAGE_STATUS\.md|PACKAGE_STATUS_V0\.13\.0_PARITY\.md|specs/VERSIONING\.md|STABILITY-CONTRACT\.md|THREAT_MODEL\.md)|packages/(core|sdk-js)/|packages/(crypto|protocol|schema)/(tests|openapi|src/(index|types|envelope))|tests/(golden|vectors)/|specs/(kernel/README|conformance/fixtures/bundle/invalid)|sdks/go/|examples/x402-node-server/)' if git grep -nE 'peac\.receipt/0\.9|peac\.dispute-bundle|wire/0\.9/|PEAC-RECEIPT-SCHEMA-v0\.9' -- ':!node_modules' ':!archive/**' \ | grep -vE "$LEGACY_WIRE_ALLOW" | grep .; then echo "FAIL: Found legacy wire format IDs - use normalized 0.1 versions" diff --git a/scripts/pack-install-smoke.sh b/scripts/pack-install-smoke.sh index c4ebae61b..2af3642bf 100755 --- a/scripts/pack-install-smoke.sh +++ b/scripts/pack-install-smoke.sh @@ -157,38 +157,34 @@ EOF node test.mjs -# Schema package-edge import smoke (deprecated alias stability) +# Schema package-edge import smoke echo "" echo "6. Running @peac/schema package-edge import smoke..." cat > schema-smoke.mjs << 'EOF' -import { - ProofMethodSchema, - PROOF_METHODS, - ProofTypeSchema, - CommerceExtensionSchema, -} from '@peac/schema'; - -// Verify deprecated ProofMethodSchema is importable and functional -if (!ProofMethodSchema) { - console.error('FAIL: ProofMethodSchema not exported from @peac/schema'); - process.exit(1); -} -const parsed = ProofMethodSchema.parse('dpop'); -if (parsed !== 'dpop') { - console.error('FAIL: ProofMethodSchema.parse("dpop") returned:', parsed); +import { ProofTypeSchema, AgentProofSchema, CommerceExtensionSchema } from '@peac/schema'; + +// Verify ProofTypeSchema (canonical trust-root proof model) +if (!ProofTypeSchema) { + console.error('FAIL: ProofTypeSchema not exported from @peac/schema'); process.exit(1); } -// Verify PROOF_METHODS array -if (!Array.isArray(PROOF_METHODS) || PROOF_METHODS.length !== 4) { - console.error('FAIL: PROOF_METHODS not exported or wrong length:', PROOF_METHODS); +// Verify AgentProofSchema accepts the inlined transport-binding method enum. +const proof = AgentProofSchema.parse({ + method: 'dpop', + key_id: 'test-key-1', + dpop_proof: 'eyJ0eXAiOiJkcG9wK2p3dCJ9..', +}); +if (proof.method !== 'dpop') { + console.error('FAIL: AgentProofSchema did not preserve method value:', proof); process.exit(1); } - -// Verify ProofTypeSchema (canonical, non-deprecated) -if (!ProofTypeSchema) { - console.error('FAIL: ProofTypeSchema not exported from @peac/schema'); +try { + AgentProofSchema.parse({ method: 'not-a-method', key_id: 'test-key-1' }); + console.error('FAIL: AgentProofSchema accepted unknown method value'); process.exit(1); +} catch { + // expected: unknown method values are rejected } // Verify CommerceExtensionSchema accepts optional event field @@ -214,8 +210,7 @@ if (noEvent.event !== undefined) { process.exit(1); } -console.log(' ProofMethodSchema: importable, parse works (deprecated alias stable)'); -console.log(' PROOF_METHODS:', PROOF_METHODS.join(', ')); +console.log(' AgentProofSchema: inlined method enum parsed correctly'); console.log(' CommerceExtensionSchema: event field accepted and absent-event valid'); console.log(' OK: @peac/schema package-edge imports verified'); EOF diff --git a/scripts/publish-manifest.json b/scripts/publish-manifest.json index 614b74c6a..7e647b2ff 100644 --- a/scripts/publish-manifest.json +++ b/scripts/publish-manifest.json @@ -127,7 +127,6 @@ "@peac/receipts", "@peac/pay402", "@peac/server", - "@peac/core", "@peac/net-node", "@peac/adapter-eat", "@peac/adapter-did", diff --git a/scripts/release/api-surface-lock.sh b/scripts/release/api-surface-lock.sh index 4536cf78a..e800fd6a3 100755 --- a/scripts/release/api-surface-lock.sh +++ b/scripts/release/api-surface-lock.sh @@ -38,7 +38,12 @@ TRACKED_PACKAGES=( "@peac/middleware-core" "@peac/adapter-eat" "@peac/mcp-server" - "@peac/sdk" + # @peac/mappings-a2a tracked from v0.13.0 PR B (DD-186 removal + # of TASK_STATE_V03_TO_V1 / normalizeTaskState / _resetDeprecationWarning + # warranted an explicit public-surface lock on this published package). + "@peac/mappings-a2a" + # @peac/sdk archived (source in archive/sdk-js/); not a tracked public + # surface in v0.13.0+. Historical snapshot preserved in git history. ) FAILED=0 diff --git a/scripts/release/npm-deprecate-v0.13.0.sh b/scripts/release/npm-deprecate-v0.13.0.sh index a781f35e7..b0ee3bcf5 100755 --- a/scripts/release/npm-deprecate-v0.13.0.sh +++ b/scripts/release/npm-deprecate-v0.13.0.sh @@ -65,15 +65,15 @@ $NPM deprecate '@peac/disc@0.13.0' \ 'One-release deprecated compatibility package. Prefer @peac/policy-kit for policy-document parsing and validation. See https://peacprotocol.org/docs/migration.' # -# @peac/core: archive coupled with legacy /verify handler rewire. If that -# change ships in the same v0.13.0 release window, add the deprecate line -# below after it merges but before promote-latest. Left commented out as a -# staging marker. +# @peac/core: archived in v0.13.0. Historical 0.9-series verify-only +# implementation; not published at v0.13.0 or later. Historical npm +# versions <=0.9.14 remain installable for verify-only use of historical +# peac.receipt/0.9 records. # -# echo -# echo "-- @peac/core (archive; coupled with legacy /verify handler rewire) --" -# $NPM deprecate '@peac/core@<=0.9.14' \ -# 'ARCHIVED in v0.13.0. Verify-only for historical 0.9-series receipt records. Use @peac/protocol + @peac/schema + @peac/crypto + @peac/kernel. See https://peacprotocol.org/docs/migration.' +echo +echo "-- @peac/core (archived; historical <=0.9.14 kept; not published 0.13.0+) --" +$NPM deprecate '@peac/core@<=0.9.14' \ + 'ARCHIVED in v0.13.0. Verify-only for historical 0.9-series receipt records. Use @peac/protocol + @peac/schema + @peac/crypto + @peac/kernel. See https://peacprotocol.org/docs/migration.' echo echo "=== complete ===" diff --git a/tests/tooling/__snapshots__/api-contract.test.ts.snap b/tests/tooling/__snapshots__/api-contract.test.ts.snap index 75efea87f..26ebbfeef 100644 --- a/tests/tooling/__snapshots__/api-contract.test.ts.snap +++ b/tests/tooling/__snapshots__/api-contract.test.ts.snap @@ -326,7 +326,6 @@ exports[`API contract: @peac/schema > barrel type exports match snapshot (static "PolicyContext", "PolicyDecision", "PrivacyExtension", - "ProofMethod", "ProofType", "ProvenanceExtension", "PurposeExtension", @@ -540,7 +539,6 @@ exports[`API contract: @peac/schema > value exports match snapshot 1`] = ` "PEAC_WIRE_TYP", "POLICY_DECISIONS", "PRIVACY_EXTENSION_KEY", - "PROOF_METHODS", "PROOF_TYPES", "PROVENANCE_EXTENSION_KEY", "PURPOSE_EXTENSION_KEY", @@ -556,7 +554,6 @@ exports[`API contract: @peac/schema > value exports match snapshot 1`] = ` "PolicyContextSchema", "PrivacyExtensionSchema", "ProblemDetailsSchema", - "ProofMethodSchema", "ProofTypeSchema", "ProvenanceExtensionSchema", "PurposeExtensionSchema", diff --git a/tests/tooling/api-contract.test.ts b/tests/tooling/api-contract.test.ts index f574f73d9..503531bdc 100644 --- a/tests/tooling/api-contract.test.ts +++ b/tests/tooling/api-contract.test.ts @@ -145,9 +145,12 @@ describe('API contract: @peac/schema', () => { expect(schemas).toHaveLength(13); }); - it('ProofMethodSchema remains exported (deprecated, compat through v0.12.x)', () => { - expect(exports).toContain('ProofMethodSchema'); - expect(exports).toContain('PROOF_METHODS'); + it('ProofMethodSchema is removed (DD-185; removed in v0.13.0 PR B)', () => { + // Transport-binding method values (http-message-signature, dpop, mtls, + // jwk-thumbprint) are now inlined on AgentProofSchema.method. See + // docs/MIGRATION_CURRENT.md and docs/STABILITY-CONTRACT.md. + expect(exports).not.toContain('ProofMethodSchema'); + expect(exports).not.toContain('PROOF_METHODS'); }); it('ProofTypeSchema remains exported (canonical trust-root model)', () => { diff --git a/tsconfig.base.json b/tsconfig.base.json index 775f8d566..c5993d737 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -27,7 +27,6 @@ "@peac/disc": ["./packages/discovery/src"], "@peac/server": ["./packages/server/src"], "@peac/cli": ["./packages/cli/src"], - "@peac/core": ["./packages/core/src"], "@peac/pay402": ["./packages/pay402/src"], "@peac/rails-stripe": ["./packages/rails/stripe/src"], "@peac/rails-x402": ["./packages/rails/x402/src"],