@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.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.
Files changed (246) hide show
  1. package/apps/storefront/astro.config.mjs +15 -0
  2. package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
  3. package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
  4. package/apps/storefront/drizzle.config.apps.ts +13 -0
  5. package/apps/storefront/env.d.ts +10 -0
  6. package/apps/storefront/migrations/README.md +13 -7
  7. package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
  8. package/apps/storefront/migrations-apps/0001_woozy_lyja.sql +22 -0
  9. package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
  10. package/apps/storefront/migrations-apps/meta/0001_snapshot.json +990 -0
  11. package/apps/storefront/migrations-apps/meta/_journal.json +20 -0
  12. package/apps/storefront/package.json +12 -1
  13. package/apps/storefront/perf/README.md +64 -0
  14. package/apps/storefront/perf/assert-budgets.ts +159 -0
  15. package/apps/storefront/playwright.config.ts +23 -0
  16. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  17. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  18. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  19. package/apps/storefront/src/components/ProductCard.astro +1 -0
  20. package/apps/storefront/src/components/admin/AdminPublishTab.astro +3562 -0
  21. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  22. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  23. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  24. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  25. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  26. package/apps/storefront/src/components/content/Callout.astro +75 -0
  27. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  28. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  29. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  30. package/apps/storefront/src/components/content/RichText.astro +44 -0
  31. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  32. package/apps/storefront/src/components/home/Hero.astro +14 -0
  33. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  34. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  35. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  36. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  37. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  38. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  39. package/apps/storefront/src/config/storeName.ts +34 -0
  40. package/apps/storefront/src/layouts/Layout.astro +97 -23
  41. package/apps/storefront/src/lib/activity/alerts.ts +428 -0
  42. package/apps/storefront/src/lib/activity/changeActorAttribution.ts +85 -0
  43. package/apps/storefront/src/lib/activity/deployVersion.ts +127 -0
  44. package/apps/storefront/src/lib/activity/ingest.ts +188 -0
  45. package/apps/storefront/src/lib/activity/ingestAuth.ts +105 -0
  46. package/apps/storefront/src/lib/activity/killSwitch.ts +80 -0
  47. package/apps/storefront/src/lib/activity/query.ts +403 -0
  48. package/apps/storefront/src/lib/activity/recordActivity.ts +105 -0
  49. package/apps/storefront/src/lib/activity/store.ts +122 -0
  50. package/apps/storefront/src/lib/activity/uiActor.ts +150 -0
  51. package/apps/storefront/src/lib/activity/workerCommit.ts +70 -0
  52. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  53. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  54. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  55. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  56. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  57. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  58. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  59. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  60. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  61. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  62. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  63. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  64. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  65. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  66. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  67. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  68. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  69. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  70. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  71. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  72. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  73. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  74. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  75. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  76. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  77. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  78. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  79. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  80. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  81. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  82. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  83. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  84. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +219 -0
  85. package/apps/storefront/src/lib/auth/route.ts +60 -5
  86. package/apps/storefront/src/lib/auth/session.ts +8 -0
  87. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  88. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  89. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  90. package/apps/storefront/src/lib/blog/types.ts +26 -0
  91. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  92. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  93. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  94. package/apps/storefront/src/lib/content/callout.ts +78 -0
  95. package/apps/storefront/src/lib/content/index.ts +27 -0
  96. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  97. package/apps/storefront/src/lib/content/prose.ts +186 -0
  98. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  99. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  100. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  101. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  102. package/apps/storefront/src/lib/d1/schema-apps.ts +267 -0
  103. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  104. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  105. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  106. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  107. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  108. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  109. package/apps/storefront/src/lib/dev/vcBinding.ts +80 -0
  110. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  111. package/apps/storefront/src/lib/env.ts +63 -0
  112. package/apps/storefront/src/lib/jsonld.ts +12 -17
  113. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  114. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  115. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  116. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  117. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  118. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  119. package/apps/storefront/src/lib/publish/shipWorkspace.ts +369 -0
  120. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  121. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  122. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  123. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  124. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  125. package/apps/storefront/src/lib/tot/query.ts +32 -0
  126. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  127. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  128. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  129. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  130. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  131. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  132. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  133. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  134. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  135. package/apps/storefront/src/middleware/index.ts +45 -13
  136. package/apps/storefront/src/pages/404.astro +21 -9
  137. package/apps/storefront/src/pages/[...slug].astro +36 -2
  138. package/apps/storefront/src/pages/admin/ops-timeline.astro +420 -0
  139. package/apps/storefront/src/pages/admin.astro +183 -9
  140. package/apps/storefront/src/pages/api/activity.ts +137 -0
  141. package/apps/storefront/src/pages/api/admin/activity-alerts.ts +73 -0
  142. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +86 -0
  143. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  144. package/apps/storefront/src/pages/api/apps/admin/install.ts +125 -0
  145. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  146. package/apps/storefront/src/pages/api/apps/admin/resume.ts +79 -0
  147. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +80 -0
  148. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +98 -0
  149. package/apps/storefront/src/pages/api/apps/admin/update.ts +126 -0
  150. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +70 -0
  151. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  152. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +183 -0
  153. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  154. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +162 -0
  155. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  156. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  157. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  158. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  159. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  160. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  161. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  162. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +74 -0
  163. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  164. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  165. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  166. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  167. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  168. package/apps/storefront/src/pages/api/cache-purge.ts +11 -0
  169. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  170. package/apps/storefront/src/pages/api/dashboard/enter-vendor.ts +30 -0
  171. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  172. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  173. package/apps/storefront/src/pages/auth/login.astro +202 -45
  174. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  175. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  176. package/apps/storefront/src/pages/blog/index.astro +98 -0
  177. package/apps/storefront/src/pages/capabilities.astro +8 -0
  178. package/apps/storefront/src/pages/cockpit.astro +470 -69
  179. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  180. package/apps/storefront/src/pages/collections/index.astro +10 -2
  181. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  182. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  183. package/apps/storefront/src/pages/index.astro +57 -0
  184. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  185. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  186. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  187. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  188. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  189. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  190. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  191. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  192. package/apps/storefront/src/styles/fonts.css +54 -0
  193. package/apps/storefront/src/styles/global.css +22 -2
  194. package/apps/storefront/src/themes/schema.ts +3 -25
  195. package/apps/storefront/tsconfig.json +1 -1
  196. package/apps/storefront/vitest.config.ts +4 -1
  197. package/package.json +1 -1
  198. package/packages/public-runtime/src/activity/README.md +146 -0
  199. package/packages/public-runtime/src/activity/catalog.ts +501 -0
  200. package/packages/public-runtime/src/activity/event.ts +168 -0
  201. package/packages/public-runtime/src/activity/index.ts +16 -0
  202. package/packages/public-runtime/src/activity/redaction.ts +263 -0
  203. package/packages/public-runtime/src/candidate-index.ts +324 -0
  204. package/packages/public-runtime/src/checkout.ts +94 -2
  205. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  206. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  207. package/packages/public-runtime/src/compliance/index.ts +22 -0
  208. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  209. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  210. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  211. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  212. package/packages/public-runtime/src/csp.ts +27 -3
  213. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  214. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  215. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  216. package/packages/public-runtime/src/extension-contract.ts +2 -1
  217. package/packages/public-runtime/src/hash.ts +25 -0
  218. package/packages/public-runtime/src/index.ts +12 -0
  219. package/packages/public-runtime/src/membership.ts +353 -0
  220. package/packages/public-runtime/src/product.ts +24 -2
  221. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  222. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  223. package/packages/public-runtime/src/tenant.ts +236 -0
  224. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  225. package/scripts/dev/publish.mjs +158 -0
  226. package/scripts/dev/transient-files.mjs +2 -1
  227. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  228. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  229. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  230. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  231. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  232. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  233. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  234. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  235. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  236. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  237. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  238. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  239. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  240. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  241. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  242. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  243. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  244. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  245. package/tenants/home/public/pages/storefront.css +23 -0
  246. package/apps/storefront/src/lib/dev/hostedCockpit.ts +0 -169
