@tokenoftrust/storefront-runner 1.3.4-rc.3 → 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.
- package/apps/storefront/astro.config.mjs +15 -0
- package/apps/storefront/dev-plugins/dev-publish.mjs +55 -0
- package/apps/storefront/dev-plugins/tenant-hot-reload.mjs +86 -7
- package/apps/storefront/drizzle.config.apps.ts +13 -0
- package/apps/storefront/env.d.ts +10 -0
- package/apps/storefront/migrations/README.md +13 -7
- package/apps/storefront/migrations-apps/0000_fast_millenium_guard.sql +129 -0
- package/apps/storefront/migrations-apps/meta/0000_snapshot.json +843 -0
- package/apps/storefront/migrations-apps/meta/_journal.json +13 -0
- package/apps/storefront/package.json +13 -1
- package/apps/storefront/perf/README.md +64 -0
- package/apps/storefront/perf/assert-budgets.ts +159 -0
- package/apps/storefront/playwright.config.ts +23 -0
- package/apps/storefront/public/js/dashboard-apps.js +173 -0
- package/apps/storefront/public/shared/commerce-marketing.css +221 -0
- package/apps/storefront/src/components/CollectionCard.astro +1 -0
- package/apps/storefront/src/components/ProductCard.astro +1 -0
- package/apps/storefront/src/components/admin/AdminPublishTab.astro +1959 -0
- package/apps/storefront/src/components/apps/AppWidgetFrame.astro +30 -0
- package/apps/storefront/src/components/chrome/NavDropdown.astro +6 -3
- package/apps/storefront/src/components/chrome/SiteFooter.astro +10 -0
- package/apps/storefront/src/components/chrome/SiteHeader.astro +10 -0
- package/apps/storefront/src/components/commerce/RatingStars.astro +3 -2
- package/apps/storefront/src/components/content/Callout.astro +75 -0
- package/apps/storefront/src/components/content/NeedsReviewCallout.astro +66 -0
- package/apps/storefront/src/components/content/ProseSections.astro +121 -0
- package/apps/storefront/src/components/content/ProseToc.astro +34 -0
- package/apps/storefront/src/components/content/RichText.astro +44 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +46 -0
- package/apps/storefront/src/components/home/Hero.astro +14 -0
- package/apps/storefront/src/components/islands/CheckoutComplianceGate.tsx +295 -0
- package/apps/storefront/src/components/islands/ImageGallery.tsx +42 -21
- package/apps/storefront/src/components/islands/VariantSelector.tsx +62 -12
- package/apps/storefront/src/components/plp/FacetSidebar.astro +2 -2
- package/apps/storefront/src/components/subscription/ManageSubscriptionEntry.astro +1 -0
- package/apps/storefront/src/config/compliance/rulesets.ts +79 -0
- package/apps/storefront/src/config/storeName.ts +34 -0
- package/apps/storefront/src/layouts/Layout.astro +97 -23
- package/apps/storefront/src/lib/analytics/budgets.json +69 -0
- package/apps/storefront/src/lib/analytics/lighthouseReport.ts +109 -0
- package/apps/storefront/src/lib/analytics/perfBudgets.ts +452 -0
- package/apps/storefront/src/lib/analytics/rumAlert.ts +179 -0
- package/apps/storefront/src/lib/analytics/webVitals.ts +269 -0
- package/apps/storefront/src/lib/apps/adminService.ts +106 -0
- package/apps/storefront/src/lib/apps/adminSession.ts +205 -0
- package/apps/storefront/src/lib/apps/apiAuth.ts +91 -0
- package/apps/storefront/src/lib/apps/apiRoute.ts +35 -0
- package/apps/storefront/src/lib/apps/catalogMapper.ts +39 -0
- package/apps/storefront/src/lib/apps/credentials.ts +153 -0
- package/apps/storefront/src/lib/apps/gatewayKeys.ts +156 -0
- package/apps/storefront/src/lib/apps/healthAggregate.ts +66 -0
- package/apps/storefront/src/lib/apps/orders/attributionService.ts +206 -0
- package/apps/storefront/src/lib/apps/orders/customerHash.ts +28 -0
- package/apps/storefront/src/lib/apps/orders/foxyOrderClient.ts +203 -0
- package/apps/storefront/src/lib/apps/orders/idempotency.ts +100 -0
- package/apps/storefront/src/lib/apps/orders/orderForwardReceiver.ts +117 -0
- package/apps/storefront/src/lib/apps/orders/orderMapper.ts +91 -0
- package/apps/storefront/src/lib/apps/orders/ordersStore.ts +181 -0
- package/apps/storefront/src/lib/apps/registryService.ts +579 -0
- package/apps/storefront/src/lib/apps/scopes.ts +79 -0
- package/apps/storefront/src/lib/apps/tokenIssuer.ts +121 -0
- package/apps/storefront/src/lib/apps/tokenVerifier.ts +148 -0
- package/apps/storefront/src/lib/apps/widgets/eligibility.ts +18 -0
- package/apps/storefront/src/lib/apps/widgets/frameProps.ts +52 -0
- package/apps/storefront/src/lib/apps/widgets/launchToken.ts +84 -0
- package/apps/storefront/src/lib/apps/widgets/placements.ts +57 -0
- package/apps/storefront/src/lib/apps/widgets/renderSlot.ts +111 -0
- package/apps/storefront/src/lib/auth/adminEntry.ts +119 -0
- package/apps/storefront/src/lib/auth/identityToken.ts +21 -2
- package/apps/storefront/src/lib/auth/loginGate.ts +102 -18
- package/apps/storefront/src/lib/auth/mcpClientAssertion.ts +215 -0
- package/apps/storefront/src/lib/auth/route.ts +16 -1
- package/apps/storefront/src/lib/auth/session.ts +8 -0
- package/apps/storefront/src/lib/auth/stepUpChallenge.ts +107 -0
- package/apps/storefront/src/lib/auth/totAccessClient.ts +208 -0
- package/apps/storefront/src/lib/blog/provider.ts +39 -0
- package/apps/storefront/src/lib/blog/types.ts +26 -0
- package/apps/storefront/src/lib/checkoutCommerce.ts +39 -1
- package/apps/storefront/src/lib/chrome/model.ts +11 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +95 -0
- package/apps/storefront/src/lib/content/callout.ts +78 -0
- package/apps/storefront/src/lib/content/index.ts +27 -0
- package/apps/storefront/src/lib/content/needsReview.ts +58 -0
- package/apps/storefront/src/lib/content/prose.ts +186 -0
- package/apps/storefront/src/lib/content/richtext.ts +76 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +74 -0
- package/apps/storefront/src/lib/content-edit/client.ts +70 -14
- package/apps/storefront/src/lib/d1/catalog.ts +12 -0
- package/apps/storefront/src/lib/d1/schema-apps.ts +213 -0
- package/apps/storefront/src/lib/dev/apiBase.ts +8 -2
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +65 -115
- package/apps/storefront/src/lib/dev/cockpitStore.ts +65 -0
- package/apps/storefront/src/lib/dev/obstacleStore.ts +214 -0
- package/apps/storefront/src/lib/dev/previewStatus.ts +112 -0
- package/apps/storefront/src/lib/dev/rendezvousBroker.ts +241 -0
- package/apps/storefront/src/lib/dev/subjectReissue.ts +67 -0
- package/apps/storefront/src/lib/email/magicLinkInviteEmail.ts +10 -10
- package/apps/storefront/src/lib/env.ts +12 -0
- package/apps/storefront/src/lib/jsonld.ts +12 -17
- package/apps/storefront/src/lib/membership/eligibility.ts +37 -0
- package/apps/storefront/src/lib/monitoring/manifest.ts +302 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +13 -5
- package/apps/storefront/src/lib/publish/apex-readiness.ts +337 -0
- package/apps/storefront/src/lib/publish/dispatchHealth.ts +269 -0
- package/apps/storefront/src/lib/publish/domainState.ts +351 -0
- package/apps/storefront/src/lib/publish/shipWorkspace.ts +362 -0
- package/apps/storefront/src/lib/rawChrome.ts +34 -3
- package/apps/storefront/src/lib/storyblok/content-model.ts +34 -2
- package/apps/storefront/src/lib/storyblok/provider.ts +11 -4
- package/apps/storefront/src/lib/subscription/model.ts +114 -0
- package/apps/storefront/src/lib/tot/ToTClient.ts +3 -3
- package/apps/storefront/src/lib/tot/query.ts +32 -0
- package/apps/storefront/src/lib/webhooks/cloudflareQueueDispatcher.ts +82 -0
- package/apps/storefront/src/lib/webhooks/deliveryEngine.ts +245 -0
- package/apps/storefront/src/lib/webhooks/deliveryMapper.ts +34 -0
- package/apps/storefront/src/lib/webhooks/deliveryStore.ts +668 -0
- package/apps/storefront/src/lib/webhooks/dispatcher.ts +168 -0
- package/apps/storefront/src/lib/webhooks/emit.ts +167 -0
- package/apps/storefront/src/lib/webhooks/endpointGuard.ts +135 -0
- package/apps/storefront/src/lib/webhooks/events.ts +98 -0
- package/apps/storefront/src/lib/webhooks/getDispatcher.ts +49 -0
- package/apps/storefront/src/lib/webhooks/signing.ts +29 -0
- package/apps/storefront/src/lib/webhooks/webhookSigningKey.ts +146 -0
- package/apps/storefront/src/middleware/index.ts +28 -13
- package/apps/storefront/src/pages/404.astro +21 -9
- package/apps/storefront/src/pages/[...slug].astro +36 -2
- package/apps/storefront/src/pages/admin.astro +29 -3
- package/apps/storefront/src/pages/api/apps/admin/credentials/rotate.ts +60 -0
- package/apps/storefront/src/pages/api/apps/admin/health.ts +44 -0
- package/apps/storefront/src/pages/api/apps/admin/install.ts +100 -0
- package/apps/storefront/src/pages/api/apps/admin/list.ts +26 -0
- package/apps/storefront/src/pages/api/apps/admin/resume.ts +56 -0
- package/apps/storefront/src/pages/api/apps/admin/suspend.ts +57 -0
- package/apps/storefront/src/pages/api/apps/admin/uninstall.ts +74 -0
- package/apps/storefront/src/pages/api/apps/admin/update.ts +103 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries/[deliveryId]/replay.ts +59 -0
- package/apps/storefront/src/pages/api/apps/admin/webhooks/deliveries.ts +51 -0
- package/apps/storefront/src/pages/api/apps/internal/order-forward.ts +172 -0
- package/apps/storefront/src/pages/api/apps/oauth/token.ts +87 -0
- package/apps/storefront/src/pages/api/apps/v1/attribution.ts +142 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products/[handle].ts +39 -0
- package/apps/storefront/src/pages/api/apps/v1/catalog/products.ts +49 -0
- package/apps/storefront/src/pages/api/apps/v1/health.ts +32 -0
- package/apps/storefront/src/pages/api/apps/v1/inventory.ts +59 -0
- package/apps/storefront/src/pages/api/apps/v1/orders/[id].ts +48 -0
- package/apps/storefront/src/pages/api/apps/v1/orders.ts +73 -0
- package/apps/storefront/src/pages/api/apps/v1/reports.ts +21 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries/[deliveryId]/replay.ts +62 -0
- package/apps/storefront/src/pages/api/apps/v1/webhooks/deliveries.ts +47 -0
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +48 -1
- package/apps/storefront/src/pages/api/auth/step-up-send.ts +57 -0
- package/apps/storefront/src/pages/api/auth/step-up-verify.ts +129 -0
- package/apps/storefront/src/pages/api/auth/verify.ts +23 -0
- package/apps/storefront/src/pages/api/compliance/preflight.ts +206 -0
- package/apps/storefront/src/pages/api/rum/vitals.ts +54 -0
- package/apps/storefront/src/pages/api/test/dev-session.ts +133 -0
- package/apps/storefront/src/pages/auth/login.astro +197 -41
- package/apps/storefront/src/pages/auth/magic.astro +75 -43
- package/apps/storefront/src/pages/blog/[slug].astro +107 -0
- package/apps/storefront/src/pages/blog/index.astro +98 -0
- package/apps/storefront/src/pages/capabilities.astro +8 -0
- package/apps/storefront/src/pages/cockpit.astro +496 -74
- package/apps/storefront/src/pages/collections/[handle].astro +8 -0
- package/apps/storefront/src/pages/collections/index.astro +10 -2
- package/apps/storefront/src/pages/dashboard/[appDomain]/apps/index.astro +119 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/index.astro +5 -0
- package/apps/storefront/src/pages/index.astro +57 -0
- package/apps/storefront/src/pages/llms.txt.ts +31 -10
- package/apps/storefront/src/pages/products/[handle].astro +100 -9
- package/apps/storefront/src/pages/sitemap.xml.ts +21 -4
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +198 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +7 -0
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +7 -0
- package/apps/storefront/src/pages/style-guide/index.astro +10 -0
- package/apps/storefront/src/styles/fonts.css +54 -0
- package/apps/storefront/src/styles/global.css +22 -2
- package/apps/storefront/src/themes/schema.ts +3 -25
- package/apps/storefront/tsconfig.json +1 -1
- package/apps/storefront/vitest.config.ts +4 -1
- package/package.json +1 -1
- package/packages/public-runtime/src/candidate-index.ts +311 -0
- package/packages/public-runtime/src/checkout.ts +94 -2
- package/packages/public-runtime/src/compliance/evaluate.ts +265 -0
- package/packages/public-runtime/src/compliance/evidence-signals.ts +81 -0
- package/packages/public-runtime/src/compliance/index.ts +22 -0
- package/packages/public-runtime/src/compliance/pact-report.ts +94 -0
- package/packages/public-runtime/src/compliance/profile.ts +198 -0
- package/packages/public-runtime/src/compliance/ruleset.ts +117 -0
- package/packages/public-runtime/src/compliance/verification.ts +81 -0
- package/packages/public-runtime/src/csp.ts +27 -3
- package/packages/public-runtime/src/customization-reconcile.ts +35 -0
- package/packages/public-runtime/src/customization-runtime.ts +8 -0
- package/packages/public-runtime/src/customization-versioning.ts +17 -0
- package/packages/public-runtime/src/extension-contract.ts +2 -1
- package/packages/public-runtime/src/hash.ts +25 -0
- package/packages/public-runtime/src/index.ts +6 -0
- package/packages/public-runtime/src/membership.ts +353 -0
- package/packages/public-runtime/src/product.ts +24 -2
- package/packages/public-runtime/src/review-trust-proof.ts +194 -0
- package/packages/public-runtime/src/tenant-assets.ts +40 -5
- package/packages/public-runtime/src/tenant.ts +236 -0
- package/packages/public-runtime/src/widget-postmessage.ts +205 -0
- package/scripts/dev/publish.mjs +158 -0
- package/scripts/dev/transient-files.mjs +2 -1
- package/tenants/home/public/fonts/inter-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/inter-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-400-normal.woff2 +0 -0
- package/tenants/home/public/fonts/jetbrains-mono-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-800-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-500-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-600-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-700-normal.woff2 +0 -0
- package/tenants/home/public/fonts/plus-jakarta-sans-latin-ext-800-normal.woff2 +0 -0
- package/tenants/home/public/pages/storefront.css +23 -0
- 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
|
|
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
|
-
|
|
57
|
+
`${basePath}/cockpit${canonical.toString() ? `?${canonical.toString()}` : ""}`,
|
|
46
58
|
302,
|
|
47
59
|
);
|
|
48
60
|
}
|
|
49
|
-
const cockpitNext = isCredentialRecovery ? "
|
|
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
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
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 &&
|
|
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 ->
|
|
572
|
-
//
|
|
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("
|
|
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("
|
|
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(
|
|
583
|
-
if (res.s === 409 && res.j && res.j.status === "no_draft") { setStatus("No unpublished edit to
|
|
584
|
-
setStatus((res.j && res.j.message) || "
|
|
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
|
}
|
|
@@ -750,12 +810,42 @@ const hostedActivityScript = `
|
|
|
750
810
|
var bKicker = document.getElementById("bKicker");
|
|
751
811
|
var bHint = document.getElementById("bHint");
|
|
752
812
|
var bDiag = document.getElementById("bDiag");
|
|
813
|
+
var bTxt = document.getElementById("bTxt");
|
|
814
|
+
var bObstacle = document.getElementById("bObstacle");
|
|
815
|
+
var bObTitle = document.getElementById("bObTitle");
|
|
816
|
+
var bObMsg = document.getElementById("bObMsg");
|
|
817
|
+
var bObCmd = document.getElementById("bObCmd");
|
|
818
|
+
var bObNote = document.getElementById("bObNote");
|
|
753
819
|
var doneRunMeta = document.getElementById("doneRunMeta");
|
|
754
|
-
function updateBridge(rt) {
|
|
820
|
+
function updateBridge(rt, obstacle) {
|
|
755
821
|
if (!bridge) return;
|
|
756
|
-
|
|
757
|
-
var live = !!rt.live;
|
|
822
|
+
var live = !!(rt && rt.live);
|
|
758
823
|
if (live) sawLive = true;
|
|
824
|
+
// Obstacle lane (v2 §6): a failed \`tot start\` is the ONE reason the strip
|
|
825
|
+
// shows BEFORE the loop was ever live this page-view — the developer needs the
|
|
826
|
+
// fix, not a silent "watching…". The server returns \`obstacle\` only when there
|
|
827
|
+
// is no live heartbeat, so a live loop supersedes it. Fill from server copy.
|
|
828
|
+
if (obstacle && !live) {
|
|
829
|
+
bridge.hidden = false;
|
|
830
|
+
bridge.setAttribute("data-live", "obstacle");
|
|
831
|
+
if (bTxt) bTxt.hidden = true;
|
|
832
|
+
if (bDiag) bDiag.hidden = true;
|
|
833
|
+
if (bGo) bGo.hidden = true;
|
|
834
|
+
if (bHint) bHint.hidden = true;
|
|
835
|
+
if (bObstacle) bObstacle.hidden = false;
|
|
836
|
+
if (bObTitle) bObTitle.textContent = obstacle.title || "Setup hit a snag";
|
|
837
|
+
if (bObMsg) bObMsg.textContent = obstacle.message || "";
|
|
838
|
+
if (bObCmd) {
|
|
839
|
+
if (obstacle.fixCommand) { bObCmd.textContent = obstacle.fixCommand; bObCmd.hidden = false; }
|
|
840
|
+
else bObCmd.hidden = true;
|
|
841
|
+
}
|
|
842
|
+
if (bObNote) bObNote.textContent = obstacle.fixNote || "";
|
|
843
|
+
return;
|
|
844
|
+
}
|
|
845
|
+
// Back to the normal live/quiet strip: restore the pieces the obstacle state hid.
|
|
846
|
+
if (bObstacle) bObstacle.hidden = true;
|
|
847
|
+
if (bTxt) bTxt.hidden = false;
|
|
848
|
+
if (!rt || !rt.url) { bridge.hidden = true; return; }
|
|
759
849
|
// On FIRST arrival, a stale runtime record (offline, never seen live this
|
|
760
850
|
// page-view — e.g. left over from a prior run, still within the heartbeat TTL)
|
|
761
851
|
// must NOT show — let Step 1 "Get your store running" do the work. Only surface
|
|
@@ -1081,29 +1171,40 @@ const hostedActivityScript = `
|
|
|
1081
1171
|
}
|
|
1082
1172
|
|
|
1083
1173
|
function poll() {
|
|
1084
|
-
fetch("/api/dev/activity", { cache: "no-store" })
|
|
1174
|
+
fetch(basePath + "/api/dev/activity", { cache: "no-store" })
|
|
1085
1175
|
.then(function (r) { return r.ok ? r.json() : Promise.reject(); })
|
|
1086
1176
|
.then(function (d) {
|
|
1087
1177
|
var events = (d && d.events) || [];
|
|
1088
1178
|
var rt = d && d.runtime;
|
|
1089
1179
|
var isLive = !!(rt && rt.live);
|
|
1090
1180
|
liveNow = isLive; // drives Step 1 minimize + the Step-2 teaser visibility below
|
|
1091
|
-
updateBridge(rt); // same poll drives the bridge strip
|
|
1181
|
+
updateBridge(rt, d && d.obstacle); // same poll drives the bridge strip (+ obstacle lane)
|
|
1092
1182
|
render(events);
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
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;
|
|
1096
1197
|
// Step 1 "Get running" completes once localhost reports LIVE (or any activity
|
|
1097
1198
|
// arrives). Once complete, renderSteps() minimizes Step 1 and reveals the Step-2 nudge.
|
|
1098
1199
|
if (isLive || events.length) completeStep("run");
|
|
1099
1200
|
if (baseline === null) {
|
|
1100
|
-
setBaseline(
|
|
1101
|
-
} else if (
|
|
1102
|
-
setBaseline(
|
|
1103
|
-
//
|
|
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).
|
|
1104
1205
|
// completeStep flips + persists once, returning true on that first poll only.
|
|
1105
|
-
if (
|
|
1106
|
-
celebrate(
|
|
1206
|
+
if (completeStep("edit")) {
|
|
1207
|
+
celebrate(lastEdit && lastEdit.file);
|
|
1107
1208
|
if (current === "edit") current = null; // minimize Step 2 → reveal Step-3 teaser
|
|
1108
1209
|
}
|
|
1109
1210
|
}
|
|
@@ -1149,13 +1250,21 @@ const hostedActivityScript = `
|
|
|
1149
1250
|
<meta charset="utf-8" />
|
|
1150
1251
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
1151
1252
|
<meta name="robots" content="noindex, nofollow" />
|
|
1152
|
-
<title>
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
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). */}
|
|
1159
1268
|
<style nonce={cspNonce}>
|
|
1160
1269
|
:root {
|
|
1161
1270
|
--bg: #f7f9fc; --bg-2: #eef3f9; --surface: #ffffff;
|
|
@@ -1250,6 +1359,8 @@ const hostedActivityScript = `
|
|
|
1250
1359
|
.brand .dom { font-family: var(--display); font-weight: 700; color: var(--ink); font-size: .98rem; letter-spacing: -.01em; }
|
|
1251
1360
|
.brand .sub { font-family: var(--mono); font-size: .7rem; color: var(--faint); text-transform: uppercase; letter-spacing: .12em; }
|
|
1252
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; }
|
|
1253
1364
|
.topbar .right .sess { display: inline-flex; align-items: center; gap: .45rem; }
|
|
1254
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); }
|
|
1255
1366
|
.themetog {
|
|
@@ -1344,6 +1455,25 @@ const hostedActivityScript = `
|
|
|
1344
1455
|
.b-go:hover { background: var(--accent-ink); transform: translateY(-1px); }
|
|
1345
1456
|
.b-hint { flex-basis: 100%; margin: 0; font-size: .8rem; color: var(--amber); }
|
|
1346
1457
|
.b-hint code { font-family: var(--mono); }
|
|
1458
|
+
/* Obstacle lane (v2 §6): a failed `tot start` — an ERROR, so it uses the
|
|
1459
|
+
existing --warn (red) token triple (NO new palette values), which also sets
|
|
1460
|
+
it apart from the amber "went quiet". Content is a stacked block, so the
|
|
1461
|
+
beacon aligns to the top rather than centering on a tall card. */
|
|
1462
|
+
.bridge[data-live="obstacle"] { align-items: flex-start; border-color: var(--warn-line); background: var(--warn-wash); }
|
|
1463
|
+
.bridge[data-live="obstacle"] .beacon { background: var(--warn); }
|
|
1464
|
+
.b-obstacle { flex: 1 1 100%; display: flex; flex-direction: column; gap: .3rem; min-width: 0; }
|
|
1465
|
+
.b-ob-title { color: var(--warn); }
|
|
1466
|
+
.b-ob-title::before { content: "\26A0 "; } /* ⚠ warning glyph */
|
|
1467
|
+
.b-ob-msg { margin: 0; font-size: .9rem; font-weight: 600; color: var(--ink); }
|
|
1468
|
+
.b-ob-fix { margin: 0; font-size: .82rem; color: var(--muted); line-height: 1.5; }
|
|
1469
|
+
.b-ob-lead { color: var(--warn); font-weight: 700; }
|
|
1470
|
+
.b-ob-cmd { font-family: var(--mono); font-size: .8rem; background: var(--bg-2); color: var(--ink); padding: .1rem .35rem; border-radius: var(--r-sm); }
|
|
1471
|
+
/* These three set their own `display`, which (author > UA) would beat the UA
|
|
1472
|
+
[hidden]{display:none} — so the script's `.hidden` toggles silently no-op on
|
|
1473
|
+
them. Re-assert [hidden] at higher specificity so hidden means hidden: the
|
|
1474
|
+
obstacle block and the live-strip text swap cleanly, AND the quiet-state CTA
|
|
1475
|
+
(b-go) actually hides. */
|
|
1476
|
+
.b-txt[hidden], .b-go[hidden], .b-obstacle[hidden] { display: none; }
|
|
1347
1477
|
@media (prefers-reduced-motion: reduce) { .bridge[data-live="1"], .beacon, .b-go { animation: none !important; transition: none; } }
|
|
1348
1478
|
@media (max-width: 34rem) { .b-diag { display: none; } }
|
|
1349
1479
|
|
|
@@ -1551,6 +1681,34 @@ const hostedActivityScript = `
|
|
|
1551
1681
|
.term-bar .dot { width: 11px; height: 11px; border-radius: 50%; }
|
|
1552
1682
|
.dot.r { background: #ff5f56; } .dot.y { background: #ffbd2e; } .dot.g { background: #27c93f; }
|
|
1553
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; }
|
|
1554
1712
|
.term-body { padding: 1rem 1.05rem 1.1rem; color: var(--term-fg); white-space: pre-wrap; word-break: break-word; }
|
|
1555
1713
|
/* block, not inline — Astro trims the whitespace text node between the spans, so the
|
|
1556
1714
|
line break must be structural or the comment + command collapse onto one line. */
|
|
@@ -1726,6 +1884,9 @@ const hostedActivityScript = `
|
|
|
1726
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; }
|
|
1727
1885
|
.df-view[readonly] { opacity: .9; }
|
|
1728
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; }
|
|
1729
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); }
|
|
1730
1891
|
.df-diff .dl { display: block; padding: 0 .9rem; white-space: pre; }
|
|
1731
1892
|
.df-diff .add { background: rgba(52, 211, 153, .16); color: #bff2e0; }
|
|
@@ -1781,6 +1942,11 @@ const hostedActivityScript = `
|
|
|
1781
1942
|
.ask-foot { padding: .7rem 1.1rem; border-top: 1px solid var(--border-2); font-family: var(--mono); font-size: .68rem; color: var(--faint); }
|
|
1782
1943
|
@media (prefers-reduced-motion: reduce) { .ask-panel { transition: none; } }
|
|
1783
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
|
+
|
|
1784
1950
|
/* ---- what's next : coming soon shelf (U4) ---- */
|
|
1785
1951
|
.next { margin-top: 3.4rem; padding-top: 2rem; border-top: 1px dashed var(--border); }
|
|
1786
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; }
|
|
@@ -1832,7 +1998,7 @@ const hostedActivityScript = `
|
|
|
1832
1998
|
<span class="mark" aria-hidden="true">
|
|
1833
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>
|
|
1834
2000
|
</span>
|
|
1835
|
-
<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>
|
|
1836
2002
|
</div>
|
|
1837
2003
|
<div class="right">
|
|
1838
2004
|
{session?.email ? <span class="sess">{session.email}</span> : null}
|
|
@@ -1840,11 +2006,23 @@ const hostedActivityScript = `
|
|
|
1840
2006
|
</div>
|
|
1841
2007
|
</header>
|
|
1842
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
|
+
|
|
1843
2012
|
{signedIn === false && (
|
|
1844
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. */}
|
|
1845
2023
|
<p>{isCredentialRecovery
|
|
1846
|
-
?
|
|
1847
|
-
? <>Sign in with
|
|
2024
|
+
? recoveryEmailFull
|
|
2025
|
+
? <>Sign in with your invite email, <strong>{recoveryEmailFull}</strong>, to generate a fresh setup command for this machine.</>
|
|
1848
2026
|
: <>Sign in with the same email that accepted the invite to generate a fresh setup command for this machine.</>
|
|
1849
2027
|
: <>Sign in with the email your invite came to — then this page shows your store and your one command.</>}</p>
|
|
1850
2028
|
<a class="action" href={loginHref}>Sign in</a>
|
|
@@ -1853,8 +2031,27 @@ const hostedActivityScript = `
|
|
|
1853
2031
|
|
|
1854
2032
|
{noStoreAccess && (
|
|
1855
2033
|
<div class="notice">
|
|
1856
|
-
|
|
1857
|
-
|
|
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>
|
|
1858
2055
|
</div>
|
|
1859
2056
|
)}
|
|
1860
2057
|
|
|
@@ -1866,13 +2063,22 @@ const hostedActivityScript = `
|
|
|
1866
2063
|
{hostedActivity && (
|
|
1867
2064
|
<section class="bridge" id="bridge" data-live="1" hidden>
|
|
1868
2065
|
<span class="beacon" aria-hidden="true"></span>
|
|
1869
|
-
<div class="b-txt">
|
|
2066
|
+
<div class="b-txt" id="bTxt">
|
|
1870
2067
|
<span class="b-kicker" id="bKicker">Live on your machine</span>
|
|
1871
2068
|
<a class="b-url" id="bUrl" href="#" target="_blank" rel="noreferrer">http://localhost/</a>
|
|
1872
2069
|
</div>
|
|
1873
2070
|
<span class="b-diag" id="bDiag" hidden></span>
|
|
1874
2071
|
<a class="b-go" id="bGo" href="#" target="_blank" rel="noreferrer">Open your store ↗</a>
|
|
1875
2072
|
<p class="b-hint" id="bHint" hidden>Your store went quiet. Run <code>tot start</code> in your project to bring it back — Step 1 has the command.</p>
|
|
2073
|
+
{/* Obstacle lane (v2 §6): a failed `tot start` (wrong Node, missing pnpm,
|
|
2074
|
+
install/clone error) beacons why via the setup command's activity token;
|
|
2075
|
+
the strip becomes an actionable fix card. Text is set via textContent from
|
|
2076
|
+
the SERVER-composed remediation copy (obstacleStore) — never innerHTML. */}
|
|
2077
|
+
<div class="b-obstacle" id="bObstacle" hidden>
|
|
2078
|
+
<span class="b-kicker b-ob-title" id="bObTitle">Setup hit a snag</span>
|
|
2079
|
+
<p class="b-ob-msg" id="bObMsg"></p>
|
|
2080
|
+
<p class="b-ob-fix"><span class="b-ob-lead">Fix:</span> <code class="b-ob-cmd" id="bObCmd" hidden></code> <span id="bObNote"></span></p>
|
|
2081
|
+
</div>
|
|
1876
2082
|
</section>
|
|
1877
2083
|
)}
|
|
1878
2084
|
|
|
@@ -1930,31 +2136,62 @@ const hostedActivityScript = `
|
|
|
1930
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>
|
|
1931
2137
|
</span>
|
|
1932
2138
|
</h1>
|
|
1933
|
-
<p class="goal">{
|
|
2139
|
+
<p class="goal">{canSetupStore
|
|
1934
2140
|
? isCredentialRecovery
|
|
1935
2141
|
? <>Generate a fresh personal setup command, then paste it into your terminal and press Enter.</>
|
|
1936
2142
|
: <>Generate your personal setup command, then paste it into your terminal and press Enter.</>
|
|
1937
|
-
:
|
|
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>
|
|
1938
2146
|
|
|
1939
2147
|
<div class="term">
|
|
1940
|
-
<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
|
|
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>
|
|
1941
2154
|
<div class="term-body"><span class="c"># your personal, single-use setup command</span>
|
|
1942
|
-
<span class="cmd is-placeholder" id="cli-cmd-text">{
|
|
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>
|
|
1943
2156
|
</div>
|
|
1944
2157
|
|
|
1945
|
-
{
|
|
2158
|
+
{canSetupStore && (
|
|
1946
2159
|
<div class="cmd-actions">
|
|
1947
|
-
<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}>
|
|
1948
2161
|
<svg viewBox="0 0 24 24" fill="none"><path d="M12 5v14M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
|
|
1949
2162
|
{isCredentialRecovery ? "Generate a fresh setup command" : "Generate my setup command"}
|
|
1950
2163
|
</button>
|
|
1951
2164
|
</div>
|
|
1952
2165
|
)}
|
|
1953
|
-
|
|
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
|
|
1954
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.</>
|
|
1955
|
-
:
|
|
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>
|
|
1956
2193
|
|
|
1957
|
-
<p class="cmd-steps">Prefer the steps? <code>npm i -g {cliInstallSpec}</code>, then <code>tot
|
|
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>
|
|
1958
2195
|
|
|
1959
2196
|
{isLocalDevLoop && (
|
|
1960
2197
|
<div class="detect" id="detect" data-listening="true">
|
|
@@ -2024,11 +2261,11 @@ const hostedActivityScript = `
|
|
|
2024
2261
|
<div class="stage-body">
|
|
2025
2262
|
<span class="kicker">Step 3</span>
|
|
2026
2263
|
<h1>Submit your change for preview.</h1>
|
|
2027
|
-
<p class="goal">You've got the loop: edit locally, see it reflected here. When a change is ready,
|
|
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>
|
|
2028
2265
|
<div class="term">
|
|
2029
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>
|
|
2030
2267
|
<div class="term-body"><span class="c"># bundle your local changes into a reviewable preview</span>
|
|
2031
|
-
<span class="cmd">
|
|
2268
|
+
<span class="cmd">tot preview</span></div>
|
|
2032
2269
|
</div>
|
|
2033
2270
|
<div class="cmd-actions">
|
|
2034
2271
|
<button class="cta" id="submitPreview" type="button">
|
|
@@ -2036,7 +2273,7 @@ const hostedActivityScript = `
|
|
|
2036
2273
|
Submit for preview
|
|
2037
2274
|
</button>
|
|
2038
2275
|
</div>
|
|
2039
|
-
<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>
|
|
2040
2277
|
</div>
|
|
2041
2278
|
</section>
|
|
2042
2279
|
</div>
|
|
@@ -2108,6 +2345,7 @@ const hostedActivityScript = `
|
|
|
2108
2345
|
<button type="button" class="df-mini" id="filesForce">Overwrite</button>
|
|
2109
2346
|
</span>
|
|
2110
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>
|
|
2111
2349
|
</div>
|
|
2112
2350
|
<textarea class="df-view" id="filesView" readonly spellcheck="false" wrap="off">Pick a file on the left to view its source.</textarea>
|
|
2113
2351
|
<pre class="df-diff" id="filesDiff" hidden></pre>
|
|
@@ -2160,18 +2398,44 @@ const hostedActivityScript = `
|
|
|
2160
2398
|
</div>
|
|
2161
2399
|
)}
|
|
2162
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"> </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
|
+
|
|
2163
2429
|
{/* Static "coming soon" cards — pure /dev rendering with no dependency on the
|
|
2164
2430
|
isLocalDevLoop-only /__tot/* endpoints and no need for the activity bridge, so
|
|
2165
2431
|
it renders unconditionally on the deployed hosted panel (and locally). Not
|
|
2166
|
-
gated on capability either — it's a teaser of what's next
|
|
2167
|
-
among several, not a prerequisite. */}
|
|
2432
|
+
gated on capability either — it's a teaser of what's next. */}
|
|
2168
2433
|
<section class="next">
|
|
2169
2434
|
<h3>The rest of the workflow</h3>
|
|
2170
2435
|
<p class="sub">Previews, publish checks, AI assistance, and diagnostics. These open up once you've got the loop — coming soon.</p>
|
|
2171
2436
|
<div class="nextgrid">
|
|
2172
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>
|
|
2173
2438
|
<div class="nextcard"><span class="path">Publish readiness</span><span class="d">What happens if you publish today — checks & blockers.</span><span class="soon">Coming soon</span></div>
|
|
2174
|
-
<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>
|
|
2175
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>
|
|
2176
2440
|
</div>
|
|
2177
2441
|
</section>
|
|
@@ -2246,16 +2510,55 @@ const hostedActivityScript = `
|
|
|
2246
2510
|
)}
|
|
2247
2511
|
|
|
2248
2512
|
<script nonce={cspNonce} is:inline>
|
|
2249
|
-
// Theme toggle: flip data-theme on <html>, seeded from the OS preference on first
|
|
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.
|
|
2250
2517
|
(function () {
|
|
2251
2518
|
var root = document.documentElement;
|
|
2252
2519
|
var btn = document.getElementById("themeBtn");
|
|
2253
2520
|
if (!btn) return;
|
|
2521
|
+
var KEY = "tot.theme";
|
|
2522
|
+
function store(v) { try { localStorage.setItem(KEY, v); } catch (e) {} }
|
|
2254
2523
|
btn.addEventListener("click", function () {
|
|
2255
2524
|
var cur = root.getAttribute("data-theme");
|
|
2256
2525
|
var next = cur === "dark" ? "light" : cur === "light" ? "dark"
|
|
2257
2526
|
: (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches ? "light" : "dark");
|
|
2258
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
|
+
});
|
|
2259
2562
|
});
|
|
2260
2563
|
})();
|
|
2261
2564
|
</script>
|
|
@@ -2283,9 +2586,16 @@ const hostedActivityScript = `
|
|
|
2283
2586
|
var note = document.getElementById("cli-cmd-note");
|
|
2284
2587
|
var toast = document.getElementById("toast");
|
|
2285
2588
|
var toastMsg = document.getElementById("toastMsg");
|
|
2589
|
+
var copyIcon = document.getElementById("copyIcon");
|
|
2286
2590
|
if (!btn || !textEl) return;
|
|
2287
2591
|
var generic = btn.getAttribute("data-generic") || "";
|
|
2288
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") || "";
|
|
2289
2599
|
var tT;
|
|
2290
2600
|
function ping(m) {
|
|
2291
2601
|
if (!toast) return;
|
|
@@ -2298,6 +2608,111 @@ const hostedActivityScript = `
|
|
|
2298
2608
|
}
|
|
2299
2609
|
function label(s) { btn.childNodes[btn.childNodes.length - 1].textContent = s; }
|
|
2300
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
|
+
|
|
2301
2716
|
if (!fastpath) {
|
|
2302
2717
|
btn.addEventListener("click", function () {
|
|
2303
2718
|
copy(generic).then(function (ok) {
|
|
@@ -2316,7 +2731,7 @@ const hostedActivityScript = `
|
|
|
2316
2731
|
label(" …"); btn.disabled = true;
|
|
2317
2732
|
var res, body;
|
|
2318
2733
|
try {
|
|
2319
|
-
res = await fetch("/api/dev/cli-signin-code", {
|
|
2734
|
+
res = await fetch(basePath + "/api/dev/cli-signin-code", {
|
|
2320
2735
|
method: "POST",
|
|
2321
2736
|
headers: { "content-type": "application/json" },
|
|
2322
2737
|
body: "{}",
|
|
@@ -2331,6 +2746,7 @@ const hostedActivityScript = `
|
|
|
2331
2746
|
var byStatus = {
|
|
2332
2747
|
401: "Your session expired — reload the page and sign in again.",
|
|
2333
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.",
|
|
2334
2750
|
502: "The sign-in service is having a moment — try again.",
|
|
2335
2751
|
503: "Terminal sign-in isn’t configured on this environment.",
|
|
2336
2752
|
};
|
|
@@ -2338,9 +2754,15 @@ const hostedActivityScript = `
|
|
|
2338
2754
|
label(" Copy the command"); btn.disabled = false; busy = false;
|
|
2339
2755
|
return;
|
|
2340
2756
|
}
|
|
2341
|
-
// Reveal the real command (
|
|
2757
|
+
// Reveal the real command (carries the non-secret rendezvous handle) as a pure
|
|
2758
|
+
// text node, then copy it.
|
|
2342
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);
|
|
2343
2764
|
var ok = await copy(body.command);
|
|
2765
|
+
if (ok) flashCopied();
|
|
2344
2766
|
label(ok ? " Copied ✓" : " Select & copy");
|
|
2345
2767
|
ping(ok ? "Copied — paste & press Enter" : "Select the command above to copy");
|
|
2346
2768
|
if (note) {
|