@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,547 @@
|
|
|
1
|
+
/** Browser controller for the admin managed-analytics surface. */
|
|
2
|
+
import type {
|
|
3
|
+
AdapterConfigValue,
|
|
4
|
+
AdapterDefinition,
|
|
5
|
+
ApiErrorEnvelope,
|
|
6
|
+
CreateIntegrationInstallRequest,
|
|
7
|
+
CreateIntegrationInstallResponse,
|
|
8
|
+
DeleteIntegrationInstallRequest,
|
|
9
|
+
DeleteIntegrationInstallResponse,
|
|
10
|
+
GetAdapterDefinitionRequest,
|
|
11
|
+
GetAdapterDefinitionResponse,
|
|
12
|
+
IntegrationInstall,
|
|
13
|
+
IntegrationRevisionV1,
|
|
14
|
+
ListAdapterDefinitionsRequest,
|
|
15
|
+
ListAdapterDefinitionsResponse,
|
|
16
|
+
ListIntegrationInstallsRequest,
|
|
17
|
+
ListIntegrationInstallsResponse,
|
|
18
|
+
ListIntegrationRevisionsRequest,
|
|
19
|
+
ListIntegrationRevisionsResponse,
|
|
20
|
+
ManagedAnalyticsApiV1Client,
|
|
21
|
+
ReviseIntegrationInstallRequest,
|
|
22
|
+
ReviseIntegrationInstallResponse,
|
|
23
|
+
SetIntegrationInstallStatusRequest,
|
|
24
|
+
SetIntegrationInstallStatusResponse,
|
|
25
|
+
} from "@tot/public-runtime";
|
|
26
|
+
import { APPROVED_MARKETING_ACTION_NAMES } from "@tot/public-runtime";
|
|
27
|
+
import type { IntegrationAdminReadModel, IntegrationReadModelItem } from "./integrationReadModel";
|
|
28
|
+
import { describeAuthorityTier } from "./integrationsView";
|
|
29
|
+
|
|
30
|
+
type AdminManagedClient = Pick<ManagedAnalyticsApiV1Client,
|
|
31
|
+
"listAdapterDefinitions" | "getAdapterDefinition" | "listIntegrationInstalls" |
|
|
32
|
+
"listIntegrationRevisions" | "createIntegrationInstall" |
|
|
33
|
+
"reviseIntegrationInstall" | "setIntegrationInstallStatus" |
|
|
34
|
+
"deleteIntegrationInstall">;
|
|
35
|
+
|
|
36
|
+
function el(tag: string, className?: string, text?: string): HTMLElement {
|
|
37
|
+
const node = document.createElement(tag);
|
|
38
|
+
if (className) node.className = className;
|
|
39
|
+
if (text !== undefined) node.textContent = text;
|
|
40
|
+
return node;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function withTenant(base: string, tenant?: string): string {
|
|
44
|
+
if (!tenant) return base;
|
|
45
|
+
const separator = base.includes("?") ? "&" : "?";
|
|
46
|
+
return `${base}${separator}tenant=${encodeURIComponent(tenant)}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function json<T>(response: Response): Promise<T> {
|
|
50
|
+
try { return await response.json() as T; }
|
|
51
|
+
catch { return {} as T; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Typed HTTP implementation of the frozen client; method names are the route suffixes. */
|
|
55
|
+
export class ManagedAnalyticsHttpClient implements AdminManagedClient {
|
|
56
|
+
constructor(private readonly base: string, private readonly tenant?: string) {}
|
|
57
|
+
private async post<T>(method: string, request: unknown): Promise<T> {
|
|
58
|
+
const response = await fetch(withTenant(`${this.base}/${method}`, this.tenant), {
|
|
59
|
+
method: "POST",
|
|
60
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
61
|
+
body: JSON.stringify(request),
|
|
62
|
+
});
|
|
63
|
+
const body = await json<T>(response);
|
|
64
|
+
if (!body || typeof body !== "object" || (!("data" in body) && !("error" in body))) {
|
|
65
|
+
throw new TypeError("Managed analytics response was not a contract envelope.");
|
|
66
|
+
}
|
|
67
|
+
return body;
|
|
68
|
+
}
|
|
69
|
+
listAdapterDefinitions(request: ListAdapterDefinitionsRequest) { return this.post<ListAdapterDefinitionsResponse>("listAdapterDefinitions", request); }
|
|
70
|
+
getAdapterDefinition(request: GetAdapterDefinitionRequest) { return this.post<GetAdapterDefinitionResponse>("getAdapterDefinition", request); }
|
|
71
|
+
listIntegrationInstalls(request: ListIntegrationInstallsRequest) { return this.post<ListIntegrationInstallsResponse>("listIntegrationInstalls", request); }
|
|
72
|
+
listIntegrationRevisions(request: ListIntegrationRevisionsRequest) { return this.post<ListIntegrationRevisionsResponse>("listIntegrationRevisions", request); }
|
|
73
|
+
createIntegrationInstall(request: CreateIntegrationInstallRequest) { return this.post<CreateIntegrationInstallResponse>("createIntegrationInstall", request); }
|
|
74
|
+
reviseIntegrationInstall(request: ReviseIntegrationInstallRequest) { return this.post<ReviseIntegrationInstallResponse>("reviseIntegrationInstall", request); }
|
|
75
|
+
setIntegrationInstallStatus(request: SetIntegrationInstallStatusRequest) { return this.post<SetIntegrationInstallStatusResponse>("setIntegrationInstallStatus", request); }
|
|
76
|
+
deleteIntegrationInstall(request: DeleteIntegrationInstallRequest) { return this.post<DeleteIntegrationInstallResponse>("deleteIntegrationInstall", request); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function isError(value: unknown): value is ApiErrorEnvelope {
|
|
80
|
+
return !!value && typeof value === "object" && "error" in value;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function key(prefix: string): string {
|
|
84
|
+
const random = globalThis.crypto?.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
85
|
+
return `${prefix}-${random}`.replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, 128);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface PendingMutation {
|
|
89
|
+
operation: string;
|
|
90
|
+
request: object;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class PendingManagedMutationError extends Error {
|
|
94
|
+
constructor(readonly operation: string) {
|
|
95
|
+
super(`A ${operation} retry is still pending.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Keeps one canonical mutation per resource until an error/success envelope is
|
|
101
|
+
* received. A thrown transport leaves the exact key and body available for the
|
|
102
|
+
* operator's retry; a different mutation cannot overtake it.
|
|
103
|
+
*/
|
|
104
|
+
export class ManagedMutationLedger {
|
|
105
|
+
private readonly pending = new Map<string, PendingMutation>();
|
|
106
|
+
|
|
107
|
+
prepare<T extends object>(resource: string, operation: string, request: T): T {
|
|
108
|
+
const existing = this.pending.get(resource);
|
|
109
|
+
if (existing) {
|
|
110
|
+
if (existing.operation !== operation) throw new PendingManagedMutationError(existing.operation);
|
|
111
|
+
return existing.request as T;
|
|
112
|
+
}
|
|
113
|
+
const canonical = JSON.parse(JSON.stringify(request)) as T;
|
|
114
|
+
this.pending.set(resource, { operation, request: canonical });
|
|
115
|
+
return canonical;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
complete(resource: string, operation: string): void {
|
|
119
|
+
if (this.pending.get(resource)?.operation === operation) this.pending.delete(resource);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
operation(resource: string): string | undefined { return this.pending.get(resource)?.operation; }
|
|
123
|
+
|
|
124
|
+
/** Clear only entries whose effect is proven by a fresh authoritative read. */
|
|
125
|
+
reconcile(provesCommitted: (resource: string, pending: Readonly<PendingMutation>) => boolean): number {
|
|
126
|
+
let cleared = 0;
|
|
127
|
+
for (const [resource, pending] of this.pending) {
|
|
128
|
+
if (!provesCommitted(resource, pending)) continue;
|
|
129
|
+
this.pending.delete(resource);
|
|
130
|
+
cleared += 1;
|
|
131
|
+
}
|
|
132
|
+
return cleared;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function requestRecord(value: unknown): Record<string, unknown> | null {
|
|
137
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
138
|
+
? value as Record<string, unknown>
|
|
139
|
+
: null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sameConfiguration(
|
|
143
|
+
left: Record<string, AdapterConfigValue>,
|
|
144
|
+
right: Record<string, AdapterConfigValue>,
|
|
145
|
+
): boolean {
|
|
146
|
+
const leftKeys = Object.keys(left).sort();
|
|
147
|
+
const rightKeys = Object.keys(right).sort();
|
|
148
|
+
return leftKeys.length === rightKeys.length &&
|
|
149
|
+
leftKeys.every((key, index) => key === rightKeys[index] && left[key] === right[key]);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Reconcile an ambiguous mutation only when a fresh list/read proves its exact
|
|
154
|
+
* effect. Mere refresh success is not enough: an uncommitted request keeps its
|
|
155
|
+
* original idempotency key and canonical body for the next retry.
|
|
156
|
+
*/
|
|
157
|
+
export function reconcileManagedMutationLedger(
|
|
158
|
+
ledger: ManagedMutationLedger,
|
|
159
|
+
hydrated: ReadonlyMap<string, ManagedInstallView>,
|
|
160
|
+
installsListComplete: boolean,
|
|
161
|
+
): number {
|
|
162
|
+
return ledger.reconcile((_resource, pending) => {
|
|
163
|
+
const request = requestRecord(pending.request);
|
|
164
|
+
const body = requestRecord(request?.body);
|
|
165
|
+
const expectedRevision = request?.expectedRevision;
|
|
166
|
+
if (!body || typeof expectedRevision !== "number") return false;
|
|
167
|
+
|
|
168
|
+
if (pending.operation === "create") {
|
|
169
|
+
const adapterId = body.adapterId;
|
|
170
|
+
const adapterVersion = body.adapterVersion;
|
|
171
|
+
const configuration = requestRecord(body.configuration) as Record<string, AdapterConfigValue> | null;
|
|
172
|
+
if (typeof adapterId !== "string" || typeof adapterVersion !== "string" || !configuration) return false;
|
|
173
|
+
return [...hydrated.values()].some((view) =>
|
|
174
|
+
view.install.adapterId === adapterId &&
|
|
175
|
+
view.revisions !== null &&
|
|
176
|
+
view.revisions.some((revision) =>
|
|
177
|
+
revision.sequence === 1 &&
|
|
178
|
+
revision.adapterVersion === adapterVersion &&
|
|
179
|
+
sameConfiguration(revision.configuration, configuration),
|
|
180
|
+
),
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const installId = body.installId;
|
|
185
|
+
if (typeof installId !== "string") return false;
|
|
186
|
+
const view = hydrated.get(installId);
|
|
187
|
+
|
|
188
|
+
if (pending.operation === "revise" || pending.operation === "rollback-stage") {
|
|
189
|
+
const adapterVersion = body.adapterVersion;
|
|
190
|
+
const configuration = requestRecord(body.configuration) as Record<string, AdapterConfigValue> | null;
|
|
191
|
+
if (!view || view.revisions === null || typeof adapterVersion !== "string" || !configuration) return false;
|
|
192
|
+
return view.install.revision > expectedRevision && view.revisions.some((revision) =>
|
|
193
|
+
revision.sequence === expectedRevision + 1 &&
|
|
194
|
+
revision.adapterVersion === adapterVersion &&
|
|
195
|
+
sameConfiguration(revision.configuration, configuration),
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (pending.operation === "publish" || pending.operation === "suspend" || pending.operation === "rollback-publish") {
|
|
200
|
+
const intendedStatus = body.status;
|
|
201
|
+
if (!view || view.install.revision !== expectedRevision + 1 || view.install.status !== intendedStatus) return false;
|
|
202
|
+
if (intendedStatus === "disabled") return true;
|
|
203
|
+
if (intendedStatus !== "enabled" || view.revisions === null || !view.install.activeRevisionId) return false;
|
|
204
|
+
return !view.revisions.some((revision) => revision.status === "draft") &&
|
|
205
|
+
view.revisions.some((revision) =>
|
|
206
|
+
revision.revisionId === view.install.activeRevisionId && revision.status === "active",
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (pending.operation === "remove") return installsListComplete && !view;
|
|
211
|
+
return false;
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function destination(revision: IntegrationRevisionV1 | undefined): string {
|
|
216
|
+
const value = revision?.configuration.destinationId;
|
|
217
|
+
return typeof value === "string" ? value : "";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function statusMessage(error: ApiErrorEnvelope): string {
|
|
221
|
+
return `${error.error.message} [${error.error.code}]`;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function manualEventVocabulary(card: AdapterDefinition): string[] {
|
|
225
|
+
return card.eventTypes.flatMap((eventType) => eventType === "storefront.action.activated.v1"
|
|
226
|
+
? [...APPROVED_MARKETING_ACTION_NAMES]
|
|
227
|
+
: [eventType]);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export interface ManagedInstallView {
|
|
231
|
+
install: IntegrationInstall;
|
|
232
|
+
definition: AdapterDefinition | null;
|
|
233
|
+
revisions: IntegrationRevisionV1[] | null;
|
|
234
|
+
degraded?: "catalog_definition_unavailable" | "revision_history_unavailable";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** MigrationDraft v1 intentionally resolves adapterVersion from the catalog. */
|
|
238
|
+
export async function resolveMigrationDraftInstallInput(
|
|
239
|
+
client: Pick<ManagedAnalyticsApiV1Client, "getAdapterDefinition">,
|
|
240
|
+
draft: { proposedAdapterId: string; proposedConfiguration: Record<string, AdapterConfigValue> },
|
|
241
|
+
): Promise<CreateIntegrationInstallRequest["body"] | ApiErrorEnvelope> {
|
|
242
|
+
const found = await client.getAdapterDefinition({ apiVersion: "v1", query: { adapterId: draft.proposedAdapterId } });
|
|
243
|
+
if (isError(found)) return found;
|
|
244
|
+
return {
|
|
245
|
+
adapterId: found.data.adapterId,
|
|
246
|
+
adapterVersion: found.data.adapterVersion,
|
|
247
|
+
configuration: { ...draft.proposedConfiguration },
|
|
248
|
+
status: "disabled",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function renderCatalog(container: HTMLElement, cards: AdapterDefinition[], canManage = true, busyAdapters: ReadonlySet<string> = new Set(), installedAdapters: ReadonlySet<string> = new Set()): void {
|
|
253
|
+
container.textContent = "";
|
|
254
|
+
for (const card of cards) {
|
|
255
|
+
const wrap = el("article", "integrations-adapter-card");
|
|
256
|
+
wrap.dataset.adapterId = card.adapterId;
|
|
257
|
+
const busy = busyAdapters.has(card.adapterId);
|
|
258
|
+
const installed = installedAdapters.has(card.adapterId);
|
|
259
|
+
wrap.setAttribute("aria-busy", busy ? "true" : "false");
|
|
260
|
+
const head = el("div", "integrations-adapter-head");
|
|
261
|
+
head.append(el("strong", undefined, card.displayName), el("span", "chip", `v${card.adapterVersion}`));
|
|
262
|
+
wrap.append(head, el("p", "subtle", card.description));
|
|
263
|
+
const meta = el("dl", "integrations-adapter-meta");
|
|
264
|
+
for (const [label, value] of [
|
|
265
|
+
["Adapter", card.adapterId],
|
|
266
|
+
["Collection origins", card.collectionOrigins.join(" ") || "none"],
|
|
267
|
+
["Consent", card.consentPurposes.join(", ") || "none"],
|
|
268
|
+
["Manual events", manualEventVocabulary(card).join(", ") || "none"],
|
|
269
|
+
]) meta.append(el("dt", undefined, label), el("dd", "publish-mono", value));
|
|
270
|
+
wrap.appendChild(meta);
|
|
271
|
+
const form = el("div", "integrations-adapter-config");
|
|
272
|
+
const input = document.createElement("input");
|
|
273
|
+
input.type = "text";
|
|
274
|
+
input.autocomplete = "off";
|
|
275
|
+
input.placeholder = card.configuration[0]?.label ?? "Destination ID";
|
|
276
|
+
input.setAttribute("aria-label", `${card.displayName} destination ID`);
|
|
277
|
+
input.dataset.adapterIdInput = card.adapterId;
|
|
278
|
+
input.disabled = !canManage || card.availability !== "available" || busy || installed;
|
|
279
|
+
const button = el("button", "btn", installed ? "Already configured" : "Configure disabled") as HTMLButtonElement;
|
|
280
|
+
button.type = "button";
|
|
281
|
+
button.dataset.configureAdapter = card.adapterId;
|
|
282
|
+
button.disabled = input.disabled;
|
|
283
|
+
form.append(input, button);
|
|
284
|
+
const result = el("p", "note integrations-adapter-result");
|
|
285
|
+
result.dataset.adapterResult = card.adapterId;
|
|
286
|
+
result.setAttribute("role", "status");
|
|
287
|
+
result.setAttribute("aria-live", "polite");
|
|
288
|
+
result.hidden = !busy;
|
|
289
|
+
if (busy) result.textContent = "Creating managed install…";
|
|
290
|
+
wrap.append(form, result);
|
|
291
|
+
container.appendChild(wrap);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function renderManagedInventory(tbody: HTMLElement, views: ManagedInstallView[], canManage = true, busyInstalls: ReadonlySet<string> = new Set()): void {
|
|
296
|
+
tbody.textContent = "";
|
|
297
|
+
if (!views.length) {
|
|
298
|
+
const row = document.createElement("tr");
|
|
299
|
+
const cell = el("td", "subtle", "No managed analytics installs yet.");
|
|
300
|
+
cell.setAttribute("colspan", "5"); row.appendChild(cell); tbody.appendChild(row); return;
|
|
301
|
+
}
|
|
302
|
+
for (const view of views) {
|
|
303
|
+
const { install, definition, revisions } = view;
|
|
304
|
+
const busy = busyInstalls.has(install.installId);
|
|
305
|
+
const row = document.createElement("tr");
|
|
306
|
+
row.dataset.managedInstallId = install.installId;
|
|
307
|
+
row.setAttribute("aria-busy", busy ? "true" : "false");
|
|
308
|
+
const identity = el("td");
|
|
309
|
+
identity.append(el("strong", undefined, definition?.displayName ?? install.adapterId), el("span", "subtle", definition ? `${install.adapterId} · v${definition.adapterVersion}` : "Catalog definition unavailable"));
|
|
310
|
+
row.appendChild(identity);
|
|
311
|
+
const state = el("td");
|
|
312
|
+
state.append(el("span", `chip ${install.status === "enabled" ? "green" : ""}`.trim(), install.status), el("span", "subtle", `CAS ${install.revision}`));
|
|
313
|
+
row.appendChild(state);
|
|
314
|
+
const history = el("td");
|
|
315
|
+
const ordered = [...(revisions ?? [])].sort((a, b) => b.sequence - a.sequence);
|
|
316
|
+
if (view.degraded) history.appendChild(el("span", "publish-diff-blocked", view.degraded === "catalog_definition_unavailable" ? "Catalog definition unavailable; changes are disabled." : "Revision history unavailable; changes are disabled."));
|
|
317
|
+
for (const revision of ordered) history.appendChild(el("span", "subtle", `#${revision.sequence} ${revision.status} · ${revision.adapterVersion}`));
|
|
318
|
+
if (ordered[0]) history.appendChild(el("span", "subtle", `Activity ${ordered[0].createdAt}`));
|
|
319
|
+
row.appendChild(history);
|
|
320
|
+
const configuration = el("td");
|
|
321
|
+
const edit = el("div", "integrations-inline");
|
|
322
|
+
const input = document.createElement("input");
|
|
323
|
+
input.type = "text"; input.autocomplete = "off"; input.value = destination(ordered.find((r) => r.status === "draft") ?? ordered.find((r) => r.status === "active"));
|
|
324
|
+
input.setAttribute("aria-label", `${definition?.displayName ?? install.adapterId} revised destination ID`);
|
|
325
|
+
input.dataset.reviseDestination = install.installId; input.disabled = !canManage || busy || !!view.degraded;
|
|
326
|
+
const revise = el("button", "btn", "Stage revision") as HTMLButtonElement;
|
|
327
|
+
revise.type = "button"; revise.dataset.managedAction = "revise"; revise.dataset.installId = install.installId; revise.disabled = !canManage || busy || !!view.degraded;
|
|
328
|
+
edit.append(input, revise); configuration.appendChild(edit); row.appendChild(configuration);
|
|
329
|
+
const actions = el("td", "action"); const bar = el("div", "integrations-actions");
|
|
330
|
+
const setButton = (label: string, action: string, applicable = true, primary = false) => { const button = el("button", `btn${primary ? " primary" : ""}`, label) as HTMLButtonElement; button.type = "button"; button.dataset.managedAction = action; button.dataset.installId = install.installId; button.disabled = !canManage || busy || !!view.degraded || !applicable; bar.appendChild(button); };
|
|
331
|
+
const hasDraft = ordered.some((revision) => revision.status === "draft");
|
|
332
|
+
const canResume = install.status === "disabled" && !hasDraft && install.activeRevisionId !== null;
|
|
333
|
+
setButton(hasDraft ? "Publish draft revision" : canResume ? "Resume active revision" : "Publish unavailable", "publish", hasDraft || canResume, true);
|
|
334
|
+
setButton("Suspend active revision", "suspend", install.status === "enabled");
|
|
335
|
+
const superseded = ordered.filter((r) => r.status === "superseded");
|
|
336
|
+
const rollback = document.createElement("select"); rollback.dataset.rollbackRevision = install.installId; rollback.setAttribute("aria-label", `${definition?.displayName ?? install.adapterId} rollback revision`); rollback.disabled = !canManage || busy || !!view.degraded || !superseded.length;
|
|
337
|
+
const placeholder = document.createElement("option"); placeholder.value = ""; placeholder.textContent = "Select prior revision"; rollback.appendChild(placeholder);
|
|
338
|
+
for (const revision of superseded) { const option = document.createElement("option"); option.value = revision.revisionId; option.textContent = `#${revision.sequence} · ${revision.adapterVersion}`; rollback.appendChild(option); }
|
|
339
|
+
bar.appendChild(rollback); setButton("Stage and publish selected rollback", "rollback", superseded.length > 0);
|
|
340
|
+
const confirm = document.createElement("input"); confirm.type = "text"; confirm.placeholder = `Type ${install.adapterId}`; confirm.setAttribute("aria-label", `Type ${install.adapterId} to confirm removal`); confirm.dataset.removeConfirmation = install.installId; confirm.disabled = !canManage || busy || !!view.degraded; bar.appendChild(confirm);
|
|
341
|
+
setButton("Remove managed install", "remove");
|
|
342
|
+
const busyStatus = el("span", "subtle", busy ? "Change in progress…" : ""); busyStatus.setAttribute("role", "status"); busyStatus.setAttribute("aria-live", "polite"); bar.appendChild(busyStatus);
|
|
343
|
+
actions.appendChild(bar); row.appendChild(actions); tbody.appendChild(row);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Existing tenant-local inventory is deliberately rendered without actions. */
|
|
348
|
+
export function consentSummary(item: IntegrationReadModelItem) {
|
|
349
|
+
const required = item.effectiveAuthority?.consentPurposeIds ?? [];
|
|
350
|
+
const eligible = item.effective?.consentEligible ?? [];
|
|
351
|
+
const set = new Set(eligible);
|
|
352
|
+
return { required: [...required], eligible: [...eligible], unmet: required.filter((p) => !set.has(p as never)) };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function buildInventoryRow(item: IntegrationReadModelItem): HTMLTableRowElement {
|
|
356
|
+
const row = document.createElement("tr"); row.dataset.localInstallRow = "";
|
|
357
|
+
const name = el("td"); name.append(el("strong", undefined, item.purpose), el("span", "subtle", `${item.appId} · tenant-local`)); row.appendChild(name);
|
|
358
|
+
row.append(el("td", undefined, item.status), el("td", undefined, describeAuthorityTier(item.authorityTier)));
|
|
359
|
+
row.appendChild(el("td", undefined, item.effective ? `${item.effective.version} (${item.effective.stage})` : "Nothing serving"));
|
|
360
|
+
const consent = consentSummary(item); row.appendChild(el("td", undefined, consent.required.length ? `Needs: ${consent.required.join(", ")}` : "No consent dependency"));
|
|
361
|
+
return row;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
function renderLocal(tbody: HTMLElement, model: IntegrationAdminReadModel): void {
|
|
365
|
+
tbody.textContent = "";
|
|
366
|
+
if (!model.items.length) { const row = document.createElement("tr"); const cell = el("td", "subtle", "No other tenant-local integrations."); cell.setAttribute("colspan", "5"); row.appendChild(cell); tbody.appendChild(row); return; }
|
|
367
|
+
for (const item of model.items) tbody.appendChild(buildInventoryRow(item));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function initIntegrationsTab(): void {
|
|
371
|
+
const panel = document.querySelector<HTMLElement>("[data-integrations-panel]");
|
|
372
|
+
if (!panel) return;
|
|
373
|
+
const base = panel.dataset.managedEndpoint;
|
|
374
|
+
if (!base) return;
|
|
375
|
+
const tenant = panel.dataset.targetTenant || undefined;
|
|
376
|
+
const canManage = panel.dataset.canManage === "true";
|
|
377
|
+
const client = new ManagedAnalyticsHttpClient(base, tenant);
|
|
378
|
+
const status = panel.querySelector<HTMLElement>("[data-integrations-status]");
|
|
379
|
+
const catalogNode = panel.querySelector<HTMLElement>("[data-managed-integrations-catalog]");
|
|
380
|
+
const managedBody = panel.querySelector<HTMLElement>("[data-managed-integrations-body]");
|
|
381
|
+
const localBody = panel.querySelector<HTMLElement>("[data-local-integrations-body]");
|
|
382
|
+
let catalog = new Map<string, AdapterDefinition>();
|
|
383
|
+
let views = new Map<string, ManagedInstallView>();
|
|
384
|
+
const ledger = new ManagedMutationLedger();
|
|
385
|
+
const busyInstalls = new Set<string>();
|
|
386
|
+
const busyAdapters = new Set<string>();
|
|
387
|
+
const announce = (message: string, tone: "" | "error" = "", focus = false) => { if (!status) return; status.textContent = message; status.dataset.tone = tone; if (focus) status.focus(); };
|
|
388
|
+
const renderManaged = () => { if (managedBody) renderManagedInventory(managedBody, [...views.values()], canManage, busyInstalls); };
|
|
389
|
+
const renderAdapters = () => { if (catalogNode) renderCatalog(catalogNode, [...catalog.values()], canManage, busyAdapters, new Set([...views.values()].map((view) => view.install.adapterId))); };
|
|
390
|
+
const setInstallBusy = (installId: string, busy: boolean) => {
|
|
391
|
+
if (busy) busyInstalls.add(installId); else busyInstalls.delete(installId);
|
|
392
|
+
const row = panel!.querySelector<HTMLElement>(`[data-managed-install-id="${CSS.escape(installId)}"]`);
|
|
393
|
+
if (row && busy) {
|
|
394
|
+
row.setAttribute("aria-busy", "true");
|
|
395
|
+
row.querySelectorAll<HTMLButtonElement | HTMLInputElement | HTMLSelectElement>("button,input,select").forEach((control) => { control.disabled = true; });
|
|
396
|
+
const live = row.querySelector<HTMLElement>('[role="status"]'); if (live) live.textContent = "Change in progress…";
|
|
397
|
+
} else if (!busy) renderManaged();
|
|
398
|
+
};
|
|
399
|
+
const setAdapterBusy = (adapterId: string, busy: boolean) => {
|
|
400
|
+
if (busy) busyAdapters.add(adapterId); else busyAdapters.delete(adapterId);
|
|
401
|
+
const card = panel!.querySelector<HTMLElement>(`[data-adapter-id="${CSS.escape(adapterId)}"]`);
|
|
402
|
+
card?.setAttribute("aria-busy", busy ? "true" : "false");
|
|
403
|
+
const installed = [...views.values()].some((view) => view.install.adapterId === adapterId);
|
|
404
|
+
card?.querySelectorAll<HTMLButtonElement | HTMLInputElement>("button,input").forEach((control) => { control.disabled = busy || installed || !canManage || catalog.get(adapterId)?.availability !== "available"; });
|
|
405
|
+
};
|
|
406
|
+
async function definitive<TRequest extends object, TResponse>(resource: string, operation: string, request: TRequest, invoke: (prepared: TRequest) => Promise<TResponse>): Promise<TResponse> {
|
|
407
|
+
const prepared = ledger.prepare(resource, operation, request);
|
|
408
|
+
const result = await invoke(prepared);
|
|
409
|
+
ledger.complete(resource, operation);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function loadManaged(): Promise<void> {
|
|
414
|
+
announce("Loading managed analytics…");
|
|
415
|
+
try {
|
|
416
|
+
const [definitions, installs] = await Promise.all([
|
|
417
|
+
client.listAdapterDefinitions({ apiVersion: "v1", query: { limit: 100 } }),
|
|
418
|
+
client.listIntegrationInstalls({ apiVersion: "v1", query: { limit: 100 } }),
|
|
419
|
+
]);
|
|
420
|
+
if (isError(definitions)) { announce(statusMessage(definitions), "error"); return; }
|
|
421
|
+
if (isError(installs)) { announce(statusMessage(installs), "error"); return; }
|
|
422
|
+
catalog = new Map(definitions.data.items.map((item) => [item.adapterId, item]));
|
|
423
|
+
const loaded = await Promise.all(installs.data.items.map(async (install) => {
|
|
424
|
+
const definition = catalog.get(install.adapterId);
|
|
425
|
+
if (!definition) return { install, definition: null, revisions: null, degraded: "catalog_definition_unavailable" as const };
|
|
426
|
+
try {
|
|
427
|
+
const revisions = await client.listIntegrationRevisions({ apiVersion: "v1", query: { installId: install.installId, limit: 100 } });
|
|
428
|
+
return isError(revisions)
|
|
429
|
+
? { install, definition, revisions: null, degraded: "revision_history_unavailable" as const }
|
|
430
|
+
: { install, definition, revisions: revisions.data.items };
|
|
431
|
+
} catch {
|
|
432
|
+
return { install, definition, revisions: null, degraded: "revision_history_unavailable" as const };
|
|
433
|
+
}
|
|
434
|
+
}));
|
|
435
|
+
views = new Map(loaded.map((item) => [item.install.installId, item]));
|
|
436
|
+
reconcileManagedMutationLedger(ledger, views, !installs.data.nextCursor);
|
|
437
|
+
renderAdapters();
|
|
438
|
+
renderManaged();
|
|
439
|
+
const degraded = loaded.filter((item) => item.degraded).length;
|
|
440
|
+
announce(degraded
|
|
441
|
+
? `${loaded.length} managed install(s); ${degraded} could not be fully loaded. Changes are disabled for degraded rows.`
|
|
442
|
+
: `${loaded.length} managed install(s) · ${catalog.size} reviewed adapters`, degraded ? "error" : "");
|
|
443
|
+
} catch { announce("The managed analytics service could not be reached.", "error"); }
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function loadLocal(): Promise<void> {
|
|
447
|
+
const endpoint = panel!.dataset.localReadmodelEndpoint;
|
|
448
|
+
if (!endpoint || !localBody) return;
|
|
449
|
+
try {
|
|
450
|
+
const query = new URLSearchParams(); if (tenant) query.set("tenant", tenant); query.set("stage", panel!.dataset.stage || "staged");
|
|
451
|
+
const response = await fetch(`${endpoint}?${query}`, { headers: { accept: "application/json" } });
|
|
452
|
+
const body = await json<{ readModel?: IntegrationAdminReadModel }>(response);
|
|
453
|
+
if (response.ok && body.readModel) renderLocal(localBody, body.readModel);
|
|
454
|
+
} catch { localBody.textContent = "Tenant-local inventory unavailable."; }
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function mutation(action: string, installId: string): Promise<void> {
|
|
458
|
+
const view = views.get(installId); if (!view) return;
|
|
459
|
+
if (!canManage) { announce("Owner or admin access is required.", "error", true); return; }
|
|
460
|
+
if (!view.definition || !view.revisions || view.degraded) { announce("This install could not be fully loaded; changes are disabled.", "error", true); return; }
|
|
461
|
+
if (busyInstalls.has(installId)) return;
|
|
462
|
+
const resource = `install:${installId}`;
|
|
463
|
+
const expectedRevision = view.install.revision;
|
|
464
|
+
let locked = false;
|
|
465
|
+
announce("Applying managed analytics change…");
|
|
466
|
+
try {
|
|
467
|
+
if (action === "revise") {
|
|
468
|
+
const input = panel!.querySelector<HTMLInputElement>(`[data-revise-destination="${CSS.escape(installId)}"]`);
|
|
469
|
+
const retrying = ledger.operation(resource) === "revise";
|
|
470
|
+
if (!retrying && !input?.value.trim()) { announce("Enter a destination ID before staging a revision.", "error", true); return; }
|
|
471
|
+
const request: ReviseIntegrationInstallRequest = { apiVersion: "v1", idempotencyKey: key("admin-revise"), expectedRevision, body: { installId, adapterVersion: view.definition.adapterVersion, configuration: { destinationId: input?.value.trim() ?? "" } } };
|
|
472
|
+
setInstallBusy(installId, true); locked = true;
|
|
473
|
+
const result = await definitive(resource, "revise", request, (prepared) => client.reviseIntegrationInstall(prepared));
|
|
474
|
+
if (isError(result)) { await loadManaged(); announce(statusMessage(result), "error", true); return; }
|
|
475
|
+
} else if (action === "publish" || action === "suspend") {
|
|
476
|
+
const request: SetIntegrationInstallStatusRequest = { apiVersion: "v1", idempotencyKey: key(`admin-${action}`), expectedRevision, body: { installId, status: action === "publish" ? "enabled" : "disabled" } };
|
|
477
|
+
setInstallBusy(installId, true); locked = true;
|
|
478
|
+
const result = await definitive(resource, action, request, (prepared) => client.setIntegrationInstallStatus(prepared));
|
|
479
|
+
if (isError(result)) { await loadManaged(); announce(statusMessage(result), "error", true); return; }
|
|
480
|
+
} else if (action === "remove") {
|
|
481
|
+
const confirm = panel!.querySelector<HTMLInputElement>(`[data-remove-confirmation="${CSS.escape(installId)}"]`);
|
|
482
|
+
const retrying = ledger.operation(resource) === "remove";
|
|
483
|
+
if (!retrying && confirm?.value !== view.install.adapterId) { announce(`Type ${view.install.adapterId} exactly to confirm removal.`, "error", true); return; }
|
|
484
|
+
const request: DeleteIntegrationInstallRequest = { apiVersion: "v1", idempotencyKey: key("admin-remove"), expectedRevision, body: { installId } };
|
|
485
|
+
setInstallBusy(installId, true); locked = true;
|
|
486
|
+
const result = await definitive(resource, "remove", request, (prepared) => client.deleteIntegrationInstall(prepared));
|
|
487
|
+
if (isError(result)) { await loadManaged(); announce(statusMessage(result), "error", true); return; }
|
|
488
|
+
} else if (action === "rollback") {
|
|
489
|
+
setInstallBusy(installId, true); locked = true;
|
|
490
|
+
let publishRevision: number;
|
|
491
|
+
if (ledger.operation(resource) === "rollback-publish") {
|
|
492
|
+
const published = await definitive(resource, "rollback-publish", {} as SetIntegrationInstallStatusRequest, (prepared) => client.setIntegrationInstallStatus(prepared));
|
|
493
|
+
if (isError(published)) { await loadManaged(); announce(`Rollback is staged but not published. ${statusMessage(published)}`, "error", true); return; }
|
|
494
|
+
await loadManaged(); announce("Managed analytics rollback published.", "", true); return;
|
|
495
|
+
}
|
|
496
|
+
const select = panel!.querySelector<HTMLSelectElement>(`[data-rollback-revision="${CSS.escape(installId)}"]`);
|
|
497
|
+
const target = view.revisions.find((revision) => revision.revisionId === select?.value && revision.status === "superseded");
|
|
498
|
+
const retrying = ledger.operation(resource) === "rollback-stage";
|
|
499
|
+
if (!retrying && !target) { announce("Select a superseded revision to roll back to.", "error", true); return; }
|
|
500
|
+
const stageRequest: ReviseIntegrationInstallRequest = { apiVersion: "v1", idempotencyKey: key("admin-rollback-stage"), expectedRevision, body: { installId, adapterVersion: target?.adapterVersion ?? view.definition.adapterVersion, configuration: { ...(target?.configuration ?? {}) } } };
|
|
501
|
+
const staged = await definitive(resource, "rollback-stage", stageRequest, (prepared) => client.reviseIntegrationInstall(prepared));
|
|
502
|
+
if (isError(staged)) { await loadManaged(); announce(statusMessage(staged), "error", true); return; }
|
|
503
|
+
publishRevision = staged.data.install.revision;
|
|
504
|
+
const publishRequest: SetIntegrationInstallStatusRequest = { apiVersion: "v1", idempotencyKey: key("admin-rollback-publish"), expectedRevision: publishRevision, body: { installId, status: "enabled" } };
|
|
505
|
+
const published = await definitive(resource, "rollback-publish", publishRequest, (prepared) => client.setIntegrationInstallStatus(prepared));
|
|
506
|
+
if (isError(published)) { await loadManaged(); announce(`Rollback is staged but not published. ${statusMessage(published)}`, "error", true); return; }
|
|
507
|
+
}
|
|
508
|
+
await loadManaged(); announce("Managed analytics change saved.", "", true);
|
|
509
|
+
} catch (cause) {
|
|
510
|
+
announce(cause instanceof PendingManagedMutationError
|
|
511
|
+
? `Retry the pending ${cause.operation} action before starting another change.`
|
|
512
|
+
: "The response was not confirmed. Retry the same action; its idempotency key and request will be reused.", "error", true);
|
|
513
|
+
} finally { if (locked) setInstallBusy(installId, false); }
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
async function configure(adapterId: string): Promise<void> {
|
|
517
|
+
const definition = catalog.get(adapterId); const resultNode = panel!.querySelector<HTMLElement>(`[data-adapter-result="${CSS.escape(adapterId)}"]`); const input = panel!.querySelector<HTMLInputElement>(`[data-adapter-id-input="${CSS.escape(adapterId)}"]`);
|
|
518
|
+
if (!definition || !resultNode) return; resultNode.hidden = false;
|
|
519
|
+
if (!canManage) { resultNode.dataset.tone = "error"; resultNode.textContent = "Owner or admin access is required."; return; }
|
|
520
|
+
const resource = `adapter:${adapterId}`;
|
|
521
|
+
const retrying = ledger.operation(resource) === "create";
|
|
522
|
+
if (!retrying && [...views.values()].some((view) => view.install.adapterId === adapterId)) { resultNode.dataset.tone = "error"; resultNode.textContent = "This adapter is already configured."; return; }
|
|
523
|
+
if (!retrying && !input?.value.trim()) { resultNode.dataset.tone = "error"; resultNode.textContent = "Enter a destination ID."; return; }
|
|
524
|
+
if (busyAdapters.has(adapterId)) return;
|
|
525
|
+
resultNode.textContent = "Creating disabled install…"; resultNode.dataset.tone = "";
|
|
526
|
+
try {
|
|
527
|
+
const request: CreateIntegrationInstallRequest = { apiVersion: "v1", idempotencyKey: key("admin-create"), expectedRevision: 0, body: { adapterId: definition.adapterId, adapterVersion: definition.adapterVersion, configuration: { destinationId: input?.value.trim() ?? "" }, status: "disabled" } };
|
|
528
|
+
setAdapterBusy(adapterId, true);
|
|
529
|
+
const response = await definitive(resource, "create", request, (prepared) => client.createIntegrationInstall(prepared));
|
|
530
|
+
if (isError(response)) { resultNode.dataset.tone = "error"; resultNode.textContent = statusMessage(response); return; }
|
|
531
|
+
resultNode.textContent = "Disabled install created. Review its draft revision, then publish."; await loadManaged();
|
|
532
|
+
} catch (cause) {
|
|
533
|
+
resultNode.dataset.tone = "error";
|
|
534
|
+
resultNode.textContent = cause instanceof PendingManagedMutationError
|
|
535
|
+
? `Retry the pending ${cause.operation} action first.`
|
|
536
|
+
: "The response was not confirmed. Retry Configure; the same idempotency key and request will be reused.";
|
|
537
|
+
} finally { setAdapterBusy(adapterId, false); }
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
panel.addEventListener("click", (event) => {
|
|
541
|
+
const target = event.target instanceof Element ? event.target : null; if (!target) return;
|
|
542
|
+
const action = target.closest<HTMLButtonElement>("[data-managed-action]"); if (action && !action.disabled && action.dataset.installId) { void mutation(action.dataset.managedAction!, action.dataset.installId); return; }
|
|
543
|
+
const configureButton = target.closest<HTMLButtonElement>("[data-configure-adapter]"); if (configureButton && !configureButton.disabled) void configure(configureButton.dataset.configureAdapter!);
|
|
544
|
+
});
|
|
545
|
+
panel.querySelector("[data-integrations-refresh]")?.addEventListener("click", () => { void loadManaged(); void loadLocal(); });
|
|
546
|
+
void loadManaged(); void loadLocal();
|
|
547
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-model contract for the `/admin#integrations` surface specified in
|
|
3
|
+
* docs/private-apps/admin-integrations-ia.md (`ext-admin-01`). Types only —
|
|
4
|
+
* no transport, no D1, no route wiring. `ext-admin-02-read-model-api` fills
|
|
5
|
+
* these shapes from real tenant-scoped data (desired config, materialized
|
|
6
|
+
* plan, installed app version, environment, consent, CSP diff, assurance,
|
|
7
|
+
* diagnostics, rollback availability); `ext-admin-07-tenant-lifecycle`
|
|
8
|
+
* implements the actions `IntegrationLifecycleAction` names. Both consume
|
|
9
|
+
* this file rather than inventing their own shape mid-implementation.
|
|
10
|
+
*
|
|
11
|
+
* `IntegrationInstallStatus` is a coarser admin-display status than
|
|
12
|
+
* `AppInstallStatus` (registryService.ts: active/suspended/uninstalled) —
|
|
13
|
+
* it folds in candidate/preview/error states the merchant-facing inventory
|
|
14
|
+
* needs to show that the install-record status alone doesn't carry. The
|
|
15
|
+
* read-model API derives one from the other; they are not the same enum.
|
|
16
|
+
*/
|
|
17
|
+
import type { IntegrationEnvironment } from "@tot/public-runtime";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Four authority tiers (0014 I1 `integration-authority-four-tiers`,
|
|
21
|
+
* refined by 0016). This is the ACTUAL authority a surface grants, resolved
|
|
22
|
+
* server-side from surface × tenant-tier — never an app's declared intent.
|
|
23
|
+
*/
|
|
24
|
+
export type IntegrationAuthorityTier =
|
|
25
|
+
| "platform-adapter"
|
|
26
|
+
| "sandbox-broker"
|
|
27
|
+
| "server"
|
|
28
|
+
| "elevated-parent";
|
|
29
|
+
|
|
30
|
+
/** Three-party field/asset ownership (0014 C2 `integration-ownership-three-party`). */
|
|
31
|
+
export type IntegrationOwner = "storefront" | "tenant" | "vendor";
|
|
32
|
+
|
|
33
|
+
/** Merchant-facing inventory/detail status. Distinct from `AppInstallStatus` — see file header. */
|
|
34
|
+
export type IntegrationInstallStatus =
|
|
35
|
+
| "configuring"
|
|
36
|
+
| "preview-ready"
|
|
37
|
+
| "live"
|
|
38
|
+
| "suspended"
|
|
39
|
+
| "error";
|
|
40
|
+
|
|
41
|
+
/** IA §5–9 lifecycle actions. `ext-admin-07` authorizes and executes each. */
|
|
42
|
+
export type IntegrationLifecycleAction =
|
|
43
|
+
| "preview"
|
|
44
|
+
| "publish"
|
|
45
|
+
| "rollback"
|
|
46
|
+
| "suspend"
|
|
47
|
+
| "resume"
|
|
48
|
+
| "uninstall";
|
|
49
|
+
|
|
50
|
+
/** One row of the IA §4 inventory table. */
|
|
51
|
+
export interface IntegrationInventoryItem {
|
|
52
|
+
installId: string;
|
|
53
|
+
appId: string;
|
|
54
|
+
/** Plain-language purpose, e.g. "Sends page and purchase events to Google Ads/Analytics" — never an internal adapter name. */
|
|
55
|
+
purpose: string;
|
|
56
|
+
status: IntegrationInstallStatus;
|
|
57
|
+
environment: IntegrationEnvironment;
|
|
58
|
+
authorityTier: IntegrationAuthorityTier;
|
|
59
|
+
lastActivity: { actor: string; at: string } | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** IA §5 panel-3 effective (not desired) permissions/CSP/data view. */
|
|
63
|
+
export interface IntegrationEffectiveAuthority {
|
|
64
|
+
authorityTier: IntegrationAuthorityTier;
|
|
65
|
+
/** Exact origins the CSP compiler emits for this install — never a wildcard or raw header (0014 E2). */
|
|
66
|
+
cspOrigins: string[];
|
|
67
|
+
/** True only if this install holds an elevated PII scope beyond the anonymous/minimal default (0014 M2). */
|
|
68
|
+
hasElevatedPiiScope: boolean;
|
|
69
|
+
/** Consent purpose ids this install's delivery depends on; empty means no dependency configured yet. */
|
|
70
|
+
consentPurposeIds: string[];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** IA §5 panel-5 developer-mode diagnostics. */
|
|
74
|
+
export interface IntegrationDiagnosticIssue {
|
|
75
|
+
severity: "blocking" | "warning";
|
|
76
|
+
/** Plain-language summary a merchant's locked/summary view can show as-is. */
|
|
77
|
+
summary: string;
|
|
78
|
+
/** Full text a developer's "Copy details" action serializes verbatim. */
|
|
79
|
+
copyableDetail: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface IntegrationDetailView extends IntegrationInventoryItem {
|
|
83
|
+
/** Owner of each editable field, by field name — drives the IA §10 ownership display. */
|
|
84
|
+
fieldOwners: Record<string, IntegrationOwner>;
|
|
85
|
+
effectiveAuthority: IntegrationEffectiveAuthority;
|
|
86
|
+
diagnostics: IntegrationDiagnosticIssue[];
|
|
87
|
+
/** Present only once a version has gone live at least once (IA §7 rollback availability). */
|
|
88
|
+
rollbackAvailable: boolean;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Plain-language label for an authority tier, for the inventory/detail chip.
|
|
93
|
+
* Pure — no lookups, so both server render and any client re-render agree.
|
|
94
|
+
*/
|
|
95
|
+
export function describeAuthorityTier(tier: IntegrationAuthorityTier): string {
|
|
96
|
+
switch (tier) {
|
|
97
|
+
case "platform-adapter":
|
|
98
|
+
return "Built-in adapter";
|
|
99
|
+
case "sandbox-broker":
|
|
100
|
+
return "Sandboxed (boxed)";
|
|
101
|
+
case "server":
|
|
102
|
+
return "Server-side";
|
|
103
|
+
case "elevated-parent":
|
|
104
|
+
return "Elevated parent access";
|
|
105
|
+
}
|
|
106
|
+
}
|