@@ -13,7 +13,7 @@
13
13
  * SHELL CONTRACT — every AdminXTab.astro component mounted below must preserve:
14
14
  * - A `<section id="{tab}" data-admin-panel data-agent-scope="{tab}">` root,
15
15
  * where {tab} is one of: orders, products, subscriptions, customers, fulfillment,
16
- * reporting, ai-workflows, settings. The shell's hash router shows/hides
16
+ * reporting, ai-workflows, publish, settings. The shell's hash router shows/hides
17
17
  * panels by matching `html[data-admin-tab]` to this id, and the AI Ask
18
18
  * envelope reads `data-agent-scope` to label its active context.
19
19
  * - No tab-switching chrome of its own — `#{tab}` deep links and the shell's
@@ -37,9 +37,11 @@ import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
37
37
  import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
38
38
  import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
39
39
  import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
40
+ import AdminPublishTab from "../components/admin/AdminPublishTab.astro";
40
41
  import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
41
42
  import { readViewerSession } from "@/lib/auth/route";
42
43
  import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
44
+ import { resolveAdminEntry } from "@/lib/auth/adminEntry";
43
45
 
44
46
  const { tenant, basePath, cspNonce } = Astro.locals;
45
47
  const adminPath = `${basePath || ""}/admin`;
@@ -49,6 +51,19 @@ const viewerEmail = (Astro.locals.viewer?.email ?? viewerSession?.email ?? "").t
49
51
  const viewerDomain = viewerEmail.split("@").at(-1) ?? "";
50
52
  const canViewInternalGuide = viewerDomain === "tokenoftrust.com";
51
53
 
54
+ // dg6 — admin session ENTRY resolved SERVER-SIDE against the resolved tenant's
55
+ // appDomain (never client input, never a bearer secret): an authenticated
56
+ // owner/team member for THIS tenant reaches the admin context; a non-member /
57
+ // anonymous visitor was already held at the sign-in gate (middleware). Consumes
58
+ // u10's owner + ship-capability model; the go-live tabs (AdminPublishTab) read
59
+ // the same host-bound `Astro.locals.viewer.capability`, so the shell just stamps
60
+ // the authoritative principal for chrome + tests — it does not re-gate.
61
+ const adminEntry = resolveAdminEntry({
62
+ record: viewerSession,
63
+ hostResource: tenant.appDomain,
64
+ nowSeconds: Math.floor(Date.now() / 1000),
65
+ });
66
+
52
67
  // Internal admin-app tenant mode (baseline doc §Admin App As Tenant,
53
68
  // tenant-envelope-contract): every mounted tab receives an explicit
54
69
  // { adminAppTenant, targetTenant } envelope. Only a real ToT-staff session
@@ -72,6 +87,11 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
72
87
  data-admin-target-tenant={tenantEnvelope.targetTenant}
73
88
  data-admin-operator={tenantEnvelope.operator ?? ""}
