@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
@@ -14,8 +14,8 @@
14
14
  * mockup's visual system — the tokenoftrust tenant theme (navy ink #0A2540 + trust teal
15
15
  * #0E9F8E on cool-white #F7F9FC), Plus Jakarta Sans / Inter / JetBrains Mono, rounded cards —
16
16
  * now with full dark-mode (system + manual toggle) and reduced-motion support. Same CSP as
17
- * every storefront response (googleapis/gstatic allowlisted, so the shared typefaces load);
18
- * inline style/script carry this request's nonce.
17
+ * every storefront response; the shared typefaces are self-hosted same-origin (@fontsource,
18
+ * imported via `@/styles/fonts.css`), and inline style/script carry this request's nonce.
19
19
  *
20
20
  * The one command honours the shipped browserless-signin fastpath: a developer who reached
21
21
  * this page from their magic link already has a session here, so the Copy button mints a
@@ -25,12 +25,25 @@
25
25
  */
26
26
  import { readViewerSession } from "@/lib/auth/route";
27
27
  import { staticTenants } from "@/config/tenants";
28
+ import { resolvePreviewTenant } from "@/config/previewTenantLookup";
29
+ import { readTenantPreviewStatus, isStoreNotSetUp } from "@/lib/dev/previewStatus";
30
+ import { ensureCockpitVcBinding } from "@/lib/dev/vcBinding";
28
31
  import { resolveDevApiBase } from "@/lib/dev/apiBase";
32
+ import { resolveCockpitStoreResource } from "@/lib/dev/cockpitStore";
29
33
  import { normalizeEmailHint, redactEmailForHint } from "@/lib/privacy/emailHint";
34
+ // Self-hosted web fonts (audit finding 6) — same-origin @fontsource, replacing
35
+ // the render-blocking fonts.googleapis.com stylesheet. Covers Plus Jakarta Sans,
36
+ // Inter, and JetBrains Mono used by this page's inline styles.
37
+ import "@/styles/fonts.css";
30
38
 
31
39
  export const prerender = false;
32
40
  const cspNonce = Astro.locals.cspNonce;
33
41
  const url = new URL(Astro.request.url);
42
+ // The tenant prefix ("" host-routed, "/<tenant>" path-routed) this cockpit is served
43
+ // under — set by the middleware. Every self-link + same-store API call below carries
44
+ // it so a `/<tenant>/cockpit` invite keeps its store across the login round-trip and
45
+ // the command mint (see resolveCockpitStoreResource / the basePath-scoped fetches).
46
+ const basePath = Astro.locals.basePath ?? "";
34
47
  const isCredentialRecovery = url.searchParams.get("recover") === "cli";
35
48
  const legacyRecoveryEmail = (url.searchParams.get("email") ?? "").trim();
36
49
  const recoveryEmailHint =
@@ -42,34 +55,41 @@ if (isCredentialRecovery && url.searchParams.has("email")) {
42
55
  canonical.delete("email");
43
56
  if (recoveryEmailHint) canonical.set("email_hint", recoveryEmailHint);
44
57
  return Astro.redirect(
45
- `/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
58
+ `${basePath}/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
46
59
  302,
47
60
  );
48
61
  }
49
- const cockpitNext = isCredentialRecovery ? "/cockpit?recover=cli" : "/cockpit";
62
+ const cockpitNext = `${basePath}/cockpit${isCredentialRecovery ? "?recover=cli" : ""}`;
50
63
  const loginParams = new URLSearchParams({ next: cockpitNext });
51
64
  if (recoveryEmailHint) loginParams.set("email_hint", recoveryEmailHint);
52
65
  const loginHref = `/auth/login?${loginParams.toString()}`;
53
66
 
54
67
  const session = await readViewerSession({ cookies: Astro.cookies });
55
68
  const signedIn = !!session;
69
+ // d1 (board: d1-unmask-invite-email) — the FULL invite address to show its OWNER in the
70
+ // recovery notice, never a redacted hint. Prefer the validated session email; else a full
71
+ // `?email=` if one ever survives to render. Empty when only a redacted `email_hint` is on
72
+ // hand (the common case — the CLI masks the address upstream before it reaches this page),
73
+ // and the notice then falls back to neutral, non-redacted copy rather than re-showing the
74
+ // owner a masked form of their own address. redactEmailForHint/emailHint.ts stay intact for
75
+ // every other caller (e.g. the /auth/login hint below) — we simply stop masking HERE.
76
+ const recoveryEmailFull =
77
+ session?.email ||
78
+ (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(legacyRecoveryEmail) ? legacyRecoveryEmail.toLowerCase() : "");
56
79
  // The fastpath (browserless terminal sign-in) is gated behind the developer's own session +
57
80
  // capability — the same gate /api/dev/cli-signin-code enforces. Mirror it here so a
58
81
  // logged-out / no-capability visitor gets the generic command, never a broken mint.
59
82
  const hasStoreAccess = signedIn && !!session?.capability && session.capability !== "none";
60
83
 
61
- // Resolve the developer's tenant from their first membership (or ?tenant= for an
62
- // ANONYMOUS preview only never override a signed-in developer's own tenant with
63
- // caller-supplied input; the mint/activity endpoints already derive tenant solely
64
- // from the session/token, but letting the query param win here still meant a
65
- // signed-in dev could be shown another vendor's branding + `tot checkout <theirs>`
66
- // instructions, which is confusing/spoofable chrome even though nothing privileged
67
- // leaked).
68
- const wanted = (
69
- session
70
- ? (session?.tenants?.[0]?.resource ?? session?.resource ?? "")
71
- : (url.searchParams.get("tenant") ?? "")
72
- ).toLowerCase();
84
+ // Resolve which store this cockpit is for. The URL wins when a `/<tenant>/cockpit`
85
+ // request named a specific store the signed-in developer can access (the trusted
86
+ // host/path seam on `locals.tenant`, membership-checked) this is the regression
87
+ // fix: an invite to another store than the session's first membership no longer
88
+ // "loses" that store. Otherwise: the developer's own first store (generic host), or
89
+ // for an ANONYMOUS viewer only the path store's branding, else `?tenant=`. Never
90
+ // a signed-in developer's store overridden by caller-supplied query input.
91
+ // See resolveCockpitStoreResource; the same resolver gates the mint + /api/dev/*.
92
+ const wanted = resolveCockpitStoreResource(Astro, session);
73
93
  const tenant =
