@tokenoftrust/storefront-runner 1.4.2-rc.0 → 2.0.1
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/apps/storefront/astro.config.mjs +3 -3
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +8 -3
- package/apps/storefront/env.d.ts +54 -5
- package/apps/storefront/integrations/materialize-guard.mjs +12 -14
- package/apps/storefront/migrations-apps/0004_deep_morph.sql +36 -0
- package/apps/storefront/migrations-apps/0005_common_mystique.sql +23 -0
- package/apps/storefront/migrations-apps/0006_broad_microchip.sql +16 -0
- package/apps/storefront/migrations-apps/0007_brave_outlaw_kid.sql +15 -0
- package/apps/storefront/migrations-apps/0008_blue_dazzler.sql +11 -0
- package/apps/storefront/migrations-apps/0009_bored_shinobi_shaw.sql +15 -0
- package/apps/storefront/migrations-apps/0010_ws4_verified_provider_measurement.sql +35 -0
- package/apps/storefront/migrations-apps/meta/0004_snapshot.json +1474 -0
- package/apps/storefront/migrations-apps/meta/0005_snapshot.json +1617 -0
- package/apps/storefront/migrations-apps/meta/0006_snapshot.json +1716 -0
- package/apps/storefront/migrations-apps/meta/0007_snapshot.json +1812 -0
- package/apps/storefront/migrations-apps/meta/0008_snapshot.json +1881 -0
- package/apps/storefront/migrations-apps/meta/0009_snapshot.json +1979 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +42 -0
- package/apps/storefront/public/apps/tot.demo.analytics/0.1.0/entry.js +42 -0
- package/apps/storefront/public/platform/amplitude-http/v1/entry.mjs +19 -0
- package/apps/storefront/public/platform/google-measurement/v1/entry.mjs +19 -0
- package/apps/storefront/public/shared/commerce-chrome.css +20 -2
- package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +68 -0
- package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +111 -0
- package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +2 -0
- package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +247 -0
- package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +73 -0
- package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +79 -0
- package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +10982 -0
- package/apps/storefront/scripts/import-v84-through-ws2.ts +38 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +2 -0
- package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +4 -0
- package/apps/storefront/scripts/install-v84-native-through-ws1.ts +185 -0
- package/apps/storefront/scripts/prove-clone-isolation.ts +506 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +148 -0
- package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +35 -0
- package/apps/storefront/scripts/replay-v84-clean-clone.ts +214 -0
- package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +18 -0
- package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +17 -0
- package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +15 -0
- package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +21 -0
- package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +42 -0
- package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +145 -0
- package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +18 -0
- package/apps/storefront/src/components/Img.astro +1 -1
- package/apps/storefront/src/components/admin/AdminBlogTab.astro +19 -16
- package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +105 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1057 -412
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +5 -5
- package/apps/storefront/src/components/chrome/NavDropdown.astro +12 -0
- package/apps/storefront/src/components/chrome/SiteFooter.astro +7 -2
- package/apps/storefront/src/components/chrome/SiteHeader.astro +46 -10
- package/apps/storefront/src/components/compliance/AdultSignatureNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/ExciseTaxNotice.astro +1 -2
- package/apps/storefront/src/components/compliance/PactActNotice.astro +1 -1
- package/apps/storefront/src/components/compliance/Prop65Warning.astro +1 -2
- package/apps/storefront/src/components/compliance/PurchaseLimitNotice.astro +2 -3
- package/apps/storefront/src/components/compliance/ShippingRestrictionNotice.astro +2 -2
- package/apps/storefront/src/components/compliance/StateEligibilityNotice.astro +2 -3
- package/apps/storefront/src/components/content/TrustStrip.astro +4 -4
- package/apps/storefront/src/components/islands/MobileNav.tsx +26 -1
- package/apps/storefront/src/components/islands/VariantSelector.tsx +27 -2
- package/apps/storefront/src/components/islands/useCheckoutHandoffCorrelation.ts +75 -0
- package/apps/storefront/src/components/membership/IllustrativeLabel.astro +1 -2
- package/apps/storefront/src/components/membership/TierCards.astro +2 -2
- package/apps/storefront/src/components/membership/TierComparisonTable.astro +2 -2
- package/apps/storefront/src/components/subscription/SavingsExplainer.astro +2 -2
- package/apps/storefront/src/config/adminTenantLookup.ts +35 -0
- package/apps/storefront/src/config/devTenantSeed.ts +9 -9
- package/apps/storefront/src/config/resolver.ts +18 -10
- package/apps/storefront/src/layouts/Layout.astro +100 -20
- package/apps/storefront/src/lib/activity/candidateSubmitLease.ts +87 -0
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +86 -6
- package/apps/storefront/src/lib/activity/deployVersion.ts +1 -1
- package/apps/storefront/src/lib/admin/adminShell.ts +2 -0
- package/apps/storefront/src/lib/admin/envelope.ts +1 -2
- package/apps/storefront/src/lib/apps/adminService.ts +10 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +1 -1
- package/apps/storefront/src/lib/apps/appVersionStore.ts +281 -0
- package/apps/storefront/src/lib/apps/consentConfigStore.ts +228 -0
- package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +447 -0
- package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +367 -0
- package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +315 -0
- package/apps/storefront/src/lib/apps/integrationLifecycle.ts +614 -0
- package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +125 -0
- package/apps/storefront/src/lib/apps/integrationReadModel.ts +563 -0
- package/apps/storefront/src/lib/apps/integrationsTab.ts +547 -0
- package/apps/storefront/src/lib/apps/integrationsView.ts +106 -0
- package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +599 -0
- package/apps/storefront/src/lib/apps/orders/orderConversionD1.ts +150 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +8 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +1 -1
- package/apps/storefront/src/lib/apps/orders/verifiedProviderMeasurement.ts +117 -0
- package/apps/storefront/src/lib/apps/registryService.ts +264 -10
- package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +170 -0
- package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +16 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +105 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +177 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +251 -0
- package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +241 -0
- package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +211 -0
- package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +409 -0
- package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +368 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +188 -0
- package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +291 -0
- package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +95 -0
- package/apps/storefront/src/lib/auth/adminApiGuard.ts +66 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +2 -2
- package/apps/storefront/src/lib/auth/adminLanding.ts +88 -0
- package/apps/storefront/src/lib/blog/collection.ts +114 -17
- package/apps/storefront/src/lib/blog/markdown.ts +1 -1
- package/apps/storefront/src/lib/blog/provider.ts +76 -9
- package/apps/storefront/src/lib/blog/types.ts +9 -9
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome-assignments.example.json +6 -0
- package/apps/storefront/src/lib/chrome/examples/tokenoftrust.chrome.example.json +79 -0
- package/apps/storefront/src/lib/chrome/model.ts +48 -253
- package/apps/storefront/src/lib/chrome/navCurrent.ts +29 -0
- package/apps/storefront/src/lib/chrome/parseConfig.ts +18 -32
- package/apps/storefront/src/lib/chrome/variant.ts +39 -0
- package/apps/storefront/src/lib/commerce/checkoutHandoffCorrelation.ts +105 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel-boot.ts +152 -0
- package/apps/storefront/src/lib/compliance/tamper-sentinel.ts +327 -0
- package/apps/storefront/src/lib/content/index.ts +5 -5
- package/apps/storefront/src/lib/content-edit/applyDiff.ts +1 -1
- package/apps/storefront/src/lib/content-edit/route.ts +1 -1
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +276 -0
- package/apps/storefront/src/lib/dev/cockpit/ask.ts +49 -0
- package/apps/storefront/src/lib/dev/cockpit/clickToSource.ts +71 -0
- package/apps/storefront/src/lib/dev/cockpit/copyCommand.ts +227 -0
- package/apps/storefront/src/lib/dev/cockpit/files.ts +249 -0
- package/apps/storefront/src/lib/dev/cockpit/hostedActivity.ts +500 -0
- package/apps/storefront/src/lib/dev/cockpit/infoPopover.ts +12 -0
- package/apps/storefront/src/lib/dev/cockpit/mcpCopy.ts +34 -0
- package/apps/storefront/src/lib/dev/cockpit/monitor.ts +107 -0
- package/apps/storefront/src/lib/dev/cockpit/stepper.ts +116 -0
- package/apps/storefront/src/lib/dev/cockpit/themeToggle.ts +23 -0
- package/apps/storefront/src/lib/dev/runtimeStore.ts +1 -1
- package/apps/storefront/src/lib/edgeCache.ts +18 -6
- package/apps/storefront/src/lib/email/candidateConflictEmail.ts +80 -0
- package/apps/storefront/src/lib/integration/browserEnvironment.ts +137 -0
- package/apps/storefront/src/lib/integration/managedInstallDeclarations.ts +72 -0
- package/apps/storefront/src/lib/integration/nonceStamp.ts +49 -0
- package/apps/storefront/src/lib/integration/tenantDeclarations.ts +116 -0
- package/apps/storefront/src/lib/membership/eligibility.ts +1 -1
- package/apps/storefront/src/lib/membership/model.ts +1 -2
- package/apps/storefront/src/lib/newsletter/confirmationEmail.ts +2 -2
- package/apps/storefront/src/lib/newsletter/consent.ts +2 -2
- package/apps/storefront/src/lib/newsletter/send.ts +3 -3
- package/apps/storefront/src/lib/previewReload.ts +57 -1
- package/apps/storefront/src/lib/publish/adminPublishTab.ts +1775 -669
- package/apps/storefront/src/lib/publish/buildStream.ts +269 -0
- package/apps/storefront/src/lib/publish/collapsePersistence.ts +72 -0
- package/apps/storefront/src/lib/publish/lifecycleState.ts +369 -0
- package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +29 -0
- package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +78 -0
- package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +62 -0
- package/apps/storefront/src/lib/publish/mergeRowFindings.ts +124 -0
- package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +75 -0
- package/apps/storefront/src/lib/publish/publish-guard.ts +88 -0
- package/apps/storefront/src/lib/publish/publishFormat.ts +165 -0
- package/apps/storefront/src/lib/publish/publishHero.ts +177 -0
- package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +66 -0
- package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +141 -0
- package/apps/storefront/src/lib/publish/reviewRelease/index.ts +41 -0
- package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +47 -0
- package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +42 -0
- package/apps/storefront/src/lib/publish/reviewRelease/types.ts +59 -0
- package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +440 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +161 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +325 -0
- package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +241 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +22 -3
- package/apps/storefront/src/lib/publish/sseRoute.ts +119 -0
- package/apps/storefront/src/lib/publish/streamLogConsole.ts +142 -0
- package/apps/storefront/src/lib/publish/versionNumber.ts +52 -0
- package/apps/storefront/src/lib/rawChrome.ts +125 -47
- package/apps/storefront/src/lib/rawMarketingHtml.ts +74 -3
- package/apps/storefront/src/lib/seo/alternates.ts +1 -1
- package/apps/storefront/src/lib/social/notify.ts +4 -4
- package/apps/storefront/src/lib/storyblok/content-model.ts +7 -33
- package/apps/storefront/src/lib/storyblok/provider.ts +203 -69
- package/apps/storefront/src/lib/subscription/model.ts +4 -4
- package/apps/storefront/src/lib/the-build/provider.ts +9 -4
- package/apps/storefront/src/middleware/index.ts +273 -51
- package/apps/storefront/src/pages/[...slug].astro +9 -4
- package/apps/storefront/src/pages/admin/review-release.astro +265 -0
- package/apps/storefront/src/pages/admin/select.astro +96 -0
- package/apps/storefront/src/pages/admin.astro +16 -2
- package/apps/storefront/src/pages/api/activity.ts +8 -8
- package/apps/storefront/src/pages/api/admin/activity-alerts.ts +5 -20
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +7 -2
- package/apps/storefront/src/pages/api/admin/orders.ts +8 -2
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +174 -0
- package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +96 -0
- package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +118 -0
- package/apps/storefront/src/pages/api/apps/admin/integrations.ts +89 -0
- package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +85 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +74 -2
- package/apps/storefront/src/pages/api/commerce/checkout-handoff.ts +77 -0
- package/apps/storefront/src/pages/api/compliance/tamper.ts +88 -0
- package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +108 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +1 -2
- package/apps/storefront/src/pages/blog/[slug].astro +5 -5
- package/apps/storefront/src/pages/blog/author/[author].astro +3 -3
- package/apps/storefront/src/pages/blog/category/[category].astro +2 -2
- package/apps/storefront/src/pages/blog/index.astro +2 -2
- package/apps/storefront/src/pages/blog/tag/[tag].astro +2 -2
- package/apps/storefront/src/pages/cockpit.astro +61 -1362
- package/apps/storefront/src/pages/index.astro +10 -5
- package/apps/storefront/src/pages/llms.txt.ts +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +7 -6
- package/apps/storefront/src/pages/sitemap.xml.ts +2 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +42 -31
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +8 -3
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +6 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +6 -2
- package/apps/storefront/src/pages/style-guide/index.astro +19 -5
- package/apps/storefront/src/pages/tenants/[id]/[...path].ts +38 -2
- package/apps/storefront/src/pages/the-build/[slug].astro +4 -4
- package/apps/storefront/src/pages/the-build/index.astro +2 -2
- package/apps/storefront/src/pages/the-build/rss.xml.ts +2 -2
- package/apps/storefront/src/styles/admin.css +27 -1
- package/apps/storefront/src/themes/tenants/index.ts +19 -7
- package/apps/storefront/tsconfig.json +1 -1
- package/package.json +1 -1
- package/packages/public-runtime/package.json +2 -1
- package/packages/public-runtime/schemas/managed-analytics/v1/adapter-definition.schema.json +427 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-integration-install-request.schema.json +161 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/create-migration-draft-request.schema.json +157 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-integration-install-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/delete-migration-draft-request.schema.json +84 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/error-envelope.schema.json +180 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-adapter-definition-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-install-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-integration-revision-request.schema.json +79 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/get-migration-draft-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-install.schema.json +167 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/integration-revision.schema.json +208 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-adapter-definitions-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-installs-request.schema.json +53 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/list-integration-revisions-request.schema.json +82 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/migration-draft.schema.json +218 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-request.schema.json +52 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/mutation-success-envelope.schema.json +49 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-request.schema.json +20 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/read-success-envelope.schema.json +45 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/revise-integration-install-request.schema.json +156 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/set-integration-install-status-request.schema.json +92 -0
- package/packages/public-runtime/schemas/managed-analytics/v1/update-migration-draft-request.schema.json +184 -0
- package/packages/public-runtime/scripts/generate-managed-analytics-api-v1-schemas.mjs +13 -0
- package/packages/public-runtime/src/activity/catalog.ts +41 -37
- package/packages/public-runtime/src/activity/event.ts +6 -6
- package/packages/public-runtime/src/activity/index.ts +2 -2
- package/packages/public-runtime/src/activity/redaction.ts +4 -4
- package/packages/public-runtime/src/candidate-index.ts +42 -8
- package/packages/public-runtime/src/checkout-orders.ts +2 -2
- package/packages/public-runtime/src/checkout-style-publish.ts +2 -2
- package/packages/public-runtime/src/checkout-style.ts +7 -7
- package/packages/public-runtime/src/checkout.ts +3 -3
- package/packages/public-runtime/src/chrome.ts +777 -0
- package/packages/public-runtime/src/compliance-obligations.ts +310 -0
- package/packages/public-runtime/src/csp.ts +99 -0
- package/packages/public-runtime/src/customization-runtime.ts +32 -1
- package/packages/public-runtime/src/destination-webhook/contract.ts +353 -0
- package/packages/public-runtime/src/embed-catalog.d.mts +9 -0
- package/packages/public-runtime/src/embed-catalog.mjs +40 -0
- package/packages/public-runtime/src/evidence.ts +288 -0
- package/packages/public-runtime/src/gtm-migration/bounds.ts +138 -0
- package/packages/public-runtime/src/gtm-migration/disposition-rules.ts +361 -0
- package/packages/public-runtime/src/gtm-migration/dispositions.ts +181 -0
- package/packages/public-runtime/src/gtm-migration/draft-generator.ts +400 -0
- package/packages/public-runtime/src/gtm-migration/graph.ts +468 -0
- package/packages/public-runtime/src/gtm-migration/index.ts +95 -0
- package/packages/public-runtime/src/gtm-migration/parse.ts +410 -0
- package/packages/public-runtime/src/gtm-migration/references.ts +266 -0
- package/packages/public-runtime/src/gtm-migration/types.ts +86 -0
- package/packages/public-runtime/src/index.ts +16 -1
- package/packages/public-runtime/src/integration/assurance-decision.ts +259 -0
- package/packages/public-runtime/src/integration/assurance.ts +228 -0
- package/packages/public-runtime/src/integration/candidate-evidence.ts +377 -0
- package/packages/public-runtime/src/integration/commerce-broker.ts +497 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +337 -0
- package/packages/public-runtime/src/integration/consent-cmp-demand.ts +115 -0
- package/packages/public-runtime/src/integration/consent-config.ts +260 -0
- package/packages/public-runtime/src/integration/consent-preview.ts +125 -0
- package/packages/public-runtime/src/integration/correlation-token.ts +444 -0
- package/packages/public-runtime/src/integration/csp-compiler.ts +267 -0
- package/packages/public-runtime/src/integration/csp.ts +105 -0
- package/packages/public-runtime/src/integration/digest.ts +43 -0
- package/packages/public-runtime/src/integration/environment.ts +228 -0
- package/packages/public-runtime/src/integration/events-catalog.ts +186 -0
- package/packages/public-runtime/src/integration/events.ts +701 -0
- package/packages/public-runtime/src/integration/g2-action-boundary.ts +106 -0
- package/packages/public-runtime/src/integration/index.ts +90 -0
- package/packages/public-runtime/src/integration/interim-bridge.ts +138 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +317 -0
- package/packages/public-runtime/src/integration/lifecycle-runtime.ts +393 -0
- package/packages/public-runtime/src/integration/lifecycle.ts +94 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.d.mts +2 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1-safety.mjs +10 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.schemas.ts +382 -0
- package/packages/public-runtime/src/integration/managed-analytics-api-v1.ts +684 -0
- package/packages/public-runtime/src/integration/managed-analytics-bridge.ts +383 -0
- package/packages/public-runtime/src/integration/managed-analytics-value-safety.ts +14 -0
- package/packages/public-runtime/src/integration/managed-analytics.ts +277 -0
- package/packages/public-runtime/src/integration/manifest.ts +470 -0
- package/packages/public-runtime/src/integration/native-amplitude.ts +362 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +352 -0
- package/packages/public-runtime/src/integration/observability.ts +451 -0
- package/packages/public-runtime/src/integration/order-conversion.ts +0 -0
- package/packages/public-runtime/src/integration/phases.ts +182 -0
- package/packages/public-runtime/src/integration/plan.ts +112 -0
- package/packages/public-runtime/src/integration/platform-adapter-runtime.ts +146 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +671 -0
- package/packages/public-runtime/src/integration/promotion-gate.ts +451 -0
- package/packages/public-runtime/src/integration/provider-checkout-extension.ts +425 -0
- package/packages/public-runtime/src/integration/receipt-measurement.ts +370 -0
- package/packages/public-runtime/src/integration/regulated-parent-cert.ts +311 -0
- package/packages/public-runtime/src/integration/render-phases.ts +267 -0
- package/packages/public-runtime/src/integration/resolve.ts +465 -0
- package/packages/public-runtime/src/integration/result.ts +239 -0
- package/packages/public-runtime/src/integration/secrets.ts +92 -0
- package/packages/public-runtime/src/integration/tamper-evidence.ts +205 -0
- package/packages/public-runtime/src/integration/tamper-recovery.ts +127 -0
- package/packages/public-runtime/src/integration/version.ts +92 -0
- package/packages/public-runtime/src/integration-capabilities.ts +520 -0
- package/packages/public-runtime/src/membership.ts +2 -2
- package/packages/public-runtime/src/raw-html-policy.ts +2 -4
- package/packages/public-runtime/src/regulated-commerce/broker/consent-dedupe.ts +144 -0
- package/packages/public-runtime/src/regulated-commerce/correlation/contract.ts +177 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/local-dispatch.ts +24 -0
- package/packages/public-runtime/src/regulated-commerce/diagnostics/read-model.ts +69 -0
- package/packages/public-runtime/src/regulated-commerce/dispatch/core.ts +134 -0
- package/packages/public-runtime/src/regulated-commerce/events/contract.ts +113 -0
- package/packages/public-runtime/src/regulated-commerce/ledger/core.ts +81 -0
- package/packages/public-runtime/src/regulated-commerce/mappings/ga4-ads.ts +87 -0
- package/packages/public-runtime/src/regulated-commerce/provider-order/bridge.ts +52 -0
- package/packages/public-runtime/src/regulated-commerce/purchase-truth/contract.ts +252 -0
- package/packages/public-runtime/src/regulated-commerce/sources/accepted-browser-operation.ts +67 -0
- package/packages/public-runtime/src/regulated-commerce/testing/fake-destination.ts +93 -0
- package/packages/public-runtime/src/static-bundle.ts +5 -5
- package/packages/public-runtime/src/tenant-assets.ts +143 -4
- package/packages/public-runtime/src/tenant.ts +136 -9
- package/packages/public-runtime/tests/fixtures/integration/browser-environment.example.json +8 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/commerce-order-event.example.json +44 -0
- package/packages/public-runtime/tests/fixtures/integration/compliance-assurance-record.example.json +50 -0
- package/packages/public-runtime/tests/fixtures/integration/integration-manifest-v2.example.json +120 -0
- package/packages/public-runtime/tests/fixtures/integration/managed-analytics-v1/fake-adapter-catalog.json +35 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-action-registry.example.json +11 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/README.md +22 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/entry.js +30 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/manifest.json +75 -0
- package/packages/public-runtime/tests/fixtures/integration/tenant-local-example/public-config.json +3 -0
- package/packages/public-runtime/tests/fixtures/regulated-commerce/fake-destinations.ts +38 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.provenance.json +29 -0
- package/packages/public-runtime/tests/gtm-migration/fixtures/gtm-sample-container-v84.redacted.json +3397 -0
- package/scripts/build/copy-tenant-assets.mjs +12 -5
- package/scripts/dev/ai-edit.mjs +2 -1
- package/scripts/dev/file-browser.mjs +2 -2
- package/scripts/dev/file-writer.mjs +2 -2
- package/scripts/dev/integration-guardrails.mjs +498 -0
- package/scripts/dev/safe-path.mjs +2 -2
- package/scripts/dev/tenant-source-root.mjs +3 -2
- package/scripts/tot-dev.mjs +5 -4
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe, NON-EXECUTING parser for a GTM (Google Tag Manager) container export.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY CONTRACT (the whole point of this module):
|
|
5
|
+
* - The export is treated as inert DATA. Nothing in it is ever executed,
|
|
6
|
+
* evaluated, interpreted, compiled, or run. There is no `eval`, no
|
|
7
|
+
* `new Function`, no dynamic `import()`, no `vm`, no template compilation.
|
|
8
|
+
* - Fields that would be code in a live GTM runtime (Custom HTML `html`,
|
|
9
|
+
* Custom Template `templateData` / `sandboxedJsForServer`, Custom JavaScript
|
|
10
|
+
* variable `javascript`) are captured as opaque inert strings inside each
|
|
11
|
+
* record's `raw` bag — see `sanitizeValue`.
|
|
12
|
+
* - Pure data-in/data-out: no I/O, no network, no filesystem, no timers, no
|
|
13
|
+
* globals mutated. `TextEncoder`/`TextDecoder`/`JSON.parse` are the only
|
|
14
|
+
* platform primitives used, all read-only.
|
|
15
|
+
* - Strict bounds are enforced BEFORE parse (byte cap) and DURING a structural
|
|
16
|
+
* walk (depth/nodes/array-len). Over-length strings are truncated with a
|
|
17
|
+
* flag, never rejected. Prototype-polluting keys are quarantined.
|
|
18
|
+
* - The parser NEVER throws on bad input; it returns a structured result.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ParseBounds } from "./bounds.js";
|
|
22
|
+
import { DEFAULT_BOUNDS, enforceBounds, resolveBounds } from "./bounds.js";
|
|
23
|
+
import type {
|
|
24
|
+
GtmContainer,
|
|
25
|
+
GtmContainerVersion,
|
|
26
|
+
GtmInertRecord,
|
|
27
|
+
GtmParseAccounting,
|
|
28
|
+
GtmParseResult,
|
|
29
|
+
} from "./types.js";
|
|
30
|
+
import type { GtmViolation } from "./bounds.js";
|
|
31
|
+
|
|
32
|
+
export { DEFAULT_BOUNDS } from "./bounds.js";
|
|
33
|
+
|
|
34
|
+
/** Keys that must never become live prototype writes; skipped into a quarantine count. */
|
|
35
|
+
const POLLUTING_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
36
|
+
|
|
37
|
+
/** `containerVersion` collection keys we model as arrays of inert records. */
|
|
38
|
+
const COLLECTION_KEYS = [
|
|
39
|
+
"tag",
|
|
40
|
+
"trigger",
|
|
41
|
+
"variable",
|
|
42
|
+
"builtInVariable",
|
|
43
|
+
"customTemplate",
|
|
44
|
+
"folder",
|
|
45
|
+
"zone",
|
|
46
|
+
"client",
|
|
47
|
+
"gtagConfig",
|
|
48
|
+
"transformation",
|
|
49
|
+
] as const;
|
|
50
|
+
|
|
51
|
+
/** `containerVersion` metadata keys we lift onto the typed model. */
|
|
52
|
+
const METADATA_KEYS = new Set([
|
|
53
|
+
"containerId",
|
|
54
|
+
"path",
|
|
55
|
+
"publicId",
|
|
56
|
+
"fingerprint",
|
|
57
|
+
"name",
|
|
58
|
+
"container", // nested container object holding some of the above
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
/** Candidate keys GTM uses for a record's id, in preference order. */
|
|
62
|
+
const ID_KEYS = [
|
|
63
|
+
"tagId",
|
|
64
|
+
"triggerId",
|
|
65
|
+
"variableId",
|
|
66
|
+
"clientId",
|
|
67
|
+
"zoneId",
|
|
68
|
+
"folderId",
|
|
69
|
+
"templateId",
|
|
70
|
+
"transformationId",
|
|
71
|
+
"gtagConfigId",
|
|
72
|
+
"builtInVariableId",
|
|
73
|
+
"id",
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
77
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** UTF-8 byte length of a string. */
|
|
81
|
+
function utf8ByteLength(input: string): number {
|
|
82
|
+
return new TextEncoder().encode(input).length;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Mutable accounting scratch threaded through extraction. */
|
|
86
|
+
interface Accountant {
|
|
87
|
+
truncatedStrings: number;
|
|
88
|
+
quarantinedKeys: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Produce a deep INERT clone of `value`:
|
|
93
|
+
* - objects become null-prototype (`Object.create(null)`), so a stray
|
|
94
|
+
* `__proto__`/`constructor`/`prototype` key can never pollute a real
|
|
95
|
+
* prototype; those keys are skipped and counted instead.
|
|
96
|
+
* - strings over `maxStringLen` are truncated with a recorded flag.
|
|
97
|
+
* - numbers/booleans/null pass through; anything exotic (function/symbol —
|
|
98
|
+
* impossible from JSON.parse, defensive only) is dropped to null.
|
|
99
|
+
*
|
|
100
|
+
* Recursion is bounded by `maxDepth` (already guaranteed by `enforceBounds`,
|
|
101
|
+
* re-checked here defensively); depth overflow collapses to null.
|
|
102
|
+
*/
|
|
103
|
+
function sanitizeValue(
|
|
104
|
+
value: unknown,
|
|
105
|
+
bounds: ParseBounds,
|
|
106
|
+
acct: Accountant,
|
|
107
|
+
depth: number,
|
|
108
|
+
): unknown {
|
|
109
|
+
if (depth > bounds.maxDepth) return null;
|
|
110
|
+
|
|
111
|
+
if (typeof value === "string") {
|
|
112
|
+
// inert-by-design: captured as opaque data, never executed. Code-bearing GTM
|
|
113
|
+
// fields (Custom HTML `html`, template `templateData`/`sandboxedJsForServer`,
|
|
114
|
+
// Custom JS `javascript`) arrive here as plain strings and stay plain strings.
|
|
115
|
+
if (value.length > bounds.maxStringLen) {
|
|
116
|
+
acct.truncatedStrings += 1;
|
|
117
|
+
return value.slice(0, bounds.maxStringLen);
|
|
118
|
+
}
|
|
119
|
+
return value;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
const out: unknown[] = [];
|
|
128
|
+
const limit = Math.min(value.length, bounds.maxArrayLen);
|
|
129
|
+
for (let i = 0; i < limit; i += 1) {
|
|
130
|
+
out.push(sanitizeValue(value[i], bounds, acct, depth + 1));
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (isPlainObject(value)) {
|
|
136
|
+
const out: Record<string, unknown> = Object.create(null);
|
|
137
|
+
for (const key of Object.keys(value)) {
|
|
138
|
+
if (POLLUTING_KEYS.has(key)) {
|
|
139
|
+
// Quarantine: never write these keys into the model.
|
|
140
|
+
acct.quarantinedKeys += 1;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
out[key] = sanitizeValue(value[key], bounds, acct, depth + 1);
|
|
144
|
+
}
|
|
145
|
+
return out;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Unreachable for JSON.parse output (no functions/symbols); defensive.
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Read a string field off an object, returning null if absent/non-string. */
|
|
153
|
+
function liftString(obj: Record<string, unknown>, key: string): string | null {
|
|
154
|
+
const v = obj[key];
|
|
155
|
+
return typeof v === "string" ? v : null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Build one inert record: lift id/name/type, keep the COMPLETE inert bag as `raw`. */
|
|
159
|
+
function toRecord(value: unknown, bounds: ParseBounds, acct: Accountant): GtmInertRecord {
|
|
160
|
+
const raw = sanitizeValue(value, bounds, acct, 0) as Record<string, unknown>;
|
|
161
|
+
let id: string | null = null;
|
|
162
|
+
let name: string | null = null;
|
|
163
|
+
let type: string | null = null;
|
|
164
|
+
if (isPlainObject(value)) {
|
|
165
|
+
for (const key of ID_KEYS) {
|
|
166
|
+
const candidate = liftString(value, key);
|
|
167
|
+
if (candidate !== null) {
|
|
168
|
+
id = candidate;
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
name = liftString(value, "name");
|
|
173
|
+
type = liftString(value, "type");
|
|
174
|
+
}
|
|
175
|
+
return { id, name, type, raw };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Extract an array-valued collection into inert records (missing/non-array → []). */
|
|
179
|
+
function toCollection(
|
|
180
|
+
containerVersion: Record<string, unknown>,
|
|
181
|
+
key: string,
|
|
182
|
+
bounds: ParseBounds,
|
|
183
|
+
acct: Accountant,
|
|
184
|
+
): GtmInertRecord[] {
|
|
185
|
+
const arr = containerVersion[key];
|
|
186
|
+
if (!Array.isArray(arr)) return [];
|
|
187
|
+
const out: GtmInertRecord[] = [];
|
|
188
|
+
const limit = Math.min(arr.length, bounds.maxArrayLen);
|
|
189
|
+
for (let i = 0; i < limit; i += 1) {
|
|
190
|
+
out.push(toRecord(arr[i], bounds, acct));
|
|
191
|
+
}
|
|
192
|
+
return out;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Parse a GTM container export (format v2) into an inert typed model.
|
|
197
|
+
*
|
|
198
|
+
* NEVER throws and NEVER executes any part of the input.
|
|
199
|
+
*/
|
|
200
|
+
export function parseGtmExport(
|
|
201
|
+
input: string | Uint8Array,
|
|
202
|
+
bounds?: Partial<ParseBounds>,
|
|
203
|
+
): GtmParseResult {
|
|
204
|
+
const b = resolveBounds(bounds);
|
|
205
|
+
const violations: GtmViolation[] = [];
|
|
206
|
+
|
|
207
|
+
// 1. Measure bytes and reject over cap BEFORE JSON.parse.
|
|
208
|
+
let byteLength: number;
|
|
209
|
+
if (input instanceof Uint8Array) {
|
|
210
|
+
byteLength = input.byteLength;
|
|
211
|
+
} else if (typeof input === "string") {
|
|
212
|
+
// Fast pre-check: UTF-8 byte length >= char count, so char count over the cap
|
|
213
|
+
// is already over the byte cap — reject without encoding a huge string.
|
|
214
|
+
if (input.length > b.maxBytes) {
|
|
215
|
+
return {
|
|
216
|
+
ok: false,
|
|
217
|
+
violations: [
|
|
218
|
+
{
|
|
219
|
+
path: "(input)",
|
|
220
|
+
code: "max_bytes",
|
|
221
|
+
message: `Input exceeds ${b.maxBytes} bytes; rejected before parse.`,
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
byteLength = utf8ByteLength(input);
|
|
227
|
+
} else {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
violations: [
|
|
231
|
+
{ path: "(input)", code: "invalid_input", message: "Input must be a string or Uint8Array." },
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
if (byteLength > b.maxBytes) {
|
|
236
|
+
return {
|
|
237
|
+
ok: false,
|
|
238
|
+
violations: [
|
|
239
|
+
{
|
|
240
|
+
path: "(input)",
|
|
241
|
+
code: "max_bytes",
|
|
242
|
+
message: `Input is ${byteLength} bytes, over the ${b.maxBytes} byte cap; rejected before parse.`,
|
|
243
|
+
},
|
|
244
|
+
],
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 2. Decode bytes → text (reject invalid UTF-8), then JSON.parse in try/catch.
|
|
249
|
+
let text: string;
|
|
250
|
+
if (input instanceof Uint8Array) {
|
|
251
|
+
try {
|
|
252
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(input);
|
|
253
|
+
} catch {
|
|
254
|
+
return {
|
|
255
|
+
ok: false,
|
|
256
|
+
violations: [
|
|
257
|
+
{ path: "(input)", code: "invalid_utf8", message: "Input bytes are not valid UTF-8." },
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
text = input;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
let root: unknown;
|
|
266
|
+
try {
|
|
267
|
+
root = JSON.parse(text);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
violations: [
|
|
272
|
+
{
|
|
273
|
+
path: "(input)",
|
|
274
|
+
code: "invalid_json",
|
|
275
|
+
message: `Input is not valid JSON: ${err instanceof Error ? err.message : "parse error"}.`,
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 3. Assert top-level v2 shape.
|
|
282
|
+
if (!isPlainObject(root)) {
|
|
283
|
+
return {
|
|
284
|
+
ok: false,
|
|
285
|
+
violations: [{ path: "(root)", code: "type", message: "Export root must be a JSON object." }],
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
const efv = root["exportFormatVersion"];
|
|
289
|
+
if (typeof efv !== "number" || !Number.isFinite(efv)) {
|
|
290
|
+
violations.push({
|
|
291
|
+
path: "exportFormatVersion",
|
|
292
|
+
code: "type",
|
|
293
|
+
message: "`exportFormatVersion` must be a finite number.",
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
const containerVersionRaw = root["containerVersion"];
|
|
297
|
+
if (!isPlainObject(containerVersionRaw)) {
|
|
298
|
+
violations.push({
|
|
299
|
+
path: "containerVersion",
|
|
300
|
+
code: "type",
|
|
301
|
+
message: "`containerVersion` must be a JSON object.",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (violations.length > 0) {
|
|
305
|
+
return { ok: false, violations };
|
|
306
|
+
}
|
|
307
|
+
const containerVersion = containerVersionRaw as Record<string, unknown>;
|
|
308
|
+
|
|
309
|
+
// 4. Structural bounds walk (depth/nodes/array-len). Any breach → hard reject.
|
|
310
|
+
const boundsViolations = enforceBounds(root, b);
|
|
311
|
+
if (boundsViolations.length > 0) {
|
|
312
|
+
return { ok: false, violations: boundsViolations };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 5. Extract collections into the inert model, quarantining polluting keys.
|
|
316
|
+
const acct: Accountant = { truncatedStrings: 0, quarantinedKeys: 0 };
|
|
317
|
+
const counts: Record<string, number> = Object.create(null);
|
|
318
|
+
|
|
319
|
+
const collections = {} as Record<(typeof COLLECTION_KEYS)[number], GtmInertRecord[]>;
|
|
320
|
+
for (const key of COLLECTION_KEYS) {
|
|
321
|
+
const records = toCollection(containerVersion, key, b, acct);
|
|
322
|
+
collections[key] = records;
|
|
323
|
+
counts[key] = records.length;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Container metadata: prefer the nested `container` object where present.
|
|
327
|
+
const nested = isPlainObject(containerVersion["container"])
|
|
328
|
+
? (containerVersion["container"] as Record<string, unknown>)
|
|
329
|
+
: undefined;
|
|
330
|
+
const meta = (key: string): string | null =>
|
|
331
|
+
liftString(containerVersion, key) ?? (nested ? liftString(nested, key) : null);
|
|
332
|
+
|
|
333
|
+
const modeledContainerVersion: GtmContainerVersion = {
|
|
334
|
+
containerId: meta("containerId"),
|
|
335
|
+
path: meta("path"),
|
|
336
|
+
publicId: meta("publicId"),
|
|
337
|
+
fingerprint: liftString(containerVersion, "fingerprint"),
|
|
338
|
+
name: meta("name"),
|
|
339
|
+
tag: collections.tag,
|
|
340
|
+
trigger: collections.trigger,
|
|
341
|
+
variable: collections.variable,
|
|
342
|
+
builtInVariable: collections.builtInVariable,
|
|
343
|
+
customTemplate: collections.customTemplate,
|
|
344
|
+
folder: collections.folder,
|
|
345
|
+
zone: collections.zone,
|
|
346
|
+
client: collections.client,
|
|
347
|
+
gtagConfig: collections.gtagConfig,
|
|
348
|
+
transformation: collections.transformation,
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
// 6. Complete accounting: nothing silently dropped.
|
|
352
|
+
const collectionSet = new Set<string>(COLLECTION_KEYS);
|
|
353
|
+
const unknownContainerKeys: string[] = [];
|
|
354
|
+
for (const key of Object.keys(containerVersion)) {
|
|
355
|
+
if (POLLUTING_KEYS.has(key)) {
|
|
356
|
+
acct.quarantinedKeys += 1;
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
if (!collectionSet.has(key) && !METADATA_KEYS.has(key)) {
|
|
360
|
+
unknownContainerKeys.push(key);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
const unknownTopLevelKeys: string[] = [];
|
|
364
|
+
for (const key of Object.keys(root)) {
|
|
365
|
+
if (POLLUTING_KEYS.has(key)) {
|
|
366
|
+
acct.quarantinedKeys += 1;
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
if (key !== "exportFormatVersion" && key !== "containerVersion") {
|
|
370
|
+
unknownTopLevelKeys.push(key);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const accounting: GtmParseAccounting = {
|
|
375
|
+
counts,
|
|
376
|
+
unknownTopLevelKeys,
|
|
377
|
+
unknownContainerKeys,
|
|
378
|
+
truncatedStrings: acct.truncatedStrings,
|
|
379
|
+
quarantinedKeys: acct.quarantinedKeys,
|
|
380
|
+
totalNodes: countNodes(root, b),
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
const container: GtmContainer = {
|
|
384
|
+
exportFormatVersion: efv as number,
|
|
385
|
+
containerVersion: modeledContainerVersion,
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
return { ok: true, container, accounting };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Count nodes for accounting (the tree already passed `enforceBounds`, so this is
|
|
393
|
+
* bounded by `maxNodes`). Iterative to stay stack-safe.
|
|
394
|
+
*/
|
|
395
|
+
function countNodes(root: unknown, bounds: ParseBounds): number {
|
|
396
|
+
let nodes = 0;
|
|
397
|
+
const stack: unknown[] = [root];
|
|
398
|
+
while (stack.length > 0 && nodes <= bounds.maxNodes) {
|
|
399
|
+
const value = stack.pop();
|
|
400
|
+
nodes += 1;
|
|
401
|
+
if (Array.isArray(value)) {
|
|
402
|
+
const limit = Math.min(value.length, bounds.maxArrayLen);
|
|
403
|
+
for (let i = 0; i < limit; i += 1) stack.push(value[i]);
|
|
404
|
+
} else if (value !== null && typeof value === "object") {
|
|
405
|
+
const obj = value as Record<string, unknown>;
|
|
406
|
+
for (const key of Object.keys(obj)) stack.push(obj[key]);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
return nodes;
|
|
410
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure, NON-EXECUTING reference-extraction helpers for the GTM migration graph.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY CONTRACT (inherited from the parser — a reviewer WILL reject violations):
|
|
5
|
+
* - This is pure DATA transformation. Nothing here executes, evaluates,
|
|
6
|
+
* interprets, or compiles any part of the (untrusted) GTM export. `{{...}}`
|
|
7
|
+
* variable-reference extraction is STRING SCANNING ONLY (a bounded regex walk),
|
|
8
|
+
* never evaluation of the referenced variable or of any code-bearing field.
|
|
9
|
+
* - Pure and deterministic: no I/O, no network, no filesystem, no timers, no
|
|
10
|
+
* globals mutated. Same input → same output, stable ordering.
|
|
11
|
+
* - Every scan is BOUNDED (token caps) so an adversarial string/bag cannot blow
|
|
12
|
+
* up the walk. Missing/wrong-typed fields degrade to empty, never throw.
|
|
13
|
+
* - `raw` bags are null-prototype (see parse.ts); reads here use own-key access
|
|
14
|
+
* and never trust a prototype chain.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { GtmInertRecord } from "./types.js";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Matches a single GTM variable reference token `{{Variable Name}}`.
|
|
21
|
+
*
|
|
22
|
+
* The inner class excludes `{` and `}` so nested/broken braces cannot span tokens,
|
|
23
|
+
* and `+?` keeps the match minimal. This is a lexical scan of inert text — the
|
|
24
|
+
* captured name is DATA (a lookup key), never something we evaluate.
|
|
25
|
+
*/
|
|
26
|
+
const TOKEN_RE = /\{\{\s*([^{}]+?)\s*\}\}/g;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* GTM custom-template type discriminator. A tag/variable backed by a custom
|
|
30
|
+
* template carries a `type` like `cvt_<containerId>_<n>` (which equals the
|
|
31
|
+
* template's `templateId`). This is a shape test only — no interpretation.
|
|
32
|
+
*/
|
|
33
|
+
const CUSTOM_TEMPLATE_TYPE_RE = /^cvt_/;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Result of a bounded `{{...}}` token scan — the recovered names plus whether
|
|
37
|
+
* the PER-STRING match cap engaged (see `extractVariableTokensReport`).
|
|
38
|
+
*/
|
|
39
|
+
export interface ExtractVariableTokensResult {
|
|
40
|
+
/** Referenced variable names, trimmed/de-duplicated, insertion-ordered. */
|
|
41
|
+
readonly tokens: string[];
|
|
42
|
+
/**
|
|
43
|
+
* True when some individual string in `value` carried MORE `{{...}}`
|
|
44
|
+
* regex matches than `cap` allows to be consumed — i.e. the per-string
|
|
45
|
+
* match cap engaged and later matches in that string were never scanned.
|
|
46
|
+
* Distinct from the overall distinct-token cap (`tokens.length >= cap`),
|
|
47
|
+
* which the caller can already observe directly.
|
|
48
|
+
*/
|
|
49
|
+
readonly perStringCapped: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recursively scan an arbitrary inert JSON value for GTM `{{Variable Name}}`
|
|
54
|
+
* references and return the referenced variable NAMES (trimmed, de-duplicated,
|
|
55
|
+
* insertion-ordered for determinism) plus per-string cap observability, capped
|
|
56
|
+
* at `cap` total names.
|
|
57
|
+
*
|
|
58
|
+
* Bounded on every axis so an adversarial value cannot blow up the walk:
|
|
59
|
+
* - at most `cap` distinct names are returned (scan stops once reached);
|
|
60
|
+
* - at most `cap` regex matches are consumed per individual string;
|
|
61
|
+
* - object/array traversal is depth-first over own keys/indices only.
|
|
62
|
+
*
|
|
63
|
+
* NEVER executes anything: `{{...}}` is matched as text, the name is kept as a
|
|
64
|
+
* string key. Non-string leaves contribute nothing.
|
|
65
|
+
*/
|
|
66
|
+
export function extractVariableTokensReport(value: unknown, cap: number): ExtractVariableTokensResult {
|
|
67
|
+
const out: string[] = [];
|
|
68
|
+
let perStringCapped = false;
|
|
69
|
+
if (!(Number.isFinite(cap) && cap > 0)) return { tokens: out, perStringCapped };
|
|
70
|
+
const seen = new Set<string>();
|
|
71
|
+
|
|
72
|
+
// Explicit stack — iterative traversal, so a deeply nested bag cannot overflow
|
|
73
|
+
// the JS call stack. (The parser already depth-bounds `raw`, defensive here.)
|
|
74
|
+
const stack: unknown[] = [value];
|
|
75
|
+
while (stack.length > 0) {
|
|
76
|
+
if (seen.size >= cap) break;
|
|
77
|
+
const node = stack.pop();
|
|
78
|
+
|
|
79
|
+
if (typeof node === "string") {
|
|
80
|
+
TOKEN_RE.lastIndex = 0;
|
|
81
|
+
let match: RegExpExecArray | null;
|
|
82
|
+
let perString = 0;
|
|
83
|
+
while ((match = TOKEN_RE.exec(node)) !== null) {
|
|
84
|
+
if (perString >= cap) {
|
|
85
|
+
// Bound work on a single adversarial string — more matches remained
|
|
86
|
+
// in this string than the per-string cap allows to be consumed.
|
|
87
|
+
perStringCapped = true;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
perString += 1;
|
|
91
|
+
const name = match[1] ?? "";
|
|
92
|
+
const trimmed = name.trim();
|
|
93
|
+
if (trimmed.length === 0) continue;
|
|
94
|
+
if (!seen.has(trimmed)) {
|
|
95
|
+
seen.add(trimmed);
|
|
96
|
+
out.push(trimmed);
|
|
97
|
+
if (seen.size >= cap) break;
|
|
98
|
+
}
|
|
99
|
+
// Guard against a zero-width match looping forever (regex uses +?, but
|
|
100
|
+
// be defensive if the engine ever returns an empty match).
|
|
101
|
+
if (match.index === TOKEN_RE.lastIndex) TOKEN_RE.lastIndex += 1;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (Array.isArray(node)) {
|
|
107
|
+
// Push in reverse so pop() yields left-to-right (stable) order.
|
|
108
|
+
for (let i = node.length - 1; i >= 0; i -= 1) stack.push(node[i]);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (node !== null && typeof node === "object") {
|
|
113
|
+
const obj = node as Record<string, unknown>;
|
|
114
|
+
const keys = Object.keys(obj);
|
|
115
|
+
for (let i = keys.length - 1; i >= 0; i -= 1) {
|
|
116
|
+
const k = keys[i];
|
|
117
|
+
if (k !== undefined) stack.push(obj[k]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { tokens: out, perStringCapped };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Convenience wrapper over `extractVariableTokensReport` for callers that only
|
|
127
|
+
* need the recovered names (not per-string cap observability).
|
|
128
|
+
*/
|
|
129
|
+
export function extractVariableTokens(value: unknown, cap: number): string[] {
|
|
130
|
+
return extractVariableTokensReport(value, cap).tokens;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Read a null-prototype-safe own value off a `raw` bag (missing → undefined). */
|
|
134
|
+
function own(raw: Record<string, unknown>, key: string): unknown {
|
|
135
|
+
return Object.prototype.hasOwnProperty.call(raw, key) ? raw[key] : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read a GTM trigger-id list off a record's `raw` under `key`
|
|
140
|
+
* (`firingTriggerId` / `blockingTriggerId`). GTM stores these as arrays of id
|
|
141
|
+
* STRINGS. Defensive: missing/non-array → `[]`; non-string / empty entries are
|
|
142
|
+
* dropped; result is de-duplicated with stable order.
|
|
143
|
+
*/
|
|
144
|
+
export function readTriggerIds(raw: Record<string, unknown>, key: string): string[] {
|
|
145
|
+
const v = own(raw, key);
|
|
146
|
+
if (!Array.isArray(v)) return [];
|
|
147
|
+
const out: string[] = [];
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
for (const entry of v) {
|
|
150
|
+
if (typeof entry !== "string") continue;
|
|
151
|
+
if (entry.length === 0) continue;
|
|
152
|
+
if (seen.has(entry)) continue;
|
|
153
|
+
seen.add(entry);
|
|
154
|
+
out.push(entry);
|
|
155
|
+
}
|
|
156
|
+
return out;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Read `parentFolderId` off a record's `raw`. Defensive: missing / non-string /
|
|
161
|
+
* empty → `null`.
|
|
162
|
+
*/
|
|
163
|
+
export function readParentFolderId(raw: Record<string, unknown>): string | null {
|
|
164
|
+
const v = own(raw, "parentFolderId");
|
|
165
|
+
return typeof v === "string" && v.length > 0 ? v : null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** True when a `type` string denotes a GTM custom template (`cvt_<containerId>_<n>`). */
|
|
169
|
+
export function isCustomTemplateType(type: string | null): boolean {
|
|
170
|
+
return typeof type === "string" && CUSTOM_TEMPLATE_TYPE_RE.test(type);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The custom-template type reference a tag/variable uses, or `null` when its
|
|
175
|
+
* `type` is not a custom-template type. The returned string is the FULL
|
|
176
|
+
* `cvt_<containerId>_<n>` token; resolve it against the `customTemplate`
|
|
177
|
+
* collection either directly (see `readTemplateTrailingId` for the common
|
|
178
|
+
* case where the collection is keyed by the bare `<n>`).
|
|
179
|
+
*/
|
|
180
|
+
export function readTemplateTypeRef(record: GtmInertRecord): string | null {
|
|
181
|
+
return isCustomTemplateType(record.type) ? record.type : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Extract the trailing `<n>` segment from a `cvt_<containerId>_<n>`
|
|
186
|
+
* custom-template type token — the substring after the LAST underscore. A
|
|
187
|
+
* real GTM tag/variable carries the full `cvt_<containerId>_<n>` as its
|
|
188
|
+
* `type`, while the `customTemplate` record it points at advertises just the
|
|
189
|
+
* bare `<n>` as its `templateId` (see `readTemplateId`). This is a pure
|
|
190
|
+
* string split, never interpretation of the token's meaning.
|
|
191
|
+
*
|
|
192
|
+
* Defensive: not a custom-template type, or a malformed/truncated token with
|
|
193
|
+
* nothing after the last underscore (e.g. `"cvt_5_"`) → `null`.
|
|
194
|
+
*/
|
|
195
|
+
export function readTemplateTrailingId(type: string): string | null {
|
|
196
|
+
if (!isCustomTemplateType(type)) return null;
|
|
197
|
+
const idx = type.lastIndexOf("_");
|
|
198
|
+
if (idx < 0 || idx === type.length - 1) return null;
|
|
199
|
+
return type.slice(idx + 1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The `templateId` a `customTemplate` record advertises (what a tag/variable's
|
|
204
|
+
* `cvt_*` type resolves against). Falls back to the record's lifted `id`.
|
|
205
|
+
* Defensive: missing/non-string → `null`.
|
|
206
|
+
*/
|
|
207
|
+
export function readTemplateId(record: GtmInertRecord): string | null {
|
|
208
|
+
const v = own(record.raw, "templateId");
|
|
209
|
+
if (typeof v === "string" && v.length > 0) return v;
|
|
210
|
+
return record.id;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* GTM reserves a high numeric id block for BUILT-IN triggers it never
|
|
215
|
+
* materializes as `trigger` collection objects (a container export lists the
|
|
216
|
+
* built-ins a tag fires on only by their reserved id, e.g. `2147479553` for
|
|
217
|
+
* "All Pages"). The block sits just under the signed-32-bit ceiling; container-
|
|
218
|
+
* relative trigger ids are small integers, so the boundary cleanly separates the
|
|
219
|
+
* two. A `firingTriggerId`/`blockingTriggerId` in this range is a valid built-in
|
|
220
|
+
* reference — NOT a broken/dangling one.
|
|
221
|
+
*/
|
|
222
|
+
export const GTM_RESERVED_TRIGGER_MIN = 2_147_479_553;
|
|
223
|
+
export const GTM_RESERVED_TRIGGER_MAX = 2_147_483_647; // signed-32-bit max
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Known reserved built-in trigger ids → their GTM label. Ids in the reserved
|
|
227
|
+
* range but absent here resolve to a generic built-in label rather than
|
|
228
|
+
* dangling. This is a pure lookup table — no interpretation of the id's meaning.
|
|
229
|
+
*/
|
|
230
|
+
const KNOWN_BUILTIN_TRIGGERS = new Map<string, string>([
|
|
231
|
+
["2147479553", "All Pages"],
|
|
232
|
+
["2147479572", "Consent Initialization"],
|
|
233
|
+
["2147479573", "Initialization"],
|
|
234
|
+
]);
|
|
235
|
+
|
|
236
|
+
/** A resolved built-in trigger: its display label + whether the exact id is known. */
|
|
237
|
+
export interface BuiltInTriggerInfo {
|
|
238
|
+
readonly label: string;
|
|
239
|
+
readonly known: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolve a trigger-id string to built-in-trigger info when it falls in GTM's
|
|
244
|
+
* reserved range, else `null` (a real container-relative id, or a non-numeric /
|
|
245
|
+
* out-of-range token that must be treated as a genuine reference). Pure string →
|
|
246
|
+
* bounded numeric parse; never interprets the id's runtime meaning.
|
|
247
|
+
*/
|
|
248
|
+
export function resolveBuiltInTrigger(id: string): BuiltInTriggerInfo | null {
|
|
249
|
+
// Bounded, digits-only parse — an adversarial huge/garbage token never resolves.
|
|
250
|
+
if (!/^[0-9]{1,10}$/.test(id)) return null;
|
|
251
|
+
const n = Number(id);
|
|
252
|
+
if (!Number.isSafeInteger(n)) return null;
|
|
253
|
+
if (n < GTM_RESERVED_TRIGGER_MIN || n > GTM_RESERVED_TRIGGER_MAX) return null;
|
|
254
|
+
const label = KNOWN_BUILTIN_TRIGGERS.get(id);
|
|
255
|
+
return label ? { label, known: true } : { label: "GTM built-in trigger", known: false };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* True when a `{{...}}` reference token names a GTM BUILT-IN variable — the
|
|
260
|
+
* underscore-prefixed internal dataLayer names (e.g. `{{_event}}`) that a
|
|
261
|
+
* container references but never materializes in the `variable` /
|
|
262
|
+
* `builtInVariable` collections. Such a reference is valid, not dangling.
|
|
263
|
+
*/
|
|
264
|
+
export function isBuiltInVariableRef(name: string): boolean {
|
|
265
|
+
return name.length > 1 && name.charCodeAt(0) === 0x5f /* '_' */;
|
|
266
|
+
}
|