@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-domain consent propagation for hosted-receipt measurement (ADR 0022
|
|
3
|
+
* `receipt-consent-propagation`, ADR 0023 Q3 `analytics-consent-required`;
|
|
4
|
+
* ext-commerce-06). The "receipt" is the post-purchase page the PROVIDER (Foxy)
|
|
5
|
+
* renders on ITS OWN origin — a DIFFERENT domain from the storefront — so any
|
|
6
|
+
* browser measurement there is cross-domain.
|
|
7
|
+
*
|
|
8
|
+
* This is the decision seam that says whether that cross-domain browser measurement
|
|
9
|
+
* may run, and if so, exactly what may cross the domain boundary with it. It is
|
|
10
|
+
* DEFAULT-SAFE to server-side-only conversion: the storefront's own verified-order
|
|
11
|
+
* → server conversion join ({@link ./order-conversion.ts}) is the authoritative,
|
|
12
|
+
* always-on path; receipt-page BROWSER measurement is an additive best-effort layer
|
|
13
|
+
* that is enabled ONLY when every gate below holds, and is silently skipped (server
|
|
14
|
+
* conversion still fires) otherwise.
|
|
15
|
+
*
|
|
16
|
+
* Two BINDING board decisions gate it, both fail-closed:
|
|
17
|
+
*
|
|
18
|
+
* - `integration-receipt-consent`: non-essential receipt-page browser measurement
|
|
19
|
+
* requires VERIFIED provider-supported consent propagation. Until provider
|
|
20
|
+
* discovery + counsel verify that the provider honours and propagates the
|
|
21
|
+
* shopper's analytics-consent decision on the receipt origin, conversion stays
|
|
22
|
+
* SERVER-SIDE ONLY. {@link ProviderReceiptSupport.consentPropagationVerified}
|
|
23
|
+
* defaults CLOSED (unverified), so with no verified provider integration this
|
|
24
|
+
* seam always returns `server-only`.
|
|
25
|
+
*
|
|
26
|
+
* - `gate-legal-q3` (analytics-consent-required): the shopper's analytics-consent
|
|
27
|
+
* decision (Q3) must TRAVEL WITH, and GATE, any browser measurement. When the
|
|
28
|
+
* shopper is not analytics-eligible, no browser measurement runs — attribution
|
|
29
|
+
* is dropped and conversion is server-side only.
|
|
30
|
+
*
|
|
31
|
+
* What crosses the boundary is a {@link ReceiptMeasurementGrant}: a bounded, opaque,
|
|
32
|
+
* CLOSED descriptor carrying ONLY public execution-context facts (tenant, stage,
|
|
33
|
+
* provider), the OPAQUE bounded correlation THREAD id (never the signed correlation
|
|
34
|
+
* TOKEN, never the signing secret, never a re-usable capability), and the
|
|
35
|
+
* analytics-consent snapshot that travels with the measurement. It carries NO PII
|
|
36
|
+
* and NO secret by construction — the shape has no such field and
|
|
37
|
+
* {@link findForbiddenFields} is asserted over every serialized grant.
|
|
38
|
+
*
|
|
39
|
+
* Pure and dependency-injected: the provider-support fact and the correlation thread
|
|
40
|
+
* are inputs from trusted server context, so the whole decision is exercised in node
|
|
41
|
+
* with no edge runtime.
|
|
42
|
+
*/
|
|
43
|
+
import type { IntegrationEnvironment } from "./environment.js";
|
|
44
|
+
import { INTEGRATION_ENVIRONMENTS } from "./environment.js";
|
|
45
|
+
import type { ConsentSnapshot } from "./consent-broker.js";
|
|
46
|
+
import { isPurposeEligible, toEventConsentSnapshot } from "./consent-broker.js";
|
|
47
|
+
import type { EventConsentSnapshot, EventCorrelation } from "./events.js";
|
|
48
|
+
import type { ConsentPurpose } from "../integration-capabilities.js";
|
|
49
|
+
import { CONSENT_PURPOSES } from "../integration-capabilities.js";
|
|
50
|
+
import { RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION, CONSENT_SNAPSHOT_SCHEMA_VERSION } from "./version.js";
|
|
51
|
+
import { findForbiddenFields } from "./secrets.js";
|
|
52
|
+
|
|
53
|
+
/** The consent purpose receipt-page browser measurement requires (ADR 0023 Q3). */
|
|
54
|
+
export const RECEIPT_MEASUREMENT_CONSENT_PURPOSE: ConsentPurpose = "analytics";
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The server-verified fact of whether a provider's hosted receipt supports propagating
|
|
58
|
+
* the shopper's analytics-consent decision to browser measurement on the receipt origin.
|
|
59
|
+
* This is NOT a caller assertion the browser can set — it is resolved from trusted
|
|
60
|
+
* server context (a provider-integration registry gated by counsel + discovery).
|
|
61
|
+
*/
|
|
62
|
+
export interface ProviderReceiptSupport {
|
|
63
|
+
/** The provider whose hosted receipt this is (e.g. `"foxy"`) — a public fact. */
|
|
64
|
+
provider: string;
|
|
65
|
+
/**
|
|
66
|
+
* Whether this provider's receipt has VERIFIED consent-propagation support. DEFAULT
|
|
67
|
+
* CLOSED: absent/unknown ⇒ `false` ⇒ server-side conversion only. Only a
|
|
68
|
+
* counsel-and-discovery-verified provider integration flips this `true`.
|
|
69
|
+
*/
|
|
70
|
+
consentPropagationVerified: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The bounded, opaque descriptor that crosses storefront → provider → app domains to
|
|
75
|
+
* enable receipt-page browser measurement. CLOSED and browser-safe: it carries ONLY
|
|
76
|
+
* public execution-context facts, the opaque correlation THREAD id, its bounded expiry,
|
|
77
|
+
* and the analytics-consent snapshot that travels with the measurement. It carries no
|
|
78
|
+
* PII, no signing secret, and no re-usable token — the correlation thread id verifies
|
|
79
|
+
* nothing on its own.
|
|
80
|
+
*/
|
|
81
|
+
export interface ReceiptMeasurementGrant {
|
|
82
|
+
schemaVersion: typeof RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION;
|
|
83
|
+
/** The provider whose receipt origin the measurement runs on — a public fact. */
|
|
84
|
+
provider: string;
|
|
85
|
+
/** The tenant the handoff ran in — a public execution-context fact. */
|
|
86
|
+
tenant: string;
|
|
87
|
+
/** The stage the handoff ran in; a staged grant can never enable a live receipt read. */
|
|
88
|
+
environment: IntegrationEnvironment;
|
|
89
|
+
/**
|
|
90
|
+
* The OPAQUE bounded correlation thread id (the handoff `eventId`) — the ONE
|
|
91
|
+
* cross-site attribution handle. NOT the signed correlation token, NOT a capability:
|
|
92
|
+
* it verifies nothing and authorizes nothing; it only threads this measurement to its
|
|
93
|
+
* authoritative server conversion.
|
|
94
|
+
*/
|
|
95
|
+
correlationId: string;
|
|
96
|
+
/** The correlation's bounded expiry (ISO 8601). Measurement past this is dropped. */
|
|
97
|
+
correlationExpiresAt: string;
|
|
98
|
+
/**
|
|
99
|
+
* The analytics-consent snapshot that TRAVELS WITH the measurement (Q3): eligible
|
|
100
|
+
* purpose names + snapshot id, never raw consent state and never PII.
|
|
101
|
+
*/
|
|
102
|
+
consent: EventConsentSnapshot;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Why receipt-page browser measurement was not enabled (server-side conversion only). */
|
|
106
|
+
export type ReceiptMeasurementDenyReason =
|
|
107
|
+
/** The shopper is not analytics-eligible (Q3 gate) — attribution dropped. */
|
|
108
|
+
| "analytics-consent-required"
|
|
109
|
+
/** The provider has no VERIFIED consent-propagation support (board decision). */
|
|
110
|
+
| "provider-unverified"
|
|
111
|
+
/** No opaque correlation thread to carry (the handoff was unattributed). */
|
|
112
|
+
| "correlation-missing"
|
|
113
|
+
/** The correlation thread is past its bounded lifetime. */
|
|
114
|
+
| "correlation-expired"
|
|
115
|
+
/** The grant's tenant/stage does not match the receipt-read's trusted context. */
|
|
116
|
+
| "context-mismatch";
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The receipt-measurement decision. `browser` carries the bounded {@link
|
|
120
|
+
* ReceiptMeasurementGrant} to propagate across the domain boundary; `server-only` is
|
|
121
|
+
* the default-safe outcome — no browser measurement, the server conversion join alone.
|
|
122
|
+
*/
|
|
123
|
+
export type ReceiptMeasurementDecision =
|
|
124
|
+
| { mode: "browser"; grant: ReceiptMeasurementGrant }
|
|
125
|
+
| { mode: "server-only"; reason: ReceiptMeasurementDenyReason };
|
|
126
|
+
|
|
127
|
+
export interface ResolveReceiptMeasurementInput {
|
|
128
|
+
/** The tenant, from trusted server execution context — never a client value. */
|
|
129
|
+
tenant: string;
|
|
130
|
+
/** The stage, from the renderer-owned `BrowserEnvironment.environment` (server-supplied). */
|
|
131
|
+
environment: IntegrationEnvironment;
|
|
132
|
+
/** The shopper's LIVE consent snapshot (parsed fail-closed by the caller). */
|
|
133
|
+
consent: ConsentSnapshot;
|
|
134
|
+
/**
|
|
135
|
+
* The provider's server-verified receipt-consent-propagation support. `null`/`undefined`
|
|
136
|
+
* ⇒ unverified ⇒ server-side only (the expected state until Foxy discovery + counsel).
|
|
137
|
+
*/
|
|
138
|
+
providerSupport: ProviderReceiptSupport | null | undefined;
|
|
139
|
+
/**
|
|
140
|
+
* The opaque bounded correlation thread from the attributed checkout handoff. `null`/
|
|
141
|
+
* `undefined` ⇒ the handoff was unattributed ⇒ nothing to carry ⇒ server-side only.
|
|
142
|
+
*/
|
|
143
|
+
correlation: EventCorrelation | null | undefined;
|
|
144
|
+
/** Injectable wall clock in ms (defaults to `Date.now()`) for deterministic tests. */
|
|
145
|
+
now?: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Parse an ISO timestamp to epoch ms, or `null` if it is not a valid date. */
|
|
149
|
+
function parseIsoMs(iso: string): number | null {
|
|
150
|
+
const ms = Date.parse(iso);
|
|
151
|
+
return Number.isFinite(ms) ? ms : null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Decide whether cross-domain browser measurement may run on the hosted receipt, and
|
|
156
|
+
* build the bounded grant that crosses the boundary if so. Fail-closed and
|
|
157
|
+
* default-safe: the gates run consent FIRST (the provider-support fact and the
|
|
158
|
+
* correlation are never even consulted for a shopper who withheld analytics consent),
|
|
159
|
+
* then verified provider support, then a present + unexpired correlation thread. Any
|
|
160
|
+
* gate that does not hold yields `server-only` — the authoritative server conversion
|
|
161
|
+
* still fires; only the additive browser layer is withheld. Never throws.
|
|
162
|
+
*/
|
|
163
|
+
export function resolveReceiptMeasurement(
|
|
164
|
+
input: ResolveReceiptMeasurementInput,
|
|
165
|
+
): ReceiptMeasurementDecision {
|
|
166
|
+
// Q3 consent gate FIRST: a shopper who is not analytics-eligible gets NO browser
|
|
167
|
+
// measurement; the provider fact and correlation are never consulted for them.
|
|
168
|
+
if (!isPurposeEligible(input.consent, RECEIPT_MEASUREMENT_CONSENT_PURPOSE)) {
|
|
169
|
+
return { mode: "server-only", reason: "analytics-consent-required" };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Board decision `integration-receipt-consent`: no browser measurement without
|
|
173
|
+
// VERIFIED provider-supported consent propagation. Default-closed.
|
|
174
|
+
if (!input.providerSupport?.consentPropagationVerified) {
|
|
175
|
+
return { mode: "server-only", reason: "provider-unverified" };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Nothing to correlate: an unattributed handoff carries no opaque thread.
|
|
179
|
+
const correlation = input.correlation;
|
|
180
|
+
if (!correlation || typeof correlation.id !== "string" || correlation.id.length === 0) {
|
|
181
|
+
return { mode: "server-only", reason: "correlation-missing" };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Bounded lifetime: never mint a grant whose correlation thread is already dead.
|
|
185
|
+
const expMs = parseIsoMs(correlation.expiresAt);
|
|
186
|
+
const nowMs = input.now ?? Date.now();
|
|
187
|
+
if (expMs === null || nowMs >= expMs) {
|
|
188
|
+
return { mode: "server-only", reason: "correlation-expired" };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const grant: ReceiptMeasurementGrant = {
|
|
192
|
+
schemaVersion: RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION,
|
|
193
|
+
provider: input.providerSupport.provider,
|
|
194
|
+
tenant: input.tenant,
|
|
195
|
+
environment: input.environment,
|
|
196
|
+
correlationId: correlation.id,
|
|
197
|
+
correlationExpiresAt: correlation.expiresAt,
|
|
198
|
+
// The analytics-consent decision travels WITH the measurement.
|
|
199
|
+
consent: toEventConsentSnapshot(input.consent),
|
|
200
|
+
};
|
|
201
|
+
return { mode: "browser", grant };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- cross-domain carry -----------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Serialize a grant for propagation across the domain boundary (a redirect param or a
|
|
208
|
+
* `postMessage` payload). Asserts, defense-in-depth, that no field name denotes secret
|
|
209
|
+
* material before it can cross — so a future field addition that names a secret is
|
|
210
|
+
* caught here, not in production. The shape is already CLOSED and PII-free by design.
|
|
211
|
+
*/
|
|
212
|
+
export function serializeReceiptMeasurementGrant(grant: ReceiptMeasurementGrant): string {
|
|
213
|
+
const forbidden = findForbiddenFields(grant);
|
|
214
|
+
if (forbidden.length > 0) {
|
|
215
|
+
throw new Error(
|
|
216
|
+
`receipt measurement grant carries forbidden field(s): ${forbidden.map((f) => f.path).join(", ")}`,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
// Fixed key order so an equal grant always serializes to byte-identical output.
|
|
220
|
+
return JSON.stringify({
|
|
221
|
+
schemaVersion: grant.schemaVersion,
|
|
222
|
+
provider: grant.provider,
|
|
223
|
+
tenant: grant.tenant,
|
|
224
|
+
environment: grant.environment,
|
|
225
|
+
correlationId: grant.correlationId,
|
|
226
|
+
correlationExpiresAt: grant.correlationExpiresAt,
|
|
227
|
+
consent: {
|
|
228
|
+
schemaVersion: grant.consent.schemaVersion,
|
|
229
|
+
snapshotId: grant.consent.snapshotId,
|
|
230
|
+
eligiblePurposes: [...grant.consent.eligiblePurposes],
|
|
231
|
+
},
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const GRANT_KEYS: readonly (keyof ReceiptMeasurementGrant)[] = [
|
|
236
|
+
"schemaVersion",
|
|
237
|
+
"provider",
|
|
238
|
+
"tenant",
|
|
239
|
+
"environment",
|
|
240
|
+
"correlationId",
|
|
241
|
+
"correlationExpiresAt",
|
|
242
|
+
"consent",
|
|
243
|
+
];
|
|
244
|
+
|
|
245
|
+
/** Validate an {@link EventConsentSnapshot} fail-closed: shape + only known purposes. */
|
|
246
|
+
function parseEventConsentSnapshot(raw: unknown): EventConsentSnapshot | null {
|
|
247
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
|
|
248
|
+
const obj = raw as Record<string, unknown>;
|
|
249
|
+
for (const key of Object.keys(obj)) {
|
|
250
|
+
if (!["schemaVersion", "snapshotId", "eligiblePurposes"].includes(key)) return null;
|
|
251
|
+
}
|
|
252
|
+
if (obj.schemaVersion !== CONSENT_SNAPSHOT_SCHEMA_VERSION) return null;
|
|
253
|
+
if (typeof obj.snapshotId !== "string" || obj.snapshotId.length === 0) return null;
|
|
254
|
+
if (!Array.isArray(obj.eligiblePurposes)) return null;
|
|
255
|
+
const known = new Set<string>(CONSENT_PURPOSES);
|
|
256
|
+
for (const p of obj.eligiblePurposes) {
|
|
257
|
+
if (typeof p !== "string" || !known.has(p)) return null;
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
schemaVersion: CONSENT_SNAPSHOT_SCHEMA_VERSION,
|
|
261
|
+
snapshotId: obj.snapshotId,
|
|
262
|
+
eligiblePurposes: [...(obj.eligiblePurposes as string[])],
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Parse a serialized grant fail-closed. An unknown schema version, an unknown or missing
|
|
268
|
+
* field, an invalid stage, a malformed consent snapshot, a non-string opaque handle, or
|
|
269
|
+
* ANY field name that denotes secret material yields `null` — a caller on the receipt
|
|
270
|
+
* origin cannot smuggle a forged, tampered, or secret-bearing grant into measurement.
|
|
271
|
+
*/
|
|
272
|
+
export function parseReceiptMeasurementGrant(raw: unknown): ReceiptMeasurementGrant | null {
|
|
273
|
+
let obj: unknown = raw;
|
|
274
|
+
if (typeof raw === "string") {
|
|
275
|
+
try {
|
|
276
|
+
obj = JSON.parse(raw);
|
|
277
|
+
} catch {
|
|
278
|
+
return null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (typeof obj !== "object" || obj === null || Array.isArray(obj)) return null;
|
|
282
|
+
const o = obj as Record<string, unknown>;
|
|
283
|
+
|
|
284
|
+
for (const key of Object.keys(o)) {
|
|
285
|
+
if (!(GRANT_KEYS as readonly string[]).includes(key)) return null;
|
|
286
|
+
}
|
|
287
|
+
// Defense-in-depth: reject any grant carrying a secret-named field, at any depth.
|
|
288
|
+
if (findForbiddenFields(o).length > 0) return null;
|
|
289
|
+
|
|
290
|
+
if (o.schemaVersion !== RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION) return null;
|
|
291
|
+
if (typeof o.provider !== "string" || o.provider.length === 0) return null;
|
|
292
|
+
if (typeof o.tenant !== "string" || o.tenant.length === 0) return null;
|
|
293
|
+
if (
|
|
294
|
+
typeof o.environment !== "string" ||
|
|
295
|
+
!(INTEGRATION_ENVIRONMENTS as readonly string[]).includes(o.environment)
|
|
296
|
+
) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
if (typeof o.correlationId !== "string" || o.correlationId.length === 0) return null;
|
|
300
|
+
if (typeof o.correlationExpiresAt !== "string" || parseIsoMs(o.correlationExpiresAt) === null) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const consent = parseEventConsentSnapshot(o.consent);
|
|
304
|
+
if (!consent) return null;
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
schemaVersion: RECEIPT_MEASUREMENT_GRANT_SCHEMA_VERSION,
|
|
308
|
+
provider: o.provider,
|
|
309
|
+
tenant: o.tenant,
|
|
310
|
+
environment: o.environment as IntegrationEnvironment,
|
|
311
|
+
correlationId: o.correlationId,
|
|
312
|
+
correlationExpiresAt: o.correlationExpiresAt,
|
|
313
|
+
consent,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** The trusted receipt-origin context a consumed grant is re-checked against. */
|
|
318
|
+
export interface ReceiptMeasurementContext {
|
|
319
|
+
/** The tenant, from the receipt read's trusted context. */
|
|
320
|
+
tenant: string;
|
|
321
|
+
/** The stage, from the receipt read's trusted context. */
|
|
322
|
+
environment: IntegrationEnvironment;
|
|
323
|
+
/** Injectable wall clock in ms (defaults to `Date.now()`). */
|
|
324
|
+
now?: number;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The consume-side gate on the receipt origin. `enabled: true` means browser
|
|
329
|
+
* measurement may fire with the parsed grant; `enabled: false` means it must not (the
|
|
330
|
+
* server conversion already covers it). `malformed` is a grant that failed the
|
|
331
|
+
* fail-closed parse.
|
|
332
|
+
*/
|
|
333
|
+
export type ReceiptMeasurementGate =
|
|
334
|
+
| { enabled: true; grant: ReceiptMeasurementGrant }
|
|
335
|
+
| { enabled: false; reason: ReceiptMeasurementDenyReason | "malformed" };
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Re-validate a propagated grant on the receipt origin before any browser measurement
|
|
339
|
+
* fires — defense-in-depth across the domain boundary, independent of the producer.
|
|
340
|
+
* Fail-closed: it re-checks that analytics consent still travels with the grant, that
|
|
341
|
+
* the grant is bound to THIS tenant/stage (env-consistency — a staged grant can never
|
|
342
|
+
* enable a live receipt read), and that the correlation thread has not expired. Any
|
|
343
|
+
* failure ⇒ `enabled: false` ⇒ server-side conversion only.
|
|
344
|
+
*/
|
|
345
|
+
export function receiptMeasurementEnabled(
|
|
346
|
+
rawGrant: unknown,
|
|
347
|
+
ctx: ReceiptMeasurementContext,
|
|
348
|
+
): ReceiptMeasurementGate {
|
|
349
|
+
const grant = parseReceiptMeasurementGrant(rawGrant);
|
|
350
|
+
if (!grant) return { enabled: false, reason: "malformed" };
|
|
351
|
+
|
|
352
|
+
// Q3 consent must STILL travel with the grant.
|
|
353
|
+
if (!grant.consent.eligiblePurposes.includes(RECEIPT_MEASUREMENT_CONSENT_PURPOSE)) {
|
|
354
|
+
return { enabled: false, reason: "analytics-consent-required" };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Env-consistency: the grant is bound to the tenant/stage it was minted in; a
|
|
358
|
+
// cross-tenant or cross-stage grant can never enable measurement here.
|
|
359
|
+
if (grant.tenant !== ctx.tenant || grant.environment !== ctx.environment) {
|
|
360
|
+
return { enabled: false, reason: "context-mismatch" };
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const expMs = parseIsoMs(grant.correlationExpiresAt);
|
|
364
|
+
const nowMs = ctx.now ?? Date.now();
|
|
365
|
+
if (expMs === null || nowMs >= expMs) {
|
|
366
|
+
return { enabled: false, reason: "correlation-expired" };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return { enabled: true, grant };
|
|
370
|
+
}
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regulated elevated-parent CERTIFICATION enforcement (ext-policy-07; ADR 0016 G2,
|
|
3
|
+
* ADR 0023 Q6, decision `integration-sandbox-first-surface-matrix`).
|
|
4
|
+
*
|
|
5
|
+
* Full parent authority (`elevated-parent`) is a review-only tier — its DOM reach
|
|
6
|
+
* is not enforceably narrow. On a `regulated` surface it is therefore DISABLED BY
|
|
7
|
+
* DEFAULT and may run ONLY behind a green certification that:
|
|
8
|
+
*
|
|
9
|
+
* (a) ADVERSARIALLY proves EACH server-derived compliance obligation (ext-policy-04)
|
|
10
|
+
* is satisfied — fail-closed on any obligation that is unproven, absent, or
|
|
11
|
+
* merely asserted without a withstood adversarial challenge;
|
|
12
|
+
* (b) is IMMUTABLY PINNED to an exact content digest — a mutable container (no
|
|
13
|
+
* pinned digest, a malformed digest, or merchant-mutable parent code) is
|
|
14
|
+
* INELIGIBLE and rejected, never "certified" (Q6);
|
|
15
|
+
* (c) auto-disables on any DRIFT — the digest actually being served must equal the
|
|
16
|
+
* certified pinned digest, or the certification no longer holds;
|
|
17
|
+
* (d) never certifies mutable parent code on regulated regardless of anything else.
|
|
18
|
+
*
|
|
19
|
+
* This module does NOT re-implement the assurance or promotion layers — it PRODUCES
|
|
20
|
+
* the `certifiedImmutable` flag those layers already consume:
|
|
21
|
+
* - `IntegrationContext.certifiedImmutable` (../integration-capabilities.ts), which
|
|
22
|
+
* turns the `cert-required` matrix cell from `elevated-parent-uncertified` into a
|
|
23
|
+
* grant; and
|
|
24
|
+
* - `AssuranceDecisionContext.certifiedImmutable` (./assurance-decision.ts), the G2
|
|
25
|
+
* clause that a regulated elevated-parent record needs proof, never a mere
|
|
26
|
+
* acknowledgement.
|
|
27
|
+
* The required obligation set is SERVER-DERIVED from authoritative tenant config via
|
|
28
|
+
* ext-policy-04's `resolveObligationIds` / `verifyObligationCoverage` — never taken
|
|
29
|
+
* from the (spoofable) certificate — so nothing external can drop a duty.
|
|
30
|
+
*/
|
|
31
|
+
import type { CodeForm, Surface } from "../integration-capabilities.js";
|
|
32
|
+
import {
|
|
33
|
+
isRegulated,
|
|
34
|
+
resolveObligationIds,
|
|
35
|
+
verifyObligationCoverage,
|
|
36
|
+
type ObligationTenant,
|
|
37
|
+
} from "../compliance-obligations.js";
|
|
38
|
+
import { digestValue } from "./digest.js";
|
|
39
|
+
import type { ExecutionTier } from "./phases.js";
|
|
40
|
+
|
|
41
|
+
/** Contract token — bump the minor for additive vocabulary, the major for a break (ADR 0006 semver). */
|
|
42
|
+
export const REGULATED_PARENT_CERT_MODEL = "regulated-parent-cert-model@1" as const;
|
|
43
|
+
export type RegulatedParentCertModel = typeof REGULATED_PARENT_CERT_MODEL;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The outcome of an adversarial challenge run against one obligation. Only
|
|
47
|
+
* `withstood` — the obligation held under an active attempt to bypass it — is
|
|
48
|
+
* accepted. `breached` (the attempt succeeded) and `not-run` (no adversarial test
|
|
49
|
+
* was performed) both fail closed: an unproven obligation is never "satisfied".
|
|
50
|
+
*/
|
|
51
|
+
export const OBLIGATION_PROOF_RESULTS = ["withstood", "breached", "not-run"] as const;
|
|
52
|
+
export type ObligationProofResult = (typeof OBLIGATION_PROOF_RESULTS)[number];
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The adversarial proof for a single compliance obligation. It is not a claim that
|
|
56
|
+
* the duty is met — it is evidence that an adversarial challenge tried to defeat the
|
|
57
|
+
* duty and could not. `evidenceDigest` pins that evidence to exact content so the
|
|
58
|
+
* proof cannot be swapped after review.
|
|
59
|
+
*/
|
|
60
|
+
export interface ObligationProof {
|
|
61
|
+
/** The obligation this proves — must be a server-recognized obligation id (ext-policy-04). */
|
|
62
|
+
obligationId: string;
|
|
63
|
+
/** The adversarial challenge that was run (what attack the obligation withstood). Non-empty. */
|
|
64
|
+
adversarialChallenge: string;
|
|
65
|
+
/** The challenge outcome. Anything other than `withstood` fails closed. */
|
|
66
|
+
result: ObligationProofResult;
|
|
67
|
+
/** Content digest pinning the evidence backing this proof. Must be a well-formed pinned digest. */
|
|
68
|
+
evidenceDigest: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* A regulated elevated-parent certification artifact. It is bound to one app+version,
|
|
73
|
+
* pinned to one immutable content digest, scoped to the `regulated` surface and the
|
|
74
|
+
* `elevated-parent` tier, and carries one adversarial proof per applicable obligation.
|
|
75
|
+
*/
|
|
76
|
+
export interface RegulatedParentCertification {
|
|
77
|
+
model: RegulatedParentCertModel;
|
|
78
|
+
appId: string;
|
|
79
|
+
version: string;
|
|
80
|
+
/** Must be `regulated`; a certificate for any other surface does not enable regulated parent code. */
|
|
81
|
+
surface: Surface;
|
|
82
|
+
/** Must be `elevated-parent`; this certification exists only for full-parent authority. */
|
|
83
|
+
executionTier: ExecutionTier;
|
|
84
|
+
/** Must be `pinned-immutable`; `mutable-container` is prohibited on regulated (Q6). */
|
|
85
|
+
codeForm: CodeForm;
|
|
86
|
+
/** The exact content digest the certified artifact is immutably pinned to. */
|
|
87
|
+
pinnedDigest: string;
|
|
88
|
+
/** One adversarial proof per applicable compliance obligation (ext-policy-04). */
|
|
89
|
+
proofs: readonly ObligationProof[];
|
|
90
|
+
certifierId: string;
|
|
91
|
+
certifiedAt: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Server-resolved evaluation context for a certification decision. */
|
|
95
|
+
export interface RegulatedParentCertContext {
|
|
96
|
+
/** The tenant's authoritative config — the ONLY source of the required obligation set + regulated classification. */
|
|
97
|
+
tenant: ObligationTenant;
|
|
98
|
+
/**
|
|
99
|
+
* The content digest of the artifact currently resolved to serve. When present and
|
|
100
|
+
* different from the certified `pinnedDigest`, the served code has DRIFTED from what
|
|
101
|
+
* was certified and enablement auto-disables (c).
|
|
102
|
+
*/
|
|
103
|
+
servedDigest?: string;
|
|
104
|
+
/**
|
|
105
|
+
* The integration being evaluated. A certificate whose app/version does not match is
|
|
106
|
+
* for a different artifact and does not cover this one.
|
|
107
|
+
*/
|
|
108
|
+
binding?: { appId: string; version: string };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type RegulatedParentCertDenialReason =
|
|
112
|
+
/** The tenant is not on the regulated surface — this certification gate does not apply here. */
|
|
113
|
+
| "not-regulated"
|
|
114
|
+
/** No certification supplied — regulated elevated-parent is disabled by default (a). */
|
|
115
|
+
| "certificate-absent"
|
|
116
|
+
| "unknown-model"
|
|
117
|
+
| "wrong-surface"
|
|
118
|
+
| "not-elevated-parent"
|
|
119
|
+
/** Merchant-mutable parent code — prohibited on regulated regardless (b/d). */
|
|
120
|
+
| "mutable-parent-prohibited"
|
|
121
|
+
/** No exact content digest (or a malformed one) — a mutable container is ineligible (b). */
|
|
122
|
+
| "unpinned"
|
|
123
|
+
/** The certificate is for a different app/version than the integration under evaluation. */
|
|
124
|
+
| "binding-mismatch"
|
|
125
|
+
/** The served digest differs from the certified pinned digest — drift (c). */
|
|
126
|
+
| "digest-drift"
|
|
127
|
+
/** ext-policy-04 coverage failed (absent / unknown / duplicated / omitted obligation). */
|
|
128
|
+
| "obligation-coverage"
|
|
129
|
+
/** A server-required obligation carries no proof at all (a). */
|
|
130
|
+
| "obligation-unproven"
|
|
131
|
+
/** A required obligation's proof did not withstand its adversarial challenge (a). */
|
|
132
|
+
| "proof-not-adversarial"
|
|
133
|
+
/** A required obligation's proof evidence is not pinned to exact content. */
|
|
134
|
+
| "proof-evidence-unpinned";
|
|
135
|
+
|
|
136
|
+
export interface RegulatedParentCertDenial {
|
|
137
|
+
reason: RegulatedParentCertDenialReason;
|
|
138
|
+
/** The offending value (obligation id, digest, surface, tier, or a coverage message), when one applies. */
|
|
139
|
+
subject?: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The certification decision. `certifiedImmutable` is the single boolean the assurance
|
|
144
|
+
* and capability layers consume: it is `true` ONLY on the `ok` branch, so a regulated
|
|
145
|
+
* elevated-parent integration is enabled exactly when every clause above holds.
|
|
146
|
+
*/
|
|
147
|
+
export type RegulatedParentCertDecision =
|
|
148
|
+
| {
|
|
149
|
+
ok: true;
|
|
150
|
+
certifiedImmutable: true;
|
|
151
|
+
certification: RegulatedParentCertification;
|
|
152
|
+
/** The server-derived obligations proven under adversarial challenge (canonical order). */
|
|
153
|
+
provenObligations: string[];
|
|
154
|
+
}
|
|
155
|
+
| { ok: false; certifiedImmutable: false; denials: RegulatedParentCertDenial[] };
|
|
156
|
+
|
|
157
|
+
// A pinned digest is either the runtime's own FNV-1a-128 identity digest (32 lowercase
|
|
158
|
+
// hex chars) or an algorithm-prefixed content hash (e.g. `sha256:<hex>`). A mutable
|
|
159
|
+
// container has neither. Anything else is treated as unpinned and fails closed.
|
|
160
|
+
const FNV128_HEX = /^[0-9a-f]{32}$/;
|
|
161
|
+
const ALGO_DIGEST = /^[a-z][a-z0-9-]*:[0-9a-f]{16,}$/;
|
|
162
|
+
|
|
163
|
+
/** True when a string is a well-formed immutable content digest (never a mutable container). */
|
|
164
|
+
export function isPinnedDigest(digest: string | null | undefined): boolean {
|
|
165
|
+
return typeof digest === "string" && (FNV128_HEX.test(digest) || ALGO_DIGEST.test(digest));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Compute the runtime's deterministic content digest for an artifact — the value a
|
|
170
|
+
* certifier pins and a mount's served-digest is compared against. Reuses the shared
|
|
171
|
+
* identity primitive (./digest.ts) so a pin and a drift check fold the same bytes.
|
|
172
|
+
*/
|
|
173
|
+
export function computeArtifactDigest(content: unknown): string {
|
|
174
|
+
return digestValue(content);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** True when a served artifact has drifted from what was certified (c). */
|
|
178
|
+
export function isCertifiedDigestDrift(
|
|
179
|
+
certification: RegulatedParentCertification,
|
|
180
|
+
servedDigest: string | undefined,
|
|
181
|
+
): boolean {
|
|
182
|
+
return servedDigest !== undefined && servedDigest !== certification.pinnedDigest;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** True when a single obligation proof is an accepted adversarial, evidence-pinned proof. */
|
|
186
|
+
function isAcceptedProof(proof: ObligationProof | undefined): boolean {
|
|
187
|
+
return (
|
|
188
|
+
!!proof &&
|
|
189
|
+
proof.result === "withstood" &&
|
|
190
|
+
proof.adversarialChallenge.trim().length > 0 &&
|
|
191
|
+
isPinnedDigest(proof.evidenceDigest)
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Decide whether a regulated elevated-parent integration may be enabled. Fail-closed
|
|
197
|
+
* and non-short-circuiting: every clause runs and EVERY reason is accumulated, so the
|
|
198
|
+
* operator sees the complete set of what to fix. `certifiedImmutable` is `true` only
|
|
199
|
+
* when nothing objected — feed it straight into `IntegrationContext` /
|
|
200
|
+
* `AssuranceDecisionContext`.
|
|
201
|
+
*
|
|
202
|
+
* The gate applies to REGULATED tenants only. A non-regulated tenant returns
|
|
203
|
+
* `not-regulated` (this is not its enablement path — see the outside-guarantee posture
|
|
204
|
+
* in ./assurance-decision.ts); it never produces a trivially-true certification.
|
|
205
|
+
*/
|
|
206
|
+
export function decideRegulatedParentCert(
|
|
207
|
+
certification: RegulatedParentCertification | null | undefined,
|
|
208
|
+
context: RegulatedParentCertContext,
|
|
209
|
+
): RegulatedParentCertDecision {
|
|
210
|
+
// The gate is scoped to the regulated surface. Elsewhere, `certifiedImmutable` is
|
|
211
|
+
// not the enablement lever, so this gate declines rather than certifies.
|
|
212
|
+
if (!isRegulated(context.tenant)) {
|
|
213
|
+
return { ok: false, certifiedImmutable: false, denials: [{ reason: "not-regulated" }] };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Disabled by default: absent certification ⇒ disabled (a).
|
|
217
|
+
if (!certification) {
|
|
218
|
+
return { ok: false, certifiedImmutable: false, denials: [{ reason: "certificate-absent" }] };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const denials: RegulatedParentCertDenial[] = [];
|
|
222
|
+
|
|
223
|
+
if (certification.model !== REGULATED_PARENT_CERT_MODEL) {
|
|
224
|
+
denials.push({ reason: "unknown-model", subject: String(certification.model) });
|
|
225
|
+
}
|
|
226
|
+
if (certification.surface !== "regulated") {
|
|
227
|
+
denials.push({ reason: "wrong-surface", subject: certification.surface });
|
|
228
|
+
}
|
|
229
|
+
if (certification.executionTier !== "elevated-parent") {
|
|
230
|
+
denials.push({ reason: "not-elevated-parent", subject: certification.executionTier });
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// (b)/(d) Immutable pinning. Merchant-mutable parent code is never
|
|
234
|
+
// certifiable on regulated — it is rejected, never "certified".
|
|
235
|
+
if (certification.codeForm === "mutable-container") {
|
|
236
|
+
denials.push({ reason: "mutable-parent-prohibited", subject: certification.codeForm });
|
|
237
|
+
} else if (certification.codeForm !== "pinned-immutable") {
|
|
238
|
+
denials.push({ reason: "unpinned", subject: certification.codeForm });
|
|
239
|
+
}
|
|
240
|
+
if (!isPinnedDigest(certification.pinnedDigest)) {
|
|
241
|
+
denials.push({ reason: "unpinned", subject: certification.pinnedDigest || "(absent)" });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// The certificate must cover the integration actually being evaluated.
|
|
245
|
+
if (context.binding) {
|
|
246
|
+
if (context.binding.appId !== certification.appId) {
|
|
247
|
+
denials.push({ reason: "binding-mismatch", subject: certification.appId });
|
|
248
|
+
}
|
|
249
|
+
if (context.binding.version !== certification.version) {
|
|
250
|
+
denials.push({ reason: "binding-mismatch", subject: certification.version });
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// (c) Drift ⇒ auto-disable: the served digest must equal the certified pinned digest.
|
|
255
|
+
if (isCertifiedDigestDrift(certification, context.servedDigest)) {
|
|
256
|
+
denials.push({ reason: "digest-drift", subject: context.servedDigest });
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// (a) Adversarial per-obligation proof of the SERVER-derived required set. First
|
|
260
|
+
// reuse the ext-policy-04 coverage authority (absent / unknown / duplicated /
|
|
261
|
+
// omitted), then require each required obligation carry a withstood, evidence-pinned
|
|
262
|
+
// proof — a mere present-and-known proof is not enough.
|
|
263
|
+
const proofIds = certification.proofs.map((p) => p.obligationId);
|
|
264
|
+
const coverage = verifyObligationCoverage(context.tenant, proofIds);
|
|
265
|
+
if (!coverage.ok) {
|
|
266
|
+
for (const blocker of coverage.blockers) {
|
|
267
|
+
denials.push({ reason: "obligation-coverage", subject: blocker });
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const proofById = new Map<string, ObligationProof>();
|
|
272
|
+
for (const proof of certification.proofs) {
|
|
273
|
+
if (!proofById.has(proof.obligationId)) proofById.set(proof.obligationId, proof);
|
|
274
|
+
}
|
|
275
|
+
const required = resolveObligationIds(context.tenant);
|
|
276
|
+
for (const id of required) {
|
|
277
|
+
const proof = proofById.get(id);
|
|
278
|
+
if (!proof) {
|
|
279
|
+
denials.push({ reason: "obligation-unproven", subject: id });
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (proof.result !== "withstood" || proof.adversarialChallenge.trim().length === 0) {
|
|
283
|
+
denials.push({ reason: "proof-not-adversarial", subject: id });
|
|
284
|
+
}
|
|
285
|
+
if (!isPinnedDigest(proof.evidenceDigest)) {
|
|
286
|
+
denials.push({ reason: "proof-evidence-unpinned", subject: id });
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (denials.length > 0) {
|
|
291
|
+
return { ok: false, certifiedImmutable: false, denials };
|
|
292
|
+
}
|
|
293
|
+
return { ok: true, certifiedImmutable: true, certification, provenObligations: required };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* True iff a regulated elevated-parent integration is enabled by this certification —
|
|
298
|
+
* the boolean `IntegrationContext.certifiedImmutable` / `AssuranceDecisionContext
|
|
299
|
+
* .certifiedImmutable` expect. Disabled by default: any absent, unpinned, drifted, or
|
|
300
|
+
* incompletely-proven certification yields `false`.
|
|
301
|
+
*/
|
|
302
|
+
export function isRegulatedParentCertified(
|
|
303
|
+
certification: RegulatedParentCertification | null | undefined,
|
|
304
|
+
context: RegulatedParentCertContext,
|
|
305
|
+
): boolean {
|
|
306
|
+
return decideRegulatedParentCert(certification, context).certifiedImmutable;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// `isAcceptedProof` is exported for downstream review UIs that render per-proof status
|
|
310
|
+
// without re-deriving the whole decision.
|
|
311
|
+
export { isAcceptedProof };
|