@tokenoftrust/storefront-runner 2.0.2 → 2.0.4

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.
Files changed (254) hide show
  1. package/apps/storefront/.astro/content.d.ts +159 -0
  2. package/apps/storefront/.astro/integrations/_astrojs_cloudflare/cloudflare.d.ts +1 -0
  3. package/apps/storefront/.astro/types.d.ts +3 -0
  4. package/apps/storefront/node_modules/.bin/acorn +43 -0
  5. package/apps/storefront/node_modules/.bin/astro +43 -0
  6. package/apps/storefront/node_modules/.bin/astro-check +43 -0
  7. package/apps/storefront/node_modules/.bin/browserslist +43 -0
  8. package/apps/storefront/node_modules/.bin/cf-wrangler +43 -0
  9. package/apps/storefront/node_modules/.bin/depcruise +43 -0
  10. package/apps/storefront/node_modules/.bin/depcruise-baseline +43 -0
  11. package/apps/storefront/node_modules/.bin/depcruise-fmt +43 -0
  12. package/apps/storefront/node_modules/.bin/depcruise-wrap-stream-in-html +43 -0
  13. package/apps/storefront/node_modules/.bin/dependency-cruise +43 -0
  14. package/apps/storefront/node_modules/.bin/dependency-cruiser +43 -0
  15. package/apps/storefront/node_modules/.bin/drizzle-kit +43 -0
  16. package/apps/storefront/node_modules/.bin/esbuild +43 -0
  17. package/apps/storefront/node_modules/.bin/jiti +43 -0
  18. package/apps/storefront/node_modules/.bin/playwright +43 -0
  19. package/apps/storefront/node_modules/.bin/playwright-core +43 -0
  20. package/apps/storefront/node_modules/.bin/prettier +43 -0
  21. package/apps/storefront/node_modules/.bin/rollup +43 -0
  22. package/apps/storefront/node_modules/.bin/tsc +43 -0
  23. package/apps/storefront/node_modules/.bin/tsserver +43 -0
  24. package/apps/storefront/node_modules/.bin/tsx +43 -0
  25. package/apps/storefront/node_modules/.bin/vite +43 -0
  26. package/apps/storefront/node_modules/.bin/vitest +43 -0
  27. package/apps/storefront/node_modules/.bin/workerd +43 -0
  28. package/apps/storefront/node_modules/.bin/wrangler +43 -0
  29. package/apps/storefront/node_modules/.bin/wrangler2 +43 -0
  30. package/apps/storefront/node_modules/.bin/yaml +43 -0
  31. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js +58 -0
  32. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_client__js.js.map +1 -0
  33. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js +741 -0
  34. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_n_@preact_signals.js.map +1 -0
  35. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js +2549 -0
  36. package/apps/storefront/node_modules/.vite/deps/@astrojs_preact_server__js.js.map +1 -0
  37. package/apps/storefront/node_modules/.vite/deps/_metadata.json +150 -0
  38. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js +434 -0
  39. package/apps/storefront/node_modules/.vite/deps/astro-BIHuWFxm.js.map +1 -0
  40. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js +6390 -0
  41. package/apps/storefront/node_modules/.vite/deps/astro_n_aria-query.js.map +1 -0
  42. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js +2705 -0
  43. package/apps/storefront/node_modules/.vite/deps/astro_n_axobject-query.js.map +1 -0
  44. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js +65 -0
  45. package/apps/storefront/node_modules/.vite/deps/astro_n_html-escaper.js.map +1 -0
  46. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js +455 -0
  47. package/apps/storefront/node_modules/.vite/deps/astro_runtime_client_dev-toolbar_entrypoint__js.js.map +1 -0
  48. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js +1535 -0
  49. package/apps/storefront/node_modules/.vite/deps/audit-DKDpcdEI.js.map +1 -0
  50. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js +48 -0
  51. package/apps/storefront/node_modules/.vite/deps/highlight-B9UoX1wa.js.map +1 -0
  52. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js +42 -0
  53. package/apps/storefront/node_modules/.vite/deps/icons-BWi2wI9n.js.map +1 -0
  54. package/apps/storefront/node_modules/.vite/deps/package.json +3 -0
  55. package/apps/storefront/node_modules/.vite/deps/preact.js +310 -0
  56. package/apps/storefront/node_modules/.vite/deps/preact.js.map +1 -0
  57. package/apps/storefront/node_modules/.vite/deps/preact_compat.js +420 -0
  58. package/apps/storefront/node_modules/.vite/deps/preact_compat.js.map +1 -0
  59. package/apps/storefront/node_modules/.vite/deps/preact_compat_jsx-runtime.js +4 -0
  60. package/apps/storefront/node_modules/.vite/deps/preact_debug.js +236 -0
  61. package/apps/storefront/node_modules/.vite/deps/preact_debug.js.map +1 -0
  62. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js +12 -0
  63. package/apps/storefront/node_modules/.vite/deps/preact_devtools.js.map +1 -0
  64. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js +192 -0
  65. package/apps/storefront/node_modules/.vite/deps/preact_hooks.js.map +1 -0
  66. package/apps/storefront/node_modules/.vite/deps/preact_jsx-dev-runtime.js +3 -0
  67. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js +92 -0
  68. package/apps/storefront/node_modules/.vite/deps/preact_jsx-runtime.js.map +1 -0
  69. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js +59 -0
  70. package/apps/storefront/node_modules/.vite/deps/preact_test-utils.js.map +1 -0
  71. package/apps/storefront/node_modules/.vite/deps/rolldown-runtime-BPOCksWG.js +24 -0
  72. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js +189 -0
  73. package/apps/storefront/node_modules/.vite/deps/settings-BVDV0Roj.js.map +1 -0
  74. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js +13 -0
  75. package/apps/storefront/node_modules/.vite/deps/static-html-DveYqwMo.js.map +1 -0
  76. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js +458 -0
  77. package/apps/storefront/node_modules/.vite/deps/toolbar-B3VIHCXg.js.map +1 -0
  78. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js +1016 -0
  79. package/apps/storefront/node_modules/.vite/deps/ui-library-BK6ricUd.js.map +1 -0
  80. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js +32 -0
  81. package/apps/storefront/node_modules/.vite/deps/window-BvxDenKN.js.map +1 -0
  82. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js +113 -0
  83. package/apps/storefront/node_modules/.vite/deps/xray-YFiRUU0Z.js.map +1 -0
  84. package/apps/storefront/node_modules/.vite/deps_ssr/_metadata.json +8 -0
  85. package/apps/storefront/node_modules/.vite/deps_ssr/package.json +3 -0
  86. package/apps/storefront/src/layouts/Layout.astro +1 -1
  87. package/apps/storefront/src/lib/apps/registryAccess.ts +45 -0
  88. package/apps/storefront/src/lib/auth/adminEntry.ts +4 -4
  89. package/apps/storefront/src/lib/shared/capability.ts +68 -0
  90. package/apps/storefront/src/lib/shared/developerInvites.ts +1250 -0
  91. package/apps/storefront/src/lib/shared/invitePreflight.ts +123 -0
  92. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +38 -4
  93. package/apps/storefront/src/lib/webhooks/emit.ts +1 -1
  94. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +1 -1
  95. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +1 -1
  96. package/apps/storefront/src/pages/index.astro +1 -1
  97. package/apps/storefront/src/pages/products/[handle].astro +1 -1
  98. package/package.json +1 -1
  99. package/packages/public-runtime/node_modules/.bin/tsc +43 -0
  100. package/packages/public-runtime/node_modules/.bin/tsserver +43 -0
  101. package/packages/public-runtime/node_modules/.bin/vitest +43 -0
  102. package/pnpm-workspace.yaml +3 -0
  103. package/scripts/lib/tenant-dirs.mjs +80 -0
  104. package/scripts/tenant/lint-embeds.mjs +180 -0
  105. package/scripts/tenant/materialize-tenant-content.mjs +533 -0
  106. package/scripts/tenant/validate.mjs +837 -0
  107. package/apps/storefront/scripts/build-v84-clean-clone-standalone.mjs +0 -68
  108. package/apps/storefront/scripts/deny-v84-clean-clone-standalone-capabilities.mjs +0 -111
  109. package/apps/storefront/scripts/deny-v84-dogfood-capabilities.mjs +0 -2
  110. package/apps/storefront/scripts/deny-v84-lifecycle-capabilities.mjs +0 -247
  111. package/apps/storefront/scripts/dogfood-v84-nonportable-routes.ts +0 -73
  112. package/apps/storefront/scripts/fixed-v84-native-install-producer.ts +0 -79
  113. package/apps/storefront/scripts/generated/v84-clean-clone-replay.bundle.mjs +0 -10982
  114. package/apps/storefront/scripts/import-v84-through-ws2.ts +0 -38
  115. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling-dependency.ts +0 -2
  116. package/apps/storefront/scripts/initialize-v84-lifecycle-tooling.ts +0 -4
  117. package/apps/storefront/scripts/install-v84-native-through-ws1.ts +0 -185
  118. package/apps/storefront/scripts/prove-clone-isolation.ts +0 -506
  119. package/apps/storefront/scripts/replay-v84-clean-clone-pass.ts +0 -148
  120. package/apps/storefront/scripts/replay-v84-clean-clone-standalone.ts +0 -35
  121. package/apps/storefront/scripts/replay-v84-clean-clone.ts +0 -214
  122. package/apps/storefront/scripts/run-fixed-v84-native-install-producer.mjs +0 -18
  123. package/apps/storefront/scripts/run-v84-clone-isolation-proof.mjs +0 -18
  124. package/apps/storefront/scripts/run-v84-clone-lifecycle-proof.mjs +0 -18
  125. package/apps/storefront/scripts/run-v84-import-through-ws2-proof.mjs +0 -17
  126. package/apps/storefront/scripts/run-v84-nonportable-route-dogfood.mjs +0 -15
  127. package/apps/storefront/scripts/sanitize-v84-proof-environment.mjs +0 -21
  128. package/apps/storefront/scripts/v84CleanCloneGraphPin.ts +0 -42
  129. package/apps/storefront/scripts/v84CleanCloneSourceGraph.ts +0 -145
  130. package/apps/storefront/scripts/validate-v84-clone-lifecycle.ts +0 -18
  131. package/apps/storefront/src/components/admin/AdminAIWorkflowsTab.astro +0 -531
  132. package/apps/storefront/src/components/admin/AdminBlogDetailRail.astro +0 -878
  133. package/apps/storefront/src/components/admin/AdminBlogTab.astro +0 -1435
  134. package/apps/storefront/src/components/admin/AdminCheckoutSettingsPanel.astro +0 -226
  135. package/apps/storefront/src/components/admin/AdminCustomersTab.astro +0 -593
  136. package/apps/storefront/src/components/admin/AdminFulfillmentTab.astro +0 -465
  137. package/apps/storefront/src/components/admin/AdminIntegrationsTab.astro +0 -105
  138. package/apps/storefront/src/components/admin/AdminProductsTab.astro +0 -901
  139. package/apps/storefront/src/components/admin/AdminPublishTab.astro +0 -2983
  140. package/apps/storefront/src/components/admin/AdminReportingTab.astro +0 -435
  141. package/apps/storefront/src/components/admin/AdminSettingsTab.astro +0 -782
  142. package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +0 -611
  143. package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +0 -1223
  144. package/apps/storefront/src/lib/apps/adminService.ts +0 -116
  145. package/apps/storefront/src/lib/apps/adminSession.ts +0 -214
  146. package/apps/storefront/src/lib/apps/apiAuth.ts +0 -91
  147. package/apps/storefront/src/lib/apps/apiRoute.ts +0 -35
  148. package/apps/storefront/src/lib/apps/catalogMapper.ts +0 -39
  149. package/apps/storefront/src/lib/apps/consentConfigStore.ts +0 -228
  150. package/apps/storefront/src/lib/apps/gtmMigrationInstall.ts +0 -447
  151. package/apps/storefront/src/lib/apps/gtmMigrationReport.ts +0 -367
  152. package/apps/storefront/src/lib/apps/gtmMigrationTab.ts +0 -315
  153. package/apps/storefront/src/lib/apps/healthAggregate.ts +0 -66
  154. package/apps/storefront/src/lib/apps/integrationLifecycle.ts +0 -614
  155. package/apps/storefront/src/lib/apps/integrationLifecycleController.ts +0 -125
  156. package/apps/storefront/src/lib/apps/integrationReadModel.ts +0 -563
  157. package/apps/storefront/src/lib/apps/integrationsTab.ts +0 -547
  158. package/apps/storefront/src/lib/apps/integrationsView.ts +0 -106
  159. package/apps/storefront/src/lib/apps/managedAnalyticsService.ts +0 -599
  160. package/apps/storefront/src/lib/apps/tenantIntegrationMaterialization.ts +0 -170
  161. package/apps/storefront/src/lib/apps/tokenIssuer.ts +0 -121
  162. package/apps/storefront/src/lib/apps/tokenVerifier.ts +0 -148
  163. package/apps/storefront/src/lib/apps/v84CleanCloneReplayContext.ts +0 -16
  164. package/apps/storefront/src/lib/apps/v84CloneLifecycleCacheEvidence.ts +0 -105
  165. package/apps/storefront/src/lib/apps/v84CloneLifecycleEvidence.ts +0 -177
  166. package/apps/storefront/src/lib/apps/v84CloneLifecycleMaterialization.ts +0 -251
  167. package/apps/storefront/src/lib/apps/v84CloneLifecycleSemantics.ts +0 -241
  168. package/apps/storefront/src/lib/apps/v84CloneMigrationEvidence.ts +0 -211
  169. package/apps/storefront/src/lib/apps/v84CloneNativeInstallEvidence.ts +0 -409
  170. package/apps/storefront/src/lib/apps/v84NonportableCodeCoaching.ts +0 -368
  171. package/apps/storefront/src/lib/apps/v84NonportableRouteContract.ts +0 -188
  172. package/apps/storefront/src/lib/apps/v84NonportableRouteDogfood.ts +0 -291
  173. package/apps/storefront/src/lib/apps/ws1ManagedAnalyticsClient.ts +0 -95
  174. package/apps/storefront/src/lib/content-edit/applyDiff.ts +0 -91
  175. package/apps/storefront/src/lib/content-edit/client.ts +0 -286
  176. package/apps/storefront/src/lib/content-edit/index.ts +0 -14
  177. package/apps/storefront/src/lib/content-edit/route.ts +0 -25
  178. package/apps/storefront/src/lib/content-edit/types.ts +0 -19
  179. package/apps/storefront/src/lib/dev/cockpitStore.ts +0 -57
  180. package/apps/storefront/src/lib/publish/adminPublishTab.ts +0 -4625
  181. package/apps/storefront/src/lib/publish/apex-readiness.ts +0 -360
  182. package/apps/storefront/src/lib/publish/buildStream.ts +0 -269
  183. package/apps/storefront/src/lib/publish/collapsePersistence.ts +0 -72
  184. package/apps/storefront/src/lib/publish/dispatchHealth.ts +0 -267
  185. package/apps/storefront/src/lib/publish/domainState.ts +0 -360
  186. package/apps/storefront/src/lib/publish/lifecycleState.ts +0 -369
  187. package/apps/storefront/src/lib/publish/liveReleaseSummary.ts +0 -29
  188. package/apps/storefront/src/lib/publish/mergeDoctorOnFailure.ts +0 -78
  189. package/apps/storefront/src/lib/publish/mergeHealthBanner.ts +0 -62
  190. package/apps/storefront/src/lib/publish/mergeRowFindings.ts +0 -124
  191. package/apps/storefront/src/lib/publish/mergeSupportEscalation.ts +0 -75
  192. package/apps/storefront/src/lib/publish/panelPolling.ts +0 -78
  193. package/apps/storefront/src/lib/publish/publish-guard.ts +0 -88
  194. package/apps/storefront/src/lib/publish/publishFormat.ts +0 -165
  195. package/apps/storefront/src/lib/publish/publishHero.ts +0 -177
  196. package/apps/storefront/src/lib/publish/reviewRelease/changeState.ts +0 -66
  197. package/apps/storefront/src/lib/publish/reviewRelease/copy.ts +0 -141
  198. package/apps/storefront/src/lib/publish/reviewRelease/index.ts +0 -41
  199. package/apps/storefront/src/lib/publish/reviewRelease/permissions.ts +0 -47
  200. package/apps/storefront/src/lib/publish/reviewRelease/releaseState.ts +0 -42
  201. package/apps/storefront/src/lib/publish/reviewRelease/types.ts +0 -59
  202. package/apps/storefront/src/lib/publish/reviewRelease/viewModel.ts +0 -440
  203. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/actions.ts +0 -161
  204. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/controller.ts +0 -325
  205. package/apps/storefront/src/lib/publish/reviewReleaseWorkspace/render.ts +0 -241
  206. package/apps/storefront/src/lib/publish/shipWorkspace.ts +0 -439
  207. package/apps/storefront/src/lib/publish/sseRoute.ts +0 -119
  208. package/apps/storefront/src/lib/publish/stageBundleUpload.ts +0 -215
  209. package/apps/storefront/src/lib/publish/streamLogConsole.ts +0 -142
  210. package/apps/storefront/src/lib/publish/versionNumber.ts +0 -52
  211. package/apps/storefront/src/pages/admin/ops-timeline.astro +0 -420
  212. package/apps/storefront/src/pages/admin/review-release.astro +0 -265
  213. package/apps/storefront/src/pages/admin/select.astro +0 -96
  214. package/apps/storefront/src/pages/admin.astro +0 -570
  215. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +0 -86
  216. package/apps/storefront/src/pages/api/apps/admin/gtm-migration-install.ts +0 -174
  217. package/apps/storefront/src/pages/api/apps/admin/gtm-migration.ts +0 -96
  218. package/apps/storefront/src/pages/api/apps/admin/health.ts +0 -44
  219. package/apps/storefront/src/pages/api/apps/admin/install.ts +0 -125
  220. package/apps/storefront/src/pages/api/apps/admin/integration-lifecycle.ts +0 -118
  221. package/apps/storefront/src/pages/api/apps/admin/integrations.ts +0 -89
  222. package/apps/storefront/src/pages/api/apps/admin/list.ts +0 -26
  223. package/apps/storefront/src/pages/api/apps/admin/platform-adapters.ts +0 -85
  224. package/apps/storefront/src/pages/api/apps/admin/resume.ts +0 -79
  225. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +0 -80
  226. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +0 -98
  227. package/apps/storefront/src/pages/api/apps/admin/update.ts +0 -126
  228. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +0 -70
  229. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +0 -51
  230. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +0 -255
  231. package/apps/storefront/src/pages/api/apps/oauth/token.ts +0 -87
  232. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +0 -162
  233. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +0 -39
  234. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +0 -49
  235. package/apps/storefront/src/pages/api/apps/v1/health.ts +0 -32
  236. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +0 -59
  237. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +0 -48
  238. package/apps/storefront/src/pages/api/apps/v1/orders.ts +0 -73
  239. package/apps/storefront/src/pages/api/apps/v1/reports.ts +0 -21
  240. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +0 -74
  241. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +0 -47
  242. package/apps/storefront/src/pages/api/cache-purge.ts +0 -55
  243. package/apps/storefront/src/pages/api/compliance/tamper.ts +0 -88
  244. package/apps/storefront/src/pages/api/content-edit/ai-edit.ts +0 -164
  245. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +0 -154
  246. package/apps/storefront/src/pages/api/dashboard/vendor-search.ts +0 -120
  247. package/apps/storefront/src/pages/api/integrations/v1/[operation].ts +0 -108
  248. package/apps/storefront/src/pages/api/publish/stage-bundle.ts +0 -70
  249. package/apps/storefront/src/pages/api/test/dev-session.ts +0 -133
  250. package/apps/storefront/src/pages/cockpit.astro +0 -1548
  251. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +0 -119
  252. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +0 -105
  253. package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +0 -177
  254. package/apps/storefront/src/pages/dashboard/index.astro +0 -160
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Invite preflight — block a developer invite when the target store's repo isn't
3
+ * provisioned on THIS environment, so a broken connection surfaces to the INVITER
4
+ * at invite time instead of dead-ending later in the invitee's terminal on
5
+ * "Repo <owner>-main is not provisioned".
6
+ *
7
+ * The check is the MCP's credential-free resource-registry read — the SAME read
8
+ * `tenant_checkout` gates on — reached the SAME way the app-binding writer +
9
+ * cli-signin-code seam reach the MCP:
10
+ * GET {origin(MCP_APP_BINDING_URL)}/internal/preview-status?tenant=<appDomain>&app=storefront&tag=main
11
+ * header x-mcp-service-secret: <MCP_SERVICE_SHARED_SECRET>
12
+ * → 200 { owner, app, repo, provisioned, previewReleased } | 400 | 502
13
+ * Contract source of truth: tot-mcp src/server/preview-status-routes.ts. We gate on
14
+ * `provisioned` — the exact precondition `tenant_checkout` needs. We do NOT gate on
15
+ * `previewReleased`: it is (provisioned && preview-approved), which is FALSE for every
16
+ * store that hasn't shipped a preview yet — gating on it would block invites for
17
+ * provisioned-but-unreleased stores whose checkout would actually succeed.
18
+ *
19
+ * FAIL-OPEN: we BLOCK only on a DEFINITIVE not-ready answer (a clean 200 with
20
+ * previewReleased:false). An unconfigured seam (no URL/secret), a non-200, a
21
+ * transport/parse fault, or a timeout is INCONCLUSIVE → allow (log a warning),
22
+ * mirroring recordDeveloperAppBinding's "skipped when not-configured" stance — we
23
+ * never break invites just because the MCP is briefly unreachable.
24
+ */
25
+ import { readEnv } from "@/lib/env";
26
+
27
+ /** How long we'll wait on the preview-status read before failing open. */
28
+ const PREFLIGHT_TIMEOUT_MS = 4000;
29
+
30
+ /** GET transport seam (injectable for tests) — mirrors HttpTransport but read-only. */
31
+ export interface PreflightTransport {
32
+ get(
33
+ url: string,
34
+ headers: Record<string, string>,
35
+ ): Promise<{ status: number; json(): Promise<unknown> }>;
36
+ }
37
+
38
+ /** The real transport: a timeout-bounded GET. */
39
+ export class FetchPreflightTransport implements PreflightTransport {
40
+ async get(url: string, headers: Record<string, string>) {
41
+ const ctrl = new AbortController();
42
+ const timer = setTimeout(() => ctrl.abort(), PREFLIGHT_TIMEOUT_MS);
43
+ try {
44
+ const res = await fetch(url, { method: "GET", headers, signal: ctrl.signal });
45
+ return { status: res.status, json: () => res.json() };
46
+ } finally {
47
+ clearTimeout(timer);
48
+ }
49
+ }
50
+ }
51
+
52
+ /** BLOCK carries the inviter-facing message; ALLOW carries a reason (for logging). */
53
+ export type PreflightDecision =
54
+ | { block: true; reason: "not-provisioned"; message: string }
55
+ | { block: false; reason: string };
56
+
57
+ /** The inviter-facing block message, naming the store. */
58
+ function blockMessage(tenant: string): string {
59
+ return (
60
+ `${tenant} isn't set up on this environment yet — its store repo isn't ` +
61
+ `provisioned. Finish provisioning that store before inviting a developer to it.`
62
+ );
63
+ }
64
+
65
+ /**
66
+ * Preflight `tenant` (the target appDomain). Returns a BLOCK only on a definitive
67
+ * not-provisioned answer; every inconclusive condition allows (fail-open). Never
68
+ * throws.
69
+ */
70
+ export async function checkStorePreviewReady(
71
+ tenant: string,
72
+ deps: { transport?: PreflightTransport } = {},
73
+ ): Promise<PreflightDecision> {
74
+ const bindingUrl = await readEnv("MCP_APP_BINDING_URL");
75
+ const secret = await readEnv("MCP_SERVICE_SHARED_SECRET");
76
+ if (!bindingUrl || !secret) {
77
+ // Seam not configured (the MCP route isn't even mounted without the secret) —
78
+ // inconclusive, so allow rather than break every invite.
79
+ return { block: false, reason: "not-configured" };
80
+ }
81
+
82
+ let endpoint: string;
83
+ try {
84
+ const origin = new URL(bindingUrl).origin;
85
+ const q = new URLSearchParams({ tenant: tenant.toLowerCase(), app: "storefront", tag: "main" });
86
+ // Canonical, namespace-neutral path (the read is generic, not dev-role-scoped).
87
+ endpoint = `${origin}/internal/preview-status?${q.toString()}`;
88
+ } catch {
89
+ return { block: false, reason: "bad-mcp-url" };
90
+ }
91
+
92
+ const transport = deps.transport ?? new FetchPreflightTransport();
93
+ let res: { status: number; json(): Promise<unknown> };
94
+ try {
95
+ res = await transport.get(endpoint, { "x-mcp-service-secret": secret });
96
+ } catch (cause) {
97
+ return {
98
+ block: false,
99
+ reason: "preflight request failed: " + (cause instanceof Error ? cause.message : String(cause)),
100
+ };
101
+ }
102
+
103
+ // Only a clean 200 is a conclusive answer; anything else is inconclusive → allow.
104
+ if (res.status < 200 || res.status >= 300) {
105
+ return { block: false, reason: `preflight returned status ${res.status}` };
106
+ }
107
+ let payload: { provisioned?: unknown; previewReleased?: unknown };
108
+ try {
109
+ payload = (await res.json()) as typeof payload;
110
+ } catch {
111
+ return { block: false, reason: "unparseable preflight response" };
112
+ }
113
+ // Gate on `provisioned` — the ONE precondition the invitee's first `tot start`
114
+ // checkout needs (a missing repo is the austin→sample-store failure). Treat only
115
+ // an explicit `false` as a definitive block; a missing/non-boolean field is
116
+ // inconclusive → allow. NOT `previewReleased` (see the file header): that now means
117
+ // provisioned && preview-approved, which is false for every not-yet-shipped store and
118
+ // would block invites whose checkout actually succeeds.
119
+ if (payload.provisioned === false) {
120
+ return { block: true, reason: "not-provisioned", message: blockMessage(tenant) };
121
+ }
122
+ return { block: false, reason: "provisioned" };
123
+ }
@@ -24,8 +24,6 @@
24
24
  */
