@tokenoftrust/storefront-runner 1.3.4-rc.4 → 1.4.0

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 (224) 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/meta/0000_snapshot.json +843 -0
  9. package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
  10. package/apps/storefront/package.json +13 -1
  11. package/apps/storefront/perf/README.md +64 -0
  12. package/apps/storefront/perf/assert-budgets.ts +159 -0
  13. package/apps/storefront/playwright.config.ts +23 -0
  14. package/apps/storefront/public/js/dashboard-apps.js +173 -0
  15. package/apps/storefront/public/shared/commerce-marketing.css +221 -0
  16. package/apps/storefront/src/components/CollectionCard.astro +1 -0
  17. package/apps/storefront/src/components/ProductCard.astro +1 -0
  18. package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
  19. package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
  20. package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
  21. package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
  22. package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
  23. package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
  24. package/apps/storefront/src/components/content/Callout.astro +75 -0
  25. package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
  26. package/apps/storefront/src/components/content/ProseSections.astro +121 -0
  27. package/apps/storefront/src/components/content/ProseToc.astro +34 -0
  28. package/apps/storefront/src/components/content/RichText.astro +44 -0
  29. package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
  30. package/apps/storefront/src/components/home/Hero.astro +14 -0
  31. package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
  32. package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
  33. package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
  34. package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
  35. package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
  36. package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
  37. package/apps/storefront/src/config/storeName.ts +34 -0
  38. package/apps/storefront/src/layouts/Layout.astro +97 -23
  39. package/apps/storefront/src/lib/analytics/budgets.json +69 -0
  40. package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
  41. package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
  42. package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
  43. package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
  44. package/apps/storefront/src/lib/apps/adminService.ts +106 -0
  45. package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
  46. package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
  47. package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
  48. package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
  49. package/apps/storefront/src/lib/apps/credentials.ts +153 -0
  50. package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
  51. package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
  52. package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
  53. package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
  54. package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
  55. package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
  56. package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
  57. package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
  58. package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
  59. package/apps/storefront/src/lib/apps/registryService.ts +579 -0
  60. package/apps/storefront/src/lib/apps/scopes.ts +79 -0
  61. package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
  62. package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
  63. package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
  64. package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
  65. package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
  66. package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
  67. package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
  68. package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
  69. package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
  70. package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
  71. package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
  72. package/apps/storefront/src/lib/auth/route.ts +16 -1
  73. package/apps/storefront/src/lib/auth/session.ts +8 -0
  74. package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
  75. package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
  76. package/apps/storefront/src/lib/blog/provider.ts +39 -0
  77. package/apps/storefront/src/lib/blog/types.ts +26 -0
  78. package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
  79. package/apps/storefront/src/lib/chrome/model.ts +11 -0
  80. package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
  81. package/apps/storefront/src/lib/content/callout.ts +78 -0
  82. package/apps/storefront/src/lib/content/index.ts +27 -0
  83. package/apps/storefront/src/lib/content/needsReview.ts +58 -0
  84. package/apps/storefront/src/lib/content/prose.ts +186 -0
  85. package/apps/storefront/src/lib/content/richtext.ts +76 -0
  86. package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
  87. package/apps/storefront/src/lib/content-edit/client.ts +70 -14
  88. package/apps/storefront/src/lib/d1/catalog.ts +12 -0
  89. package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
  90. package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
  91. package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
  92. package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
  93. package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
  94. package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
  95. package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
  96. package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
  97. package/apps/storefront/src/lib/env.ts +12 -0
  98. package/apps/storefront/src/lib/jsonld.ts +12 -17
  99. package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
  100. package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
  101. package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
  102. package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
  103. package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
  104. package/apps/storefront/src/lib/publish/domainState.ts +351 -0
  105. package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
  106. package/apps/storefront/src/lib/rawChrome.ts +34 -3
  107. package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
  108. package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
  109. package/apps/storefront/src/lib/subscription/model.ts +114 -0
  110. package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
  111. package/apps/storefront/src/lib/tot/query.ts +32 -0
  112. package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
  113. package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
  114. package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
  115. package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
  116. package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
  117. package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
  118. package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
  119. package/apps/storefront/src/lib/webhooks/events.ts +98 -0
  120. package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
  121. package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
  122. package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
  123. package/apps/storefront/src/middleware/index.ts +28 -13
  124. package/apps/storefront/src/pages/404.astro +21 -9
  125. package/apps/storefront/src/pages/[...slug].astro +36 -2
  126. package/apps/storefront/src/pages/admin.astro +29 -3
  127. package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
  128. package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
  129. package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
  130. package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
  131. package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
  132. package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
  133. package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
  134. package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
  135. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
  136. package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
  137. package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
  138. package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
  139. package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
  140. package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
  141. package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
  142. package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
  143. package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
  144. package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
  145. package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
  146. package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
  147. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
  148. package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
  149. package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
  150. package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
  151. package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
  152. package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
  153. package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
  154. package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
  155. package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
  156. package/apps/storefront/src/pages/auth/login.astro +197 -41
  157. package/apps/storefront/src/pages/auth/magic.astro +75 -43
  158. package/apps/storefront/src/pages/blog/[slug].astro +107 -0
  159. package/apps/storefront/src/pages/blog/index.astro +98 -0
  160. package/apps/storefront/src/pages/capabilities.astro +8 -0
  161. package/apps/storefront/src/pages/cockpit.astro +433 -69
  162. package/apps/storefront/src/pages/collections/[handle].astro +8 -0
  163. package/apps/storefront/src/pages/collections/index.astro +10 -2
  164. package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
  165. package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
  166. package/apps/storefront/src/pages/index.astro +57 -0
  167. package/apps/storefront/src/pages/llms.txt.ts +31 -10
  168. package/apps/storefront/src/pages/products/[handle].astro +100 -9
  169. package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
  170. package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
  171. package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
  172. package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
  173. package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
  174. package/apps/storefront/src/pages/style-guide/index.astro +10 -0
  175. package/apps/storefront/src/styles/fonts.css +54 -0
  176. package/apps/storefront/src/styles/global.css +22 -2
  177. package/apps/storefront/src/themes/schema.ts +3 -25
  178. package/apps/storefront/tsconfig.json +1 -1
  179. package/apps/storefront/vitest.config.ts +4 -1
  180. package/package.json +1 -1
  181. package/packages/public-runtime/src/candidate-index.ts +311 -0
  182. package/packages/public-runtime/src/checkout.ts +94 -2
  183. package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
  184. package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
  185. package/packages/public-runtime/src/compliance/index.ts +22 -0
  186. package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
  187. package/packages/public-runtime/src/compliance/profile.ts +198 -0
  188. package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
  189. package/packages/public-runtime/src/compliance/verification.ts +81 -0
  190. package/packages/public-runtime/src/csp.ts +27 -3
  191. package/packages/public-runtime/src/customization-reconcile.ts +35 -0
  192. package/packages/public-runtime/src/customization-runtime.ts +8 -0
  193. package/packages/public-runtime/src/customization-versioning.ts +17 -0
  194. package/packages/public-runtime/src/extension-contract.ts +2 -1
  195. package/packages/public-runtime/src/hash.ts +25 -0
  196. package/packages/public-runtime/src/index.ts +6 -0
  197. package/packages/public-runtime/src/membership.ts +353 -0
  198. package/packages/public-runtime/src/product.ts +24 -2
  199. package/packages/public-runtime/src/review-trust-proof.ts +194 -0
  200. package/packages/public-runtime/src/tenant-assets.ts +40 -5
  201. package/packages/public-runtime/src/tenant.ts +236 -0
  202. package/packages/public-runtime/src/widget-postmessage.ts +205 -0
  203. package/scripts/dev/publish.mjs +158 -0
  204. package/scripts/dev/transient-files.mjs +2 -1
  205. package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
  206. package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
  207. package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
  208. package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
  209. package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
  210. package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
  211. package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
  212. package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
  213. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
  214. package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
  215. package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
  216. package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
  217. package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
  218. package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
  219. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
  220. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
  221. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
  222. package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
  223. package/tenants/home/public/pages/storefront.css +23 -0
  224. 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,24 @@
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";
28
30
  import { resolveDevApiBase } from "@/lib/dev/apiBase";