74
89
  data-admin-is-staff={isStaff ? "true" : "false"}
90
+ data-admin-viewer-authenticated={viewerSession ? "true" : "false"}
91
+ data-admin-entry-principal={adminEntry.principal}
92
+ data-admin-is-owner={adminEntry.isOwnerViewer ? "true" : "false"}
93
+ data-admin-can-ship={adminEntry.shipCapabilities.shipOnBehalf ? "true" : "false"}
94
+ data-admin-can-apex-cutover={adminEntry.shipCapabilities.apexCutover ? "true" : "false"}
75
95
  >
76
96
  <head>
77
97
  <meta charset="utf-8" />
@@ -80,10 +100,12 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
80
100
  <title>{tenantLabel} Admin</title>
81
101
  <script is:inline nonce={cspNonce}>
82
102
  (() => {
83
- const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
103
+ const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
84
104
  const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
85
105
  const defaultTab = canViewInternalGuide ? "orders" : "settings";
86
- const hash = window.location.hash.slice(1);
106
+ // A deep link may carry a query suffix in the hash (e.g. #publish?pr=42);
107
+ // the tab id is only the part before "?".
108
+ const hash = window.location.hash.slice(1).split("?")[0];
87
109
  const activeTab = adminTabs.includes(hash) ? hash : defaultTab;
88
110
  const mode = canViewInternalGuide && hash && !adminTabs.includes(hash) ? "guide" : "work-area";
89
111
 
@@ -232,6 +254,100 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
232
254
  background: rgba(255, 255, 255, 0.06);
233
255
  }
234
256
 
257
+ /* Cross-surface hop to the developer cockpit — same card language as the
258
+ sidebar's other blocks, kept visually quieter than the nav tabs. */
259
+ .cockpit-link {
260
+ display: flex;
261
+ align-items: center;
262
+ gap: 8px;
263
+ padding: 10px 12px;
264
+ border: 1px solid rgba(255, 255, 255, 0.12);
265
+ border-radius: var(--admin-radius);
266
+ background: rgba(255, 255, 255, 0.04);
267
+ color: rgba(238, 247, 245, 0.85);
268
+ font-size: 13px;
269
+ text-decoration: none;
270
+ }
271
+ .cockpit-link:hover {
272
+ background: rgba(255, 255, 255, 0.1);
273
+ color: #fff;
274
+ }
275
+ .cockpit-link svg {
276
+ width: 15px;
277
+ height: 15px;
278
+ stroke: currentColor;
279
+ fill: none;
280
+ }
281
+
282
+ /* Signed-in-as chip (identity visibility — Trello 13075 postscript: an
283
+ owner staring at a read-only Publish tab could not tell WHO the shell
284
+ thought they were). Mirrors .store's card language; the standing line
285
+ states the resolved principal FOR THIS STORE, not a global role. */
286
+ .account {
287
+ display: grid;
288
+ grid-template-columns: auto 1fr;
289
+ gap: 10px;
290
+ align-items: center;
291
+ padding: 12px;
292
+ border: 1px solid rgba(255, 255, 255, 0.12);
293
+ border-radius: var(--admin-radius);
294
+ background: rgba(255, 255, 255, 0.06);
295
+ /* Pinned to the sidebar's lower-left (the conventional account slot) —
296
+ the sidebar is a flex column, so this soaks the remaining space. */
297
+ margin-top: auto;
298
+ }
299
+ .account .account-avatar {
300
+ display: grid;
301
+ place-items: center;
302
+ width: 32px;
303
+ height: 32px;
304
+ border-radius: 50%;
305
+ border: 1px solid rgba(255, 255, 255, 0.2);
306
+ background: rgba(255, 255, 255, 0.08);
307
+ }
308
+ .account .account-avatar svg {
309
+ width: 16px;
310
+ height: 16px;
311
+ stroke: currentColor;
312
+ fill: none;
313
+ stroke-width: 1.8;
314
+ }
315
+ .account .account-meta {
316
+ display: grid;
317
+ gap: 2px;
318
+ min-width: 0;
319
+ }
320
+ .account .account-email {
321
+ font-weight: 640;
322
+ font-size: 13px;
323
+ overflow: hidden;
324
+ text-overflow: ellipsis;
325
+ white-space: nowrap;
326
+ }
327
+ .account .account-standing {
328
+ color: rgba(238, 247, 245, 0.7);
329
+ font-size: 11px;
330
+ }
331
+ .account .account-standing[data-standing="none"] {
332
+ color: rgba(255, 196, 120, 0.9);
333
+ }
334
+ .account a {
335
+ color: inherit;
336
+ }
337
+ .account .account-signout button {
338
+ margin-top: 2px;
339
+ padding: 0;
340
+ border: 0;
341
+ background: none;
342
+ color: rgba(238, 247, 245, 0.6);
343
+ font-size: 11px;
344
+ text-decoration: underline;
345
+ cursor: pointer;
346
+ }
347
+ .account .account-signout button:hover {
348
+ color: rgba(238, 247, 245, 0.9);
349
+ }
350
+
235
351
  .tenant-picker {
236
352
  display: grid;
237
353
  gap: 4px;
@@ -508,6 +624,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
508
624
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
509
625
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
510
626
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="ai-workflows"] #ai-workflows,
627
+ html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="publish"] #publish,
511
628
  html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="settings"] #settings {
512
629
  display: grid;
513
630
  }
@@ -784,6 +901,13 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
784
901
  background: #fff6e7;