25
25
  import { WebhookDeliveryStore, type WebhookDelivery, type WebhookEvent, type WebhookTopic } from "./deliveryStore.js";
26
26
  import type { AppInstall } from "../apps/registryService.js";
27
- import { loadWebhookSigningConfig, type WebhookSigningConfig } from "./webhookSigningKey.js";
28
- import { signWebhookRequest, type SignWebhookRequestResult } from "./signing.js";
29
27
  import { buildCloudEventEnvelope } from "./events.js";
30
28
  import { computeBackoffDelaySeconds, MAX_DELIVERY_ATTEMPTS, type AttemptOutcome } from "./dispatcher.js";
31
29
  import { validateWebhookEndpoint } from "./endpointGuard.js";
@@ -33,6 +31,28 @@ import { validateWebhookEndpoint } from "./endpointGuard.js";
33
31
  /** Bounded per-attempt timeout — a hung receiver must not hold a dispatcher slot open indefinitely. */
34
32
  const DEFAULT_TIMEOUT_MS = 10_000;
35
33
 
34
+ // Both signing.js and webhookSigningKey.js ultimately re-export from
35
+ // @tokenoftrust/private-apps-devkit, a workspace-only package never vendored
36
+ // into the public storefront-runner — a static import of either (type-only
37
+ // included: `tsc` resolves a module for `import type` too) would be a
38
+ // dangling import in that build. The VALUES are loaded via dynamic import at
39
+ // their call sites below (see attemptDelivery/deliverSigned); these two
40
+ // shapes are duplicated here, type-only, so this module's own types don't
41
+ // need those files to exist in the runner. Keep in sync with
42
+ // webhookSigningKey.ts's WebhookSigningConfig and
43
+ // private-apps-devkit/src/signing.ts's SignWebhookRequestResult.
44
+ interface WebhookSigningConfig {
45
+ key: CryptoKey;
46
+ kid: string;
47
+ }
48
+ interface SignWebhookRequestResult {
49
+ headers: {
50
+ "content-digest": string;
51
+ "signature-input": string;
52
+ signature: string;
53
+ };
54
+ }
55
+
36
56
  /** The minimal registry seam the engine needs: resolving the target install by id. Satisfied by `AppRegistryService`. */
