@adcp/sdk 12.0.0 → 12.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/adapters/content-standards-adapter.mjs +0 -6
- package/dist/lib/adapters/content-standards-adapter.mjs.map +1 -1
- package/dist/lib/adapters/derived-account-store.mjs +0 -6
- package/dist/lib/adapters/derived-account-store.mjs.map +1 -1
- package/dist/lib/adapters/governance-adapter.mjs +0 -6
- package/dist/lib/adapters/governance-adapter.mjs.map +1 -1
- package/dist/lib/adapters/implicit-account-store.mjs +0 -6
- package/dist/lib/adapters/implicit-account-store.mjs.map +1 -1
- package/dist/lib/adapters/index.mjs +0 -6
- package/dist/lib/adapters/index.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/create_media_buy.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/create_media_buy.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/get_products.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/get_products.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/index.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/index.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/list_creative_formats.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/list_creative_formats.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/preview_creative.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/preview_creative.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/sync_creatives.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/sync_creatives.mjs.map +1 -1
- package/dist/lib/adapters/legacy/v2-5/types.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/update_media_buy.mjs +0 -6
- package/dist/lib/adapters/legacy/v2-5/update_media_buy.mjs.map +1 -1
- package/dist/lib/adapters/oauth-passthrough-resolver.mjs +0 -6
- package/dist/lib/adapters/oauth-passthrough-resolver.mjs.map +1 -1
- package/dist/lib/adapters/property-list-adapter.mjs +0 -6
- package/dist/lib/adapters/property-list-adapter.mjs.map +1 -1
- package/dist/lib/adapters/roster-account-store.mjs +0 -6
- package/dist/lib/adapters/roster-account-store.mjs.map +1 -1
- package/dist/lib/adapters/si-session-manager.mjs +0 -6
- package/dist/lib/adapters/si-session-manager.mjs.map +1 -1
- package/dist/lib/adapters/version/3.0/brand-fields.mjs +0 -6
- package/dist/lib/adapters/version/3.0/brand-fields.mjs.map +1 -1
- package/dist/lib/adapters/version/3.0/get-products.mjs +0 -6
- package/dist/lib/adapters/version/3.0/get-products.mjs.map +1 -1
- package/dist/lib/adapters/version/3.0/index.mjs +0 -6
- package/dist/lib/adapters/version/3.0/index.mjs.map +1 -1
- package/dist/lib/adapters/version/3.0/sync-accounts.mjs +0 -6
- package/dist/lib/adapters/version/3.0/sync-accounts.mjs.map +1 -1
- package/dist/lib/adapters/version/index.mjs +0 -6
- package/dist/lib/adapters/version/index.mjs.map +1 -1
- package/dist/lib/adapters/version/types.mjs +0 -6
- package/dist/lib/advanced.mjs +0 -6
- package/dist/lib/advanced.mjs.map +1 -1
- package/dist/lib/agents/index.generated.mjs +0 -6
- package/dist/lib/agents/index.generated.mjs.map +1 -1
- package/dist/lib/auth/index.mjs +0 -6
- package/dist/lib/auth/index.mjs.map +1 -1
- package/dist/lib/auth/oauth/CLIFlowHandler.mjs +0 -6
- package/dist/lib/auth/oauth/CLIFlowHandler.mjs.map +1 -1
- package/dist/lib/auth/oauth/ClientCredentialsFlow.mjs +0 -6
- package/dist/lib/auth/oauth/ClientCredentialsFlow.mjs.map +1 -1
- package/dist/lib/auth/oauth/MCPOAuthProvider.mjs +0 -6
- package/dist/lib/auth/oauth/MCPOAuthProvider.mjs.map +1 -1
- package/dist/lib/auth/oauth/NonInteractiveFlowHandler.mjs +0 -6
- package/dist/lib/auth/oauth/NonInteractiveFlowHandler.mjs.map +1 -1
- package/dist/lib/auth/oauth/authorization-required.mjs +0 -6
- package/dist/lib/auth/oauth/authorization-required.mjs.map +1 -1
- package/dist/lib/auth/oauth/diagnose.mjs +0 -6
- package/dist/lib/auth/oauth/diagnose.mjs.map +1 -1
- package/dist/lib/auth/oauth/diagnostics.mjs +0 -6
- package/dist/lib/auth/oauth/diagnostics.mjs.map +1 -1
- package/dist/lib/auth/oauth/discovery.mjs +0 -6
- package/dist/lib/auth/oauth/discovery.mjs.map +1 -1
- package/dist/lib/auth/oauth/file-storage.mjs +0 -6
- package/dist/lib/auth/oauth/file-storage.mjs.map +1 -1
- package/dist/lib/auth/oauth/index.mjs +0 -6
- package/dist/lib/auth/oauth/index.mjs.map +1 -1
- package/dist/lib/auth/oauth/secret-resolver.mjs +0 -6
- package/dist/lib/auth/oauth/secret-resolver.mjs.map +1 -1
- package/dist/lib/auth/oauth/storage-registry.mjs +0 -6
- package/dist/lib/auth/oauth/storage-registry.mjs.map +1 -1
- package/dist/lib/auth/oauth/types.mjs +0 -6
- package/dist/lib/auth/oauth/types.mjs.map +1 -1
- package/dist/lib/auth/oauth/web-flow.mjs +0 -6
- package/dist/lib/auth/oauth/web-flow.mjs.map +1 -1
- package/dist/lib/brand/index.mjs +0 -6
- package/dist/lib/brand/index.mjs.map +1 -1
- package/dist/lib/canonical-references/index.mjs +6 -7
- package/dist/lib/canonical-references/index.mjs.map +1 -1
- package/dist/lib/client/index.mjs +0 -6
- package/dist/lib/client/index.mjs.map +1 -1
- package/dist/lib/compliance/index.mjs +0 -6
- package/dist/lib/compliance/index.mjs.map +1 -1
- package/dist/lib/compliance-fixtures/index.mjs +0 -6
- package/dist/lib/compliance-fixtures/index.mjs.map +1 -1
- package/dist/lib/compliance-fixtures/test-authorization-server.mjs +0 -6
- package/dist/lib/compliance-fixtures/test-authorization-server.mjs.map +1 -1
- package/dist/lib/conformance/index.mjs +0 -6
- package/dist/lib/conformance/index.mjs.map +1 -1
- package/dist/lib/conformance/invariants/uniformError.mjs +0 -6
- package/dist/lib/conformance/invariants/uniformError.mjs.map +1 -1
- package/dist/lib/conformance/invariants/uniformErrorComparator.mjs +0 -6
- package/dist/lib/conformance/invariants/uniformErrorComparator.mjs.map +1 -1
- package/dist/lib/conformance/oracle.mjs +0 -6
- package/dist/lib/conformance/oracle.mjs.map +1 -1
- package/dist/lib/conformance/runConformance.mjs +0 -6
- package/dist/lib/conformance/runConformance.mjs.map +1 -1
- package/dist/lib/conformance/runners.mjs +0 -6
- package/dist/lib/conformance/runners.mjs.map +1 -1
- package/dist/lib/conformance/schemaArbitrary.mjs +0 -6
- package/dist/lib/conformance/schemaArbitrary.mjs.map +1 -1
- package/dist/lib/conformance/schemaLoader.mjs +5 -6
- package/dist/lib/conformance/schemaLoader.mjs.map +1 -1
- package/dist/lib/conformance/seeder.mjs +0 -6
- package/dist/lib/conformance/seeder.mjs.map +1 -1
- package/dist/lib/conformance/types.mjs +0 -6
- package/dist/lib/conformance/types.mjs.map +1 -1
- package/dist/lib/conformance/webhook.mjs +0 -6
- package/dist/lib/conformance/webhook.mjs.map +1 -1
- package/dist/lib/core/ADCPMultiAgentClient.mjs +0 -6
- package/dist/lib/core/ADCPMultiAgentClient.mjs.map +1 -1
- package/dist/lib/core/AgentClient.mjs +0 -6
- package/dist/lib/core/AgentClient.mjs.map +1 -1
- package/dist/lib/core/AsyncHandler.mjs +0 -6
- package/dist/lib/core/AsyncHandler.mjs.map +1 -1
- package/dist/lib/core/ConfigurationManager.mjs +0 -6
- package/dist/lib/core/ConfigurationManager.mjs.map +1 -1
- package/dist/lib/core/ConversationTypes.mjs +0 -6
- package/dist/lib/core/CreativeAgentClient.mjs +0 -6
- package/dist/lib/core/CreativeAgentClient.mjs.map +1 -1
- package/dist/lib/core/GovernanceMiddleware.mjs +0 -6
- package/dist/lib/core/GovernanceMiddleware.mjs.map +1 -1
- package/dist/lib/core/GovernanceTypes.mjs +0 -6
- package/dist/lib/core/GovernanceTypes.mjs.map +1 -1
- package/dist/lib/core/ProtocolResponseParser.mjs +0 -6
- package/dist/lib/core/ProtocolResponseParser.mjs.map +1 -1
- package/dist/lib/core/ResponseValidator.mjs +0 -6
- package/dist/lib/core/ResponseValidator.mjs.map +1 -1
- package/dist/lib/core/SingleAgentClient.mjs +0 -6
- package/dist/lib/core/SingleAgentClient.mjs.map +1 -1
- package/dist/lib/core/TaskEventTypes.mjs +0 -6
- package/dist/lib/core/TaskEventTypes.mjs.map +1 -1
- package/dist/lib/core/TaskExecutor.mjs +0 -6
- package/dist/lib/core/TaskExecutor.mjs.map +1 -1
- package/dist/lib/core/match.mjs +0 -6
- package/dist/lib/core/match.mjs.map +1 -1
- package/dist/lib/core/task-status.mjs +0 -6
- package/dist/lib/core/task-status.mjs.map +1 -1
- package/dist/lib/core/webhook-url.mjs +0 -6
- package/dist/lib/core/webhook-url.mjs.map +1 -1
- package/dist/lib/discovery/adagents-redirects.mjs +0 -6
- package/dist/lib/discovery/adagents-redirects.mjs.map +1 -1
- package/dist/lib/discovery/agent-directory.mjs +0 -6
- package/dist/lib/discovery/agent-directory.mjs.map +1 -1
- package/dist/lib/discovery/inline-publisher-properties.mjs +0 -6
- package/dist/lib/discovery/inline-publisher-properties.mjs.map +1 -1
- package/dist/lib/discovery/network-consistency-checker.mjs +0 -6
- package/dist/lib/discovery/network-consistency-checker.mjs.map +1 -1
- package/dist/lib/discovery/property-crawler.mjs +0 -6
- package/dist/lib/discovery/property-crawler.mjs.map +1 -1
- package/dist/lib/discovery/property-index.mjs +0 -6
- package/dist/lib/discovery/property-index.mjs.map +1 -1
- package/dist/lib/discovery/publisher-property-selector.mjs +0 -6
- package/dist/lib/discovery/publisher-property-selector.mjs.map +1 -1
- package/dist/lib/discovery/resolve-agent-properties.mjs +0 -6
- package/dist/lib/discovery/resolve-agent-properties.mjs.map +1 -1
- package/dist/lib/discovery/types.mjs +0 -6
- package/dist/lib/discovery/validate-adagents.mjs +0 -6
- package/dist/lib/discovery/validate-adagents.mjs.map +1 -1
- package/dist/lib/enums.mjs +0 -6
- package/dist/lib/enums.mjs.map +1 -1
- package/dist/lib/errors/index.mjs +0 -6
- package/dist/lib/errors/index.mjs.map +1 -1
- package/dist/lib/express-mcp/index.mjs +0 -6
- package/dist/lib/express-mcp/index.mjs.map +1 -1
- package/dist/lib/governance/index.mjs +0 -6
- package/dist/lib/governance/index.mjs.map +1 -1
- package/dist/lib/handlers/types.mjs +0 -6
- package/dist/lib/handlers/types.mjs.map +1 -1
- package/dist/lib/index.d.mts +1 -1
- package/dist/lib/index.d.ts +1 -1
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/index.mjs +0 -6
- package/dist/lib/index.mjs.map +1 -1
- package/dist/lib/media-buy/available-actions.mjs +0 -6
- package/dist/lib/media-buy/available-actions.mjs.map +1 -1
- package/dist/lib/media-buy/index.mjs +0 -6
- package/dist/lib/media-buy/index.mjs.map +1 -1
- package/dist/lib/media-buy/preflight.mjs +0 -6
- package/dist/lib/media-buy/preflight.mjs.map +1 -1
- package/dist/lib/media-buy/property-policy.mjs +0 -6
- package/dist/lib/media-buy/property-policy.mjs.map +1 -1
- package/dist/lib/media-buy/types.mjs +0 -6
- package/dist/lib/media-buy/types.mjs.map +1 -1
- package/dist/lib/media-buy/update-fields.generated.mjs +0 -6
- package/dist/lib/media-buy/update-fields.generated.mjs.map +1 -1
- package/dist/lib/mock-server/creative-ad-server/seed-data.mjs +0 -6
- package/dist/lib/mock-server/creative-ad-server/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/creative-ad-server/server.mjs +0 -6
- package/dist/lib/mock-server/creative-ad-server/server.mjs.map +1 -1
- package/dist/lib/mock-server/creative-template/seed-data.mjs +0 -6
- package/dist/lib/mock-server/creative-template/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/creative-template/server.mjs +0 -6
- package/dist/lib/mock-server/creative-template/server.mjs.map +1 -1
- package/dist/lib/mock-server/index.mjs +0 -6
- package/dist/lib/mock-server/index.mjs.map +1 -1
- package/dist/lib/mock-server/sales-guaranteed/recipe.mjs +0 -6
- package/dist/lib/mock-server/sales-guaranteed/recipe.mjs.map +1 -1
- package/dist/lib/mock-server/sales-guaranteed/seed-data.mjs +0 -6
- package/dist/lib/mock-server/sales-guaranteed/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/sales-guaranteed/server.mjs +0 -6
- package/dist/lib/mock-server/sales-guaranteed/server.mjs.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.mjs +0 -6
- package/dist/lib/mock-server/sales-non-guaranteed/recipe.mjs.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.mjs +0 -6
- package/dist/lib/mock-server/sales-non-guaranteed/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/sales-non-guaranteed/server.mjs +0 -6
- package/dist/lib/mock-server/sales-non-guaranteed/server.mjs.map +1 -1
- package/dist/lib/mock-server/sales-social/seed-data.mjs +0 -6
- package/dist/lib/mock-server/sales-social/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/sales-social/server.mjs +0 -6
- package/dist/lib/mock-server/sales-social/server.mjs.map +1 -1
- package/dist/lib/mock-server/scenario.mjs +0 -6
- package/dist/lib/mock-server/scenario.mjs.map +1 -1
- package/dist/lib/mock-server/signal-marketplace/seed-data.mjs +0 -6
- package/dist/lib/mock-server/signal-marketplace/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/signal-marketplace/server.mjs +0 -6
- package/dist/lib/mock-server/signal-marketplace/server.mjs.map +1 -1
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.mjs +0 -6
- package/dist/lib/mock-server/sponsored-intelligence/seed-data.mjs.map +1 -1
- package/dist/lib/mock-server/sponsored-intelligence/server.mjs +0 -6
- package/dist/lib/mock-server/sponsored-intelligence/server.mjs.map +1 -1
- package/dist/lib/net/address-guards.mjs +0 -6
- package/dist/lib/net/address-guards.mjs.map +1 -1
- package/dist/lib/net/index.mjs +0 -6
- package/dist/lib/net/index.mjs.map +1 -1
- package/dist/lib/net/ssrf-fetch.mjs +0 -6
- package/dist/lib/net/ssrf-fetch.mjs.map +1 -1
- package/dist/lib/observability/index.mjs +0 -6
- package/dist/lib/observability/index.mjs.map +1 -1
- package/dist/lib/observability/tracing.mjs +6 -7
- package/dist/lib/observability/tracing.mjs.map +1 -1
- package/dist/lib/protocols/a2a.mjs +0 -6
- package/dist/lib/protocols/a2a.mjs.map +1 -1
- package/dist/lib/protocols/abort.mjs +0 -6
- package/dist/lib/protocols/abort.mjs.map +1 -1
- package/dist/lib/protocols/index.mjs +0 -6
- package/dist/lib/protocols/index.mjs.map +1 -1
- package/dist/lib/protocols/mcp-modern.mjs +0 -6
- package/dist/lib/protocols/mcp-modern.mjs.map +1 -1
- package/dist/lib/protocols/mcp-tasks.mjs +0 -6
- package/dist/lib/protocols/mcp-tasks.mjs.map +1 -1
- package/dist/lib/protocols/mcp.mjs +0 -6
- package/dist/lib/protocols/mcp.mjs.map +1 -1
- package/dist/lib/protocols/rawResponseCapture.mjs +0 -6
- package/dist/lib/protocols/rawResponseCapture.mjs.map +1 -1
- package/dist/lib/protocols/responseSizeLimit.mjs +0 -6
- package/dist/lib/protocols/responseSizeLimit.mjs.map +1 -1
- package/dist/lib/protocols/transportDiagnostics.mjs +0 -6
- package/dist/lib/protocols/transportDiagnostics.mjs.map +1 -1
- package/dist/lib/registry/cursor-store.mjs +0 -6
- package/dist/lib/registry/cursor-store.mjs.map +1 -1
- package/dist/lib/registry/feed-stream.mjs +0 -6
- package/dist/lib/registry/feed-stream.mjs.map +1 -1
- package/dist/lib/registry/index.mjs +0 -6
- package/dist/lib/registry/index.mjs.map +1 -1
- package/dist/lib/registry/property-registry.mjs +0 -6
- package/dist/lib/registry/property-registry.mjs.map +1 -1
- package/dist/lib/registry/sync.mjs +0 -6
- package/dist/lib/registry/sync.mjs.map +1 -1
- package/dist/lib/registry/types.generated.mjs +0 -6
- package/dist/lib/registry/types.mjs +0 -6
- package/dist/lib/schemas/index.mjs +0 -6
- package/dist/lib/schemas/index.mjs.map +1 -1
- package/dist/lib/schemas-data/v2.5/_provenance.json +1 -1
- package/dist/lib/server/a2a-adapter.mjs +0 -6
- package/dist/lib/server/a2a-adapter.mjs.map +1 -1
- package/dist/lib/server/account-mode.mjs +0 -6
- package/dist/lib/server/account-mode.mjs.map +1 -1
- package/dist/lib/server/adcp-server.mjs +0 -6
- package/dist/lib/server/adcp-server.mjs.map +1 -1
- package/dist/lib/server/auth-introspection.mjs +0 -6
- package/dist/lib/server/auth-introspection.mjs.map +1 -1
- package/dist/lib/server/auth-signature.mjs +0 -6
- package/dist/lib/server/auth-signature.mjs.map +1 -1
- package/dist/lib/server/auth.mjs +0 -6
- package/dist/lib/server/auth.mjs.map +1 -1
- package/dist/lib/server/create-adcp-server.d.mts +13 -0
- package/dist/lib/server/create-adcp-server.d.ts +13 -0
- package/dist/lib/server/create-adcp-server.d.ts.map +1 -1
- package/dist/lib/server/create-adcp-server.js +3 -2
- package/dist/lib/server/create-adcp-server.js.map +1 -1
- package/dist/lib/server/create-adcp-server.mjs +3 -8
- package/dist/lib/server/create-adcp-server.mjs.map +1 -1
- package/dist/lib/server/credential-policy.mjs +0 -6
- package/dist/lib/server/credential-policy.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/memory.mjs +0 -6
- package/dist/lib/server/ctx-metadata/backends/memory.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/pg.mjs +0 -6
- package/dist/lib/server/ctx-metadata/backends/pg.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/backends/redis.mjs +0 -6
- package/dist/lib/server/ctx-metadata/backends/redis.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/index.mjs +0 -6
- package/dist/lib/server/ctx-metadata/index.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/store.mjs +0 -6
- package/dist/lib/server/ctx-metadata/store.mjs.map +1 -1
- package/dist/lib/server/ctx-metadata/wire-shape.mjs +0 -6
- package/dist/lib/server/ctx-metadata/wire-shape.mjs.map +1 -1
- package/dist/lib/server/decisioning/account.mjs +0 -6
- package/dist/lib/server/decisioning/account.mjs.map +1 -1
- package/dist/lib/server/decisioning/admin-router.mjs +0 -6
- package/dist/lib/server/decisioning/admin-router.mjs.map +1 -1
- package/dist/lib/server/decisioning/assembly-helpers.mjs +0 -6
- package/dist/lib/server/decisioning/assembly-helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/async-outcome.mjs +0 -6
- package/dist/lib/server/decisioning/async-outcome.mjs.map +1 -1
- package/dist/lib/server/decisioning/buyer-agent.mjs +0 -6
- package/dist/lib/server/decisioning/buyer-agent.mjs.map +1 -1
- package/dist/lib/server/decisioning/capabilities.mjs +0 -6
- package/dist/lib/server/decisioning/capabilities.mjs.map +1 -1
- package/dist/lib/server/decisioning/compose.mjs +0 -6
- package/dist/lib/server/decisioning/compose.mjs.map +1 -1
- package/dist/lib/server/decisioning/context.mjs +0 -6
- package/dist/lib/server/decisioning/errors-typed.mjs +0 -6
- package/dist/lib/server/decisioning/errors-typed.mjs.map +1 -1
- package/dist/lib/server/decisioning/helpers.mjs +0 -6
- package/dist/lib/server/decisioning/helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/index.mjs +0 -6
- package/dist/lib/server/decisioning/index.mjs.map +1 -1
- package/dist/lib/server/decisioning/list-helpers.mjs +0 -6
- package/dist/lib/server/decisioning/list-helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/manifest-helpers.mjs +0 -6
- package/dist/lib/server/decisioning/manifest-helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/pagination.mjs +0 -6
- package/dist/lib/server/decisioning/platform-helpers.mjs +0 -6
- package/dist/lib/server/decisioning/platform-helpers.mjs.map +1 -1
- package/dist/lib/server/decisioning/platform.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/dispatch.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/dispatch.mjs.map +1 -1
- package/dist/lib/server/decisioning/proposal/index.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/index.mjs.map +1 -1
- package/dist/lib/server/decisioning/proposal/lifecycle.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/lifecycle.mjs.map +1 -1
- package/dist/lib/server/decisioning/proposal/mock-manager.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/mock-manager.mjs.map +1 -1
- package/dist/lib/server/decisioning/proposal/store.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/store.mjs.map +1 -1
- package/dist/lib/server/decisioning/proposal/types.mjs +0 -6
- package/dist/lib/server/decisioning/proposal/types.mjs.map +1 -1
- package/dist/lib/server/decisioning/resolve-presets.mjs +0 -6
- package/dist/lib/server/decisioning/resolve-presets.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/entity-hydration.generated.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/entity-hydration.generated.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/from-platform.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/from-platform.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/observed-modes.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/observed-modes.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/postgres-task-registry.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/protocol-for-tool.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/task-registry.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/task-registry.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/to-context.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/to-context.mjs.map +1 -1
- package/dist/lib/server/decisioning/runtime/validate-platform.mjs +0 -6
- package/dist/lib/server/decisioning/runtime/validate-platform.mjs.map +1 -1
- package/dist/lib/server/decisioning/specialisms/audiences.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/brand-rights.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/campaign-governance.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/content-standards.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/creative-ad-server.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/creative.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/lists.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/sales.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/signals.mjs +0 -6
- package/dist/lib/server/decisioning/specialisms/sponsored-intelligence.mjs +0 -6
- package/dist/lib/server/decisioning/start-time.mjs +0 -6
- package/dist/lib/server/decisioning/start-time.mjs.map +1 -1
- package/dist/lib/server/decisioning/status-changes.mjs +0 -6
- package/dist/lib/server/decisioning/status-changes.mjs.map +1 -1
- package/dist/lib/server/decisioning/status-mappers.mjs +0 -6
- package/dist/lib/server/decisioning/status-mappers.mjs.map +1 -1
- package/dist/lib/server/decisioning/tenant-registry.mjs +0 -6
- package/dist/lib/server/decisioning/tenant-registry.mjs.map +1 -1
- package/dist/lib/server/decisioning/tenant-store.mjs +0 -6
- package/dist/lib/server/decisioning/tenant-store.mjs.map +1 -1
- package/dist/lib/server/decisioning/validate-specialisms.mjs +0 -6
- package/dist/lib/server/decisioning/validate-specialisms.mjs.map +1 -1
- package/dist/lib/server/dynamic-registry.mjs +0 -6
- package/dist/lib/server/dynamic-registry.mjs.map +1 -1
- package/dist/lib/server/envelope-allowlist.mjs +0 -6
- package/dist/lib/server/envelope-allowlist.mjs.map +1 -1
- package/dist/lib/server/error-arm-tools.mjs +5 -6
- package/dist/lib/server/error-arm-tools.mjs.map +1 -1
- package/dist/lib/server/errors.mjs +0 -6
- package/dist/lib/server/errors.mjs.map +1 -1
- package/dist/lib/server/example-tld-guard.mjs +0 -6
- package/dist/lib/server/example-tld-guard.mjs.map +1 -1
- package/dist/lib/server/express-adapter.mjs +0 -6
- package/dist/lib/server/express-adapter.mjs.map +1 -1
- package/dist/lib/server/governance.mjs +0 -6
- package/dist/lib/server/governance.mjs.map +1 -1
- package/dist/lib/server/idempotency/backends/lazy.mjs +0 -6
- package/dist/lib/server/idempotency/backends/lazy.mjs.map +1 -1
- package/dist/lib/server/idempotency/backends/memory.mjs +0 -6
- package/dist/lib/server/idempotency/backends/memory.mjs.map +1 -1
- package/dist/lib/server/idempotency/backends/pg.mjs +0 -6
- package/dist/lib/server/idempotency/backends/pg.mjs.map +1 -1
- package/dist/lib/server/idempotency/backends/redis.mjs +0 -6
- package/dist/lib/server/idempotency/backends/redis.mjs.map +1 -1
- package/dist/lib/server/idempotency/index.mjs +0 -6
- package/dist/lib/server/idempotency/index.mjs.map +1 -1
- package/dist/lib/server/idempotency/store.mjs +0 -6
- package/dist/lib/server/idempotency/store.mjs.map +1 -1
- package/dist/lib/server/index.d.mts +1 -1
- package/dist/lib/server/index.d.ts +1 -1
- package/dist/lib/server/index.d.ts.map +1 -1
- package/dist/lib/server/index.js.map +1 -1
- package/dist/lib/server/index.mjs +0 -6
- package/dist/lib/server/index.mjs.map +1 -1
- package/dist/lib/server/legacy/v5/index.d.mts +1 -1
- package/dist/lib/server/legacy/v5/index.d.ts +1 -1
- package/dist/lib/server/legacy/v5/index.d.ts.map +1 -1
- package/dist/lib/server/legacy/v5/index.js.map +1 -1
- package/dist/lib/server/legacy/v5/index.mjs +0 -6
- package/dist/lib/server/legacy/v5/index.mjs.map +1 -1
- package/dist/lib/server/mcp-modern-server.mjs +0 -6
- package/dist/lib/server/mcp-modern-server.mjs.map +1 -1
- package/dist/lib/server/media-buy-actions.mjs +0 -6
- package/dist/lib/server/media-buy-actions.mjs.map +1 -1
- package/dist/lib/server/media-buy-helpers.mjs +0 -6
- package/dist/lib/server/media-buy-helpers.mjs.map +1 -1
- package/dist/lib/server/media-buy-store.mjs +0 -6
- package/dist/lib/server/media-buy-store.mjs.map +1 -1
- package/dist/lib/server/normalize-errors.mjs +0 -6
- package/dist/lib/server/normalize-errors.mjs.map +1 -1
- package/dist/lib/server/operational-platform.mjs +0 -6
- package/dist/lib/server/operational-platform.mjs.map +1 -1
- package/dist/lib/server/pick-safe-details.mjs +0 -6
- package/dist/lib/server/pick-safe-details.mjs.map +1 -1
- package/dist/lib/server/pin-and-bind-fetch.mjs +0 -6
- package/dist/lib/server/pin-and-bind-fetch.mjs.map +1 -1
- package/dist/lib/server/postgres-state-store.mjs +0 -6
- package/dist/lib/server/postgres-state-store.mjs.map +1 -1
- package/dist/lib/server/postgres-task-store.mjs +0 -6
- package/dist/lib/server/postgres-task-store.mjs.map +1 -1
- package/dist/lib/server/product-defaults.mjs +0 -6
- package/dist/lib/server/product-defaults.mjs.map +1 -1
- package/dist/lib/server/redact.mjs +0 -6
- package/dist/lib/server/redact.mjs.map +1 -1
- package/dist/lib/server/responses.mjs +0 -6
- package/dist/lib/server/responses.mjs.map +1 -1
- package/dist/lib/server/serve.mjs +0 -6
- package/dist/lib/server/serve.mjs.map +1 -1
- package/dist/lib/server/socket-mode/conformance-client.mjs +0 -6
- package/dist/lib/server/socket-mode/conformance-client.mjs.map +1 -1
- package/dist/lib/server/socket-mode/index.mjs +0 -6
- package/dist/lib/server/socket-mode/index.mjs.map +1 -1
- package/dist/lib/server/socket-mode/ws-transport.mjs +0 -6
- package/dist/lib/server/socket-mode/ws-transport.mjs.map +1 -1
- package/dist/lib/server/state-machine.mjs +0 -6
- package/dist/lib/server/state-machine.mjs.map +1 -1
- package/dist/lib/server/state-store.mjs +0 -6
- package/dist/lib/server/state-store.mjs.map +1 -1
- package/dist/lib/server/structured-serialize.mjs +0 -6
- package/dist/lib/server/structured-serialize.mjs.map +1 -1
- package/dist/lib/server/targeting-helpers.mjs +0 -6
- package/dist/lib/server/targeting-helpers.mjs.map +1 -1
- package/dist/lib/server/tasks.mjs +0 -6
- package/dist/lib/server/tasks.mjs.map +1 -1
- package/dist/lib/server/test-controller-bridge.mjs +0 -6
- package/dist/lib/server/test-controller-bridge.mjs.map +1 -1
- package/dist/lib/server/test-controller.mjs +0 -6
- package/dist/lib/server/test-controller.mjs.map +1 -1
- package/dist/lib/server/upstream-helpers.mjs +0 -6
- package/dist/lib/server/upstream-helpers.mjs.map +1 -1
- package/dist/lib/server/webhook-emitter.mjs +0 -6
- package/dist/lib/server/webhook-emitter.mjs.map +1 -1
- package/dist/lib/server/wire-safe.mjs +0 -6
- package/dist/lib/server/wire-safe.mjs.map +1 -1
- package/dist/lib/server/wire-spec-fields.generated.mjs +0 -6
- package/dist/lib/server/wire-spec-fields.generated.mjs.map +1 -1
- package/dist/lib/server/wrap-envelope.mjs +0 -6
- package/dist/lib/server/wrap-envelope.mjs.map +1 -1
- package/dist/lib/signing/agent-context.mjs +0 -6
- package/dist/lib/signing/agent-context.mjs.map +1 -1
- package/dist/lib/signing/agent-fetch.mjs +0 -6
- package/dist/lib/signing/agent-fetch.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/canonicalize.mjs +0 -6
- package/dist/lib/signing/agent-resolver/canonicalize.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/capabilities-types.mjs +0 -6
- package/dist/lib/signing/agent-resolver/capabilities-types.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/consistency.mjs +0 -6
- package/dist/lib/signing/agent-resolver/consistency.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/errors.mjs +0 -6
- package/dist/lib/signing/agent-resolver/errors.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/etld.mjs +0 -6
- package/dist/lib/signing/agent-resolver/etld.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs +0 -6
- package/dist/lib/signing/agent-resolver/fetch-helpers.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/index.mjs +0 -6
- package/dist/lib/signing/agent-resolver/index.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/jwks-set.mjs +0 -6
- package/dist/lib/signing/agent-resolver/jwks-set.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/resolve-agent.mjs +0 -6
- package/dist/lib/signing/agent-resolver/resolve-agent.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/select-agent.mjs +0 -6
- package/dist/lib/signing/agent-resolver/select-agent.mjs.map +1 -1
- package/dist/lib/signing/agent-resolver/strict-json.mjs +0 -6
- package/dist/lib/signing/agent-resolver/strict-json.mjs.map +1 -1
- package/dist/lib/signing/brand-jwks.mjs +0 -6
- package/dist/lib/signing/brand-jwks.mjs.map +1 -1
- package/dist/lib/signing/canonicalize.mjs +0 -6
- package/dist/lib/signing/canonicalize.mjs.map +1 -1
- package/dist/lib/signing/capability-cache.mjs +0 -6
- package/dist/lib/signing/capability-cache.mjs.map +1 -1
- package/dist/lib/signing/capability-priming.mjs +0 -6
- package/dist/lib/signing/capability-priming.mjs.map +1 -1
- package/dist/lib/signing/client.mjs +0 -6
- package/dist/lib/signing/client.mjs.map +1 -1
- package/dist/lib/signing/content-digest.mjs +0 -6
- package/dist/lib/signing/content-digest.mjs.map +1 -1
- package/dist/lib/signing/crypto.mjs +0 -6
- package/dist/lib/signing/crypto.mjs.map +1 -1
- package/dist/lib/signing/ecdsa-encoding.mjs +0 -6
- package/dist/lib/signing/ecdsa-encoding.mjs.map +1 -1
- package/dist/lib/signing/errors.mjs +0 -6
- package/dist/lib/signing/errors.mjs.map +1 -1
- package/dist/lib/signing/fetch-async.mjs +0 -6
- package/dist/lib/signing/fetch-async.mjs.map +1 -1
- package/dist/lib/signing/fetch.mjs +0 -6
- package/dist/lib/signing/fetch.mjs.map +1 -1
- package/dist/lib/signing/index.mjs +0 -6
- package/dist/lib/signing/index.mjs.map +1 -1
- package/dist/lib/signing/jwks-helpers.mjs +0 -6
- package/dist/lib/signing/jwks-helpers.mjs.map +1 -1
- package/dist/lib/signing/jwks-https.mjs +0 -6
- package/dist/lib/signing/jwks-https.mjs.map +1 -1
- package/dist/lib/signing/jwks.mjs +0 -6
- package/dist/lib/signing/jwks.mjs.map +1 -1
- package/dist/lib/signing/middleware.mjs +0 -6
- package/dist/lib/signing/middleware.mjs.map +1 -1
- package/dist/lib/signing/parser.mjs +0 -6
- package/dist/lib/signing/parser.mjs.map +1 -1
- package/dist/lib/signing/postgres-replay-store.mjs +0 -6
- package/dist/lib/signing/postgres-replay-store.mjs.map +1 -1
- package/dist/lib/signing/protocol-response.mjs +0 -6
- package/dist/lib/signing/protocol-response.mjs.map +1 -1
- package/dist/lib/signing/provider.mjs +0 -6
- package/dist/lib/signing/redis-replay-store.mjs +0 -6
- package/dist/lib/signing/redis-replay-store.mjs.map +1 -1
- package/dist/lib/signing/replay.mjs +0 -6
- package/dist/lib/signing/replay.mjs.map +1 -1
- package/dist/lib/signing/request-context.mjs +0 -6
- package/dist/lib/signing/request-context.mjs.map +1 -1
- package/dist/lib/signing/revocation-https.mjs +0 -6
- package/dist/lib/signing/revocation-https.mjs.map +1 -1
- package/dist/lib/signing/revocation.mjs +0 -6
- package/dist/lib/signing/revocation.mjs.map +1 -1
- package/dist/lib/signing/server.mjs +0 -6
- package/dist/lib/signing/server.mjs.map +1 -1
- package/dist/lib/signing/signer-async.mjs +0 -6
- package/dist/lib/signing/signer-async.mjs.map +1 -1
- package/dist/lib/signing/signer.mjs +0 -6
- package/dist/lib/signing/signer.mjs.map +1 -1
- package/dist/lib/signing/testing.mjs +0 -6
- package/dist/lib/signing/testing.mjs.map +1 -1
- package/dist/lib/signing/types.mjs +0 -6
- package/dist/lib/signing/types.mjs.map +1 -1
- package/dist/lib/signing/verifier.mjs +0 -6
- package/dist/lib/signing/verifier.mjs.map +1 -1
- package/dist/lib/signing/webhook-auth-detection.mjs +0 -6
- package/dist/lib/signing/webhook-auth-detection.mjs.map +1 -1
- package/dist/lib/signing/webhook-verifier.mjs +0 -6
- package/dist/lib/signing/webhook-verifier.mjs.map +1 -1
- package/dist/lib/storage/MemoryStorage.mjs +0 -6
- package/dist/lib/storage/MemoryStorage.mjs.map +1 -1
- package/dist/lib/storage/interfaces.mjs +0 -6
- package/dist/lib/substitution/encoder/SubstitutionEncoder.mjs +0 -6
- package/dist/lib/substitution/encoder/SubstitutionEncoder.mjs.map +1 -1
- package/dist/lib/substitution/encoder/index.mjs +0 -6
- package/dist/lib/substitution/encoder/index.mjs.map +1 -1
- package/dist/lib/substitution/index.mjs +0 -6
- package/dist/lib/substitution/index.mjs.map +1 -1
- package/dist/lib/substitution/observer/SubstitutionObserver.mjs +0 -6
- package/dist/lib/substitution/observer/SubstitutionObserver.mjs.map +1 -1
- package/dist/lib/substitution/observer/alignment.mjs +0 -6
- package/dist/lib/substitution/observer/alignment.mjs.map +1 -1
- package/dist/lib/substitution/observer/assertions.mjs +0 -6
- package/dist/lib/substitution/observer/assertions.mjs.map +1 -1
- package/dist/lib/substitution/observer/html-parser.mjs +0 -6
- package/dist/lib/substitution/observer/html-parser.mjs.map +1 -1
- package/dist/lib/substitution/observer/index.mjs +0 -6
- package/dist/lib/substitution/observer/index.mjs.map +1 -1
- package/dist/lib/substitution/observer/ssrf.mjs +0 -6
- package/dist/lib/substitution/observer/ssrf.mjs.map +1 -1
- package/dist/lib/substitution/rfc3986.mjs +0 -6
- package/dist/lib/substitution/rfc3986.mjs.map +1 -1
- package/dist/lib/substitution/translate.mjs +0 -6
- package/dist/lib/substitution/translate.mjs.map +1 -1
- package/dist/lib/substitution/types.mjs +0 -6
- package/dist/lib/substitution/vectors.mjs +0 -6
- package/dist/lib/substitution/vectors.mjs.map +1 -1
- package/dist/lib/testing/agent-tester.mjs +0 -6
- package/dist/lib/testing/agent-tester.mjs.map +1 -1
- package/dist/lib/testing/client.mjs +0 -6
- package/dist/lib/testing/client.mjs.map +1 -1
- package/dist/lib/testing/compliance/briefs.mjs +0 -6
- package/dist/lib/testing/compliance/briefs.mjs.map +1 -1
- package/dist/lib/testing/compliance/comply.mjs +0 -6
- package/dist/lib/testing/compliance/comply.mjs.map +1 -1
- package/dist/lib/testing/compliance/index.mjs +0 -6
- package/dist/lib/testing/compliance/index.mjs.map +1 -1
- package/dist/lib/testing/compliance/spec-conformance.mjs +0 -6
- package/dist/lib/testing/compliance/spec-conformance.mjs.map +1 -1
- package/dist/lib/testing/compliance/storyboard-tracks.mjs +0 -6
- package/dist/lib/testing/compliance/storyboard-tracks.mjs.map +1 -1
- package/dist/lib/testing/compliance/summary.mjs +0 -6
- package/dist/lib/testing/compliance/summary.mjs.map +1 -1
- package/dist/lib/testing/compliance/types.mjs +0 -6
- package/dist/lib/testing/comply-controller.mjs +0 -6
- package/dist/lib/testing/comply-controller.mjs.map +1 -1
- package/dist/lib/testing/controller-assertions.mjs +0 -6
- package/dist/lib/testing/controller-assertions.mjs.map +1 -1
- package/dist/lib/testing/formatter.mjs +0 -6
- package/dist/lib/testing/formatter.mjs.map +1 -1
- package/dist/lib/testing/index.mjs +0 -6
- package/dist/lib/testing/index.mjs.map +1 -1
- package/dist/lib/testing/local-agent-runner.mjs +0 -6
- package/dist/lib/testing/local-agent-runner.mjs.map +1 -1
- package/dist/lib/testing/orchestrator.mjs +0 -6
- package/dist/lib/testing/orchestrator.mjs.map +1 -1
- package/dist/lib/testing/personas/index.mjs +0 -6
- package/dist/lib/testing/personas/index.mjs.map +1 -1
- package/dist/lib/testing/scenarios/brand-rights.mjs +0 -6
- package/dist/lib/testing/scenarios/brand-rights.mjs.map +1 -1
- package/dist/lib/testing/scenarios/capabilities.mjs +0 -6
- package/dist/lib/testing/scenarios/capabilities.mjs.map +1 -1
- package/dist/lib/testing/scenarios/creative.mjs +0 -6
- package/dist/lib/testing/scenarios/creative.mjs.map +1 -1
- package/dist/lib/testing/scenarios/deterministic.mjs +0 -6
- package/dist/lib/testing/scenarios/deterministic.mjs.map +1 -1
- package/dist/lib/testing/scenarios/discovery.mjs +0 -6
- package/dist/lib/testing/scenarios/discovery.mjs.map +1 -1
- package/dist/lib/testing/scenarios/edge-cases.mjs +0 -6
- package/dist/lib/testing/scenarios/edge-cases.mjs.map +1 -1
- package/dist/lib/testing/scenarios/error-compliance.mjs +0 -6
- package/dist/lib/testing/scenarios/error-compliance.mjs.map +1 -1
- package/dist/lib/testing/scenarios/governance.mjs +0 -6
- package/dist/lib/testing/scenarios/governance.mjs.map +1 -1
- package/dist/lib/testing/scenarios/health.mjs +0 -6
- package/dist/lib/testing/scenarios/health.mjs.map +1 -1
- package/dist/lib/testing/scenarios/index.mjs +0 -6
- package/dist/lib/testing/scenarios/index.mjs.map +1 -1
- package/dist/lib/testing/scenarios/media-buy.mjs +0 -6
- package/dist/lib/testing/scenarios/media-buy.mjs.map +1 -1
- package/dist/lib/testing/scenarios/schema-compliance.mjs +0 -6
- package/dist/lib/testing/scenarios/schema-compliance.mjs.map +1 -1
- package/dist/lib/testing/scenarios/signals.mjs +0 -6
- package/dist/lib/testing/scenarios/signals.mjs.map +1 -1
- package/dist/lib/testing/scenarios/sponsored-intelligence.mjs +0 -6
- package/dist/lib/testing/scenarios/sponsored-intelligence.mjs.map +1 -1
- package/dist/lib/testing/scenarios/trusted-match.mjs +0 -6
- package/dist/lib/testing/scenarios/trusted-match.mjs.map +1 -1
- package/dist/lib/testing/seed-merge.mjs +0 -6
- package/dist/lib/testing/seed-merge.mjs.map +1 -1
- package/dist/lib/testing/storyboard/agent-routing.mjs +0 -6
- package/dist/lib/testing/storyboard/agent-routing.mjs.map +1 -1
- package/dist/lib/testing/storyboard/assertions.mjs +0 -6
- package/dist/lib/testing/storyboard/assertions.mjs.map +1 -1
- package/dist/lib/testing/storyboard/canonical-format-satisfaction.mjs +0 -6
- package/dist/lib/testing/storyboard/canonical-format-satisfaction.mjs.map +1 -1
- package/dist/lib/testing/storyboard/compliance.mjs +5 -6
- package/dist/lib/testing/storyboard/compliance.mjs.map +1 -1
- package/dist/lib/testing/storyboard/context.mjs +0 -6
- package/dist/lib/testing/storyboard/context.mjs.map +1 -1
- package/dist/lib/testing/storyboard/default-invariants.mjs +0 -6
- package/dist/lib/testing/storyboard/default-invariants.mjs.map +1 -1
- package/dist/lib/testing/storyboard/index.mjs +0 -6
- package/dist/lib/testing/storyboard/index.mjs.map +1 -1
- package/dist/lib/testing/storyboard/junit.mjs +0 -6
- package/dist/lib/testing/storyboard/junit.mjs.map +1 -1
- package/dist/lib/testing/storyboard/loader.mjs +0 -6
- package/dist/lib/testing/storyboard/loader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/parallel-dispatch.mjs +0 -6
- package/dist/lib/testing/storyboard/parallel-dispatch.mjs.map +1 -1
- package/dist/lib/testing/storyboard/path.mjs +0 -6
- package/dist/lib/testing/storyboard/path.mjs.map +1 -1
- package/dist/lib/testing/storyboard/probes.mjs +0 -6
- package/dist/lib/testing/storyboard/probes.mjs.map +1 -1
- package/dist/lib/testing/storyboard/rate-limit-trip.mjs +0 -6
- package/dist/lib/testing/storyboard/rate-limit-trip.mjs.map +1 -1
- package/dist/lib/testing/storyboard/rejection-hints.mjs +0 -6
- package/dist/lib/testing/storyboard/rejection-hints.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-builder.mjs +0 -6
- package/dist/lib/testing/storyboard/request-builder.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/builder.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/builder.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/grader.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/grader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/index.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/index.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/probe-dispatch.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/probe.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/probe.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/synthesize.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/synthesize.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/test-kit.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/test-kit.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/types.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/types.mjs.map +1 -1
- package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs +0 -6
- package/dist/lib/testing/storyboard/request-signing/vector-loader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/runner.mjs +0 -6
- package/dist/lib/testing/storyboard/runner.mjs.map +1 -1
- package/dist/lib/testing/storyboard/sandbox-entities.mjs +0 -6
- package/dist/lib/testing/storyboard/sandbox-entities.mjs.map +1 -1
- package/dist/lib/testing/storyboard/seeding.mjs +0 -6
- package/dist/lib/testing/storyboard/seeding.mjs.map +1 -1
- package/dist/lib/testing/storyboard/shape-drift-hints.mjs +0 -6
- package/dist/lib/testing/storyboard/shape-drift-hints.mjs.map +1 -1
- package/dist/lib/testing/storyboard/signer-grader/grader.mjs +0 -6
- package/dist/lib/testing/storyboard/signer-grader/grader.mjs.map +1 -1
- package/dist/lib/testing/storyboard/signer-grader/index.mjs +0 -6
- package/dist/lib/testing/storyboard/signer-grader/index.mjs.map +1 -1
- package/dist/lib/testing/storyboard/signer-grader/types.mjs +0 -6
- package/dist/lib/testing/storyboard/strict-validation-hints.mjs +0 -6
- package/dist/lib/testing/storyboard/strict-validation-hints.mjs.map +1 -1
- package/dist/lib/testing/storyboard/task-map.mjs +0 -6
- package/dist/lib/testing/storyboard/task-map.mjs.map +1 -1
- package/dist/lib/testing/storyboard/test-kit.mjs +0 -6
- package/dist/lib/testing/storyboard/test-kit.mjs.map +1 -1
- package/dist/lib/testing/storyboard/types.mjs +0 -6
- package/dist/lib/testing/storyboard/types.mjs.map +1 -1
- package/dist/lib/testing/storyboard/validations.mjs +0 -6
- package/dist/lib/testing/storyboard/validations.mjs.map +1 -1
- package/dist/lib/testing/storyboard/webhook-assertions.mjs +0 -6
- package/dist/lib/testing/storyboard/webhook-assertions.mjs.map +1 -1
- package/dist/lib/testing/storyboard/webhook-receiver.mjs +0 -6
- package/dist/lib/testing/storyboard/webhook-receiver.mjs.map +1 -1
- package/dist/lib/testing/stubs/governance-agent-stub.mjs +0 -6
- package/dist/lib/testing/stubs/governance-agent-stub.mjs.map +1 -1
- package/dist/lib/testing/stubs/index.mjs +0 -6
- package/dist/lib/testing/stubs/index.mjs.map +1 -1
- package/dist/lib/testing/test-controller.mjs +0 -6
- package/dist/lib/testing/test-controller.mjs.map +1 -1
- package/dist/lib/testing/test-helpers.mjs +0 -6
- package/dist/lib/testing/test-helpers.mjs.map +1 -1
- package/dist/lib/testing/types.mjs +0 -6
- package/dist/lib/types/adcp.mjs +0 -6
- package/dist/lib/types/adcp.mjs.map +1 -1
- package/dist/lib/types/asset-instances.mjs +0 -6
- package/dist/lib/types/compat.mjs +0 -6
- package/dist/lib/types/compat.mjs.map +1 -1
- package/dist/lib/types/core.generated.mjs +0 -6
- package/dist/lib/types/enums.generated.mjs +0 -6
- package/dist/lib/types/enums.generated.mjs.map +1 -1
- package/dist/lib/types/error-codes.mjs +0 -6
- package/dist/lib/types/error-codes.mjs.map +1 -1
- package/dist/lib/types/error-details.aliases.mjs +0 -6
- package/dist/lib/types/error-details.aliases.mjs.map +1 -1
- package/dist/lib/types/format-asset-slots.mjs +0 -6
- package/dist/lib/types/forward-compat-error-codes.mjs +0 -6
- package/dist/lib/types/forward-compat-error-codes.mjs.map +1 -1
- package/dist/lib/types/index.mjs +0 -6
- package/dist/lib/types/index.mjs.map +1 -1
- package/dist/lib/types/inline-enums.aliases.mjs +0 -6
- package/dist/lib/types/inline-enums.aliases.mjs.map +1 -1
- package/dist/lib/types/inline-enums.generated.mjs +0 -6
- package/dist/lib/types/inline-enums.generated.mjs.map +1 -1
- package/dist/lib/types/manifest.generated.mjs +0 -6
- package/dist/lib/types/manifest.generated.mjs.map +1 -1
- package/dist/lib/types/schemas.generated.mjs +0 -6
- package/dist/lib/types/schemas.generated.mjs.map +1 -1
- package/dist/lib/types/server-payload-aliases.mjs +0 -6
- package/dist/lib/types/server-payload.mjs +0 -6
- package/dist/lib/types/sync-rows.mjs +0 -6
- package/dist/lib/types/tools.generated.mjs +0 -6
- package/dist/lib/types/v2-5/index.mjs +0 -6
- package/dist/lib/types/v2-5/index.mjs.map +1 -1
- package/dist/lib/types/v2-5/tools.generated.mjs +0 -6
- package/dist/lib/types/v3-1-beta/index.mjs +0 -6
- package/dist/lib/types/v3-1-beta/index.mjs.map +1 -1
- package/dist/lib/types/v3-1-beta/tools.generated.mjs +0 -6
- package/dist/lib/types/wellknown-schemas.generated.mjs +0 -6
- package/dist/lib/types/wellknown-schemas.generated.mjs.map +1 -1
- package/dist/lib/upstream-recorder/constants.mjs +0 -6
- package/dist/lib/upstream-recorder/constants.mjs.map +1 -1
- package/dist/lib/upstream-recorder/index.mjs +0 -6
- package/dist/lib/upstream-recorder/index.mjs.map +1 -1
- package/dist/lib/upstream-recorder/recorder.mjs +0 -6
- package/dist/lib/upstream-recorder/recorder.mjs.map +1 -1
- package/dist/lib/upstream-recorder/types.mjs +0 -6
- package/dist/lib/upstream-recorder/types.mjs.map +1 -1
- package/dist/lib/utils/a2a-artifacts.mjs +0 -6
- package/dist/lib/utils/a2a-artifacts.mjs.map +1 -1
- package/dist/lib/utils/a2a-discovery.mjs +0 -6
- package/dist/lib/utils/a2a-discovery.mjs.map +1 -1
- package/dist/lib/utils/activation-key-builders.mjs +0 -6
- package/dist/lib/utils/activation-key-builders.mjs.map +1 -1
- package/dist/lib/utils/adcp-version-config.mjs +0 -6
- package/dist/lib/utils/adcp-version-config.mjs.map +1 -1
- package/dist/lib/utils/asset-builders.mjs +0 -6
- package/dist/lib/utils/asset-builders.mjs.map +1 -1
- package/dist/lib/utils/build-creative-return-builders.mjs +0 -6
- package/dist/lib/utils/build-creative-return-builders.mjs.map +1 -1
- package/dist/lib/utils/buyer-retry-policy.mjs +0 -6
- package/dist/lib/utils/buyer-retry-policy.mjs.map +1 -1
- package/dist/lib/utils/capabilities.mjs +0 -6
- package/dist/lib/utils/capabilities.mjs.map +1 -1
- package/dist/lib/utils/capability-rollups.mjs +0 -6
- package/dist/lib/utils/capability-rollups.mjs.map +1 -1
- package/dist/lib/utils/creative-adapter.mjs +0 -6
- package/dist/lib/utils/creative-adapter.mjs.map +1 -1
- package/dist/lib/utils/creative-delivery.mjs +0 -6
- package/dist/lib/utils/creative-delivery.mjs.map +1 -1
- package/dist/lib/utils/deprecation.mjs +0 -6
- package/dist/lib/utils/deprecation.mjs.map +1 -1
- package/dist/lib/utils/envelope-status-compat.mjs +0 -6
- package/dist/lib/utils/envelope-status-compat.mjs.map +1 -1
- package/dist/lib/utils/error-extraction.mjs +0 -6
- package/dist/lib/utils/error-extraction.mjs.map +1 -1
- package/dist/lib/utils/extract-result.mjs +0 -6
- package/dist/lib/utils/extract-result.mjs.map +1 -1
- package/dist/lib/utils/format-asset-slot-builders.mjs +0 -6
- package/dist/lib/utils/format-asset-slot-builders.mjs.map +1 -1
- package/dist/lib/utils/format-assets.mjs +0 -6
- package/dist/lib/utils/format-assets.mjs.map +1 -1
- package/dist/lib/utils/format-render-builders.mjs +0 -6
- package/dist/lib/utils/format-render-builders.mjs.map +1 -1
- package/dist/lib/utils/format-renders.mjs +0 -6
- package/dist/lib/utils/format-renders.mjs.map +1 -1
- package/dist/lib/utils/get-products-cache-scope.mjs +0 -6
- package/dist/lib/utils/get-products-cache-scope.mjs.map +1 -1
- package/dist/lib/utils/glob.mjs +0 -6
- package/dist/lib/utils/glob.mjs.map +1 -1
- package/dist/lib/utils/global-async-local-storage.mjs +0 -6
- package/dist/lib/utils/global-async-local-storage.mjs.map +1 -1
- package/dist/lib/utils/idempotency.mjs +0 -6
- package/dist/lib/utils/idempotency.mjs.map +1 -1
- package/dist/lib/utils/index.mjs +0 -6
- package/dist/lib/utils/index.mjs.map +1 -1
- package/dist/lib/utils/jcs.mjs +0 -6
- package/dist/lib/utils/jcs.mjs.map +1 -1
- package/dist/lib/utils/json-depth.mjs +0 -6
- package/dist/lib/utils/json-depth.mjs.map +1 -1
- package/dist/lib/utils/logger.mjs +0 -6
- package/dist/lib/utils/logger.mjs.map +1 -1
- package/dist/lib/utils/media-buy-delivery-notification-builders.mjs +0 -6
- package/dist/lib/utils/media-buy-delivery-notification-builders.mjs.map +1 -1
- package/dist/lib/utils/media-buy-status.mjs +0 -6
- package/dist/lib/utils/media-buy-status.mjs.map +1 -1
- package/dist/lib/utils/pagination.mjs +0 -6
- package/dist/lib/utils/pagination.mjs.map +1 -1
- package/dist/lib/utils/preview-creative-builders.mjs +0 -6
- package/dist/lib/utils/preview-creative-builders.mjs.map +1 -1
- package/dist/lib/utils/preview-normalizer.mjs +0 -6
- package/dist/lib/utils/preview-normalizer.mjs.map +1 -1
- package/dist/lib/utils/preview-utils.mjs +0 -6
- package/dist/lib/utils/preview-utils.mjs.map +1 -1
- package/dist/lib/utils/pricing-adapter.mjs +0 -6
- package/dist/lib/utils/pricing-adapter.mjs.map +1 -1
- package/dist/lib/utils/probe-policy.mjs +0 -6
- package/dist/lib/utils/probe-policy.mjs.map +1 -1
- package/dist/lib/utils/protocol-detection.mjs +0 -6
- package/dist/lib/utils/protocol-detection.mjs.map +1 -1
- package/dist/lib/utils/redact-secrets.mjs +0 -6
- package/dist/lib/utils/redact-secrets.mjs.map +1 -1
- package/dist/lib/utils/redis-default-prefix-warn.mjs +0 -6
- package/dist/lib/utils/redis-default-prefix-warn.mjs.map +1 -1
- package/dist/lib/utils/render-builders.mjs +0 -6
- package/dist/lib/utils/render-builders.mjs.map +1 -1
- package/dist/lib/utils/request-normalizer.mjs +0 -6
- package/dist/lib/utils/request-normalizer.mjs.map +1 -1
- package/dist/lib/utils/response-schemas.mjs +0 -6
- package/dist/lib/utils/response-schemas.mjs.map +1 -1
- package/dist/lib/utils/response-unwrapper.mjs +0 -6
- package/dist/lib/utils/response-unwrapper.mjs.map +1 -1
- package/dist/lib/utils/retry.mjs +0 -6
- package/dist/lib/utils/retry.mjs.map +1 -1
- package/dist/lib/utils/signal-discovery-helpers.mjs +0 -6
- package/dist/lib/utils/signal-discovery-helpers.mjs.map +1 -1
- package/dist/lib/utils/signal-id-builders.mjs +0 -6
- package/dist/lib/utils/signal-id-builders.mjs.map +1 -1
- package/dist/lib/utils/sync-creatives-adapter.mjs +0 -6
- package/dist/lib/utils/sync-creatives-adapter.mjs.map +1 -1
- package/dist/lib/utils/task-state.mjs +0 -6
- package/dist/lib/utils/task-state.mjs.map +1 -1
- package/dist/lib/utils/tool-request-schemas.mjs +0 -6
- package/dist/lib/utils/tool-request-schemas.mjs.map +1 -1
- package/dist/lib/utils/typeGuards.mjs +0 -6
- package/dist/lib/utils/typeGuards.mjs.map +1 -1
- package/dist/lib/utils/union-errors.mjs +0 -6
- package/dist/lib/utils/union-errors.mjs.map +1 -1
- package/dist/lib/utils/validate-user-agent.mjs +0 -6
- package/dist/lib/utils/validate-user-agent.mjs.map +1 -1
- package/dist/lib/v2/format-schema/fetch.mjs +0 -6
- package/dist/lib/v2/format-schema/fetch.mjs.map +1 -1
- package/dist/lib/v2/format-schema/index.mjs +0 -6
- package/dist/lib/v2/format-schema/index.mjs.map +1 -1
- package/dist/lib/v2/format-schema/regex-safety.mjs +0 -6
- package/dist/lib/v2/format-schema/regex-safety.mjs.map +1 -1
- package/dist/lib/v2/format-schema/resolver.mjs +7 -8
- package/dist/lib/v2/format-schema/resolver.mjs.map +1 -1
- package/dist/lib/v2/format-schema/sandbox-refs.mjs +0 -6
- package/dist/lib/v2/format-schema/sandbox-refs.mjs.map +1 -1
- package/dist/lib/v2/projection/augment-response.mjs +0 -6
- package/dist/lib/v2/projection/augment-response.mjs.map +1 -1
- package/dist/lib/v2/projection/builders.mjs +0 -6
- package/dist/lib/v2/projection/builders.mjs.map +1 -1
- package/dist/lib/v2/projection/cache-versions.mjs +0 -6
- package/dist/lib/v2/projection/cache-versions.mjs.map +1 -1
- package/dist/lib/v2/projection/canonical-properties.mjs +5 -6
- package/dist/lib/v2/projection/canonical-properties.mjs.map +1 -1
- package/dist/lib/v2/projection/catalog.mjs +5 -6
- package/dist/lib/v2/projection/catalog.mjs.map +1 -1
- package/dist/lib/v2/projection/constants.mjs +0 -6
- package/dist/lib/v2/projection/constants.mjs.map +1 -1
- package/dist/lib/v2/projection/index.mjs +0 -6
- package/dist/lib/v2/projection/index.mjs.map +1 -1
- package/dist/lib/v2/projection/registry.mjs +5 -6
- package/dist/lib/v2/projection/registry.mjs.map +1 -1
- package/dist/lib/v2/projection/types.mjs +0 -6
- package/dist/lib/v2/projection/v1-to-v2.mjs +0 -6
- package/dist/lib/v2/projection/v1-to-v2.mjs.map +1 -1
- package/dist/lib/v2/projection/v2-to-v1.mjs +0 -6
- package/dist/lib/v2/projection/v2-to-v1.mjs.map +1 -1
- package/dist/lib/v2/projection/write-side.mjs +0 -6
- package/dist/lib/v2/projection/write-side.mjs.map +1 -1
- package/dist/lib/v2/publisher-catalog/formats.mjs +0 -6
- package/dist/lib/v2/publisher-catalog/formats.mjs.map +1 -1
- package/dist/lib/v2/publisher-catalog/index.mjs +0 -6
- package/dist/lib/v2/publisher-catalog/index.mjs.map +1 -1
- package/dist/lib/validation/client-hooks.mjs +0 -6
- package/dist/lib/validation/client-hooks.mjs.map +1 -1
- package/dist/lib/validation/hints.mjs +0 -6
- package/dist/lib/validation/hints.mjs.map +1 -1
- package/dist/lib/validation/index.mjs +0 -6
- package/dist/lib/validation/index.mjs.map +1 -1
- package/dist/lib/validation/schema-errors.mjs +0 -6
- package/dist/lib/validation/schema-errors.mjs.map +1 -1
- package/dist/lib/validation/schema-loader.mjs +5 -6
- package/dist/lib/validation/schema-loader.mjs.map +1 -1
- package/dist/lib/validation/schema-validator.mjs +0 -6
- package/dist/lib/validation/schema-validator.mjs.map +1 -1
- package/dist/lib/validation/sync-creatives.mjs +0 -6
- package/dist/lib/validation/sync-creatives.mjs.map +1 -1
- package/dist/lib/version.d.mts +3 -3
- package/dist/lib/version.d.ts +3 -3
- package/dist/lib/version.js +3 -3
- package/dist/lib/version.js.map +1 -1
- package/dist/lib/version.mjs +3 -9
- package/dist/lib/version.mjs.map +1 -1
- package/dist/lib/webhooks/index.mjs +0 -6
- package/dist/lib/webhooks/index.mjs.map +1 -1
- package/dist/lib/wholesale-feed-sync/index.mjs +0 -6
- package/dist/lib/wholesale-feed-sync/index.mjs.map +1 -1
- package/dist/lib/wholesale-feed-sync/sync.mjs +0 -6
- package/dist/lib/wholesale-feed-sync/sync.mjs.map +1 -1
- package/dist/lib/wholesale-feed-sync/types.mjs +0 -6
- package/dist/lib/wholesale-feed-sync/webhook-notification.mjs +0 -6
- package/dist/lib/wholesale-feed-sync/webhook-notification.mjs.map +1 -1
- package/package.json +2 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/fetch.ts"],"sourcesContent":["import { signRequest, type SignerKey, type SignRequestOptions } from './signer';\n\n/** Callback form for `coverContentDigest` — lets the wrapper decide per call. */\nexport type CoverContentDigestPredicate = (url: string, init: RequestInit | undefined) => boolean;\n\nexport interface SigningFetchOptions extends Omit<SignRequestOptions, 'coverContentDigest'> {\n shouldSign?: (url: string, init: RequestInit | undefined) => boolean;\n /**\n * Whether to cover `content-digest`. May be a boolean (static) or a\n * predicate resolved at signing time against the current request — used by\n * the AdCP agent wrapper to honor the seller's `covers_content_digest`\n * policy (`required` / `forbidden` / `either`) per operation.\n */\n coverContentDigest?: boolean | CoverContentDigestPredicate;\n}\n\n/**\n * Minimal fetch shape that the signing wrappers compose around. Exported\n * so external call sites (`src/lib/protocols/a2a.ts:cancelA2ATask`) can\n * type their upstream wrapper without resorting to casts.\n */\nexport type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\n/**\n * Header names whose wire values are produced by the signer itself. Any\n * caller-supplied value gets stripped before signing so a misconfigured\n * custom-headers bag can't silently overwrite (or bypass) the RFC 9421\n * signature output.\n *\n * `authorization` is intentionally NOT in this set. AdCP's RFC 9421 profile\n * does not cover `Authorization` (see `MANDATORY_COMPONENTS` in\n * `src/lib/signing/types.ts`), so a caller-supplied bearer or RFC 7617 Basic\n * header passes through unmodified. If a future profile adds `authorization`\n * to `covered_components`, the canonicalizer must read the FINAL outgoing\n * value (post-`mergeHeaders` injection in `bin/adcp.js` for the CLI Basic\n * path) — `createSigningFetch` already does because it reads `init.headers`\n * at fetch time. Note RFC 9421 §7.5.7 warns about signing over long-lived\n * credentials (Basic doesn't rotate) — re-attacking the signed credential is\n * a real risk; a `covers_authorization` policy needs that in scope.\n */\nconst SIGNING_RESERVED_HEADERS = new Set(['signature', 'signature-input', 'content-digest']);\n\nexport function createSigningFetch(upstream: FetchLike, key: SignerKey, options: SigningFetchOptions = {}): FetchLike {\n return async (input, init) => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;\n const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();\n const shouldSign = options.shouldSign ?? (() => method !== 'GET');\n if (!shouldSign(url, init)) return upstream(input, init);\n\n if (input instanceof Request) {\n throw new TypeError(\n 'createSigningFetch does not accept Request objects (the body would be consumed out from under the signer). Pass a URL string and a separate init.'\n );\n }\n\n const headers = headersToRecord(init?.headers);\n for (const name of Object.keys(headers)) {\n if (SIGNING_RESERVED_HEADERS.has(name.toLowerCase())) {\n delete headers[name];\n }\n }\n const hasContentType = Object.keys(headers).some(k => k.toLowerCase() === 'content-type');\n if (!hasContentType && (init?.body !== undefined || method !== 'GET')) {\n headers['content-type'] = 'application/json';\n }\n const body = bodyToString(init?.body);\n\n const coverContentDigest =\n typeof options.coverContentDigest === 'function'\n ? options.coverContentDigest(url, init)\n : options.coverContentDigest;\n const { coverContentDigest: _omit, ...signerOptionsBase } = options;\n const signerOptions: SignRequestOptions = { ...signerOptionsBase, coverContentDigest };\n\n const signed = signRequest({ method, url, headers, body }, key, signerOptions);\n\n const mergedInit: RequestInit = { ...init, method, headers: signed.headers };\n if (body !== undefined && mergedInit.body === undefined) mergedInit.body = body;\n return upstream(url, mergedInit);\n };\n}\n\nfunction headersToRecord(headers: HeadersInit | Headers | undefined): Record<string, string> {\n if (!headers) return {};\n if (headers instanceof Headers) {\n const out: Record<string, string> = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n if (Array.isArray(headers)) {\n return Object.fromEntries(headers);\n }\n return { ...(headers as Record<string, string>) };\n}\n\nfunction bodyToString(body: RequestInit['body']): string | undefined {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return body;\n if (body instanceof Uint8Array) return decodeUtf8Strict(body);\n if (body instanceof ArrayBuffer) return decodeUtf8Strict(new Uint8Array(body));\n throw new TypeError(\n 'createSigningFetch requires a string, Uint8Array, or ArrayBuffer body. FormData / Blob / ReadableStream are not supported because the signature must cover the exact wire bytes.'\n );\n}\n\n/**\n * Decode bytes to UTF-8, throwing on invalid sequences. Permissive decode\n * (the `Buffer.toString('utf8')` default) replaces invalid bytes with\n * `U+FFFD`, which would silently corrupt binary or non-UTF-8 payloads — the\n * signer would commit to the lossy string and the wire would carry the same\n * lossy bytes, so verification still passes but the seller receives\n * mangled content. Throwing forces the caller to send a string body or\n * ensure their Uint8Array is valid UTF-8.\n */\nfunction decodeUtf8Strict(bytes: Uint8Array): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n } catch {\n throw new TypeError(\n 'createSigningFetch received a Uint8Array/ArrayBuffer body that is not valid UTF-8. ' +\n 'Pass a string body, ensure the bytes are UTF-8, or sign the request manually with `signRequest` against the exact wire bytes you intend to send.'\n );\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/fetch.ts"],"sourcesContent":["import { signRequest, type SignerKey, type SignRequestOptions } from './signer';\n\n/** Callback form for `coverContentDigest` — lets the wrapper decide per call. */\nexport type CoverContentDigestPredicate = (url: string, init: RequestInit | undefined) => boolean;\n\nexport interface SigningFetchOptions extends Omit<SignRequestOptions, 'coverContentDigest'> {\n shouldSign?: (url: string, init: RequestInit | undefined) => boolean;\n /**\n * Whether to cover `content-digest`. May be a boolean (static) or a\n * predicate resolved at signing time against the current request — used by\n * the AdCP agent wrapper to honor the seller's `covers_content_digest`\n * policy (`required` / `forbidden` / `either`) per operation.\n */\n coverContentDigest?: boolean | CoverContentDigestPredicate;\n}\n\n/**\n * Minimal fetch shape that the signing wrappers compose around. Exported\n * so external call sites (`src/lib/protocols/a2a.ts:cancelA2ATask`) can\n * type their upstream wrapper without resorting to casts.\n */\nexport type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;\n\n/**\n * Header names whose wire values are produced by the signer itself. Any\n * caller-supplied value gets stripped before signing so a misconfigured\n * custom-headers bag can't silently overwrite (or bypass) the RFC 9421\n * signature output.\n *\n * `authorization` is intentionally NOT in this set. AdCP's RFC 9421 profile\n * does not cover `Authorization` (see `MANDATORY_COMPONENTS` in\n * `src/lib/signing/types.ts`), so a caller-supplied bearer or RFC 7617 Basic\n * header passes through unmodified. If a future profile adds `authorization`\n * to `covered_components`, the canonicalizer must read the FINAL outgoing\n * value (post-`mergeHeaders` injection in `bin/adcp.js` for the CLI Basic\n * path) — `createSigningFetch` already does because it reads `init.headers`\n * at fetch time. Note RFC 9421 §7.5.7 warns about signing over long-lived\n * credentials (Basic doesn't rotate) — re-attacking the signed credential is\n * a real risk; a `covers_authorization` policy needs that in scope.\n */\nconst SIGNING_RESERVED_HEADERS = new Set(['signature', 'signature-input', 'content-digest']);\n\nexport function createSigningFetch(upstream: FetchLike, key: SignerKey, options: SigningFetchOptions = {}): FetchLike {\n return async (input, init) => {\n const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;\n const method = (init?.method ?? (input instanceof Request ? input.method : 'GET')).toUpperCase();\n const shouldSign = options.shouldSign ?? (() => method !== 'GET');\n if (!shouldSign(url, init)) return upstream(input, init);\n\n if (input instanceof Request) {\n throw new TypeError(\n 'createSigningFetch does not accept Request objects (the body would be consumed out from under the signer). Pass a URL string and a separate init.'\n );\n }\n\n const headers = headersToRecord(init?.headers);\n for (const name of Object.keys(headers)) {\n if (SIGNING_RESERVED_HEADERS.has(name.toLowerCase())) {\n delete headers[name];\n }\n }\n const hasContentType = Object.keys(headers).some(k => k.toLowerCase() === 'content-type');\n if (!hasContentType && (init?.body !== undefined || method !== 'GET')) {\n headers['content-type'] = 'application/json';\n }\n const body = bodyToString(init?.body);\n\n const coverContentDigest =\n typeof options.coverContentDigest === 'function'\n ? options.coverContentDigest(url, init)\n : options.coverContentDigest;\n const { coverContentDigest: _omit, ...signerOptionsBase } = options;\n const signerOptions: SignRequestOptions = { ...signerOptionsBase, coverContentDigest };\n\n const signed = signRequest({ method, url, headers, body }, key, signerOptions);\n\n const mergedInit: RequestInit = { ...init, method, headers: signed.headers };\n if (body !== undefined && mergedInit.body === undefined) mergedInit.body = body;\n return upstream(url, mergedInit);\n };\n}\n\nfunction headersToRecord(headers: HeadersInit | Headers | undefined): Record<string, string> {\n if (!headers) return {};\n if (headers instanceof Headers) {\n const out: Record<string, string> = {};\n headers.forEach((v, k) => {\n out[k] = v;\n });\n return out;\n }\n if (Array.isArray(headers)) {\n return Object.fromEntries(headers);\n }\n return { ...(headers as Record<string, string>) };\n}\n\nfunction bodyToString(body: RequestInit['body']): string | undefined {\n if (body === undefined || body === null) return undefined;\n if (typeof body === 'string') return body;\n if (body instanceof Uint8Array) return decodeUtf8Strict(body);\n if (body instanceof ArrayBuffer) return decodeUtf8Strict(new Uint8Array(body));\n throw new TypeError(\n 'createSigningFetch requires a string, Uint8Array, or ArrayBuffer body. FormData / Blob / ReadableStream are not supported because the signature must cover the exact wire bytes.'\n );\n}\n\n/**\n * Decode bytes to UTF-8, throwing on invalid sequences. Permissive decode\n * (the `Buffer.toString('utf8')` default) replaces invalid bytes with\n * `U+FFFD`, which would silently corrupt binary or non-UTF-8 payloads — the\n * signer would commit to the lossy string and the wire would carry the same\n * lossy bytes, so verification still passes but the seller receives\n * mangled content. Throwing forces the caller to send a string body or\n * ensure their Uint8Array is valid UTF-8.\n */\nfunction decodeUtf8Strict(bytes: Uint8Array): string {\n try {\n return new TextDecoder('utf-8', { fatal: true }).decode(bytes);\n } catch {\n throw new TypeError(\n 'createSigningFetch received a Uint8Array/ArrayBuffer body that is not valid UTF-8. ' +\n 'Pass a string body, ensure the bytes are UTF-8, or sign the request manually with `signRequest` against the exact wire bytes you intend to send.'\n );\n }\n}\n"],"mappings":"AAAA,SAAS,mBAA4D;AAwCrE,MAAM,2BAA2B,oBAAI,IAAI,CAAC,aAAa,mBAAmB,gBAAgB,CAAC;AAEpF,SAAS,mBAAmB,UAAqB,KAAgB,UAA+B,CAAC,GAAc;AACpH,SAAO,OAAO,OAAO,SAAS;AAC5B,UAAM,MAAM,OAAO,UAAU,WAAW,QAAQ,iBAAiB,MAAM,MAAM,SAAS,IAAI,MAAM;AAChG,UAAM,UAAU,MAAM,WAAW,iBAAiB,UAAU,MAAM,SAAS,QAAQ,YAAY;AAC/F,UAAM,aAAa,QAAQ,eAAe,MAAM,WAAW;AAC3D,QAAI,CAAC,WAAW,KAAK,IAAI,EAAG,QAAO,SAAS,OAAO,IAAI;AAEvD,QAAI,iBAAiB,SAAS;AAC5B,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AAEA,UAAM,UAAU,gBAAgB,MAAM,OAAO;AAC7C,eAAW,QAAQ,OAAO,KAAK,OAAO,GAAG;AACvC,UAAI,yBAAyB,IAAI,KAAK,YAAY,CAAC,GAAG;AACpD,eAAO,QAAQ,IAAI;AAAA,MACrB;AAAA,IACF;AACA,UAAM,iBAAiB,OAAO,KAAK,OAAO,EAAE,KAAK,OAAK,EAAE,YAAY,MAAM,cAAc;AACxF,QAAI,CAAC,mBAAmB,MAAM,SAAS,UAAa,WAAW,QAAQ;AACrE,cAAQ,cAAc,IAAI;AAAA,IAC5B;AACA,UAAM,OAAO,aAAa,MAAM,IAAI;AAEpC,UAAM,qBACJ,OAAO,QAAQ,uBAAuB,aAClC,QAAQ,mBAAmB,KAAK,IAAI,IACpC,QAAQ;AACd,UAAM,EAAE,oBAAoB,OAAO,GAAG,kBAAkB,IAAI;AAC5D,UAAM,gBAAoC,EAAE,GAAG,mBAAmB,mBAAmB;AAErF,UAAM,SAAS,YAAY,EAAE,QAAQ,KAAK,SAAS,KAAK,GAAG,KAAK,aAAa;AAE7E,UAAM,aAA0B,EAAE,GAAG,MAAM,QAAQ,SAAS,OAAO,QAAQ;AAC3E,QAAI,SAAS,UAAa,WAAW,SAAS,OAAW,YAAW,OAAO;AAC3E,WAAO,SAAS,KAAK,UAAU;AAAA,EACjC;AACF;AAEA,SAAS,gBAAgB,SAAoE;AAC3F,MAAI,CAAC,QAAS,QAAO,CAAC;AACtB,MAAI,mBAAmB,SAAS;AAC9B,UAAM,MAA8B,CAAC;AACrC,YAAQ,QAAQ,CAAC,GAAG,MAAM;AACxB,UAAI,CAAC,IAAI;AAAA,IACX,CAAC;AACD,WAAO;AAAA,EACT;AACA,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,WAAO,OAAO,YAAY,OAAO;AAAA,EACnC;AACA,SAAO,EAAE,GAAI,QAAmC;AAClD;AAEA,SAAS,aAAa,MAA+C;AACnE,MAAI,SAAS,UAAa,SAAS,KAAM,QAAO;AAChD,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,MAAI,gBAAgB,WAAY,QAAO,iBAAiB,IAAI;AAC5D,MAAI,gBAAgB,YAAa,QAAO,iBAAiB,IAAI,WAAW,IAAI,CAAC;AAC7E,QAAM,IAAI;AAAA,IACR;AAAA,EACF;AACF;AAWA,SAAS,iBAAiB,OAA2B;AACnD,MAAI;AACF,WAAO,IAAI,YAAY,SAAS,EAAE,OAAO,KAAK,CAAC,EAAE,OAAO,KAAK;AAAA,EAC/D,QAAQ;AACN,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACF;","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
export * from "./client.mjs";
|
|
8
2
|
export * from "./server.mjs";
|
|
9
3
|
//# sourceMappingURL=index.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/index.ts"],"sourcesContent":["/**\n * Aggregate signing barrel. Re-exports the `client` and `server` sub-barrels\n * so existing consumers of `@adcp/sdk/signing` keep working.\n *\n * New code should import from the narrower surface that matches its role:\n *\n * import { createSigningFetch } from '@adcp/sdk/signing/client';\n * import { createExpressVerifier } from '@adcp/sdk/signing/server';\n *\n * — coding agents reading a file cold only need to hold one half of the\n * taxonomy, not all 30+ symbols. This aggregate barrel remains the stable\n * entry point until a future major release drops it.\n */\nexport * from './client';\nexport * from './server';\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/index.ts"],"sourcesContent":["/**\n * Aggregate signing barrel. Re-exports the `client` and `server` sub-barrels\n * so existing consumers of `@adcp/sdk/signing` keep working.\n *\n * New code should import from the narrower surface that matches its role:\n *\n * import { createSigningFetch } from '@adcp/sdk/signing/client';\n * import { createExpressVerifier } from '@adcp/sdk/signing/server';\n *\n * — coding agents reading a file cold only need to hold one half of the\n * taxonomy, not all 30+ symbols. This aggregate barrel remains the stable\n * entry point until a future major release drops it.\n */\nexport * from './client';\nexport * from './server';\n"],"mappings":"AAaA,cAAc;AACd,cAAc;","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { createPublicKey } from "node:crypto";
|
|
8
2
|
const WIRE_ALG_TO_JOSE = {
|
|
9
3
|
ed25519: "EdDSA",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/jwks-helpers.ts"],"sourcesContent":["import { createPublicKey } from 'node:crypto';\nimport type { AdcpJsonWebKey, AdcpSignAlg } from './types';\n\n/**\n * Maps AdCP wire-format algorithm identifiers to JOSE `alg` names.\n *\n * These are distinct vocabularies used in different contexts:\n * - AdCP wire identifier (`'ed25519'`, `'ecdsa-p256-sha256'`) appears in\n * `Signature-Input` and `SigningProvider.algorithm`.\n * - JOSE alg name (`'EdDSA'`, `'ES256'`) appears in published JWKs so JOSE\n * consumers can locate the right verifier. The AdCP step-8 check asserts this\n * mapping; using an AdCP wire identifier in place of the JOSE name produces a\n * `request_signature_key_purpose_invalid` rejection before any crypto runs.\n */\nconst WIRE_ALG_TO_JOSE: Record<AdcpSignAlg, string> = {\n ed25519: 'EdDSA',\n 'ecdsa-p256-sha256': 'ES256',\n};\n\n/**\n * AdCP JWK purpose discriminator.\n *\n * `'webhook-signing'` is **deprecated** (pending removal — adcontextprotocol/adcp#5555): webhooks are\n * signed with a `'request-signing'` key, differentiated from request\n * signatures by the RFC 9421 `tag`. Verifiers still accept `'webhook-signing'`\n * on the webhook path for backward compatibility, but new signers SHOULD\n * publish and sign with `'request-signing'` keys only (use a second\n * `'request-signing'` key under a distinct `kid` when webhook key isolation\n * is desired).\n */\nexport type AdcpUse = 'request-signing' | 'webhook-signing' | 'response-signing' | 'governance-signing';\n\nconst ADCP_USE_VALUES = new Set<AdcpUse>([\n 'request-signing',\n 'webhook-signing',\n 'response-signing',\n 'governance-signing',\n]);\n\nexport function assertAdcpUse(value: unknown, helperName: string): asserts value is AdcpUse {\n if (typeof value !== 'string' || !ADCP_USE_VALUES.has(value as AdcpUse)) {\n throw new TypeError(\n `${helperName}: unsupported adcp_use '${String(value)}'. ` +\n `Supported: ${Array.from(ADCP_USE_VALUES).join(', ')}.`\n );\n }\n}\n\nexport interface PemToAdcpJwkOptions {\n /** `kid` to embed in the JWK — must match the value published in `Signature-Input`. */\n kid: string;\n /** AdCP wire-format algorithm. Controls which JOSE `alg` name is emitted. */\n algorithm: AdcpSignAlg;\n /**\n * Purpose binding, enforced by AdCP verifiers at step 8.\n * - `'request-signing'` — for JWKs published at the buyer's `jwks_uri`. Also\n * signs outbound webhook callbacks (differentiated by the RFC 9421 `tag`).\n * - `'webhook-signing'` — **deprecated** (pending removal — adcontextprotocol/adcp#5555); use\n * `'request-signing'` for webhooks. Still accepted by verifiers for\n * backward compatibility.\n * - `'response-signing'` — for compatibility with agents that sign JSON\n * transport responses directly.\n * - `'governance-signing'` — for JWKs used to sign governance context\n * (JWS-signed, not RFC 9421). Declared on JWKs published in a tenant's\n * aggregated JWKS so JSON-typed consumers (e.g., third-party verifiers\n * filtering by `adcp_use`) can identify governance-signing material;\n * this SDK does not yet ship a `signGovernanceContext` helper — the\n * verifier surface for governance JWS is deferred work tracked under\n * adcp-client#1844.\n */\n adcp_use: AdcpUse;\n}\n\n/**\n * Convert a public-key PEM (SPKI / `BEGIN PUBLIC KEY` format) to an AdCP JWK\n * with the correct fields for publication at `/.well-known/jwks.json`.\n *\n * The returned JWK uses the JOSE `alg` name (`\"EdDSA\"`, `\"ES256\"`), not the\n * AdCP wire identifier (`\"ed25519\"`, `\"ecdsa-p256-sha256\"`). Confusing the two\n * is a common footgun — they appear in different protocol layers and the AdCP\n * step-8 verifier asserts the JOSE name. Using a wire identifier in the JWK\n * causes a `request_signature_key_purpose_invalid` rejection before the\n * signature is verified.\n *\n * Fields set by this helper and why:\n * - `alg` — JOSE name; required for AdCP step-8 consistency check.\n * - `use` — `\"sig\"`; RFC 7517 §4.2 intent hint for JOSE consumers.\n * - `adcp_use` — purpose binding; enforced hard gate at AdCP verifier step 8.\n * - `key_ops` — `[\"verify\"]`; AdCP verifier checks for `\"verify\"`, not `\"sign\"`,\n * because the published JWK is the public half of the keypair.\n *\n * @throws TypeError when given a private-key PEM or an unparseable PEM.\n *\n * @example\n * ```ts\n * import { pemToAdcpJwk } from '@adcp/sdk/signing';\n *\n * const jwk = pemToAdcpJwk(fs.readFileSync('keys/addie-2026-04.pem', 'utf8'), {\n * kid: 'addie-2026-04',\n * algorithm: 'ed25519',\n * adcp_use: 'request-signing',\n * });\n * // Serve this object in your /.well-known/jwks.json `keys` array.\n * ```\n */\nexport function pemToAdcpJwk(pem: string, options: PemToAdcpJwkOptions): AdcpJsonWebKey {\n assertAdcpUse(options.adcp_use, 'pemToAdcpJwk');\n\n // Anchored to the BEGIN line so a public-key PEM that mentions \"PRIVATE\n // KEY\" in surrounding metadata or comments doesn't false-positive. RFC\n // 7468 mandates exact uppercase between dashes; matching all standard\n // private-key headers (`PRIVATE KEY` PKCS#8, `RSA/EC/OPENSSH/ENCRYPTED\n // PRIVATE KEY`).\n if (/-----BEGIN [^-]*PRIVATE KEY-----/.test(pem)) {\n throw new TypeError(\n 'pemToAdcpJwk received a private-key PEM. ' +\n 'Pass only a public-key PEM (SPKI format: \"BEGIN PUBLIC KEY\"). ' +\n 'Publishing a private key in JWKS is a credential leak.'\n );\n }\n\n let keyObj;\n try {\n keyObj = createPublicKey({ key: pem, format: 'pem' });\n } catch (err) {\n throw new TypeError(\n `pemToAdcpJwk: failed to parse PEM as a public key — ` +\n `${err instanceof Error ? err.message : String(err)}. ` +\n `Expected SPKI format (\"BEGIN PUBLIC KEY\").`\n );\n }\n\n const joseAlg = WIRE_ALG_TO_JOSE[options.algorithm];\n if (!joseAlg) {\n throw new TypeError(\n `pemToAdcpJwk: unsupported algorithm '${options.algorithm}'. ` +\n `Supported: ${Object.keys(WIRE_ALG_TO_JOSE).join(', ')}.`\n );\n }\n\n const exported = keyObj.export({ format: 'jwk' }) as Record<string, unknown>;\n\n return {\n ...exported,\n kid: options.kid,\n alg: joseAlg,\n use: 'sig',\n adcp_use: options.adcp_use,\n key_ops: ['verify'],\n } as AdcpJsonWebKey;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/jwks-helpers.ts"],"sourcesContent":["import { createPublicKey } from 'node:crypto';\nimport type { AdcpJsonWebKey, AdcpSignAlg } from './types';\n\n/**\n * Maps AdCP wire-format algorithm identifiers to JOSE `alg` names.\n *\n * These are distinct vocabularies used in different contexts:\n * - AdCP wire identifier (`'ed25519'`, `'ecdsa-p256-sha256'`) appears in\n * `Signature-Input` and `SigningProvider.algorithm`.\n * - JOSE alg name (`'EdDSA'`, `'ES256'`) appears in published JWKs so JOSE\n * consumers can locate the right verifier. The AdCP step-8 check asserts this\n * mapping; using an AdCP wire identifier in place of the JOSE name produces a\n * `request_signature_key_purpose_invalid` rejection before any crypto runs.\n */\nconst WIRE_ALG_TO_JOSE: Record<AdcpSignAlg, string> = {\n ed25519: 'EdDSA',\n 'ecdsa-p256-sha256': 'ES256',\n};\n\n/**\n * AdCP JWK purpose discriminator.\n *\n * `'webhook-signing'` is **deprecated** (pending removal — adcontextprotocol/adcp#5555): webhooks are\n * signed with a `'request-signing'` key, differentiated from request\n * signatures by the RFC 9421 `tag`. Verifiers still accept `'webhook-signing'`\n * on the webhook path for backward compatibility, but new signers SHOULD\n * publish and sign with `'request-signing'` keys only (use a second\n * `'request-signing'` key under a distinct `kid` when webhook key isolation\n * is desired).\n */\nexport type AdcpUse = 'request-signing' | 'webhook-signing' | 'response-signing' | 'governance-signing';\n\nconst ADCP_USE_VALUES = new Set<AdcpUse>([\n 'request-signing',\n 'webhook-signing',\n 'response-signing',\n 'governance-signing',\n]);\n\nexport function assertAdcpUse(value: unknown, helperName: string): asserts value is AdcpUse {\n if (typeof value !== 'string' || !ADCP_USE_VALUES.has(value as AdcpUse)) {\n throw new TypeError(\n `${helperName}: unsupported adcp_use '${String(value)}'. ` +\n `Supported: ${Array.from(ADCP_USE_VALUES).join(', ')}.`\n );\n }\n}\n\nexport interface PemToAdcpJwkOptions {\n /** `kid` to embed in the JWK — must match the value published in `Signature-Input`. */\n kid: string;\n /** AdCP wire-format algorithm. Controls which JOSE `alg` name is emitted. */\n algorithm: AdcpSignAlg;\n /**\n * Purpose binding, enforced by AdCP verifiers at step 8.\n * - `'request-signing'` — for JWKs published at the buyer's `jwks_uri`. Also\n * signs outbound webhook callbacks (differentiated by the RFC 9421 `tag`).\n * - `'webhook-signing'` — **deprecated** (pending removal — adcontextprotocol/adcp#5555); use\n * `'request-signing'` for webhooks. Still accepted by verifiers for\n * backward compatibility.\n * - `'response-signing'` — for compatibility with agents that sign JSON\n * transport responses directly.\n * - `'governance-signing'` — for JWKs used to sign governance context\n * (JWS-signed, not RFC 9421). Declared on JWKs published in a tenant's\n * aggregated JWKS so JSON-typed consumers (e.g., third-party verifiers\n * filtering by `adcp_use`) can identify governance-signing material;\n * this SDK does not yet ship a `signGovernanceContext` helper — the\n * verifier surface for governance JWS is deferred work tracked under\n * adcp-client#1844.\n */\n adcp_use: AdcpUse;\n}\n\n/**\n * Convert a public-key PEM (SPKI / `BEGIN PUBLIC KEY` format) to an AdCP JWK\n * with the correct fields for publication at `/.well-known/jwks.json`.\n *\n * The returned JWK uses the JOSE `alg` name (`\"EdDSA\"`, `\"ES256\"`), not the\n * AdCP wire identifier (`\"ed25519\"`, `\"ecdsa-p256-sha256\"`). Confusing the two\n * is a common footgun — they appear in different protocol layers and the AdCP\n * step-8 verifier asserts the JOSE name. Using a wire identifier in the JWK\n * causes a `request_signature_key_purpose_invalid` rejection before the\n * signature is verified.\n *\n * Fields set by this helper and why:\n * - `alg` — JOSE name; required for AdCP step-8 consistency check.\n * - `use` — `\"sig\"`; RFC 7517 §4.2 intent hint for JOSE consumers.\n * - `adcp_use` — purpose binding; enforced hard gate at AdCP verifier step 8.\n * - `key_ops` — `[\"verify\"]`; AdCP verifier checks for `\"verify\"`, not `\"sign\"`,\n * because the published JWK is the public half of the keypair.\n *\n * @throws TypeError when given a private-key PEM or an unparseable PEM.\n *\n * @example\n * ```ts\n * import { pemToAdcpJwk } from '@adcp/sdk/signing';\n *\n * const jwk = pemToAdcpJwk(fs.readFileSync('keys/addie-2026-04.pem', 'utf8'), {\n * kid: 'addie-2026-04',\n * algorithm: 'ed25519',\n * adcp_use: 'request-signing',\n * });\n * // Serve this object in your /.well-known/jwks.json `keys` array.\n * ```\n */\nexport function pemToAdcpJwk(pem: string, options: PemToAdcpJwkOptions): AdcpJsonWebKey {\n assertAdcpUse(options.adcp_use, 'pemToAdcpJwk');\n\n // Anchored to the BEGIN line so a public-key PEM that mentions \"PRIVATE\n // KEY\" in surrounding metadata or comments doesn't false-positive. RFC\n // 7468 mandates exact uppercase between dashes; matching all standard\n // private-key headers (`PRIVATE KEY` PKCS#8, `RSA/EC/OPENSSH/ENCRYPTED\n // PRIVATE KEY`).\n if (/-----BEGIN [^-]*PRIVATE KEY-----/.test(pem)) {\n throw new TypeError(\n 'pemToAdcpJwk received a private-key PEM. ' +\n 'Pass only a public-key PEM (SPKI format: \"BEGIN PUBLIC KEY\"). ' +\n 'Publishing a private key in JWKS is a credential leak.'\n );\n }\n\n let keyObj;\n try {\n keyObj = createPublicKey({ key: pem, format: 'pem' });\n } catch (err) {\n throw new TypeError(\n `pemToAdcpJwk: failed to parse PEM as a public key — ` +\n `${err instanceof Error ? err.message : String(err)}. ` +\n `Expected SPKI format (\"BEGIN PUBLIC KEY\").`\n );\n }\n\n const joseAlg = WIRE_ALG_TO_JOSE[options.algorithm];\n if (!joseAlg) {\n throw new TypeError(\n `pemToAdcpJwk: unsupported algorithm '${options.algorithm}'. ` +\n `Supported: ${Object.keys(WIRE_ALG_TO_JOSE).join(', ')}.`\n );\n }\n\n const exported = keyObj.export({ format: 'jwk' }) as Record<string, unknown>;\n\n return {\n ...exported,\n kid: options.kid,\n alg: joseAlg,\n use: 'sig',\n adcp_use: options.adcp_use,\n key_ops: ['verify'],\n } as AdcpJsonWebKey;\n}\n"],"mappings":"AAAA,SAAS,uBAAuB;AAchC,MAAM,mBAAgD;AAAA,EACpD,SAAS;AAAA,EACT,qBAAqB;AACvB;AAeA,MAAM,kBAAkB,oBAAI,IAAa;AAAA,EACvC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAEM,SAAS,cAAc,OAAgB,YAA8C;AAC1F,MAAI,OAAO,UAAU,YAAY,CAAC,gBAAgB,IAAI,KAAgB,GAAG;AACvE,UAAM,IAAI;AAAA,MACR,GAAG,UAAU,2BAA2B,OAAO,KAAK,CAAC,iBACrC,MAAM,KAAK,eAAe,EAAE,KAAK,IAAI,CAAC;AAAA,IACxD;AAAA,EACF;AACF;AA2DO,SAAS,aAAa,KAAa,SAA8C;AACtF,gBAAc,QAAQ,UAAU,cAAc;AAO9C,MAAI,mCAAmC,KAAK,GAAG,GAAG;AAChD,UAAM,IAAI;AAAA,MACR;AAAA,IAGF;AAAA,EACF;AAEA,MAAI;AACJ,MAAI;AACF,aAAS,gBAAgB,EAAE,KAAK,KAAK,QAAQ,MAAM,CAAC;AAAA,EACtD,SAAS,KAAK;AACZ,UAAM,IAAI;AAAA,MACR,4DACK,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAAA,IAEvD;AAAA,EACF;AAEA,QAAM,UAAU,iBAAiB,QAAQ,SAAS;AAClD,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI;AAAA,MACR,wCAAwC,QAAQ,SAAS,iBACzC,OAAO,KAAK,gBAAgB,EAAE,KAAK,IAAI,CAAC;AAAA,IAC1D;AAAA,EACF;AAEA,QAAM,WAAW,OAAO,OAAO,EAAE,QAAQ,MAAM,CAAC;AAEhD,SAAO;AAAA,IACL,GAAG;AAAA,IACH,KAAK,QAAQ;AAAA,IACb,KAAK;AAAA,IACL,KAAK;AAAA,IACL,UAAU,QAAQ;AAAA,IAClB,SAAS,CAAC,QAAQ;AAAA,EACpB;AACF;","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { ssrfSafeFetch } from "../net/index.mjs";
|
|
8
2
|
const DEFAULT_MIN_COOLDOWN_SECONDS = 30;
|
|
9
3
|
const DEFAULT_MAX_AGE_SECONDS = 3600;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/jwks-https.ts"],"sourcesContent":["import { ssrfSafeFetch } from '../net';\nimport type { AdcpJsonWebKey } from './types';\nimport type { JwksResolver } from './jwks';\n\nexport interface HttpsJwksResolverOptions {\n /**\n * Minimum seconds between refetches, regardless of cache-control or\n * key-unknown triggers. Guards counterparties from being hammered when a\n * mis-configured or rotating keyid drives repeated refresh attempts.\n * Default 30s (AdCP spec floor).\n */\n minCooldownSeconds?: number;\n /**\n * Absolute cap on how long a cached snapshot may be used before a refetch\n * is attempted, even if the counterparty's Cache-Control would allow\n * longer. Default 3600s (1 hour).\n */\n maxAgeSeconds?: number;\n /** Allow `http://` / private-IP JWKS URLs (dev loops only). Default false. */\n allowPrivateIp?: boolean;\n /** Clock override for deterministic tests. Returns epoch seconds. */\n now?: () => number;\n}\n\ninterface CacheSnapshot {\n keys: Map<string, AdcpJsonWebKey>;\n etag?: string;\n fetchedAt: number;\n /** Epoch seconds at which the cache is considered expired (from Cache-Control max-age, capped by maxAgeSeconds). */\n expiresAt: number;\n}\n\nconst DEFAULT_MIN_COOLDOWN_SECONDS = 30;\nconst DEFAULT_MAX_AGE_SECONDS = 3600;\n\n/**\n * JWKS resolver that fetches and caches a JSON Web Key Set from an HTTPS URL.\n *\n * Behavior:\n * - Lazy first-fetch on the first `resolve()` call.\n * - Caches keyed by `kid`. A resolve() for an unknown kid triggers a\n * lazy refetch (honoring the minimum cooldown), so a counterparty that\n * rotates keys mid-session is picked up without a process restart.\n * - Honors `ETag` (sends `If-None-Match` on refetch) and `Cache-Control:\n * max-age=N` (uses whichever expires sooner: the server's max-age or\n * `maxAgeSeconds`).\n * - Keeps stale entries on transient fetch failures — verification goes\n * down only when we have no snapshot at all.\n * - Runs through the SSRF-safe fetch primitive: an attacker-supplied JWKS\n * URL can't resolve to IMDS or the server's private network.\n *\n * Not a ReplayStore-style singleton: construct one per counterparty URL (or\n * keyed by `iss`) and pass into `verifyRequestSignature.jwks`.\n */\nexport class HttpsJwksResolver implements JwksResolver {\n private readonly url: string;\n private readonly minCooldown: number;\n private readonly maxAge: number;\n private readonly allowPrivateIp: boolean;\n private readonly now: () => number;\n private cache: CacheSnapshot | undefined;\n private inFlight: Promise<void> | undefined;\n\n constructor(url: string, options: HttpsJwksResolverOptions = {}) {\n this.url = url;\n this.minCooldown = options.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS;\n this.maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;\n this.allowPrivateIp = options.allowPrivateIp ?? false;\n this.now = options.now ?? (() => Math.floor(Date.now() / 1000));\n }\n\n async resolve(keyid: string): Promise<AdcpJsonWebKey | null> {\n if (!this.cache) {\n await this.refresh();\n } else if (this.cache.keys.has(keyid)) {\n // Fast path — known kid in a non-expired snapshot.\n if (this.now() <= this.cache.expiresAt) {\n return this.cache.keys.get(keyid) ?? null;\n }\n // Cache past its expiry; refresh if cooldown elapsed. When the cooldown\n // hasn't elapsed we deliberately serve the expired key: the `minCooldown`\n // protects the counterparty's JWKS endpoint from being hammered, and the\n // spec's 30-second floor is also the maximum staleness a verifier may\n // tolerate in this path.\n if (this.now() - this.cache.fetchedAt >= this.minCooldown) {\n await this.refresh().catch(() => {\n /* keep stale on transient failure */\n });\n }\n } else if (this.now() - this.cache.fetchedAt >= this.minCooldown) {\n // Unknown kid and cooldown elapsed — counterparty may have rotated.\n await this.refresh().catch(() => {\n /* keep stale on transient failure */\n });\n }\n return this.cache?.keys.get(keyid) ?? null;\n }\n\n /**\n * Force a refetch, bypassing the cooldown. Primarily for tests and for\n * operator-triggered rotation flushes; normal callers should rely on the\n * lazy key-unknown refresh path.\n */\n async forceRefresh(): Promise<void> {\n this.cache = undefined;\n await this.refresh();\n }\n\n private async refresh(): Promise<void> {\n if (this.inFlight) {\n await this.inFlight;\n return;\n }\n this.inFlight = this.doRefresh().finally(() => {\n this.inFlight = undefined;\n });\n await this.inFlight;\n }\n\n private async doRefresh(): Promise<void> {\n const headers: Record<string, string> = {\n accept: 'application/jwk-set+json, application/json',\n };\n if (this.cache?.etag) headers['if-none-match'] = this.cache.etag;\n\n // SSRF refusals and transport errors both propagate here; the caller path\n // in `resolve()` decides whether to swallow them (keep stale) or surface\n // them (first-fetch).\n const res = await ssrfSafeFetch(this.url, {\n method: 'GET',\n headers,\n allowPrivateIp: this.allowPrivateIp,\n });\n\n if (res.status === 304 && this.cache) {\n // Not modified — extend cache lifetime using the new response's\n // cache-control (or fall back to maxAgeSeconds). RFC 7232 permits the\n // origin to emit a new, stronger ETag on a 304; adopt it so our next\n // If-None-Match matches what the origin is currently willing to\n // validate against.\n this.cache = {\n ...this.cache,\n etag: res.headers['etag'] ?? this.cache.etag,\n fetchedAt: this.now(),\n expiresAt: this.computeExpiry(res.headers),\n };\n return;\n }\n\n if (res.status !== 200) {\n throw new Error(`JWKS fetch ${this.url} returned HTTP ${res.status}`);\n }\n\n const text = Buffer.from(res.body).toString('utf8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new Error(`JWKS fetch ${this.url} returned non-JSON body`);\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`JWKS fetch ${this.url} returned a non-object body`);\n }\n const keysField = (parsed as { keys?: unknown }).keys;\n if (!Array.isArray(keysField)) {\n throw new Error(`JWKS fetch ${this.url} response missing \\`keys\\` array`);\n }\n\n const byKid = new Map<string, AdcpJsonWebKey>();\n for (const entry of keysField) {\n if (entry && typeof entry === 'object' && typeof (entry as AdcpJsonWebKey).kid === 'string') {\n const jwk = entry as AdcpJsonWebKey;\n byKid.set(jwk.kid, jwk);\n }\n }\n\n this.cache = {\n keys: byKid,\n etag: res.headers['etag'],\n fetchedAt: this.now(),\n expiresAt: this.computeExpiry(res.headers),\n };\n }\n\n private computeExpiry(headers: Record<string, string>): number {\n const cacheCtl = headers['cache-control']?.toLowerCase() ?? '';\n // `no-store` / `no-cache` → treat as \"expires immediately\"; the next\n // resolve() will refresh past the cooldown, which is the spec-mandated\n // floor (30s) for not hammering the counterparty.\n if (/\\bno-store\\b|\\bno-cache\\b/.test(cacheCtl)) return this.now();\n const match = /max-age\\s*=\\s*(\\d+)/.exec(cacheCtl);\n if (match) {\n const serverMax = Number(match[1]);\n if (Number.isFinite(serverMax)) return this.now() + Math.min(serverMax, this.maxAge);\n }\n return this.now() + this.maxAge;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/jwks-https.ts"],"sourcesContent":["import { ssrfSafeFetch } from '../net';\nimport type { AdcpJsonWebKey } from './types';\nimport type { JwksResolver } from './jwks';\n\nexport interface HttpsJwksResolverOptions {\n /**\n * Minimum seconds between refetches, regardless of cache-control or\n * key-unknown triggers. Guards counterparties from being hammered when a\n * mis-configured or rotating keyid drives repeated refresh attempts.\n * Default 30s (AdCP spec floor).\n */\n minCooldownSeconds?: number;\n /**\n * Absolute cap on how long a cached snapshot may be used before a refetch\n * is attempted, even if the counterparty's Cache-Control would allow\n * longer. Default 3600s (1 hour).\n */\n maxAgeSeconds?: number;\n /** Allow `http://` / private-IP JWKS URLs (dev loops only). Default false. */\n allowPrivateIp?: boolean;\n /** Clock override for deterministic tests. Returns epoch seconds. */\n now?: () => number;\n}\n\ninterface CacheSnapshot {\n keys: Map<string, AdcpJsonWebKey>;\n etag?: string;\n fetchedAt: number;\n /** Epoch seconds at which the cache is considered expired (from Cache-Control max-age, capped by maxAgeSeconds). */\n expiresAt: number;\n}\n\nconst DEFAULT_MIN_COOLDOWN_SECONDS = 30;\nconst DEFAULT_MAX_AGE_SECONDS = 3600;\n\n/**\n * JWKS resolver that fetches and caches a JSON Web Key Set from an HTTPS URL.\n *\n * Behavior:\n * - Lazy first-fetch on the first `resolve()` call.\n * - Caches keyed by `kid`. A resolve() for an unknown kid triggers a\n * lazy refetch (honoring the minimum cooldown), so a counterparty that\n * rotates keys mid-session is picked up without a process restart.\n * - Honors `ETag` (sends `If-None-Match` on refetch) and `Cache-Control:\n * max-age=N` (uses whichever expires sooner: the server's max-age or\n * `maxAgeSeconds`).\n * - Keeps stale entries on transient fetch failures — verification goes\n * down only when we have no snapshot at all.\n * - Runs through the SSRF-safe fetch primitive: an attacker-supplied JWKS\n * URL can't resolve to IMDS or the server's private network.\n *\n * Not a ReplayStore-style singleton: construct one per counterparty URL (or\n * keyed by `iss`) and pass into `verifyRequestSignature.jwks`.\n */\nexport class HttpsJwksResolver implements JwksResolver {\n private readonly url: string;\n private readonly minCooldown: number;\n private readonly maxAge: number;\n private readonly allowPrivateIp: boolean;\n private readonly now: () => number;\n private cache: CacheSnapshot | undefined;\n private inFlight: Promise<void> | undefined;\n\n constructor(url: string, options: HttpsJwksResolverOptions = {}) {\n this.url = url;\n this.minCooldown = options.minCooldownSeconds ?? DEFAULT_MIN_COOLDOWN_SECONDS;\n this.maxAge = options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS;\n this.allowPrivateIp = options.allowPrivateIp ?? false;\n this.now = options.now ?? (() => Math.floor(Date.now() / 1000));\n }\n\n async resolve(keyid: string): Promise<AdcpJsonWebKey | null> {\n if (!this.cache) {\n await this.refresh();\n } else if (this.cache.keys.has(keyid)) {\n // Fast path — known kid in a non-expired snapshot.\n if (this.now() <= this.cache.expiresAt) {\n return this.cache.keys.get(keyid) ?? null;\n }\n // Cache past its expiry; refresh if cooldown elapsed. When the cooldown\n // hasn't elapsed we deliberately serve the expired key: the `minCooldown`\n // protects the counterparty's JWKS endpoint from being hammered, and the\n // spec's 30-second floor is also the maximum staleness a verifier may\n // tolerate in this path.\n if (this.now() - this.cache.fetchedAt >= this.minCooldown) {\n await this.refresh().catch(() => {\n /* keep stale on transient failure */\n });\n }\n } else if (this.now() - this.cache.fetchedAt >= this.minCooldown) {\n // Unknown kid and cooldown elapsed — counterparty may have rotated.\n await this.refresh().catch(() => {\n /* keep stale on transient failure */\n });\n }\n return this.cache?.keys.get(keyid) ?? null;\n }\n\n /**\n * Force a refetch, bypassing the cooldown. Primarily for tests and for\n * operator-triggered rotation flushes; normal callers should rely on the\n * lazy key-unknown refresh path.\n */\n async forceRefresh(): Promise<void> {\n this.cache = undefined;\n await this.refresh();\n }\n\n private async refresh(): Promise<void> {\n if (this.inFlight) {\n await this.inFlight;\n return;\n }\n this.inFlight = this.doRefresh().finally(() => {\n this.inFlight = undefined;\n });\n await this.inFlight;\n }\n\n private async doRefresh(): Promise<void> {\n const headers: Record<string, string> = {\n accept: 'application/jwk-set+json, application/json',\n };\n if (this.cache?.etag) headers['if-none-match'] = this.cache.etag;\n\n // SSRF refusals and transport errors both propagate here; the caller path\n // in `resolve()` decides whether to swallow them (keep stale) or surface\n // them (first-fetch).\n const res = await ssrfSafeFetch(this.url, {\n method: 'GET',\n headers,\n allowPrivateIp: this.allowPrivateIp,\n });\n\n if (res.status === 304 && this.cache) {\n // Not modified — extend cache lifetime using the new response's\n // cache-control (or fall back to maxAgeSeconds). RFC 7232 permits the\n // origin to emit a new, stronger ETag on a 304; adopt it so our next\n // If-None-Match matches what the origin is currently willing to\n // validate against.\n this.cache = {\n ...this.cache,\n etag: res.headers['etag'] ?? this.cache.etag,\n fetchedAt: this.now(),\n expiresAt: this.computeExpiry(res.headers),\n };\n return;\n }\n\n if (res.status !== 200) {\n throw new Error(`JWKS fetch ${this.url} returned HTTP ${res.status}`);\n }\n\n const text = Buffer.from(res.body).toString('utf8');\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n throw new Error(`JWKS fetch ${this.url} returned non-JSON body`);\n }\n if (!parsed || typeof parsed !== 'object') {\n throw new Error(`JWKS fetch ${this.url} returned a non-object body`);\n }\n const keysField = (parsed as { keys?: unknown }).keys;\n if (!Array.isArray(keysField)) {\n throw new Error(`JWKS fetch ${this.url} response missing \\`keys\\` array`);\n }\n\n const byKid = new Map<string, AdcpJsonWebKey>();\n for (const entry of keysField) {\n if (entry && typeof entry === 'object' && typeof (entry as AdcpJsonWebKey).kid === 'string') {\n const jwk = entry as AdcpJsonWebKey;\n byKid.set(jwk.kid, jwk);\n }\n }\n\n this.cache = {\n keys: byKid,\n etag: res.headers['etag'],\n fetchedAt: this.now(),\n expiresAt: this.computeExpiry(res.headers),\n };\n }\n\n private computeExpiry(headers: Record<string, string>): number {\n const cacheCtl = headers['cache-control']?.toLowerCase() ?? '';\n // `no-store` / `no-cache` → treat as \"expires immediately\"; the next\n // resolve() will refresh past the cooldown, which is the spec-mandated\n // floor (30s) for not hammering the counterparty.\n if (/\\bno-store\\b|\\bno-cache\\b/.test(cacheCtl)) return this.now();\n const match = /max-age\\s*=\\s*(\\d+)/.exec(cacheCtl);\n if (match) {\n const serverMax = Number(match[1]);\n if (Number.isFinite(serverMax)) return this.now() + Math.min(serverMax, this.maxAge);\n }\n return this.now() + this.maxAge;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
class StaticJwksResolver {
|
|
8
2
|
byKid = /* @__PURE__ */ new Map();
|
|
9
3
|
constructor(keys) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/jwks.ts"],"sourcesContent":["import type { AdcpJsonWebKey } from './types';\n\nexport interface JwksResolver {\n resolve(keyid: string): Promise<AdcpJsonWebKey | null>;\n}\n\nexport class StaticJwksResolver implements JwksResolver {\n private readonly byKid = new Map<string, AdcpJsonWebKey>();\n\n constructor(keys: AdcpJsonWebKey[]) {\n for (const k of keys) this.byKid.set(k.kid, k);\n }\n\n async resolve(keyid: string): Promise<AdcpJsonWebKey | null> {\n return this.byKid.get(keyid) ?? null;\n }\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/jwks.ts"],"sourcesContent":["import type { AdcpJsonWebKey } from './types';\n\nexport interface JwksResolver {\n resolve(keyid: string): Promise<AdcpJsonWebKey | null>;\n}\n\nexport class StaticJwksResolver implements JwksResolver {\n private readonly byKid = new Map<string, AdcpJsonWebKey>();\n\n constructor(keys: AdcpJsonWebKey[]) {\n for (const k of keys) this.byKid.set(k.kid, k);\n }\n\n async resolve(keyid: string): Promise<AdcpJsonWebKey | null> {\n return this.byKid.get(keyid) ?? null;\n }\n}\n"],"mappings":"AAMO,MAAM,mBAA2C;AAAA,EACrC,QAAQ,oBAAI,IAA4B;AAAA,EAEzD,YAAY,MAAwB;AAClC,eAAW,KAAK,KAAM,MAAK,MAAM,IAAI,EAAE,KAAK,CAAC;AAAA,EAC/C;AAAA,EAEA,MAAM,QAAQ,OAA+C;AAC3D,WAAO,KAAK,MAAM,IAAI,KAAK,KAAK;AAAA,EAClC;AACF;","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { getHeaderValue } from "./canonicalize.mjs";
|
|
8
2
|
import { RequestSignatureError } from "./errors.mjs";
|
|
9
3
|
import { InMemoryReplayStore } from "./replay.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/middleware.ts"],"sourcesContent":["import type { RequestLike } from './canonicalize';\nimport { getHeaderValue } from './canonicalize';\nimport { RequestSignatureError } from './errors';\nimport { InMemoryReplayStore } from './replay';\nimport { InMemoryRevocationStore } from './revocation';\nimport { verifyRequestSignature, type VerifyRequestOptions } from './verifier';\nimport type { VerifiedSigner } from './types';\n\ndeclare module 'http' {\n interface IncomingMessage {\n /**\n * Populated by {@link createExpressVerifier} iff a real signature was\n * verified. Unsigned-but-acceptable requests leave this `undefined` so\n * downstream handlers can read `req.verifiedSigner !== undefined` as\n * \"signed and verified.\"\n */\n verifiedSigner?: VerifiedSigner;\n rawBody?: string;\n }\n}\n\nexport interface ExpressLike {\n method: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n rawBody?: string;\n body?: unknown;\n protocol?: string;\n get?(header: string): string | undefined;\n [key: string]: unknown;\n}\n\nexport interface ExpressMiddlewareOptions extends Omit<\n VerifyRequestOptions,\n 'operation' | 'replayStore' | 'revocationStore'\n> {\n /**\n * Stores `(keyid, signature-bytes, expires)` tuples for replay detection.\n * Defaults to a fresh {@link InMemoryReplayStore} — suitable for single-process\n * deployments only. **Multi-replica deployments MUST pass an explicit shared\n * store** (Redis, Postgres, etc.) — the default in-memory store does not\n * survive process boundaries, so a signature accepted on replica A is\n * invisible to replica B and can be replayed there within the signature window.\n */\n replayStore?: VerifyRequestOptions['replayStore'];\n /**\n * Consulted for revoked `kid` / `jti` before accepting a signature.\n * Defaults to a fresh {@link InMemoryRevocationStore}, which starts empty\n * and does not poll for updates. The default is sufficient when you don't\n * revoke keys at runtime; when you do, pass a store backed by your secrets\n * manager or admin tooling so revocations take effect without redeployment.\n */\n revocationStore?: VerifyRequestOptions['revocationStore'];\n /**\n * Extract the AdCP operation name from the incoming request so the verifier\n * can consult `capability.required_for`. Return `undefined` for requests\n * that don't map to an AdCP operation (health checks, discovery probes) —\n * the verifier will then treat the request as \"not in any required_for\"\n * and accept unsigned traffic rather than rejecting.\n *\n * SECURITY: a `resolveOperation` that always returns `undefined` — for\n * example, a routing helper that silently fails to match — disables\n * `required_for` enforcement globally. Unsigned requests on signed-only\n * operations will pass. Verify the implementation actually resolves the\n * operation name for every AdCP route you care to protect; a unit test\n * asserting `resolveOperation(req)` is non-undefined for sample signed\n * requests is the simplest guard.\n */\n resolveOperation: (req: ExpressLike) => string | undefined;\n /**\n * Override how the request's full URL is reconstructed. Use when the server\n * sits behind a TLS-terminating or path-rewriting load balancer, since\n * `req.protocol`/`req.get('host')` may not reflect what the signer signed.\n * Must return the exact URL the signer used (scheme + authority + path + query).\n */\n getUrl?: (req: ExpressLike) => string;\n}\n\ntype NextFn = (err?: unknown) => void;\n\nexport function createExpressVerifier(options: ExpressMiddlewareOptions) {\n // Instantiate defaults once at wire-up so every request on this middleware\n // shares the same replay/revocation state — lazy per-request construction\n // would defeat replay detection entirely.\n const replayStore = options.replayStore ?? new InMemoryReplayStore();\n const revocationStore = options.revocationStore ?? new InMemoryRevocationStore();\n\n return async function requestSignatureMiddleware(\n req: ExpressLike,\n res: { status: (code: number) => { set: (k: string, v: string) => { json: (body: unknown) => void } } },\n next: NextFn\n ): Promise<void> {\n try {\n const url = options.getUrl ? options.getUrl(req) : defaultUrl(req);\n const body = resolveRawBody(req);\n const requestLike: RequestLike = {\n method: req.method,\n url,\n headers: req.headers,\n body,\n };\n const result = await verifyRequestSignature(requestLike, {\n ...options,\n replayStore,\n revocationStore,\n operation: options.resolveOperation(req),\n });\n if (result.status === 'verified') {\n req.verifiedSigner = {\n keyid: result.keyid,\n agent_url: result.agent_url,\n verified_at: result.verified_at,\n };\n }\n next();\n } catch (err) {\n if (err instanceof RequestSignatureError) {\n // `failed_step` is informational per spec; keep it in server-side logs\n // rather than the 401 body so anonymous callers can't enumerate the\n // verifier pipeline. Do NOT call next() here — Express convention is\n // that a terminal response ends the middleware chain without\n // invoking downstream handlers. Promise-based wrappers that need to\n // await completion should race against the response's 'close' event\n // rather than the next-callback.\n res.status(401).set('WWW-Authenticate', `Signature error=\"${err.code}\"`).json({\n error: err.code,\n message: 'Request signature verification failed',\n });\n return;\n }\n next(err);\n }\n };\n}\n\nfunction resolveRawBody(req: ExpressLike): string {\n if (typeof req.rawBody === 'string') return req.rawBody;\n const contentLengthHeader = getHeaderValue(req.headers, 'content-length');\n const contentLength = contentLengthHeader ? Number(contentLengthHeader) : 0;\n if (Number.isFinite(contentLength) && contentLength > 0) {\n throw new RequestSignatureError(\n 'request_signature_header_malformed',\n 1,\n 'req.rawBody is required for signed requests with a body; install a raw-body capture middleware ahead of createExpressVerifier'\n );\n }\n return '';\n}\n\nfunction defaultUrl(req: ExpressLike): string {\n const proto = req.protocol ?? (typeof req.get === 'function' ? req.get('x-forwarded-proto') : undefined) ?? 'https';\n const host = typeof req.get === 'function' ? req.get('host') : undefined;\n if (!host) {\n throw new Error(\n 'Unable to derive request URL: missing Host header. Pass `getUrl` on createExpressVerifier to supply the canonical URL explicitly.'\n );\n }\n return `${proto}://${host}${req.originalUrl ?? req.url}`;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/middleware.ts"],"sourcesContent":["import type { RequestLike } from './canonicalize';\nimport { getHeaderValue } from './canonicalize';\nimport { RequestSignatureError } from './errors';\nimport { InMemoryReplayStore } from './replay';\nimport { InMemoryRevocationStore } from './revocation';\nimport { verifyRequestSignature, type VerifyRequestOptions } from './verifier';\nimport type { VerifiedSigner } from './types';\n\ndeclare module 'http' {\n interface IncomingMessage {\n /**\n * Populated by {@link createExpressVerifier} iff a real signature was\n * verified. Unsigned-but-acceptable requests leave this `undefined` so\n * downstream handlers can read `req.verifiedSigner !== undefined` as\n * \"signed and verified.\"\n */\n verifiedSigner?: VerifiedSigner;\n rawBody?: string;\n }\n}\n\nexport interface ExpressLike {\n method: string;\n originalUrl?: string;\n url: string;\n headers: Record<string, string | string[] | undefined>;\n rawBody?: string;\n body?: unknown;\n protocol?: string;\n get?(header: string): string | undefined;\n [key: string]: unknown;\n}\n\nexport interface ExpressMiddlewareOptions extends Omit<\n VerifyRequestOptions,\n 'operation' | 'replayStore' | 'revocationStore'\n> {\n /**\n * Stores `(keyid, signature-bytes, expires)` tuples for replay detection.\n * Defaults to a fresh {@link InMemoryReplayStore} — suitable for single-process\n * deployments only. **Multi-replica deployments MUST pass an explicit shared\n * store** (Redis, Postgres, etc.) — the default in-memory store does not\n * survive process boundaries, so a signature accepted on replica A is\n * invisible to replica B and can be replayed there within the signature window.\n */\n replayStore?: VerifyRequestOptions['replayStore'];\n /**\n * Consulted for revoked `kid` / `jti` before accepting a signature.\n * Defaults to a fresh {@link InMemoryRevocationStore}, which starts empty\n * and does not poll for updates. The default is sufficient when you don't\n * revoke keys at runtime; when you do, pass a store backed by your secrets\n * manager or admin tooling so revocations take effect without redeployment.\n */\n revocationStore?: VerifyRequestOptions['revocationStore'];\n /**\n * Extract the AdCP operation name from the incoming request so the verifier\n * can consult `capability.required_for`. Return `undefined` for requests\n * that don't map to an AdCP operation (health checks, discovery probes) —\n * the verifier will then treat the request as \"not in any required_for\"\n * and accept unsigned traffic rather than rejecting.\n *\n * SECURITY: a `resolveOperation` that always returns `undefined` — for\n * example, a routing helper that silently fails to match — disables\n * `required_for` enforcement globally. Unsigned requests on signed-only\n * operations will pass. Verify the implementation actually resolves the\n * operation name for every AdCP route you care to protect; a unit test\n * asserting `resolveOperation(req)` is non-undefined for sample signed\n * requests is the simplest guard.\n */\n resolveOperation: (req: ExpressLike) => string | undefined;\n /**\n * Override how the request's full URL is reconstructed. Use when the server\n * sits behind a TLS-terminating or path-rewriting load balancer, since\n * `req.protocol`/`req.get('host')` may not reflect what the signer signed.\n * Must return the exact URL the signer used (scheme + authority + path + query).\n */\n getUrl?: (req: ExpressLike) => string;\n}\n\ntype NextFn = (err?: unknown) => void;\n\nexport function createExpressVerifier(options: ExpressMiddlewareOptions) {\n // Instantiate defaults once at wire-up so every request on this middleware\n // shares the same replay/revocation state — lazy per-request construction\n // would defeat replay detection entirely.\n const replayStore = options.replayStore ?? new InMemoryReplayStore();\n const revocationStore = options.revocationStore ?? new InMemoryRevocationStore();\n\n return async function requestSignatureMiddleware(\n req: ExpressLike,\n res: { status: (code: number) => { set: (k: string, v: string) => { json: (body: unknown) => void } } },\n next: NextFn\n ): Promise<void> {\n try {\n const url = options.getUrl ? options.getUrl(req) : defaultUrl(req);\n const body = resolveRawBody(req);\n const requestLike: RequestLike = {\n method: req.method,\n url,\n headers: req.headers,\n body,\n };\n const result = await verifyRequestSignature(requestLike, {\n ...options,\n replayStore,\n revocationStore,\n operation: options.resolveOperation(req),\n });\n if (result.status === 'verified') {\n req.verifiedSigner = {\n keyid: result.keyid,\n agent_url: result.agent_url,\n verified_at: result.verified_at,\n };\n }\n next();\n } catch (err) {\n if (err instanceof RequestSignatureError) {\n // `failed_step` is informational per spec; keep it in server-side logs\n // rather than the 401 body so anonymous callers can't enumerate the\n // verifier pipeline. Do NOT call next() here — Express convention is\n // that a terminal response ends the middleware chain without\n // invoking downstream handlers. Promise-based wrappers that need to\n // await completion should race against the response's 'close' event\n // rather than the next-callback.\n res.status(401).set('WWW-Authenticate', `Signature error=\"${err.code}\"`).json({\n error: err.code,\n message: 'Request signature verification failed',\n });\n return;\n }\n next(err);\n }\n };\n}\n\nfunction resolveRawBody(req: ExpressLike): string {\n if (typeof req.rawBody === 'string') return req.rawBody;\n const contentLengthHeader = getHeaderValue(req.headers, 'content-length');\n const contentLength = contentLengthHeader ? Number(contentLengthHeader) : 0;\n if (Number.isFinite(contentLength) && contentLength > 0) {\n throw new RequestSignatureError(\n 'request_signature_header_malformed',\n 1,\n 'req.rawBody is required for signed requests with a body; install a raw-body capture middleware ahead of createExpressVerifier'\n );\n }\n return '';\n}\n\nfunction defaultUrl(req: ExpressLike): string {\n const proto = req.protocol ?? (typeof req.get === 'function' ? req.get('x-forwarded-proto') : undefined) ?? 'https';\n const host = typeof req.get === 'function' ? req.get('host') : undefined;\n if (!host) {\n throw new Error(\n 'Unable to derive request URL: missing Host header. Pass `getUrl` on createExpressVerifier to supply the canonical URL explicitly.'\n );\n }\n return `${proto}://${host}${req.originalUrl ?? req.url}`;\n}\n"],"mappings":"AACA,SAAS,sBAAsB;AAC/B,SAAS,6BAA6B;AACtC,SAAS,2BAA2B;AACpC,SAAS,+BAA+B;AACxC,SAAS,8BAAyD;AA4E3D,SAAS,sBAAsB,SAAmC;AAIvE,QAAM,cAAc,QAAQ,eAAe,IAAI,oBAAoB;AACnE,QAAM,kBAAkB,QAAQ,mBAAmB,IAAI,wBAAwB;AAE/E,SAAO,eAAe,2BACpB,KACA,KACA,MACe;AACf,QAAI;AACF,YAAM,MAAM,QAAQ,SAAS,QAAQ,OAAO,GAAG,IAAI,WAAW,GAAG;AACjE,YAAM,OAAO,eAAe,GAAG;AAC/B,YAAM,cAA2B;AAAA,QAC/B,QAAQ,IAAI;AAAA,QACZ;AAAA,QACA,SAAS,IAAI;AAAA,QACb;AAAA,MACF;AACA,YAAM,SAAS,MAAM,uBAAuB,aAAa;AAAA,QACvD,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,WAAW,QAAQ,iBAAiB,GAAG;AAAA,MACzC,CAAC;AACD,UAAI,OAAO,WAAW,YAAY;AAChC,YAAI,iBAAiB;AAAA,UACnB,OAAO,OAAO;AAAA,UACd,WAAW,OAAO;AAAA,UAClB,aAAa,OAAO;AAAA,QACtB;AAAA,MACF;AACA,WAAK;AAAA,IACP,SAAS,KAAK;AACZ,UAAI,eAAe,uBAAuB;AAQxC,YAAI,OAAO,GAAG,EAAE,IAAI,oBAAoB,oBAAoB,IAAI,IAAI,GAAG,EAAE,KAAK;AAAA,UAC5E,OAAO,IAAI;AAAA,UACX,SAAS;AAAA,QACX,CAAC;AACD;AAAA,MACF;AACA,WAAK,GAAG;AAAA,IACV;AAAA,EACF;AACF;AAEA,SAAS,eAAe,KAA0B;AAChD,MAAI,OAAO,IAAI,YAAY,SAAU,QAAO,IAAI;AAChD,QAAM,sBAAsB,eAAe,IAAI,SAAS,gBAAgB;AACxE,QAAM,gBAAgB,sBAAsB,OAAO,mBAAmB,IAAI;AAC1E,MAAI,OAAO,SAAS,aAAa,KAAK,gBAAgB,GAAG;AACvD,UAAM,IAAI;AAAA,MACR;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,WAAW,KAA0B;AAC5C,QAAM,QAAQ,IAAI,aAAa,OAAO,IAAI,QAAQ,aAAa,IAAI,IAAI,mBAAmB,IAAI,WAAc;AAC5G,QAAM,OAAO,OAAO,IAAI,QAAQ,aAAa,IAAI,IAAI,MAAM,IAAI;AAC/D,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,GAAG,KAAK,MAAM,IAAI,GAAG,IAAI,eAAe,IAAI,GAAG;AACxD;","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { ParseError, parseDictionary, serializeInnerList } from "structured-headers";
|
|
8
2
|
import { RequestSignatureError } from "./errors.mjs";
|
|
9
3
|
const STRING_PARAMS = /* @__PURE__ */ new Set(["nonce", "keyid", "alg", "tag"]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/parser.ts"],"sourcesContent":["import { ParseError, parseDictionary, serializeInnerList, type InnerList } from 'structured-headers';\nimport { RequestSignatureError } from './errors';\n\nexport interface ParsedSignatureInput {\n label: string;\n components: string[];\n /**\n * The `Signature-Input` value verbatim for the selected label (minus the\n * `<label>=` prefix). Re-emitted as the `@signature-params` line in the\n * signature base so verifier and signer stay byte-identical regardless of\n * the sender's param ordering.\n */\n signatureParamsValue: string;\n params: {\n created: number;\n expires: number;\n nonce: string;\n keyid: string;\n alg: string;\n tag: string;\n [extra: string]: string | number | undefined;\n };\n}\n\nexport interface ParsedSignature {\n label: string;\n bytes: Uint8Array;\n}\n\nconst STRING_PARAMS = new Set(['nonce', 'keyid', 'alg', 'tag']);\nconst INTEGER_PARAMS = new Set(['created', 'expires']);\n\nfunction malformed(message: string): never {\n throw new RequestSignatureError('request_signature_header_malformed', 1, message);\n}\n\nexport function parseSignatureInput(headerValue: string): ParsedSignatureInput {\n rejectDuplicateDictionaryKeys(headerValue, 'Signature-Input');\n let dict;\n try {\n dict = parseDictionary(headerValue);\n } catch (e: unknown) {\n if (e instanceof ParseError) malformed(`Signature-Input header is malformed: ${e.message}`);\n throw e;\n }\n if (dict.size === 0) malformed('Signature-Input header is empty');\n const label = dict.has('sig1') ? 'sig1' : (dict.keys().next().value as string);\n const entry = dict.get(label)!;\n if (!isInnerList(entry)) {\n malformed('Signature-Input value must be a parenthesized component list');\n }\n const components: string[] = [];\n for (const [bare, itemParams] of entry[0]) {\n if (typeof bare !== 'string') malformed('Signature-Input components must all be strings');\n components.push(itemParams instanceof Map && itemParams.get('req') === true ? `${bare};req` : bare);\n }\n const params: Record<string, string | number> = {};\n for (const [key, value] of entry[1]) {\n if (STRING_PARAMS.has(key)) {\n if (typeof value !== 'string') {\n malformed(`Signature parameter \"${key}\" must be a quoted string`);\n }\n params[key] = value;\n } else if (INTEGER_PARAMS.has(key)) {\n if (typeof value !== 'number' || !Number.isInteger(value)) {\n malformed(`Signature parameter \"${key}\" must be an integer`);\n }\n params[key] = value;\n } else if (typeof value === 'string' || typeof value === 'number') {\n params[key] = value;\n }\n }\n return {\n label,\n components,\n signatureParamsValue: serializeInnerList(entry),\n params: params as ParsedSignatureInput['params'],\n };\n}\n\nexport function parseSignature(headerValue: string, expectedLabel: string): ParsedSignature {\n let dict;\n try {\n // RFC 9421 signatures commonly use base64url (`-`/`_`); RFC 8941 byte\n // sequences only permit standard base64. Translate the characters inside\n // byte-sequence delimiters before handing to the strict parser.\n dict = parseDictionary(normalizeByteSequenceBase64(headerValue));\n } catch (e: unknown) {\n if (e instanceof ParseError) {\n if (/base64/i.test(e.message)) malformed('Signature value contains non-base64 characters');\n malformed(`Signature header is malformed: ${e.message}`);\n }\n throw e;\n }\n const entry = dict.get(expectedLabel);\n if (!entry) {\n malformed(`Signature header does not contain label \"${expectedLabel}\"`);\n }\n if (!(entry[0] instanceof ArrayBuffer)) {\n malformed(`Signature value for \"${expectedLabel}\" must be a byte sequence`);\n }\n return { label: expectedLabel, bytes: new Uint8Array(entry[0].slice(0)) };\n}\n\nfunction isInnerList(entry: unknown): entry is InnerList {\n return Array.isArray(entry) && Array.isArray((entry as unknown[])[0]);\n}\n\n/**\n * RFC 8941 §3.2 parsers deduplicate by keeping the last value. The AdCP\n * profile (step 1) overrides that: duplicate keys are a downgrade vector —\n * a proxy could smuggle a weaker component set past a verifier that reads\n * the first. Detect duplicates at the raw-header level before the library\n * silently drops them.\n */\nexport function rejectDuplicateDictionaryKeys(headerValue: string, headerName: string): void {\n const keys = extractTopLevelDictKeys(headerValue);\n const seen = new Set<string>();\n for (const key of keys) {\n if (seen.has(key)) {\n malformed(`${headerName} header declares key \"${key}\" more than once`);\n }\n seen.add(key);\n }\n}\n\nfunction extractTopLevelDictKeys(input: string): string[] {\n const keys: string[] = [];\n let i = 0;\n const len = input.length;\n let atEntryStart = true;\n while (i < len) {\n if (atEntryStart) {\n while (i < len && (input[i] === ' ' || input[i] === '\\t')) i++;\n const keyStart = i;\n while (i < len && /[A-Za-z0-9_*-]/.test(input[i]!)) i++;\n if (i > keyStart) keys.push(input.slice(keyStart, i).toLowerCase());\n atEntryStart = false;\n continue;\n }\n const ch = input[i]!;\n if (ch === '\"') {\n i++;\n while (i < len) {\n if (input[i] === '\\\\' && i + 1 < len) {\n i += 2;\n continue;\n }\n if (input[i] === '\"') {\n i++;\n break;\n }\n i++;\n }\n continue;\n }\n if (ch === ':') {\n i++;\n while (i < len && input[i] !== ':') i++;\n if (i < len) i++;\n continue;\n }\n if (ch === '(') {\n i++;\n let depth = 1;\n while (i < len && depth > 0) {\n const c = input[i]!;\n if (c === '\"') {\n i++;\n while (i < len) {\n if (input[i] === '\\\\' && i + 1 < len) {\n i += 2;\n continue;\n }\n if (input[i] === '\"') {\n i++;\n break;\n }\n i++;\n }\n continue;\n }\n if (c === '(') depth++;\n else if (c === ')') depth--;\n i++;\n }\n continue;\n }\n if (ch === ',') {\n atEntryStart = true;\n i++;\n continue;\n }\n i++;\n }\n return keys;\n}\n\n/**\n * Translate base64url (`-`/`_`) inside byte-sequence delimiters to standard\n * base64 (`+`/`/`) so the strict RFC 8941 parser accepts the value. Enforces\n * the spec's single-alphabet rule: a byte-sequence token that mixes\n * base64url (`[-_]`) with standard base64 (`[+/=]`) is ambiguous and MUST\n * be rejected (`webhook_signature_header_malformed` / `request_signature_header_malformed`\n * per profile).\n */\nfunction normalizeByteSequenceBase64(input: string): string {\n let out = '';\n let inString = false;\n let inBytes = false;\n let sawStandardAlphabet = false;\n let sawUrlSafeAlphabet = false;\n for (let i = 0; i < input.length; i++) {\n const ch = input[i]!;\n if (inString) {\n out += ch;\n if (ch === '\\\\' && i + 1 < input.length) {\n out += input[++i]!;\n continue;\n }\n if (ch === '\"') inString = false;\n continue;\n }\n if (inBytes) {\n if (ch === ':') {\n inBytes = false;\n out += ch;\n if (sawStandardAlphabet && sawUrlSafeAlphabet) {\n malformed('Signature value mixes base64url and standard-base64 alphabets');\n }\n sawStandardAlphabet = false;\n sawUrlSafeAlphabet = false;\n } else if (ch === '-') {\n sawUrlSafeAlphabet = true;\n out += '+';\n } else if (ch === '_') {\n sawUrlSafeAlphabet = true;\n out += '/';\n } else {\n if (ch === '+' || ch === '/' || ch === '=') sawStandardAlphabet = true;\n out += ch;\n }\n continue;\n }\n if (ch === '\"') inString = true;\n else if (ch === ':') inBytes = true;\n out += ch;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/parser.ts"],"sourcesContent":["import { ParseError, parseDictionary, serializeInnerList, type InnerList } from 'structured-headers';\nimport { RequestSignatureError } from './errors';\n\nexport interface ParsedSignatureInput {\n label: string;\n components: string[];\n /**\n * The `Signature-Input` value verbatim for the selected label (minus the\n * `<label>=` prefix). Re-emitted as the `@signature-params` line in the\n * signature base so verifier and signer stay byte-identical regardless of\n * the sender's param ordering.\n */\n signatureParamsValue: string;\n params: {\n created: number;\n expires: number;\n nonce: string;\n keyid: string;\n alg: string;\n tag: string;\n [extra: string]: string | number | undefined;\n };\n}\n\nexport interface ParsedSignature {\n label: string;\n bytes: Uint8Array;\n}\n\nconst STRING_PARAMS = new Set(['nonce', 'keyid', 'alg', 'tag']);\nconst INTEGER_PARAMS = new Set(['created', 'expires']);\n\nfunction malformed(message: string): never {\n throw new RequestSignatureError('request_signature_header_malformed', 1, message);\n}\n\nexport function parseSignatureInput(headerValue: string): ParsedSignatureInput {\n rejectDuplicateDictionaryKeys(headerValue, 'Signature-Input');\n let dict;\n try {\n dict = parseDictionary(headerValue);\n } catch (e: unknown) {\n if (e instanceof ParseError) malformed(`Signature-Input header is malformed: ${e.message}`);\n throw e;\n }\n if (dict.size === 0) malformed('Signature-Input header is empty');\n const label = dict.has('sig1') ? 'sig1' : (dict.keys().next().value as string);\n const entry = dict.get(label)!;\n if (!isInnerList(entry)) {\n malformed('Signature-Input value must be a parenthesized component list');\n }\n const components: string[] = [];\n for (const [bare, itemParams] of entry[0]) {\n if (typeof bare !== 'string') malformed('Signature-Input components must all be strings');\n components.push(itemParams instanceof Map && itemParams.get('req') === true ? `${bare};req` : bare);\n }\n const params: Record<string, string | number> = {};\n for (const [key, value] of entry[1]) {\n if (STRING_PARAMS.has(key)) {\n if (typeof value !== 'string') {\n malformed(`Signature parameter \"${key}\" must be a quoted string`);\n }\n params[key] = value;\n } else if (INTEGER_PARAMS.has(key)) {\n if (typeof value !== 'number' || !Number.isInteger(value)) {\n malformed(`Signature parameter \"${key}\" must be an integer`);\n }\n params[key] = value;\n } else if (typeof value === 'string' || typeof value === 'number') {\n params[key] = value;\n }\n }\n return {\n label,\n components,\n signatureParamsValue: serializeInnerList(entry),\n params: params as ParsedSignatureInput['params'],\n };\n}\n\nexport function parseSignature(headerValue: string, expectedLabel: string): ParsedSignature {\n let dict;\n try {\n // RFC 9421 signatures commonly use base64url (`-`/`_`); RFC 8941 byte\n // sequences only permit standard base64. Translate the characters inside\n // byte-sequence delimiters before handing to the strict parser.\n dict = parseDictionary(normalizeByteSequenceBase64(headerValue));\n } catch (e: unknown) {\n if (e instanceof ParseError) {\n if (/base64/i.test(e.message)) malformed('Signature value contains non-base64 characters');\n malformed(`Signature header is malformed: ${e.message}`);\n }\n throw e;\n }\n const entry = dict.get(expectedLabel);\n if (!entry) {\n malformed(`Signature header does not contain label \"${expectedLabel}\"`);\n }\n if (!(entry[0] instanceof ArrayBuffer)) {\n malformed(`Signature value for \"${expectedLabel}\" must be a byte sequence`);\n }\n return { label: expectedLabel, bytes: new Uint8Array(entry[0].slice(0)) };\n}\n\nfunction isInnerList(entry: unknown): entry is InnerList {\n return Array.isArray(entry) && Array.isArray((entry as unknown[])[0]);\n}\n\n/**\n * RFC 8941 §3.2 parsers deduplicate by keeping the last value. The AdCP\n * profile (step 1) overrides that: duplicate keys are a downgrade vector —\n * a proxy could smuggle a weaker component set past a verifier that reads\n * the first. Detect duplicates at the raw-header level before the library\n * silently drops them.\n */\nexport function rejectDuplicateDictionaryKeys(headerValue: string, headerName: string): void {\n const keys = extractTopLevelDictKeys(headerValue);\n const seen = new Set<string>();\n for (const key of keys) {\n if (seen.has(key)) {\n malformed(`${headerName} header declares key \"${key}\" more than once`);\n }\n seen.add(key);\n }\n}\n\nfunction extractTopLevelDictKeys(input: string): string[] {\n const keys: string[] = [];\n let i = 0;\n const len = input.length;\n let atEntryStart = true;\n while (i < len) {\n if (atEntryStart) {\n while (i < len && (input[i] === ' ' || input[i] === '\\t')) i++;\n const keyStart = i;\n while (i < len && /[A-Za-z0-9_*-]/.test(input[i]!)) i++;\n if (i > keyStart) keys.push(input.slice(keyStart, i).toLowerCase());\n atEntryStart = false;\n continue;\n }\n const ch = input[i]!;\n if (ch === '\"') {\n i++;\n while (i < len) {\n if (input[i] === '\\\\' && i + 1 < len) {\n i += 2;\n continue;\n }\n if (input[i] === '\"') {\n i++;\n break;\n }\n i++;\n }\n continue;\n }\n if (ch === ':') {\n i++;\n while (i < len && input[i] !== ':') i++;\n if (i < len) i++;\n continue;\n }\n if (ch === '(') {\n i++;\n let depth = 1;\n while (i < len && depth > 0) {\n const c = input[i]!;\n if (c === '\"') {\n i++;\n while (i < len) {\n if (input[i] === '\\\\' && i + 1 < len) {\n i += 2;\n continue;\n }\n if (input[i] === '\"') {\n i++;\n break;\n }\n i++;\n }\n continue;\n }\n if (c === '(') depth++;\n else if (c === ')') depth--;\n i++;\n }\n continue;\n }\n if (ch === ',') {\n atEntryStart = true;\n i++;\n continue;\n }\n i++;\n }\n return keys;\n}\n\n/**\n * Translate base64url (`-`/`_`) inside byte-sequence delimiters to standard\n * base64 (`+`/`/`) so the strict RFC 8941 parser accepts the value. Enforces\n * the spec's single-alphabet rule: a byte-sequence token that mixes\n * base64url (`[-_]`) with standard base64 (`[+/=]`) is ambiguous and MUST\n * be rejected (`webhook_signature_header_malformed` / `request_signature_header_malformed`\n * per profile).\n */\nfunction normalizeByteSequenceBase64(input: string): string {\n let out = '';\n let inString = false;\n let inBytes = false;\n let sawStandardAlphabet = false;\n let sawUrlSafeAlphabet = false;\n for (let i = 0; i < input.length; i++) {\n const ch = input[i]!;\n if (inString) {\n out += ch;\n if (ch === '\\\\' && i + 1 < input.length) {\n out += input[++i]!;\n continue;\n }\n if (ch === '\"') inString = false;\n continue;\n }\n if (inBytes) {\n if (ch === ':') {\n inBytes = false;\n out += ch;\n if (sawStandardAlphabet && sawUrlSafeAlphabet) {\n malformed('Signature value mixes base64url and standard-base64 alphabets');\n }\n sawStandardAlphabet = false;\n sawUrlSafeAlphabet = false;\n } else if (ch === '-') {\n sawUrlSafeAlphabet = true;\n out += '+';\n } else if (ch === '_') {\n sawUrlSafeAlphabet = true;\n out += '/';\n } else {\n if (ch === '+' || ch === '/' || ch === '=') sawStandardAlphabet = true;\n out += ch;\n }\n continue;\n }\n if (ch === '\"') inString = true;\n else if (ch === ':') inBytes = true;\n out += ch;\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
const DEFAULT_TABLE = "adcp_replay_cache";
|
|
8
2
|
const DEFAULT_CAP = 1e5;
|
|
9
3
|
const VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/postgres-replay-store.ts"],"sourcesContent":["/**\n * PostgreSQL-backed `ReplayStore` for distributed AdCP verifier deployments.\n *\n * Replaces `InMemoryReplayStore` when running multiple verifier instances\n * behind a load balancer. The default in-memory store is per-process; a\n * signature captured by an attacker can be replayed against a sibling\n * instance whose cache hasn't seen the nonce. Sharing replay state via\n * Postgres closes that hole using a `(keyid, scope, nonce)` primary key\n * the verifier checks on every signed request.\n *\n * @example\n * ```typescript\n * import { Pool } from 'pg';\n * import { PostgresReplayStore, getReplayStoreMigration } from '@adcp/sdk/signing/server';\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * await pool.query(getReplayStoreMigration()); // run once at boot\n *\n * const replayStore = new PostgresReplayStore(pool);\n *\n * app.use(createExpressVerifier({\n * capability: { ... },\n * jwks,\n * replayStore, // <-- shared across instances\n * resolveOperation: mcpToolNameResolver,\n * }));\n *\n * // Schedule the sweeper somewhere (cron, app timer, pg_cron, etc.):\n * setInterval(() => sweepExpiredReplays(pool).catch(console.error), 60_000);\n * ```\n *\n * Reuses the structural `PgQueryable` interface from `postgres-task-store`\n * so the SDK stays free of a hard `pg` dependency — callers pass any\n * pg-compatible query executor.\n */\n\nimport type { PgQueryable } from '../server/postgres-task-store';\nimport type { ReplayInsertResult, ReplayStore } from './replay';\n\nconst DEFAULT_TABLE = 'adcp_replay_cache';\nconst DEFAULT_CAP = 100_000;\nconst VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;\n\n/**\n * Reject non-finite or out-of-range timestamps before they reach Postgres.\n * `to_timestamp(NaN)` raises a parse error and `to_timestamp(±Infinity)`\n * silently produces an `infinity` timestamp — neither would lapse replay\n * protection, but a buggy `options.now()` injection point could DoS the\n * verifier with PG errors. Bound to JS-safe-integer territory; verifiers\n * pass UNIX seconds, not microseconds, so the upper bound is effectively\n * \"year 9999 plus epsilon.\"\n */\nfunction assertFiniteSeconds(label: string, value: number): void {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {\n throw new TypeError(`PostgresReplayStore: ${label} must be a finite non-negative number; received ${value}`);\n }\n}\n\nexport interface PostgresReplayStoreOptions {\n /** Table name. Lowercase letters, digits, underscores only. Defaults to `adcp_replay_cache`. */\n tableName?: string;\n /**\n * Max retained (unexpired) nonces per `(keyid, scope)` pair before\n * `insert` returns `'rate_abuse'`. Mirrors `InMemoryReplayStore`'s\n * `maxEntriesPerKeyid`. Defaults to 100,000.\n *\n * The cap is **best-effort** under concurrency: two simultaneous inserts\n * at `cap-1` can both observe `n = cap-1` from the same MVCC snapshot\n * and both insert, briefly overshooting by one. This matches\n * `InMemoryReplayStore` semantics — the cap is a soft DoS guard, not a\n * hard invariant.\n */\n cap?: number;\n}\n\n/**\n * Pre-baked migration SQL for the default `adcp_replay_cache` table. Use\n * when callers don't need a custom table name. Mirrors the convention of\n * `MCP_TASKS_MIGRATION` (`postgres-task-store.ts`) and `ADCP_STATE_MIGRATION`\n * (`postgres-state-store.ts`).\n */\nexport const REPLAY_CACHE_MIGRATION: string = renderReplayStoreMigration(DEFAULT_TABLE);\n\n/**\n * Generate the SQL DDL for the replay-cache table. Idempotent — safe to\n * run on existing tables. Run once at server startup, before constructing\n * `PostgresReplayStore`.\n *\n * Schema:\n * - `(keyid, scope, nonce)` is the primary key, so concurrent inserts of\n * the same tuple serialize against the unique index — the second insert\n * silently fails via `ON CONFLICT DO NOTHING`, which the adapter detects\n * to return `'replayed'`.\n * - `expires_at` carries the per-row TTL since Postgres has no native TTL.\n * Lookups filter `expires_at > now()`; expired rows are deleted by the\n * exported {@link sweepExpiredReplays} helper which callers schedule\n * themselves.\n */\nexport function getReplayStoreMigration(tableName: string = DEFAULT_TABLE): string {\n return renderReplayStoreMigration(tableName);\n}\n\nfunction renderReplayStoreMigration(tableName: string): string {\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n return `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n keyid TEXT NOT NULL,\n scope TEXT NOT NULL,\n nonce TEXT NOT NULL,\n expires_at TIMESTAMPTZ NOT NULL,\n PRIMARY KEY (keyid, scope, nonce)\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_expires_at\n ON ${tableName}(expires_at);\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_keyid_scope_active\n ON ${tableName}(keyid, scope, expires_at);\n `;\n}\n\nexport class PostgresReplayStore implements ReplayStore {\n private readonly db: PgQueryable;\n private readonly tableName: string;\n private readonly cap: number;\n\n constructor(db: PgQueryable, options: PostgresReplayStoreOptions = {}) {\n const tableName = options.tableName ?? DEFAULT_TABLE;\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n this.db = db;\n this.tableName = tableName;\n this.cap = options.cap ?? DEFAULT_CAP;\n }\n\n async has(keyid: string, scope: string, nonce: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const result = await this.db.query(\n `SELECT 1 FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND nonce = $3 AND expires_at > to_timestamp($4)\n LIMIT 1`,\n [keyid, scope, nonce, now]\n );\n return (result.rowCount ?? 0) > 0;\n }\n\n async isCapHit(keyid: string, scope: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const result = await this.db.query(\n `SELECT count(*)::bigint AS active FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND expires_at > to_timestamp($3)`,\n [keyid, scope, now]\n );\n const row = result.rows[0] as { active: string | number } | undefined;\n if (!row) return false;\n // count(*)::bigint returns string in node-postgres by default; coerce.\n const active = typeof row.active === 'string' ? Number(row.active) : row.active;\n return active >= this.cap;\n }\n\n /**\n * Atomically check replay → cap → insert in a single MVCC-snapshotted\n * statement. Result precedence matches `InMemoryReplayStore`: replay\n * wins over rate_abuse wins over ok.\n *\n * Recycles expired rows in place via `ON CONFLICT DO UPDATE WHERE\n * existing-is-expired`. Without that, a same-nonce insert *after* the\n * previous registration's TTL elapsed (but before the sweeper ran)\n * would falsely report `'replayed'` — the sync `InMemoryReplayStore`\n * prunes expired entries before the existence check, so we have to\n * achieve the same semantics here without a separate prune step.\n *\n * The `RETURNING 1` is empty when (a) the cap blocked the insert\n * (`WHERE` clause filtered the conditional INSERT to zero rows) or\n * (b) the concurrent-same-nonce race lost — another transaction\n * inserted the same `(keyid, scope, nonce)` between this statement's\n * snapshot and our `ON CONFLICT`. Both branches are handled by the\n * outer `CASE`.\n */\n async insert(\n keyid: string,\n scope: string,\n nonce: string,\n ttlSeconds: number,\n now: number\n ): Promise<ReplayInsertResult> {\n assertFiniteSeconds('now', now);\n assertFiniteSeconds('ttlSeconds', ttlSeconds);\n const expiresAt = now + ttlSeconds;\n const result = await this.db.query(\n `WITH\n existing AS (\n SELECT 1 FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND nonce = $3 AND expires_at > to_timestamp($4)\n ),\n active AS (\n SELECT count(*)::bigint AS n FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND expires_at > to_timestamp($4)\n ),\n upsert AS (\n INSERT INTO ${this.tableName} (keyid, scope, nonce, expires_at)\n SELECT $1, $2, $3, to_timestamp($5)\n WHERE NOT EXISTS (SELECT 1 FROM existing)\n AND (SELECT n FROM active) < $6::bigint\n ON CONFLICT (keyid, scope, nonce) DO UPDATE\n SET expires_at = EXCLUDED.expires_at\n WHERE ${this.tableName}.expires_at <= to_timestamp($4)\n RETURNING 1\n )\n SELECT\n CASE\n WHEN EXISTS (SELECT 1 FROM existing) THEN 'replayed'\n WHEN (SELECT n FROM active) >= $6::bigint THEN 'rate_abuse'\n WHEN EXISTS (SELECT 1 FROM upsert) THEN 'ok'\n ELSE 'replayed'\n END AS result`,\n [keyid, scope, nonce, now, expiresAt, this.cap]\n );\n const row = result.rows[0] as { result: ReplayInsertResult } | undefined;\n if (!row) {\n throw new Error('PostgresReplayStore.insert: query returned no rows');\n }\n return row.result;\n }\n}\n\nexport interface SweepExpiredReplaysOptions {\n /** Table name. Defaults to `adcp_replay_cache`. */\n tableName?: string;\n /** Current time in seconds (UNIX epoch). Defaults to `Date.now() / 1000`. */\n now?: number;\n /**\n * Limit the number of rows deleted per call. Useful for very large\n * tables where a single `DELETE` would lock the table too long. When\n * unset, deletes all expired rows in one statement.\n */\n batchSize?: number;\n}\n\n/**\n * Delete expired rows from the replay-cache table. Postgres has no native\n * TTL; callers schedule this helper themselves (cron, an app-side timer,\n * a `pg_cron` job, etc.).\n *\n * Returns the count of rows deleted, so callers can tune sweep frequency\n * against observed accumulation. A typical schedule is once per minute\n * for moderate-traffic verifiers; a hot-path verifier signing thousands\n * per second may want every 10–15 seconds.\n *\n * @example\n * ```typescript\n * setInterval(async () => {\n * const { deleted } = await sweepExpiredReplays(pool);\n * if (deleted > 0) metrics.replayCacheSweep(deleted);\n * }, 60_000);\n * ```\n */\nexport async function sweepExpiredReplays(\n db: PgQueryable,\n options: SweepExpiredReplaysOptions = {}\n): Promise<{ deleted: number }> {\n const tableName = options.tableName ?? DEFAULT_TABLE;\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n const now = options.now ?? Math.floor(Date.now() / 1000);\n const batchSize = options.batchSize;\n\n if (batchSize === undefined) {\n const result = await db.query(`DELETE FROM ${tableName} WHERE expires_at <= to_timestamp($1)`, [now]);\n return { deleted: result.rowCount ?? 0 };\n }\n // Bounded sweep — use a CTE to delete only `batchSize` rows. Useful when\n // the table has accumulated a long tail and a single DELETE would\n // hold a long table lock.\n const result = await db.query(\n `WITH expired AS (\n SELECT keyid, scope, nonce\n FROM ${tableName}\n WHERE expires_at <= to_timestamp($1)\n LIMIT $2\n )\n DELETE FROM ${tableName}\n USING expired\n WHERE ${tableName}.keyid = expired.keyid\n AND ${tableName}.scope = expired.scope\n AND ${tableName}.nonce = expired.nonce`,\n [now, batchSize]\n );\n return { deleted: result.rowCount ?? 0 };\n}\n"],"mappings":"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","names":["result"]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/postgres-replay-store.ts"],"sourcesContent":["/**\n * PostgreSQL-backed `ReplayStore` for distributed AdCP verifier deployments.\n *\n * Replaces `InMemoryReplayStore` when running multiple verifier instances\n * behind a load balancer. The default in-memory store is per-process; a\n * signature captured by an attacker can be replayed against a sibling\n * instance whose cache hasn't seen the nonce. Sharing replay state via\n * Postgres closes that hole using a `(keyid, scope, nonce)` primary key\n * the verifier checks on every signed request.\n *\n * @example\n * ```typescript\n * import { Pool } from 'pg';\n * import { PostgresReplayStore, getReplayStoreMigration } from '@adcp/sdk/signing/server';\n *\n * const pool = new Pool({ connectionString: process.env.DATABASE_URL });\n * await pool.query(getReplayStoreMigration()); // run once at boot\n *\n * const replayStore = new PostgresReplayStore(pool);\n *\n * app.use(createExpressVerifier({\n * capability: { ... },\n * jwks,\n * replayStore, // <-- shared across instances\n * resolveOperation: mcpToolNameResolver,\n * }));\n *\n * // Schedule the sweeper somewhere (cron, app timer, pg_cron, etc.):\n * setInterval(() => sweepExpiredReplays(pool).catch(console.error), 60_000);\n * ```\n *\n * Reuses the structural `PgQueryable` interface from `postgres-task-store`\n * so the SDK stays free of a hard `pg` dependency — callers pass any\n * pg-compatible query executor.\n */\n\nimport type { PgQueryable } from '../server/postgres-task-store';\nimport type { ReplayInsertResult, ReplayStore } from './replay';\n\nconst DEFAULT_TABLE = 'adcp_replay_cache';\nconst DEFAULT_CAP = 100_000;\nconst VALID_IDENTIFIER = /^[a-z_][a-z0-9_]*$/;\n\n/**\n * Reject non-finite or out-of-range timestamps before they reach Postgres.\n * `to_timestamp(NaN)` raises a parse error and `to_timestamp(±Infinity)`\n * silently produces an `infinity` timestamp — neither would lapse replay\n * protection, but a buggy `options.now()` injection point could DoS the\n * verifier with PG errors. Bound to JS-safe-integer territory; verifiers\n * pass UNIX seconds, not microseconds, so the upper bound is effectively\n * \"year 9999 plus epsilon.\"\n */\nfunction assertFiniteSeconds(label: string, value: number): void {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {\n throw new TypeError(`PostgresReplayStore: ${label} must be a finite non-negative number; received ${value}`);\n }\n}\n\nexport interface PostgresReplayStoreOptions {\n /** Table name. Lowercase letters, digits, underscores only. Defaults to `adcp_replay_cache`. */\n tableName?: string;\n /**\n * Max retained (unexpired) nonces per `(keyid, scope)` pair before\n * `insert` returns `'rate_abuse'`. Mirrors `InMemoryReplayStore`'s\n * `maxEntriesPerKeyid`. Defaults to 100,000.\n *\n * The cap is **best-effort** under concurrency: two simultaneous inserts\n * at `cap-1` can both observe `n = cap-1` from the same MVCC snapshot\n * and both insert, briefly overshooting by one. This matches\n * `InMemoryReplayStore` semantics — the cap is a soft DoS guard, not a\n * hard invariant.\n */\n cap?: number;\n}\n\n/**\n * Pre-baked migration SQL for the default `adcp_replay_cache` table. Use\n * when callers don't need a custom table name. Mirrors the convention of\n * `MCP_TASKS_MIGRATION` (`postgres-task-store.ts`) and `ADCP_STATE_MIGRATION`\n * (`postgres-state-store.ts`).\n */\nexport const REPLAY_CACHE_MIGRATION: string = renderReplayStoreMigration(DEFAULT_TABLE);\n\n/**\n * Generate the SQL DDL for the replay-cache table. Idempotent — safe to\n * run on existing tables. Run once at server startup, before constructing\n * `PostgresReplayStore`.\n *\n * Schema:\n * - `(keyid, scope, nonce)` is the primary key, so concurrent inserts of\n * the same tuple serialize against the unique index — the second insert\n * silently fails via `ON CONFLICT DO NOTHING`, which the adapter detects\n * to return `'replayed'`.\n * - `expires_at` carries the per-row TTL since Postgres has no native TTL.\n * Lookups filter `expires_at > now()`; expired rows are deleted by the\n * exported {@link sweepExpiredReplays} helper which callers schedule\n * themselves.\n */\nexport function getReplayStoreMigration(tableName: string = DEFAULT_TABLE): string {\n return renderReplayStoreMigration(tableName);\n}\n\nfunction renderReplayStoreMigration(tableName: string): string {\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n return `\n CREATE TABLE IF NOT EXISTS ${tableName} (\n keyid TEXT NOT NULL,\n scope TEXT NOT NULL,\n nonce TEXT NOT NULL,\n expires_at TIMESTAMPTZ NOT NULL,\n PRIMARY KEY (keyid, scope, nonce)\n );\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_expires_at\n ON ${tableName}(expires_at);\n\n CREATE INDEX IF NOT EXISTS idx_${tableName}_keyid_scope_active\n ON ${tableName}(keyid, scope, expires_at);\n `;\n}\n\nexport class PostgresReplayStore implements ReplayStore {\n private readonly db: PgQueryable;\n private readonly tableName: string;\n private readonly cap: number;\n\n constructor(db: PgQueryable, options: PostgresReplayStoreOptions = {}) {\n const tableName = options.tableName ?? DEFAULT_TABLE;\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n this.db = db;\n this.tableName = tableName;\n this.cap = options.cap ?? DEFAULT_CAP;\n }\n\n async has(keyid: string, scope: string, nonce: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const result = await this.db.query(\n `SELECT 1 FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND nonce = $3 AND expires_at > to_timestamp($4)\n LIMIT 1`,\n [keyid, scope, nonce, now]\n );\n return (result.rowCount ?? 0) > 0;\n }\n\n async isCapHit(keyid: string, scope: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const result = await this.db.query(\n `SELECT count(*)::bigint AS active FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND expires_at > to_timestamp($3)`,\n [keyid, scope, now]\n );\n const row = result.rows[0] as { active: string | number } | undefined;\n if (!row) return false;\n // count(*)::bigint returns string in node-postgres by default; coerce.\n const active = typeof row.active === 'string' ? Number(row.active) : row.active;\n return active >= this.cap;\n }\n\n /**\n * Atomically check replay → cap → insert in a single MVCC-snapshotted\n * statement. Result precedence matches `InMemoryReplayStore`: replay\n * wins over rate_abuse wins over ok.\n *\n * Recycles expired rows in place via `ON CONFLICT DO UPDATE WHERE\n * existing-is-expired`. Without that, a same-nonce insert *after* the\n * previous registration's TTL elapsed (but before the sweeper ran)\n * would falsely report `'replayed'` — the sync `InMemoryReplayStore`\n * prunes expired entries before the existence check, so we have to\n * achieve the same semantics here without a separate prune step.\n *\n * The `RETURNING 1` is empty when (a) the cap blocked the insert\n * (`WHERE` clause filtered the conditional INSERT to zero rows) or\n * (b) the concurrent-same-nonce race lost — another transaction\n * inserted the same `(keyid, scope, nonce)` between this statement's\n * snapshot and our `ON CONFLICT`. Both branches are handled by the\n * outer `CASE`.\n */\n async insert(\n keyid: string,\n scope: string,\n nonce: string,\n ttlSeconds: number,\n now: number\n ): Promise<ReplayInsertResult> {\n assertFiniteSeconds('now', now);\n assertFiniteSeconds('ttlSeconds', ttlSeconds);\n const expiresAt = now + ttlSeconds;\n const result = await this.db.query(\n `WITH\n existing AS (\n SELECT 1 FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND nonce = $3 AND expires_at > to_timestamp($4)\n ),\n active AS (\n SELECT count(*)::bigint AS n FROM ${this.tableName}\n WHERE keyid = $1 AND scope = $2 AND expires_at > to_timestamp($4)\n ),\n upsert AS (\n INSERT INTO ${this.tableName} (keyid, scope, nonce, expires_at)\n SELECT $1, $2, $3, to_timestamp($5)\n WHERE NOT EXISTS (SELECT 1 FROM existing)\n AND (SELECT n FROM active) < $6::bigint\n ON CONFLICT (keyid, scope, nonce) DO UPDATE\n SET expires_at = EXCLUDED.expires_at\n WHERE ${this.tableName}.expires_at <= to_timestamp($4)\n RETURNING 1\n )\n SELECT\n CASE\n WHEN EXISTS (SELECT 1 FROM existing) THEN 'replayed'\n WHEN (SELECT n FROM active) >= $6::bigint THEN 'rate_abuse'\n WHEN EXISTS (SELECT 1 FROM upsert) THEN 'ok'\n ELSE 'replayed'\n END AS result`,\n [keyid, scope, nonce, now, expiresAt, this.cap]\n );\n const row = result.rows[0] as { result: ReplayInsertResult } | undefined;\n if (!row) {\n throw new Error('PostgresReplayStore.insert: query returned no rows');\n }\n return row.result;\n }\n}\n\nexport interface SweepExpiredReplaysOptions {\n /** Table name. Defaults to `adcp_replay_cache`. */\n tableName?: string;\n /** Current time in seconds (UNIX epoch). Defaults to `Date.now() / 1000`. */\n now?: number;\n /**\n * Limit the number of rows deleted per call. Useful for very large\n * tables where a single `DELETE` would lock the table too long. When\n * unset, deletes all expired rows in one statement.\n */\n batchSize?: number;\n}\n\n/**\n * Delete expired rows from the replay-cache table. Postgres has no native\n * TTL; callers schedule this helper themselves (cron, an app-side timer,\n * a `pg_cron` job, etc.).\n *\n * Returns the count of rows deleted, so callers can tune sweep frequency\n * against observed accumulation. A typical schedule is once per minute\n * for moderate-traffic verifiers; a hot-path verifier signing thousands\n * per second may want every 10–15 seconds.\n *\n * @example\n * ```typescript\n * setInterval(async () => {\n * const { deleted } = await sweepExpiredReplays(pool);\n * if (deleted > 0) metrics.replayCacheSweep(deleted);\n * }, 60_000);\n * ```\n */\nexport async function sweepExpiredReplays(\n db: PgQueryable,\n options: SweepExpiredReplaysOptions = {}\n): Promise<{ deleted: number }> {\n const tableName = options.tableName ?? DEFAULT_TABLE;\n if (!VALID_IDENTIFIER.test(tableName)) {\n throw new Error(`Invalid table name: \"${tableName}\". Must match /^[a-z_][a-z0-9_]*$/.`);\n }\n const now = options.now ?? Math.floor(Date.now() / 1000);\n const batchSize = options.batchSize;\n\n if (batchSize === undefined) {\n const result = await db.query(`DELETE FROM ${tableName} WHERE expires_at <= to_timestamp($1)`, [now]);\n return { deleted: result.rowCount ?? 0 };\n }\n // Bounded sweep — use a CTE to delete only `batchSize` rows. Useful when\n // the table has accumulated a long tail and a single DELETE would\n // hold a long table lock.\n const result = await db.query(\n `WITH expired AS (\n SELECT keyid, scope, nonce\n FROM ${tableName}\n WHERE expires_at <= to_timestamp($1)\n LIMIT $2\n )\n DELETE FROM ${tableName}\n USING expired\n WHERE ${tableName}.keyid = expired.keyid\n AND ${tableName}.scope = expired.scope\n AND ${tableName}.nonce = expired.nonce`,\n [now, batchSize]\n );\n return { deleted: result.rowCount ?? 0 };\n}\n"],"mappings":"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","names":["result"]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { getLatestA2ADataPartFromTask } from "../utils/a2a-artifacts.mjs";
|
|
8
2
|
function unwrapProtocolResponse(response) {
|
|
9
3
|
if (!response || typeof response !== "object") return response;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/protocol-response.ts"],"sourcesContent":["/**\n * Unwrap the AdCP payload from a protocol-level response. Both the\n * capability-priming flow and the agent-URL-bootstrap discovery chain need\n * to peel a `get_adcp_capabilities` response back to its plain-object body\n * regardless of which transport delivered it:\n *\n * - MCP `CallToolResult` — `structuredContent` wins when present;\n * otherwise the first `content[].text` chunk is parsed as JSON.\n * - A2A JSON-RPC `SendMessageResponse` — `result` is a `Task` or `Message`\n * with latest structured DataPart data.\n * - Already-unwrapped payload — returned as-is.\n *\n * Lives in its own module so the two callers (`capability-priming` and\n * `agent-resolver`) share one source of truth instead of forking the\n * envelope walk.\n */\n\nimport { getLatestA2ADataPartFromTask } from '../utils/a2a-artifacts';\n\nexport function unwrapProtocolResponse(response: unknown): unknown {\n if (!response || typeof response !== 'object') return response;\n const r = response as Record<string, unknown>;\n\n if (r.structuredContent && typeof r.structuredContent === 'object') return r.structuredContent;\n\n const content = r.content;\n if (Array.isArray(content)) {\n for (const chunk of content) {\n if (chunk && typeof chunk === 'object' && typeof (chunk as { text?: unknown }).text === 'string') {\n try {\n return JSON.parse((chunk as { text: string }).text);\n } catch {\n // non-JSON text chunk — keep looking\n }\n }\n }\n }\n\n const result = r.result;\n if (result && typeof result === 'object') {\n const artifactData = getLatestA2ADataPartFromTask(result)?.data;\n if (artifactData) {\n return artifactData;\n }\n\n const parts = (result as { parts?: unknown }).parts;\n const data = findLatestDataPart(parts);\n if (data) return data;\n }\n\n return response;\n}\n\nfunction findLatestDataPart(parts: unknown): unknown {\n if (!Array.isArray(parts)) return undefined;\n for (let i = parts.length - 1; i >= 0; i -= 1) {\n const part = parts[i];\n if (part && typeof part === 'object') {\n const p = part as { kind?: unknown; data?: unknown };\n if (p.kind === 'data' && p.data && typeof p.data === 'object') return p.data;\n }\n }\n return undefined;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/protocol-response.ts"],"sourcesContent":["/**\n * Unwrap the AdCP payload from a protocol-level response. Both the\n * capability-priming flow and the agent-URL-bootstrap discovery chain need\n * to peel a `get_adcp_capabilities` response back to its plain-object body\n * regardless of which transport delivered it:\n *\n * - MCP `CallToolResult` — `structuredContent` wins when present;\n * otherwise the first `content[].text` chunk is parsed as JSON.\n * - A2A JSON-RPC `SendMessageResponse` — `result` is a `Task` or `Message`\n * with latest structured DataPart data.\n * - Already-unwrapped payload — returned as-is.\n *\n * Lives in its own module so the two callers (`capability-priming` and\n * `agent-resolver`) share one source of truth instead of forking the\n * envelope walk.\n */\n\nimport { getLatestA2ADataPartFromTask } from '../utils/a2a-artifacts';\n\nexport function unwrapProtocolResponse(response: unknown): unknown {\n if (!response || typeof response !== 'object') return response;\n const r = response as Record<string, unknown>;\n\n if (r.structuredContent && typeof r.structuredContent === 'object') return r.structuredContent;\n\n const content = r.content;\n if (Array.isArray(content)) {\n for (const chunk of content) {\n if (chunk && typeof chunk === 'object' && typeof (chunk as { text?: unknown }).text === 'string') {\n try {\n return JSON.parse((chunk as { text: string }).text);\n } catch {\n // non-JSON text chunk — keep looking\n }\n }\n }\n }\n\n const result = r.result;\n if (result && typeof result === 'object') {\n const artifactData = getLatestA2ADataPartFromTask(result)?.data;\n if (artifactData) {\n return artifactData;\n }\n\n const parts = (result as { parts?: unknown }).parts;\n const data = findLatestDataPart(parts);\n if (data) return data;\n }\n\n return response;\n}\n\nfunction findLatestDataPart(parts: unknown): unknown {\n if (!Array.isArray(parts)) return undefined;\n for (let i = parts.length - 1; i >= 0; i -= 1) {\n const part = parts[i];\n if (part && typeof part === 'object') {\n const p = part as { kind?: unknown; data?: unknown };\n if (p.kind === 'data' && p.data && typeof p.data === 'object') return p.data;\n }\n }\n return undefined;\n}\n"],"mappings":"AAiBA,SAAS,oCAAoC;AAEtC,SAAS,uBAAuB,UAA4B;AACjE,MAAI,CAAC,YAAY,OAAO,aAAa,SAAU,QAAO;AACtD,QAAM,IAAI;AAEV,MAAI,EAAE,qBAAqB,OAAO,EAAE,sBAAsB,SAAU,QAAO,EAAE;AAE7E,QAAM,UAAU,EAAE;AAClB,MAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,eAAW,SAAS,SAAS;AAC3B,UAAI,SAAS,OAAO,UAAU,YAAY,OAAQ,MAA6B,SAAS,UAAU;AAChG,YAAI;AACF,iBAAO,KAAK,MAAO,MAA2B,IAAI;AAAA,QACpD,QAAQ;AAAA,QAER;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,EAAE;AACjB,MAAI,UAAU,OAAO,WAAW,UAAU;AACxC,UAAM,eAAe,6BAA6B,MAAM,GAAG;AAC3D,QAAI,cAAc;AAChB,aAAO;AAAA,IACT;AAEA,UAAM,QAAS,OAA+B;AAC9C,UAAM,OAAO,mBAAmB,KAAK;AACrC,QAAI,KAAM,QAAO;AAAA,EACnB;AAEA,SAAO;AACT;AAEA,SAAS,mBAAmB,OAAyB;AACnD,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,WAAS,IAAI,MAAM,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG;AAC7C,UAAM,OAAO,MAAM,CAAC;AACpB,QAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,YAAM,IAAI;AACV,UAAI,EAAE,SAAS,UAAU,EAAE,QAAQ,OAAO,EAAE,SAAS,SAAU,QAAO,EAAE;AAAA,IAC1E;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
//# sourceMappingURL=provider.mjs.map
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
import { maybeWarnOnSharedRedisPrefix } from "../utils/redis-default-prefix-warn.mjs";
|
|
8
2
|
const DEFAULT_KEY_PREFIX = "adcp:replay:";
|
|
9
3
|
const DEFAULT_CAP = 1e5;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/signing/redis-replay-store.ts"],"sourcesContent":["/**\n * Redis-backed `ReplayStore` for distributed AdCP verifier deployments.\n *\n * Sister of `PostgresReplayStore`. The spec comment on `ReplayStore.insert`\n * (`src/lib/signing/replay.ts`) literally names this as a canonical\n * implementation: \"Multi-replica adopters writing Redis / Postgres\n * stores MUST implement this as a single atomic operation (e.g. Redis\n * `SET NX EX`, Postgres `INSERT ... ON CONFLICT DO NOTHING RETURNING`,\n * a `WATCH/MULTI/EXEC` transaction).\"\n *\n * **Data model.** One Redis sorted set per `(keyid, scope)` pair. The\n * sorted-set members are nonces; the score is `expiresAt` (unix epoch\n * seconds). Sorted sets give us all three primitives the `ReplayStore`\n * interface needs in single Redis commands:\n *\n * - `has(nonce)` → `ZSCORE` + JS-side `score > now` check.\n * - `isCapHit()` → `ZCOUNT key (now +inf` (active nonces).\n * - `insert()` → a Lua script that runs `ZREMRANGEBYSCORE -inf now`\n * (drop expired) → `ZSCORE` (replay check) → `ZCARD` (cap check) →\n * `ZADD` (insert) + `PEXPIREAT` (extend set's own TTL) atomically.\n *\n * **No sweeper needed.** Expired nonces are dropped at the start of\n * every `insert` (the `ZREMRANGEBYSCORE` step), and Redis evicts the\n * entire sorted set when its `PEXPIREAT` lapses if no further inserts\n * arrive. The Postgres backend's `sweepExpiredReplays` cron has no\n * Redis equivalent — eviction is automatic.\n *\n * **Atomicity.** The Lua script runs as a single uninterruptable\n * operation against the Redis server (Redis is single-threaded; Lua\n * blocks everything else for the script duration). Two concurrent\n * `insert` calls with the same `(keyid, scope, nonce)` are guaranteed\n * to see exactly one `'ok'` and one `'replayed'` — matches the\n * `InMemoryReplayStore` semantics the spec mandates.\n *\n * **Redis memory policy — set this on the deployment.** Each\n * `(keyid, scope)` sorted set is capped (default 100k nonces). A\n * hostile signer with a valid keyid + scope can fill the set to its\n * cap; legitimate fan-out (many sibling verifiers, many scopes) grows\n * the number of sets. Configure your Redis with:\n *\n * - **`maxmemory-policy volatile-lru`** (recommended) — evicts only\n * TTL'd keys. All sets this store writes carry `PEXPIREAT`, so\n * they're all evictable. Safe on a shared Redis instance.\n * - **`maxmemory-policy allkeys-lru`** — only on a dedicated db.\n * Otherwise evicts unrelated app keys.\n * - **`maxmemory-policy noeviction`** (Redis default) — fail-closed:\n * writes start erroring at the memory limit; the verifier will\n * throw and the middleware will return 500 (replay protection\n * stays intact — never fail-open). Operationally noisy but\n * never serves stale replay decisions.\n *\n * Eviction-on-replay implication: if `volatile-lru` evicts a sorted\n * set before its members would have expired naturally, an attacker's\n * later replay of one of those nonces sees a fresh set with no prior\n * entry, returns `'ok'`. The cap is the primary defense; memory-policy\n * eviction is a secondary recovery mode. Size Redis to keep the\n * working set in memory under normal traffic and treat eviction as\n * pressure to scale up, not as a feature.\n *\n * @example\n * ```typescript\n * import { createClient } from 'redis';\n * import { RedisReplayStore } from '@adcp/sdk/signing/server';\n *\n * const client = createClient({ url: process.env.REDIS_URL });\n * client.on('error', (err) => console.error('redis error', err));\n * await client.connect();\n *\n * const replayStore = new RedisReplayStore(client);\n *\n * app.use(createExpressVerifier({\n * capability: { ... },\n * jwks,\n * replayStore, // <-- shared across instances\n * resolveOperation: mcpToolNameResolver,\n * }));\n * ```\n */\n\n// `import type` is erased at emit — `redis` stays an optional peer dep.\nimport type { RedisClientType } from 'redis';\nimport type { ReplayInsertResult, ReplayStore } from './replay';\nimport { maybeWarnOnSharedRedisPrefix } from '../utils/redis-default-prefix-warn';\n\n/**\n * Escape-hatch interface for adopters not using the official `redis`\n * client (node-redis v4/v5) — `ioredis`, Upstash, test doubles.\n * Mirrors the methods this store calls.\n *\n * **Footgun: `ioredis.zscore` returns `Promise<string | null>`**, not\n * `Promise<number | null>` like node-redis. The `Number(v)` coercion\n * in the adapter is **mandatory** — without it, the `score > now`\n * comparison inside `has()` becomes a string-vs-number compare that's\n * silently wrong near unix-second boundaries (JS coerces the number\n * to a string for `>`, producing lexicographic ordering — replay\n * decisions break). Test doubles writing their own `zScore` MUST\n * also return `number | null`.\n *\n * @example ioredis adapter\n * ```typescript\n * import Redis from 'ioredis';\n * const ioredis = new Redis(process.env.REDIS_URL!);\n *\n * const client: ReplayRedisLikeClient = {\n * eval: (script, opts) =>\n * ioredis.eval(script, opts.keys.length, ...opts.keys, ...opts.arguments),\n * // Number() coercion is REQUIRED — ioredis.zscore returns string|null.\n * zScore: (k, m) => ioredis.zscore(k, m).then(v => (v === null ? null : Number(v))),\n * zCount: (k, min, max) => ioredis.zcount(k, min, max),\n * ping: () => ioredis.ping(),\n * };\n * ```\n */\nexport interface ReplayRedisLikeClient {\n eval(script: string, options: { keys: string[]; arguments: string[] }): Promise<unknown>;\n zScore(key: string, member: string): Promise<number | null>;\n zCount(key: string, min: number | string, max: number | string): Promise<number>;\n ping(): Promise<string>;\n}\n\n/**\n * Accepted client shape: either a real node-redis client (the typical\n * path — pass `createClient(...)` straight in) or an adapter\n * conforming to `ReplayRedisLikeClient`.\n */\nexport type ReplayRedisBackendClient = RedisClientType<any, any, any> | ReplayRedisLikeClient;\n\nconst DEFAULT_KEY_PREFIX = 'adcp:replay:';\nconst DEFAULT_CAP = 100_000;\n/**\n * Extra seconds added to the sorted-set's own `PEXPIREAT` past the\n * latest entry's `expiresAt`. Without this, a sorted set whose last\n * entry just expired would be evicted by Redis between `insert` calls,\n * losing the cap-counter accounting briefly. 1 hour is conservative —\n * memory-cheap because the set is also empty (`ZREMRANGEBYSCORE` ran\n * before the eventual eviction) and short enough that abandoned\n * `(keyid, scope)` tuples don't leak indefinitely.\n */\nconst DEFAULT_SET_TTL_GRACE_SECONDS = 3600;\n\nexport interface RedisReplayStoreOptions {\n /**\n * Key prefix prepended to every `(keyid, scope)` Redis key. Defaults\n * to `\"adcp:replay:\"`.\n *\n * **Sharing a Redis db across deployments? Override this.** The\n * default is fine for a dedicated Redis (or db index). Two verifier\n * deployments sharing the same db with the same default prefix can\n * collide on overlapping `keyid`s — set a deployment-unique prefix\n * or use separate dbs.\n */\n keyPrefix?: string;\n /**\n * Max retained (unexpired) nonces per `(keyid, scope)` pair before\n * `insert` returns `'rate_abuse'`. Mirrors `PostgresReplayStore`'s\n * cap. Defaults to 100,000.\n *\n * Unlike the Postgres backend, the cap check is **strictly atomic**\n * — it runs inside the same Lua script as the insert. Concurrent\n * inserts at `cap - 1` can never both succeed.\n */\n cap?: number;\n /**\n * How many seconds past the latest entry's `expiresAt` to keep the\n * sorted set alive in Redis. Defaults to 3600 (1h). See the\n * `DEFAULT_SET_TTL_GRACE_SECONDS` comment for rationale.\n */\n setTtlGraceSeconds?: number;\n /**\n * Suppress the one-time `console.warn` at construction when the\n * default `keyPrefix` is used against a node-redis client on db 0.\n */\n suppressDefaultPrefixWarning?: boolean;\n}\n\n/**\n * Lua script — atomic replay check, cap check, and insert.\n *\n * KEYS[1] — the sorted-set key for this `(keyid, scope)` pair.\n * ARGV[1] — nonce.\n * ARGV[2] — expiresAt (unix epoch seconds).\n * ARGV[3] — now (unix epoch seconds).\n * ARGV[4] — cap (max retained nonces).\n * ARGV[5] — setTtlGraceSeconds (extra TTL on the sorted set itself).\n *\n * Returns the literal string `'ok'`, `'replayed'`, or `'rate_abuse'`\n * to match the `ReplayInsertResult` enum.\n *\n * Precedence (matches `InMemoryReplayStore` and `PostgresReplayStore`):\n * 1. replay wins,\n * 2. then rate_abuse,\n * 3. then ok.\n *\n * The `ZREMRANGEBYSCORE` step drops entries whose score is `<= now` —\n * matching the pg backend's `expires_at > to_timestamp($4)` \"still\n * valid\" semantic.\n */\nconst INSERT_LUA = `\nlocal key = KEYS[1]\nlocal nonce = ARGV[1]\nlocal expiresAt = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal grace = tonumber(ARGV[5])\n\n-- Drop expired entries first (inclusive: score <= now is expired)\nredis.call('ZREMRANGEBYSCORE', key, '-inf', now)\n\n-- Replay check — only unexpired entries can be found by ZSCORE because\n-- the cleanup above ran.\nif redis.call('ZSCORE', key, nonce) then\n return 'replayed'\nend\n\n-- Cap check (count of unexpired entries — already pruned above)\nif redis.call('ZCARD', key) >= cap then\n return 'rate_abuse'\nend\n\n-- Insert the new nonce.\nredis.call('ZADD', key, expiresAt, nonce)\n\n-- Extend set TTL — but only forward, never backward. A short-lived\n-- insert after a long-lived one must NOT shrink the set's eviction\n-- time below the longest-still-valid nonce's expiry; otherwise the\n-- set would evict early and take still-valid nonces with it (replay\n-- bypass: has() returns false because the key disappeared, and the\n-- attacker's retry of the long-lived nonce is wrongly accepted).\n--\n-- Desired set expiry = max(existing PEXPIREAT, expiresAt + grace).\n-- PTTL returns -1 (no TTL set) or ms-remaining. Since ZADD ran above,\n-- the key exists, so PTTL won't return -2. nowMs is passed from the\n-- SDK side as ARGV[6] — using Redis's TIME command would make the\n-- script non-deterministic for replication.\nlocal desiredExpireAtMs = (expiresAt + grace) * 1000\nlocal currentTtlMs = redis.call('PTTL', key)\nlocal nowMs = tonumber(ARGV[6])\nif currentTtlMs < 0 then\n -- -1: no TTL set. (ZADD just wrote the key so -2 is impossible.)\n redis.call('PEXPIREAT', key, desiredExpireAtMs)\nelse\n local currentExpireAtMs = nowMs + currentTtlMs\n if desiredExpireAtMs > currentExpireAtMs then\n redis.call('PEXPIREAT', key, desiredExpireAtMs)\n end\nend\nreturn 'ok'\n`;\n\n/**\n * Reject non-finite / out-of-range timestamps before they reach Redis.\n * Matches the Postgres backend's defensive guard so a buggy `options.now()`\n * injection can't DoS the verifier with Redis parse errors.\n */\nfunction assertFiniteSeconds(label: string, value: number): void {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {\n throw new TypeError(`RedisReplayStore: ${label} must be a finite non-negative number; received ${value}`);\n }\n}\n\nexport class RedisReplayStore implements ReplayStore {\n private readonly c: ReplayRedisLikeClient;\n private readonly keyPrefix: string;\n private readonly cap: number;\n private readonly setTtlGraceSeconds: number;\n\n constructor(client: ReplayRedisBackendClient, options: RedisReplayStoreOptions = {}) {\n this.c = client as ReplayRedisLikeClient;\n this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;\n this.cap = options.cap ?? DEFAULT_CAP;\n this.setTtlGraceSeconds = options.setTtlGraceSeconds ?? DEFAULT_SET_TTL_GRACE_SECONDS;\n\n if (!Number.isFinite(this.cap) || this.cap <= 0) {\n throw new Error(`RedisReplayStore: cap must be a positive finite number. Got ${this.cap}.`);\n }\n if (!Number.isFinite(this.setTtlGraceSeconds) || this.setTtlGraceSeconds < 0) {\n throw new Error(\n `RedisReplayStore: setTtlGraceSeconds must be a non-negative finite number. Got ${this.setTtlGraceSeconds}.`\n );\n }\n\n maybeWarnOnSharedRedisPrefix({\n client,\n callerKeyPrefix: options.keyPrefix,\n defaultKeyPrefix: DEFAULT_KEY_PREFIX,\n suppress: options.suppressDefaultPrefixWarning,\n backendName: 'RedisReplayStore',\n });\n }\n\n /**\n * Compose the sorted-set Redis key for a `(keyid, scope)` pair.\n * Uses `\\x1f` (unit separator) between segments — illegal in RFC\n * 7517 JWK kids and in URLs, so no legitimate input collides.\n */\n private redisKey(keyid: string, scope: string): string {\n return `${this.keyPrefix}${keyid}\\x1f${scope}`;\n }\n\n /**\n * Probe — ping Redis. Not part of the `ReplayStore` interface; call\n * before serving traffic if you want a boot-time readiness check.\n */\n async probe(): Promise<void> {\n try {\n await this.c.ping();\n } catch (err) {\n throw new Error(\n `RedisReplayStore probe failed: Redis is unreachable or misconfigured. ` +\n `The verifier would accept signed requests but every replay-cache write would fail. ` +\n `Check REDIS_URL and that the instance is up. See server logs for the underlying cause.`,\n { cause: err }\n );\n }\n }\n\n async has(keyid: string, scope: string, nonce: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const score = await this.c.zScore(this.redisKey(keyid, scope), nonce);\n // Score === expiresAt. An entry is \"present\" only if it's still\n // unexpired (score > now). The Lua script also prunes expired\n // entries on insert, but `has` is called independently and can\n // race a never-since-touched key.\n return score !== null && score > now;\n }\n\n async isCapHit(keyid: string, scope: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n // ZCOUNT with `(now` lower bound is exclusive — only entries\n // strictly in the future count toward the cap. Stale entries\n // beyond their expiresAt don't inflate the count even before the\n // next insert prunes them.\n const active = await this.c.zCount(this.redisKey(keyid, scope), `(${now}`, '+inf');\n return active >= this.cap;\n }\n\n async insert(\n keyid: string,\n scope: string,\n nonce: string,\n ttlSeconds: number,\n now: number\n ): Promise<ReplayInsertResult> {\n assertFiniteSeconds('now', now);\n assertFiniteSeconds('ttlSeconds', ttlSeconds);\n const expiresAt = now + ttlSeconds;\n // `nowMs` (ARGV[6]) is `now * 1000` rather than `Date.now()` so the\n // script's \"extend forward only\" branch uses the SAME clock as the\n // rest of the verifier path (`now` is the verifier-supplied second-\n // precision time, used for prune + replay + cap). Using `Date.now()`\n // here would let scripts disagree with the surrounding logic when\n // tests inject `options.now()`.\n const result = await this.c.eval(INSERT_LUA, {\n keys: [this.redisKey(keyid, scope)],\n arguments: [\n nonce,\n String(expiresAt),\n String(now),\n String(this.cap),\n String(this.setTtlGraceSeconds),\n String(now * 1000),\n ],\n });\n if (result !== 'ok' && result !== 'replayed' && result !== 'rate_abuse') {\n throw new Error(`RedisReplayStore.insert: Lua script returned unexpected value: ${String(result)}`);\n }\n return result;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/signing/redis-replay-store.ts"],"sourcesContent":["/**\n * Redis-backed `ReplayStore` for distributed AdCP verifier deployments.\n *\n * Sister of `PostgresReplayStore`. The spec comment on `ReplayStore.insert`\n * (`src/lib/signing/replay.ts`) literally names this as a canonical\n * implementation: \"Multi-replica adopters writing Redis / Postgres\n * stores MUST implement this as a single atomic operation (e.g. Redis\n * `SET NX EX`, Postgres `INSERT ... ON CONFLICT DO NOTHING RETURNING`,\n * a `WATCH/MULTI/EXEC` transaction).\"\n *\n * **Data model.** One Redis sorted set per `(keyid, scope)` pair. The\n * sorted-set members are nonces; the score is `expiresAt` (unix epoch\n * seconds). Sorted sets give us all three primitives the `ReplayStore`\n * interface needs in single Redis commands:\n *\n * - `has(nonce)` → `ZSCORE` + JS-side `score > now` check.\n * - `isCapHit()` → `ZCOUNT key (now +inf` (active nonces).\n * - `insert()` → a Lua script that runs `ZREMRANGEBYSCORE -inf now`\n * (drop expired) → `ZSCORE` (replay check) → `ZCARD` (cap check) →\n * `ZADD` (insert) + `PEXPIREAT` (extend set's own TTL) atomically.\n *\n * **No sweeper needed.** Expired nonces are dropped at the start of\n * every `insert` (the `ZREMRANGEBYSCORE` step), and Redis evicts the\n * entire sorted set when its `PEXPIREAT` lapses if no further inserts\n * arrive. The Postgres backend's `sweepExpiredReplays` cron has no\n * Redis equivalent — eviction is automatic.\n *\n * **Atomicity.** The Lua script runs as a single uninterruptable\n * operation against the Redis server (Redis is single-threaded; Lua\n * blocks everything else for the script duration). Two concurrent\n * `insert` calls with the same `(keyid, scope, nonce)` are guaranteed\n * to see exactly one `'ok'` and one `'replayed'` — matches the\n * `InMemoryReplayStore` semantics the spec mandates.\n *\n * **Redis memory policy — set this on the deployment.** Each\n * `(keyid, scope)` sorted set is capped (default 100k nonces). A\n * hostile signer with a valid keyid + scope can fill the set to its\n * cap; legitimate fan-out (many sibling verifiers, many scopes) grows\n * the number of sets. Configure your Redis with:\n *\n * - **`maxmemory-policy volatile-lru`** (recommended) — evicts only\n * TTL'd keys. All sets this store writes carry `PEXPIREAT`, so\n * they're all evictable. Safe on a shared Redis instance.\n * - **`maxmemory-policy allkeys-lru`** — only on a dedicated db.\n * Otherwise evicts unrelated app keys.\n * - **`maxmemory-policy noeviction`** (Redis default) — fail-closed:\n * writes start erroring at the memory limit; the verifier will\n * throw and the middleware will return 500 (replay protection\n * stays intact — never fail-open). Operationally noisy but\n * never serves stale replay decisions.\n *\n * Eviction-on-replay implication: if `volatile-lru` evicts a sorted\n * set before its members would have expired naturally, an attacker's\n * later replay of one of those nonces sees a fresh set with no prior\n * entry, returns `'ok'`. The cap is the primary defense; memory-policy\n * eviction is a secondary recovery mode. Size Redis to keep the\n * working set in memory under normal traffic and treat eviction as\n * pressure to scale up, not as a feature.\n *\n * @example\n * ```typescript\n * import { createClient } from 'redis';\n * import { RedisReplayStore } from '@adcp/sdk/signing/server';\n *\n * const client = createClient({ url: process.env.REDIS_URL });\n * client.on('error', (err) => console.error('redis error', err));\n * await client.connect();\n *\n * const replayStore = new RedisReplayStore(client);\n *\n * app.use(createExpressVerifier({\n * capability: { ... },\n * jwks,\n * replayStore, // <-- shared across instances\n * resolveOperation: mcpToolNameResolver,\n * }));\n * ```\n */\n\n// `import type` is erased at emit — `redis` stays an optional peer dep.\nimport type { RedisClientType } from 'redis';\nimport type { ReplayInsertResult, ReplayStore } from './replay';\nimport { maybeWarnOnSharedRedisPrefix } from '../utils/redis-default-prefix-warn';\n\n/**\n * Escape-hatch interface for adopters not using the official `redis`\n * client (node-redis v4/v5) — `ioredis`, Upstash, test doubles.\n * Mirrors the methods this store calls.\n *\n * **Footgun: `ioredis.zscore` returns `Promise<string | null>`**, not\n * `Promise<number | null>` like node-redis. The `Number(v)` coercion\n * in the adapter is **mandatory** — without it, the `score > now`\n * comparison inside `has()` becomes a string-vs-number compare that's\n * silently wrong near unix-second boundaries (JS coerces the number\n * to a string for `>`, producing lexicographic ordering — replay\n * decisions break). Test doubles writing their own `zScore` MUST\n * also return `number | null`.\n *\n * @example ioredis adapter\n * ```typescript\n * import Redis from 'ioredis';\n * const ioredis = new Redis(process.env.REDIS_URL!);\n *\n * const client: ReplayRedisLikeClient = {\n * eval: (script, opts) =>\n * ioredis.eval(script, opts.keys.length, ...opts.keys, ...opts.arguments),\n * // Number() coercion is REQUIRED — ioredis.zscore returns string|null.\n * zScore: (k, m) => ioredis.zscore(k, m).then(v => (v === null ? null : Number(v))),\n * zCount: (k, min, max) => ioredis.zcount(k, min, max),\n * ping: () => ioredis.ping(),\n * };\n * ```\n */\nexport interface ReplayRedisLikeClient {\n eval(script: string, options: { keys: string[]; arguments: string[] }): Promise<unknown>;\n zScore(key: string, member: string): Promise<number | null>;\n zCount(key: string, min: number | string, max: number | string): Promise<number>;\n ping(): Promise<string>;\n}\n\n/**\n * Accepted client shape: either a real node-redis client (the typical\n * path — pass `createClient(...)` straight in) or an adapter\n * conforming to `ReplayRedisLikeClient`.\n */\nexport type ReplayRedisBackendClient = RedisClientType<any, any, any> | ReplayRedisLikeClient;\n\nconst DEFAULT_KEY_PREFIX = 'adcp:replay:';\nconst DEFAULT_CAP = 100_000;\n/**\n * Extra seconds added to the sorted-set's own `PEXPIREAT` past the\n * latest entry's `expiresAt`. Without this, a sorted set whose last\n * entry just expired would be evicted by Redis between `insert` calls,\n * losing the cap-counter accounting briefly. 1 hour is conservative —\n * memory-cheap because the set is also empty (`ZREMRANGEBYSCORE` ran\n * before the eventual eviction) and short enough that abandoned\n * `(keyid, scope)` tuples don't leak indefinitely.\n */\nconst DEFAULT_SET_TTL_GRACE_SECONDS = 3600;\n\nexport interface RedisReplayStoreOptions {\n /**\n * Key prefix prepended to every `(keyid, scope)` Redis key. Defaults\n * to `\"adcp:replay:\"`.\n *\n * **Sharing a Redis db across deployments? Override this.** The\n * default is fine for a dedicated Redis (or db index). Two verifier\n * deployments sharing the same db with the same default prefix can\n * collide on overlapping `keyid`s — set a deployment-unique prefix\n * or use separate dbs.\n */\n keyPrefix?: string;\n /**\n * Max retained (unexpired) nonces per `(keyid, scope)` pair before\n * `insert` returns `'rate_abuse'`. Mirrors `PostgresReplayStore`'s\n * cap. Defaults to 100,000.\n *\n * Unlike the Postgres backend, the cap check is **strictly atomic**\n * — it runs inside the same Lua script as the insert. Concurrent\n * inserts at `cap - 1` can never both succeed.\n */\n cap?: number;\n /**\n * How many seconds past the latest entry's `expiresAt` to keep the\n * sorted set alive in Redis. Defaults to 3600 (1h). See the\n * `DEFAULT_SET_TTL_GRACE_SECONDS` comment for rationale.\n */\n setTtlGraceSeconds?: number;\n /**\n * Suppress the one-time `console.warn` at construction when the\n * default `keyPrefix` is used against a node-redis client on db 0.\n */\n suppressDefaultPrefixWarning?: boolean;\n}\n\n/**\n * Lua script — atomic replay check, cap check, and insert.\n *\n * KEYS[1] — the sorted-set key for this `(keyid, scope)` pair.\n * ARGV[1] — nonce.\n * ARGV[2] — expiresAt (unix epoch seconds).\n * ARGV[3] — now (unix epoch seconds).\n * ARGV[4] — cap (max retained nonces).\n * ARGV[5] — setTtlGraceSeconds (extra TTL on the sorted set itself).\n *\n * Returns the literal string `'ok'`, `'replayed'`, or `'rate_abuse'`\n * to match the `ReplayInsertResult` enum.\n *\n * Precedence (matches `InMemoryReplayStore` and `PostgresReplayStore`):\n * 1. replay wins,\n * 2. then rate_abuse,\n * 3. then ok.\n *\n * The `ZREMRANGEBYSCORE` step drops entries whose score is `<= now` —\n * matching the pg backend's `expires_at > to_timestamp($4)` \"still\n * valid\" semantic.\n */\nconst INSERT_LUA = `\nlocal key = KEYS[1]\nlocal nonce = ARGV[1]\nlocal expiresAt = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal cap = tonumber(ARGV[4])\nlocal grace = tonumber(ARGV[5])\n\n-- Drop expired entries first (inclusive: score <= now is expired)\nredis.call('ZREMRANGEBYSCORE', key, '-inf', now)\n\n-- Replay check — only unexpired entries can be found by ZSCORE because\n-- the cleanup above ran.\nif redis.call('ZSCORE', key, nonce) then\n return 'replayed'\nend\n\n-- Cap check (count of unexpired entries — already pruned above)\nif redis.call('ZCARD', key) >= cap then\n return 'rate_abuse'\nend\n\n-- Insert the new nonce.\nredis.call('ZADD', key, expiresAt, nonce)\n\n-- Extend set TTL — but only forward, never backward. A short-lived\n-- insert after a long-lived one must NOT shrink the set's eviction\n-- time below the longest-still-valid nonce's expiry; otherwise the\n-- set would evict early and take still-valid nonces with it (replay\n-- bypass: has() returns false because the key disappeared, and the\n-- attacker's retry of the long-lived nonce is wrongly accepted).\n--\n-- Desired set expiry = max(existing PEXPIREAT, expiresAt + grace).\n-- PTTL returns -1 (no TTL set) or ms-remaining. Since ZADD ran above,\n-- the key exists, so PTTL won't return -2. nowMs is passed from the\n-- SDK side as ARGV[6] — using Redis's TIME command would make the\n-- script non-deterministic for replication.\nlocal desiredExpireAtMs = (expiresAt + grace) * 1000\nlocal currentTtlMs = redis.call('PTTL', key)\nlocal nowMs = tonumber(ARGV[6])\nif currentTtlMs < 0 then\n -- -1: no TTL set. (ZADD just wrote the key so -2 is impossible.)\n redis.call('PEXPIREAT', key, desiredExpireAtMs)\nelse\n local currentExpireAtMs = nowMs + currentTtlMs\n if desiredExpireAtMs > currentExpireAtMs then\n redis.call('PEXPIREAT', key, desiredExpireAtMs)\n end\nend\nreturn 'ok'\n`;\n\n/**\n * Reject non-finite / out-of-range timestamps before they reach Redis.\n * Matches the Postgres backend's defensive guard so a buggy `options.now()`\n * injection can't DoS the verifier with Redis parse errors.\n */\nfunction assertFiniteSeconds(label: string, value: number): void {\n if (!Number.isFinite(value) || value < 0 || value > Number.MAX_SAFE_INTEGER) {\n throw new TypeError(`RedisReplayStore: ${label} must be a finite non-negative number; received ${value}`);\n }\n}\n\nexport class RedisReplayStore implements ReplayStore {\n private readonly c: ReplayRedisLikeClient;\n private readonly keyPrefix: string;\n private readonly cap: number;\n private readonly setTtlGraceSeconds: number;\n\n constructor(client: ReplayRedisBackendClient, options: RedisReplayStoreOptions = {}) {\n this.c = client as ReplayRedisLikeClient;\n this.keyPrefix = options.keyPrefix ?? DEFAULT_KEY_PREFIX;\n this.cap = options.cap ?? DEFAULT_CAP;\n this.setTtlGraceSeconds = options.setTtlGraceSeconds ?? DEFAULT_SET_TTL_GRACE_SECONDS;\n\n if (!Number.isFinite(this.cap) || this.cap <= 0) {\n throw new Error(`RedisReplayStore: cap must be a positive finite number. Got ${this.cap}.`);\n }\n if (!Number.isFinite(this.setTtlGraceSeconds) || this.setTtlGraceSeconds < 0) {\n throw new Error(\n `RedisReplayStore: setTtlGraceSeconds must be a non-negative finite number. Got ${this.setTtlGraceSeconds}.`\n );\n }\n\n maybeWarnOnSharedRedisPrefix({\n client,\n callerKeyPrefix: options.keyPrefix,\n defaultKeyPrefix: DEFAULT_KEY_PREFIX,\n suppress: options.suppressDefaultPrefixWarning,\n backendName: 'RedisReplayStore',\n });\n }\n\n /**\n * Compose the sorted-set Redis key for a `(keyid, scope)` pair.\n * Uses `\\x1f` (unit separator) between segments — illegal in RFC\n * 7517 JWK kids and in URLs, so no legitimate input collides.\n */\n private redisKey(keyid: string, scope: string): string {\n return `${this.keyPrefix}${keyid}\\x1f${scope}`;\n }\n\n /**\n * Probe — ping Redis. Not part of the `ReplayStore` interface; call\n * before serving traffic if you want a boot-time readiness check.\n */\n async probe(): Promise<void> {\n try {\n await this.c.ping();\n } catch (err) {\n throw new Error(\n `RedisReplayStore probe failed: Redis is unreachable or misconfigured. ` +\n `The verifier would accept signed requests but every replay-cache write would fail. ` +\n `Check REDIS_URL and that the instance is up. See server logs for the underlying cause.`,\n { cause: err }\n );\n }\n }\n\n async has(keyid: string, scope: string, nonce: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n const score = await this.c.zScore(this.redisKey(keyid, scope), nonce);\n // Score === expiresAt. An entry is \"present\" only if it's still\n // unexpired (score > now). The Lua script also prunes expired\n // entries on insert, but `has` is called independently and can\n // race a never-since-touched key.\n return score !== null && score > now;\n }\n\n async isCapHit(keyid: string, scope: string, now: number): Promise<boolean> {\n assertFiniteSeconds('now', now);\n // ZCOUNT with `(now` lower bound is exclusive — only entries\n // strictly in the future count toward the cap. Stale entries\n // beyond their expiresAt don't inflate the count even before the\n // next insert prunes them.\n const active = await this.c.zCount(this.redisKey(keyid, scope), `(${now}`, '+inf');\n return active >= this.cap;\n }\n\n async insert(\n keyid: string,\n scope: string,\n nonce: string,\n ttlSeconds: number,\n now: number\n ): Promise<ReplayInsertResult> {\n assertFiniteSeconds('now', now);\n assertFiniteSeconds('ttlSeconds', ttlSeconds);\n const expiresAt = now + ttlSeconds;\n // `nowMs` (ARGV[6]) is `now * 1000` rather than `Date.now()` so the\n // script's \"extend forward only\" branch uses the SAME clock as the\n // rest of the verifier path (`now` is the verifier-supplied second-\n // precision time, used for prune + replay + cap). Using `Date.now()`\n // here would let scripts disagree with the surrounding logic when\n // tests inject `options.now()`.\n const result = await this.c.eval(INSERT_LUA, {\n keys: [this.redisKey(keyid, scope)],\n arguments: [\n nonce,\n String(expiresAt),\n String(now),\n String(this.cap),\n String(this.setTtlGraceSeconds),\n String(now * 1000),\n ],\n });\n if (result !== 'ok' && result !== 'replayed' && result !== 'rate_abuse') {\n throw new Error(`RedisReplayStore.insert: Lua script returned unexpected value: ${String(result)}`);\n }\n return result;\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
import { fileURLToPath as __adcpFileURLToPath } from 'node:url';
|
|
2
|
-
import { dirname as __adcpDirname } from 'node:path';
|
|
3
|
-
import { createRequire as __adcpCreateRequire } from 'node:module';
|
|
4
|
-
const __filename = __adcpFileURLToPath(import.meta.url);
|
|
5
|
-
const __dirname = __adcpDirname(__filename);
|
|
6
|
-
const require = __adcpCreateRequire(import.meta.url);
|
|
7
1
|
function compositeKey(keyid, scope) {
|
|
8
2
|
return `${keyid}\0${scope}`;
|
|
9
3
|
}
|