785
902
  }
786
903
 
904
+ /* A JS-toggled `.risk-alert` (e.g. the degraded-forge strip) must stay hidden
905
+ when `hidden` is set — the base display:flex above would otherwise win over
906
+ the UA [hidden] rule and keep it visible. */
907
+ .risk-alert[hidden] {
908
+ display: none;
909
+ }
910
+
787
911
  .risk-alert svg {
788
912
  color: var(--admin-amber);
789
913
  }
@@ -1068,6 +1192,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1068
1192
  <symbol id="i-orders" viewBox="0 0 24 24"><path d="M7 4h10a2 2 0 0 1 2 2v14l-3-2-2 2-2-2-2 2-3 2V6a2 2 0 0 1 2-2Z"></path><path d="M9 9h6M9 13h6"></path></symbol>
1069
1193
  <symbol id="i-box" viewBox="0 0 24 24"><path d="m3 7 9-4 9 4-9 4-9-4Z"></path><path d="M3 7v10l9 4 9-4V7"></path><path d="M12 11v10"></path></symbol>
1070
1194
  <symbol id="i-users" viewBox="0 0 24 24"><path d="M16 21v-2a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v2"></path><circle cx="9.5" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.9"></path><path d="M16 3.1a4 4 0 0 1 0 7.8"></path></symbol>
1195
+ <symbol id="i-user" viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle></symbol>
1071
1196
  <symbol id="i-shield" viewBox="0 0 24 24"><path d="M12 3 20 6v6c0 5-3.4 8.4-8 9-4.6-.6-8-4-8-9V6l8-3Z"></path><path d="m8.8 12 2.2 2.2L15.8 9"></path></symbol>
1072
1197
  <symbol id="i-chart" viewBox="0 0 24 24"><path d="M4 19V5"></path><path d="M4 19h16"></path><path d="M8 16v-5"></path><path d="M12 16V8"></path><path d="M16 16v-3"></path></symbol>
1073
1198
  <symbol id="i-workflows" viewBox="0 0 24 24"><path d="M6 5h6a4 4 0 0 1 4 4v1"></path><path d="m19 7-3 3-3-3"></path><path d="M18 19h-6a4 4 0 0 1-4-4v-1"></path><path d="m5 17 3-3 3 3"></path><circle cx="6" cy="5" r="2"></circle><circle cx="18" cy="19" r="2"></circle></symbol>
@@ -1133,6 +1258,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1133
1258
  <a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
1134
1259
  <a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
1135
1260
  <a href="#ai-workflows" data-admin-tab-link><svg><use href="#i-workflows"></use></svg><span>AI Workflows</span></a>
1261
+ <a href="#publish" data-admin-tab-link><svg><use href="#i-export"></use></svg><span>Publish</span></a>
1136
1262
  <a href="#settings" data-admin-tab-link><svg><use href="#i-gear"></use></svg><span>Settings</span></a>
1137
1263
  </nav>
1138
1264
 
@@ -1141,6 +1267,44 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1141
1267
  <div class="health-row"><span>Payments</span><strong>OK</strong></div>
1142
1268
  <div class="health-row"><span>Risk checks</span><strong>OK</strong></div>
1143
1269
  </div>
1270
+
1271
+ <!-- Cross-surface hop: the developer cockpit is a SIBLING surface (dev
1272
+ setup + connection status), not an admin tab — link, don't embed.
1273
+ basePath-aware so it stays inside the tenant prefix. -->
1274
+ <a class="cockpit-link" href={`${basePath || ""}/cockpit`}>
1275
+ <svg><use href="#i-gear"></use></svg><span>Developer cockpit</span>
1276
+ </a>
1277
+
1278
+ <div class="account" data-admin-account>
1279
+ <span class="account-avatar" aria-hidden="true"><svg><use href="#i-user"></use></svg></span>
1280
+ <span class="account-meta">
1281
+ {viewerEmail ? (
1282
+ <>
1283
+ <span class="account-email" title={viewerEmail}>{viewerEmail}</span>
1284
+ <span
1285
+ class="account-standing"
1286
+ data-standing={adminEntry.isOwnerViewer ? "owner" : isStaff ? "staff" : "none"}
1287
+ >
1288
+ {adminEntry.isOwnerViewer
1289
+ ? `owner/admin of ${tenantEnvelope.targetTenant}`
1290
+ : isStaff
1291
+ ? `ToT staff · not an owner of ${tenantEnvelope.targetTenant}`
1292
+ : `signed in · no owner/admin standing for ${tenantEnvelope.targetTenant}`}
1293
+ </span>
1294
+ <form class="account-signout" method="post" action="/api/auth/logout">
1295
+ <button type="submit">Sign out</button>
1296
+ </form>
1297
+ </>
1298
+ ) : (
1299
+ <>
1300
+ <span class="account-email">Not signed in</span>
1301
+ <span class="account-standing" data-standing="none">
1302
+ <a href="/auth/login">sign in</a> as an owner/admin of {tenantEnvelope.targetTenant} to publish
1303
+ </span>
1304
+ </>
1305
+ )}
1306
+ </span>
1307
+ </div>
1144
1308
  </aside>
1145
1309
 
1146
1310
  <main class="main">
@@ -1159,6 +1323,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1159
1323
  <a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1160
1324
  <a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
1161
1325
  <a class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
1326
+ <a class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
1162
1327
  <a class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
1163
1328
  </div>
1164
1329
 
@@ -1206,6 +1371,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1206
1371
  <a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
1207
1372
  <a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
