@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/substitution/observer/alignment.ts"],"sourcesContent":["/**\n * Macro-position alignment: map each observed tracker URL back to the\n * template position its macro occupied, so the verifier can compare\n * observed bytes against the expected encoding at exactly that slot.\n *\n * Implements the normative algorithm from the test-kit contract:\n *\n * 1. Tokenize the template from its raw string so literal AdCP macros\n * (`{SKU}`) survive — WHATWG URL percent-encodes `{` in paths, which\n * would mask the macro position.\n * 2. Parse observed URLs with WHATWG URL (they are properly encoded).\n * 3. Align observed query pairs by decoded key (positionally among\n * pairs sharing the same key); align path segments positionally.\n * 4. Emit BindingMatch entries the verifier can assert on.\n */\n\nimport type { BindingMatch, CatalogBinding, CatalogMacroVector, TrackerUrlRecord } from '../types';\nimport { encodeUnreserved } from '../rfc3986';\nimport { getCatalogMacroVector } from '../vectors';\n\ninterface ParsedQueryPair {\n /** Decoded key per WHATWG URLSearchParams semantics. */\n key: string;\n /** Raw value as it appears in the query string (pre-decode). */\n raw_value: string;\n /** Ordinal position in the original query string (0-indexed). */\n index: number;\n}\n\ninterface TemplateLayout {\n origin: string;\n path_segments: string[];\n query_pairs: ParsedQueryPair[];\n href: string;\n}\n\ninterface ObservedLayout {\n url: URL;\n path_segments: string[];\n query_pairs: ParsedQueryPair[];\n href: string;\n}\n\nconst MACRO_SEGMENT_RE = /\\{[A-Z][A-Z0-9_]*\\}/;\n\n/**\n * Resolve the fixture vector or inline overrides for a binding. The\n * `expected_encoded` field is the per-position encoded substring\n * (e.g., `00013%26cmd%3Ddrop`), NOT the full fixture URL.\n *\n * Canonical vectors (referenced by `vector_name` and present in\n * {@link CATALOG_MACRO_VECTORS}) resolve from the fixture. Custom\n * bindings MUST supply BOTH `raw_value` and `expected_encoded` — the\n * runner MUST NOT produce the oracle by calling its own encoder on\n * the seller's input (that makes the test tautological). A custom\n * binding missing `expected_encoded` is dropped; the caller surfaces\n * it as `substitution_binding_missing`.\n */\nfunction resolveBinding(\n binding: CatalogBinding\n): { raw_value: string; expected_encoded: string; is_custom_vector: boolean } | null {\n let vector: CatalogMacroVector | undefined;\n if (binding.vector_name) vector = getCatalogMacroVector(binding.vector_name);\n\n if (vector) {\n const raw_value = binding.raw_value ?? vector.value;\n // The fixture's `expected` is a full URL; the runner needs the\n // encoded substring at the macro position. `encodeUnreserved` is\n // the same algorithm the fixture's expected was produced by — the\n // parity test in `substitution-fixture-parity.test.js` locks that\n // invariant, so this is NOT tautological for canonical vectors.\n const expected_encoded = binding.expected_encoded ?? encodeUnreserved(vector.value);\n return { raw_value, expected_encoded, is_custom_vector: false };\n }\n\n // No canonical vector — caller must supply both raw_value AND\n // expected_encoded, otherwise the binding is unresolvable.\n if (binding.raw_value === undefined || binding.expected_encoded === undefined) {\n return null;\n }\n return {\n raw_value: binding.raw_value,\n expected_encoded: binding.expected_encoded,\n is_custom_vector: true,\n };\n}\n\n/**\n * Match each observed record to the template positions its bindings\n * occupy. A binding may match multiple records when the observed\n * preview contains repetitions (e.g., impression + click pixels\n * sharing a SKU).\n */\nexport function matchBindings(\n records: readonly TrackerUrlRecord[],\n template: URL | string,\n bindings: readonly CatalogBinding[]\n): BindingMatch[] {\n const templateLayout = parseTemplate(template);\n const matches: BindingMatch[] = [];\n\n for (const binding of bindings) {\n const resolved = resolveBinding(binding);\n if (!resolved) continue;\n\n const positions = findMacroPositions(templateLayout, binding.macro);\n if (positions.length === 0) continue;\n\n for (const record of sameTemplateRecords(records, templateLayout)) {\n const observed = parseObserved(record.url);\n for (const position of positions) {\n const observed_value = resolveObservedValue(observed, position);\n if (observed_value === null) continue;\n matches.push({\n binding,\n raw_value: resolved.raw_value,\n expected_encoded: resolved.expected_encoded,\n observed_url: record.url,\n record,\n position,\n observed_value,\n is_custom_vector: resolved.is_custom_vector,\n });\n }\n }\n }\n\n return matches;\n}\n\n/**\n * Tokenize a template from its raw string. Preserves literal `{MACRO}`\n * sequences in path and query. Throws if the template is not an\n * absolute URL. Avoids regex parsing of untrusted input to sidestep\n * any polynomial-backtracking risk on adversarial templates.\n */\nfunction parseTemplate(template: URL | string): TemplateLayout {\n const raw = typeof template === 'string' ? template : template.href;\n const parts = splitAbsoluteUrl(raw);\n if (!parts) throw new Error(`Template is not an absolute URL: ${raw}`);\n const { origin, path } = parts;\n const search = parts.search;\n\n const path_segments = path === '' || path === '/' ? [] : path.replace(/^\\//, '').split('/');\n\n const query_pairs: ParsedQueryPair[] = search\n ? search.split('&').map((segment, index) => {\n const eq = segment.indexOf('=');\n const keyRaw = eq === -1 ? segment : segment.slice(0, eq);\n const valueRaw = eq === -1 ? '' : segment.slice(eq + 1);\n return { key: safeDecode(keyRaw), raw_value: valueRaw, index };\n })\n : [];\n\n // Origin reported as WHATWG URL's `origin` shape so comparison with\n // observed URLs (which are WHATWG-parsed) is apples-to-apples.\n const normalizedOrigin = normalizeOrigin(origin);\n\n return {\n origin: normalizedOrigin,\n path_segments,\n query_pairs,\n href: raw,\n };\n}\n\nfunction normalizeOrigin(scheme_and_authority: string): string {\n // Handle `scheme://authority` form.\n try {\n const u = new URL(scheme_and_authority + '/');\n return u.origin;\n } catch {\n return scheme_and_authority;\n }\n}\n\n/**\n * Linear-time absolute-URL splitter. Returns the scheme+authority\n * prefix, the raw path, and the raw search (without leading `?`), or\n * `null` if the input isn't shaped `scheme://authority...`. Avoids\n * regex on untrusted input so adversarial templates can't trigger\n * polynomial backtracking.\n */\nfunction splitAbsoluteUrl(raw: string): { origin: string; path: string; search: string } | null {\n // Locate the `://` delimiter. Scheme MUST be non-empty and start with\n // a letter; we enforce that minimum here without scanning the whole\n // scheme for validity (WHATWG URL would accept most absolute forms).\n const schemeEnd = raw.indexOf('://');\n if (schemeEnd <= 0) return null;\n const firstChar = raw.charCodeAt(0);\n const isAlpha = (firstChar >= 0x41 && firstChar <= 0x5a) || (firstChar >= 0x61 && firstChar <= 0x7a);\n if (!isAlpha) return null;\n\n const afterScheme = schemeEnd + 3;\n // Authority ends at the first `/`, `?`, or `#` — or end of string.\n let authorityEnd = raw.length;\n for (let i = afterScheme; i < raw.length; i++) {\n const c = raw.charCodeAt(i);\n if (c === 0x2f /* / */ || c === 0x3f /* ? */ || c === 0x23 /* # */) {\n authorityEnd = i;\n break;\n }\n }\n const origin = raw.slice(0, authorityEnd);\n\n // Path runs from authorityEnd up to `?`, `#`, or end of string.\n let pathEnd = raw.length;\n for (let i = authorityEnd; i < raw.length; i++) {\n const c = raw.charCodeAt(i);\n if (c === 0x3f /* ? */ || c === 0x23 /* # */) {\n pathEnd = i;\n break;\n }\n }\n const path = raw.slice(authorityEnd, pathEnd);\n\n // Search runs from `?` up to `#` or end. No leading `?` in the returned form.\n let search = '';\n if (raw.charCodeAt(pathEnd) === 0x3f) {\n let searchEnd = raw.length;\n for (let i = pathEnd + 1; i < raw.length; i++) {\n if (raw.charCodeAt(i) === 0x23 /* # */) {\n searchEnd = i;\n break;\n }\n }\n search = raw.slice(pathEnd + 1, searchEnd);\n }\n\n return { origin, path, search };\n}\n\nfunction parseObserved(url: URL): ObservedLayout {\n const path_segments = url.pathname === '/' ? [] : url.pathname.replace(/^\\//, '').split('/');\n const rawSearch = url.search.startsWith('?') ? url.search.slice(1) : url.search;\n const query_pairs: ParsedQueryPair[] = rawSearch\n ? rawSearch.split('&').map((segment, index) => {\n const eq = segment.indexOf('=');\n const keyRaw = eq === -1 ? segment : segment.slice(0, eq);\n const valueRaw = eq === -1 ? '' : segment.slice(eq + 1);\n return { key: safeDecode(keyRaw), raw_value: valueRaw, index };\n })\n : [];\n return { url, path_segments, query_pairs, href: url.href };\n}\n\n/**\n * Narrow observed records to those that could plausibly be substituted\n * forms of `template`: same origin, same number of path segments,\n * literal segments matching where the template has no macro, and every\n * template query key present in the observed URL.\n *\n * This is a same-shape filter, not an alignment oracle — two templates\n * with identical path+key shapes but different semantics will confuse\n * it. Runners SHOULD invoke `match_bindings` once per `(template,\n * preview)` pair rather than pass all records from a multi-template\n * preview to a single call.\n */\nfunction sameTemplateRecords(records: readonly TrackerUrlRecord[], template: TemplateLayout): TrackerUrlRecord[] {\n const templateKeys = new Set(template.query_pairs.map(p => p.key));\n const templateSegCount = template.path_segments.length;\n return records.filter(r => {\n if (r.url.origin !== template.origin) return false;\n const observed = parseObserved(r.url);\n if (observed.path_segments.length !== templateSegCount) return false;\n for (let i = 0; i < templateSegCount; i++) {\n const seg = template.path_segments[i] ?? '';\n const obs = observed.path_segments[i] ?? '';\n if (!MACRO_SEGMENT_RE.test(seg) && seg !== obs) return false;\n }\n for (const key of templateKeys) {\n if (!observed.query_pairs.some(p => p.key === key)) return false;\n }\n return true;\n });\n}\n\n/**\n * Find the template positions where `macro` appears. Whole-value\n * matches win first (rare; legal for `<a href=\"{CLICK}\">`). Otherwise\n * emit every query-value and path-segment match in scan order.\n */\nfunction findMacroPositions(layout: TemplateLayout, macro: string): BindingMatch['position'][] {\n const out: BindingMatch['position'][] = [];\n\n if (layout.href === macro) {\n out.push({ kind: 'href_whole_value' });\n }\n\n for (const pair of layout.query_pairs) {\n if (pair.raw_value === macro) {\n out.push({ kind: 'query', key: pair.key, index: pair.index });\n }\n }\n\n layout.path_segments.forEach((seg, index) => {\n if (seg === macro) {\n out.push({ kind: 'path', index });\n }\n });\n\n return out;\n}\n\nfunction resolveObservedValue(layout: ObservedLayout, position: BindingMatch['position']): string | null {\n if (position.kind === 'href_whole_value') {\n return layout.href;\n }\n if (position.kind === 'path') {\n return layout.path_segments[position.index] ?? null;\n }\n const matching = layout.query_pairs.filter(p => p.key === position.key);\n return matching[0]?.raw_value ?? null;\n}\n\nfunction safeDecode(s: string): string {\n try {\n return decodeURIComponent(s.replace(/\\+/g, ' '));\n } catch {\n return s;\n }\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/substitution/observer/alignment.ts"],"sourcesContent":["/**\n * Macro-position alignment: map each observed tracker URL back to the\n * template position its macro occupied, so the verifier can compare\n * observed bytes against the expected encoding at exactly that slot.\n *\n * Implements the normative algorithm from the test-kit contract:\n *\n * 1. Tokenize the template from its raw string so literal AdCP macros\n * (`{SKU}`) survive — WHATWG URL percent-encodes `{` in paths, which\n * would mask the macro position.\n * 2. Parse observed URLs with WHATWG URL (they are properly encoded).\n * 3. Align observed query pairs by decoded key (positionally among\n * pairs sharing the same key); align path segments positionally.\n * 4. Emit BindingMatch entries the verifier can assert on.\n */\n\nimport type { BindingMatch, CatalogBinding, CatalogMacroVector, TrackerUrlRecord } from '../types';\nimport { encodeUnreserved } from '../rfc3986';\nimport { getCatalogMacroVector } from '../vectors';\n\ninterface ParsedQueryPair {\n /** Decoded key per WHATWG URLSearchParams semantics. */\n key: string;\n /** Raw value as it appears in the query string (pre-decode). */\n raw_value: string;\n /** Ordinal position in the original query string (0-indexed). */\n index: number;\n}\n\ninterface TemplateLayout {\n origin: string;\n path_segments: string[];\n query_pairs: ParsedQueryPair[];\n href: string;\n}\n\ninterface ObservedLayout {\n url: URL;\n path_segments: string[];\n query_pairs: ParsedQueryPair[];\n href: string;\n}\n\nconst MACRO_SEGMENT_RE = /\\{[A-Z][A-Z0-9_]*\\}/;\n\n/**\n * Resolve the fixture vector or inline overrides for a binding. The\n * `expected_encoded` field is the per-position encoded substring\n * (e.g., `00013%26cmd%3Ddrop`), NOT the full fixture URL.\n *\n * Canonical vectors (referenced by `vector_name` and present in\n * {@link CATALOG_MACRO_VECTORS}) resolve from the fixture. Custom\n * bindings MUST supply BOTH `raw_value` and `expected_encoded` — the\n * runner MUST NOT produce the oracle by calling its own encoder on\n * the seller's input (that makes the test tautological). A custom\n * binding missing `expected_encoded` is dropped; the caller surfaces\n * it as `substitution_binding_missing`.\n */\nfunction resolveBinding(\n binding: CatalogBinding\n): { raw_value: string; expected_encoded: string; is_custom_vector: boolean } | null {\n let vector: CatalogMacroVector | undefined;\n if (binding.vector_name) vector = getCatalogMacroVector(binding.vector_name);\n\n if (vector) {\n const raw_value = binding.raw_value ?? vector.value;\n // The fixture's `expected` is a full URL; the runner needs the\n // encoded substring at the macro position. `encodeUnreserved` is\n // the same algorithm the fixture's expected was produced by — the\n // parity test in `substitution-fixture-parity.test.js` locks that\n // invariant, so this is NOT tautological for canonical vectors.\n const expected_encoded = binding.expected_encoded ?? encodeUnreserved(vector.value);\n return { raw_value, expected_encoded, is_custom_vector: false };\n }\n\n // No canonical vector — caller must supply both raw_value AND\n // expected_encoded, otherwise the binding is unresolvable.\n if (binding.raw_value === undefined || binding.expected_encoded === undefined) {\n return null;\n }\n return {\n raw_value: binding.raw_value,\n expected_encoded: binding.expected_encoded,\n is_custom_vector: true,\n };\n}\n\n/**\n * Match each observed record to the template positions its bindings\n * occupy. A binding may match multiple records when the observed\n * preview contains repetitions (e.g., impression + click pixels\n * sharing a SKU).\n */\nexport function matchBindings(\n records: readonly TrackerUrlRecord[],\n template: URL | string,\n bindings: readonly CatalogBinding[]\n): BindingMatch[] {\n const templateLayout = parseTemplate(template);\n const matches: BindingMatch[] = [];\n\n for (const binding of bindings) {\n const resolved = resolveBinding(binding);\n if (!resolved) continue;\n\n const positions = findMacroPositions(templateLayout, binding.macro);\n if (positions.length === 0) continue;\n\n for (const record of sameTemplateRecords(records, templateLayout)) {\n const observed = parseObserved(record.url);\n for (const position of positions) {\n const observed_value = resolveObservedValue(observed, position);\n if (observed_value === null) continue;\n matches.push({\n binding,\n raw_value: resolved.raw_value,\n expected_encoded: resolved.expected_encoded,\n observed_url: record.url,\n record,\n position,\n observed_value,\n is_custom_vector: resolved.is_custom_vector,\n });\n }\n }\n }\n\n return matches;\n}\n\n/**\n * Tokenize a template from its raw string. Preserves literal `{MACRO}`\n * sequences in path and query. Throws if the template is not an\n * absolute URL. Avoids regex parsing of untrusted input to sidestep\n * any polynomial-backtracking risk on adversarial templates.\n */\nfunction parseTemplate(template: URL | string): TemplateLayout {\n const raw = typeof template === 'string' ? template : template.href;\n const parts = splitAbsoluteUrl(raw);\n if (!parts) throw new Error(`Template is not an absolute URL: ${raw}`);\n const { origin, path } = parts;\n const search = parts.search;\n\n const path_segments = path === '' || path === '/' ? [] : path.replace(/^\\//, '').split('/');\n\n const query_pairs: ParsedQueryPair[] = search\n ? search.split('&').map((segment, index) => {\n const eq = segment.indexOf('=');\n const keyRaw = eq === -1 ? segment : segment.slice(0, eq);\n const valueRaw = eq === -1 ? '' : segment.slice(eq + 1);\n return { key: safeDecode(keyRaw), raw_value: valueRaw, index };\n })\n : [];\n\n // Origin reported as WHATWG URL's `origin` shape so comparison with\n // observed URLs (which are WHATWG-parsed) is apples-to-apples.\n const normalizedOrigin = normalizeOrigin(origin);\n\n return {\n origin: normalizedOrigin,\n path_segments,\n query_pairs,\n href: raw,\n };\n}\n\nfunction normalizeOrigin(scheme_and_authority: string): string {\n // Handle `scheme://authority` form.\n try {\n const u = new URL(scheme_and_authority + '/');\n return u.origin;\n } catch {\n return scheme_and_authority;\n }\n}\n\n/**\n * Linear-time absolute-URL splitter. Returns the scheme+authority\n * prefix, the raw path, and the raw search (without leading `?`), or\n * `null` if the input isn't shaped `scheme://authority...`. Avoids\n * regex on untrusted input so adversarial templates can't trigger\n * polynomial backtracking.\n */\nfunction splitAbsoluteUrl(raw: string): { origin: string; path: string; search: string } | null {\n // Locate the `://` delimiter. Scheme MUST be non-empty and start with\n // a letter; we enforce that minimum here without scanning the whole\n // scheme for validity (WHATWG URL would accept most absolute forms).\n const schemeEnd = raw.indexOf('://');\n if (schemeEnd <= 0) return null;\n const firstChar = raw.charCodeAt(0);\n const isAlpha = (firstChar >= 0x41 && firstChar <= 0x5a) || (firstChar >= 0x61 && firstChar <= 0x7a);\n if (!isAlpha) return null;\n\n const afterScheme = schemeEnd + 3;\n // Authority ends at the first `/`, `?`, or `#` — or end of string.\n let authorityEnd = raw.length;\n for (let i = afterScheme; i < raw.length; i++) {\n const c = raw.charCodeAt(i);\n if (c === 0x2f /* / */ || c === 0x3f /* ? */ || c === 0x23 /* # */) {\n authorityEnd = i;\n break;\n }\n }\n const origin = raw.slice(0, authorityEnd);\n\n // Path runs from authorityEnd up to `?`, `#`, or end of string.\n let pathEnd = raw.length;\n for (let i = authorityEnd; i < raw.length; i++) {\n const c = raw.charCodeAt(i);\n if (c === 0x3f /* ? */ || c === 0x23 /* # */) {\n pathEnd = i;\n break;\n }\n }\n const path = raw.slice(authorityEnd, pathEnd);\n\n // Search runs from `?` up to `#` or end. No leading `?` in the returned form.\n let search = '';\n if (raw.charCodeAt(pathEnd) === 0x3f) {\n let searchEnd = raw.length;\n for (let i = pathEnd + 1; i < raw.length; i++) {\n if (raw.charCodeAt(i) === 0x23 /* # */) {\n searchEnd = i;\n break;\n }\n }\n search = raw.slice(pathEnd + 1, searchEnd);\n }\n\n return { origin, path, search };\n}\n\nfunction parseObserved(url: URL): ObservedLayout {\n const path_segments = url.pathname === '/' ? [] : url.pathname.replace(/^\\//, '').split('/');\n const rawSearch = url.search.startsWith('?') ? url.search.slice(1) : url.search;\n const query_pairs: ParsedQueryPair[] = rawSearch\n ? rawSearch.split('&').map((segment, index) => {\n const eq = segment.indexOf('=');\n const keyRaw = eq === -1 ? segment : segment.slice(0, eq);\n const valueRaw = eq === -1 ? '' : segment.slice(eq + 1);\n return { key: safeDecode(keyRaw), raw_value: valueRaw, index };\n })\n : [];\n return { url, path_segments, query_pairs, href: url.href };\n}\n\n/**\n * Narrow observed records to those that could plausibly be substituted\n * forms of `template`: same origin, same number of path segments,\n * literal segments matching where the template has no macro, and every\n * template query key present in the observed URL.\n *\n * This is a same-shape filter, not an alignment oracle — two templates\n * with identical path+key shapes but different semantics will confuse\n * it. Runners SHOULD invoke `match_bindings` once per `(template,\n * preview)` pair rather than pass all records from a multi-template\n * preview to a single call.\n */\nfunction sameTemplateRecords(records: readonly TrackerUrlRecord[], template: TemplateLayout): TrackerUrlRecord[] {\n const templateKeys = new Set(template.query_pairs.map(p => p.key));\n const templateSegCount = template.path_segments.length;\n return records.filter(r => {\n if (r.url.origin !== template.origin) return false;\n const observed = parseObserved(r.url);\n if (observed.path_segments.length !== templateSegCount) return false;\n for (let i = 0; i < templateSegCount; i++) {\n const seg = template.path_segments[i] ?? '';\n const obs = observed.path_segments[i] ?? '';\n if (!MACRO_SEGMENT_RE.test(seg) && seg !== obs) return false;\n }\n for (const key of templateKeys) {\n if (!observed.query_pairs.some(p => p.key === key)) return false;\n }\n return true;\n });\n}\n\n/**\n * Find the template positions where `macro` appears. Whole-value\n * matches win first (rare; legal for `<a href=\"{CLICK}\">`). Otherwise\n * emit every query-value and path-segment match in scan order.\n */\nfunction findMacroPositions(layout: TemplateLayout, macro: string): BindingMatch['position'][] {\n const out: BindingMatch['position'][] = [];\n\n if (layout.href === macro) {\n out.push({ kind: 'href_whole_value' });\n }\n\n for (const pair of layout.query_pairs) {\n if (pair.raw_value === macro) {\n out.push({ kind: 'query', key: pair.key, index: pair.index });\n }\n }\n\n layout.path_segments.forEach((seg, index) => {\n if (seg === macro) {\n out.push({ kind: 'path', index });\n }\n });\n\n return out;\n}\n\nfunction resolveObservedValue(layout: ObservedLayout, position: BindingMatch['position']): string | null {\n if (position.kind === 'href_whole_value') {\n return layout.href;\n }\n if (position.kind === 'path') {\n return layout.path_segments[position.index] ?? null;\n }\n const matching = layout.query_pairs.filter(p => p.key === position.key);\n return matching[0]?.raw_value ?? null;\n}\n\nfunction safeDecode(s: string): string {\n try {\n return decodeURIComponent(s.replace(/\\+/g, ' '));\n } catch {\n return s;\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
|
import { createHash } from "node:crypto";
|
|
8
2
|
import { divergenceOffset, equalUnderHexCasePolicy, isUnreservedOnly } from "../rfc3986.mjs";
|
|
9
3
|
const DEFAULT_MACRO_PROHIBITED_PATTERN = /\{[^{}\s]+\}/;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/substitution/observer/assertions.ts"],"sourcesContent":["/**\n * Per-binding assertions the runner runs on each BindingMatch. Each\n * returns an {@link AssertionResult} with a contract error code on\n * failure. Custom-vector payloads are SHA-256 redacted by default to\n * honor the contract's `error_report_payload_policy`; canonical\n * fixture vectors are echoed verbatim.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { AssertionOptions, AssertionResult, BindingMatch } from '../types';\nimport { divergenceOffset, equalUnderHexCasePolicy, isUnreservedOnly } from '../rfc3986';\n\n/**\n * Default prohibited pattern for {@link assertNoNestedExpansion} —\n * any brace-delimited token, regardless of case or naming convention.\n * Storyboards that need a narrower check (a specific sentinel macro)\n * pass a custom `RegExp`.\n */\nexport const DEFAULT_MACRO_PROHIBITED_PATTERN = /\\{[^{}\\s]+\\}/;\n\n/**\n * Byte-for-byte comparison of the observed value against the expected\n * encoded substring under the contract's hex-case policy. Uppercase\n * and lowercase hex digits inside a `%NN` triplet are treated as\n * equivalent; bytes outside triplets compare case-sensitively.\n */\nexport function assertRfc3986Safe(match: BindingMatch, options: AssertionOptions = {}): AssertionResult {\n const { observed_value, expected_encoded } = match;\n if (equalUnderHexCasePolicy(observed_value, expected_encoded)) {\n return { ok: true, byte_offset: -1 };\n }\n const offset = divergenceOffset(observed_value, expected_encoded);\n const { observed, expected } = redactPayloads(match, observed_value, expected_encoded, options);\n return {\n ok: false,\n error_code: 'substitution_encoding_violation',\n byte_offset: offset,\n expected,\n observed,\n message: `Encoded bytes diverge at offset ${offset} (expected=${truncate(expected)} observed=${truncate(observed)})`,\n };\n}\n\n/**\n * Stricter variant of {@link assertRfc3986Safe} — every byte at the\n * macro position is either an unreserved character or a valid `%NN`\n * triplet. Producers using a reserved-char denylist instead of the\n * unreserved whitelist fail here even when the byte-equal check passes\n * (the canonical counterexamples are parens `(` `)` and sub-delims).\n */\nexport function assertUnreservedOnly(match: BindingMatch, options: AssertionOptions = {}): AssertionResult {\n if (isUnreservedOnly(match.observed_value)) {\n return { ok: true };\n }\n const { observed, expected } = redactPayloads(match, match.observed_value, match.expected_encoded, options);\n return {\n ok: false,\n error_code: 'substitution_encoding_violation',\n observed,\n expected,\n message: `Observed value contains bytes outside RFC 3986 unreserved + %NN: ${truncate(observed)}`,\n };\n}\n\n/**\n * Reject second-round AdCP macro expansion at the macro position. A\n * seller that re-scanned its output after substitution would resolve\n * a `{DEVICE_ID}` literal inside the catalog value — this assertion\n * fails that behavior. The default `prohibited_pattern` matches any\n * brace-delimited token ({@link DEFAULT_MACRO_PROHIBITED_PATTERN});\n * storyboards that bind a specific sentinel macro SHOULD pass a\n * narrower pattern.\n */\nexport function assertNoNestedExpansion(\n match: BindingMatch,\n prohibited_pattern: RegExp = DEFAULT_MACRO_PROHIBITED_PATTERN,\n options: AssertionOptions = {}\n): AssertionResult {\n // Only literal brace characters indicate re-expansion risk —\n // percent-encoded `%7B...%7D` is the contract-required safe form.\n if (!/[{}]/.test(match.observed_value)) {\n return { ok: true };\n }\n const hit = prohibited_pattern.exec(match.observed_value);\n if (!hit) {\n return { ok: true };\n }\n const { observed, expected } = redactPayloads(match, match.observed_value, match.expected_encoded, options);\n return {\n ok: false,\n error_code: 'nested_macro_re_expansion',\n observed,\n expected,\n message: `Observed value contains a literal AdCP macro token at the macro position — second-round expansion risk.`,\n };\n}\n\n/**\n * Verify the observed URL's scheme matches the template's scheme. Only\n * meaningful at `href_whole_value` positions where a catalog value\n * could replace an entire URL and change its scheme (e.g., a\n * `javascript:`-scheme injection into `<a href=\"{CLICK}\">`).\n */\nexport function assertSchemePreserved(match: BindingMatch, template_scheme: string): AssertionResult {\n if (match.position.kind !== 'href_whole_value') {\n // Scheme preservation is only meaningful for whole-value bindings;\n // sub-value positions can't alter the enclosing URL's scheme.\n return { ok: true };\n }\n const normalizedExpected = template_scheme.replace(/:$/, '').toLowerCase();\n const observedScheme = match.observed_url.protocol.replace(/:$/, '').toLowerCase();\n if (observedScheme === normalizedExpected) {\n return { ok: true };\n }\n // Scheme values are a closed set (URL parsers normalize them) —\n // safe to echo without SHA-256 redaction regardless of vector kind.\n return {\n ok: false,\n error_code: 'substitution_scheme_injection',\n observed: match.observed_url.protocol,\n expected: `${normalizedExpected}:`,\n message: `Observed URL scheme ${match.observed_url.protocol} does not match template scheme ${normalizedExpected}:`,\n };\n}\n\n/**\n * Redact custom-vector payloads to SHA-256 digests unless the caller\n * explicitly opts into verbatim echo. Mirrors the contract's\n * `error_report_payload_policy` — canonical fixture values are safe\n * to echo because the fixtures are public; seller-specific payloads\n * SHOULD NOT become searchable text in CI logs.\n */\nfunction redactPayloads(\n match: BindingMatch,\n observed_value: string,\n expected_encoded: string,\n options: AssertionOptions\n): { observed: string; expected: string } {\n if (!match.is_custom_vector || options.include_raw_payloads) {\n return { observed: observed_value, expected: expected_encoded };\n }\n return {\n observed: `sha256:${sha256Hex(observed_value)}`,\n expected: `sha256:${sha256Hex(expected_encoded)}`,\n };\n}\n\nfunction sha256Hex(value: string): string {\n return createHash('sha256').update(value, 'utf8').digest('hex');\n}\n\nfunction truncate(s: string | undefined): string {\n if (s === undefined) return '';\n return s.length > 120 ? s.slice(0, 117) + '...' : s;\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/substitution/observer/assertions.ts"],"sourcesContent":["/**\n * Per-binding assertions the runner runs on each BindingMatch. Each\n * returns an {@link AssertionResult} with a contract error code on\n * failure. Custom-vector payloads are SHA-256 redacted by default to\n * honor the contract's `error_report_payload_policy`; canonical\n * fixture vectors are echoed verbatim.\n */\n\nimport { createHash } from 'node:crypto';\n\nimport type { AssertionOptions, AssertionResult, BindingMatch } from '../types';\nimport { divergenceOffset, equalUnderHexCasePolicy, isUnreservedOnly } from '../rfc3986';\n\n/**\n * Default prohibited pattern for {@link assertNoNestedExpansion} —\n * any brace-delimited token, regardless of case or naming convention.\n * Storyboards that need a narrower check (a specific sentinel macro)\n * pass a custom `RegExp`.\n */\nexport const DEFAULT_MACRO_PROHIBITED_PATTERN = /\\{[^{}\\s]+\\}/;\n\n/**\n * Byte-for-byte comparison of the observed value against the expected\n * encoded substring under the contract's hex-case policy. Uppercase\n * and lowercase hex digits inside a `%NN` triplet are treated as\n * equivalent; bytes outside triplets compare case-sensitively.\n */\nexport function assertRfc3986Safe(match: BindingMatch, options: AssertionOptions = {}): AssertionResult {\n const { observed_value, expected_encoded } = match;\n if (equalUnderHexCasePolicy(observed_value, expected_encoded)) {\n return { ok: true, byte_offset: -1 };\n }\n const offset = divergenceOffset(observed_value, expected_encoded);\n const { observed, expected } = redactPayloads(match, observed_value, expected_encoded, options);\n return {\n ok: false,\n error_code: 'substitution_encoding_violation',\n byte_offset: offset,\n expected,\n observed,\n message: `Encoded bytes diverge at offset ${offset} (expected=${truncate(expected)} observed=${truncate(observed)})`,\n };\n}\n\n/**\n * Stricter variant of {@link assertRfc3986Safe} — every byte at the\n * macro position is either an unreserved character or a valid `%NN`\n * triplet. Producers using a reserved-char denylist instead of the\n * unreserved whitelist fail here even when the byte-equal check passes\n * (the canonical counterexamples are parens `(` `)` and sub-delims).\n */\nexport function assertUnreservedOnly(match: BindingMatch, options: AssertionOptions = {}): AssertionResult {\n if (isUnreservedOnly(match.observed_value)) {\n return { ok: true };\n }\n const { observed, expected } = redactPayloads(match, match.observed_value, match.expected_encoded, options);\n return {\n ok: false,\n error_code: 'substitution_encoding_violation',\n observed,\n expected,\n message: `Observed value contains bytes outside RFC 3986 unreserved + %NN: ${truncate(observed)}`,\n };\n}\n\n/**\n * Reject second-round AdCP macro expansion at the macro position. A\n * seller that re-scanned its output after substitution would resolve\n * a `{DEVICE_ID}` literal inside the catalog value — this assertion\n * fails that behavior. The default `prohibited_pattern` matches any\n * brace-delimited token ({@link DEFAULT_MACRO_PROHIBITED_PATTERN});\n * storyboards that bind a specific sentinel macro SHOULD pass a\n * narrower pattern.\n */\nexport function assertNoNestedExpansion(\n match: BindingMatch,\n prohibited_pattern: RegExp = DEFAULT_MACRO_PROHIBITED_PATTERN,\n options: AssertionOptions = {}\n): AssertionResult {\n // Only literal brace characters indicate re-expansion risk —\n // percent-encoded `%7B...%7D` is the contract-required safe form.\n if (!/[{}]/.test(match.observed_value)) {\n return { ok: true };\n }\n const hit = prohibited_pattern.exec(match.observed_value);\n if (!hit) {\n return { ok: true };\n }\n const { observed, expected } = redactPayloads(match, match.observed_value, match.expected_encoded, options);\n return {\n ok: false,\n error_code: 'nested_macro_re_expansion',\n observed,\n expected,\n message: `Observed value contains a literal AdCP macro token at the macro position — second-round expansion risk.`,\n };\n}\n\n/**\n * Verify the observed URL's scheme matches the template's scheme. Only\n * meaningful at `href_whole_value` positions where a catalog value\n * could replace an entire URL and change its scheme (e.g., a\n * `javascript:`-scheme injection into `<a href=\"{CLICK}\">`).\n */\nexport function assertSchemePreserved(match: BindingMatch, template_scheme: string): AssertionResult {\n if (match.position.kind !== 'href_whole_value') {\n // Scheme preservation is only meaningful for whole-value bindings;\n // sub-value positions can't alter the enclosing URL's scheme.\n return { ok: true };\n }\n const normalizedExpected = template_scheme.replace(/:$/, '').toLowerCase();\n const observedScheme = match.observed_url.protocol.replace(/:$/, '').toLowerCase();\n if (observedScheme === normalizedExpected) {\n return { ok: true };\n }\n // Scheme values are a closed set (URL parsers normalize them) —\n // safe to echo without SHA-256 redaction regardless of vector kind.\n return {\n ok: false,\n error_code: 'substitution_scheme_injection',\n observed: match.observed_url.protocol,\n expected: `${normalizedExpected}:`,\n message: `Observed URL scheme ${match.observed_url.protocol} does not match template scheme ${normalizedExpected}:`,\n };\n}\n\n/**\n * Redact custom-vector payloads to SHA-256 digests unless the caller\n * explicitly opts into verbatim echo. Mirrors the contract's\n * `error_report_payload_policy` — canonical fixture values are safe\n * to echo because the fixtures are public; seller-specific payloads\n * SHOULD NOT become searchable text in CI logs.\n */\nfunction redactPayloads(\n match: BindingMatch,\n observed_value: string,\n expected_encoded: string,\n options: AssertionOptions\n): { observed: string; expected: string } {\n if (!match.is_custom_vector || options.include_raw_payloads) {\n return { observed: observed_value, expected: expected_encoded };\n }\n return {\n observed: `sha256:${sha256Hex(observed_value)}`,\n expected: `sha256:${sha256Hex(expected_encoded)}`,\n };\n}\n\nfunction sha256Hex(value: string): string {\n return createHash('sha256').update(value, 'utf8').digest('hex');\n}\n\nfunction truncate(s: string | undefined): string {\n if (s === undefined) return '';\n return s.length > 120 ? s.slice(0, 117) + '...' : s;\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 SYNTHETIC_BASE = "https://observer.test/";
|
|
8
2
|
const TAG_SPECIFIC_ATTRS = {
|
|
9
3
|
a: ["href"],
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/substitution/observer/html-parser.ts"],"sourcesContent":["/**\n * Targeted HTML attribute extractor. Pulls tracker URLs from the\n * contract's normative tag/attribute set only — deliberately narrower\n * than a general-purpose HTML parser.\n *\n * The set (from `substitution-observer-runner.yaml`):\n * a/href, img/src, img/srcset, iframe/src, source/src, source/srcset,\n * link/href, meta/content, *\\/data-impression-url, *\\/data-click-url,\n * *\\/data-tracker-url, *\\/data-vast-url\n *\n * Script text and HTML comments are ignored. `srcset` values are split\n * per-descriptor; every URL component is emitted.\n */\n\nimport type { TrackerUrlRecord } from '../types';\n\nconst SYNTHETIC_BASE = 'https://observer.test/';\n\nconst TAG_SPECIFIC_ATTRS: Record<string, readonly string[]> = {\n a: ['href'],\n img: ['src', 'srcset'],\n iframe: ['src'],\n source: ['src', 'srcset'],\n link: ['href'],\n meta: ['content'],\n};\n\nconst WILDCARD_ATTRS: readonly string[] = [\n 'data-impression-url',\n 'data-click-url',\n 'data-tracker-url',\n 'data-vast-url',\n];\n\nconst SRCSET_ATTRS = new Set(['srcset']);\n\ninterface AttrHit {\n tag: string;\n attr: string;\n value: string;\n line: number;\n}\n\n/**\n * Parse `html` and return every tracker URL in the normative extraction\n * set. URLs that fail WHATWG URL parsing (even after resolving against\n * `https://observer.test/`) are skipped — the runner asserts on\n * extractable URLs only.\n */\nexport function extractTrackerUrls(html: string): TrackerUrlRecord[] {\n const hits = findAttributeHits(html);\n const records: TrackerUrlRecord[] = [];\n for (const hit of hits) {\n const values = SRCSET_ATTRS.has(hit.attr) ? splitSrcset(hit.value) : [hit.value];\n for (const raw of values) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n // Defense against entity-smuggled URLs (`javascript	:...`):\n // we decode a known-safe set of named entities inside the attribute\n // value and drop any value whose remaining ampersand sequences\n // suggest an entity the decoder did not handle. A browser would\n // resolve the missing entity; we would not — and the divergence\n // is the exact gap the #2620 rule exists to close.\n if (__hasResidualEntity(trimmed)) continue;\n const parsed = tryParseUrl(trimmed);\n if (!parsed) continue;\n records.push({\n url: parsed,\n source_attr: hit.attr,\n source_tag: hit.tag,\n line_hint: hit.line,\n });\n }\n }\n return records;\n}\n\nfunction tryParseUrl(raw: string): URL | null {\n try {\n return new URL(raw, SYNTHETIC_BASE);\n } catch {\n return null;\n }\n}\n\n/**\n * `srcset` carries `URL [descriptor]` pairs separated by commas. The\n * descriptor is optional (`1x`, `2x`, `640w`, ...). Commas can appear\n * inside URLs only percent-encoded, so `,` splits are unambiguous.\n */\nfunction splitSrcset(value: string): string[] {\n return value\n .split(',')\n .map(part => part.trim().split(/\\s+/)[0] ?? '')\n .filter(u => u.length > 0);\n}\n\n/**\n * Walk `html` as a simple state machine, skipping comments, CDATA,\n * <script>, and <style>. Emit one hit per matching attribute.\n */\nfunction findAttributeHits(html: string): AttrHit[] {\n const hits: AttrHit[] = [];\n const len = html.length;\n let i = 0;\n let line = 1;\n\n const advance = (to: number): void => {\n for (let k = i; k < to && k < len; k++) {\n if (html.charCodeAt(k) === 0x0a) line += 1;\n }\n i = to;\n };\n\n while (i < len) {\n const c = html.charCodeAt(i);\n if (c === 0x0a) {\n line += 1;\n i += 1;\n continue;\n }\n if (c !== 0x3c /* < */) {\n i += 1;\n continue;\n }\n\n // Comment <!-- ... -->\n if (html.startsWith('<!--', i)) {\n const end = html.indexOf('-->', i + 4);\n if (end === -1) break;\n advance(end + 3);\n continue;\n }\n\n // CDATA <![CDATA[ ... ]]>\n if (html.startsWith('<![CDATA[', i)) {\n const end = html.indexOf(']]>', i + 9);\n if (end === -1) break;\n advance(end + 3);\n continue;\n }\n\n // DOCTYPE / XML decl — ignore up to first '>'\n if (html.startsWith('<!', i) || html.startsWith('<?', i)) {\n const end = html.indexOf('>', i + 2);\n if (end === -1) break;\n advance(end + 1);\n continue;\n }\n\n // Closing tag </foo>\n if (html.startsWith('</', i)) {\n const end = html.indexOf('>', i + 2);\n if (end === -1) break;\n advance(end + 1);\n continue;\n }\n\n // Only opening / self-closing tags remain. Parse tag name.\n const tagMatch = /^[A-Za-z][A-Za-z0-9:_-]*/.exec(html.slice(i + 1));\n if (!tagMatch) {\n i += 1;\n continue;\n }\n const tag = tagMatch[0].toLowerCase();\n const tagEnd = findTagEnd(html, i);\n if (tagEnd === -1) break;\n const tagStartLine = line;\n const tagBody = html.slice(i + 1 + tag.length, tagEnd);\n\n // <script>/<style>: skip until the matching close tag. Content\n // is ignored per the contract (script_text_content: ignored).\n if (tag === 'script' || tag === 'style') {\n advance(tagEnd + 1);\n const closeRegex = new RegExp(`</${tag}\\\\s*>`, 'i');\n const rest = html.slice(i);\n const m = closeRegex.exec(rest);\n if (!m) break;\n advance(i + m.index + m[0].length);\n continue;\n }\n\n collectAttributes(tagBody, tag, tagStartLine, hits);\n advance(tagEnd + 1);\n }\n\n return hits;\n}\n\n/**\n * Return the offset (relative to `html`) of the closing `>` that ends\n * the opening-tag token starting at `start`. Handles quoted attribute\n * values so a `>` inside an attribute doesn't close the tag early.\n */\nfunction findTagEnd(html: string, start: number): number {\n const len = html.length;\n let k = start + 1;\n let quote: number | null = null;\n while (k < len) {\n const ch = html.charCodeAt(k);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else {\n if (ch === 0x22 /* \" */ || ch === 0x27 /* ' */) {\n quote = ch;\n } else if (ch === 0x3e /* > */) {\n return k;\n }\n }\n k += 1;\n }\n return -1;\n}\n\nconst ATTR_REGEX = /([A-Za-z_:][A-Za-z0-9_.:-]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'`=<>]+))/g;\n\nfunction collectAttributes(body: string, tag: string, line: number, out: AttrHit[]): void {\n const tagAttrs = TAG_SPECIFIC_ATTRS[tag] ?? [];\n ATTR_REGEX.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = ATTR_REGEX.exec(body)) !== null) {\n const name = (m[1] ?? '').toLowerCase();\n const value = m[2] ?? m[3] ?? m[4] ?? '';\n const isTagAttr = tagAttrs.includes(name);\n const isWildcard = WILDCARD_ATTRS.includes(name);\n if (!isTagAttr && !isWildcard) continue;\n out.push({ tag, attr: name, value: decodeHtmlEntities(value), line });\n }\n}\n\n/**\n * Entities that browsers decode inside attribute values and that a\n * seller could weaponize to smuggle a javascript:-scheme URL past a\n * naive extractor. The set covers:\n *\n * - The five basic entities (`& < > " '`) that\n * every HTML consumer handles.\n * - Whitespace / control entities that let a colon or scheme prefix\n * hide: `	`, `
`, ` `, variants of newlines.\n * - Punctuation that appears in URL scheme injection or reserved-\n * char breakout: `:`, `/`, `(`, `)`, `{`,\n * `}`.\n *\n * This is NOT the full HTML5 named-character-reference table (~2200\n * entries). The decoder additionally post-filters via\n * `__hasResidualEntity`: if the value still contains a browser-decodable\n * entity this table missed (a `&name;` with a trailing semicolon, or a\n * numeric `&#...` reference), it is treated as undecodable and the URL is\n * dropped — refusing to classify rather than risking under-extraction.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n // Whitespace / control — used in URL-scheme smuggling.\n Tab: '\\t',\n tab: '\\t',\n NewLine: '\\n',\n newline: '\\n',\n nbsp: '\\u00A0',\n NonBreakingSpace: '\\u00A0',\n // Punctuation entities browsers decode.\n colon: ':',\n sol: '/',\n bsol: '\\\\',\n lpar: '(',\n rpar: ')',\n lbrace: '{',\n rbrace: '}',\n lsqb: '[',\n rsqb: ']',\n period: '.',\n comma: ',',\n semi: ';',\n excl: '!',\n quest: '?',\n equals: '=',\n num: '#',\n dollar: '$',\n commat: '@',\n percnt: '%',\n};\n\n/**\n * Detects an ampersand sequence that a browser would decode but the\n * limited `NAMED_ENTITIES` table above did not — after decoding, any\n * match signals browser-visible content that diverges from what we\n * extracted, so callers drop the value rather than extract a\n * partially-decoded URL.\n *\n * Two branches mirror how browsers decode references:\n *\n * - NAMED (`&name;`): a trailing semicolon is REQUIRED. Browsers only\n * decode a fixed legacy set of named references without a semicolon\n * (`&`, `<`, `>`, `"`, `©`, ` `, `Æ`, ...),\n * and none of those legacy no-semicolon entities are the dangerous\n * URL-scheme chars (`:`, `/`, `\\`) — those require the semicolon\n * (`:`, `/`, `\`). Demanding a semicolon here means a\n * literal query string like `?mb=1&pkg=2` (which a browser renders\n * verbatim, decoding nothing) is not mistaken for a smuggled entity,\n * while `&someentity;` the table missed still trips the check.\n * - NUMERIC (`:` / `:`): the semicolon is optional, matching\n * the browser's lenient numeric decoding. A `&key=value` query never\n * starts a key with `#`, so this branch does not collide with normal\n * query strings.\n */\nconst RESIDUAL_ENTITY_RE = /&(?:[A-Za-z][A-Za-z0-9]{1,31};|#[xX]?[0-9A-Fa-f]+;?)/;\n\nfunction decodeHtmlEntities(s: string): string {\n return (\n s\n .replace(/&#[xX]([0-9A-Fa-f]+);?/g, (_, hex) => safeCodePoint(parseInt(hex, 16)))\n .replace(/&#([0-9]+);?/g, (_, dec) => safeCodePoint(parseInt(dec, 10)))\n // Semicolon is optional per HTML5 for legacy entities; match both.\n .replace(/&([A-Za-z][A-Za-z0-9]{1,31});?/g, (whole, name: string) => {\n const hit = NAMED_ENTITIES[name];\n return hit ?? whole;\n })\n );\n}\n\nfunction safeCodePoint(cp: number): string {\n if (!Number.isFinite(cp) || cp < 0 || cp > 0x10ffff) return '\\uFFFD';\n try {\n return String.fromCodePoint(cp);\n } catch {\n return '\\uFFFD';\n }\n}\n\n/**\n * True if the decoded value still contains an entity-shaped ampersand\n * sequence (named or numeric) — a signal that browser-visible content\n * may differ from what we extracted. The caller discards such values\n * to avoid under-extraction of a smuggled javascript:-scheme URL.\n */\nexport function __hasResidualEntity(decoded: string): boolean {\n return RESIDUAL_ENTITY_RE.test(decoded);\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/substitution/observer/html-parser.ts"],"sourcesContent":["/**\n * Targeted HTML attribute extractor. Pulls tracker URLs from the\n * contract's normative tag/attribute set only — deliberately narrower\n * than a general-purpose HTML parser.\n *\n * The set (from `substitution-observer-runner.yaml`):\n * a/href, img/src, img/srcset, iframe/src, source/src, source/srcset,\n * link/href, meta/content, *\\/data-impression-url, *\\/data-click-url,\n * *\\/data-tracker-url, *\\/data-vast-url\n *\n * Script text and HTML comments are ignored. `srcset` values are split\n * per-descriptor; every URL component is emitted.\n */\n\nimport type { TrackerUrlRecord } from '../types';\n\nconst SYNTHETIC_BASE = 'https://observer.test/';\n\nconst TAG_SPECIFIC_ATTRS: Record<string, readonly string[]> = {\n a: ['href'],\n img: ['src', 'srcset'],\n iframe: ['src'],\n source: ['src', 'srcset'],\n link: ['href'],\n meta: ['content'],\n};\n\nconst WILDCARD_ATTRS: readonly string[] = [\n 'data-impression-url',\n 'data-click-url',\n 'data-tracker-url',\n 'data-vast-url',\n];\n\nconst SRCSET_ATTRS = new Set(['srcset']);\n\ninterface AttrHit {\n tag: string;\n attr: string;\n value: string;\n line: number;\n}\n\n/**\n * Parse `html` and return every tracker URL in the normative extraction\n * set. URLs that fail WHATWG URL parsing (even after resolving against\n * `https://observer.test/`) are skipped — the runner asserts on\n * extractable URLs only.\n */\nexport function extractTrackerUrls(html: string): TrackerUrlRecord[] {\n const hits = findAttributeHits(html);\n const records: TrackerUrlRecord[] = [];\n for (const hit of hits) {\n const values = SRCSET_ATTRS.has(hit.attr) ? splitSrcset(hit.value) : [hit.value];\n for (const raw of values) {\n const trimmed = raw.trim();\n if (!trimmed) continue;\n // Defense against entity-smuggled URLs (`javascript	:...`):\n // we decode a known-safe set of named entities inside the attribute\n // value and drop any value whose remaining ampersand sequences\n // suggest an entity the decoder did not handle. A browser would\n // resolve the missing entity; we would not — and the divergence\n // is the exact gap the #2620 rule exists to close.\n if (__hasResidualEntity(trimmed)) continue;\n const parsed = tryParseUrl(trimmed);\n if (!parsed) continue;\n records.push({\n url: parsed,\n source_attr: hit.attr,\n source_tag: hit.tag,\n line_hint: hit.line,\n });\n }\n }\n return records;\n}\n\nfunction tryParseUrl(raw: string): URL | null {\n try {\n return new URL(raw, SYNTHETIC_BASE);\n } catch {\n return null;\n }\n}\n\n/**\n * `srcset` carries `URL [descriptor]` pairs separated by commas. The\n * descriptor is optional (`1x`, `2x`, `640w`, ...). Commas can appear\n * inside URLs only percent-encoded, so `,` splits are unambiguous.\n */\nfunction splitSrcset(value: string): string[] {\n return value\n .split(',')\n .map(part => part.trim().split(/\\s+/)[0] ?? '')\n .filter(u => u.length > 0);\n}\n\n/**\n * Walk `html` as a simple state machine, skipping comments, CDATA,\n * <script>, and <style>. Emit one hit per matching attribute.\n */\nfunction findAttributeHits(html: string): AttrHit[] {\n const hits: AttrHit[] = [];\n const len = html.length;\n let i = 0;\n let line = 1;\n\n const advance = (to: number): void => {\n for (let k = i; k < to && k < len; k++) {\n if (html.charCodeAt(k) === 0x0a) line += 1;\n }\n i = to;\n };\n\n while (i < len) {\n const c = html.charCodeAt(i);\n if (c === 0x0a) {\n line += 1;\n i += 1;\n continue;\n }\n if (c !== 0x3c /* < */) {\n i += 1;\n continue;\n }\n\n // Comment <!-- ... -->\n if (html.startsWith('<!--', i)) {\n const end = html.indexOf('-->', i + 4);\n if (end === -1) break;\n advance(end + 3);\n continue;\n }\n\n // CDATA <![CDATA[ ... ]]>\n if (html.startsWith('<![CDATA[', i)) {\n const end = html.indexOf(']]>', i + 9);\n if (end === -1) break;\n advance(end + 3);\n continue;\n }\n\n // DOCTYPE / XML decl — ignore up to first '>'\n if (html.startsWith('<!', i) || html.startsWith('<?', i)) {\n const end = html.indexOf('>', i + 2);\n if (end === -1) break;\n advance(end + 1);\n continue;\n }\n\n // Closing tag </foo>\n if (html.startsWith('</', i)) {\n const end = html.indexOf('>', i + 2);\n if (end === -1) break;\n advance(end + 1);\n continue;\n }\n\n // Only opening / self-closing tags remain. Parse tag name.\n const tagMatch = /^[A-Za-z][A-Za-z0-9:_-]*/.exec(html.slice(i + 1));\n if (!tagMatch) {\n i += 1;\n continue;\n }\n const tag = tagMatch[0].toLowerCase();\n const tagEnd = findTagEnd(html, i);\n if (tagEnd === -1) break;\n const tagStartLine = line;\n const tagBody = html.slice(i + 1 + tag.length, tagEnd);\n\n // <script>/<style>: skip until the matching close tag. Content\n // is ignored per the contract (script_text_content: ignored).\n if (tag === 'script' || tag === 'style') {\n advance(tagEnd + 1);\n const closeRegex = new RegExp(`</${tag}\\\\s*>`, 'i');\n const rest = html.slice(i);\n const m = closeRegex.exec(rest);\n if (!m) break;\n advance(i + m.index + m[0].length);\n continue;\n }\n\n collectAttributes(tagBody, tag, tagStartLine, hits);\n advance(tagEnd + 1);\n }\n\n return hits;\n}\n\n/**\n * Return the offset (relative to `html`) of the closing `>` that ends\n * the opening-tag token starting at `start`. Handles quoted attribute\n * values so a `>` inside an attribute doesn't close the tag early.\n */\nfunction findTagEnd(html: string, start: number): number {\n const len = html.length;\n let k = start + 1;\n let quote: number | null = null;\n while (k < len) {\n const ch = html.charCodeAt(k);\n if (quote !== null) {\n if (ch === quote) quote = null;\n } else {\n if (ch === 0x22 /* \" */ || ch === 0x27 /* ' */) {\n quote = ch;\n } else if (ch === 0x3e /* > */) {\n return k;\n }\n }\n k += 1;\n }\n return -1;\n}\n\nconst ATTR_REGEX = /([A-Za-z_:][A-Za-z0-9_.:-]*)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'`=<>]+))/g;\n\nfunction collectAttributes(body: string, tag: string, line: number, out: AttrHit[]): void {\n const tagAttrs = TAG_SPECIFIC_ATTRS[tag] ?? [];\n ATTR_REGEX.lastIndex = 0;\n let m: RegExpExecArray | null;\n while ((m = ATTR_REGEX.exec(body)) !== null) {\n const name = (m[1] ?? '').toLowerCase();\n const value = m[2] ?? m[3] ?? m[4] ?? '';\n const isTagAttr = tagAttrs.includes(name);\n const isWildcard = WILDCARD_ATTRS.includes(name);\n if (!isTagAttr && !isWildcard) continue;\n out.push({ tag, attr: name, value: decodeHtmlEntities(value), line });\n }\n}\n\n/**\n * Entities that browsers decode inside attribute values and that a\n * seller could weaponize to smuggle a javascript:-scheme URL past a\n * naive extractor. The set covers:\n *\n * - The five basic entities (`& < > " '`) that\n * every HTML consumer handles.\n * - Whitespace / control entities that let a colon or scheme prefix\n * hide: `	`, `
`, ` `, variants of newlines.\n * - Punctuation that appears in URL scheme injection or reserved-\n * char breakout: `:`, `/`, `(`, `)`, `{`,\n * `}`.\n *\n * This is NOT the full HTML5 named-character-reference table (~2200\n * entries). The decoder additionally post-filters via\n * `__hasResidualEntity`: if the value still contains a browser-decodable\n * entity this table missed (a `&name;` with a trailing semicolon, or a\n * numeric `&#...` reference), it is treated as undecodable and the URL is\n * dropped — refusing to classify rather than risking under-extraction.\n */\nconst NAMED_ENTITIES: Record<string, string> = {\n amp: '&',\n lt: '<',\n gt: '>',\n quot: '\"',\n apos: \"'\",\n // Whitespace / control — used in URL-scheme smuggling.\n Tab: '\\t',\n tab: '\\t',\n NewLine: '\\n',\n newline: '\\n',\n nbsp: '\\u00A0',\n NonBreakingSpace: '\\u00A0',\n // Punctuation entities browsers decode.\n colon: ':',\n sol: '/',\n bsol: '\\\\',\n lpar: '(',\n rpar: ')',\n lbrace: '{',\n rbrace: '}',\n lsqb: '[',\n rsqb: ']',\n period: '.',\n comma: ',',\n semi: ';',\n excl: '!',\n quest: '?',\n equals: '=',\n num: '#',\n dollar: '$',\n commat: '@',\n percnt: '%',\n};\n\n/**\n * Detects an ampersand sequence that a browser would decode but the\n * limited `NAMED_ENTITIES` table above did not — after decoding, any\n * match signals browser-visible content that diverges from what we\n * extracted, so callers drop the value rather than extract a\n * partially-decoded URL.\n *\n * Two branches mirror how browsers decode references:\n *\n * - NAMED (`&name;`): a trailing semicolon is REQUIRED. Browsers only\n * decode a fixed legacy set of named references without a semicolon\n * (`&`, `<`, `>`, `"`, `©`, ` `, `Æ`, ...),\n * and none of those legacy no-semicolon entities are the dangerous\n * URL-scheme chars (`:`, `/`, `\\`) — those require the semicolon\n * (`:`, `/`, `\`). Demanding a semicolon here means a\n * literal query string like `?mb=1&pkg=2` (which a browser renders\n * verbatim, decoding nothing) is not mistaken for a smuggled entity,\n * while `&someentity;` the table missed still trips the check.\n * - NUMERIC (`:` / `:`): the semicolon is optional, matching\n * the browser's lenient numeric decoding. A `&key=value` query never\n * starts a key with `#`, so this branch does not collide with normal\n * query strings.\n */\nconst RESIDUAL_ENTITY_RE = /&(?:[A-Za-z][A-Za-z0-9]{1,31};|#[xX]?[0-9A-Fa-f]+;?)/;\n\nfunction decodeHtmlEntities(s: string): string {\n return (\n s\n .replace(/&#[xX]([0-9A-Fa-f]+);?/g, (_, hex) => safeCodePoint(parseInt(hex, 16)))\n .replace(/&#([0-9]+);?/g, (_, dec) => safeCodePoint(parseInt(dec, 10)))\n // Semicolon is optional per HTML5 for legacy entities; match both.\n .replace(/&([A-Za-z][A-Za-z0-9]{1,31});?/g, (whole, name: string) => {\n const hit = NAMED_ENTITIES[name];\n return hit ?? whole;\n })\n );\n}\n\nfunction safeCodePoint(cp: number): string {\n if (!Number.isFinite(cp) || cp < 0 || cp > 0x10ffff) return '\\uFFFD';\n try {\n return String.fromCodePoint(cp);\n } catch {\n return '\\uFFFD';\n }\n}\n\n/**\n * True if the decoded value still contains an entity-shaped ampersand\n * sequence (named or numeric) — a signal that browser-visible content\n * may differ from what we extracted. The caller discards such values\n * to avoid under-extraction of a smuggled javascript:-scheme URL.\n */\nexport function __hasResidualEntity(decoded: string): boolean {\n return RESIDUAL_ENTITY_RE.test(decoded);\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
|
import { SubstitutionObserver, PreviewFetchError } from "./SubstitutionObserver.mjs";
|
|
8
2
|
import { extractTrackerUrls } from "./html-parser.mjs";
|
|
9
3
|
import { matchBindings } from "./alignment.mjs";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/substitution/observer/index.ts"],"sourcesContent":["export { SubstitutionObserver, PreviewFetchError } from './SubstitutionObserver';\nexport type { ObserverFetchOptions } from './SubstitutionObserver';\n\nexport { extractTrackerUrls } from './html-parser';\nexport { matchBindings } from './alignment';\nexport {\n assertNoNestedExpansion,\n assertRfc3986Safe,\n assertSchemePreserved,\n assertUnreservedOnly,\n DEFAULT_MACRO_PROHIBITED_PATTERN,\n} from './assertions';\nexport { enforceSsrfPolicy, enforceSsrfPolicyResolved, DEFAULT_SSRF_POLICY } from './ssrf';\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/substitution/observer/index.ts"],"sourcesContent":["export { SubstitutionObserver, PreviewFetchError } from './SubstitutionObserver';\nexport type { ObserverFetchOptions } from './SubstitutionObserver';\n\nexport { extractTrackerUrls } from './html-parser';\nexport { matchBindings } from './alignment';\nexport {\n assertNoNestedExpansion,\n assertRfc3986Safe,\n assertSchemePreserved,\n assertUnreservedOnly,\n DEFAULT_MACRO_PROHIBITED_PATTERN,\n} from './assertions';\nexport { enforceSsrfPolicy, enforceSsrfPolicyResolved, DEFAULT_SSRF_POLICY } from './ssrf';\n"],"mappings":"AAAA,SAAS,sBAAsB,yBAAyB;AAGxD,SAAS,0BAA0B;AACnC,SAAS,qBAAqB;AAC9B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,mBAAmB,2BAA2B,2BAA2B;","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 { BlockList, isIP, isIPv4, isIPv6 } from "node:net";
|
|
8
2
|
const DEFAULT_SSRF_POLICY = Object.freeze({
|
|
9
3
|
schemes_allowed: Object.freeze(["https"]),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/lib/substitution/observer/ssrf.ts"],"sourcesContent":["/**\n * SSRF policy enforcement for `preview_url` fetches. The deny lists\n * below mirror the contract's normative policy block — a verifier\n * consumer MUST enforce every rule to claim AdCP Verified grading.\n */\n\nimport { BlockList, isIP, isIPv4, isIPv6 } from 'node:net';\nimport type { PolicyResult, SsrfPolicy } from '../types';\n\n/**\n * Canonical policy from\n * `compliance/cache/latest/test-kits/substitution-observer-runner.yaml`.\n * Callers SHOULD use this constant — overriding the policy is\n * intended for local-dev workflows (`host_literal_policy: 'allow'`),\n * not for loosening deny lists.\n */\nexport const DEFAULT_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['https']),\n schemes_denied: Object.freeze(['http', 'file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n '127.0.0.0/8',\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n '::1/128',\n '::/128',\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'reject',\n}) as SsrfPolicy;\n\ninterface CompiledPolicy {\n policy: SsrfPolicy;\n ipv4: BlockList;\n ipv6: BlockList;\n metadataHostnames: Set<string>;\n schemesAllowed: Set<string>;\n schemesDenied: Set<string>;\n}\n\nconst POLICY_CACHE = new WeakMap<SsrfPolicy, CompiledPolicy>();\n\nfunction compile(policy: SsrfPolicy): CompiledPolicy {\n const cached = POLICY_CACHE.get(policy);\n if (cached) return cached;\n const ipv4 = new BlockList();\n const ipv6 = new BlockList();\n for (const cidr of policy.hosts_denied_ipv4_cidrs) {\n const [ip = '', prefix = '0'] = cidr.split('/');\n ipv4.addSubnet(ip, Number(prefix), 'ipv4');\n }\n for (const cidr of policy.hosts_denied_ipv6_cidrs) {\n const [ip = '', prefix = '0'] = cidr.split('/');\n ipv6.addSubnet(normalizeIpv6(ip), Number(prefix), 'ipv6');\n }\n const compiled: CompiledPolicy = {\n policy,\n ipv4,\n ipv6,\n metadataHostnames: new Set(policy.hosts_denied_metadata.map(h => h.toLowerCase())),\n schemesAllowed: new Set(policy.schemes_allowed.map(s => s.toLowerCase())),\n schemesDenied: new Set(policy.schemes_denied.map(s => s.toLowerCase())),\n };\n POLICY_CACHE.set(policy, compiled);\n return compiled;\n}\n\n/**\n * Evaluate `url` against the policy's synchronous rules: scheme, bare-\n * IP-literal rejection, metadata hostname, and IP-literal CIDR deny.\n * DNS revalidation (required when the host is a name) is handled by\n * {@link enforceSsrfPolicyWithResolution}; this pure check covers every\n * non-DNS case without touching the network.\n */\nexport function enforceSsrfPolicy(url: URL, policy: SsrfPolicy = DEFAULT_SSRF_POLICY): PolicyResult {\n const compiled = compile(policy);\n const scheme = url.protocol.replace(/:$/, '').toLowerCase();\n\n if (compiled.schemesDenied.has(scheme)) {\n return {\n allowed: false,\n rule: `schemes_denied:${scheme}`,\n message: `URL scheme ${scheme} is on the deny list.`,\n };\n }\n if (!compiled.schemesAllowed.has(scheme)) {\n return {\n allowed: false,\n rule: `schemes_allowed:${scheme}`,\n message: `URL scheme ${scheme} is not in the allow list.`,\n };\n }\n\n const hostname = stripBrackets(url.hostname).toLowerCase();\n\n if (compiled.metadataHostnames.has(hostname)) {\n return {\n allowed: false,\n rule: `hosts_denied_metadata:${hostname}`,\n message: `Hostname ${hostname} is a known cloud metadata endpoint.`,\n };\n }\n\n if (isIP(hostname) !== 0) {\n if (policy.host_literal_policy === 'reject') {\n return {\n allowed: false,\n rule: 'host_literal_policy:reject',\n message: `Bare IP literal ${hostname} rejected under Verified host policy.`,\n };\n }\n return checkIp(hostname, compiled);\n }\n\n return { allowed: true };\n}\n\n/**\n * Evaluate `url` after DNS resolution. Every resolved address must\n * pass the CIDR deny list. Callers pin the request to the first\n * allowed address to prevent DNS rebinding between lookup and connect.\n */\nexport function enforceSsrfPolicyResolved(\n url: URL,\n addresses: readonly string[],\n policy: SsrfPolicy = DEFAULT_SSRF_POLICY\n): PolicyResult {\n const syncResult = enforceSsrfPolicy(url, policy);\n if (!syncResult.allowed) return syncResult;\n const compiled = compile(policy);\n if (addresses.length === 0) {\n return {\n allowed: false,\n rule: 'dns_revalidation:no_addresses',\n message: `DNS resolution returned no addresses for ${url.hostname}.`,\n };\n }\n for (const addr of addresses) {\n const r = checkIp(addr, compiled);\n if (!r.allowed) return r;\n }\n return { allowed: true };\n}\n\nfunction checkIp(ip: string, compiled: CompiledPolicy): PolicyResult {\n if (isIPv4(ip)) {\n if (compiled.ipv4.check(ip, 'ipv4')) {\n return {\n allowed: false,\n rule: matchIpv4Rule(ip, compiled),\n message: `Resolved address ${ip} is in a denied IPv4 range.`,\n };\n }\n return { allowed: true };\n }\n if (isIPv6(ip)) {\n const normalized = normalizeIpv6(ip);\n // IPv4-mapped IPv6 (::ffff:a.b.c.d) is re-checked against v4 deny.\n const v4Suffix = extractMappedIpv4(normalized);\n if (v4Suffix && compiled.ipv4.check(v4Suffix, 'ipv4')) {\n return {\n allowed: false,\n rule: matchIpv4Rule(v4Suffix, compiled),\n message: `IPv4-mapped address ${ip} resolves to denied IPv4 ${v4Suffix}.`,\n };\n }\n if (compiled.ipv6.check(normalized, 'ipv6')) {\n return {\n allowed: false,\n rule: matchIpv6Rule(normalized, compiled),\n message: `Resolved address ${ip} is in a denied IPv6 range.`,\n };\n }\n return { allowed: true };\n }\n return {\n allowed: false,\n rule: 'invalid_ip',\n message: `Address ${ip} is not a valid IPv4 or IPv6 literal.`,\n };\n}\n\nfunction stripBrackets(host: string): string {\n return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;\n}\n\nfunction normalizeIpv6(ip: string): string {\n // BlockList expects normalized lowercase; URL hostname is lowercase\n // but IPv6 literals may arrive with varied compression.\n return stripBrackets(ip).toLowerCase();\n}\n\nfunction extractMappedIpv4(ipv6: string): string | null {\n const m = /^::ffff:([0-9a-f:]+)$/i.exec(ipv6);\n if (!m) return null;\n const tail = m[1] ?? '';\n if (isIPv4(tail)) return tail;\n // ::ffff:c0a8:0101 form — convert four hex halves to dotted quad.\n const parts = tail.split(':');\n if (parts.length === 2 && parts.every(p => /^[0-9a-f]{1,4}$/.test(p))) {\n const hi = parseInt(parts[0] ?? '0', 16);\n const lo = parseInt(parts[1] ?? '0', 16);\n return `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;\n }\n return null;\n}\n\nfunction matchIpv4Rule(ip: string, compiled: CompiledPolicy): string {\n for (const cidr of compiled.policy.hosts_denied_ipv4_cidrs) {\n const [base = '', prefix = '0'] = cidr.split('/');\n const bl = new BlockList();\n bl.addSubnet(base, Number(prefix), 'ipv4');\n if (bl.check(ip, 'ipv4')) return `hosts_denied_ipv4_cidrs:${cidr}`;\n }\n return 'hosts_denied_ipv4_cidrs:unknown';\n}\n\nfunction matchIpv6Rule(ip: string, compiled: CompiledPolicy): string {\n for (const cidr of compiled.policy.hosts_denied_ipv6_cidrs) {\n const [base = '', prefix = '0'] = cidr.split('/');\n const bl = new BlockList();\n bl.addSubnet(normalizeIpv6(base), Number(prefix), 'ipv6');\n if (bl.check(ip, 'ipv6')) return `hosts_denied_ipv6_cidrs:${cidr}`;\n }\n return 'hosts_denied_ipv6_cidrs:unknown';\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../../src/lib/substitution/observer/ssrf.ts"],"sourcesContent":["/**\n * SSRF policy enforcement for `preview_url` fetches. The deny lists\n * below mirror the contract's normative policy block — a verifier\n * consumer MUST enforce every rule to claim AdCP Verified grading.\n */\n\nimport { BlockList, isIP, isIPv4, isIPv6 } from 'node:net';\nimport type { PolicyResult, SsrfPolicy } from '../types';\n\n/**\n * Canonical policy from\n * `compliance/cache/latest/test-kits/substitution-observer-runner.yaml`.\n * Callers SHOULD use this constant — overriding the policy is\n * intended for local-dev workflows (`host_literal_policy: 'allow'`),\n * not for loosening deny lists.\n */\nexport const DEFAULT_SSRF_POLICY: SsrfPolicy = Object.freeze({\n schemes_allowed: Object.freeze(['https']),\n schemes_denied: Object.freeze(['http', 'file', 'gopher', 'ftp', 'ftps', 'data', 'javascript', 'about', 'ws', 'wss']),\n hosts_denied_ipv4_cidrs: Object.freeze([\n '0.0.0.0/8',\n '10.0.0.0/8',\n '100.64.0.0/10',\n '127.0.0.0/8',\n '169.254.0.0/16',\n '172.16.0.0/12',\n '192.0.0.0/24',\n '192.168.0.0/16',\n '224.0.0.0/4',\n '240.0.0.0/4',\n ]),\n hosts_denied_ipv6_cidrs: Object.freeze([\n '::1/128',\n '::/128',\n '::ffff:0:0/96',\n '64:ff9b::/96',\n 'fc00::/7',\n 'fe80::/10',\n 'ff00::/8',\n ]),\n hosts_denied_metadata: Object.freeze([\n 'metadata.google.internal',\n 'metadata',\n 'metadata.packet.net',\n 'fd00:ec2::254',\n ]),\n host_literal_policy: 'reject',\n}) as SsrfPolicy;\n\ninterface CompiledPolicy {\n policy: SsrfPolicy;\n ipv4: BlockList;\n ipv6: BlockList;\n metadataHostnames: Set<string>;\n schemesAllowed: Set<string>;\n schemesDenied: Set<string>;\n}\n\nconst POLICY_CACHE = new WeakMap<SsrfPolicy, CompiledPolicy>();\n\nfunction compile(policy: SsrfPolicy): CompiledPolicy {\n const cached = POLICY_CACHE.get(policy);\n if (cached) return cached;\n const ipv4 = new BlockList();\n const ipv6 = new BlockList();\n for (const cidr of policy.hosts_denied_ipv4_cidrs) {\n const [ip = '', prefix = '0'] = cidr.split('/');\n ipv4.addSubnet(ip, Number(prefix), 'ipv4');\n }\n for (const cidr of policy.hosts_denied_ipv6_cidrs) {\n const [ip = '', prefix = '0'] = cidr.split('/');\n ipv6.addSubnet(normalizeIpv6(ip), Number(prefix), 'ipv6');\n }\n const compiled: CompiledPolicy = {\n policy,\n ipv4,\n ipv6,\n metadataHostnames: new Set(policy.hosts_denied_metadata.map(h => h.toLowerCase())),\n schemesAllowed: new Set(policy.schemes_allowed.map(s => s.toLowerCase())),\n schemesDenied: new Set(policy.schemes_denied.map(s => s.toLowerCase())),\n };\n POLICY_CACHE.set(policy, compiled);\n return compiled;\n}\n\n/**\n * Evaluate `url` against the policy's synchronous rules: scheme, bare-\n * IP-literal rejection, metadata hostname, and IP-literal CIDR deny.\n * DNS revalidation (required when the host is a name) is handled by\n * {@link enforceSsrfPolicyWithResolution}; this pure check covers every\n * non-DNS case without touching the network.\n */\nexport function enforceSsrfPolicy(url: URL, policy: SsrfPolicy = DEFAULT_SSRF_POLICY): PolicyResult {\n const compiled = compile(policy);\n const scheme = url.protocol.replace(/:$/, '').toLowerCase();\n\n if (compiled.schemesDenied.has(scheme)) {\n return {\n allowed: false,\n rule: `schemes_denied:${scheme}`,\n message: `URL scheme ${scheme} is on the deny list.`,\n };\n }\n if (!compiled.schemesAllowed.has(scheme)) {\n return {\n allowed: false,\n rule: `schemes_allowed:${scheme}`,\n message: `URL scheme ${scheme} is not in the allow list.`,\n };\n }\n\n const hostname = stripBrackets(url.hostname).toLowerCase();\n\n if (compiled.metadataHostnames.has(hostname)) {\n return {\n allowed: false,\n rule: `hosts_denied_metadata:${hostname}`,\n message: `Hostname ${hostname} is a known cloud metadata endpoint.`,\n };\n }\n\n if (isIP(hostname) !== 0) {\n if (policy.host_literal_policy === 'reject') {\n return {\n allowed: false,\n rule: 'host_literal_policy:reject',\n message: `Bare IP literal ${hostname} rejected under Verified host policy.`,\n };\n }\n return checkIp(hostname, compiled);\n }\n\n return { allowed: true };\n}\n\n/**\n * Evaluate `url` after DNS resolution. Every resolved address must\n * pass the CIDR deny list. Callers pin the request to the first\n * allowed address to prevent DNS rebinding between lookup and connect.\n */\nexport function enforceSsrfPolicyResolved(\n url: URL,\n addresses: readonly string[],\n policy: SsrfPolicy = DEFAULT_SSRF_POLICY\n): PolicyResult {\n const syncResult = enforceSsrfPolicy(url, policy);\n if (!syncResult.allowed) return syncResult;\n const compiled = compile(policy);\n if (addresses.length === 0) {\n return {\n allowed: false,\n rule: 'dns_revalidation:no_addresses',\n message: `DNS resolution returned no addresses for ${url.hostname}.`,\n };\n }\n for (const addr of addresses) {\n const r = checkIp(addr, compiled);\n if (!r.allowed) return r;\n }\n return { allowed: true };\n}\n\nfunction checkIp(ip: string, compiled: CompiledPolicy): PolicyResult {\n if (isIPv4(ip)) {\n if (compiled.ipv4.check(ip, 'ipv4')) {\n return {\n allowed: false,\n rule: matchIpv4Rule(ip, compiled),\n message: `Resolved address ${ip} is in a denied IPv4 range.`,\n };\n }\n return { allowed: true };\n }\n if (isIPv6(ip)) {\n const normalized = normalizeIpv6(ip);\n // IPv4-mapped IPv6 (::ffff:a.b.c.d) is re-checked against v4 deny.\n const v4Suffix = extractMappedIpv4(normalized);\n if (v4Suffix && compiled.ipv4.check(v4Suffix, 'ipv4')) {\n return {\n allowed: false,\n rule: matchIpv4Rule(v4Suffix, compiled),\n message: `IPv4-mapped address ${ip} resolves to denied IPv4 ${v4Suffix}.`,\n };\n }\n if (compiled.ipv6.check(normalized, 'ipv6')) {\n return {\n allowed: false,\n rule: matchIpv6Rule(normalized, compiled),\n message: `Resolved address ${ip} is in a denied IPv6 range.`,\n };\n }\n return { allowed: true };\n }\n return {\n allowed: false,\n rule: 'invalid_ip',\n message: `Address ${ip} is not a valid IPv4 or IPv6 literal.`,\n };\n}\n\nfunction stripBrackets(host: string): string {\n return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;\n}\n\nfunction normalizeIpv6(ip: string): string {\n // BlockList expects normalized lowercase; URL hostname is lowercase\n // but IPv6 literals may arrive with varied compression.\n return stripBrackets(ip).toLowerCase();\n}\n\nfunction extractMappedIpv4(ipv6: string): string | null {\n const m = /^::ffff:([0-9a-f:]+)$/i.exec(ipv6);\n if (!m) return null;\n const tail = m[1] ?? '';\n if (isIPv4(tail)) return tail;\n // ::ffff:c0a8:0101 form — convert four hex halves to dotted quad.\n const parts = tail.split(':');\n if (parts.length === 2 && parts.every(p => /^[0-9a-f]{1,4}$/.test(p))) {\n const hi = parseInt(parts[0] ?? '0', 16);\n const lo = parseInt(parts[1] ?? '0', 16);\n return `${(hi >> 8) & 0xff}.${hi & 0xff}.${(lo >> 8) & 0xff}.${lo & 0xff}`;\n }\n return null;\n}\n\nfunction matchIpv4Rule(ip: string, compiled: CompiledPolicy): string {\n for (const cidr of compiled.policy.hosts_denied_ipv4_cidrs) {\n const [base = '', prefix = '0'] = cidr.split('/');\n const bl = new BlockList();\n bl.addSubnet(base, Number(prefix), 'ipv4');\n if (bl.check(ip, 'ipv4')) return `hosts_denied_ipv4_cidrs:${cidr}`;\n }\n return 'hosts_denied_ipv4_cidrs:unknown';\n}\n\nfunction matchIpv6Rule(ip: string, compiled: CompiledPolicy): string {\n for (const cidr of compiled.policy.hosts_denied_ipv6_cidrs) {\n const [base = '', prefix = '0'] = cidr.split('/');\n const bl = new BlockList();\n bl.addSubnet(normalizeIpv6(base), Number(prefix), 'ipv6');\n if (bl.check(ip, 'ipv6')) return `hosts_denied_ipv6_cidrs:${cidr}`;\n }\n return 'hosts_denied_ipv6_cidrs:unknown';\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 UPPER_HEX = "0123456789ABCDEF";
|
|
8
2
|
function isUnreservedByte(b) {
|
|
9
3
|
return b >= 65 && b <= 90 || // A-Z
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/substitution/rfc3986.ts"],"sourcesContent":["/**\n * RFC 3986 percent-encoding primitives shared by the observer (verifier)\n * and encoder (producer) modules. One implementation, one bug-fix path.\n *\n * - `encodeUnreserved` implements the §2.1/§2.3 unreserved-whitelist rule\n * used by catalog-item macro substitution (#2620): only ALPHA / DIGIT\n * / \"-\" / \".\" / \"_\" / \"~\" remain unescaped, non-ASCII is percent-encoded\n * after UTF-8 (§2.5), hex digits are uppercase (§2.1).\n *\n * - `equalUnderHexCasePolicy` implements the verifier-side comparison:\n * byte-for-byte equality, but the two hex digits inside a `%NN` triplet\n * compare case-insensitively so a producer emitting `%c3%a9` is not\n * falsely flagged when the fixture says `%C3%A9`. Bytes outside triplets\n * compare case-sensitively.\n *\n * - `isUnreservedOnly` is the stricter check behind\n * `rfc3986_unreserved_only_at_macro_position`: every byte in the input\n * is either an unreserved character or a valid `%NN` triplet.\n */\n\nconst UPPER_HEX = '0123456789ABCDEF';\n\n/**\n * True iff the byte is RFC 3986 §2.3 `unreserved`:\n * `ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"`.\n */\nfunction isUnreservedByte(b: number): boolean {\n return (\n (b >= 0x41 && b <= 0x5a) || // A-Z\n (b >= 0x61 && b <= 0x7a) || // a-z\n (b >= 0x30 && b <= 0x39) || // 0-9\n b === 0x2d || // -\n b === 0x2e || // .\n b === 0x5f || // _\n b === 0x7e // ~\n );\n}\n\n/**\n * Percent-encode `raw` for a URL-value context per the catalog-item macro\n * substitution rule. Only unreserved characters pass through; everything\n * else (including sub-delims `(`, `)`, `&`, `=`, reserved chars, CRLF,\n * AdCP macro braces, bidi overrides, non-ASCII) is UTF-8 encoded then\n * percent-encoded with uppercase hex digits.\n */\nexport function encodeUnreserved(raw: string): string {\n const bytes = new TextEncoder().encode(raw);\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i] as number;\n if (isUnreservedByte(b)) {\n out += String.fromCharCode(b);\n } else {\n out += '%' + UPPER_HEX[(b >> 4) & 0xf] + UPPER_HEX[b & 0xf];\n }\n }\n return out;\n}\n\n/**\n * Compare two encoded strings under the contract's hex-case policy:\n * producers SHOULD emit uppercase hex (§2.1), but verifiers MUST accept\n * either case within a `%NN` triplet. Bytes outside triplets compare\n * case-sensitively — an attacker-controlled value differing by a letter\n * case outside a triplet is a real divergence, not a hex-case variant.\n */\nexport function equalUnderHexCasePolicy(observed: string, expected: string): boolean {\n if (observed.length !== expected.length) return false;\n for (let i = 0; i < observed.length; i++) {\n const o = observed[i];\n const e = expected[i];\n if (o === '%' && e === '%' && i + 2 < observed.length) {\n const oh = observed.slice(i + 1, i + 3);\n const eh = expected.slice(i + 1, i + 3);\n if (!isHex(oh) || !isHex(eh)) return false;\n if (oh.toUpperCase() !== eh.toUpperCase()) return false;\n i += 2;\n continue;\n }\n if (o !== e) return false;\n }\n return true;\n}\n\n/**\n * True iff every byte in `encoded` is either an unreserved character\n * (passes through unchanged) or a well-formed `%NN` triplet. Stricter\n * than `equalUnderHexCasePolicy` — catches producers that use a\n * reserved-char allowlist (e.g., `encodeURI`) rather than the\n * unreserved-whitelist the #2620 rule requires.\n */\nexport function isUnreservedOnly(encoded: string): boolean {\n for (let i = 0; i < encoded.length; i++) {\n const c = encoded.charCodeAt(i);\n if (c > 0x7f) return false; // non-ASCII byte => not encoded\n if (isUnreservedByte(c)) continue;\n if (encoded[i] === '%' && i + 2 < encoded.length && isHex(encoded.slice(i + 1, i + 3))) {\n i += 2;\n continue;\n }\n return false;\n }\n return true;\n}\n\n/**\n * Find the first byte offset where `observed` and `expected` diverge\n * under the hex-case policy, or `-1` if they match. Offsets are in\n * characters of the encoded string (not decoded bytes), suitable for\n * error reports that need to point at the offending percent-triplet.\n */\nexport function divergenceOffset(observed: string, expected: string): number {\n const min = Math.min(observed.length, expected.length);\n for (let i = 0; i < min; i++) {\n const o = observed[i];\n const e = expected[i];\n if (o === '%' && e === '%' && i + 2 < observed.length && i + 2 < expected.length) {\n const oh = observed.slice(i + 1, i + 3);\n const eh = expected.slice(i + 1, i + 3);\n if (!isHex(oh) || !isHex(eh) || oh.toUpperCase() !== eh.toUpperCase()) return i;\n i += 2;\n continue;\n }\n if (o !== e) return i;\n }\n return observed.length === expected.length ? -1 : min;\n}\n\nfunction isHex(s: string): boolean {\n return /^[0-9A-Fa-f]{2}$/.test(s);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/substitution/rfc3986.ts"],"sourcesContent":["/**\n * RFC 3986 percent-encoding primitives shared by the observer (verifier)\n * and encoder (producer) modules. One implementation, one bug-fix path.\n *\n * - `encodeUnreserved` implements the §2.1/§2.3 unreserved-whitelist rule\n * used by catalog-item macro substitution (#2620): only ALPHA / DIGIT\n * / \"-\" / \".\" / \"_\" / \"~\" remain unescaped, non-ASCII is percent-encoded\n * after UTF-8 (§2.5), hex digits are uppercase (§2.1).\n *\n * - `equalUnderHexCasePolicy` implements the verifier-side comparison:\n * byte-for-byte equality, but the two hex digits inside a `%NN` triplet\n * compare case-insensitively so a producer emitting `%c3%a9` is not\n * falsely flagged when the fixture says `%C3%A9`. Bytes outside triplets\n * compare case-sensitively.\n *\n * - `isUnreservedOnly` is the stricter check behind\n * `rfc3986_unreserved_only_at_macro_position`: every byte in the input\n * is either an unreserved character or a valid `%NN` triplet.\n */\n\nconst UPPER_HEX = '0123456789ABCDEF';\n\n/**\n * True iff the byte is RFC 3986 §2.3 `unreserved`:\n * `ALPHA / DIGIT / \"-\" / \".\" / \"_\" / \"~\"`.\n */\nfunction isUnreservedByte(b: number): boolean {\n return (\n (b >= 0x41 && b <= 0x5a) || // A-Z\n (b >= 0x61 && b <= 0x7a) || // a-z\n (b >= 0x30 && b <= 0x39) || // 0-9\n b === 0x2d || // -\n b === 0x2e || // .\n b === 0x5f || // _\n b === 0x7e // ~\n );\n}\n\n/**\n * Percent-encode `raw` for a URL-value context per the catalog-item macro\n * substitution rule. Only unreserved characters pass through; everything\n * else (including sub-delims `(`, `)`, `&`, `=`, reserved chars, CRLF,\n * AdCP macro braces, bidi overrides, non-ASCII) is UTF-8 encoded then\n * percent-encoded with uppercase hex digits.\n */\nexport function encodeUnreserved(raw: string): string {\n const bytes = new TextEncoder().encode(raw);\n let out = '';\n for (let i = 0; i < bytes.length; i++) {\n const b = bytes[i] as number;\n if (isUnreservedByte(b)) {\n out += String.fromCharCode(b);\n } else {\n out += '%' + UPPER_HEX[(b >> 4) & 0xf] + UPPER_HEX[b & 0xf];\n }\n }\n return out;\n}\n\n/**\n * Compare two encoded strings under the contract's hex-case policy:\n * producers SHOULD emit uppercase hex (§2.1), but verifiers MUST accept\n * either case within a `%NN` triplet. Bytes outside triplets compare\n * case-sensitively — an attacker-controlled value differing by a letter\n * case outside a triplet is a real divergence, not a hex-case variant.\n */\nexport function equalUnderHexCasePolicy(observed: string, expected: string): boolean {\n if (observed.length !== expected.length) return false;\n for (let i = 0; i < observed.length; i++) {\n const o = observed[i];\n const e = expected[i];\n if (o === '%' && e === '%' && i + 2 < observed.length) {\n const oh = observed.slice(i + 1, i + 3);\n const eh = expected.slice(i + 1, i + 3);\n if (!isHex(oh) || !isHex(eh)) return false;\n if (oh.toUpperCase() !== eh.toUpperCase()) return false;\n i += 2;\n continue;\n }\n if (o !== e) return false;\n }\n return true;\n}\n\n/**\n * True iff every byte in `encoded` is either an unreserved character\n * (passes through unchanged) or a well-formed `%NN` triplet. Stricter\n * than `equalUnderHexCasePolicy` — catches producers that use a\n * reserved-char allowlist (e.g., `encodeURI`) rather than the\n * unreserved-whitelist the #2620 rule requires.\n */\nexport function isUnreservedOnly(encoded: string): boolean {\n for (let i = 0; i < encoded.length; i++) {\n const c = encoded.charCodeAt(i);\n if (c > 0x7f) return false; // non-ASCII byte => not encoded\n if (isUnreservedByte(c)) continue;\n if (encoded[i] === '%' && i + 2 < encoded.length && isHex(encoded.slice(i + 1, i + 3))) {\n i += 2;\n continue;\n }\n return false;\n }\n return true;\n}\n\n/**\n * Find the first byte offset where `observed` and `expected` diverge\n * under the hex-case policy, or `-1` if they match. Offsets are in\n * characters of the encoded string (not decoded bytes), suitable for\n * error reports that need to point at the offending percent-triplet.\n */\nexport function divergenceOffset(observed: string, expected: string): number {\n const min = Math.min(observed.length, expected.length);\n for (let i = 0; i < min; i++) {\n const o = observed[i];\n const e = expected[i];\n if (o === '%' && e === '%' && i + 2 < observed.length && i + 2 < expected.length) {\n const oh = observed.slice(i + 1, i + 3);\n const eh = expected.slice(i + 1, i + 3);\n if (!isHex(oh) || !isHex(eh) || oh.toUpperCase() !== eh.toUpperCase()) return i;\n i += 2;\n continue;\n }\n if (o !== e) return i;\n }\n return observed.length === expected.length ? -1 : min;\n}\n\nfunction isHex(s: string): boolean {\n return /^[0-9A-Fa-f]{2}$/.test(s);\n}\n"],"mappings":"AAoBA,MAAM,YAAY;AAMlB,SAAS,iBAAiB,GAAoB;AAC5C,SACG,KAAK,MAAQ,KAAK;AAAA,EAClB,KAAK,MAAQ,KAAK;AAAA,EAClB,KAAK,MAAQ,KAAK;AAAA,EACnB,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAAA,EACN,MAAM;AAEV;AASO,SAAS,iBAAiB,KAAqB;AACpD,QAAM,QAAQ,IAAI,YAAY,EAAE,OAAO,GAAG;AAC1C,MAAI,MAAM;AACV,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,IAAI,MAAM,CAAC;AACjB,QAAI,iBAAiB,CAAC,GAAG;AACvB,aAAO,OAAO,aAAa,CAAC;AAAA,IAC9B,OAAO;AACL,aAAO,MAAM,UAAW,KAAK,IAAK,EAAG,IAAI,UAAU,IAAI,EAAG;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;AASO,SAAS,wBAAwB,UAAkB,UAA2B;AACnF,MAAI,SAAS,WAAW,SAAS,OAAQ,QAAO;AAChD,WAAS,IAAI,GAAG,IAAI,SAAS,QAAQ,KAAK;AACxC,UAAM,IAAI,SAAS,CAAC;AACpB,UAAM,IAAI,SAAS,CAAC;AACpB,QAAI,MAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,QAAQ;AACrD,YAAM,KAAK,SAAS,MAAM,IAAI,GAAG,IAAI,CAAC;AACtC,YAAM,KAAK,SAAS,MAAM,IAAI,GAAG,IAAI,CAAC;AACtC,UAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,EAAG,QAAO;AACrC,UAAI,GAAG,YAAY,MAAM,GAAG,YAAY,EAAG,QAAO;AAClD,WAAK;AACL;AAAA,IACF;AACA,QAAI,MAAM,EAAG,QAAO;AAAA,EACtB;AACA,SAAO;AACT;AASO,SAAS,iBAAiB,SAA0B;AACzD,WAAS,IAAI,GAAG,IAAI,QAAQ,QAAQ,KAAK;AACvC,UAAM,IAAI,QAAQ,WAAW,CAAC;AAC9B,QAAI,IAAI,IAAM,QAAO;AACrB,QAAI,iBAAiB,CAAC,EAAG;AACzB,QAAI,QAAQ,CAAC,MAAM,OAAO,IAAI,IAAI,QAAQ,UAAU,MAAM,QAAQ,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG;AACtF,WAAK;AACL;AAAA,IACF;AACA,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAQO,SAAS,iBAAiB,UAAkB,UAA0B;AAC3E,QAAM,MAAM,KAAK,IAAI,SAAS,QAAQ,SAAS,MAAM;AACrD,WAAS,IAAI,GAAG,IAAI,KAAK,KAAK;AAC5B,UAAM,IAAI,SAAS,CAAC;AACpB,UAAM,IAAI,SAAS,CAAC;AACpB,QAAI,MAAM,OAAO,MAAM,OAAO,IAAI,IAAI,SAAS,UAAU,IAAI,IAAI,SAAS,QAAQ;AAChF,YAAM,KAAK,SAAS,MAAM,IAAI,GAAG,IAAI,CAAC;AACtC,YAAM,KAAK,SAAS,MAAM,IAAI,GAAG,IAAI,CAAC;AACtC,UAAI,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,GAAG,YAAY,MAAM,GAAG,YAAY,EAAG,QAAO;AAC9E,WAAK;AACL;AAAA,IACF;AACA,QAAI,MAAM,EAAG,QAAO;AAAA,EACtB;AACA,SAAO,SAAS,WAAW,SAAS,SAAS,KAAK;AACpD;AAEA,SAAS,MAAM,GAAoB;AACjC,SAAO,mBAAmB,KAAK,CAAC;AAClC;","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 { encodeUnreserved } from "./rfc3986.mjs";
|
|
8
2
|
const UNIVERSAL_MACRO = /\{[A-Z][A-Z0-9_]*\}/g;
|
|
9
3
|
const NATIVE_TOKEN_SHAPE = /^(?:%%.+%%|\{\{.+\}\}|\$\{.+\}|\[[A-Z][A-Z0-9_]*\])$/;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/substitution/translate.ts"],"sourcesContent":["/**\n * Seller-side helper that translates universal macro tokens in a pixel URL's\n * query parameter VALUES using a caller-supplied mapping. Macro tokens in key\n * position pass through untouched — only query-parameter values are translated.\n *\n * Universal macros match `\\{[A-Z][A-Z0-9_]*\\}` (upper-snake, single braces).\n * Native ad-server tokens (`%%X%%`, `{{x}}`) are not matched and pass through.\n *\n * Per query parameter:\n * - No universal macro → left untouched (literal param passes through).\n * - All macros in the param are in `mapping` → every occurrence replaced in\n * a single pass. A `native` entry is inserted raw; a `value` entry is\n * percent-encoded with the RFC 3986 unreserved whitelist.\n * - Any macro in the param is NOT in `mapping` → the entire parameter is\n * dropped, its key is recorded in `dropped_params`, and each unmapped\n * macro token is recorded in `unmapped_macros`.\n *\n * The substitution is a single pass: a translated value that itself contains\n * `{…}` is not re-expanded.\n *\n * A URL ending in a bare `?` with no parameters returns without the trailing\n * `?` (e.g. `https://px.example/i?` → `https://px.example/i`).\n *\n * Privacy note: dropping a parameter whose macro is unmapped silently removes\n * that tracker — including consent/privacy macros (`{GDPR_CONSENT}`,\n * `{US_PRIVACY}`) if a mapping is forgotten. Consent/privacy macros that get\n * dropped are surfaced separately in `dropped_consent_macros`; callers SHOULD\n * inspect it (and `unmapped_macros`) so a missing consent signal is caught\n * rather than shipped as a degraded pixel.\n *\n * URLSearchParams is intentionally avoided — it force-encodes values and would\n * corrupt native ad-server tokens (e.g. `%%GDPR%%`). Query manipulation is\n * done textually so native tokens remain raw and value-encoding is controlled.\n */\n\nimport { encodeUnreserved } from './rfc3986';\n\n/** Universal macro token pattern: `{UPPER_SNAKE}`. */\nconst UNIVERSAL_MACRO = /\\{[A-Z][A-Z0-9_]*\\}/g;\n\n/**\n * Shapes of common ad-server native tokens (`%%X%%`, `{{x}}`, `${x}`, `[X]`).\n * A legitimate `value` entry — a literal data value — never takes this shape,\n * so a `value` that matches it almost certainly belongs in a `native` entry.\n * The bracket form is constrained to upper-snake token names (VAST-style, e.g.\n * `[CACHEBUSTING]`) so ordinary bracketed values like `[1,2,3]` or `[redacted]`\n * are not flagged.\n */\nconst NATIVE_TOKEN_SHAPE = /^(?:%%.+%%|\\{\\{.+\\}\\}|\\$\\{.+\\}|\\[[A-Z][A-Z0-9_]*\\])$/;\n\n/**\n * Consent/privacy-signalling universal macros. Dropping one because its\n * mapping was forgotten is a compliance hazard, so these are reported\n * separately from benign drops.\n */\nconst CONSENT_MACROS = new Set<string>([\n '{GDPR}',\n '{GDPR_CONSENT}',\n '{US_PRIVACY}',\n '{GPP_STRING}',\n '{GPP_SID}',\n '{LIMIT_AD_TRACKING}',\n]);\n\n/**\n * A mapping from universal macro token (e.g. `'{GDPR}'`) to either:\n * - `{ native: string }` — inserted verbatim (not percent-encoded), for\n * downstream ad-server tokens like `%%GDPR%%`.\n * - `{ value: string }` — inserted after RFC 3986 unreserved-whitelist\n * percent-encoding, for seller-supplied data values.\n */\nexport type MacroMapping = Record<string, { native: string } | { value: string }>;\n\n/** Result of {@link translateUniversalMacros}. */\nexport interface TranslateResult {\n /** The translated URL. Base, path, and fragment are unchanged. */\n url: string;\n /**\n * Keys of query parameters that were dropped due to unmapped macros.\n * One entry per dropped parameter instance, so a repeated key can appear\n * more than once.\n */\n dropped_params: string[];\n /**\n * Unique unmapped macro tokens encountered across all dropped parameters.\n * Deduplicated: the same token is recorded at most once regardless of how\n * many parameters reference it.\n */\n unmapped_macros: string[];\n /**\n * Subset of `unmapped_macros` that are consent/privacy signals (`{GDPR}`,\n * `{GDPR_CONSENT}`, `{US_PRIVACY}`, `{GPP_STRING}`, `{GPP_SID}`,\n * `{LIMIT_AD_TRACKING}`). Surfaced separately so a forgotten-mapping drop of\n * a compliance-relevant macro isn't lost among benign drops. Deduplicated.\n */\n dropped_consent_macros: string[];\n /**\n * Macro tokens whose `value` entry looks like a native ad-server token\n * (`%%…%%`, `{{…}}`, `${…}`, `[UPPER_SNAKE]`). Such a value will be\n * percent-encoded and break at impression time — almost always it should\n * have been a `native` entry. Deduplicated.\n *\n * Note: this is mapping-scoped, not URL-scoped — it lints the whole\n * `mapping`, so a suspect entry is reported even if its macro never appears\n * in `input_pixel_url` (unlike `unmapped_macros`/`dropped_params`, which are\n * URL-scoped). Empty when no mapping is suspect.\n */\n suspect_native_values: string[];\n}\n\n/**\n * Translate universal macro tokens in the query-parameter values of\n * `input_pixel_url` using `mapping`. See module-level documentation for the\n * substitution rules and the privacy note on dropped parameters.\n */\nexport function translateUniversalMacros(input_pixel_url: string, mapping: MacroMapping): TranslateResult {\n const suspect_native_values: string[] = [];\n const suspectSeen = new Set<string>();\n for (const [macro, entry] of Object.entries(mapping)) {\n if ('value' in entry && NATIVE_TOKEN_SHAPE.test(entry.value) && !suspectSeen.has(macro)) {\n suspectSeen.add(macro);\n suspect_native_values.push(macro);\n }\n }\n\n // Split off the fragment first so it is never touched.\n const fragmentIdx = input_pixel_url.indexOf('#');\n const withoutFragment = fragmentIdx === -1 ? input_pixel_url : input_pixel_url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : input_pixel_url.slice(fragmentIdx);\n\n // Split base (scheme + host + path) from query.\n const queryIdx = withoutFragment.indexOf('?');\n if (queryIdx === -1) {\n return {\n url: input_pixel_url,\n dropped_params: [],\n unmapped_macros: [],\n dropped_consent_macros: [],\n suspect_native_values,\n };\n }\n\n const base = withoutFragment.slice(0, queryIdx);\n const rawQuery = withoutFragment.slice(queryIdx + 1);\n\n const dropped_params: string[] = [];\n const unmapped_macros: string[] = [];\n const unmappedSeen = new Set<string>();\n const dropped_consent_macros: string[] = [];\n const consentSeen = new Set<string>();\n\n const outputParts: string[] = [];\n\n for (const rawParam of rawQuery.split('&')) {\n const eqIdx = rawParam.indexOf('=');\n const key = eqIdx === -1 ? rawParam : rawParam.slice(0, eqIdx);\n const value = eqIdx === -1 ? '' : rawParam.slice(eqIdx + 1);\n\n // Find every universal macro token in the raw value.\n const tokens = value.match(UNIVERSAL_MACRO);\n\n if (!tokens) {\n // No universal macros — pass through verbatim.\n outputParts.push(rawParam);\n continue;\n }\n\n // Check for any unmapped macro in this parameter.\n const missing = tokens.filter(t => !(t in mapping));\n if (missing.length > 0) {\n dropped_params.push(key);\n for (const m of missing) {\n if (!unmappedSeen.has(m)) {\n unmappedSeen.add(m);\n unmapped_macros.push(m);\n }\n if (CONSENT_MACROS.has(m) && !consentSeen.has(m)) {\n consentSeen.add(m);\n dropped_consent_macros.push(m);\n }\n }\n continue;\n }\n\n // All macros are mapped — replace in a single pass.\n // String.replace with a global regex does not re-scan substituted output,\n // so macro tokens inside a translated value are never expanded.\n const translated = value.replace(UNIVERSAL_MACRO, token => {\n const entry = mapping[token];\n if (!entry) {\n // Unreachable: all tokens were verified above, but TypeScript narrows.\n return token;\n }\n return 'native' in entry ? entry.native : encodeUnreserved(entry.value);\n });\n\n outputParts.push(`${key}=${translated}`);\n }\n\n const newQuery = outputParts.join('&');\n const url = newQuery ? `${base}?${newQuery}${fragment}` : `${base}${fragment}`;\n\n return { url, dropped_params, unmapped_macros, dropped_consent_macros, suspect_native_values };\n}\n"],"mappings":";;;;;;AAmCA,SAAS,wBAAwB;AAGjC,MAAM,kBAAkB;AAUxB,MAAM,qBAAqB;AAO3B,MAAM,iBAAiB,oBAAI,IAAY;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAqDM,SAAS,yBAAyB,iBAAyB,SAAwC;AACxG,QAAM,wBAAkC,CAAC;AACzC,QAAM,cAAc,oBAAI,IAAY;AACpC,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACpD,QAAI,WAAW,SAAS,mBAAmB,KAAK,MAAM,KAAK,KAAK,CAAC,YAAY,IAAI,KAAK,GAAG;AACvF,kBAAY,IAAI,KAAK;AACrB,4BAAsB,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAGA,QAAM,cAAc,gBAAgB,QAAQ,GAAG;AAC/C,QAAM,kBAAkB,gBAAgB,KAAK,kBAAkB,gBAAgB,MAAM,GAAG,WAAW;AACnG,QAAM,WAAW,gBAAgB,KAAK,KAAK,gBAAgB,MAAM,WAAW;AAG5E,QAAM,WAAW,gBAAgB,QAAQ,GAAG;AAC5C,MAAI,aAAa,IAAI;AACnB,WAAO;AAAA,MACL,KAAK;AAAA,MACL,gBAAgB,CAAC;AAAA,MACjB,iBAAiB,CAAC;AAAA,MAClB,wBAAwB,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,gBAAgB,MAAM,GAAG,QAAQ;AAC9C,QAAM,WAAW,gBAAgB,MAAM,WAAW,CAAC;AAEnD,QAAM,iBAA2B,CAAC;AAClC,QAAM,kBAA4B,CAAC;AACnC,QAAM,eAAe,oBAAI,IAAY;AACrC,QAAM,yBAAmC,CAAC;AAC1C,QAAM,cAAc,oBAAI,IAAY;AAEpC,QAAM,cAAwB,CAAC;AAE/B,aAAW,YAAY,SAAS,MAAM,GAAG,GAAG;AAC1C,UAAM,QAAQ,SAAS,QAAQ,GAAG;AAClC,UAAM,MAAM,UAAU,KAAK,WAAW,SAAS,MAAM,GAAG,KAAK;AAC7D,UAAM,QAAQ,UAAU,KAAK,KAAK,SAAS,MAAM,QAAQ,CAAC;AAG1D,UAAM,SAAS,MAAM,MAAM,eAAe;AAE1C,QAAI,CAAC,QAAQ;AAEX,kBAAY,KAAK,QAAQ;AACzB;AAAA,IACF;AAGA,UAAM,UAAU,OAAO,OAAO,OAAK,EAAE,KAAK,QAAQ;AAClD,QAAI,QAAQ,SAAS,GAAG;AACtB,qBAAe,KAAK,GAAG;AACvB,iBAAW,KAAK,SAAS;AACvB,YAAI,CAAC,aAAa,IAAI,CAAC,GAAG;AACxB,uBAAa,IAAI,CAAC;AAClB,0BAAgB,KAAK,CAAC;AAAA,QACxB;AACA,YAAI,eAAe,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,GAAG;AAChD,sBAAY,IAAI,CAAC;AACjB,iCAAuB,KAAK,CAAC;AAAA,QAC/B;AAAA,MACF;AACA;AAAA,IACF;AAKA,UAAM,aAAa,MAAM,QAAQ,iBAAiB,WAAS;AACzD,YAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAI,CAAC,OAAO;AAEV,eAAO;AAAA,MACT;AACA,aAAO,YAAY,QAAQ,MAAM,SAAS,iBAAiB,MAAM,KAAK;AAAA,IACxE,CAAC;AAED,gBAAY,KAAK,GAAG,GAAG,IAAI,UAAU,EAAE;AAAA,EACzC;AAEA,QAAM,WAAW,YAAY,KAAK,GAAG;AACrC,QAAM,MAAM,WAAW,GAAG,IAAI,IAAI,QAAQ,GAAG,QAAQ,KAAK,GAAG,IAAI,GAAG,QAAQ;AAE5E,SAAO,EAAE,KAAK,gBAAgB,iBAAiB,wBAAwB,sBAAsB;AAC/F;","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/substitution/translate.ts"],"sourcesContent":["/**\n * Seller-side helper that translates universal macro tokens in a pixel URL's\n * query parameter VALUES using a caller-supplied mapping. Macro tokens in key\n * position pass through untouched — only query-parameter values are translated.\n *\n * Universal macros match `\\{[A-Z][A-Z0-9_]*\\}` (upper-snake, single braces).\n * Native ad-server tokens (`%%X%%`, `{{x}}`) are not matched and pass through.\n *\n * Per query parameter:\n * - No universal macro → left untouched (literal param passes through).\n * - All macros in the param are in `mapping` → every occurrence replaced in\n * a single pass. A `native` entry is inserted raw; a `value` entry is\n * percent-encoded with the RFC 3986 unreserved whitelist.\n * - Any macro in the param is NOT in `mapping` → the entire parameter is\n * dropped, its key is recorded in `dropped_params`, and each unmapped\n * macro token is recorded in `unmapped_macros`.\n *\n * The substitution is a single pass: a translated value that itself contains\n * `{…}` is not re-expanded.\n *\n * A URL ending in a bare `?` with no parameters returns without the trailing\n * `?` (e.g. `https://px.example/i?` → `https://px.example/i`).\n *\n * Privacy note: dropping a parameter whose macro is unmapped silently removes\n * that tracker — including consent/privacy macros (`{GDPR_CONSENT}`,\n * `{US_PRIVACY}`) if a mapping is forgotten. Consent/privacy macros that get\n * dropped are surfaced separately in `dropped_consent_macros`; callers SHOULD\n * inspect it (and `unmapped_macros`) so a missing consent signal is caught\n * rather than shipped as a degraded pixel.\n *\n * URLSearchParams is intentionally avoided — it force-encodes values and would\n * corrupt native ad-server tokens (e.g. `%%GDPR%%`). Query manipulation is\n * done textually so native tokens remain raw and value-encoding is controlled.\n */\n\nimport { encodeUnreserved } from './rfc3986';\n\n/** Universal macro token pattern: `{UPPER_SNAKE}`. */\nconst UNIVERSAL_MACRO = /\\{[A-Z][A-Z0-9_]*\\}/g;\n\n/**\n * Shapes of common ad-server native tokens (`%%X%%`, `{{x}}`, `${x}`, `[X]`).\n * A legitimate `value` entry — a literal data value — never takes this shape,\n * so a `value` that matches it almost certainly belongs in a `native` entry.\n * The bracket form is constrained to upper-snake token names (VAST-style, e.g.\n * `[CACHEBUSTING]`) so ordinary bracketed values like `[1,2,3]` or `[redacted]`\n * are not flagged.\n */\nconst NATIVE_TOKEN_SHAPE = /^(?:%%.+%%|\\{\\{.+\\}\\}|\\$\\{.+\\}|\\[[A-Z][A-Z0-9_]*\\])$/;\n\n/**\n * Consent/privacy-signalling universal macros. Dropping one because its\n * mapping was forgotten is a compliance hazard, so these are reported\n * separately from benign drops.\n */\nconst CONSENT_MACROS = new Set<string>([\n '{GDPR}',\n '{GDPR_CONSENT}',\n '{US_PRIVACY}',\n '{GPP_STRING}',\n '{GPP_SID}',\n '{LIMIT_AD_TRACKING}',\n]);\n\n/**\n * A mapping from universal macro token (e.g. `'{GDPR}'`) to either:\n * - `{ native: string }` — inserted verbatim (not percent-encoded), for\n * downstream ad-server tokens like `%%GDPR%%`.\n * - `{ value: string }` — inserted after RFC 3986 unreserved-whitelist\n * percent-encoding, for seller-supplied data values.\n */\nexport type MacroMapping = Record<string, { native: string } | { value: string }>;\n\n/** Result of {@link translateUniversalMacros}. */\nexport interface TranslateResult {\n /** The translated URL. Base, path, and fragment are unchanged. */\n url: string;\n /**\n * Keys of query parameters that were dropped due to unmapped macros.\n * One entry per dropped parameter instance, so a repeated key can appear\n * more than once.\n */\n dropped_params: string[];\n /**\n * Unique unmapped macro tokens encountered across all dropped parameters.\n * Deduplicated: the same token is recorded at most once regardless of how\n * many parameters reference it.\n */\n unmapped_macros: string[];\n /**\n * Subset of `unmapped_macros` that are consent/privacy signals (`{GDPR}`,\n * `{GDPR_CONSENT}`, `{US_PRIVACY}`, `{GPP_STRING}`, `{GPP_SID}`,\n * `{LIMIT_AD_TRACKING}`). Surfaced separately so a forgotten-mapping drop of\n * a compliance-relevant macro isn't lost among benign drops. Deduplicated.\n */\n dropped_consent_macros: string[];\n /**\n * Macro tokens whose `value` entry looks like a native ad-server token\n * (`%%…%%`, `{{…}}`, `${…}`, `[UPPER_SNAKE]`). Such a value will be\n * percent-encoded and break at impression time — almost always it should\n * have been a `native` entry. Deduplicated.\n *\n * Note: this is mapping-scoped, not URL-scoped — it lints the whole\n * `mapping`, so a suspect entry is reported even if its macro never appears\n * in `input_pixel_url` (unlike `unmapped_macros`/`dropped_params`, which are\n * URL-scoped). Empty when no mapping is suspect.\n */\n suspect_native_values: string[];\n}\n\n/**\n * Translate universal macro tokens in the query-parameter values of\n * `input_pixel_url` using `mapping`. See module-level documentation for the\n * substitution rules and the privacy note on dropped parameters.\n */\nexport function translateUniversalMacros(input_pixel_url: string, mapping: MacroMapping): TranslateResult {\n const suspect_native_values: string[] = [];\n const suspectSeen = new Set<string>();\n for (const [macro, entry] of Object.entries(mapping)) {\n if ('value' in entry && NATIVE_TOKEN_SHAPE.test(entry.value) && !suspectSeen.has(macro)) {\n suspectSeen.add(macro);\n suspect_native_values.push(macro);\n }\n }\n\n // Split off the fragment first so it is never touched.\n const fragmentIdx = input_pixel_url.indexOf('#');\n const withoutFragment = fragmentIdx === -1 ? input_pixel_url : input_pixel_url.slice(0, fragmentIdx);\n const fragment = fragmentIdx === -1 ? '' : input_pixel_url.slice(fragmentIdx);\n\n // Split base (scheme + host + path) from query.\n const queryIdx = withoutFragment.indexOf('?');\n if (queryIdx === -1) {\n return {\n url: input_pixel_url,\n dropped_params: [],\n unmapped_macros: [],\n dropped_consent_macros: [],\n suspect_native_values,\n };\n }\n\n const base = withoutFragment.slice(0, queryIdx);\n const rawQuery = withoutFragment.slice(queryIdx + 1);\n\n const dropped_params: string[] = [];\n const unmapped_macros: string[] = [];\n const unmappedSeen = new Set<string>();\n const dropped_consent_macros: string[] = [];\n const consentSeen = new Set<string>();\n\n const outputParts: string[] = [];\n\n for (const rawParam of rawQuery.split('&')) {\n const eqIdx = rawParam.indexOf('=');\n const key = eqIdx === -1 ? rawParam : rawParam.slice(0, eqIdx);\n const value = eqIdx === -1 ? '' : rawParam.slice(eqIdx + 1);\n\n // Find every universal macro token in the raw value.\n const tokens = value.match(UNIVERSAL_MACRO);\n\n if (!tokens) {\n // No universal macros — pass through verbatim.\n outputParts.push(rawParam);\n continue;\n }\n\n // Check for any unmapped macro in this parameter.\n const missing = tokens.filter(t => !(t in mapping));\n if (missing.length > 0) {\n dropped_params.push(key);\n for (const m of missing) {\n if (!unmappedSeen.has(m)) {\n unmappedSeen.add(m);\n unmapped_macros.push(m);\n }\n if (CONSENT_MACROS.has(m) && !consentSeen.has(m)) {\n consentSeen.add(m);\n dropped_consent_macros.push(m);\n }\n }\n continue;\n }\n\n // All macros are mapped — replace in a single pass.\n // String.replace with a global regex does not re-scan substituted output,\n // so macro tokens inside a translated value are never expanded.\n const translated = value.replace(UNIVERSAL_MACRO, token => {\n const entry = mapping[token];\n if (!entry) {\n // Unreachable: all tokens were verified above, but TypeScript narrows.\n return token;\n }\n return 'native' in entry ? entry.native : encodeUnreserved(entry.value);\n });\n\n outputParts.push(`${key}=${translated}`);\n }\n\n const newQuery = outputParts.join('&');\n const url = newQuery ? `${base}?${newQuery}${fragment}` : `${base}${fragment}`;\n\n return { url, dropped_params, unmapped_macros, dropped_consent_macros, suspect_native_values };\n}\n"],"mappings":"AAmCA,SAAS,wBAAwB;AAGjC,MAAM,kBAAkB;AAUxB,MAAM,qBAAqB;AAO3B,MAAM,iBAAiB,oBAAI,IAAY;AAAA,EACrC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAqDM,SAAS,yBAAyB,iBAAyB,SAAwC;AACxG,QAAM,wBAAkC,CAAC;AACzC,QAAM,cAAc,oBAAI,IAAY;AACpC,aAAW,CAAC,OAAO,KAAK,KAAK,OAAO,QAAQ,OAAO,GAAG;AACpD,QAAI,WAAW,SAAS,mBAAmB,KAAK,MAAM,KAAK,KAAK,CAAC,YAAY,IAAI,KAAK,GAAG;AACvF,kBAAY,IAAI,KAAK;AACrB,4BAAsB,KAAK,KAAK;AAAA,IAClC;AAAA,EACF;AAGA,QAAM,cAAc,gBAAgB,QAAQ,GAAG;AAC/C,QAAM,kBAAkB,gBAAgB,KAAK,kBAAkB,gBAAgB,MAAM,GAAG,WAAW;AACnG,QAAM,WAAW,gBAAgB,KAAK,KAAK,gBAAgB,MAAM,WAAW;AAG5E,QAAM,WAAW,gBAAgB,QAAQ,GAAG;AAC5C,MAAI,aAAa,IAAI;AACnB,WAAO;AAAA,MACL,KAAK;AAAA,MACL,gBAAgB,CAAC;AAAA,MACjB,iBAAiB,CAAC;AAAA,MAClB,wBAAwB,CAAC;AAAA,MACzB;AAAA,IACF;AAAA,EACF;AAEA,QAAM,OAAO,gBAAgB,MAAM,GAAG,QAAQ;AAC9C,QAAM,WAAW,gBAAgB,MAAM,WAAW,CAAC;AAEnD,QAAM,iBAA2B,CAAC;AAClC,QAAM,kBAA4B,CAAC;AACnC,QAAM,eAAe,oBAAI,IAAY;AACrC,QAAM,yBAAmC,CAAC;AAC1C,QAAM,cAAc,oBAAI,IAAY;AAEpC,QAAM,cAAwB,CAAC;AAE/B,aAAW,YAAY,SAAS,MAAM,GAAG,GAAG;AAC1C,UAAM,QAAQ,SAAS,QAAQ,GAAG;AAClC,UAAM,MAAM,UAAU,KAAK,WAAW,SAAS,MAAM,GAAG,KAAK;AAC7D,UAAM,QAAQ,UAAU,KAAK,KAAK,SAAS,MAAM,QAAQ,CAAC;AAG1D,UAAM,SAAS,MAAM,MAAM,eAAe;AAE1C,QAAI,CAAC,QAAQ;AAEX,kBAAY,KAAK,QAAQ;AACzB;AAAA,IACF;AAGA,UAAM,UAAU,OAAO,OAAO,OAAK,EAAE,KAAK,QAAQ;AAClD,QAAI,QAAQ,SAAS,GAAG;AACtB,qBAAe,KAAK,GAAG;AACvB,iBAAW,KAAK,SAAS;AACvB,YAAI,CAAC,aAAa,IAAI,CAAC,GAAG;AACxB,uBAAa,IAAI,CAAC;AAClB,0BAAgB,KAAK,CAAC;AAAA,QACxB;AACA,YAAI,eAAe,IAAI,CAAC,KAAK,CAAC,YAAY,IAAI,CAAC,GAAG;AAChD,sBAAY,IAAI,CAAC;AACjB,iCAAuB,KAAK,CAAC;AAAA,QAC/B;AAAA,MACF;AACA;AAAA,IACF;AAKA,UAAM,aAAa,MAAM,QAAQ,iBAAiB,WAAS;AACzD,YAAM,QAAQ,QAAQ,KAAK;AAC3B,UAAI,CAAC,OAAO;AAEV,eAAO;AAAA,MACT;AACA,aAAO,YAAY,QAAQ,MAAM,SAAS,iBAAiB,MAAM,KAAK;AAAA,IACxE,CAAC;AAED,gBAAY,KAAK,GAAG,GAAG,IAAI,UAAU,EAAE;AAAA,EACzC;AAEA,QAAM,WAAW,YAAY,KAAK,GAAG;AACrC,QAAM,MAAM,WAAW,GAAG,IAAI,IAAI,QAAQ,GAAG,QAAQ,KAAK,GAAG,IAAI,GAAG,QAAQ;AAE5E,SAAO,EAAE,KAAK,gBAAgB,iBAAiB,wBAAwB,sBAAsB;AAC/F;","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=types.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
|
const CATALOG_MACRO_VECTORS = Object.freeze([
|
|
8
2
|
Object.freeze({
|
|
9
3
|
name: "reserved-character-breakout",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/lib/substitution/vectors.ts"],"sourcesContent":["/**\n * Canonical catalog-item macro substitution vectors.\n *\n * Mirrors `static/test-vectors/catalog-macro-substitution.json` in the\n * upstream AdCP repo at the schema version this client targets. The\n * fixture is the source of truth for `raw_value` and `expected_encoded`\n * — these constants are synchronized copies so the library can be used\n * without filesystem side effects (works in browser bundles, avoids\n * shipping a JSON file through the tsc pipeline).\n *\n * The companion JSON at `fixtures/catalog-macro-substitution.json` is\n * byte-equal to the upstream fixture and is tested for parity.\n */\n\nimport type { CatalogMacroVector } from './types';\n\nexport type CatalogMacroVectorName =\n | 'reserved-character-breakout'\n | 'nested-expansion-preserved-as-literal'\n | 'crlf-injection-neutralized'\n | 'non-ascii-utf8-percent-encoding'\n | 'mixed-path-and-query-contexts'\n | 'bidi-override-neutralized'\n | 'url-scheme-injection-neutralized';\n\nexport const CATALOG_MACRO_VECTORS: readonly CatalogMacroVector[] = Object.freeze([\n Object.freeze({\n name: 'reserved-character-breakout',\n description:\n 'A catalog value containing `&` and `=` would break out of the surrounding query string if substituted raw. Percent-encoding neutralizes the breakout.',\n macro: '{GTIN}',\n value: '00013&cmd=drop',\n template: 'https://track.example/imp?g={GTIN}',\n expected: 'https://track.example/imp?g=00013%26cmd%3Ddrop',\n }),\n Object.freeze({\n name: 'nested-expansion-preserved-as-literal',\n description:\n 'A catalog value containing AdCP macro syntax `{DEVICE_ID}` MUST NOT be re-expanded. The braces are percent-encoded; the inner token survives as a literal.',\n macro: '{JOB_ID}',\n value: 'vacancy-{DEVICE_ID}-42',\n template: 'https://track.example/click?j={JOB_ID}',\n expected: 'https://track.example/click?j=vacancy-%7BDEVICE_ID%7D-42',\n }),\n Object.freeze({\n name: 'crlf-injection-neutralized',\n description:\n 'A catalog value containing CR/LF would enable request-header smuggling or log-row injection in downstream consumers. The unreserved-whitelist rule escapes both bytes, defeating the vector.',\n macro: '{SKU}',\n value: 'abc\\r\\nHost: evil.example',\n template: 'https://track.example/imp?s={SKU}',\n expected: 'https://track.example/imp?s=abc%0D%0AHost%3A%20evil.example',\n }),\n Object.freeze({\n name: 'non-ascii-utf8-percent-encoding',\n description:\n 'Non-ASCII octets MUST be percent-encoded after UTF-8 encoding per RFC 3986 §2.5. A value containing `café` emits `%C3%A9` for the `é`.',\n macro: '{STORE_ID}',\n value: 'café-amsterdam',\n template: 'https://track.example/imp?s={STORE_ID}',\n expected: 'https://track.example/imp?s=caf%C3%A9-amsterdam',\n }),\n Object.freeze({\n name: 'mixed-path-and-query-contexts',\n description: 'Verifies encoding applies equally to path segments and query strings within the same URL.',\n macro: '{CATALOG_ID}',\n value: 'gmc/primary feed',\n template: 'https://track.example/catalog/{CATALOG_ID}/ping?cid={CATALOG_ID}',\n expected: 'https://track.example/catalog/gmc%2Fprimary%20feed/ping?cid=gmc%2Fprimary%20feed',\n }),\n Object.freeze({\n name: 'bidi-override-neutralized',\n description:\n 'Unicode bidi override characters (U+202E, U+2066-U+2069) in catalog values can spoof audit-log rendering. The unreserved-whitelist rule percent-encodes them as their UTF-8 bytes.',\n macro: '{VEHICLE_ID}',\n value: 'VIN-\\u202E1234',\n template: 'https://track.example/imp?v={VEHICLE_ID}',\n expected: 'https://track.example/imp?v=VIN-%E2%80%AE1234',\n }),\n Object.freeze({\n name: 'url-scheme-injection-neutralized',\n description:\n 'A catalog value containing `javascript:alert(0)` substituted into an href-whole-value position (e.g., `<a href=\"{CLICK}\">`) would otherwise execute as a javascript: scheme URL. The strict unreserved-whitelist rule percent-encodes the colon and parens so the browser parses the result as a relative URL against the base, neutralizing the injection. Parentheses `(` and `)` are NOT in RFC 3986 `unreserved` (they are sub-delims), so the strict rule encodes them — encodeURIComponent-based encoders that leave parens alone FAIL this vector, which is how the runtime observer (#2638) distinguishes strict-RFC-3986 encoders from permissive ones.',\n macro: '{CLICK}',\n value: 'javascript:alert(0)',\n template: 'https://track.example/go?c={CLICK}',\n expected: 'https://track.example/go?c=javascript%3Aalert%280%29',\n }),\n]);\n\nexport function getCatalogMacroVector(name: CatalogMacroVectorName | string): CatalogMacroVector | undefined {\n return CATALOG_MACRO_VECTORS.find(v => v.name === name);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/substitution/vectors.ts"],"sourcesContent":["/**\n * Canonical catalog-item macro substitution vectors.\n *\n * Mirrors `static/test-vectors/catalog-macro-substitution.json` in the\n * upstream AdCP repo at the schema version this client targets. The\n * fixture is the source of truth for `raw_value` and `expected_encoded`\n * — these constants are synchronized copies so the library can be used\n * without filesystem side effects (works in browser bundles, avoids\n * shipping a JSON file through the tsc pipeline).\n *\n * The companion JSON at `fixtures/catalog-macro-substitution.json` is\n * byte-equal to the upstream fixture and is tested for parity.\n */\n\nimport type { CatalogMacroVector } from './types';\n\nexport type CatalogMacroVectorName =\n | 'reserved-character-breakout'\n | 'nested-expansion-preserved-as-literal'\n | 'crlf-injection-neutralized'\n | 'non-ascii-utf8-percent-encoding'\n | 'mixed-path-and-query-contexts'\n | 'bidi-override-neutralized'\n | 'url-scheme-injection-neutralized';\n\nexport const CATALOG_MACRO_VECTORS: readonly CatalogMacroVector[] = Object.freeze([\n Object.freeze({\n name: 'reserved-character-breakout',\n description:\n 'A catalog value containing `&` and `=` would break out of the surrounding query string if substituted raw. Percent-encoding neutralizes the breakout.',\n macro: '{GTIN}',\n value: '00013&cmd=drop',\n template: 'https://track.example/imp?g={GTIN}',\n expected: 'https://track.example/imp?g=00013%26cmd%3Ddrop',\n }),\n Object.freeze({\n name: 'nested-expansion-preserved-as-literal',\n description:\n 'A catalog value containing AdCP macro syntax `{DEVICE_ID}` MUST NOT be re-expanded. The braces are percent-encoded; the inner token survives as a literal.',\n macro: '{JOB_ID}',\n value: 'vacancy-{DEVICE_ID}-42',\n template: 'https://track.example/click?j={JOB_ID}',\n expected: 'https://track.example/click?j=vacancy-%7BDEVICE_ID%7D-42',\n }),\n Object.freeze({\n name: 'crlf-injection-neutralized',\n description:\n 'A catalog value containing CR/LF would enable request-header smuggling or log-row injection in downstream consumers. The unreserved-whitelist rule escapes both bytes, defeating the vector.',\n macro: '{SKU}',\n value: 'abc\\r\\nHost: evil.example',\n template: 'https://track.example/imp?s={SKU}',\n expected: 'https://track.example/imp?s=abc%0D%0AHost%3A%20evil.example',\n }),\n Object.freeze({\n name: 'non-ascii-utf8-percent-encoding',\n description:\n 'Non-ASCII octets MUST be percent-encoded after UTF-8 encoding per RFC 3986 §2.5. A value containing `café` emits `%C3%A9` for the `é`.',\n macro: '{STORE_ID}',\n value: 'café-amsterdam',\n template: 'https://track.example/imp?s={STORE_ID}',\n expected: 'https://track.example/imp?s=caf%C3%A9-amsterdam',\n }),\n Object.freeze({\n name: 'mixed-path-and-query-contexts',\n description: 'Verifies encoding applies equally to path segments and query strings within the same URL.',\n macro: '{CATALOG_ID}',\n value: 'gmc/primary feed',\n template: 'https://track.example/catalog/{CATALOG_ID}/ping?cid={CATALOG_ID}',\n expected: 'https://track.example/catalog/gmc%2Fprimary%20feed/ping?cid=gmc%2Fprimary%20feed',\n }),\n Object.freeze({\n name: 'bidi-override-neutralized',\n description:\n 'Unicode bidi override characters (U+202E, U+2066-U+2069) in catalog values can spoof audit-log rendering. The unreserved-whitelist rule percent-encodes them as their UTF-8 bytes.',\n macro: '{VEHICLE_ID}',\n value: 'VIN-\\u202E1234',\n template: 'https://track.example/imp?v={VEHICLE_ID}',\n expected: 'https://track.example/imp?v=VIN-%E2%80%AE1234',\n }),\n Object.freeze({\n name: 'url-scheme-injection-neutralized',\n description:\n 'A catalog value containing `javascript:alert(0)` substituted into an href-whole-value position (e.g., `<a href=\"{CLICK}\">`) would otherwise execute as a javascript: scheme URL. The strict unreserved-whitelist rule percent-encodes the colon and parens so the browser parses the result as a relative URL against the base, neutralizing the injection. Parentheses `(` and `)` are NOT in RFC 3986 `unreserved` (they are sub-delims), so the strict rule encodes them — encodeURIComponent-based encoders that leave parens alone FAIL this vector, which is how the runtime observer (#2638) distinguishes strict-RFC-3986 encoders from permissive ones.',\n macro: '{CLICK}',\n value: 'javascript:alert(0)',\n template: 'https://track.example/go?c={CLICK}',\n expected: 'https://track.example/go?c=javascript%3Aalert%280%29',\n }),\n]);\n\nexport function getCatalogMacroVector(name: CatalogMacroVectorName | string): CatalogMacroVector | undefined {\n return CATALOG_MACRO_VECTORS.find(v => v.name === name);\n}\n"],"mappings":"AAyBO,MAAM,wBAAuD,OAAO,OAAO;AAAA,EAChF,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aAAa;AAAA,IACb,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AAAA,EACD,OAAO,OAAO;AAAA,IACZ,MAAM;AAAA,IACN,aACE;AAAA,IACF,OAAO;AAAA,IACP,OAAO;AAAA,IACP,UAAU;AAAA,IACV,UAAU;AAAA,EACZ,CAAC;AACH,CAAC;AAEM,SAAS,sBAAsB,MAAuE;AAC3G,SAAO,sBAAsB,KAAK,OAAK,EAAE,SAAS,IAAI;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 { setAgentTesterLogger, getLogger, createTestClient, runStep } from "./client.mjs";
|
|
8
2
|
import { formatTestResults, formatTestResultsJSON, formatTestResultsSummary } from "./formatter.mjs";
|
|
9
3
|
import {
|