31
+ import { resolveCockpitStoreResource } from "@/lib/dev/cockpitStore";
29
32
  import { normalizeEmailHint, redactEmailForHint } from "@/lib/privacy/emailHint";
33
+ // Self-hosted web fonts (audit finding 6) — same-origin @fontsource, replacing
34
+ // the render-blocking fonts.googleapis.com stylesheet. Covers Plus Jakarta Sans,
35
+ // Inter, and JetBrains Mono used by this page's inline styles.
36
+ import "@/styles/fonts.css";
30
37
 
31
38
  export const prerender = false;
32
39
  const cspNonce = Astro.locals.cspNonce;
33
40
  const url = new URL(Astro.request.url);
41
+ // The tenant prefix ("" host-routed, "/<tenant>" path-routed) this cockpit is served
42
+ // under — set by the middleware. Every self-link + same-store API call below carries
43
+ // it so a `/<tenant>/cockpit` invite keeps its store across the login round-trip and
44
+ // the command mint (see resolveCockpitStoreResource / the basePath-scoped fetches).
45
+ const basePath = Astro.locals.basePath ?? "";
34
46
  const isCredentialRecovery = url.searchParams.get("recover") === "cli";
35
47
  const legacyRecoveryEmail = (url.searchParams.get("email") ?? "").trim();
36
48
  const recoveryEmailHint =
@@ -42,34 +54,41 @@ if (isCredentialRecovery && url.searchParams.has("email")) {
42
54
  canonical.delete("email");
43
55
  if (recoveryEmailHint) canonical.set("email_hint", recoveryEmailHint);
44
56
  return Astro.redirect(
45
- `/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
57
+ `${basePath}/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
46
58
  302,
47
59
  );
48
60
  }
49
- const cockpitNext = isCredentialRecovery ? "/cockpit?recover=cli" : "/cockpit";
61
+ const cockpitNext = `${basePath}/cockpit${isCredentialRecovery ? "?recover=cli" : ""}`;
50
62
  const loginParams = new URLSearchParams({ next: cockpitNext });
51
63
  if (recoveryEmailHint) loginParams.set("email_hint", recoveryEmailHint);
52
64
  const loginHref = `/auth/login?${loginParams.toString()}`;
53
65
 
54
66
  const session = await readViewerSession({ cookies: Astro.cookies });
55
67
  const signedIn = !!session;
68
+ // d1 (board: d1-unmask-invite-email) — the FULL invite address to show its OWNER in the
69
+ // recovery notice, never a redacted hint. Prefer the validated session email; else a full
70
+ // `?email=` if one ever survives to render. Empty when only a redacted `email_hint` is on
71
+ // hand (the common case — the CLI masks the address upstream before it reaches this page),
72
+ // and the notice then falls back to neutral, non-redacted copy rather than re-showing the
73
+ // owner a masked form of their own address. redactEmailForHint/emailHint.ts stay intact for
74
+ // every other caller (e.g. the /auth/login hint below) — we simply stop masking HERE.
75
+ const recoveryEmailFull =
76
+ session?.email ||
77
+ (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(legacyRecoveryEmail) ? legacyRecoveryEmail.toLowerCase() : "");
56
78
  // The fastpath (browserless terminal sign-in) is gated behind the developer's own session +
57
79
  // capability — the same gate /api/dev/cli-signin-code enforces. Mirror it here so a
58
80
  // logged-out / no-capability visitor gets the generic command, never a broken mint.
59
81
  const hasStoreAccess = signedIn && !!session?.capability && session.capability !== "none";
60
82
 
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();
83
+ // Resolve which store this cockpit is for. The URL wins when a `/<tenant>/cockpit`
84
+ // request named a specific store the signed-in developer can access (the trusted
85
+ // host/path seam on `locals.tenant`, membership-checked) this is the regression
86
+ // fix: an invite to another store than the session's first membership no longer
87
+ // "loses" that store. Otherwise: the developer's own first store (generic host), or
88
+ // for an ANONYMOUS viewer only the path store's branding, else `?tenant=`. Never
89
+ // a signed-in developer's store overridden by caller-supplied query input.
90
+ // See resolveCockpitStoreResource; the same resolver gates the mint + /api/dev/*.
91
+ const wanted = resolveCockpitStoreResource(Astro, session);
73
92
  const tenant =