1208
1373
  <a id="admin-tab-ai-workflows" class="tab" href="#ai-workflows" role="tab" aria-controls="ai-workflows" aria-selected="false" data-admin-tab-link>AI Workflows</a>
1374
+ <a id="admin-tab-publish" class="tab" href="#publish" role="tab" aria-controls="publish" aria-selected="false" data-admin-tab-link>Publish</a>
1209
1375
  <a id="admin-tab-settings" class="tab" href="#settings" role="tab" aria-controls="settings" aria-selected="false" data-admin-tab-link>Settings</a>
1210
1376
  </div>
1211
1377
 
@@ -1331,6 +1497,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1331
1497
  <AdminFulfillmentTab envelope={tenantEnvelope} />
1332
1498
  <AdminReportingTab envelope={tenantEnvelope} />
1333
1499
  <AdminAIWorkflowsTab envelope={tenantEnvelope} />
1500
+ <AdminPublishTab envelope={tenantEnvelope} />
1334
1501
  <AdminSettingsTab envelope={tenantEnvelope} />
1335
1502
 
1336
1503
  {canViewInternalGuide && (
@@ -1447,7 +1614,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1447
1614
 
1448
1615
  <script is:inline nonce={cspNonce}>
1449
1616
  (() => {
1450
- const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
1617
+ const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "publish", "settings"];
1451
1618
  const tabSet = new Set(tabIds);
1452
1619
  const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
1453
1620
  const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
@@ -1460,16 +1627,20 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1460
1627
  fulfillment: "Fulfillment",
1461
1628
  reporting: "Reporting",
1462
1629
  "ai-workflows": "AI Workflows",
1630
+ publish: "Publish",
1463
1631
  settings: "Settings",
1464
1632
  };
1465
1633
 
1466
1634
  const panelIdFromHash = () => {
1467
- const hash = window.location.hash.slice(1);
1468
- if (!hash) return defaultTab;
1635
+ const rawHash = window.location.hash.slice(1);
1636
+ // The tab id is the part before any "?" query suffix (#publish?pr=42);
1637
+ // the raw hash is still used for internal-guide anchor resolution.
1638
+ const hash = rawHash.split("?")[0];
1639
+ if (!rawHash) return defaultTab;
1469
1640
  if (tabSet.has(hash)) return hash;
1470
1641
  if (!canViewInternalGuide) return defaultTab;
1471
1642
 
1472
- const target = hash ? document.getElementById(hash) : null;
1643
+ const target = rawHash ? document.getElementById(rawHash) : null;
1473
1644
  const panel = target?.closest?.("[data-admin-panel]");
1474
1645
  if (panel?.id && tabSet.has(panel.id)) return panel.id;
1475
1646
 
@@ -1599,8 +1770,11 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
1599
1770
 
1600
1771
  if (panelId) {
1601
1772
  setActiveTab(panelId);
1602
- const hash = window.location.hash.slice(1);
1603
- if (!canViewInternalGuide && (!hash || !tabSet.has(hash))) {
1773
+ // Strip any query suffix before deciding whether the hash names a
1774
+ // real tab a valid deep link like #publish?pr=42 must not be
1775
+ // normalized away to #settings for merchant viewers.
1776
+ const hashTab = window.location.hash.slice(1).split("?")[0];
1777
+ if (!canViewInternalGuide && !tabSet.has(hashTab)) {
1604
1778
  history.replaceState(null, "", "#settings");
1605
1779
  }
1606
1780
  if (scroll) requestAnimationFrame(() => scrollToHashTarget(panelId));
@@ -0,0 +1,137 @@
1
+ /**
2
+ * POST /api/activity — the ingest endpoint of the operational activity telemetry
3
+ * spine (card D4). Accepts one `ActivityEvent` (or a batch array) as JSON from
4
+ * the emitter cohorts — the CLI (D3) and the UI (D6), both posting the wire shape
5
+ * of `packages/public-runtime/src/activity/event.ts` — authenticates the caller,
6
+ * re-redacts server-side, and fans each accepted event out to the sink(s) its
7
+ * catalog entry declares. See DECISIONS.md §8 for the store decision.
8
+ *
9
+ * AUTH — HMAC-SHA256 service-auth (`src/lib/activity/ingestAuth.ts`). The caller
10
+ * presents the authenticated tenant in `x-tot-activity-tenant` and a hex
11
+ * signature over `<tenant>\n<rawBody>` in `x-tot-activity-signature`, keyed by
12
+ * the `ACTIVITY_INGEST_SECRET` Worker secret. Binding the tenant into the signed
13
+ * material is what makes the tenant scope trustworthy: the route writes every
14
+ * row under THIS verified tenant and discards any body-supplied `scope.tenantId`
15
+ * (multi-tenant isolation). An unset secret is a 503 (seam not configured — the
16
+ * same convention as the MCP webhook receivers), not an auth failure.
17
+ *
18
+ * DEFENSE IN DEPTH — every event is rebuilt through `createActivityEvent()`
19
+ * (`ingestEvents`), re-applying the default-deny allowlist + canary scan even
20
+ * though emitters already redacted. A value-canary trip is self-telemetried as
21
+ * `system.ingest.rejected` so a leak attempt is visible (D6/D7 alert on it).
22
+ *
23
+ * OFF THE CRITICAL PATH — auth + parse + validate + redact are cheap synchronous
24
+ * CPU; the sink WRITES (D1 + Analytics Engine) run past the response via
25
+ * `runAfterResponse` (Workers `waitUntil`). The response is a 202 with per-event
26
+ * accepted/rejected counts the instant validation is done — a slow D1 write never
27
+ * holds the emitter's request open. JSON bodies are exempt from Astro's CSRF
28
+ * origin check, so the server-to-server callers work.
29
+ */
30
+ import type { APIContext } from "astro";
31
+ import { readD1, readEnv, readAnalytics, runAfterResponse, type CfExecutionContextLike } from "@/lib/env";
32
+ import { fromD1 } from "@/lib/d1/catalog";
33
+ import { verifyIngestAuth } from "@/lib/activity/ingestAuth";
34
+ import { ingestEvents, type IngestRejection } from "@/lib/activity/ingest";
35
+ import { fanOutActivity, type AnalyticsEngineLike, type ActivitySinks } from "@/lib/activity/store";
36
+ import { isActivityDisabled } from "@/lib/activity/killSwitch";
37
+ import { createActivityEvent } from "@tot/public-runtime";
38
+
39
+ export const prerender = false;
40
+
41
+ /** Cap the batch so a single POST can't fan out unbounded work past the response. */
42
+ const MAX_BATCH = 200;
43
+
44
+ function json(body: unknown, status: number): Response {
45
+ return new Response(JSON.stringify(body), {
46
+ status,
47
+ // Write-path ack — never cacheable. Mirrors the house convention.
48
+ headers: { "content-type": "application/json", "cache-control": "no-store" },
49
+ });
50
+ }
51
+
52
+ export async function POST(context: APIContext): Promise<Response> {
53
+ // Kill switch (D8): telemetry off ⇒ accept-and-drop. Fail-open toward the EMITTER —
54
+ // a 202 (never a 4xx/5xx) so a CLI/UI poster never errors, retries, or is delayed by
55
+ // the switch; nothing is validated, stored, or fanned out.
56
+ if (isActivityDisabled()) {
57
+ return json({ ok: true, disabled: true, accepted: 0, rejected: [] }, 202);
58
+ }
59
+
60
+ const secret = await readEnv("ACTIVITY_INGEST_SECRET");
61
+ if (!secret) {
62
+ // Seam not configured (emitters don't post without the shared ingest secret).
63
+ return new Response("activity ingest not configured", { status: 503 });
64
+ }
65
+
66
+ // Read the RAW body once: it is both the auth material and the JSON payload.
67
+ const rawBody = await context.request.text();
68
+
69
+ const auth = await verifyIngestAuth(context.request.headers, rawBody, secret);
70
+ if (!auth.ok) return new Response(auth.reason, { status: auth.status });
71
+
72
+ let parsed: unknown;
73
+ try {
74
+ parsed = JSON.parse(rawBody);
75
+ } catch {
76
+ return json({ ok: false, reason: "invalid JSON body" }, 400);
77
+ }
78
+ const events = Array.isArray(parsed) ? parsed : [parsed];
79
+ if (events.length === 0) return json({ ok: true, accepted: 0, rejected: [] }, 202);
80
+ if (events.length > MAX_BATCH) {
81
+ return json({ ok: false, reason: `batch too large (max ${MAX_BATCH})` }, 413);
82
+ }
83
+
84
+ // Validate + re-redact under the AUTHENTICATED tenant (cheap, synchronous).
85
+ const { accepted, rejected } = ingestEvents(events, auth.tenantId);
86
+
87
+ // Resolve the sink bindings (absent in dev/until provisioned ⇒ that sink is
88
+ // skipped, never an error).
89
+ const d1 = await readD1("STOREFRONT_APPS_DB");
90
+ const analytics = (await readAnalytics("ACTIVITY_ANALYTICS")) as AnalyticsEngineLike | undefined;
91
+ const sinks: ActivitySinks = {
92
+ ...(d1 ? { db: fromD1(d1) } : {}),
93
+ ...(analytics ? { analytics } : {}),
94
+ ingestedAt: new Date().toISOString(),
95
+ };
96
+
97
+ // Self-telemetry: a value-canary trip during server-side redaction means an
98
+ // emitter leaked (or tried to leak) a secret/PII into the payload. Emit a
99
+ // `system.ingest.rejected` event (itself routed to logs+analytics) so D6/D7 can
100
+ // alert — the fact of the trip, the action, never the offending value. Rejected
101
+ // events get the same self-telemetry so a spoof/unknown-action attempt is
102
+ // visible. Built here (synchronously) but fanned out off-path below.
103
+ const selfTelemetry = [
104
+ ...accepted
105
+ .filter((a) => a.canaryTripped)
106
+ .map((a) =>
107
+ createActivityEvent({
108
+ action: "system.ingest.rejected",
109
+ actor: { kind: "system", id: "activity-ingest" },
110
+ source: "server",
111
+ outcome: { status: "refused", errorClass: "canary_tripped" },
112
+ scope: { tenantId: auth.tenantId },
113
+ payload: { args: { reason: "canary_tripped", attemptedAction: a.event.action } },
114
+ }).event,
115
+ ),
116
+ ...rejected.map((r: IngestRejection) =>
117
+ createActivityEvent({
118
+ action: "system.ingest.rejected",
119
+ actor: { kind: "system", id: "activity-ingest" },
120
+ source: "server",
121
+ outcome: { status: "refused", errorClass: r.reason },
122
+ scope: { tenantId: auth.tenantId },
123
+ payload: { args: { reason: r.reason, ...(r.attemptedAction ? { attemptedAction: r.attemptedAction } : {}) } },
124
+ }).event,
125
+ ),
126
+ ];
127
+
128
+ // Fan every accepted event + the self-telemetry out to its sinks, PAST the
129
+ // response. Best-effort per sink (never throws). The adapter's per-request
130
+ // ExecutionContext lives at `locals.runtime.ctx`; runAfterResponse floats the
131
+ // promise in dev/Node when it is absent.
132
+ const cfContext = (context.locals as { runtime?: { ctx?: CfExecutionContextLike } })?.runtime?.ctx ?? null;
133
+ const work = Promise.all([...accepted.map((a) => a.event), ...selfTelemetry].map((e) => fanOutActivity(e, sinks)));
134
+ runAfterResponse(work, cfContext);
135
+
136
+ return json({ ok: true, accepted: accepted.length, rejected }, 202);
137
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * `GET /api/admin/activity-alerts` — the polled/on-demand surface for card D7's
3
+ * operational-activity alert conditions.
4
+ *
5
+ * On-demand by design (see `lib/activity/alerts.ts` header): D7 deliberately does
6
+ * NOT add a cron/scheduled runner (no established precedent for one in this repo).
7
+ * This route runs `checkActivityAlerts()` fresh on every call, so it doubles as:
8
+ * (a) the data source for the `/admin/ops-timeline` alerts banner (that page
9
+ * calls the same function directly, in-process — this route is for OTHER
10
+ * callers), and
11
+ * (b) a lightweight endpoint an external uptime/monitor tool can poll on an
12
+ * interval, turning "on-demand" into "effectively continuous" without this
13
+ * repo standing up its own scheduler.
14
+ *
15
+ * ACCESS CONTROL mirrors `/admin/ops-timeline` exactly (same owner/admin-of-tenant
16
+ * or ToT-staff gate via `resolveAdminEntry`/`resolveAdminTenantMode`) rather than a
17
+ * new service-secret seam — this repo's other machine-to-machine surfaces
18
+ * (candidate-reconcile, the activity ingest route) authenticate a SPECIFIC known
19
+ * caller (MCP, an emitter) via a shared secret; there is no such caller for
20
+ * "a monitor polls for alerts" yet, and inventing one is the "new notification
21
+ * infra" the card scopes OUT. An admin/staff session (the same one that already
22
+ * views ops-timeline) is the correct, already-available caller today.
23
+ *
24
+ * TENANT ISOLATION — scoped to `Astro.locals.tenant.tenant_id` exactly like
25
+ * ops-timeline; never a URL/query value.
26
+ */
27
+ import type { APIContext } from "astro";
28
+ import { readViewerSession } from "@/lib/auth/route";
29
+ import { resolveAdminEntry } from "@/lib/auth/adminEntry";
30
+ import { resolveAdminTenantMode } from "@/lib/adminTenantEnvelope";
31
+ import { readD1 } from "@/lib/env";
32
+ import { fromD1 } from "@/lib/d1/catalog";
33
+ import { checkActivityAlerts } from "@/lib/activity/alerts";
34
+
35
+ export const prerender = false;
36
+
37
+ const JSON_HEADERS = {
38
+ "content-type": "application/json; charset=utf-8",
39
+ "cache-control": "no-store",
40
+ "x-robots-tag": "noindex, nofollow",
41
+ };
42
+
43
+ function json(body: unknown, status = 200): Response {
44
+ return new Response(JSON.stringify(body), { status, headers: JSON_HEADERS });
45
+ }
46
+
47
+ export async function GET(context: APIContext): Promise<Response> {
48
+ const { tenant } = context.locals;
49
+ const nowSeconds = Math.floor(Date.now() / 1000);
50
+
51
+ const viewerSession = await readViewerSession(context);
52
+ if (!viewerSession) return json({ ok: false, reason: "sign_in_required" }, 401);
53
+
54
+ const adminEntry = resolveAdminEntry({
55
+ record: viewerSession,
56
+ hostResource: tenant.appDomain,
57
+ nowSeconds,
58
+ });
59
+ const { isStaff } = resolveAdminTenantMode({
60
+ session: viewerSession,
61
+ adminAppTenant: tenant.appDomain,
62
+ requestedTarget: null,
63
+ nowSeconds,
64
+ });
65
+ const authorized = adminEntry.isOwnerViewer || isStaff;
66
+ if (!authorized) return json({ ok: false, reason: "not_authorized" }, 403);
67
+
68
+ const d1 = await readD1("STOREFRONT_APPS_DB");
69
+ if (!d1) return json({ ok: true, dbAvailable: false, alerts: [] });
70
+
71
+ const alerts = await checkActivityAlerts(fromD1(d1), tenant.tenant_id);
72
+ return json({ ok: true, dbAvailable: true, alerts });
73
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * POST /api/apps/admin/credentials/rotate — rotate an install's client
3
+ * secret, keeping the same `client_id` (PrivateApps epic, D2 Chunk C; D8
4
+ * console-session auth added). The new secret is returned ONCE here and never
5
+ * again — the same one-time-secret guarantee `install()` gives (see
6
+ * `registryService.ts`'s `InstallResult` doc).
7
+ *
8
+ * `registryService.rotateCredentials()` takes a bare `installId` with no
9
+ * tenant check of its own (Chunk A assumption #1) — so this route resolves
10
+ * the install via `requireOwnedInstall` BEFORE calling it, rejecting a
11
+ * foreign-tenant `installId` with the SAME 404 a truly-missing one gets (no
12
+ * cross-tenant existence oracle). Mutating: `requireManageRole` gates a
13
+ * console `developer` role out — the host-bound/Bearer path is exempt (no
14
+ * `role`).
15
+ */
16
+ import type { APIContext } from "astro";
17
+ import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
18
+ import {
19
+ getRegistryService,
20
+ registryErrorResponse,
21
+ requireOwnedInstall,
22
+ requireManageRole,
23
+ parseInstallId,
24
+ json,
25
+ } from "@/lib/apps/adminService.js";
26
+ import { RegistryError } from "@/lib/apps/registryService.js";
27
+ import { resolveUiActor } from "@/lib/activity/uiActor";
28
+ import { recordActivity } from "@/lib/activity/recordActivity";
29
+
30
+ export const prerender = false;
31
+
32
+ export async function POST(context: APIContext): Promise<Response> {
33
+ const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
34
+ const auth = await resolveOperatorSession(context, requestedTenant);
35
+ if (!auth.ok) return json({ error: auth.message }, auth.status);
36
+ const manageDenied = requireManageRole(auth);
37
+ if (manageDenied) return manageDenied;
38
+
39
+ let body: unknown;
40
+ try {
41
+ body = await context.request.json();
42
+ } catch {
43
+ return json({ error: "invalid JSON body" }, 400);
44
+ }
45
+
46
+ const installId = parseInstallId(body);
47
+ if (!installId) return json({ error: "installId is required" }, 400);
48
+
49
+ const registry = await getRegistryService();
50
+ if (registry instanceof Response) return registry;
51
+
52
+ const owned = await requireOwnedInstall(registry, installId, auth.tenantId);
53
+ if (owned instanceof Response) return owned;
54
+
55
+ // D10: acting cohort (merchant/admin), emitted source "server". SECURITY: the
56
+ // rotated secret (`issued.secret`) is NEVER placed in the payload — the allowlist
57
+ // names only the non-sensitive app handle (appId/env, from the owned install) +
58
+ // rotation time.
59
+ const activityActor = await resolveUiActor(context, { owner: auth.tenantId });
60
+
61
+ try {
62
+ const issued = await registry.rotateCredentials(installId, auth.actor);
63
+ recordActivity({
64
+ action: "app.credential.rotated",
65
+ actor: activityActor,
66
+ source: "server",
67
+ outcome: { status: "succeeded" },
68
+ scope: { tenantId: auth.tenantId },
69
+ payload: { args: { appId: owned.appId, env: owned.env, rotatedAt: new Date().toISOString() } },
70
+ });
71
+ return json({ credential: { clientId: issued.clientId, secret: issued.secret } });
72
+ } catch (err) {
73
+ if (err instanceof RegistryError) {
74
+ recordActivity({
75
+ action: "app.credential.rotated",
76
+ actor: activityActor,
77
+ source: "server",
78
+ outcome: { status: "failed", errorClass: err.code },
79
+ scope: { tenantId: auth.tenantId },
80
+ payload: { args: { appId: owned.appId, env: owned.env } },
81
+ });
82
+ return registryErrorResponse(err);
83
+ }
84
+ throw err;
85
+ }
86
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GET /api/apps/admin/health — the console's per-install health rollup for
3
+ * the resolved operator tenant (PrivateApps epic, D8 Chunk C). Read-only:
4
+ * any membership role admits (mirrors `list.ts`'s `?tenant=`-as-candidate
5
+ * scoping — the tenant actually queried is always `auth.tenantId`, never the
6
+ * raw param).
7
+ *
8
+ * Folds `registryService.listByTenant` with `deliveryStore.getInstallHealth`
9
+ * per install through `healthAggregate.ts`'s `buildInstallHealthView` — that
10
+ * view is what data-minimizes `webhookEndpoint` down to `hasWebhookEndpoint`,
11
+ * so this route never even holds the raw endpoint value to leak.
12
+ */
13
+ import type { APIContext } from "astro";
14
+ import { resolveOperatorSession } from "@/lib/apps/adminSession.js";
15
+ import { getRegistryService, json } from "@/lib/apps/adminService.js";
16
+ import { WebhookDeliveryStore } from "@/lib/webhooks/deliveryStore.js";
17
+ import { buildInstallHealthView, type AppInstallHealthView } from "@/lib/apps/healthAggregate.js";
18
+ import { fromD1 } from "@/lib/d1/catalog.js";
19
+ import { readD1 } from "@/lib/env";
20
+
21
+ export const prerender = false;
22
+
23
+ export async function GET(context: APIContext): Promise<Response> {
24
+ const requestedTenant = new URL(context.request.url).searchParams.get("tenant");
25
+ const auth = await resolveOperatorSession(context, requestedTenant);
26
+ if (!auth.ok) return json({ error: auth.message }, auth.status);
27
+
28
+ const registry = await getRegistryService();
29
+ if (registry instanceof Response) return registry;
30
+
31
+ const d1 = await readD1("STOREFRONT_APPS_DB");
32
+ if (!d1) return json({ error: "webhook store not configured" }, 503);
33
+ const deliveryStore = new WebhookDeliveryStore(fromD1(d1));
34
+
35
+ const installs = await registry.listByTenant(auth.tenantId);
36
+ const apps: AppInstallHealthView[] = await Promise.all(
37
+ installs.map(async (install) => {
38
+ const health = await deliveryStore.getInstallHealth(install.installId);
39
+ return buildInstallHealthView(install, health);
40
+ }),
41
+ );
42
+
43
+ return json({ apps });
44
+ }