37
57
  export interface InstallResolver {
38
58
  get(installId: string): Promise<AppInstall | null>;
@@ -83,7 +103,17 @@ export async function attemptDelivery(deliveryId: string, deps: AttemptDeliveryD
83
103
  const now = deps.now ?? (() => new Date());
84
104
  const timeoutMs = deps.timeoutMs ?? DEFAULT_TIMEOUT_MS;
85
105
  const fetchImpl = deps.fetchImpl ?? fetch;
86
- const loadSigningConfig = deps.loadSigningConfig ?? loadWebhookSigningConfig;
106
+ const loadSigningConfig =
107
+ deps.loadSigningConfig ??
108
+ (async () => {
109
+ // Absent from the public storefront-runner (dropped; see WebhookSigningConfig above).
110
+ // ts-ignore, not ts-expect-error: this module DOES exist in the full repo's own
111
+ // typecheck (only missing from the runner-extracted tree), so there's no error to
112
+ // "expect" here in that context — ts-expect-error would itself error as unused.
113
+ // @ts-ignore
114
+ const mod = await import("./webhookSigningKey.js");
115
+ return mod.loadWebhookSigningConfig() as Promise<WebhookSigningConfig>;
116
+ });
87
117
 
88
118
  const delivery = await store.getDelivery(deliveryId);
89
119
  if (!delivery) {
@@ -172,7 +202,11 @@ async function deliverSigned(
172
202
  now: ctx.now,
173
203
  });
174
204
  const body = JSON.stringify(envelope);
175
- const { headers } = await signWebhookRequest({
205
+ // Absent from the public storefront-runner (dropped; see SignWebhookRequestResult above);
206
+ // ts-ignore not ts-expect-error, same reasoning as loadSigningConfig above.
207
+ // @ts-ignore
208
+ const { signWebhookRequest } = await import("./signing.js");
209
+ const { headers }: SignWebhookRequestResult = await signWebhookRequest({
176
210
  method: "POST",
177
211
  url: install.webhookEndpoint!,
178
212
  body,
@@ -48,7 +48,7 @@
48
48
  import { WebhookDeliveryStore, type WebhookTopic } from "./deliveryStore.js";
49
49
  import { TOPIC_REQUIRED_SCOPE } from "./events.js";
50
50
  import { getDispatcher } from "./getDispatcher.js";
51
- import { getRegistryService } from "../apps/adminService.js";
51
+ import { getRegistryService } from "../apps/registryAccess.js";
52
52
  import type { AppInstall } from "../apps/registryService.js";
53
53
  import { fromD1 } from "../d1/catalog.js";
54
54
  import { readD1 } from "../env.js";
@@ -21,7 +21,7 @@ import { CloudflareQueueDispatcher, type CloudflareQueueSender } from "./cloudfl
21
21
  import { AppRegistryService } from "../apps/registryService.js";
22
22
  import { D1CredentialStore } from "../apps/credentials.js";
23
23
  import { DEFAULT_APP_GATEWAY_KID, APP_GATEWAY_KID_ENV } from "../apps/gatewayKeys.js";
24
- import { json } from "../apps/adminService.js";
24
+ import { json } from "../apps/registryAccess.js";
25
25
  import { fromD1 } from "../d1/catalog.js";
26
26
  import { readD1, readEnv, readQueue } from "../env.js";
27
27
 
@@ -25,7 +25,7 @@ import {
25
25
  } from "@/lib/auth/identityToken";
26
26
  import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
27
27
  import { normalizeEmailHint } from "@/lib/privacy/emailHint";
28
- import { acceptDeveloperInvite } from "@/lib/grants/developerInvites";
28
+ import { acceptDeveloperInvite } from "@/lib/shared/developerInvites";
29
29
 
30
30
  export const prerender = false;
31
31
 
@@ -17,7 +17,7 @@ import TierCards from "@/components/membership/TierCards.astro";
17
17
  import { withBase } from "@/lib/basePath";
18
18
  import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
19
19
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
20
- import { getRegistryService } from "@/lib/apps/adminService.js";
20
+ import { getRegistryService } from "@/lib/apps/registryAccess.js";
21
21
  import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
22
22
  import type { CatalogCollection, CatalogProduct } from "@tot/public-runtime";
23
23
  import {
@@ -42,7 +42,7 @@ import { productLd, breadcrumbLd, reviewLd } from "@/lib/jsonld";
42
42
  import { isUsableImageUrl } from "@/lib/placeholder";
43
43
  import { withBase } from "@/lib/basePath";
44
44
  import AppWidgetFrame from "@/components/apps/AppWidgetFrame.astro";
45
- import { getRegistryService } from "@/lib/apps/adminService.js";
45
+ import { getRegistryService } from "@/lib/apps/registryAccess.js";
46
46
  import { renderWidgetSlot } from "@/lib/apps/widgets/renderSlot.js";
47
47
 
48
48
  const { handle } = Astro.params;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsc" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../typescript/bin/tsc" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../typescript/bin/tsc" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../typescript/bin/tsc" "$@"
40
+ else
41
+ exec node "$basedir/../typescript/bin/tsc" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/typescript/bin/tsc
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/typescript@5.9.3/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../typescript/bin/tsserver" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../typescript/bin/tsserver" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../typescript/bin/tsserver" "$@"
40
+ else
41
+ exec node "$basedir/../typescript/bin/tsserver" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/typescript/bin/tsserver
@@ -0,0 +1,43 @@
1
+ #!/bin/sh
2
+ basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')")
3
+ basedir_win="$basedir"
4
+ exe=""
5
+ msys=""
6
+
7
+ case `uname -a` in
8
+ *CYGWIN*|*MINGW*|*MSYS*)
9
+ if command -v cygpath > /dev/null 2>&1; then
10
+ basedir_win=`cygpath -w "$basedir"`
11
+ fi
12
+ exe=".exe"
13
+ msys="true"
14
+ ;;
15
+ *WSL2*)
16
+ if command -v wslpath > /dev/null 2>&1; then
17
+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)"
18
+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then
19
+ basedir_win="$basedir"
20
+ else
21
+ exe=".exe"
22
+ fi
23
+ fi
24
+ ;;
25
+ esac
26
+
27
+ if [ -z "$NODE_PATH" ]; then
28
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.1_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.1_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules"
29
+ else
30
+ export NODE_PATH="/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.1_lightningcss@1.33.0/node_modules/vitest/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/vitest@2.1.8_@types+node@26.4.1_happy-dom@20.13.1_lightningcss@1.33.0/node_modules:/home/runner/work/storefront/storefront/storefront-runner-dist/node_modules/.pnpm/node_modules:$NODE_PATH"
31
+ fi
32
+ if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then
33
+ exec "$basedir/node.exe" "$basedir_win/../vitest/vitest.mjs" "$@"
34
+ elif [ -x "$basedir/node" ]; then
35
+ exec "$basedir/node" "$basedir/../vitest/vitest.mjs" "$@"
36
+ elif command -v node >/dev/null 2>&1; then
37
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
38
+ elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then
39
+ exec node.exe "$basedir_win/../vitest/vitest.mjs" "$@"
40
+ else
41
+ exec node "$basedir/../vitest/vitest.mjs" "$@"
42
+ fi
43
+ # cmd-shim-target=/home/runner/work/storefront/storefront/storefront-runner-dist/packages/public-runtime/node_modules/vitest/vitest.mjs
@@ -1,6 +1,9 @@
1
1
  packages:
2
2
  - "apps/*"
3
3
  - "packages/*"
4
+ allowBuilds:
5
+ esbuild: set this to true or false
6
+ workerd: set this to true or false
4
7
  linkWorkspacePackages: true
5
8
  onlyBuiltDependencies:
6
9
  - esbuild
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Where a tenant's colocated directory lives — the NODE-side copy.
3
+ *
4
+ * A tenant IS a directory. Real tenants sit directly under `tenants/<id>/`;
5
+ * PLATFORM TEST tenants (every id ending `.e2e.test`) sit one level deeper,
6
+ * under `tenants/e2e/<id>/`.
7
+ *
8
+ * `@tot/public-runtime` owns this contract for the Worker/TS side
9
+ * (`packages/public-runtime/src/tenant.ts`), but it is TypeScript source with no
10
+ * build output, so the `.mjs` build/dev/tenant scripts cannot import it. This is
11
+ * that same total mapping, byte-for-byte in behavior, and
12
+ * `packages/public-runtime/tests/tenant-dirs-parity.test.ts` fails the build if
13
+ * the two ever disagree on any id.
14
+ */
15
+
16
+ /** The directory segment platform test tenants are nested under. */
17
+ export const E2E_TENANT_DIR = "e2e";
18
+
19
+ /** True for a PLATFORM TEST tenant id — the `<scenario>.e2e.test` class. */
20
+ export function isE2ETenantId(tenantId) {
21
+ return /\.e2e\.test$/.test(tenantId);
22
+ }
23
+
24
+ /** Repo-relative, POSIX-separated directory for a tenant id. */
25
+ export function tenantDirRelative(tenantId) {
26
+ return isE2ETenantId(tenantId)
27
+ ? `tenants/${E2E_TENANT_DIR}/${tenantId}`
28
+ : `tenants/${tenantId}`;
29
+ }
30
+
31
+ /** The `/tenants/…/<id>/` substring identifying one tenant inside a path. */
32
+ export function tenantDirSegment(tenantId) {
33
+ return `/${tenantDirRelative(tenantId)}/`;
34
+ }
35
+
36
+ /** The tenant id a `…/tenants/[e2e/]<id>/…` path encodes, or null. */
37
+ export function tenantIdFromPath(path) {
38
+ return /\/tenants\/(?:e2e\/)?([^/]+)\//.exec(path)?.[1] ?? null;
39
+ }
40
+
41
+ /**
42
+ * Narrow a set of tenant ids to what a generalized listing should show —
43
+ * platform test tenants are dropped unless explicitly requested.
44
+ */
45
+ export function listedTenantIds(ids, opts = {}) {
46
+ return opts.includeTestTenants ? [...ids] : ids.filter((id) => !isE2ETenantId(id));
47
+ }
48
+
49
+ /**
50
+ * Path segments (relative to the repo root) for a tenant's own directory —
51
+ * ready for `join(repoRoot, ...tenantDirSegments(id))`.
52
+ */
53
+ export function tenantDirSegments(tenantId) {
54
+ return tenantDirRelative(tenantId).split("/");
55
+ }
56
+
57
+ /**
58
+ * Every tenant id with a directory under `tenants/`, real and test, from one
59
+ * `readdir`-shaped view of the tree. `readdirSync` is passed in so the walk
60
+ * stays testable without a filesystem.
61
+ *
62
+ * @param tenantsDir absolute path to the repo's `tenants/` directory
63
+ * @param readdir `(dir) => string[]` listing a directory's entry names
64
+ * @param isDir `(path) => boolean`
65
+ * @param join path join
66
+ */
67
+ export function listTenantDirIds(tenantsDir, readdir, isDir, join) {
68
+ const ids = [];
69
+ for (const entry of readdir(tenantsDir)) {
70
+ if (!isDir(join(tenantsDir, entry))) continue;
71
+ if (entry === E2E_TENANT_DIR) {
72
+ for (const nested of readdir(join(tenantsDir, entry))) {
73
+ if (isDir(join(tenantsDir, entry, nested))) ids.push(nested);
74
+ }
75
+ continue;
76
+ }
77
+ ids.push(entry);
78
+ }
79
+ return ids;
80
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * Dev-loop lint for third-party embeds (shift-left for the CSP allowlist).
3
+ *
4
+ * A strict CSP silently blocks any external <script>/<iframe> a tenant page
5
+ * embeds unless the origin is allowlisted (see packages/public-runtime/src/csp.ts
6
+ * — EMBED_PROVIDERS + embedCspAdditions). A blocked embed produces no build
7
+ * error and no page error — just a blank spot — so historically the only way it
8
+ * surfaced was a human noticing and escalating to the platform team.
9
+ *
10
+ * This lint closes that gap at author/preview time: it scans a tenant's HTML for
11
+ * external embed origins and tells the developer, right where they're working:
12
+ * - KNOWN provider present → make sure the tenant opts the slug into `embeds`
13
+ * (if we know the opt-in, hard-fail when it's missing).
14
+ * - UNKNOWN external origin → it will be blocked; a platform catalog entry is
15
+ * needed (EMBED_PROVIDERS), not a raw-origin config edit.
16
+ *
17
+ * Source of truth for the catalog is EMBED_PROVIDERS in TS; KNOWN_EMBEDS below
18
+ * mirrors it for dependency-free `node` execution and a vitest parity test
19
+ * (lint-embeds.test.mjs) fails if the two drift.
20
+ *
21
+ * Usage:
22
+ * node scripts/tenant/lint-embeds.mjs --dir tenants/<id>/content [--declared pipedrive,calendly]
23
+ * node scripts/tenant/lint-embeds.mjs --dir <tenant-repo>/content
24
+ * Exit non-zero when a blocking finding exists (undeclared known provider given
25
+ * a known opt-in, or an unknown external origin) so `tot preview` / CI can gate.
26
+ */
27
+ import { readdirSync, readFileSync, statSync } from "node:fs";
28
+ import { join, extname } from "node:path";
29
+ import { EMBED_PROVIDERS } from "../../packages/public-runtime/src/embed-catalog.mjs";
30
+
31
+ /**
32
+ * The embed catalog, flattened for the lint from the shared source of truth
33
+ * (packages/public-runtime/src/embed-catalog.mjs — imported directly, no mirror).
34
+ * `markers` = substrings meaning the embed is on a page; `origins` = the union
35
+ * of the provider's exact CSP hosts.
36
+ */
37
+ export const KNOWN_EMBEDS = Object.entries(EMBED_PROVIDERS).map(([slug, p]) => ({
38
+ slug,
39
+ label: p.label,
40
+ markers: p.markers,
41
+ origins: [...new Set([...(p.scriptSrc ?? []), ...(p.connectSrc ?? []), ...(p.frameSrc ?? [])])],
42
+ }));
43
+
44
+ /** Hosts the platform always allows (self is implicit); embeds to these are fine. */
45
+ const ALWAYS_ALLOWED_HOSTS = new Set([
46
+ "static.cloudflareinsights.com",
47
+ "cloudflareinsights.com",
48
+ ]);
49
+
50
+ // Only <script src> and <iframe src> load executable/frameable content the CSP
51
+ // gates (script-src / frame-src). We deliberately DON'T scan generic URLs
52
+ // (canonical links, JSON-LD @context, <a href>, og:url) — those aren't embeds
53
+ // and would be noisy false positives. Provider-specific embeds that mount via a
54
+ // data-attribute (e.g. Pipedrive's data-pd-webforms) are caught by their catalog
55
+ // marker instead, so they're covered without a generic URL sweep.
56
+ const SRC_RE = /<(?:script|iframe)\b[^>]*\bsrc\s*=\s*["']([^"']+)["']/gi;
57
+
58
+ /** Origin (host) of an absolute URL, or null for relative/self URLs. */
59
+ function originOf(u) {
60
+ try {
61
+ return new URL(u).origin;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ /**
68
+ * Classify one HTML document's embeds.
69
+ * @returns {{ knownSlugs: string[], externalOrigins: string[], unknownOrigins: string[] }}
70
+ */
71
+ export function scanHtmlForEmbeds(html) {
72
+ const knownSlugs = new Set();
73
+ for (const { slug, markers } of KNOWN_EMBEDS) {
74
+ if (markers.some((m) => html.includes(m))) knownSlugs.add(slug);
75
+ }
76
+ const external = new Set();
77
+ SRC_RE.lastIndex = 0;
78
+ let m;
79
+ while ((m = SRC_RE.exec(html))) {
80
+ const origin = originOf(m[1]);
81
+ if (!origin) continue;
82
+ const host = new URL(origin).hostname;
83
+ if (ALWAYS_ALLOWED_HOSTS.has(host)) continue;
84
+ external.add(origin);
85
+ }
86
+ // An external origin is "unknown" if no catalog provider claims it.
87
+ const catalogOrigins = new Set(KNOWN_EMBEDS.flatMap((p) => p.origins));
88
+ const unknown = [...external].filter((o) => !catalogOrigins.has(o));
89
+ return {
90
+ knownSlugs: [...knownSlugs],
91
+ externalOrigins: [...external],
92
+ unknownOrigins: unknown,
93
+ };
94
+ }
95
+
96
+ function walkHtml(dir) {
97
+ /** @type {string[]} */
98
+ const out = [];
99
+ for (const entry of readdirSync(dir)) {
100
+ const p = join(dir, entry);
101
+ const st = statSync(p);
102
+ if (st.isDirectory()) out.push(...walkHtml(p));
103
+ else if (extname(p) === ".html") out.push(p);
104
+ }
105
+ return out;
106
+ }
107
+
108
+ /**
109
+ * Lint every HTML file under `dir`.
110
+ * @param {{ dir: string, declared?: string[] | null }} opts
111
+ * `declared` = the tenant's opted-in slugs if known (platform CI); when null,
112
+ * findings for known providers are advisory (we can't prove the opt-in here).
113
+ * @returns {{ findings: Array<{level:'error'|'warn'|'info', file:string, message:string}>, blocking: boolean }}
114
+ */
115
+ export function lintTenantEmbeds({ dir, declared = null }) {
116
+ /** @type {Array<{level:'error'|'warn'|'info', file:string, message:string}>} */
117
+ const findings = [];
118
+ const declaredSet = declared ? new Set(declared) : null;
119
+ for (const file of walkHtml(dir)) {
120
+ const html = readFileSync(file, "utf8");
121
+ const { knownSlugs, unknownOrigins } = scanHtmlForEmbeds(html);
122
+ for (const slug of knownSlugs) {
123
+ const label = KNOWN_EMBEDS.find((p) => p.slug === slug)?.label ?? slug;
124
+ if (declaredSet && !declaredSet.has(slug)) {
125
+ findings.push({
126
+ level: "error",
127
+ file,
128
+ message: `embeds ${label} but the tenant has not opted "${slug}" into its embeds allowlist — the CSP will block it. Add "${slug}" to the tenant's embeds config.`,
129
+ });
130
+ } else if (!declaredSet) {
131
+ findings.push({
132
+ level: "info",
133
+ file,
134
+ message: `embeds ${label}: ensure "${slug}" is in the tenant's embeds allowlist, or the CSP will block it.`,
135
+ });
136
+ }
137
+ }
138
+ for (const origin of unknownOrigins) {
139
+ findings.push({
140
+ level: "error",
141
+ file,
142
+ message: `embeds ${origin}, which is not a known embed provider — the CSP will block it. A platform catalog entry (EMBED_PROVIDERS) is required, or remove the embed.`,
143
+ });
144
+ }
145
+ }
146
+ return { findings, blocking: findings.some((f) => f.level === "error") };
147
+ }
148
+
149
+ function parseArgs(argv) {
150
+ const args = { dir: null, declared: null };
151
+ for (let i = 0; i < argv.length; i++) {
152
+ if (argv[i] === "--dir") args.dir = argv[++i];
153
+ else if (argv[i] === "--declared") args.declared = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
154
+ }
155
+ return args;
156
+ }
157
+
158
+ function main() {
159
+ const { dir, declared } = parseArgs(process.argv.slice(2));
160
+ if (!dir) {
161
+ console.error("usage: node scripts/tenant/lint-embeds.mjs --dir <content-dir> [--declared slug,slug]");
162
+ process.exit(2);
163
+ }
164
+ const { findings, blocking } = lintTenantEmbeds({ dir, declared });
165
+ if (findings.length === 0) {
166
+ console.log("embed lint: no external embeds found (or all allowlisted).");
167
+ return;
168
+ }
169
+ for (const f of findings) {
170
+ const tag = f.level === "error" ? "BLOCKED" : f.level === "warn" ? "WARN" : "note";
171
+ console.log(`[embed-lint:${tag}] ${f.file}\n ${f.message}`);
172
+ }
173
+ if (blocking) {
174
+ console.error(`\nembed lint: ${findings.filter((f) => f.level === "error").length} embed(s) will be blocked by CSP — see above.`);
175
+ process.exit(1);
176
+ }
177
+ }
178
+
179
+ // Only run main when invoked directly, not when imported by the parity test.
180
+ if (import.meta.url === `file://${process.argv[1]}`) main();