74
93
  (wanted
75
94
  ? staticTenants.find((t) =>
@@ -82,9 +101,42 @@ const tenant =
82
101
  staticTenants[0];
83
102
 
84
103
  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;
104
+
105
+ // fix-staff-cockpit-403: `hasStoreAccess` above only proves the session carries SOME
106
+ // eligible capability the federated sign-in path stamps capability:"member"
107
+ // unconditionally (auth/magic-exchange), so a ToT-staff viewer admitted by the
108
+ // loginGate staff bypass (no real store membership) passes it too, even on the home
109
+ // host where `wanted` never names a real store. Every /api/dev/* route instead gates
110
+ // on this SAME tenant actually RESOLVING (resolvePreviewTenant — see
111
+ // lib/dev/hostedCockpit.ts, api/dev/activity.ts) — mirror that here so we never mint a
112
+ // setup command or start polling for a viewer who'd just 403-loop against it.
113
+ const resolvedStoreTenant = hasStoreAccess && wanted ? await resolvePreviewTenant(wanted) : null;
114
+ // Capability-eligible, but no store resolves for this viewer/host (the staff/home-host
115
+ // case above). Gets its own calm "pick a store" state instead of the full flow.
116
+ const noResolvableStore = hasStoreAccess && !resolvedStoreTenant;
117
+
118
+ // STORE-NOT-SET-UP (#8425 invite-gated-auto-build): a developer WITH access + a
119
+ // resolvable store, but whose forge repo isn't provisioned yet — the invite landed
120
+ // before (or without) the store being set up. Offering the fastpath here would hand
121
+ // them a `tot login --code … && tot start` that dies at checkout on "Repo … is not
122
+ // provisioned" (James's first-run failure). Ask the credential-free MCP preview-status
123
+ // seam; only a DEFINITIVE not-provisioned flips the state (an unconfigured / can't-answer
124
+ // seam keeps the normal fastpath, with the CLI's own clean "not set up yet" error as the
125
+ // backstop — so no regression on local/dev or a transient MCP blip). This is a DISTINCT
126
+ // axis from noResolvableStore/noStoreAccess and gets its own honest notice below.
127
+ const storeNotSetUp =
128
+ hasStoreAccess && !noResolvableStore && isStoreNotSetUp(await readTenantPreviewStatus(appDomain));
129
+
130
+ // The gate for rendering the setup-command + activity-poll flow: eligible AND a real
131
+ // store to point it at AND that store is actually provisioned. Use this (never the raw
132
+ // hasStoreAccess) for anything that mints a command or starts the activity poll.
133
+ const canSetupStore = hasStoreAccess && !noResolvableStore && !storeNotSetUp;
134
+
135
+ // Signed in, but either no developer capability on any store OR (staff bypass) no store
136
+ // resolves for this viewer/host — an honest recovery state (not a 401): show what to
137
+ // do, not the fastpath. NOTE: storeNotSetUp is deliberately NOT folded in here — it is
138
+ // its own state with its own notice, so this stays computed from resolvable-store only.
139
+ const noStoreAccess = signedIn && (!hasStoreAccess || noResolvableStore);
88
140
  // The generic command shown (and copied) when we can't mint a personalised one.
89
141
  // Pinned to the EXACT CLI version this build was cut alongside (`@tokenoftrust/cli@<version>`,
90
142
  // astro.config Vite define) so the paste installs precisely the CLI this cockpit expects —
@@ -112,7 +164,7 @@ const isLocalDevLoop = import.meta.env.DEV;
112
164
  // on our hosted surface. It is NOT a port of the local-only Monitor/Files/preview
113
165
  // panels (those depend on /__tot/* endpoints that exist only under `astro dev`;
114
166
  // bringing those hosted is cp-tree/cp-file/cp-preview, tracked separately).
115
- const hostedActivity = !isLocalDevLoop && hasStoreAccess;
167
+ const hostedActivity = !isLocalDevLoop && canSetupStore;
116
168
 
117
169
  // cp-env (cloud-dev-cockpit Phase 0): the file-tree/viewer/diff + click-to-source
118
170
  // cockpit scripts below (filesScript, c8Script) are ENVIRONMENT-AWARE — same client
@@ -121,7 +173,7 @@ const hostedActivity = !isLocalDevLoop && hasStoreAccess;
121
173
  // cp-locate — separate cards). Resolving it here, unconditionally, means those cards
122
174
  // don't each have to invent the same env check; on isLocalDevLoop this is a no-op
123
175
  // (resolves to the unchanged /__tot base). See decision architecture-environment-aware-cockpit.
124
- const apiBase = resolveDevApiBase(isLocalDevLoop);
176
+ const apiBase = resolveDevApiBase(isLocalDevLoop, basePath);
125
177
 
126
178
  // Live loop Monitor (C1) client. Polls the dev server's /__tot/dev-loop-status
127
179
  // and renders the loopbar: server health, watched file, and the save→reload
@@ -568,20 +620,22 @@ const filesScript = `
568
620
  }
569
621
  if (diffBtn) diffBtn.addEventListener("click", doDiff);
570
622
 
571
- // cp-publish: draft -> TEST channel (never live see decision
572
- // cp-publish-scope-correction). Confirm-before-publish per the card.
623
+ // cp-publish: draft -> PR-backed candidate for review (unit s6, milestone
624
+ // "PR-Backed Hosted Review Loop") never straight to live, and no longer
625
+ // straight to the test channel either; it opens/updates a pull request an
626
+ // owner reviews and accepts. Confirm-before-submit per the card.
573
627
  function doPublish() {
574
628
  if (!cur || !cur.writable || !publishBtn) return;
575
- if (!window.confirm("Publish your saved edit to the test channel? (Not your live site.)")) return;
629
+ if (!window.confirm("Submit your saved edit for review? (Opens/updates a pull request \\u2014 not your live site.)")) return;
576
630
  var prevLabel = publishBtn.textContent;
577
631
  publishBtn.disabled = true;
578
- setStatus("Publishing\\u2026");
632
+ setStatus("Submitting\\u2026");
579
633
  fetch(apiBase + "/publish", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ path: cur.path }) })
580
634
  .then(function (r) { return r.json().then(function (j) { return { s: r.status, j: j }; }); })
581
635
  .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");
636
+ if (res.s === 200 && res.j && res.j.ok) { setStatus("Submitted for review \\u2713" + (res.j.prNumber ? " (PR #" + res.j.prNumber + ")" : ""), "ok"); return; }
637
+ if (res.s === 409 && res.j && res.j.status === "no_draft") { setStatus("No unpublished edit to submit.", "warn"); return; }
638
+ setStatus((res.j && res.j.message) || "Submit failed", "warn");
585
639
  })
586
640
  .catch(function () { setStatus(offlineMsg, "warn"); })
587
641
  .then(function () { publishBtn.disabled = false; publishBtn.textContent = prevLabel; });
@@ -718,6 +772,12 @@ const hostedActivityScript = `
718
772
  var root = document.getElementById("hostedActivity");
719
773
  if (!root) return;
720
774
 
775
+ // The request's tenant prefix ("" host-routed, "/<tenant>" path-routed). Every
776
+ // /api/dev/* call below carries it so the activity feed + the CLI sign-in-code mint
777
+ // run in THIS store's scope (locals.tenant on the API request) — the same store this
778
+ // /<tenant>/cockpit rendered for, never the session's first membership.
779
+ var basePath = ${JSON.stringify(basePath)};
780
+
721
781
  // ?reset — a TRUE reset for re-testing the onboarding loop: clear THIS browser's
722
782
  // stepper progress AND the server-side KV (live-runtime record + activity buffer),
723
783
  // then reload clean. Clearing the server too means a stopped local loop stops reading
@@ -730,7 +790,7 @@ const hostedActivityScript = `
730
790
  var reloaded = false;
731
791
  var doneReset = function () { if (reloaded) return; reloaded = true; location.replace(location.pathname); };
732
792
  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); }
793
+ try { fetch(basePath + "/api/dev/activity", { method: "DELETE", cache: "no-store" }).then(doneReset, doneReset); }
734
794
  catch (e) { doneReset(); }
735
795
  return; // the post-clear reload re-runs this script against a fresh, empty state
736
796
  }
@@ -1111,7 +1171,7 @@ const hostedActivityScript = `
1111
1171
  }
1112
1172
 
1113
1173
  function poll() {
1114
- fetch("/api/dev/activity", { cache: "no-store" })
1174
+ fetch(basePath + "/api/dev/activity", { cache: "no-store" })
1115
1175
  .then(function (r) { return r.ok ? r.json() : Promise.reject(); })
1116
1176
  .then(function (d) {
1117
1177
  var events = (d && d.events) || [];
@@ -1120,20 +1180,31 @@ const hostedActivityScript = `
1120
1180
  liveNow = isLive; // drives Step 1 minimize + the Step-2 teaser visibility below
1121
1181
  updateBridge(rt, d && d.obstacle); // same poll drives the bridge strip (+ obstacle lane)
1122
1182
  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
1183
+ // b1 fix Step-2 completion must key off the SAME events the feed shows:
1184
+ // REAL file edits (event !== "up"), NOT the single newest buffer entry. The
1185
+ // local loop interleaves recurring "up" liveness pings into this one ring
1186
+ // buffer, so the newest event is frequently an "up" ping even right after a
1187
+ // save. The old gate read events[last]: when an "up" ping landed AFTER the
1188
+ // edit it (1) made lastIsEdit false so completeStep("edit") never fired, and
1189
+ // (2) still advanced the baseline watermark past the edit's timestamp —
1190
+ // permanently stranding Step 2 even though the save rendered in "Your
1191
+ // activity". Filter to real edits (mirroring render()'s filter) and advance
1192
+ // the watermark only from real edits, so a liveness ping can neither satisfy
1193
+ // nor poison the gate.
1194
+ var realEdits = events.filter(function (e) { return e && e.event !== "up"; });
1195
+ var lastEdit = realEdits.length ? realEdits[realEdits.length - 1] : null;
1196
+ var latestEdit = lastEdit ? lastEdit.at : 0;
1126
1197
  // Step 1 "Get running" completes once localhost reports LIVE (or any activity
1127
1198
  // arrives). Once complete, renderSteps() minimizes Step 1 and reveals the Step-2 nudge.
1128
1199
  if (isLive || events.length) completeStep("run");
1129
1200
  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).
1201
+ setBaseline(latestEdit); // first poll primes the watermark from real edits — no edit inferred yet
1202
+ } else if (latestEdit > baseline) {
1203
+ setBaseline(latestEdit);
1204
+ // A real FILE edit landed after the watermark Step 2 completes (once).
1134
1205
  // completeStep flips + persists once, returning true on that first poll only.
1135
- if (lastIsEdit && completeStep("edit")) {
1136
- celebrate(last && last.file);
1206
+ if (completeStep("edit")) {
1207
+ celebrate(lastEdit && lastEdit.file);
1137
1208
  if (current === "edit") current = null; // minimize Step 2 → reveal Step-3 teaser
1138
1209
  }
1139
1210
  }
@@ -1179,13 +1250,21 @@ const hostedActivityScript = `
1179
1250
  <meta charset="utf-8" />
1180
1251
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1181
1252
  <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
- />
1253
+ <title>Developer Cockpit — {appDomain}</title>
1254
+ {/* cockpit-theme-persist: seed data-theme from the STORED choice before the stylesheet/
1255
+ style below ever paint, so a reload of a manually-toggled theme doesn't flash the OS
1256
+ default first. Runs before the theme toggle's own script (end of body) exists — it
1257
+ only ever READS here; the toggle is what writes. No stored choice → leave the
1258
+ attribute unset, so the `@media (prefers-color-scheme)` block keeps deciding. */}
1259
+ <script nonce={cspNonce} is:inline>
1260
+ (function () {
1261
+ try {
1262
+ var v = localStorage.getItem("tot.theme");
1263
+ if (v === "dark" || v === "light") document.documentElement.setAttribute("data-theme", v);
1264
+ } catch (e) {}
1265
+ })();
1266
+ </script>
1267
+ {/* Web fonts are self-hosted via `@/styles/fonts.css` (imported above). */}
1189
1268
  <style nonce={cspNonce}>
1190
1269
  :root {
1191
1270
  --bg: #f7f9fc; --bg-2: #eef3f9; --surface: #ffffff;
@@ -1280,6 +1359,8 @@ const hostedActivityScript = `
1280
1359
  .brand .dom { font-family: var(--display); font-weight: 700; color: var(--ink); font-size: .98rem; letter-spacing: -.01em; }
1281
1360
  .brand .sub { font-family: var(--mono); font-size: .7rem; color: var(--faint); text-transform: uppercase; letter-spacing: .12em; }
1282
1361
  .topbar .right { display: flex; align-items: center; gap: .9rem; font-family: var(--mono); font-size: .78rem; color: var(--muted); }
1362
+ .cockpit-lede { margin: -0.6rem 0 1.6rem; font-family: var(--body); font-size: .92rem; color: var(--muted); max-width: 60ch; }
1363
+ .cockpit-lede strong { color: var(--ink); font-weight: 600; }
1283
1364
  .topbar .right .sess { display: inline-flex; align-items: center; gap: .45rem; }
1284
1365
  .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
1366
  .themetog {
@@ -1600,6 +1681,34 @@ const hostedActivityScript = `
1600
1681
  .term-bar .dot { width: 11px; height: 11px; border-radius: 50%; }
1601
1682
  .dot.r { background: #ff5f56; } .dot.y { background: #ffbd2e; } .dot.g { background: #27c93f; }
1602
1683
  .term-bar .ttl { margin-left: .4rem; font-size: .72rem; color: var(--term-dim); }
1684
+ /* Standard "copy this code block" affordance, docked in the terminal chrome.
1685
+ Revealed once the command is minted; its icon→check swap is the obvious,
1686
+ inline copy confirmation (no reliance on the transient toast). */
1687
+ .term-copy {
1688
+ margin-left: auto; display: inline-flex; align-items: center; justify-content: center;
1689
+ width: 30px; height: 26px; padding: 0; border-radius: var(--r-sm); cursor: pointer;
1690
+ background: rgba(255,255,255,.04); border: 1px solid var(--term-line);
1691
+ color: var(--term-dim); transition: color .12s, background .12s, border-color .12s;
1692
+ }
1693
+ .term-copy:hover { color: var(--term-fg); background: rgba(255,255,255,.09); border-color: var(--term-dim); }
1694
+ .term-copy svg { width: 15px; height: 15px; }
1695
+ .term-copy .ic-check { display: none; }
1696
+ .term-copy[data-copied="1"] { color: var(--term-ok); border-color: var(--term-ok); }
1697
+ .term-copy[data-copied="1"] .ic-copy { display: none; }
1698
+ .term-copy[data-copied="1"] .ic-check { display: block; }
1699
+ /* Rendezvous approval widget — the confirm-your-terminal step after pasting. */
1700
+ .rzv { margin-top: 14px; }
1701
+ .rzv[hidden] { display: none; }
1702
+ .rzv-wait { display: flex; align-items: center; gap: 10px; font-size: .92rem; opacity: .8; }
1703
+ .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); }
1704
+ .rzv-q { margin: 0 0 6px; font-weight: 600; }
1705
+ .rzv-fp { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 1.75rem; letter-spacing: .12em; text-align: center; margin: 8px 0 16px; }
1706
+ .rzv-actions { display: flex; gap: 10px; flex-wrap: wrap; }
1707
+ .rzv-actions .cta { margin: 0; }
1708
+ .rzv-deny { background: none; border: 1px solid currentColor; opacity: .7; border-radius: 8px; padding: 8px 14px; cursor: pointer; color: inherit; font: inherit; }
1709
+ .rzv-deny:hover { opacity: 1; }
1710
+ .rzv-deny[disabled], .rzv-actions .cta[disabled] { opacity: .5; cursor: default; }
1711
+ .rzv-done { margin-top: 12px; font-weight: 600; }
1603
1712
  .term-body { padding: 1rem 1.05rem 1.1rem; color: var(--term-fg); white-space: pre-wrap; word-break: break-word; }
1604
1713
  /* block, not inline — Astro trims the whitespace text node between the spans, so the
1605
1714
  line break must be structural or the comment + command collapse onto one line. */
@@ -1775,6 +1884,9 @@ const hostedActivityScript = `
1775
1884
  .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
1885
  .df-view[readonly] { opacity: .9; }
1777
1886
  .df-diffbtn { margin-left: auto; }
1887
+ .df-publishbtn { border-color: var(--accent); color: var(--accent-ink); font-weight: 600; }
1888
+ .df-publishbtn:hover { background: var(--accent-wash); }
1889
+ .df-publishbtn:disabled { opacity: .6; cursor: default; }
1778
1890
  .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
1891
  .df-diff .dl { display: block; padding: 0 .9rem; white-space: pre; }
1780
1892
  .df-diff .add { background: rgba(52, 211, 153, .16); color: #bff2e0; }
@@ -1830,6 +1942,11 @@ const hostedActivityScript = `
1830
1942
  .ask-foot { padding: .7rem 1.1rem; border-top: 1px solid var(--border-2); font-family: var(--mono); font-size: .68rem; color: var(--faint); }
1831
1943
  @media (prefers-reduced-motion: reduce) { .ask-panel { transition: none; } }
1832
1944
 
1945
+ /* ---- connect the MCP (d3-unhold-dev-step0-connect-mcp) ---- */
1946
+ #stage-mcp { margin-top: 2rem; }
1947
+ /* disable ligatures so `--transport` renders as literal characters, not a glyph */
1948
+ #mcpCmdText { font-variant-ligatures: none; font-feature-settings: "liga" 0, "calt" 0; }
1949
+
1833
1950
  /* ---- what's next : coming soon shelf (U4) ---- */
1834
1951
  .next { margin-top: 3.4rem; padding-top: 2rem; border-top: 1px dashed var(--border); }
1835
1952
  .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 +1998,7 @@ const hostedActivityScript = `
1881
1998
  <span class="mark" aria-hidden="true">
1882
1999
  <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
2000
  </span>
1884
- <span class="who"><span class="dom">{appDomain}</span><span class="sub">Developer</span></span>
2001
+ <span class="who"><span class="dom">{appDomain}</span><span class="sub">Developer Cockpit</span></span>
1885
2002
  </div>
1886
2003
  <div class="right">
1887
2004
  {session?.email ? <span class="sess">{session.email}</span> : null}
@@ -1889,11 +2006,23 @@ const hostedActivityScript = `
1889
2006
  </div>
1890
2007
  </header>
1891
2008
 
2009
+ {/* U1 anchor — name the surface so a developer knows which page is "home". */}
2010
+ <p class="cockpit-lede">This is your <strong>Developer Cockpit</strong> — your home base while your store spins up.</p>
2011
+
1892
2012
  {signedIn === false && (
1893
2013
  <div class="notice">
2014
+ {/* d1 (board: d1-unmask-invite-email) — the invitee owns this address (the
2015
+ link was sent to it), so masking it back to them is pointless friction.
2016
+ Show the FULL invite email when we actually have it; NEVER show the redacted
2017
+ hint to its owner. This branch is signed-OUT, so there's no session.email to
2018
+ read; the recovery link arrives carrying only a redacted `email_hint` (the
2019
+ full address is masked UPSTREAM by the CLI before it ever reaches this page —
2020
+ see report), so the full email is genuinely unavailable here and we fall back
2021
+ to neutral copy rather than re-displaying the redacted form. `recoveryEmailFull`
2022
+ lights up automatically the moment a full address is available. */}
1894
2023
  <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.</>
2024
+ ? recoveryEmailFull
2025
+ ? <>Sign in with your invite email, <strong>{recoveryEmailFull}</strong>, to generate a fresh setup command for this machine.</>
1897
2026
  : <>Sign in with the same email that accepted the invite to generate a fresh setup command for this machine.</>
1898
2027
  : <>Sign in with the email your invite came to — then this page shows your store and your one command.</>}</p>
1899
2028
  <a class="action" href={loginHref}>Sign in</a>
@@ -1902,8 +2031,27 @@ const hostedActivityScript = `
1902
2031
 
1903
2032
  {noStoreAccess && (
1904
2033
  <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>
2034
+ {noResolvableStore
2035
+ ? <>
2036
+ <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>
2037
+ <a class="action" href="/dashboard">Pick a store</a>
2038
+ </>
2039
+ : <>
2040
+ <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>
2041
+ <a class="action" href="/auth/logout">Switch account</a>
2042
+ </>}
2043
+ </div>
2044
+ )}
2045
+
2046
+ {/* STORE-NOT-SET-UP notice — a real, signed-in developer whose store's repo isn't
2047
+ provisioned yet. We deliberately withhold the setup command here (canSetupStore
2048
+ is false, so the mint fastpath + activity rail below don't render) rather than
2049
+ hand out a `tot start` that would die at checkout. Honest, low-friction copy:
2050
+ nothing for them to do but wait + reload. */}
2051
+ {storeNotSetUp && (
2052
+ <div class="notice">
2053
+ <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>
2054
+ <a class="action" href="/cockpit">Reload</a>
1907
2055
  </div>
1908
2056
  )}
1909
2057
 
@@ -1988,31 +2136,62 @@ const hostedActivityScript = `
1988
2136
  <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
2137
  </span>
1990
2138
  </h1>
1991
- <p class="goal">{hasStoreAccess
2139
+ <p class="goal">{canSetupStore
1992
2140
  ? isCredentialRecovery
1993
2141
  ? <>Generate a fresh personal setup command, then paste it into your terminal and press Enter.</>
1994
2142
  : <>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>
2143
+ : storeNotSetUp
2144
+ ? <>We're finishing setting up <strong>{appDomain}</strong>. Your one-paste setup command appears here the moment it's ready — check back shortly.</>
2145
+ : <>Open the invite link we emailed you to unlock your personal setup command.</>}</p>
1996
2146
 
1997
2147
  <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>
2148
+ <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 && (
2149
+ <button class="term-copy" id="copyIcon" type="button" aria-label="Copy the command" title="Copy the command" hidden>
2150
+ <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>
2151
+ <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>
2152
+ </button>
2153
+ )}</div>
1999
2154
  <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>
2155
+ <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
2156
  </div>
2002
2157
 
2003
- {hasStoreAccess && (
2158
+ {canSetupStore && (
2004
2159
  <div class="cmd-actions">
2005
- <button class="cta" id="copyCmd" type="button" data-fastpath="1">
2160
+ <button class="cta" id="copyCmd" type="button" data-fastpath="1" data-base-path={basePath}>
2006
2161
  <svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
2007
2162
  {isCredentialRecovery ? "Generate a fresh setup command" : "Generate my setup command"}
2008
2163
  </button>
2009
2164
  </div>
2010
2165
  )}
2011
- <p class="cmd-note" id="cli-cmd-note">{hasStoreAccess
2166
+
2167
+ {/* Rendezvous approval — after you paste the command, your terminal connects here and
2168
+ you confirm the code it printed matches (the load-bearing security step: it's how
2169
+ you prove it's YOUR terminal, not an interceptor's). Hidden until a command is
2170
+ generated; the client script drives the wait → confirm → done states. */}
2171
+ {canSetupStore && (
2172
+ <div class="rzv" id="rzvApprove" hidden aria-live="polite">
2173
+ <div class="rzv-wait" id="rzvWait" hidden>
2174
+ <span class="radar" aria-hidden="true"></span>
2175
+ <span>Waiting for your terminal to connect — run the pasted command, then come back here.</span>
2176
+ </div>
2177
+ <div class="rzv-confirm" id="rzvConfirm" hidden>
2178
+ <p class="rzv-q">Your terminal is asking to sign in. Confirm the code it printed matches:</p>
2179
+ <p class="rzv-fp" id="rzvFp">····-····</p>
2180
+ <div class="rzv-actions">
2181
+ <button class="cta" id="rzvApproveBtn" type="button">Yes — that's my terminal, approve</button>
2182
+ <button class="rzv-deny" id="rzvDenyBtn" type="button">Not me — reject</button>
2183
+ </div>
2184
+ </div>
2185
+ <p class="rzv-done" id="rzvDone" hidden></p>
2186
+ </div>
2187
+ )}
2188
+ <p class="cmd-note" id="cli-cmd-note">{canSetupStore
2012
2189
  ? <>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>
2190
+ : storeNotSetUp
2191
+ ? <>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.</>
2192
+ : <>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
2193
 
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>
2194
+ <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
2195
 
2017
2196
  {isLocalDevLoop && (
2018
2197
  <div class="detect" id="detect" data-listening="true">
@@ -2082,11 +2261,11 @@ const hostedActivityScript = `
2082
2261
  <div class="stage-body">
2083
2262
  <span class="kicker">Step 3</span>
2084
2263
  <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>
2264
+ <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
2265
  <div class="term">
2087
2266
  <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
2267
  <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>
2268
+ <span class="cmd">tot preview</span></div>
2090
2269
  </div>
2091
2270
  <div class="cmd-actions">
2092
2271
  <button class="cta" id="submitPreview" type="button">
@@ -2094,7 +2273,7 @@ const hostedActivityScript = `
2094
2273
  Submit for preview
2095
2274
  </button>
2096
2275
  </div>
2097
- <p class="cmd-note">This packages your changes and kicks off a compliance-reviewed preview build.</p>
2276
+ <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
2277
  </div>
2099
2278
  </section>
2100
2279
  </div>
@@ -2166,6 +2345,7 @@ const hostedActivityScript = `
2166
2345
  <button type="button" class="df-mini" id="filesForce">Overwrite</button>
2167
2346
  </span>
2168
2347
  <button type="button" class="df-mini df-diffbtn" id="filesDiffBtn" hidden>Diff</button>
2348
+ <button type="button" class="df-mini df-publishbtn" id="filesPublishBtn" hidden>Publish</button>
2169
2349
  </div>
2170
2350
  <textarea class="df-view" id="filesView" readonly spellcheck="false" wrap="off">Pick a file on the left to view its source.</textarea>
2171
2351
  <pre class="df-diff" id="filesDiff" hidden></pre>
@@ -2218,18 +2398,44 @@ const hostedActivityScript = `
2218
2398
  </div>
2219
2399
  )}
2220
2400
 
2401
+ {/* d3-unhold-dev-step0-connect-mcp: un-held now that MCP access follows the tot20
2402
+ developer grant (federated sign-in / 8425 Stage B is live) — an invited developer
2403
+ already has what this needs, so it's no longer aspirational. Copy/command carried
2404
+ over from the pre-U1-strip /dev (dev.astro @ 110f73e); the "FUNCTIONALLY PENDING"
2405
+ caveat from e58eab5 is dropped since the prerequisite is now real. Static, renders
2406
+ unconditionally like the "next" teaser grid below — not gated on capability. */}
2407
+ <section class="stage" id="stage-mcp">
2408
+ <div class="stage-body">
2409
+ <span class="kicker">Connect the MCP</span>
2410
+ <h1>Bring your AI assistant into the loop.</h1>
2411
+ <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>
2412
+ <div class="term">
2413
+ <div class="term-bar">
2414
+ <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>
2415
+ <button class="term-copy" id="mcpCopyIcon" type="button" aria-label="Copy the command" title="Copy the command">
2416
+ <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>
2417
+ <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>
2418
+ </button>
2419
+ </div>
2420
+ <div class="term-body"><span class="c"># add the Token of Trust MCP</span>
2421
+ <span class="cmd" id="mcpCmdText">claude mcp add --transport http tot https://mcp.tokenoftrust.com/mcp</span>
2422
+ <span class="c">&nbsp;</span>
2423
+ <span class="c"># then run /mcp in Claude and sign in with your invite email</span></div>
2424
+ </div>
2425
+ <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>
2426
+ </div>
2427
+ </section>
2428
+
2221
2429
  {/* Static "coming soon" cards — pure /dev rendering with no dependency on the
2222
2430
  isLocalDevLoop-only /__tot/* endpoints and no need for the activity bridge, so
2223
2431
  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. */}
2432
+ gated on capability either — it's a teaser of what's next. */}
2226
2433
  <section class="next">
2227
2434
  <h3>The rest of the workflow</h3>
2228
2435
  <p class="sub">Previews, publish checks, AI assistance, and diagnostics. These open up once you've got the loop — coming soon.</p>
2229
2436
  <div class="nextgrid">
2230
2437
  <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
2438
  <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
2439
  <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
2440
  </div>
2235
2441
  </section>
@@ -2304,16 +2510,55 @@ const hostedActivityScript = `
2304
2510
  )}
2305
2511
 
2306
2512
  <script nonce={cspNonce} is:inline>
2307
- // Theme toggle: flip data-theme on <html>, seeded from the OS preference on first click.
2513
+ // Theme toggle: flip data-theme on <html>, seeded from the OS preference on first
2514
+ // click, and PERSISTED to localStorage so a reload stays on the manual choice (the
2515
+ // head seed script above reads this same key before first paint). Clearing storage
2516
+ // falls back to prefers-color-scheme again, same as a first-ever visit.
2308
2517
  (function () {
2309
2518
  var root = document.documentElement;
2310
2519
  var btn = document.getElementById("themeBtn");
2311
2520
  if (!btn) return;
2521
+ var KEY = "tot.theme";
2522
+ function store(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
2312
2523
  btn.addEventListener("click", function () {
2313
2524
  var cur = root.getAttribute("data-theme");
2314
2525
  var next = cur === "dark" ? "light" : cur === "light" ? "dark"
2315
2526
  : (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark");
2316
2527
  root.setAttribute("data-theme", next);
2528
+ store(next);
2529
+ });
2530
+ })();
2531
+ </script>
2532
+
2533
+ <script nonce={cspNonce} is:inline>
2534
+ // Copy the "connect the MCP" command (d3-unhold-dev-step0-connect-mcp). Static
2535
+ // text (no minting), so unlike #copyIcon this button is visible from load.
2536
+ (function () {
2537
+ var copyIcon = document.getElementById("mcpCopyIcon");
2538
+ var cmdText = document.getElementById("mcpCmdText");
2539
+ var toast = document.getElementById("toast");
2540
+ var toastMsg = document.getElementById("toastMsg");
2541
+ if (!copyIcon || !cmdText) return;
2542
+ var tT;
2543
+ function ping(m) {
2544
+ if (!toast || !toastMsg) return;
2545
+ toastMsg.textContent = m; toast.classList.add("show");
2546
+ clearTimeout(tT); tT = setTimeout(function () { toast.classList.remove("show"); }, 1900);
2547
+ }
2548
+ copyIcon.addEventListener("click", function () {
2549
+ var text = cmdText.textContent;
2550
+ var done = navigator.clipboard
2551
+ ? navigator.clipboard.writeText(text).then(function () { return true; }).catch(function () { return false; })
2552
+ : Promise.resolve(false);
2553
+ done.then(function (ok) {
2554
+ if (ok) {
2555
+ copyIcon.setAttribute("data-copied", "1");
2556
+ setTimeout(function () { copyIcon.removeAttribute("data-copied"); }, 1600);
2557
+ ping("Copied");
2558
+ } else {
2559
+ ping("Select the command above to copy");
2560
+ }
2561
+ });
2317
2562
  });
2318
2563
  })();
2319
2564
  </script>
@@ -2341,9 +2586,16 @@ const hostedActivityScript = `
2341
2586
  var note = document.getElementById("cli-cmd-note");
2342
2587
  var toast = document.getElementById("toast");
2343
2588
  var toastMsg = document.getElementById("toastMsg");
2589
+ var copyIcon = document.getElementById("copyIcon");
2344
2590
  if (!btn || !textEl) return;
2345
2591
  var generic = btn.getAttribute("data-generic") || "";
2346
2592
  var fastpath = btn.getAttribute("data-fastpath") === "1";
2593
+ // The request's tenant prefix ("" host-routed, "/<tenant>" path-routed). Carried
2594
+ // as a data-attr because `basePath` is otherwise defined only inside the SEPARATE
2595
+ // hostedActivity IIFE — referencing it here (the cli-signin-code / -status / -approve
2596
+ // fetches below) was a ReferenceError that the mint's catch surfaced as the opaque
2597
+ // "Couldn't reach the sign-in service" with no request ever sent.
2598
+ var basePath = btn.getAttribute("data-base-path") || "";
2347
2599
  var tT;
2348
2600
  function ping(m) {
2349
2601
  if (!toast) return;
@@ -2356,6 +2608,111 @@ const hostedActivityScript = `
2356
2608
  }
2357
2609
  function label(s) { btn.childNodes[btn.childNodes.length - 1].textContent = s; }
2358
2610
 
2611
+ // Standard copy-icon affordance on the terminal box. Once the command is
2612
+ // revealed, this is the obvious, always-there way to (re)copy it — the
2613
+ // icon→check swap is its own inline confirmation, so copying no longer
2614
+ // depends on the transient toast being noticed.
2615
+ var iconT;
2616
+ function flashCopied() {
2617
+ if (!copyIcon) return;
2618
+ copyIcon.setAttribute("data-copied", "1");
2619
+ copyIcon.setAttribute("aria-label", "Copied");
2620
+ copyIcon.setAttribute("title", "Copied");
2621
+ clearTimeout(iconT);
2622
+ iconT = setTimeout(function () {
2623
+ copyIcon.removeAttribute("data-copied");
2624
+ copyIcon.setAttribute("aria-label", "Copy the command");
2625
+ copyIcon.setAttribute("title", "Copy the command");
2626
+ }, 1600);
2627
+ }
2628
+ function revealIcon() {
2629
+ if (copyIcon) copyIcon.hidden = false;
2630
+ }
2631
+ if (copyIcon) {
2632
+ copyIcon.addEventListener("click", function () {
2633
+ copy(textEl.textContent).then(function (ok) {
2634
+ if (ok) { flashCopied(); ping("Copied — paste & press Enter"); }
2635
+ else ping("Select the command above to copy");
2636
+ });
2637
+ });
2638
+ }
2639
+
2640
+ // ── Rendezvous approval: after a command is generated, the terminal ATTACHES
2641
+ // its own PKCE key and prints a fingerprint; the developer confirms it HERE. This
2642
+ // is the load-bearing anti-interception step — approving the wrong fingerprint is
2643
+ // how a hijack is caught, so the confirm is an explicit, human decision.
2644
+ var rzvRoot = document.getElementById("rzvApprove");
2645
+ var rzvWait = document.getElementById("rzvWait");
2646
+ var rzvConfirm = document.getElementById("rzvConfirm");
2647
+ var rzvFp = document.getElementById("rzvFp");
2648
+ var rzvApproveBtn = document.getElementById("rzvApproveBtn");
2649
+ var rzvDenyBtn = document.getElementById("rzvDenyBtn");
2650
+ var rzvDone = document.getElementById("rzvDone");
2651
+ var rzvTimer = null, rzvCode = null, rzvDecided = false;
2652
+ function rzvShow(el) { if (el) el.hidden = false; }
2653
+ function rzvHide(el) { if (el) el.hidden = true; }
2654
+ function rzvStopPoll() { if (rzvTimer) { clearTimeout(rzvTimer); rzvTimer = null; } }
2655
+ function rzvFinish(msg) {
2656
+ rzvStopPoll(); rzvDecided = true;
2657
+ rzvHide(rzvWait); rzvHide(rzvConfirm);
2658
+ if (rzvDone) { rzvDone.textContent = msg; rzvDone.hidden = false; }
2659
+ }
2660
+ async function rzvPoll() {
2661
+ if (rzvDecided) return;
2662
+ var r, b;
2663
+ try {
2664
+ r = await fetch(basePath + "/api/dev/cli-signin-status", {
2665
+ method: "POST", headers: { "content-type": "application/json" },
2666
+ body: JSON.stringify({ rendezvousCode: rzvCode }),
2667
+ });
2668
+ b = await r.json().catch(function () { return {}; });
2669
+ } catch (e) { rzvTimer = setTimeout(rzvPoll, 2800); return; }
2670
+ if (!r || !r.ok) { rzvTimer = setTimeout(rzvPoll, 3200); return; }
2671
+ if (b.state === "attached" && b.fingerprint) {
2672
+ rzvHide(rzvWait); rzvShow(rzvConfirm);
2673
+ if (rzvFp) { rzvFp.textContent = b.fingerprint; rzvFp.setAttribute("data-fp", b.fingerprint); }
2674
+ return; // stop polling — wait for the human decision
2675
+ }
2676
+ if (b.state === "approved") { rzvFinish("Signed in ✓ — your terminal is good to go."); return; }
2677
+ if (b.state === "denied") { rzvFinish("Rejected — that terminal wasn’t approved. You’re safe; generate a fresh command to try again."); return; }
2678
+ if (b.state === "expired") { rzvFinish("This sign-in expired. Generate a fresh setup command above."); return; }
2679
+ rzvTimer = setTimeout(rzvPoll, 2500); // pending → keep watching
2680
+ }
2681
+ function rzvStart(code) {
2682
+ rzvCode = code; rzvDecided = false;
2683
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2684
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2685
+ rzvShow(rzvRoot); rzvShow(rzvWait); rzvHide(rzvConfirm); rzvHide(rzvDone);
2686
+ rzvStopPoll(); rzvPoll();
2687
+ }
2688
+ async function rzvDecide(decision) {
2689
+ if (rzvDecided) return;
2690
+ var fp = rzvFp ? (rzvFp.getAttribute("data-fp") || "") : "";
2691
+ if (rzvApproveBtn) rzvApproveBtn.disabled = true;
2692
+ if (rzvDenyBtn) rzvDenyBtn.disabled = true;
2693
+ var r;
2694
+ try {
2695
+ r = await fetch(basePath + "/api/dev/cli-signin-approve", {
2696
+ method: "POST", headers: { "content-type": "application/json" },
2697
+ body: JSON.stringify({ rendezvousCode: rzvCode, fingerprint: fp, decision: decision }),
2698
+ });
2699
+ } catch (e) {
2700
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2701
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2702
+ return;
2703
+ }
2704
+ if (decision === "deny") { rzvFinish("Rejected — that terminal wasn’t approved."); return; }
2705
+ if (r.ok) { rzvFinish("Signed in ✓ — your terminal is good to go."); return; }
2706
+ if (rzvApproveBtn) rzvApproveBtn.disabled = false;
2707
+ if (rzvDenyBtn) rzvDenyBtn.disabled = false;
2708
+ var m = r.status === 403 ? "That didn’t match your identity — generate a fresh command and try again."
2709
+ : r.status === 409 ? "That code didn’t match a waiting terminal — re-check it, or generate a fresh command."
2710
+ : "Couldn’t complete approval — try again.";
2711
+ if (rzvDone) { rzvDone.textContent = m; rzvDone.hidden = false; }
2712
+ }
2713
+ if (rzvApproveBtn) rzvApproveBtn.addEventListener("click", function () { rzvDecide("approve"); });
2714
+ if (rzvDenyBtn) rzvDenyBtn.addEventListener("click", function () { rzvDecide("deny"); });
2715
+
2359
2716
  if (!fastpath) {
2360
2717
  btn.addEventListener("click", function () {
2361
2718
  copy(generic).then(function (ok) {
@@ -2374,7 +2731,7 @@ const hostedActivityScript = `
2374
2731
  label(" …"); btn.disabled = true;
2375
2732
  var res, body;
2376
2733
  try {
2377
- res = await fetch("/api/dev/cli-signin-code", {
2734
+ res = await fetch(basePath + "/api/dev/cli-signin-code", {
2378
2735
  method: "POST",
2379
2736
  headers: { "content-type": "application/json" },
2380
2737
  body: "{}",
@@ -2389,6 +2746,7 @@ const hostedActivityScript = `
2389
2746
  var byStatus = {
2390
2747
  401: "Your session expired — reload the page and sign in again.",
2391
2748
  403: "This account doesn’t have developer access to a store yet.",
2749
+ 409: "Your store isn’t set up yet — reload this page for its status.",
2392
2750
  502: "The sign-in service is having a moment — try again.",
2393
2751
  503: "Terminal sign-in isn’t configured on this environment.",
2394
2752
  };
@@ -2396,9 +2754,15 @@ const hostedActivityScript = `
2396
2754
  label(" Copy the command"); btn.disabled = false; busy = false;
2397
2755
  return;
2398
2756
  }
2399
- // Reveal the real command (contains the one-time code) as a pure text node, then copy it.
2757
+ // Reveal the real command (carries the non-secret rendezvous handle) as a pure
2758
+ // text node, then copy it.
2400
2759
  textEl.textContent = body.command;
2760
+ textEl.classList.remove("is-placeholder");
2761
+ revealIcon();
2762
+ // Start watching for the terminal to attach so we can prompt for approval.
2763
+ if (body.rendezvousCode) rzvStart(body.rendezvousCode);
2401
2764
  var ok = await copy(body.command);
2765
+ if (ok) flashCopied();
2402
2766
  label(ok ? " Copied ✓" : " Select & copy");
2403
2767
  ping(ok ? "Copied — paste & press Enter" : "Select the command above to copy");
2404
2768
  if (note) {