@tokenoftrust/storefront-runner 1.4.2-rc.0 → 2.0.0
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/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 +4 -4
- 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 +97 -10
- package/apps/storefront/src/lib/blog/markdown.ts +1 -1
- package/apps/storefront/src/lib/blog/provider.ts +57 -3
- 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 +168 -60
- 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 +31 -27
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +1 -1
- 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/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 +45 -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/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 +1 -1
- package/scripts/tot-dev.mjs +2 -2
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Managed analytics API v1 facade over the canonical app registry authorities.
|
|
3
|
+
* Installs live in `app_installs`; live versions in `app_version_snapshots`;
|
|
4
|
+
* candidate history in `app_audit_log`; inert drafts in their narrow store;
|
|
5
|
+
* retries in the existing `idempotency_keys` ledger.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
MANAGED_ANALYTICS_API_VERSION, MANAGED_ANALYTICS_SCHEMA_VERSION,
|
|
9
|
+
getPlatformAdapter, platformAdapterCatalog, sha256Hex,
|
|
10
|
+
buildPlatformAdapterDeclaration, validateIntegrationManifest,
|
|
11
|
+
resolveEffectivePlan, compileIntegrationCspFromPlan,
|
|
12
|
+
collectDeclaredCspProfiles, ENVIRONMENT_SCHEMA_VERSION,
|
|
13
|
+
validateManagedAnalyticsSafePayload,
|
|
14
|
+
validateApiErrorEnvelope, validateIntegrationInstall,
|
|
15
|
+
validateAdapterDefinition, validateIntegrationRevisionV1, validateMigrationDraftV1,
|
|
16
|
+
validateCreateMigrationDraftRequest, validateUpdateMigrationDraftRequest,
|
|
17
|
+
validateDeleteMigrationDraftRequest, validateCreateIntegrationInstallRequest,
|
|
18
|
+
validateReviseIntegrationInstallRequest, validateSetIntegrationInstallStatusRequest,
|
|
19
|
+
validateDeleteIntegrationInstallRequest,
|
|
20
|
+
snapshotManagedAnalyticsDeclarativeInput,
|
|
21
|
+
type AdapterConfigValue, type AdapterDefinition, type ApiErrorEnvelope,
|
|
22
|
+
type ApiResult, type CreateIntegrationInstallRequest,
|
|
23
|
+
type CreateMigrationDraftRequest, type DeleteIntegrationInstallRequest,
|
|
24
|
+
type DeleteMigrationDraftRequest, type IntegrationInstall,
|
|
25
|
+
type IntegrationRevisionV1, type ListAdapterDefinitionsRequest,
|
|
26
|
+
type ListIntegrationInstallsRequest, type MigrationDraftV1,
|
|
27
|
+
type MutationResult, type ReviseIntegrationInstallRequest,
|
|
28
|
+
type SetIntegrationInstallStatusRequest, type UpdateMigrationDraftRequest,
|
|
29
|
+
type IntegrationManifest,
|
|
30
|
+
} from "@tot/public-runtime";
|
|
31
|
+
import type { Queryable, QueryStatement } from "../d1/catalog.js";
|
|
32
|
+
import { AppVersionStore, type AppVersionFacets } from "./appVersionStore.js";
|
|
33
|
+
|
|
34
|
+
export type ManagedAnalyticsOperation =
|
|
35
|
+
| "listAdapterDefinitions" | "getAdapterDefinition"
|
|
36
|
+
| "listIntegrationInstalls" | "getIntegrationInstall"
|
|
37
|
+
| "listIntegrationRevisions" | "getIntegrationRevision"
|
|
38
|
+
| "getMigrationDraft" | "createMigrationDraft" | "updateMigrationDraft"
|
|
39
|
+
| "deleteMigrationDraft" | "createIntegrationInstall"
|
|
40
|
+
| "reviseIntegrationInstall" | "setIntegrationInstallStatus"
|
|
41
|
+
| "deleteIntegrationInstall";
|
|
42
|
+
|
|
43
|
+
export const MANAGED_ANALYTICS_READ_OPERATIONS: ReadonlySet<ManagedAnalyticsOperation> = new Set([
|
|
44
|
+
"listAdapterDefinitions", "getAdapterDefinition", "listIntegrationInstalls",
|
|
45
|
+
"getIntegrationInstall", "listIntegrationRevisions", "getIntegrationRevision",
|
|
46
|
+
"getMigrationDraft",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
export interface ManagedAnalyticsActorContext { tenantId: string; actor: string }
|
|
50
|
+
interface Effects { digest: string; cspOrigins: string[]; subscriptions: string[]; cacheIdentity: string }
|
|
51
|
+
interface CandidateRecord { revision: IntegrationRevisionV1; digest: string; effects: Effects; approvedAt: string }
|
|
52
|
+
interface StoredCandidate extends CandidateRecord {
|
|
53
|
+
manifest: IntegrationManifest;
|
|
54
|
+
facets: AppVersionFacets;
|
|
55
|
+
/** Immutable activated snapshot selected by a revise-to-prior-config rollback. */
|
|
56
|
+
rollbackFromSeq?: number;
|
|
57
|
+
}
|
|
58
|
+
interface InstallRow {
|
|
59
|
+
install_id: string; app_id: string; app_version: string; tenant_id: string;
|
|
60
|
+
env: string; status: string; scopes: string; manifest: string;
|
|
61
|
+
webhook_endpoint: string | null; widget_placements: string; install_actor: string;
|
|
62
|
+
managed_kind: string | null; managed_revision: number; managed_active_revision_id: string | null;
|
|
63
|
+
managed_candidate: string | null;
|
|
64
|
+
created_at: string; updated_at: string;
|
|
65
|
+
}
|
|
66
|
+
interface AuditRow { id: string; event: string; detail: string; at: string }
|
|
67
|
+
interface DraftRow { draft_id: string; tenant_id: string; revision: number; state: string; record: string; created_at: string; updated_at: string }
|
|
68
|
+
interface IdempotencyRow { body_hash: string; response_body: string }
|
|
69
|
+
type CandidateValidation =
|
|
70
|
+
| { ok: true; effects: Effects; facets: AppVersionFacets; manifest: IntegrationManifest }
|
|
71
|
+
| { ok: false; details: Array<{ path: string; code: string; message: string }> };
|
|
72
|
+
interface MutationPlan<T> {
|
|
73
|
+
response: MutationResult<T>;
|
|
74
|
+
statements: QueryStatement[];
|
|
75
|
+
casStillCurrent?: () => Promise<boolean>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const KIND = "managed-analytics-v1";
|
|
79
|
+
/**
|
|
80
|
+
* Preserve UUID entropy while preventing any contract-visible identifier from
|
|
81
|
+
* accidentally resembling a phone/card number to the frozen value canary.
|
|
82
|
+
* Underscores are PUBLIC_ID-safe and split every hexadecimal run at four chars.
|
|
83
|
+
*/
|
|
84
|
+
export function managedAnalyticsOpaqueId(prefix: string, uuid = crypto.randomUUID()): string {
|
|
85
|
+
const hex = uuid.replaceAll("-", "");
|
|
86
|
+
if (!/^[0-9a-f]{32}$/i.test(hex)) throw new TypeError("Managed analytics entropy must be a UUID.");
|
|
87
|
+
return `${prefix}_${hex.match(/.{1,4}/g)!.join("_")}`;
|
|
88
|
+
}
|
|
89
|
+
function requestId(): string { return managedAnalyticsOpaqueId("req"); }
|
|
90
|
+
function publicId(prefix: string): string { return managedAnalyticsOpaqueId(prefix); }
|
|
91
|
+
function stableValue(value: unknown): unknown {
|
|
92
|
+
if (Array.isArray(value)) return value.map(stableValue);
|
|
93
|
+
if (value && typeof value === "object") return Object.fromEntries(
|
|
94
|
+
Object.entries(value as Record<string, unknown>).sort(([a], [b]) => a.localeCompare(b))
|
|
95
|
+
.map(([key, child]) => [key, stableValue(child)]),
|
|
96
|
+
);
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function canonicalJson(value: unknown): string { return JSON.stringify(stableValue(value)); }
|
|
100
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
101
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null;
|
|
102
|
+
}
|
|
103
|
+
function exactKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
|
|
104
|
+
return canonicalJson(Object.keys(value).sort()) === canonicalJson([...keys].sort());
|
|
105
|
+
}
|
|
106
|
+
function validatesReplayData(operation: ManagedAnalyticsOperation, data: unknown): boolean {
|
|
107
|
+
if (operation === "createMigrationDraft" || operation === "updateMigrationDraft") {
|
|
108
|
+
return validateMigrationDraftV1(data).ok;
|
|
109
|
+
}
|
|
110
|
+
if (operation === "setIntegrationInstallStatus") return validateIntegrationInstall(data).ok;
|
|
111
|
+
if (operation === "createIntegrationInstall" || operation === "reviseIntegrationInstall") {
|
|
112
|
+
const value = record(data);
|
|
113
|
+
return !!value && exactKeys(value, ["install", "revision"]) &&
|
|
114
|
+
validateIntegrationInstall(value["install"]).ok && validateIntegrationRevisionV1(value["revision"]).ok;
|
|
115
|
+
}
|
|
116
|
+
if (operation === "deleteMigrationDraft" || operation === "deleteIntegrationInstall") {
|
|
117
|
+
const value = record(data);
|
|
118
|
+
return !!value && exactKeys(value, ["deleted"]) && value["deleted"] === true;
|
|
119
|
+
}
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
function validatesPage(data: unknown, itemValidator: (item: unknown) => { ok: boolean }): boolean {
|
|
123
|
+
const value = record(data);
|
|
124
|
+
if (!value || !Array.isArray(value["items"]) || !exactKeys(value, value["nextCursor"] === undefined ? ["items"] : ["items", "nextCursor"])) return false;
|
|
125
|
+
if (value["nextCursor"] !== undefined && !validOpaqueId(value["nextCursor"])) return false;
|
|
126
|
+
return value["items"].every((item) => itemValidator(item).ok);
|
|
127
|
+
}
|
|
128
|
+
function validOpaqueId(value: unknown): boolean {
|
|
129
|
+
return validateApiErrorEnvelope({ apiVersion: "v1", requestId: value, error: { code: "internal_error", message: "Internal error." } }).ok;
|
|
130
|
+
}
|
|
131
|
+
function validSafeOpaqueId(value: unknown): value is string {
|
|
132
|
+
return typeof value === "string" && validOpaqueId(value) && validateManagedAnalyticsSafePayload(value).ok;
|
|
133
|
+
}
|
|
134
|
+
function validIdempotencyKey(value: unknown): value is string {
|
|
135
|
+
return validSafeOpaqueId(value) && value.length >= 8 && value.length <= 128;
|
|
136
|
+
}
|
|
137
|
+
function validatesOutboundData(operation: ManagedAnalyticsOperation, data: unknown): boolean {
|
|
138
|
+
switch (operation) {
|
|
139
|
+
case "listAdapterDefinitions": return validatesPage(data, validateAdapterDefinition);
|
|
140
|
+
case "getAdapterDefinition": return validateAdapterDefinition(data).ok;
|
|
141
|
+
case "listIntegrationInstalls": return validatesPage(data, validateIntegrationInstall);
|
|
142
|
+
case "getIntegrationInstall": return validateIntegrationInstall(data).ok;
|
|
143
|
+
case "listIntegrationRevisions": return validatesPage(data, validateIntegrationRevisionV1);
|
|
144
|
+
case "getIntegrationRevision": return validateIntegrationRevisionV1(data).ok;
|
|
145
|
+
case "getMigrationDraft": return validateMigrationDraftV1(data).ok;
|
|
146
|
+
default: return validatesReplayData(operation, data);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Final frozen-wire guard and detached value used by persistence/routes. */
|
|
151
|
+
export function sanitizeManagedAnalyticsOutbound(operation: ManagedAnalyticsOperation, input: unknown): unknown | null {
|
|
152
|
+
const safe = validateManagedAnalyticsSafePayload(input);
|
|
153
|
+
if (!safe.ok) return null;
|
|
154
|
+
const envelope = record(safe.value);
|
|
155
|
+
if (!envelope) return null;
|
|
156
|
+
if (Object.prototype.hasOwnProperty.call(envelope, "error")) return validateApiErrorEnvelope(envelope).ok ? safe.value : null;
|
|
157
|
+
const mutation = !MANAGED_ANALYTICS_READ_OPERATIONS.has(operation);
|
|
158
|
+
const keys = mutation ? ["apiVersion", "requestId", "data", "idempotencyReplayed"] : ["apiVersion", "requestId", "data"];
|
|
159
|
+
if (!exactKeys(envelope, keys) || envelope["apiVersion"] !== "v1" || !validOpaqueId(envelope["requestId"])) return null;
|
|
160
|
+
if (mutation && typeof envelope["idempotencyReplayed"] !== "boolean") return null;
|
|
161
|
+
return validatesOutboundData(operation, envelope["data"]) ? safe.value : null;
|
|
162
|
+
}
|
|
163
|
+
export function validateManagedAnalyticsOutbound(operation: ManagedAnalyticsOperation, input: unknown): boolean {
|
|
164
|
+
return sanitizeManagedAnalyticsOutbound(operation, input) !== null;
|
|
165
|
+
}
|
|
166
|
+
function success<T>(data: T): ApiResult<T> {
|
|
167
|
+
return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), data };
|
|
168
|
+
}
|
|
169
|
+
function mutationSuccess<T>(data: T): MutationResult<T> {
|
|
170
|
+
return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), data, idempotencyReplayed: false };
|
|
171
|
+
}
|
|
172
|
+
function error(code: ApiErrorEnvelope["error"]["code"], message: string, details?: ApiErrorEnvelope["error"]["details"]): ApiErrorEnvelope {
|
|
173
|
+
return { apiVersion: MANAGED_ANALYTICS_API_VERSION, requestId: requestId(), error: { code, message, ...(details?.length ? { details } : {}) } };
|
|
174
|
+
}
|
|
175
|
+
function notFound(): ApiErrorEnvelope { return error("not_found", "The requested resource was not found."); }
|
|
176
|
+
function stale(): ApiErrorEnvelope { return error("stale_revision", "The resource changed; refresh and retry."); }
|
|
177
|
+
function internalError(): ApiErrorEnvelope { return error("internal_error", "The integration service could not complete the request."); }
|
|
178
|
+
function parseCandidate(row: InstallRow): StoredCandidate | null {
|
|
179
|
+
return row.managed_candidate ? JSON.parse(row.managed_candidate) as StoredCandidate : null;
|
|
180
|
+
}
|
|
181
|
+
function parseLive(row: InstallRow): IntegrationManifest | null {
|
|
182
|
+
const value = JSON.parse(row.manifest) as unknown;
|
|
183
|
+
const valid = validateIntegrationManifest(value);
|
|
184
|
+
return valid.ok ? valid.value : null;
|
|
185
|
+
}
|
|
186
|
+
function publicInstall(row: InstallRow): IntegrationInstall {
|
|
187
|
+
const live = parseLive(row);
|
|
188
|
+
const value: IntegrationInstall = {
|
|
189
|
+
schemaVersion: MANAGED_ANALYTICS_SCHEMA_VERSION, installId: row.install_id,
|
|
190
|
+
adapterId: row.app_id, status: row.status === "active" && live ? "enabled" : "disabled",
|
|
191
|
+
activeRevisionId: row.managed_active_revision_id, revision: row.managed_revision,
|
|
192
|
+
createdAt: row.created_at, updatedAt: row.updated_at,
|
|
193
|
+
};
|
|
194
|
+
const validated = validateIntegrationInstall(value);
|
|
195
|
+
if (!validated.ok) throw new Error("Managed analytics install failed persisted-data validation.");
|
|
196
|
+
return validated.value;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Build the public managed-analytics adapter catalog from the canonical platform
|
|
200
|
+
* adapter cards. Exported for read-only/offline contract proofs; callers receive
|
|
201
|
+
* detached data and no tenant or persistence authority.
|
|
202
|
+
*/
|
|
203
|
+
export function managedAnalyticsAdapterDefinitions(): AdapterDefinition[] {
|
|
204
|
+
return platformAdapterCatalog().map((card) => ({
|
|
205
|
+
schemaVersion: MANAGED_ANALYTICS_SCHEMA_VERSION, adapterId: card.adapterId,
|
|
206
|
+
adapterVersion: "1.0.0", displayName: card.name, description: card.purpose,
|
|
207
|
+
availability: "available",
|
|
208
|
+
configuration: [{ key: "destinationId", label: card.idLabel, description: "Public destination identifier used by this reviewed adapter.", kind: "string", required: true }],
|
|
209
|
+
eventTypes: [...card.eventTypes], consentPurposes: [...card.consentPurposes],
|
|
210
|
+
collectionOrigins: [...card.cspOrigins].sort(),
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
213
|
+
const CATALOG = managedAnalyticsAdapterDefinitions();
|
|
214
|
+
const CATALOG_BY_ID: Readonly<Record<string, AdapterDefinition>> = Object.assign(
|
|
215
|
+
Object.create(null) as Record<string, AdapterDefinition>,
|
|
216
|
+
Object.fromEntries(CATALOG.map((item) => [item.adapterId, item])),
|
|
217
|
+
);
|
|
218
|
+
function page<T extends Record<I, string>, I extends string>(items: T[], id: I, query: { cursor?: string; limit?: number }) {
|
|
219
|
+
const found = query.cursor ? items.findIndex((item) => item[id] === query.cursor) : -1;
|
|
220
|
+
const start = found < 0 ? 0 : found + 1;
|
|
221
|
+
const limit = Math.min(query.limit ?? 50, 100);
|
|
222
|
+
const selected = items.slice(start, start + limit);
|
|
223
|
+
return { items: selected, ...(start + limit < items.length && selected.length ? { nextCursor: selected.at(-1)![id] } : {}) };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Pure validation and declarative effect compilation; performs no I/O. */
|
|
227
|
+
export async function validateManagedAnalyticsCandidate(input: {
|
|
228
|
+
adapterId: string; adapterVersion: string; configuration: Record<string, AdapterConfigValue>;
|
|
229
|
+
}, tenantPublicId = "managed-validation.invalid"): Promise<CandidateValidation> {
|
|
230
|
+
const definition = Object.prototype.hasOwnProperty.call(CATALOG_BY_ID, input.adapterId)
|
|
231
|
+
? CATALOG_BY_ID[input.adapterId] : undefined;
|
|
232
|
+
if (!definition) return { ok: false, details: [{ path: "body.adapterId", code: "unknown_adapter", message: "Unknown adapter." }] };
|
|
233
|
+
if (definition.adapterVersion !== input.adapterVersion) return { ok: false, details: [{ path: "body.adapterVersion", code: "unknown_version", message: "Unknown adapter version." }] };
|
|
234
|
+
const keys = Object.keys(input.configuration);
|
|
235
|
+
if (keys.length !== 1 || keys[0] !== "destinationId") return { ok: false, details: [{ path: "body.configuration", code: "configuration_shape", message: "Configuration does not match the adapter definition." }] };
|
|
236
|
+
const destinationId = input.configuration.destinationId;
|
|
237
|
+
const platform = getPlatformAdapter(input.adapterId);
|
|
238
|
+
if (typeof destinationId !== "string" || !platform?.validateId(destinationId)) return { ok: false, details: [{ path: "body.configuration.destinationId", code: "format", message: "Destination identifier has an invalid format." }] };
|
|
239
|
+
const safe = validateManagedAnalyticsSafePayload({ adapterId: input.adapterId, adapterVersion: input.adapterVersion, configuration: input.configuration });
|
|
240
|
+
if (!safe.ok) return { ok: false, details: safe.violations };
|
|
241
|
+
// The frozen public candidate body is the only value scanned by the safety
|
|
242
|
+
// validator above. The auth-derived tenant scope is a separate materializer
|
|
243
|
+
// argument and can therefore never become an unpublished request field.
|
|
244
|
+
const tenantId = tenantPublicId;
|
|
245
|
+
const built = buildPlatformAdapterDeclaration({
|
|
246
|
+
adapterId: input.adapterId,
|
|
247
|
+
ids: { live: destinationId },
|
|
248
|
+
tenantPublicId: tenantId,
|
|
249
|
+
environment: {
|
|
250
|
+
schemaVersion: ENVIRONMENT_SCHEMA_VERSION,
|
|
251
|
+
siteSurface: "admin",
|
|
252
|
+
environment: "live",
|
|
253
|
+
canonicalHost: tenantId,
|
|
254
|
+
revision: "managed-analytics-v1",
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
if (!built.ok) return { ok: false, details: built.violations.map(({ path, code, message }) => ({ path: path ? `body.${path}` : "body", code, message })) };
|
|
258
|
+
const manifest = validateIntegrationManifest(built.declaration.manifest);
|
|
259
|
+
if (!manifest.ok) return { ok: false, details: manifest.violations };
|
|
260
|
+
const resolved = resolveEffectivePlan({ tenantPublicId: tenantId, environment: {
|
|
261
|
+
schemaVersion: ENVIRONMENT_SCHEMA_VERSION, siteSurface: "admin", environment: "live",
|
|
262
|
+
canonicalHost: tenantId, revision: "managed-analytics-v1",
|
|
263
|
+
}, declarations: [built.declaration] });
|
|
264
|
+
if (!resolved.ok) return { ok: false, details: resolved.violations };
|
|
265
|
+
const compiled = compileIntegrationCspFromPlan(resolved.value, collectDeclaredCspProfiles([built.declaration]));
|
|
266
|
+
if (!compiled.ok) return { ok: false, details: compiled.violations };
|
|
267
|
+
const cspOrigins = [...new Set(Object.values(compiled.value.additions).flatMap((origins) => origins ?? []))].sort();
|
|
268
|
+
const integration = resolved.value.integrations.find((item) => item.appId === input.adapterId)!;
|
|
269
|
+
const digest = manifest.value.app.digest;
|
|
270
|
+
const effects: Effects = { digest, cspOrigins, subscriptions: [...built.card.eventTypes], cacheIdentity: `managed-analytics:${digest}` };
|
|
271
|
+
return { ok: true, effects, manifest: manifest.value, facets: {
|
|
272
|
+
appVersion: integration.version, digest, executionTier: integration.executionTier,
|
|
273
|
+
entry: integration.entry, publicConfig: { ...input.configuration },
|
|
274
|
+
contributions: [...resolved.value.contributions], scopes: [], consent: integration.consent,
|
|
275
|
+
cspOrigins,
|
|
276
|
+
} };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Thin v1 facade; canonical registry/version/audit/idempotency tables are authority. */
|
|
280
|
+
export class ManagedAnalyticsApiV1Facade {
|
|
281
|
+
private readonly versions: AppVersionStore;
|
|
282
|
+
constructor(private readonly db: Queryable, private readonly now: () => string = () => new Date().toISOString()) {
|
|
283
|
+
this.versions = new AppVersionStore(db, now);
|
|
284
|
+
}
|
|
285
|
+
listAdapterDefinitions(request: ListAdapterDefinitionsRequest) { return success(page(CATALOG, "adapterId", request.query)); }
|
|
286
|
+
getAdapterDefinition(adapterId: string): ApiResult<AdapterDefinition> {
|
|
287
|
+
const found = Object.prototype.hasOwnProperty.call(CATALOG_BY_ID, adapterId) ? CATALOG_BY_ID[adapterId] : undefined;
|
|
288
|
+
return found ? success(found) : notFound();
|
|
289
|
+
}
|
|
290
|
+
async listIntegrationInstalls(ctx: ManagedAnalyticsActorContext, request: ListIntegrationInstallsRequest) {
|
|
291
|
+
try {
|
|
292
|
+
const rows = await this.db.all<InstallRow>(`SELECT * FROM app_installs WHERE tenant_id = ? AND managed_kind = ? AND status != 'uninstalled' ORDER BY created_at, install_id`, ctx.tenantId, KIND);
|
|
293
|
+
return success(page(rows.map(publicInstall), "installId", request.query));
|
|
294
|
+
} catch { return internalError(); }
|
|
295
|
+
}
|
|
296
|
+
async getIntegrationInstall(ctx: ManagedAnalyticsActorContext, installId: string) {
|
|
297
|
+
try {
|
|
298
|
+
const row = await this.ownedInstall(ctx.tenantId, installId);
|
|
299
|
+
return row ? success(publicInstall(row)) : notFound();
|
|
300
|
+
} catch { return internalError(); }
|
|
301
|
+
}
|
|
302
|
+
async listIntegrationRevisions(ctx: ManagedAnalyticsActorContext, installId: string, query: { cursor?: string; limit?: number }) {
|
|
303
|
+
try {
|
|
304
|
+
const row = await this.ownedInstall(ctx.tenantId, installId);
|
|
305
|
+
if (!row) return notFound();
|
|
306
|
+
return success(page(await this.revisions(row), "revisionId", query));
|
|
307
|
+
} catch { return internalError(); }
|
|
308
|
+
}
|
|
309
|
+
async getIntegrationRevision(ctx: ManagedAnalyticsActorContext, installId: string, revisionId: string) {
|
|
310
|
+
try {
|
|
311
|
+
const row = await this.ownedInstall(ctx.tenantId, installId);
|
|
312
|
+
if (!row) return notFound();
|
|
313
|
+
const found = (await this.revisions(row)).find((item) => item.revisionId === revisionId);
|
|
314
|
+
return found ? success(found) : notFound();
|
|
315
|
+
} catch { return internalError(); }
|
|
316
|
+
}
|
|
317
|
+
async getMigrationDraft(ctx: ManagedAnalyticsActorContext, draftId: string) {
|
|
318
|
+
try {
|
|
319
|
+
const draft = await this.loadDraft(ctx.tenantId, draftId);
|
|
320
|
+
return draft?.open ? success(draft.draft) : notFound();
|
|
321
|
+
} catch { return internalError(); }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async createMigrationDraft(ctx: ManagedAnalyticsActorContext, request: CreateMigrationDraftRequest) {
|
|
325
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<CreateMigrationDraftRequest>(request);
|
|
326
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
327
|
+
const prepared = validateCreateMigrationDraftRequest(snapshot.value);
|
|
328
|
+
if (!prepared.ok) return this.rejectInvalidMutation<MigrationDraftV1>(ctx, "createMigrationDraft", snapshot.value, prepared.violations);
|
|
329
|
+
request = prepared.value;
|
|
330
|
+
return this.idempotent(ctx, "createMigrationDraft", request, this.tenantScope(ctx), async () => {
|
|
331
|
+
if (request.expectedRevision !== 0) return { response: stale(), statements: [] };
|
|
332
|
+
const now = this.now();
|
|
333
|
+
const draft: MigrationDraftV1 = { schemaVersion: 1, draftId: publicId("migration"), revision: 1, source: "approved-migration", status: "draft", enabled: false, publishes: false, proposedAdapterId: request.body.proposedAdapterId, proposedConfiguration: { ...request.body.proposedConfiguration }, findings: [...request.body.findings], createdAt: now, updatedAt: now };
|
|
334
|
+
const safe = validateManagedAnalyticsSafePayload(draft);
|
|
335
|
+
if (!safe.ok) return { response: error("validation_failed", "The migration draft failed validation.", safe.violations), statements: [] };
|
|
336
|
+
return { response: mutationSuccess(draft), statements: [
|
|
337
|
+
{ sql: `INSERT INTO integration_migration_drafts (draft_id, tenant_id, revision, state, record, created_at, updated_at) VALUES (?,?,?,'open',?,?,?)`, params: [draft.draftId, ctx.tenantId, draft.revision, JSON.stringify(draft), now, now] },
|
|
338
|
+
this.auditStatement(draft.draftId, ctx, "managed_analytics.migration_draft_created", { revision: draft.revision }),
|
|
339
|
+
] };
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
async updateMigrationDraft(ctx: ManagedAnalyticsActorContext, request: UpdateMigrationDraftRequest) {
|
|
343
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<UpdateMigrationDraftRequest>(request);
|
|
344
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
345
|
+
const prepared = validateUpdateMigrationDraftRequest(snapshot.value);
|
|
346
|
+
if (!prepared.ok) return this.rejectInvalidMutation<MigrationDraftV1>(ctx, "updateMigrationDraft", snapshot.value, prepared.violations, "draftId");
|
|
347
|
+
request = prepared.value;
|
|
348
|
+
const scope = request.body.draftId;
|
|
349
|
+
return this.idempotent(ctx, "updateMigrationDraft", request, scope, async () => {
|
|
350
|
+
const current = await this.loadDraft(ctx.tenantId, scope);
|
|
351
|
+
if (!current?.open) return { response: notFound(), statements: [] };
|
|
352
|
+
if (current.draft.revision !== request.expectedRevision) return { response: stale(), statements: [] };
|
|
353
|
+
const draft: MigrationDraftV1 = { ...current.draft, proposedAdapterId: request.body.proposedAdapterId, proposedConfiguration: { ...request.body.proposedConfiguration }, findings: [...request.body.findings], revision: current.draft.revision + 1, updatedAt: this.now() };
|
|
354
|
+
const safe = validateManagedAnalyticsSafePayload(draft);
|
|
355
|
+
if (!safe.ok) return { response: error("validation_failed", "The migration draft failed validation.", safe.violations), statements: [] };
|
|
356
|
+
const update: QueryStatement = { sql: `UPDATE integration_migration_drafts SET revision = ?, record = ?, updated_at = ? WHERE draft_id = ? AND tenant_id = ? AND state = 'open' AND revision = ?`, params: [draft.revision, JSON.stringify(draft), draft.updatedAt, scope, ctx.tenantId, request.expectedRevision] };
|
|
357
|
+
return { response: mutationSuccess(draft), statements: [update, this.auditStatement(scope, ctx, "managed_analytics.migration_draft_updated", { revision: draft.revision }, true)], casStillCurrent: async () => { const latest = await this.loadDraft(ctx.tenantId, scope); return latest?.open === true && latest.draft.revision === request.expectedRevision; } };
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
async deleteMigrationDraft(ctx: ManagedAnalyticsActorContext, request: DeleteMigrationDraftRequest) {
|
|
361
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<DeleteMigrationDraftRequest>(request);
|
|
362
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
363
|
+
const prepared = validateDeleteMigrationDraftRequest(snapshot.value);
|
|
364
|
+
if (!prepared.ok) return this.rejectInvalidMutation<{ deleted: true }>(ctx, "deleteMigrationDraft", snapshot.value, prepared.violations, "draftId");
|
|
365
|
+
request = prepared.value;
|
|
366
|
+
const scope = request.body.draftId;
|
|
367
|
+
return this.idempotent(ctx, "deleteMigrationDraft", request, scope, async () => {
|
|
368
|
+
const current = await this.loadDraft(ctx.tenantId, scope);
|
|
369
|
+
if (!current?.open) return { response: notFound(), statements: [] };
|
|
370
|
+
if (current.draft.revision !== request.expectedRevision) return { response: stale(), statements: [] };
|
|
371
|
+
const revision = current.draft.revision + 1;
|
|
372
|
+
const update: QueryStatement = { sql: `UPDATE integration_migration_drafts SET revision = ?, state = 'rejected', updated_at = ? WHERE draft_id = ? AND tenant_id = ? AND state = 'open' AND revision = ?`, params: [revision, this.now(), scope, ctx.tenantId, request.expectedRevision] };
|
|
373
|
+
return { response: mutationSuccess({ deleted: true as const }), statements: [update, this.auditStatement(scope, ctx, "managed_analytics.migration_draft_rejected", { revision }, true)], casStillCurrent: async () => { const latest = await this.loadDraft(ctx.tenantId, scope); return latest?.open === true && latest.draft.revision === request.expectedRevision; } };
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async createIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: CreateIntegrationInstallRequest) {
|
|
378
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<CreateIntegrationInstallRequest>(request);
|
|
379
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
380
|
+
const prepared = validateCreateIntegrationInstallRequest(snapshot.value);
|
|
381
|
+
if (!prepared.ok) return this.rejectInvalidMutation<{ install: IntegrationInstall; revision: IntegrationRevisionV1 }>(ctx, "createIntegrationInstall", snapshot.value, prepared.violations);
|
|
382
|
+
request = prepared.value;
|
|
383
|
+
return this.idempotent(ctx, "createIntegrationInstall", request, this.tenantScope(ctx), async () => {
|
|
384
|
+
if (request.expectedRevision !== 0) return { response: stale(), statements: [] };
|
|
385
|
+
const validated = await validateManagedAnalyticsCandidate(request.body, ctx.tenantId);
|
|
386
|
+
if (!validated.ok) return { response: error("validation_failed", "The integration revision failed validation.", validated.details), statements: [] };
|
|
387
|
+
const existing = await this.adapterSlot(ctx.tenantId, request.body.adapterId);
|
|
388
|
+
if (existing && (existing.managed_kind !== KIND || existing.status !== "uninstalled")) {
|
|
389
|
+
return { response: error("conflict", "This adapter is already configured."), statements: [] };
|
|
390
|
+
}
|
|
391
|
+
const now = this.now();
|
|
392
|
+
const installId = existing?.install_id ?? publicId("integration");
|
|
393
|
+
const nextRevision = existing ? existing.managed_revision + 1 : 1;
|
|
394
|
+
const revision: IntegrationRevisionV1 = { schemaVersion: 1, revisionId: publicId("revision"), installId, adapterId: request.body.adapterId, adapterVersion: request.body.adapterVersion, sequence: nextRevision, status: "draft", configuration: { ...request.body.configuration }, createdAt: now };
|
|
395
|
+
const candidate: StoredCandidate = { revision, digest: validated.effects.digest, effects: validated.effects, manifest: validated.manifest, facets: validated.facets, approvedAt: now };
|
|
396
|
+
const install: IntegrationInstall = { schemaVersion: 1, installId, adapterId: request.body.adapterId, status: "disabled", activeRevisionId: null, revision: nextRevision, createdAt: existing?.created_at ?? now, updatedAt: now };
|
|
397
|
+
const statements: QueryStatement[] = [
|
|
398
|
+
existing
|
|
399
|
+
? { sql: `UPDATE app_installs SET app_version = ?, status = 'suspended', scopes = '[]', manifest = '{}', webhook_endpoint = NULL, widget_placements = '[]', install_actor = ?, managed_active_revision_id = NULL, managed_candidate = ?, managed_revision = ?, updated_at = ? WHERE install_id = ? AND app_id = ? AND tenant_id = ? AND env = 'production' AND managed_kind = ? AND status = 'uninstalled' AND managed_revision = ?`, params: [request.body.adapterVersion, ctx.actor, JSON.stringify(candidate), nextRevision, now, installId, request.body.adapterId, ctx.tenantId, KIND, existing.managed_revision] }
|
|
400
|
+
: { sql: `INSERT INTO app_installs (install_id, app_id, app_version, tenant_id, env, status, scopes, manifest, webhook_endpoint, widget_placements, install_actor, managed_kind, managed_revision, managed_candidate, created_at, updated_at) VALUES (?,?,?,?,?,'suspended','[]','{}',NULL,'[]',?,?,?,?,?,?)`, params: [installId, request.body.adapterId, request.body.adapterVersion, ctx.tenantId, "production", ctx.actor, KIND, nextRevision, JSON.stringify(candidate), now, now] },
|
|
401
|
+
this.auditStatement(installId, ctx, "managed_analytics.candidate_staged", { revision, digest: candidate.digest }, existing !== null),
|
|
402
|
+
];
|
|
403
|
+
return {
|
|
404
|
+
response: mutationSuccess({ install, revision }),
|
|
405
|
+
statements,
|
|
406
|
+
...(existing ? { casStillCurrent: () => this.installStillTombstoned(ctx.tenantId, installId, existing.managed_revision) } : {}),
|
|
407
|
+
};
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
async reviseIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: ReviseIntegrationInstallRequest) {
|
|
411
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<ReviseIntegrationInstallRequest>(request);
|
|
412
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
413
|
+
const prepared = validateReviseIntegrationInstallRequest(snapshot.value);
|
|
414
|
+
if (!prepared.ok) return this.rejectInvalidMutation<{ install: IntegrationInstall; revision: IntegrationRevisionV1 }>(ctx, "reviseIntegrationInstall", snapshot.value, prepared.violations, "installId");
|
|
415
|
+
request = prepared.value;
|
|
416
|
+
return this.idempotent(ctx, "reviseIntegrationInstall", request, request.body.installId, async () => {
|
|
417
|
+
const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
|
|
418
|
+
if (!row) return { response: notFound(), statements: [] };
|
|
419
|
+
if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
|
|
420
|
+
const validated = await validateManagedAnalyticsCandidate({ adapterId: row.app_id, adapterVersion: request.body.adapterVersion, configuration: request.body.configuration }, ctx.tenantId);
|
|
421
|
+
if (!validated.ok) return { response: error("validation_failed", "The integration revision failed validation.", validated.details), statements: [] };
|
|
422
|
+
const now = this.now();
|
|
423
|
+
const revision: IntegrationRevisionV1 = { schemaVersion: 1, revisionId: publicId("revision"), installId: row.install_id, adapterId: row.app_id, adapterVersion: request.body.adapterVersion, sequence: row.managed_revision + 1, status: "draft", configuration: { ...request.body.configuration }, createdAt: now };
|
|
424
|
+
const priorApproved = (await this.versions.listVersions(row.install_id)).find((snapshot) =>
|
|
425
|
+
snapshot.appVersion === request.body.adapterVersion &&
|
|
426
|
+
canonicalJson(snapshot.facets.publicConfig) === canonicalJson(request.body.configuration) &&
|
|
427
|
+
snapshot.facets.digest === validated.effects.digest);
|
|
428
|
+
const candidate: StoredCandidate = { revision, digest: validated.effects.digest, effects: validated.effects, manifest: validated.manifest, facets: validated.facets, approvedAt: now, ...(priorApproved ? { rollbackFromSeq: priorApproved.seq } : {}) };
|
|
429
|
+
const install = { ...publicInstall(row), revision: row.managed_revision + 1, updatedAt: now };
|
|
430
|
+
const update: QueryStatement = { sql: `UPDATE app_installs SET managed_candidate = ?, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [JSON.stringify(candidate), install.revision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
|
|
431
|
+
return { response: mutationSuccess({ install, revision }), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.candidate_staged", { revision, digest: candidate.digest }, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async setIntegrationInstallStatus(ctx: ManagedAnalyticsActorContext, request: SetIntegrationInstallStatusRequest) {
|
|
436
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<SetIntegrationInstallStatusRequest>(request);
|
|
437
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
438
|
+
const prepared = validateSetIntegrationInstallStatusRequest(snapshot.value);
|
|
439
|
+
if (!prepared.ok) return this.rejectInvalidMutation<IntegrationInstall>(ctx, "setIntegrationInstallStatus", snapshot.value, prepared.violations, "installId");
|
|
440
|
+
request = prepared.value;
|
|
441
|
+
return this.idempotent(ctx, "setIntegrationInstallStatus", request, request.body.installId, async () => {
|
|
442
|
+
const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
|
|
443
|
+
if (!row) return { response: notFound(), statements: [] };
|
|
444
|
+
if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
|
|
445
|
+
const now = this.now(); const nextRevision = row.managed_revision + 1;
|
|
446
|
+
if (request.body.status === "disabled") {
|
|
447
|
+
if (row.status === "suspended") {
|
|
448
|
+
return { response: mutationSuccess(publicInstall(row)), statements: [] };
|
|
449
|
+
}
|
|
450
|
+
const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'suspended', managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
|
|
451
|
+
const install = { ...publicInstall(row), status: "disabled" as const, revision: nextRevision, updatedAt: now };
|
|
452
|
+
return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.suspended", {}, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
|
|
453
|
+
}
|
|
454
|
+
const candidate = parseCandidate(row); const existingLive = parseLive(row);
|
|
455
|
+
if (!candidate && !existingLive) return { response: error("conflict", "No approved candidate is available to publish."), statements: [] };
|
|
456
|
+
if (!candidate && existingLive) {
|
|
457
|
+
const current = await this.versions.currentVersion(row.install_id);
|
|
458
|
+
const validated = current ? await validateManagedAnalyticsCandidate({ adapterId: row.app_id, adapterVersion: current.appVersion, configuration: current.facets.publicConfig as Record<string, AdapterConfigValue> }, ctx.tenantId) : null;
|
|
459
|
+
if (!validated?.ok || validated.effects.digest !== current?.facets.digest || validated.manifest.app.digest !== existingLive.app.digest || !row.managed_active_revision_id) return { response: error("conflict", "The approved live digest no longer matches."), statements: [] };
|
|
460
|
+
if (row.status === "active") {
|
|
461
|
+
return { response: mutationSuccess(publicInstall(row)), statements: [] };
|
|
462
|
+
}
|
|
463
|
+
const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'active', managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status = 'suspended'`, params: [nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
|
|
464
|
+
const install = { ...publicInstall(row), status: "enabled" as const, revision: nextRevision, updatedAt: now };
|
|
465
|
+
return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.resumed", { revisionId: row.managed_active_revision_id, digest: current.facets.digest }, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
|
|
466
|
+
}
|
|
467
|
+
const approved = await validateManagedAnalyticsCandidate({ adapterId: candidate!.revision.adapterId, adapterVersion: candidate!.revision.adapterVersion, configuration: candidate!.revision.configuration }, ctx.tenantId);
|
|
468
|
+
if (!approved.ok || approved.effects.digest !== candidate!.digest || canonicalJson(approved.manifest) !== canonicalJson(candidate!.manifest)) return { response: error("conflict", "The approved candidate digest no longer matches."), statements: [] };
|
|
469
|
+
const planned = candidate!.rollbackFromSeq !== undefined
|
|
470
|
+
? await this.versions.planRollbackTo({ installId: row.install_id, toSeq: candidate!.rollbackFromSeq, actor: ctx.actor, note: "managed-analytics rollback publish" })
|
|
471
|
+
: await this.versions.planRetainVersion({ installId: row.install_id, tenantId: ctx.tenantId, facets: candidate!.facets, actor: ctx.actor, note: "managed-analytics publish" });
|
|
472
|
+
const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'active', app_version = ?, manifest = ?, managed_active_revision_id = ?, managed_candidate = NULL, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [candidate!.revision.adapterVersion, JSON.stringify(candidate!.manifest), candidate!.revision.revisionId, nextRevision, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
|
|
473
|
+
const install: IntegrationInstall = { ...publicInstall(row), status: "enabled", activeRevisionId: candidate!.revision.revisionId, revision: nextRevision, updatedAt: now };
|
|
474
|
+
return { response: mutationSuccess(install), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.published", { revisionId: candidate!.revision.revisionId, digest: candidate!.digest }, true), planned.statement], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
async deleteIntegrationInstall(ctx: ManagedAnalyticsActorContext, request: DeleteIntegrationInstallRequest) {
|
|
478
|
+
const snapshot = snapshotManagedAnalyticsDeclarativeInput<DeleteIntegrationInstallRequest>(request);
|
|
479
|
+
if (!snapshot.ok) return error("validation_failed", "The request failed validation.", snapshot.violations);
|
|
480
|
+
const prepared = validateDeleteIntegrationInstallRequest(snapshot.value);
|
|
481
|
+
if (!prepared.ok) return this.rejectInvalidMutation<{ deleted: true }>(ctx, "deleteIntegrationInstall", snapshot.value, prepared.violations, "installId");
|
|
482
|
+
request = prepared.value;
|
|
483
|
+
return this.idempotent(ctx, "deleteIntegrationInstall", request, request.body.installId, async () => {
|
|
484
|
+
const row = await this.ownedInstall(ctx.tenantId, request.body.installId);
|
|
485
|
+
if (!row) return { response: notFound(), statements: [] };
|
|
486
|
+
if (row.managed_revision !== request.expectedRevision) return { response: stale(), statements: [] };
|
|
487
|
+
const now = this.now();
|
|
488
|
+
const update: QueryStatement = { sql: `UPDATE app_installs SET status = 'uninstalled', app_version = 'removed', scopes = '[]', manifest = '{}', webhook_endpoint = NULL, widget_placements = '[]', managed_active_revision_id = NULL, managed_candidate = NULL, managed_revision = ?, updated_at = ? WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND managed_revision = ? AND status != 'uninstalled'`, params: [row.managed_revision + 1, now, row.install_id, ctx.tenantId, KIND, request.expectedRevision] };
|
|
489
|
+
return { response: mutationSuccess({ deleted: true as const }), statements: [update, this.auditStatement(row.install_id, ctx, "managed_analytics.removed", {}, true)], casStillCurrent: () => this.installStillCurrent(ctx.tenantId, row.install_id, request.expectedRevision) };
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private async ownedInstall(tenantId: string, installId: string): Promise<InstallRow | null> {
|
|
494
|
+
return this.db.first<InstallRow>(`SELECT * FROM app_installs WHERE install_id = ? AND tenant_id = ? AND managed_kind = ? AND status != 'uninstalled'`, installId, tenantId, KIND);
|
|
495
|
+
}
|
|
496
|
+
private async adapterSlot(tenantId: string, adapterId: string): Promise<InstallRow | null> {
|
|
497
|
+
return this.db.first<InstallRow>(`SELECT * FROM app_installs WHERE app_id = ? AND tenant_id = ? AND env = 'production'`, adapterId, tenantId);
|
|
498
|
+
}
|
|
499
|
+
private async installStillTombstoned(tenantId: string, installId: string, revision: number): Promise<boolean> {
|
|
500
|
+
const row = await this.db.first<Pick<InstallRow, "status" | "managed_kind" | "managed_revision">>(
|
|
501
|
+
`SELECT status, managed_kind, managed_revision FROM app_installs WHERE install_id = ? AND tenant_id = ?`,
|
|
502
|
+
installId,
|
|
503
|
+
tenantId,
|
|
504
|
+
);
|
|
505
|
+
return row?.status === "uninstalled" && row.managed_kind === KIND && row.managed_revision === revision;
|
|
506
|
+
}
|
|
507
|
+
private async installStillCurrent(tenantId: string, installId: string, revision: number): Promise<boolean> {
|
|
508
|
+
return (await this.ownedInstall(tenantId, installId))?.managed_revision === revision;
|
|
509
|
+
}
|
|
510
|
+
private async revisions(row: InstallRow): Promise<IntegrationRevisionV1[]> {
|
|
511
|
+
const audits = await this.db.all<AuditRow>(`SELECT id, event, detail, at FROM app_audit_log WHERE install_id = ? AND tenant_id = ? AND event = 'managed_analytics.candidate_staged' ORDER BY at, id`, row.install_id, row.tenant_id);
|
|
512
|
+
const liveId = row.managed_active_revision_id; const candidateId = parseCandidate(row)?.revision.revisionId ?? null;
|
|
513
|
+
return audits.map((audit) => {
|
|
514
|
+
const detail = JSON.parse(audit.detail) as unknown;
|
|
515
|
+
const safe = validateManagedAnalyticsSafePayload(detail);
|
|
516
|
+
const revision = detail && typeof detail === "object" && !Array.isArray(detail)
|
|
517
|
+
? validateIntegrationRevisionV1((detail as Record<string, unknown>)["revision"])
|
|
518
|
+
: { ok: false as const, violations: [] };
|
|
519
|
+
if (!safe.ok || !revision.ok || revision.value.installId !== row.install_id) {
|
|
520
|
+
throw new Error("Managed analytics revision failed persisted-data validation.");
|
|
521
|
+
}
|
|
522
|
+
return revision.value;
|
|
523
|
+
})
|
|
524
|
+
.map((revision) => ({ ...revision, status: revision.revisionId === liveId ? "active" : revision.revisionId === candidateId ? "draft" : "superseded" }));
|
|
525
|
+
}
|
|
526
|
+
private async loadDraft(tenantId: string, draftId: string): Promise<{ draft: MigrationDraftV1; open: boolean } | null> {
|
|
527
|
+
const row = await this.db.first<DraftRow>(`SELECT * FROM integration_migration_drafts WHERE draft_id = ? AND tenant_id = ? AND state = 'open'`, draftId, tenantId);
|
|
528
|
+
if (!row) return null;
|
|
529
|
+
const draft = validateMigrationDraftV1(JSON.parse(row.record) as unknown);
|
|
530
|
+
if (!draft.ok || draft.value.draftId !== row.draft_id || draft.value.revision !== row.revision) {
|
|
531
|
+
throw new Error("Managed analytics draft failed persisted-data validation.");
|
|
532
|
+
}
|
|
533
|
+
return { draft: draft.value, open: true };
|
|
534
|
+
}
|
|
535
|
+
private tenantScope(ctx: ManagedAnalyticsActorContext): string { return `managed-analytics-tenant:${ctx.tenantId}`; }
|
|
536
|
+
private async rejectInvalidMutation<T>(
|
|
537
|
+
ctx: ManagedAnalyticsActorContext,
|
|
538
|
+
operation: ManagedAnalyticsOperation,
|
|
539
|
+
snapshot: unknown,
|
|
540
|
+
violations: NonNullable<ApiErrorEnvelope["error"]["details"]>,
|
|
541
|
+
resourceKey?: "draftId" | "installId",
|
|
542
|
+
): Promise<MutationResult<T>> {
|
|
543
|
+
const request = record(snapshot);
|
|
544
|
+
const rejected = error("validation_failed", "The request failed validation.", violations);
|
|
545
|
+
if (!request || !validIdempotencyKey(request["idempotencyKey"])) {
|
|
546
|
+
return error("validation_failed", "The request failed validation.");
|
|
547
|
+
}
|
|
548
|
+
const body = record(request["body"]);
|
|
549
|
+
const resourceId = resourceKey && body ? body[resourceKey] : undefined;
|
|
550
|
+
const scope = resourceKey && validSafeOpaqueId(resourceId) ? resourceId : this.tenantScope(ctx);
|
|
551
|
+
return this.idempotent<T>(ctx, operation, request as { idempotencyKey: string }, scope, async () => ({ response: rejected, statements: [] }));
|
|
552
|
+
}
|
|
553
|
+
private auditStatement(installId: string, ctx: ManagedAnalyticsActorContext, event: string, detail: Record<string, unknown>, assertPreviousChange = false, id = publicId("audit")): QueryStatement {
|
|
554
|
+
return { sql: `INSERT INTO app_audit_log (id, install_id, tenant_id, event, actor, trace_id, detail, at) VALUES (${assertPreviousChange ? "CASE WHEN changes() = 1 THEN ? ELSE NULL END" : "?"},?,?,?,?,NULL,?,?)`, params: [id, installId, ctx.tenantId, event, ctx.actor, JSON.stringify(detail), this.now()] };
|
|
555
|
+
}
|
|
556
|
+
private async idempotent<T>(ctx: ManagedAnalyticsActorContext, operation: ManagedAnalyticsOperation, request: { idempotencyKey: string }, scope: string, planMutation: () => Promise<MutationPlan<T>>): Promise<MutationResult<T>> {
|
|
557
|
+
const key = `${operation}:${request.idempotencyKey}`; const digest = await sha256Hex(canonicalJson(request));
|
|
558
|
+
// `idempotency_keys.install_id` is a generic resource-scope column, not a
|
|
559
|
+
// foreign key. Hash the authenticated tenant together with the public
|
|
560
|
+
// resource scope so replay is impossible before (or without) an ownership
|
|
561
|
+
// read, even when another tenant knows both an id and a retry key.
|
|
562
|
+
const ledgerScope = `managed-analytics:${await sha256Hex(canonicalJson([ctx.tenantId, scope]))}`;
|
|
563
|
+
const prior = await this.db.first<IdempotencyRow>(`SELECT body_hash, response_body FROM idempotency_keys WHERE install_id = ? AND idempotency_key = ?`, ledgerScope, key);
|
|
564
|
+
if (prior) return this.replay<T>(prior, digest, operation);
|
|
565
|
+
const plan = await planMutation();
|
|
566
|
+
// The ledger is itself a persisted outbound-response authority. Refuse to
|
|
567
|
+
// store anything that the frozen v1 response contract would reject, even
|
|
568
|
+
// if a future mutation planner accidentally constructs an unsafe value.
|
|
569
|
+
const outbound = sanitizeManagedAnalyticsOutbound(operation, plan.response);
|
|
570
|
+
if (!outbound) return internalError();
|
|
571
|
+
const persistedResponse = outbound as MutationResult<T>;
|
|
572
|
+
if (!this.db.batch) throw new Error("Atomic D1 batch support is required.");
|
|
573
|
+
const ledger: QueryStatement = { sql: `INSERT INTO idempotency_keys (install_id, idempotency_key, body_hash, response_body, created_at) VALUES (?,?,?,?,?)`, params: [ledgerScope, key, digest, JSON.stringify(persistedResponse), this.now()] };
|
|
574
|
+
try {
|
|
575
|
+
await this.db.batch([ledger, ...plan.statements]);
|
|
576
|
+
return persistedResponse;
|
|
577
|
+
} catch (cause) {
|
|
578
|
+
const raced = await this.db.first<IdempotencyRow>(`SELECT body_hash, response_body FROM idempotency_keys WHERE install_id = ? AND idempotency_key = ?`, ledgerScope, key);
|
|
579
|
+
if (raced) return this.replay<T>(raced, digest, operation);
|
|
580
|
+
if (plan.casStillCurrent && !(await plan.casStillCurrent())) return stale();
|
|
581
|
+
throw cause;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
private replay<T>(row: IdempotencyRow, digest: string, operation: ManagedAnalyticsOperation): MutationResult<T> {
|
|
585
|
+
if (row.body_hash !== digest) return error("idempotency_conflict", "The idempotency key was already used for a different request.");
|
|
586
|
+
try {
|
|
587
|
+
const parsed = JSON.parse(row.response_body) as unknown;
|
|
588
|
+
const response = record(parsed);
|
|
589
|
+
if (!response) return internalError();
|
|
590
|
+
if (Object.prototype.hasOwnProperty.call(response, "error")) {
|
|
591
|
+
const validated = validateApiErrorEnvelope(response);
|
|
592
|
+
return validated.ok ? validated.value : internalError();
|
|
593
|
+
}
|
|
594
|
+
const outbound = sanitizeManagedAnalyticsOutbound(operation, response);
|
|
595
|
+
if (!outbound) return internalError();
|
|
596
|
+
return { ...(outbound as MutationResult<T>), idempotencyReplayed: true };
|
|
597
|
+
} catch { return internalError(); }
|
|
598
|
+
}
|
|
599
|
+
}
|