diff --git a/.changeset/calm-canvases-coordinate.md b/.changeset/calm-canvases-coordinate.md new file mode 100644 index 000000000..5593788bc --- /dev/null +++ b/.changeset/calm-canvases-coordinate.md @@ -0,0 +1,6 @@ +--- +'@adcp/sdk': patch +--- + +Complete the AdCP 3.2.0-beta.6 adoption with updated migration guidance, +consistent prerelease fixtures, and A2A 1.0 request-signing method coverage. diff --git a/AGENTS.md b/AGENTS.md index 7cfafcf74..101930dfd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ if (!artifact.name) { ... } Used for receiving task completion/progress notifications. Placement differs by protocol: -- **AdCP 3.2.0-beta.5 on A2A**: Goes in skill parameters as +- **AdCP 3.2.0-beta.6 on A2A**: Goes in skill parameters as `push_notification_config` (snake_case), including `operation_id`. The native A2A `params.configuration.pushNotificationConfig` is a distinct transport facility; the SDK may retain it for compatibility, but it does not replace diff --git a/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md b/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md index 5d26ac616..7221a95be 100644 --- a/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md +++ b/docs/guides/MEDIA-BUY-3.2-COMPATIBILITY.md @@ -7,9 +7,12 @@ calls the established names. The SDK is pinned to the signed `3.2.0-beta.6` bundle. That exact prerelease supersedes beta.5 and adds delivery metric identities, requested-metric -narrowing, sortable breakdowns, and completeness echoes. Beta.5 introduced -the normative async identity, cross-channel convergence, webhook retry-horizon, -and continuation-generation contract. +narrowing, sortable breakdowns, completeness echoes, coordinated placements, +seller-rendered stateful display, creative component assets, and A2A 1.0 +request-signing method names. Beta.5 introduced the normative async identity, +cross-channel convergence, webhook retry-horizon, and continuation-generation +contract; beta.4 introduced flexible-window availability and the products-only +legacy purchase-continuation contract. ## MCP surface comparison diff --git a/docs/migration-12-to-14.md b/docs/migration-12-to-14.md index 91ca48410..dc9684ebc 100644 --- a/docs/migration-12-to-14.md +++ b/docs/migration-12-to-14.md @@ -1,13 +1,15 @@ # Migrating from 12.x to 14 beta -This is the direct upgrade path for applications skipping SDK 13. SDK 14 includes both SDK 13's canonical-creative and security boundary changes and the AdCP `3.2.0-beta.5` preview surface. Treat it as two review checkpoints even if you deploy one package update. +This is the direct upgrade path for applications skipping SDK 13. SDK 14 includes both SDK 13's canonical-creative and security boundary changes and the AdCP `3.2.0-beta.6` preview surface. Treat it as two review checkpoints even if you deploy one package update. -AdCP prerelease pins are exact: beta.5 supersedes beta.4. Beta.2 added canonical +AdCP prerelease pins are exact: beta.6 supersedes beta.5. Beta.2 added canonical compact proposal and direct-buy lifecycle storyboards through operational control and MediaBuy readback; beta.4 adds flexible-window availability and durable products-only legacy purchase continuations. Beta.5 defines stable async identity, cross-channel terminal convergence, webhook retry horizons, -and crash-safe continuation generation replacement. +and crash-safe continuation generation replacement. Beta.6 adds coordinated +placements, seller-rendered stateful display, creative component assets, and +A2A 1.0 request-signing method names. Install the beta explicitly: diff --git a/docs/migration-13-to-14.md b/docs/migration-13-to-14.md index 400545d1d..0c8703951 100644 --- a/docs/migration-13-to-14.md +++ b/docs/migration-13-to-14.md @@ -1,8 +1,8 @@ # Migrating from 13.x to 14 beta -SDK 14 adopts AdCP `3.2.0-beta.5` while preserving the canonical creative boundary introduced in SDK 13. Most SDK 13 applications can install the beta and continue using the established 3.x tools unchanged; adopt the compact 3.2 lifecycle only after the remote agent advertises it. +SDK 14 adopts AdCP `3.2.0-beta.6` while preserving the canonical creative boundary introduced in SDK 13. Most SDK 13 applications can install the beta and continue using the established 3.x tools unchanged; adopt the compact 3.2 lifecycle only after the remote agent advertises it. -AdCP 3.2 prereleases are exact protocol pins: beta.5 replaces beta.4 in the +AdCP 3.2 prereleases are exact protocol pins: beta.6 replaces beta.5 in the SDK's compatible-version list rather than extending a rolling 3.2-beta range. Beta.1 restored `adcp_major_version` on `buy_products`, `accept_proposal`, and `control_media_buy`; the SDK now sends that field again @@ -12,16 +12,17 @@ storyboards through operational control and MediaBuy readback; beta.4 adds flexible-window availability and durable products-only legacy purchase continuations. Beta.5 defines stable async identity, cross-channel terminal convergence, webhook retry horizons, and crash-safe continuation generation -replacement. +replacement. Beta.6 adds coordinated placements, seller-rendered stateful +display, creative component assets, and A2A 1.0 request-signing method names. ### Beta.5 task webhook registration and polling `push_notification_config` is now an AdCP application-layer field across MCP, A2A, and REST. On A2A it is carried in skill parameters and remains distinct -from native `TaskPushNotificationConfig`. Every beta.5 registration must carry +from native `TaskPushNotificationConfig`. Every beta.5-or-later registration must carry a buyer `operation_id`; SDK clients generate and reuse one identity across the authorized request, registration provenance, route, and webhook envelope. -Beta.5 sellers return `INVALID_REQUEST` before handler dispatch when the field +Beta.5-or-later sellers return `INVALID_REQUEST` before handler dispatch when the field is missing or malformed. Explicitly negotiated older bundles keep their prior wire behavior. @@ -83,7 +84,7 @@ loading; keep using `requires_capability` for a singular predicate. 10. Upgrade durable idempotency storage before application traffic: add the nullable PostgreSQL `retain_until` column/index, preserve `IdempotencyCacheEntry.retainUntil`, and add atomic `putIfAbsent()`, `replaceIfPayloadHash()`, `replaceIfPayloadHashAndExpired()`, and `deleteIfPayloadHash()` to every custom backend. 11. Upgrade custom deferred-task storage with `putForSettlementOperationIfAbsent()`, `getBySettlementOperationId()`, and `replaceForSettlementOperationIfVersion()`. The initial token/index write and nested A→B index move must each be atomic. 12. Replace webhook emitter `operation_id` arguments with SDK-local `delivery_id` values and upgrade custom stores to `WebhookDeliveryStore`. One delivery ID binds one canonical payload and key; use a fresh delivery ID for each changed status observation while retaining the AdCP `operation_id` inside the payload. -13. Ensure custom beta.5 buyers include `push_notification_config.operation_id`, and update A2A integrations to keep the AdCP registration in skill parameters even when native A2A push configuration is also present. +13. Ensure custom 3.2 buyers include `push_notification_config.operation_id`, and update A2A integrations to keep the AdCP registration in skill parameters even when native A2A push configuration is also present. 14. Treat failed/rejected task results as canonical terminal artifacts when `include_result` is requested; do not discard them while preserving only the summary error. ### Webhook delivery identity and retry horizons diff --git a/package.json b/package.json index c826c7d9f..91bd34688 100644 --- a/package.json +++ b/package.json @@ -485,13 +485,13 @@ "pretest": "npm run schemas:ensure", "test": "npm run test:node && npm test --workspace=packages/eslint-plugin --if-present && npm test --workspace=@adcp/reference-renderers", "test:node": "npm run test:node:fast && npm run test:node:slow", - "test:node:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", - "test:node:fast:shard": "test -n \"$TEST_SHARD\" && NODE_ENV=test node --test-timeout=60000 --test-force-exit --test-shard=$TEST_SHARD --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", - "test:node:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/canonical-creatives-a2a-e2e.test.js test/generate-zod-object-intersections.test.js test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/storyboard-notices.test.js", + "test:node:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'media-buy-lifecycle-release-gate.test.js' ! -name 'storyboard-notices.test.js' -print)", + "test:node:fast:shard": "test -n \"$TEST_SHARD\" && NODE_ENV=test node --test-timeout=60000 --test-force-exit --test-shard=$TEST_SHARD --test $(find test -maxdepth 1 -name '*.test.js' ! -name 'canonical-creatives-a2a-e2e.test.js' ! -name 'generate-zod-object-intersections.test.js' -print) $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'media-buy-lifecycle-release-gate.test.js' ! -name 'storyboard-notices.test.js' -print)", + "test:node:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/canonical-creatives-a2a-e2e.test.js test/generate-zod-object-intersections.test.js test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/media-buy-lifecycle-release-gate.test.js test/lib/storyboard-notices.test.js", "pretest:lib": "npm run schemas:ensure", "test:lib": "npm run test:lib:fast && npm run test:lib:slow", - "test:lib:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'storyboard-notices.test.js' -print)", - "test:lib:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/storyboard-notices.test.js", + "test:lib:fast": "NODE_ENV=test node --test-timeout=60000 --test-force-exit --test $(find test/lib -maxdepth 1 -name '*.test.js' ! -name 'cli-auth-scheme.test.js' ! -name 'cli-soft-fail.test.js' ! -name 'cli-webhook-receiver-flag.test.js' ! -name 'conformance-cli.test.js' ! -name 'media-buy-lifecycle-release-gate.test.js' ! -name 'storyboard-notices.test.js' -print)", + "test:lib:slow": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test test/lib/cli-auth-scheme.test.js test/lib/cli-soft-fail.test.js test/lib/cli-webhook-receiver-flag.test.js test/lib/conformance-cli.test.js test/lib/media-buy-lifecycle-release-gate.test.js test/lib/storyboard-notices.test.js", "pretest:examples": "npm run build", "test:examples": "NODE_ENV=test node --test-timeout=180000 --test-force-exit --test-concurrency=1 --test test/examples/*.test.js", "test:e2e": "node --test test/e2e/registry-live.test.js", diff --git a/test/lib/adcp-version-projection.test.js b/test/lib/adcp-version-projection.test.js index f68919541..3128e2927 100644 --- a/test/lib/adcp-version-projection.test.js +++ b/test/lib/adcp-version-projection.test.js @@ -12,6 +12,7 @@ test('isAdcpVersionAtLeast compares release-precision prerelease pins', () => { assert.equal(isAdcpVersionAtLeast('3.2-beta.5', '3.2.0-beta.5'), true); assert.equal(isAdcpVersionAtLeast('3.2-beta.6', '3.2.0-beta.5'), true); assert.equal(isAdcpVersionAtLeast('3.2.0', '3.2.0-beta.5'), true); + assert.equal(isAdcpVersionAtLeast('3.3.0', '3.2.0-beta.5'), true); }); test('sellerAdvertises31: buildVersion is advisory and never used for negotiation', () => { diff --git a/test/lib/preview-creative-client.test.js b/test/lib/preview-creative-client.test.js index 6d39f5a9e..63a5eb66f 100644 --- a/test/lib/preview-creative-client.test.js +++ b/test/lib/preview-creative-client.test.js @@ -40,7 +40,7 @@ function runtimeClient(executeTask, config = {}) { client.cachedCapabilities = { version: 'v3', majorVersions: [3], - supportedVersions: ['3.2.0-beta.5'], + supportedVersions: ['3.2.0-beta.6'], protocols: ['creative'], features: {}, extensions: [], diff --git a/test/lib/proposal-negotiation.test.js b/test/lib/proposal-negotiation.test.js index b74c0ecb0..2b80c9be5 100644 --- a/test/lib/proposal-negotiation.test.js +++ b/test/lib/proposal-negotiation.test.js @@ -56,7 +56,7 @@ function revise(overrides = {}) { } function request(refinements = [revise()]) { - return { adcp_version: '3.2-beta.5', adcp_major_version: 3, idempotency_key: KEY, refinements }; + return { adcp_version: '3.2-beta.6', adcp_major_version: 3, idempotency_key: KEY, refinements }; } function completed(data) { diff --git a/test/lib/v2-getproducts-autowire.test.js b/test/lib/v2-getproducts-autowire.test.js index d9cbc2274..b97ec42ca 100644 --- a/test/lib/v2-getproducts-autowire.test.js +++ b/test/lib/v2-getproducts-autowire.test.js @@ -32,8 +32,8 @@ async function buildMockSeller(getProductsResponse, clientConfig = {}, { adverti server.registerTool('get_adcp_capabilities', { inputSchema: {} }, async () => { const capabilities = { status: 'completed', - adcp_version: '3.2.0-beta.5', - adcp: { major_versions: [3], supported_versions: ['3.2.0-beta.5'] }, + adcp_version: '3.2.0-beta.6', + adcp: { major_versions: [3], supported_versions: ['3.2.0-beta.6'] }, supported_protocols: ['media_buy'], specialisms: [], }; diff --git a/test/request-signing-verifier-api.test.js b/test/request-signing-verifier-api.test.js index 9db9de42f..b8c81ae40 100644 --- a/test/request-signing-verifier-api.test.js +++ b/test/request-signing-verifier-api.test.js @@ -110,6 +110,36 @@ describe('verifier API v3: operation optional + VerifyResult discriminated union ); }); + it('matches A2A 1.0 PascalCase protocol methods exactly', async () => { + const request = { + method: 'POST', + url: 'https://seller.example.com/a2a', + headers: { 'Content-Type': 'application/json' }, + body: '{"jsonrpc":"2.0","method":"CancelTask","params":{"id":"task_conformance_001"},"id":1}', + }; + const options = { + ...baseStores(), + capability: { + supported: true, + covers_content_digest: 'either', + required_for: [], + protocol_methods_required_for: ['CancelTask'], + }, + now: () => 1_776_520_800, + }; + + await assert.rejects( + () => verifyRequestSignature(request, options), + err => err instanceof RequestSignatureError && err.code === 'request_signature_required' && err.failedStep === 0 + ); + + const mismatch = await verifyRequestSignature(request, { + ...options, + capability: { ...options.capability, protocol_methods_required_for: ['tasks/cancel'] }, + }); + assert.strictEqual(mismatch.status, 'unsigned', '0.3 names must not normalize to A2A 1.0 names'); + }); + it('unsigned JSON-RPC batch with protocol method in protocol_methods_required_for throws request_signature_required', async () => { await assert.rejects( () =>