74
94
  (wanted
75
95
  ? staticTenants.find((t) =>
@@ -82,9 +102,66 @@ const tenant =
82
102
  staticTenants[0];
83
103
 
84
104
  const appDomain = tenant?.appDomain ?? "tokenoftrust.com";
85
- // Signed in, but the account carries no developer capability on any store — an honest
86
- // recovery state (not a 401): show what to do, not the fastpath.
87
- const noStoreAccess = signedIn && !hasStoreAccess;
105
+
106
+ // fix-staff-cockpit-403: `hasStoreAccess` above only proves the session carries SOME
107
+ // eligible capability the federated sign-in path stamps capability:"member"
108
+ // unconditionally (auth/magic-exchange), so a ToT-staff viewer admitted by the
109
+ // loginGate staff bypass (no real store membership) passes it too, even on the home
110
+ // host where `wanted` never names a real store. Every /api/dev/* route instead gates
111
+ // on this SAME tenant actually RESOLVING (resolvePreviewTenant — see
112
+ // lib/dev/hostedCockpit.ts, api/dev/activity.ts) — mirror that here so we never mint a
113
+ // setup command or start polling for a viewer who'd just 403-loop against it.
114
+ const resolvedStoreTenant = hasStoreAccess && wanted ? await resolvePreviewTenant(wanted) : null;
115
+ // Capability-eligible, but no store resolves for this viewer/host (the staff/home-host
116
+ // case above). Gets its own calm "pick a store" state instead of the full flow.
117
+ const noResolvableStore = hasStoreAccess && !resolvedStoreTenant;
118
+
119
+ // STORE-NOT-SET-UP (#8425 invite-gated-auto-build): a developer WITH access + a
120
+ // resolvable store, but whose forge repo isn't provisioned yet — the invite landed
121
+ // before (or without) the store being set up. Offering the fastpath here would hand
122
+ // them a `tot login --code … && tot start` that dies at checkout on "Repo … is not
123
+ // provisioned" (James's first-run failure). Ask the credential-free MCP preview-status
124
+ // seam; only a DEFINITIVE not-provisioned flips the state (an unconfigured / can't-answer
125
+ // seam keeps the normal fastpath, with the CLI's own clean "not set up yet" error as the
126
+ // backstop — so no regression on local/dev or a transient MCP blip). This is a DISTINCT
127
+ // axis from noResolvableStore/noStoreAccess and gets its own honest notice below.
128
+ const storeNotSetUp =
129
+ hasStoreAccess && !noResolvableStore && isStoreNotSetUp(await readTenantPreviewStatus(appDomain));
130
+
131
+ // The gate for rendering the setup-command + activity-poll flow: eligible AND a real
132
+ // store to point it at AND that store is actually provisioned. Use this (never the raw
133
+ // hasStoreAccess) for anything that mints a command or starts the activity poll.
134
+ const canSetupStore = hasStoreAccess && !noResolvableStore && !storeNotSetUp;
135
+
136
+ // vc-app-binding safety-net (08-18, §5): a BROWSER-FIRST developer reached this page
137
+ // via the storefront's own sign-in (a tot_session cookie) without ever crossing an
138
+ // MCP binding seam (rendezvous approve / native OAuth), so `appForCaller` doesn't
139
+ // resolve them and the `tot start` command we're about to hand them would 403 with
140
+ // "No version-control app is bound." Ensure the binding now — on-behalf of the
141
+ // verified session identity, via the app's private_key_jwt channel. Best-effort +
142
+ // fail-open (never blocks render), throttled to once per browser session so it stays
143
+ // off the hot render path; the authority-seam binding + backfill remain the real
144
+ // guarantee. Gated on canSetupStore (entitled + resolvable + provisioned), the exact
145
+ // condition under which we render a runnable command.
146
+ if (canSetupStore && session && !Astro.cookies.has("tot_vc_bound")) {
147
+ const vcBind = await ensureCockpitVcBinding(session.email);
148
+ if (vcBind.ok) {
149
+ // Throttle only on a real success — a fail-open miss retries on the next load.
150
+ Astro.cookies.set("tot_vc_bound", "1", {
151
+ path: "/",
152
+ httpOnly: true,
153
+ sameSite: "lax",
154
+ secure: true,
155
+ maxAge: 3600, // re-check hourly so a lost/revoked binding self-heals
156
+ });
157
+ }
158
+ }
159
+
160
+ // Signed in, but either no developer capability on any store OR (staff bypass) no store
161
+ // resolves for this viewer/host — an honest recovery state (not a 401): show what to
162
+ // do, not the fastpath. NOTE: storeNotSetUp is deliberately NOT folded in here — it is
163
+ // its own state with its own notice, so this stays computed from resolvable-store only.
164
+ const noStoreAccess = signedIn && (!hasStoreAccess || noResolvableStore);
88
165
  // The generic command shown (and copied) when we can't mint a personalised one.
89
166
  // Pinned to the EXACT CLI version this build was cut alongside (`@tokenoftrust/cli@<version>`,
90
167
  // astro.config Vite define) so the paste installs precisely the CLI this cockpit expects —
@@ -112,7 +189,7 @@ const isLocalDevLoop = import.meta.env.DEV;
112
189
  // on our hosted surface. It is NOT a port of the local-only Monitor/Files/preview
113
190
  // panels (those depend on /__tot/* endpoints that exist only under `astro dev`;
114
191
  // bringing those hosted is cp-tree/cp-file/cp-preview, tracked separately).
115
- const hostedActivity = !isLocalDevLoop && hasStoreAccess;
192
+ const hostedActivity = !isLocalDevLoop && canSetupStore;
116
193
 
117
194
  // cp-env (cloud-dev-cockpit Phase 0): the file-tree/viewer/diff + click-to-source
118
195
  // cockpit scripts below (filesScript, c8Script) are ENVIRONMENT-AWARE — same client
@@ -121,7 +198,7 @@ const hostedActivity = !isLocalDevLoop && hasStoreAccess;
121
198
  // cp-locate — separate cards). Resolving it here, unconditionally, means those cards
122
199
  // don't each have to invent the same env check; on isLocalDevLoop this is a no-op
123
200
  // (resolves to the unchanged /__tot base). See decision architecture-environment-aware-cockpit.
124
- const apiBase = resolveDevApiBase(isLocalDevLoop);
201
+ const apiBase = resolveDevApiBase(isLocalDevLoop, basePath);
125
202
 
126
203
  // Live loop Monitor (C1) client. Polls the dev server's /__tot/dev-loop-status
127
204
  // and renders the loopbar: server health, watched file, and the save→reload
@@ -568,20 +645,22 @@ const filesScript = `
568
645
  }
569
646
  if (diffBtn) diffBtn.addEventListener("click", doDiff);
570
647
 
571
- // cp-publish: draft -> TEST channel (never live see decision
572
- // cp-publish-scope-correction). Confirm-before-publish per the card.
648
+ // cp-publish: draft -> PR-backed candidate for review (unit s6, milestone
649
+ // "PR-Backed Hosted Review Loop") never straight to live, and no longer
650
+ // straight to the test channel either; it opens/updates a pull request an
651
+ // owner reviews and accepts. Confirm-before-submit per the card.
573
652
  function doPublish() {
574
653
  if (!cur || !cur.writable || !publishBtn) return;
575
- if (!window.confirm("Publish your saved edit to the test channel? (Not your live site.)")) return;
654
+ if (!window.confirm("Submit your saved edit for review? (Opens/updates a pull request \\u2014 not your live site.)")) return;
576
655
  var prevLabel = publishBtn.textContent;
577
656
  publishBtn.disabled = true;
578
- setStatus("Publishing\\u2026");
657
+ setStatus("Submitting\\u2026");
579
658
  fetch(apiBase + "/publish", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: cur.path }) })
580
659
  .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })
581
660
  .then(function (res) {
582
- if (res.s === 200 && res.j && res.j.ok) { setStatus(res.j.applied ? "Published to test \\u2713" : "Published (not persisted \\u2014 no storage configured)", "ok"); return; }
583
- if (res.s === 409 && res.j && res.j.status === "no_draft") { setStatus("No unpublished edit to publish.", "warn"); return; }
584
- setStatus((res.j && res.j.message) || "Publish failed", "warn");
661
+ if (res.s === 200 && res.j && res.j.ok) { setStatus("Submitted for review \\u2713" + (res.j.prNumber ? " (PR #" + res.j.prNumber + ")" : ""), "ok"); return; }
662
+ if (res.s === 409 && res.j && res.j.status === "no_draft") { setStatus("No unpublished edit to submit.", "warn"); return; }
663
+ setStatus((res.j && res.j.message) || "Submit failed", "warn");
585
664
  })
586
665
  .catch(function () { setStatus(offlineMsg, "warn"); })
587
666
  .then(function () { publishBtn.disabled = false; publishBtn.textContent = prevLabel; });
@@ -718,6 +797,12 @@ const hostedActivityScript = `
718
797
  var root = document.getElementById("hostedActivity");
719
798
  if (!root) return;
720
799
 
800
+ // The request's tenant prefix ("" host-routed, "/<tenant>" path-routed). Every
801
+ // /api/dev/* call below carries it so the activity feed + the CLI sign-in-code mint
802
+ // run in THIS store's scope (locals.tenant on the API request) — the same store this
803
+ // /<tenant>/cockpit rendered for, never the session's first membership.
804
+ var basePath = ${JSON.stringify(basePath)};
805
+
721
806
  // ?reset — a TRUE reset for re-testing the onboarding loop: clear THIS browser's
722
807
  // stepper progress AND the server-side KV (live-runtime record + activity buffer),
723
808
  // then reload clean. Clearing the server too means a stopped local loop stops reading
@@ -730,7 +815,7 @@ const hostedActivityScript = `
730
815
  var reloaded = false;
731
816
  var doneReset = function () { if (reloaded) return; reloaded = true; location.replace(location.pathname); };
732
817
  setTimeout(doneReset, 2000); // fallback so a hung request still lands us fresh
733
- try { fetch("/api/dev/activity", { method: "DELETE", cache: "no-store" }).then(doneReset, doneReset); }
818
+ try { fetch(basePath + "/api/dev/activity", { method: "DELETE", cache: "no-store" }).then(doneReset, doneReset); }
734
819
  catch (e) { doneReset(); }
735
820
  return; // the post-clear reload re-runs this script against a fresh, empty state
736
821
  }
@@ -1111,7 +1196,7 @@ const hostedActivityScript = `
1111
1196
  }
1112
1197
 
1113
1198
  function poll() {
1114
- fetch("/api/dev/activity", { cache: "no-store" })
1199
+ fetch(basePath + "/api/dev/activity", { cache: "no-store" })
1115
1200
  .then(function (r) { return r.ok ? r.json() : Promise.reject(); })
1116
1201
  .then(function (d) {
1117
1202
  var events = (d && d.events) || [];
@@ -1120,20 +1205,31 @@ const hostedActivityScript = `
1120
1205
  liveNow = isLive; // drives Step 1 minimize + the Step-2 teaser visibility below
1121
1206
  updateBridge(rt, d && d.obstacle); // same poll drives the bridge strip (+ obstacle lane)
1122
1207
  render(events);
1123
- var last = events.length ? events[events.length - 1] : null;
1124
- var latest = last ? last.at : 0;
1125
- var lastIsEdit = last && last.event !== "up"; // "up" = a server-running ping, not an edit
1208
+ // b1 fix Step-2 completion must key off the SAME events the feed shows:
1209
+ // REAL file edits (event !== "up"), NOT the single newest buffer entry. The
1210
+ // local loop interleaves recurring "up" liveness pings into this one ring
1211
+ // buffer, so the newest event is frequently an "up" ping even right after a
1212
+ // save. The old gate read events[last]: when an "up" ping landed AFTER the
1213
+ // edit it (1) made lastIsEdit false so completeStep("edit") never fired, and
1214
+ // (2) still advanced the baseline watermark past the edit's timestamp —
1215
+ // permanently stranding Step 2 even though the save rendered in "Your
1216
+ // activity". Filter to real edits (mirroring render()'s filter) and advance
1217
+ // the watermark only from real edits, so a liveness ping can neither satisfy
1218
+ // nor poison the gate.
1219
+ var realEdits = events.filter(function (e) { return e && e.event !== "up"; });
1220
+ var lastEdit = realEdits.length ? realEdits[realEdits.length - 1] : null;
1221
+ var latestEdit = lastEdit ? lastEdit.at : 0;
1126
1222
  // Step 1 "Get running" completes once localhost reports LIVE (or any activity
1127
1223
  // arrives). Once complete, renderSteps() minimizes Step 1 and reveals the Step-2 nudge.
1128
1224
  if (isLive || events.length) completeStep("run");
1129
1225
  if (baseline === null) {
1130
- setBaseline(latest); // first poll primes the watermark — no edit inferred yet
1131
- } else if (latest > baseline) {
1132
- setBaseline(latest);
1133
- // Step 2 completes only on a real FILE edit landing (not a running ping).
1226
+ setBaseline(latestEdit); // first poll primes the watermark from real edits — no edit inferred yet
1227
+ } else if (latestEdit > baseline) {
1228
+ setBaseline(latestEdit);
1229
+ // A real FILE edit landed after the watermark Step 2 completes (once).
1134
1230
  // completeStep flips + persists once, returning true on that first poll only.
1135
- if (lastIsEdit && completeStep("edit")) {
1136
- celebrate(last && last.file);
1231
+ if (completeStep("edit")) {
1232
+ celebrate(lastEdit && lastEdit.file);
1137
1233
  if (current === "edit") current = null; // minimize Step 2 → reveal Step-3 teaser
1138
1234
  }
1139
1235
  }
@@ -1179,13 +1275,21 @@ const hostedActivityScript = `
1179
1275
  <meta charset="utf-8" />
1180
1276
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1181
1277
  <meta name="robots" content="noindex, nofollow" />
1182
- <title>Get your store running — {appDomain}</title>
1183
- <link rel="preconnect" href="https://fonts.googleapis.com" />
1184
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
1185
- <link
1186
- rel="stylesheet"
1187
- href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@500;600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap"
1188
- />
1278
+ <title>Developer Cockpit — {appDomain}</title>
1279
+ {/* cockpit-theme-persist: seed data-theme from the STORED choice before the stylesheet/
1280
+ style below ever paint, so a reload of a manually-toggled theme doesn't flash the OS
1281
+ default first. Runs before the theme toggle's own script (end of body) exists — it
1282
+ only ever READS here; the toggle is what writes. No stored choice → leave the
1283
+ attribute unset, so the `@media (prefers-color-scheme)` block keeps deciding. */}
1284
+ <script nonce={cspNonce} is:inline>
1285
+ (function () {
1286
+ try {
1287
+ var v = localStorage.getItem("tot.theme");
1288
+ if (v === "dark" || v === "light") document.documentElement.setAttribute("data-theme", v);
1289
+ } catch (e) {}
1290
+ })();
1291
+ </script>
1292
+ {/* Web fonts are self-hosted via `@/styles/fonts.css` (imported above). */}
1189
1293
  <style nonce={cspNonce}>
1190
1294
  :root {
1191
1295
  --bg: #f7f9fc; --bg-2: #eef3f9; --surface: #ffffff;
@@ -1280,6 +1384,8 @@ const hostedActivityScript = `
1280
1384
  .brand .dom { font-family: var(--display); font-weight: 700; color: var(--ink); font-size: .98rem; letter-spacing: -.01em; }
1281
1385
  .brand .sub { font-family: var(--mono); font-size: .7rem; color: var(--faint); text-transform: uppercase; letter-spacing: .12em; }
1282
1386
  .topbar .right { display: flex; align-items: center; gap: .9rem; font-family: var(--mono); font-size: .78rem; color: var(--muted); }
1387
+ .cockpit-lede { margin: -0.6rem 0 1.6rem; font-family: var(--body); font-size: .92rem; color: var(--muted); max-width: 60ch; }
1388
+ .cockpit-lede strong { color: var(--ink); font-weight: 600; }
1283
1389
  .topbar .right .sess { display: inline-flex; align-items: center; gap: .45rem; }
1284
1390
  .topbar .right .sess::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 3px var(--accent-wash); }
1285
1391
  .themetog {
@@ -1600,6 +1706,34 @@ const hostedActivityScript = `
1600
1706
  .term-bar .dot { width: 11px; height: 11px; border-radius: 50%; }
1601
1707
  .dot.r { background: #ff5f56; } .dot.y { background: #ffbd2e; } .dot.g { background: #27c93f; }
1602
1708
  .term-bar .ttl { margin-left: .4rem; font-size: .72rem; color: var(--term-dim); }
1709
+ /* Standard "copy this code block" affordance, docked in the terminal chrome.
1710
+ Revealed once the command is minted; its icon→check swap is the obvious,
1711
+ inline copy confirmation (no reliance on the transient toast). */
1712
+ .term-copy {
1713
+ margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
1714
+ width: 30px; height: 26px; padding: 0; border-radius: var(--r-sm); cursor: pointer;
1715
+ background: rgba(255,255,255,.04); border: 1px solid var(--term-line);
1716
+ color: var(--term-dim); transition: color .12s, background .12s, border-color .12s;
1717
+ }
1718
+ .term-copy:hover { color: var(--term-fg); background: rgba(255,255,255,.09); border-color: var(--term-dim); }
1719
+ .term-copy svg { width: 15px; height: 15px; }
1720
+ .term-copy .ic-check { display: none; }
1721
+ .term-copy[data-copied="1"] { color: var(--term-ok); border-color: var(--term-ok); }
1722
+ .term-copy[data-copied="1"] .ic-copy { display: none; }
1723
+ .term-copy[data-copied="1"] .ic-check { display: block; }
1724
+ /* Rendezvous approval widget — the confirm-your-terminal step after pasting. */
1725
+ .rzv { margin-top: 14px; }
1726
+ .rzv[hidden] { display: none; }
1727
+ .rzv-wait { display: flex; align-items: center; gap: 10px; font-size: .92rem; opacity: .8; }
1728
+ .rzv-confirm { border: 1px solid var(--term-ok, #0e9f8e); border-radius: 12px; padding: 16px 18px; background: color-mix(in srgb, var(--term-ok, #0e9f8e) 8%, transparent); }
1729
+ .rzv-q { margin: 0 0 6px; font-weight: 600; }
1730
+ .rzv-fp { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1.75rem; letter-spacing: .12em; text-align: center; margin: 8px 0 16px; }
1731
+ .rzv-actions { display: flex; gap: 10px; flex-wrap: wrap; }
1732
+ .rzv-actions .cta { margin: 0; }
1733
+ .rzv-deny { background: none; border: 1px solid currentColor; opacity: .7; border-radius: 8px; padding: 8px 14px; cursor: pointer; color: inherit; font: inherit; }
1734
+ .rzv-deny:hover { opacity: 1; }
1735
+ .rzv-deny[disabled], .rzv-actions .cta[disabled] { opacity: .5; cursor: default; }
1736
+ .rzv-done { margin-top: 12px; font-weight: 600; }
1603
1737
  .term-body { padding: 1rem 1.05rem 1.1rem; color: var(--term-fg); white-space: pre-wrap; word-break: break-word; }
1604
1738
  /* block, not inline — Astro trims the whitespace text node between the spans, so the
1605
1739
  line break must be structural or the comment + command collapse onto one line. */
@@ -1775,6 +1909,9 @@ const hostedActivityScript = `
1775
1909
  .df-view { flex: 1 1 auto; width: 100%; box-sizing: border-box; display: block; margin: 0; border: 0; resize: none; outline: none; overflow: auto; padding: .75rem .9rem; font-family: var(--mono); font-size: .76rem; line-height: 1.55; color: var(--term-fg); background: var(--term); white-space: pre; tab-size: 2; }
1776
1910
  .df-view[readonly] { opacity: .9; }
1777
1911
  .df-diffbtn { margin-left: auto; }
1912
+ .df-publishbtn { border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
1913
+ .df-publishbtn:hover { background: var(--accent-wash); }
1914
+ .df-publishbtn:disabled { opacity: .6; cursor: default; }
1778
1915
  .df-diff { flex: 1 1 auto; margin: 0; overflow: auto; padding: .5rem 0; font-family: var(--mono); font-size: .76rem; line-height: 1.5; background: var(--term); color: var(--term-fg); }
1779
1916
  .df-diff .dl { display: block; padding: 0 .9rem; white-space: pre; }
1780
1917
  .df-diff .add { background: rgba(52, 211, 153, .16); color: #bff2e0; }
@@ -1830,6 +1967,11 @@ const hostedActivityScript = `
1830
1967
  .ask-foot { padding: .7rem 1.1rem; border-top: 1px solid var(--border-2); font-family: var(--mono); font-size: .68rem; color: var(--faint); }
1831
1968
  @media (prefers-reduced-motion: reduce) { .ask-panel { transition: none; } }
1832
1969
 
1970
+ /* ---- connect the MCP (d3-unhold-dev-step0-connect-mcp) ---- */
1971
+ #stage-mcp { margin-top: 2rem; }
1972
+ /* disable ligatures so `--transport` renders as literal characters, not a glyph */
1973
+ #mcpCmdText { font-variant-ligatures: none; font-feature-settings: "liga" 0, "calt" 0; }
1974
+
1833
1975
  /* ---- what's next : coming soon shelf (U4) ---- */
1834
1976
  .next { margin-top: 3.4rem; padding-top: 2rem; border-top: 1px dashed var(--border); }
1835
1977
  .next h3 { font-family: var(--display); font-weight: 700; color: var(--ink-2); font-size: 1.1rem; margin: 0 0 .3rem; letter-spacing: -.02em; }
@@ -1881,7 +2023,7 @@ const hostedActivityScript = `
1881
2023
  <span class="mark" aria-hidden="true">
1882
2024
  <svg viewBox="0 0 24 24" fill="none"><path d="M12 2.5 20 6v6c0 4.6-3.2 7.9-8 9.5-4.8-1.6-8-4.9-8-9.5V6l8-3.5Z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m8.5 12 2.4 2.4L15.8 9.5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
1883
2025
  </span>
1884
- <span class="who"><span class="dom">{appDomain}</span><span class="sub">Developer</span></span>
2026
+ <span class="who"><span class="dom">{appDomain}</span><span class="sub">Developer Cockpit</span></span>
1885
2027
  </div>
1886
2028
  <div class="right">
1887
2029
  {session?.email ? <span class="sess">{session.email}</span> : null}
@@ -1889,11 +2031,23 @@ const hostedActivityScript = `
1889
2031
  </div>
1890
2032
  </header>
1891
2033
 
2034
+ {/* U1 anchor — name the surface so a developer knows which page is "home". */}
2035
+ <p class="cockpit-lede">This is your <strong>Developer Cockpit</strong> — your home base while your store spins up.</p>
2036
+
1892
2037
  {signedIn === false && (
1893
2038
  <div class="notice">
2039
+ {/* d1 (board: d1-unmask-invite-email) — the invitee owns this address (the
2040
+ link was sent to it), so masking it back to them is pointless friction.
2041
+ Show the FULL invite email when we actually have it; NEVER show the redacted
2042
+ hint to its owner. This branch is signed-OUT, so there's no session.email to
2043
+ read; the recovery link arrives carrying only a redacted `email_hint` (the
2044
+ full address is masked UPSTREAM by the CLI before it ever reaches this page —
2045
+ see report), so the full email is genuinely unavailable here and we fall back
2046
+ to neutral copy rather than re-displaying the redacted form. `recoveryEmailFull`
2047
+ lights up automatically the moment a full address is available. */}
1894
2048
  <p>{isCredentialRecovery
1895
- ? recoveryEmailHint
1896
- ? <>Sign in with the invite email hinted as <strong>{recoveryEmailHint}</strong> to generate a fresh setup command for this machine.</>
2049
+ ? recoveryEmailFull
2050
+ ? <>Sign in with your invite email, <strong>{recoveryEmailFull}</strong>, to generate a fresh setup command for this machine.</>
1897
2051
  : <>Sign in with the same email that accepted the invite to generate a fresh setup command for this machine.</>
1898
2052
  : <>Sign in with the email your invite came to — then this page shows your store and your one command.</>}</p>
1899
2053
  <a class="action" href={loginHref}>Sign in</a>
@@ -1902,8 +2056,27 @@ const hostedActivityScript = `
1902
2056
 
1903
2057
  {noStoreAccess && (
1904
2058
  <div class="notice">
1905
- <p>You're signed in{session?.email ? <>{" "}as <strong>{session.email}</strong></> : null}, but this account isn't linked to a store yet. Ask whoever sent your invite to grant developer access — then reload this page.</p>
1906
- <a class="action" href="/auth/logout">Switch account</a>
2059
+ {noResolvableStore
2060
+ ? <>
2061
+ <p>You're signed in{session?.email ? <>{" "}as <strong>{session.email}</strong></> : null}, but this page doesn't have a store to set up yet. Pick one from your dashboard to get its setup command.</p>
2062
+ <a class="action" href="/dashboard">Pick a store</a>
2063
+ </>
2064
+ : <>
2065
+ <p>You're signed in{session?.email ? <>{" "}as <strong>{session.email}</strong></> : null}, but this account isn't linked to a store yet. Ask whoever sent your invite to grant developer access — then reload this page.</p>
2066
+ <a class="action" href="/auth/logout">Switch account</a>
2067
+ </>}
2068
+ </div>
2069
+ )}
2070
+
2071
+ {/* STORE-NOT-SET-UP notice — a real, signed-in developer whose store's repo isn't
2072
+ provisioned yet. We deliberately withhold the setup command here (canSetupStore
2073
+ is false, so the mint fastpath + activity rail below don't render) rather than
2074
+ hand out a `tot start` that would die at checkout. Honest, low-friction copy:
2075
+ nothing for them to do but wait + reload. */}
2076
+ {storeNotSetUp && (
2077
+ <div class="notice">
2078
+ <p>You're all set{session?.email ? <>{" "}as <strong>{session.email}</strong></> : null} — but <strong>{appDomain}</strong>'s store is still being set up. Your one-paste setup command appears here the moment it's ready. Nothing to do on your end; check back shortly, or ask your Token of Trust contact if it's urgent.</p>
2079
+ <a class="action" href="/cockpit">Reload</a>
1907
2080
  </div>
1908
2081
  )}
1909
2082
 
@@ -1988,31 +2161,62 @@ const hostedActivityScript = `
1988
2161
  <span class="info-pop" role="tooltip">Installs the <code>tot</code> command, signs your terminal in with a single-use code, checks out your store into a new <code>{appDomain}</code> folder, runs it, and opens your browser. No Docker, no browser sign-in, nothing to provision. From then on it's just <code>tot start</code>.</span>
1989
2162
  </span>
1990
2163
  </h1>
1991
- <p class="goal">{hasStoreAccess
2164
+ <p class="goal">{canSetupStore
1992
2165
  ? isCredentialRecovery
1993
2166
  ? <>Generate a fresh personal setup command, then paste it into your terminal and press Enter.</>
1994
2167
  : <>Generate your personal setup command, then paste it into your terminal and press Enter.</>
1995
- : <>Open the invite link we emailed you to unlock your personal setup command.</>}</p>
2168
+ : storeNotSetUp
2169
+ ? <>We're finishing setting up <strong>{appDomain}</strong>. Your one-paste setup command appears here the moment it's ready — check back shortly.</>
2170
+ : <>Open the invite link we emailed you to unlock your personal setup command.</>}</p>
1996
2171
 
1997
2172
  <div class="term">
1998
- <div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">your terminal</span></div>
2173
+ <div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">your terminal</span>{canSetupStore && (
2174
+ <button class="term-copy" id="copyIcon" type="button" aria-label="Copy the command" title="Copy the command" hidden>
2175
+ <svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
2176
+ <svg class="ic-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
2177
+ </button>
2178
+ )}</div>
1999
2179
  <div class="term-body"><span class="c"># your personal, single-use setup command</span>
2000
- <span class="cmd is-placeholder" id="cli-cmd-text">{hasStoreAccess ? (isCredentialRecovery ? "→ click “Generate a fresh setup command” below" : "→ click “Generate my setup command” below") : "→ available once you’re signed in via your invite"}</span></div>
2180
+ <span class="cmd is-placeholder" id="cli-cmd-text">{canSetupStore ? (isCredentialRecovery ? "→ click “Generate a fresh setup command” below" : "→ click “Generate my setup command” below") : storeNotSetUp ? "→ available once your store finishes setting up" : "→ available once you’re signed in via your invite"}</span></div>
2001
2181
  </div>
2002
2182
 
2003
- {hasStoreAccess && (
2183
+ {canSetupStore && (
2004
2184
  <div class="cmd-actions">
2005
- <button class="cta" id="copyCmd" type="button" data-fastpath="1">
2185
+ <button class="cta" id="copyCmd" type="button" data-fastpath="1" data-base-path={basePath}>
2006
2186
  <svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
2007
2187
  {isCredentialRecovery ? "Generate a fresh setup command" : "Generate my setup command"}
2008
2188
  </button>
2009
2189
  </div>
2010
2190
  )}
2011
- <p class="cmd-note" id="cli-cmd-note">{hasStoreAccess
2191
+
2192
+ {/* Rendezvous approval — after you paste the command, your terminal connects here and
2193
+ you confirm the code it printed matches (the load-bearing security step: it's how
2194
+ you prove it's YOUR terminal, not an interceptor's). Hidden until a command is
2195
+ generated; the client script drives the wait → confirm → done states. */}
2196
+ {canSetupStore && (
2197
+ <div class="rzv" id="rzvApprove" hidden aria-live="polite">
2198
+ <div class="rzv-wait" id="rzvWait" hidden>
2199
+ <span class="radar" aria-hidden="true"></span>
2200
+ <span>Waiting for your terminal to connect — run the pasted command, then come back here.</span>
2201
+ </div>
2202
+ <div class="rzv-confirm" id="rzvConfirm" hidden>
2203
+ <p class="rzv-q">Your terminal is asking to sign in. Confirm the code it printed matches:</p>
2204
+ <p class="rzv-fp" id="rzvFp">····-····</p>
2205
+ <div class="rzv-actions">
2206
+ <button class="cta" id="rzvApproveBtn" type="button">Yes — that's my terminal, approve</button>
2207
+ <button class="rzv-deny" id="rzvDenyBtn" type="button">Not me — reject</button>
2208
+ </div>
2209
+ </div>
2210
+ <p class="rzv-done" id="rzvDone" hidden></p>
2211
+ </div>
2212
+ )}
2213
+ <p class="cmd-note" id="cli-cmd-note">{canSetupStore
2012
2214
  ? <>This mints a <em>single-use, short-lived</em> sign-in code, shows the full command here, and copies it to your clipboard. Paste it <em>from the folder where you keep projects</em> — it creates a <code>{appDomain}</code> folder there with your store's code, signs you in, runs your store, and opens your browser. Nothing to retype.</>
2013
- : <>You'll need <em>Node.js 22.12+ (24 LTS recommended) and your invite</em>. Sign in through your invite link and this page hands you a one-paste setup command — no Docker, nothing to provision.</>}</p>
2215
+ : storeNotSetUp
2216
+ ? <>Nothing to do on your end — a Token of Trust reviewer is finishing your store's first setup. Once it's ready, reload and this page hands you the one-paste command.</>
2217
+ : <>You'll need <em>Node.js 22.12+ (24 LTS recommended) and your invite</em>. Sign in through your invite link and this page hands you a one-paste setup command — no Docker, nothing to provision.</>}</p>
2014
2218
 
2015
- <p class="cmd-steps">Prefer the steps? <code>npm i -g {cliInstallSpec}</code>, then <code>tot checkout {appDomain}</code>, <code>cd {appDomain}</code>, <code>tot dev</code>. New to Node? Get Node 24 (LTS) at <a href="https://nodejs.org">nodejs.org</a> — it includes <code>npm</code>.</p>
2219
+ <p class="cmd-steps">Prefer the steps? <code>npm i -g {cliInstallSpec}</code>, then <code>tot clone {appDomain}</code>, <code>cd {appDomain}</code>, <code>tot dev</code>. New to Node? Get Node 24 (LTS) at <a href="https://nodejs.org">nodejs.org</a> — it includes <code>npm</code>.</p>
2016
2220
 
2017
2221
  {isLocalDevLoop && (
2018
2222
  <div class="detect" id="detect" data-listening="true">
@@ -2082,11 +2286,11 @@ const hostedActivityScript = `
2082
2286
  <div class="stage-body">
2083
2287
  <span class="kicker">Step 3</span>
2084
2288
  <h1>Submit your change for preview.</h1>
2085
- <p class="goal">You've got the loop: edit locally, see it reflected here. When a change is ready, submit it for a preview build it goes through the compliance review step before it can go live.</p>
2289
+ <p class="goal">You've got the loop: edit locally, see it reflected here. When a change is ready, <code>tot preview</code> bundles it into a compliance-reviewed preview build; once that preview looks right, <code>tot ship</code> promotes it live.</p>
2086
2290
  <div class="term">
2087
2291
  <div class="term-bar"><span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">preview</span></div>
2088
2292
  <div class="term-body"><span class="c"># bundle your local changes into a reviewable preview</span>
2089
- <span class="cmd">COMING SOON (tot submit)</span></div>
2293
+ <span class="cmd">tot preview</span></div>
2090
2294
  </div>
2091
2295
  <div class="cmd-actions">
2092
2296
  <button class="cta" id="submitPreview" type="button">
@@ -2094,7 +2298,7 @@ const hostedActivityScript = `
2094
2298
  Submit for preview
2095
2299
  </button>
2096
2300
  </div>
2097
- <p class="cmd-note">This packages your changes and kicks off a compliance-reviewed preview build.</p>
2301
+ <p class="cmd-note">This packages your changes and kicks off a compliance-reviewed preview build. Once it reconciles cleanly, <code>tot ship</code> promotes it live — it always shows you the diff and asks for a confirm first.</p>
2098
2302
  </div>
2099
2303
  </section>
2100
2304
  </div>
@@ -2166,6 +2370,7 @@ const hostedActivityScript = `
2166
2370
  <button type="button" class="df-mini" id="filesForce">Overwrite</button>
2167
2371
  </span>
2168
2372
  <button type="button" class="df-mini df-diffbtn" id="filesDiffBtn" hidden>Diff</button>
2373
+ <button type="button" class="df-mini df-publishbtn" id="filesPublishBtn" hidden>Publish</button>
2169
2374
  </div>
2170
2375
  <textarea class="df-view" id="filesView" readonly spellcheck="false" wrap="off">Pick a file on the left to view its source.</textarea>
2171
2376
  <pre class="df-diff" id="filesDiff" hidden></pre>
@@ -2218,18 +2423,44 @@ const hostedActivityScript = `
2218
2423
  </div>
2219
2424
  )}
2220
2425
 
2426
+ {/* d3-unhold-dev-step0-connect-mcp: un-held now that MCP access follows the tot20
2427
+ developer grant (federated sign-in / 8425 Stage B is live) — an invited developer
2428
+ already has what this needs, so it's no longer aspirational. Copy/command carried
2429
+ over from the pre-U1-strip /dev (dev.astro @ 110f73e); the "FUNCTIONALLY PENDING"
2430
+ caveat from e58eab5 is dropped since the prerequisite is now real. Static, renders
2431
+ unconditionally like the "next" teaser grid below — not gated on capability. */}
2432
+ <section class="stage" id="stage-mcp">
2433
+ <div class="stage-body">
2434
+ <span class="kicker">Connect the MCP</span>
2435
+ <h1>Bring your AI assistant into the loop.</h1>
2436
+ <p class="goal">The Regulated Storefront is a native AI platform — connecting the Token of Trust MCP lets Claude, Cursor, or any MCP-speaking assistant work your store directly. It works with every major AI platform. On Claude:</p>
2437
+ <div class="term">
2438
+ <div class="term-bar">
2439
+ <span class="dots"><span class="dot r"></span><span class="dot y"></span><span class="dot g"></span></span><span class="ttl">Claude Code</span>
2440
+ <button class="term-copy" id="mcpCopyIcon" type="button" aria-label="Copy the command" title="Copy the command">
2441
+ <svg class="ic-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
2442
+ <svg class="ic-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>
2443
+ </button>
2444
+ </div>
2445
+ <div class="term-body"><span class="c"># add the Token of Trust MCP</span>
2446
+ <span class="cmd" id="mcpCmdText">claude mcp add --transport http tot https://mcp.tokenoftrust.com/mcp</span>
2447
+ <span class="c">&nbsp;</span>
2448
+ <span class="c"># then run /mcp in Claude and sign in with your invite email</span></div>
2449
+ </div>
2450
+ <p class="cmd-note">Other assistants: point them at <code>https://mcp.tokenoftrust.com/mcp</code>. Sign in with the email your invite came to — your access follows your developer grant on this store.</p>
2451
+ </div>
2452
+ </section>
2453
+
2221
2454
  {/* Static "coming soon" cards — pure /dev rendering with no dependency on the
2222
2455
  isLocalDevLoop-only /__tot/* endpoints and no need for the activity bridge, so
2223
2456
  it renders unconditionally on the deployed hosted panel (and locally). Not
2224
- gated on capability either — it's a teaser of what's next, and AI is one card
2225
- among several, not a prerequisite. */}
2457
+ gated on capability either — it's a teaser of what's next. */}
2226
2458
  <section class="next">
2227
2459
  <h3>The rest of the workflow</h3>
2228
2460
  <p class="sub">Previews, publish checks, AI assistance, and diagnostics. These open up once you've got the loop — coming soon.</p>
2229
2461
  <div class="nextgrid">
2230
2462
  <div class="nextcard"><span class="path">Share a preview</span><span class="d">Put a branch or PR up at a shareable URL.</span><span class="soon">Coming soon</span></div>
2231
2463
  <div class="nextcard"><span class="path">Publish readiness</span><span class="d">What happens if you publish today — checks &amp; blockers.</span><span class="soon">Coming soon</span></div>
2232
- <div class="nextcard"><span class="path">Edit with AI</span><span class="d">Connect Claude, Cursor, or Codex to your store.</span><span class="soon">Coming soon</span></div>
2233
2464
  <div class="nextcard"><span class="path">Troubleshooting</span><span class="d">Doctor, logs, server health, and auth checks.</span><span class="soon">Coming soon</span></div>
2234
2465
  </div>
2235
2466
  </section>
@@ -2304,16 +2535,55 @@ const hostedActivityScript = `
2304
2535
  )}
2305
2536
 
2306
2537
  <script nonce={cspNonce} is:inline>
2307
- // Theme toggle: flip data-theme on <html>, seeded from the OS preference on first click.
2538
+ // Theme toggle: flip data-theme on <html>, seeded from the OS preference on first
2539
+ // click, and PERSISTED to localStorage so a reload stays on the manual choice (the
2540
+ // head seed script above reads this same key before first paint). Clearing storage
2541
+ // falls back to prefers-color-scheme again, same as a first-ever visit.
2308
2542
  (function () {
2309
2543
  var root = document.documentElement;
2310
2544
  var btn = document.getElementById("themeBtn");
2311
2545
  if (!btn) return;
2546
+ var KEY = "tot.theme";
2547
+ function store(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
2312
2548
  btn.addEventListener("click", function () {
2313
2549
  var cur = root.getAttribute("data-theme");
2314
2550
  var next = cur === "dark" ? "light" : cur === "light" ? "dark"
2315
2551
  : (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark");
2316
2552
  root.setAttribute("data-theme", next);
2553
+ store(next);
2554
+ });
2555
+ })();
2556
+ </script>
2557
+
2558
+ <script nonce={cspNonce} is:inline>
2559
+ // Copy the "connect the MCP" command (d3-unhold-dev-step0-connect-mcp). Static
2560
+ // text (no minting), so unlike #copyIcon this button is visible from load.
2561
+ (function () {
2562
+ var copyIcon = document.getElementById("mcpCopyIcon");
2563
+ var cmdText = document.getElementById("mcpCmdText");
2564
+ var toast = document.getElementById("toast");
2565
+ var toastMsg = document.getElementById("toastMsg");
2566
+ if (!copyIcon || !cmdText) return;
2567
+ var tT;
2568
+ function ping(m) {
2569
+ if (!toast || !toastMsg) return;
2570
+ toastMsg.textContent = m; toast.classList.add("show");
2571
+ clearTimeout(tT); tT = setTimeout(function () { toast.classList.remove("show"); }, 1900);
2572
+ }
2573
+ copyIcon.addEventListener("click", function () {
2574
+ var text = cmdText.textContent;
2575
+ var done = navigator.clipboard
2576
+ ? navigator.clipboard.writeText(text).then(function () { return true; }).catch(function () { return false; })
2577
+ : Promise.resolve(false);
2578
+ done.then(function (ok) {
2579
+ if (ok) {
2580
+ copyIcon.setAttribute("data-copied", "1");
2581
+ setTimeout(function () { copyIcon.removeAttribute("data-copied"); }, 1600);
2582
+ ping("Copied");
2583
+ } else {
2584
+ ping("Select the command above to copy");
2585
+ }
2586
+ });
2317
2587
  });
2318
2588
  })();
2319
2589
  </script>
@@ -2341,9 +2611,16 @@ const hostedActivityScript = `
2341
2611
  var note = document.getElementById("cli-cmd-note");
2342
2612
  var toast = document.getElementById("toast");
2343
2613
  var toastMsg = document.getElementById("toastMsg");
2614
+ var copyIcon = document.getElementById("copyIcon");
2344
2615
  if (!btn || !textEl) return;
2345
2616
  var generic = btn.getAttribute("data-generic") || "";
2346
2617
  var fastpath = btn.getAttribute("data-fastpath") === "1";
2618
+ // The request's tenant prefix ("" host-routed, "/<tenant>" path-routed). Carried
2619
+ // as a data-attr because `basePath` is otherwise defined only inside the SEPARATE
2620
+ // hostedActivity IIFE — referencing it here (the cli-signin-code / -status / -approve
2621
+ // fetches below) was a ReferenceError that the mint's catch surfaced as the opaque
2622
+ // "Couldn't reach the sign-in service" with no request ever sent.
2623
+ var basePath = btn.getAttribute("data-base-path") || "";
2347
2624
  var tT;
2348
2625
  function ping(m) {
2349
2626
  if (!toast) return;
@@ -2356,6 +2633,123 @@ const hostedActivityScript = `
2356
2633
  }
2357
2634
  function label(s) { btn.childNodes[btn.childNodes.length - 1].textContent = s; }
2358
2635
 
2636
+ // Standard copy-icon affordance on the terminal box. Once the command is
2637
+ // revealed, this is the obvious, always-there way to (re)copy it — the
2638
+ // icon→check swap is its own inline confirmation, so copying no longer
2639
+ // depends on the transient toast being noticed.
2640
+ var iconT;
2641
+ function flashCopied() {
2642
+ if (!copyIcon) return;
2643
+ copyIcon.setAttribute("data-copied", "1");
2644
+ copyIcon.setAttribute("aria-label", "Copied");
2645
+ copyIcon.setAttribute("title", "Copied");
2646
+ clearTimeout(iconT);
2647
+ iconT = setTimeout(function () {
2648
+ copyIcon.removeAttribute("data-copied");
2649
+ copyIcon.setAttribute("aria-label", "Copy the command");
2650
+ copyIcon.setAttribute("title", "Copy the command");
2651
+ }, 1600);
2652
+ }
2653
+ function revealIcon() {
2654
+ if (copyIcon) copyIcon.hidden = false;
2655
+ }
2656
+ if (copyIcon) {
2657
+ copyIcon.addEventListener("click", function () {
2658
+ copy(textEl.textContent).then(function (ok) {
2659
+ if (ok) { flashCopied(); ping("Copied — paste & press Enter"); }
2660
+ else ping("Select the command above to copy");
2661
+ });
2662
+ });
2663
+ }
2664
+
2665
+ // ── Rendezvous approval: after a command is generated, the terminal ATTACHES
2666
+ // its own PKCE key and prints a fingerprint; the developer confirms it HERE. This
2667
+ // is the load-bearing anti-interception step — approving the wrong fingerprint is
2668
+ // how a hijack is caught, so the confirm is an explicit, human decision.
2669
+ var rzvRoot = document.getElementById("rzvApprove");
2670
+ var rzvWait = document.getElementById("rzvWait");
2671
+ var rzvConfirm = document.getElementById("rzvConfirm");
2672
+ var rzvFp = document.getElementById("rzvFp");
2673
+ var rzvApproveBtn = document.getElementById("rzvApproveBtn");
2674
+ var rzvDenyBtn = document.getElementById("rzvDenyBtn");
2675
+ var rzvDone = document.getElementById("rzvDone");
2676
+ var rzvTimer = null, rzvCode = null, rzvDecided = false;
2677
+ function rzvShow(el) { if (el) el.hidden = false; }
2678
+ function rzvHide(el) { if (el) el.hidden = true; }
2679
+ // Once the terminal has ATTACHED, the command has plainly been pasted and
2680
+ // run — hide the copy affordances so the approve prompt is the page's
2681
+ // single focus (DZ 2026-08-18). An EXPIRED sign-in restores the generate
2682
+ // button (a fresh mint is needed); the stale command's copy icon stays gone.
2683
+ function rzvHideCopyCtas() {
2684
+ if (btn) btn.hidden = true;
2685
+ if (copyIcon) copyIcon.hidden = true;
2686
+ }
2687
+ function rzvRestoreGenerate() {
2688
+ if (btn) btn.hidden = false;
2689
+ }
2690
+ function rzvStopPoll() { if (rzvTimer) { clearTimeout(rzvTimer); rzvTimer = null; } }
2691
+ function rzvFinish(msg) {
2692
+ rzvStopPoll(); rzvDecided = true;
2693
+ rzvHide(rzvWait); rzvHide(rzvConfirm);
2694
+ if (rzvDone) { rzvDone.textContent = msg; rzvDone.hidden = false; }
2695
+ }
2696
+ async function rzvPoll() {
2697
+ if (rzvDecided) return;
2698
+ var r, b;
2699
+ try {
2700
+ r = await fetch(basePath + "/api/dev/cli-signin-status", {
2701
+ method: "POST", headers: { "content-type": "application/json" },
2702
+ body: JSON.stringify({ rendezvousCode: rzvCode }),
2703
+ });
2704
+ b = await r.json().catch(function () { return {}; });
2705
+ } catch (e) { rzvTimer = setTimeout(rzvPoll, 2800); return; }
2706
+ if (!r || !r.ok) { rzvTimer = setTimeout(rzvPoll, 3200); return; }
2707
+ if (b.state === "attached" && b.fingerprint) {
2708
+ rzvHideCopyCtas();
2709
+ rzvHide(rzvWait); rzvShow(rzvConfirm);
2710
+ if (rzvFp) { rzvFp.textContent = b.fingerprint; rzvFp.setAttribute("data-fp", b.fingerprint); }
2711
+ return; // stop polling — wait for the human decision
2712
+ }
2713
+ if (b.state === "approved") { rzvHideCopyCtas(); rzvFinish("Signed in ✓ — your terminal is good to go."); return; }
2714
+ if (b.state === "denied") { rzvHideCopyCtas(); rzvFinish("Rejected — that terminal wasn’t approved. You’re safe; generate a fresh command to try again."); return; }
2715
+ if (b.state === "expired") { rzvRestoreGenerate(); rzvFinish("This sign-in expired. Generate a fresh setup command above."); return; }
2716
+ rzvTimer = setTimeout(rzvPoll, 2500); // pending → keep watching
2717
+ }
2718
+ function rzvStart(code) {
2719
+ rzvCode = code; rzvDecided = false;
2720
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2721
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2722
+ rzvShow(rzvRoot); rzvShow(rzvWait); rzvHide(rzvConfirm); rzvHide(rzvDone);
2723
+ rzvStopPoll(); rzvPoll();
2724
+ }
2725
+ async function rzvDecide(decision) {
2726
+ if (rzvDecided) return;
2727
+ var fp = rzvFp ? (rzvFp.getAttribute("data-fp") || "") : "";
2728
+ if (rzvApproveBtn) rzvApproveBtn.disabled = true;
2729
+ if (rzvDenyBtn) rzvDenyBtn.disabled = true;
2730
+ var r;
2731
+ try {
2732
+ r = await fetch(basePath + "/api/dev/cli-signin-approve", {
2733
+ method: "POST", headers: { "content-type": "application/json" },
2734
+ body: JSON.stringify({ rendezvousCode: rzvCode, fingerprint: fp, decision: decision }),
2735
+ });
2736
+ } catch (e) {
2737
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2738
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2739
+ return;
2740
+ }
2741
+ if (decision === "deny") { rzvFinish("Rejected — that terminal wasn’t approved."); return; }
2742
+ if (r.ok) { rzvFinish("Signed in ✓ — your terminal is good to go."); return; }
2743
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2744
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2745
+ var m = r.status === 403 ? "That didn’t match your identity — generate a fresh command and try again."
2746
+ : r.status === 409 ? "That code didn’t match a waiting terminal — re-check it, or generate a fresh command."
2747
+ : "Couldn’t complete approval — try again.";
2748
+ if (rzvDone) { rzvDone.textContent = m; rzvDone.hidden = false; }
2749
+ }
2750
+ if (rzvApproveBtn) rzvApproveBtn.addEventListener("click", function () { rzvDecide("approve"); });
2751
+ if (rzvDenyBtn) rzvDenyBtn.addEventListener("click", function () { rzvDecide("deny"); });
2752
+
2359
2753
  if (!fastpath) {
2360
2754
  btn.addEventListener("click", function () {
2361
2755
  copy(generic).then(function (ok) {
@@ -2374,7 +2768,7 @@ const hostedActivityScript = `
2374
2768
  label(" …"); btn.disabled = true;
2375
2769
  var res, body;
2376
2770
  try {
2377
- res = await fetch("/api/dev/cli-signin-code", {
2771
+ res = await fetch(basePath + "/api/dev/cli-signin-code", {
2378
2772
  method: "POST",
2379
2773
  headers: { "content-type": "application/json" },
2380
2774
  body: "{}",
@@ -2389,6 +2783,7 @@ const hostedActivityScript = `
2389
2783
  var byStatus = {
2390
2784
  401: "Your session expired — reload the page and sign in again.",
2391
2785
  403: "This account doesn’t have developer access to a store yet.",
2786
+ 409: "Your store isn’t set up yet — reload this page for its status.",
2392
2787
  502: "The sign-in service is having a moment — try again.",
2393
2788
  503: "Terminal sign-in isn’t configured on this environment.",
2394
2789
  };
@@ -2396,9 +2791,15 @@ const hostedActivityScript = `
2396
2791
  label(" Copy the command"); btn.disabled = false; busy = false;
2397
2792
  return;
2398
2793
  }
2399
- // Reveal the real command (contains the one-time code) as a pure text node, then copy it.
2794
+ // Reveal the real command (carries the non-secret rendezvous handle) as a pure
2795
+ // text node, then copy it.
2400
2796
  textEl.textContent = body.command;
2797
+ textEl.classList.remove("is-placeholder");
2798
+ revealIcon();
2799
+ // Start watching for the terminal to attach so we can prompt for approval.
2800
+ if (body.rendezvousCode) rzvStart(body.rendezvousCode);
2401
2801
  var ok = await copy(body.command);
2802
+ if (ok) flashCopied();
2402
2803
  label(ok ? " Copied ✓" : " Select & copy");
2403
2804
  ping(ok ? "Copied — paste & press Enter" : "Select the command above to copy");
2404
2805
  if (note) {