@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.
- 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/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 +433 -69
- 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
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
* reach the browser. Inline style carries the per-request CSP nonce.
|
|
12
12
|
*/
|
|
13
13
|
import { normalizeEmailHint } from "@/lib/privacy/emailHint";
|
|
14
|
+
// Self-hosted web fonts (audit finding 6) — same-origin @fontsource, replacing
|
|
15
|
+
// the render-blocking fonts.googleapis.com stylesheet. Covers Plus Jakarta Sans,
|
|
16
|
+
// Inter, and JetBrains Mono used by this page's inline styles.
|
|
17
|
+
import "@/styles/fonts.css";
|
|
14
18
|
|
|
15
19
|
export const prerender = false;
|
|
16
20
|
|
|
@@ -25,6 +29,19 @@ const next = (() => {
|
|
|
25
29
|
return n.startsWith("/") && !n.startsWith("//") ? n : "/";
|
|
26
30
|
})();
|
|
27
31
|
|
|
32
|
+
// RFC 9470 step-up: tot core told /api/auth/verify (or /api/auth/magic-exchange,
|
|
33
|
+
// which redirects to /auth/magic instead) that it needs a fresher credential
|
|
34
|
+
// before it will complete sign-in. `reason` is distinct from `error` — see
|
|
35
|
+
// `loginRedirect` in `@/lib/auth/route`.
|
|
36
|
+
const reason = url.searchParams.get("reason");
|
|
37
|
+
const requiredAcr = url.searchParams.get("requiredAcr") ?? "";
|
|
38
|
+
const stepUpRequired = reason === "step-up-required";
|
|
39
|
+
const successPending = reason === "success-pending";
|
|
40
|
+
const ACR_LABELS: Record<string, string> = {
|
|
41
|
+
"tot:aal:sms": "a text message (SMS) code",
|
|
42
|
+
};
|
|
43
|
+
const stepUpFactorLabel = requiredAcr ? (ACR_LABELS[requiredAcr] ?? "an additional verification step") : "an additional verification step";
|
|
44
|
+
|
|
28
45
|
const ERROR_MESSAGES: Record<string, string> = {
|
|
29
46
|
bad_email: "That email doesn't look right — please try again.",
|
|
30
47
|
unconfigured: "Sign-in isn't available right now. Please try again shortly.",
|
|
@@ -32,6 +49,7 @@ const ERROR_MESSAGES: Record<string, string> = {
|
|
|
32
49
|
expired: "That code has expired — request a fresh one.",
|
|
33
50
|
too_many_attempts: "Too many attempts. Request a new code to continue.",
|
|
34
51
|
verify_failed: "We couldn't verify your sign-in. Please request a fresh code and try again.",
|
|
52
|
+
error: "We couldn't verify that. Please try again.",
|
|
35
53
|
};
|
|
36
54
|
const errorMessage = errorCode ? (ERROR_MESSAGES[errorCode] ?? "Something went wrong — try again.") : null;
|
|
37
55
|
// A stale/failed code should drop back to the email step so a new code can be requested.
|
|
@@ -54,59 +72,166 @@ if (devMode) {
|
|
|
54
72
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
55
73
|
<meta name="robots" content="noindex, nofollow" />
|
|
56
74
|
<title>Sign in — Regulated Storefront by Token of Trust©</title>
|
|
75
|
+
{/* Web fonts are self-hosted via `@/styles/fonts.css` (imported above). */}
|
|
57
76
|
<style nonce={cspNonce}>
|
|
58
|
-
|
|
77
|
+
/* storefront.tokenoftrust.store visual system — the code-entry door wears the
|
|
78
|
+
same skin as the marketing storefront it belongs to (in lockstep with the
|
|
79
|
+
magic-link landing, auth/magic.astro): warm cream + deep navy, the signature
|
|
80
|
+
teal→blue gradient (--grad), Plus Jakarta / Inter / JetBrains. Token values
|
|
81
|
+
mirror /tenants/home/pages/storefront.css. Theme via the triple: :root
|
|
82
|
+
default light, @media dark (the site's dark-hero palette), and [data-theme]. */
|
|
83
|
+
:root {
|
|
84
|
+
--bg: #faf8f4; --surface: #ffffff;
|
|
85
|
+
--ink: #10294b; --ink-2: #1b2a40; --muted: #4d5a70; --faint: #8593a8;
|
|
86
|
+
--accent: #0d9391; --accent-ink: #0b6360; --accent-wash: #e4f4f2; --accent-line: #c1e7e2;
|
|
87
|
+
--border: #e6e2d9;
|
|
88
|
+
--warn: #bf3d2c; --warn-wash: #fbecea; --warn-line: #f0ccc6;
|
|
89
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
90
|
+
--sh: 0 18px 50px rgba(16, 41, 75, .13);
|
|
91
|
+
--display: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
92
|
+
--body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
93
|
+
--mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
|
|
94
|
+
color-scheme: light;
|
|
95
|
+
}
|
|
96
|
+
@media (prefers-color-scheme: dark) {
|
|
97
|
+
:root {
|
|
98
|
+
--bg: #121118; --surface: #1b1a26;
|
|
99
|
+
--ink: #f3f5f9; --ink-2: #c7cede; --muted: #98a1b5; --faint: #6f7994;
|
|
100
|
+
--accent: #5fe0cf; --accent-ink: #7fe3d6; --accent-wash: #10302d; --accent-line: #1f524c;
|
|
101
|
+
--border: #2a2836;
|
|
102
|
+
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
103
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
104
|
+
--sh: 0 18px 44px -18px rgba(0, 0, 0, .6);
|
|
105
|
+
color-scheme: dark;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
:root[data-theme="dark"] {
|
|
109
|
+
--bg: #121118; --surface: #1b1a26;
|
|
110
|
+
--ink: #f3f5f9; --ink-2: #c7cede; --muted: #98a1b5; --faint: #6f7994;
|
|
111
|
+
--accent: #5fe0cf; --accent-ink: #7fe3d6; --accent-wash: #10302d; --accent-line: #1f524c;
|
|
112
|
+
--border: #2a2836;
|
|
113
|
+
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
114
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
115
|
+
--sh: 0 18px 44px -18px rgba(0, 0, 0, .6);
|
|
116
|
+
color-scheme: dark;
|
|
117
|
+
}
|
|
118
|
+
:root[data-theme="light"] {
|
|
119
|
+
--bg: #faf8f4; --surface: #ffffff;
|
|
120
|
+
--ink: #10294b; --ink-2: #1b2a40; --muted: #4d5a70; --faint: #8593a8;
|
|
121
|
+
--accent: #0d9391; --accent-ink: #0b6360; --accent-wash: #e4f4f2; --accent-line: #c1e7e2;
|
|
122
|
+
--border: #e6e2d9;
|
|
123
|
+
--warn: #bf3d2c; --warn-wash: #fbecea; --warn-line: #f0ccc6;
|
|
124
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
125
|
+
--sh: 0 18px 50px rgba(16, 41, 75, .13);
|
|
126
|
+
color-scheme: light;
|
|
127
|
+
}
|
|
128
|
+
|
|
59
129
|
* { box-sizing: border-box; }
|
|
60
130
|
body {
|
|
61
|
-
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
62
131
|
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
63
|
-
padding: 2rem 1.25rem;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
132
|
+
padding: 2rem 1.25rem;
|
|
133
|
+
background:
|
|
134
|
+
radial-gradient(34rem 18rem at 92% -8%, var(--accent-wash) 0%, transparent 60%),
|
|
135
|
+
var(--bg);
|
|
136
|
+
background-repeat: no-repeat;
|
|
137
|
+
color: var(--ink-2); font-family: var(--body); font-size: 15px; line-height: 1.6;
|
|
138
|
+
letter-spacing: -.011em; -webkit-font-smoothing: antialiased;
|
|
67
139
|
}
|
|
68
140
|
.card {
|
|
69
|
-
width: 100%; max-width: 26rem; background:
|
|
70
|
-
border
|
|
141
|
+
width: 100%; max-width: 26rem; background: var(--surface);
|
|
142
|
+
border: 1px solid var(--accent-line); border-radius: 20px;
|
|
143
|
+
padding: 2.1rem 1.9rem 1.9rem; position: relative; overflow: hidden;
|
|
144
|
+
box-shadow: var(--sh); animation: rise .5s cubic-bezier(.2, .9, .3, 1) both;
|
|
145
|
+
}
|
|
146
|
+
.card::before {
|
|
147
|
+
content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
|
|
148
|
+
background: var(--grad);
|
|
149
|
+
}
|
|
150
|
+
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
151
|
+
|
|
152
|
+
.mark {
|
|
153
|
+
width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center;
|
|
154
|
+
color: #fff; background: var(--grad);
|
|
155
|
+
box-shadow: 0 8px 20px -6px rgba(13, 147, 145, .6); position: relative; margin: 0 0 1.15rem;
|
|
156
|
+
}
|
|
157
|
+
.mark::after {
|
|
158
|
+
content: ""; position: absolute; inset: -5px; border-radius: 17px;
|
|
159
|
+
border: 1.5px solid var(--accent); opacity: 0; animation: halo 2.8s ease-in-out infinite;
|
|
160
|
+
}
|
|
161
|
+
@keyframes halo { 0%, 100% { opacity: 0; transform: scale(.96); } 50% { opacity: .5; transform: scale(1.04); } }
|
|
162
|
+
.mark svg { width: 24px; height: 24px; }
|
|
163
|
+
|
|
164
|
+
.eyebrow {
|
|
165
|
+
font-family: var(--mono); font-size: .66rem; font-weight: 600; text-transform: uppercase;
|
|
166
|
+
letter-spacing: .13em; color: var(--accent-ink); margin: 0 0 .55rem;
|
|
167
|
+
}
|
|
168
|
+
h1 {
|
|
169
|
+
font-family: var(--display); font-weight: 800; letter-spacing: -.025em; color: var(--ink);
|
|
170
|
+
font-size: 1.55rem; margin: 0 0 .5rem; text-wrap: balance;
|
|
171
|
+
}
|
|
172
|
+
.sub { color: var(--muted); font-size: .95rem; line-height: 1.55; margin: 0 0 1.4rem; }
|
|
173
|
+
label {
|
|
174
|
+
display: block; font-family: var(--mono); font-size: .66rem; font-weight: 600;
|
|
175
|
+
text-transform: uppercase; letter-spacing: .12em; color: var(--faint); margin: 0 0 .4rem;
|
|
71
176
|
}
|
|
72
|
-
@media (prefers-color-scheme: dark) {
|
|
73
|
-
.card { background: #1e1e28; border-color: #33333f; box-shadow: none; }
|
|
74
|
-
}
|
|
75
|
-
.eyebrow { font: 600 .68rem/1 ui-monospace, monospace; letter-spacing: .12em;
|
|
76
|
-
text-transform: uppercase; color: #8a7f66; margin: 0 0 .75rem; }
|
|
77
|
-
h1 { font-size: 1.4rem; letter-spacing: -.01em; margin: 0 0 .4rem; }
|
|
78
|
-
.sub { color: #6a6a6a; margin: 0 0 1.5rem; font-size: .95rem; }
|
|
79
|
-
@media (prefers-color-scheme: dark) { .sub { color: #a2a2ad; } }
|
|
80
|
-
label { display: block; font-weight: 600; font-size: .82rem; margin: 0 0 .4rem; }
|
|
81
177
|
input[type=email], input[type=text] {
|
|
82
|
-
width: 100%;
|
|
83
|
-
|
|
178
|
+
width: 100%; border: 1px solid var(--border); border-radius: 11px;
|
|
179
|
+
padding: .8rem .85rem; color: var(--ink); background: var(--bg);
|
|
180
|
+
font: 600 .95rem/1.35 var(--body);
|
|
84
181
|
}
|
|
85
|
-
|
|
86
|
-
|
|
182
|
+
input[type=email]:focus-visible, input[type=text]:focus-visible {
|
|
183
|
+
outline: 2px solid var(--accent); outline-offset: 2px;
|
|
184
|
+
}
|
|
185
|
+
input[type=text].code {
|
|
186
|
+
letter-spacing: .4em; font: 600 1.25rem var(--mono); text-align: center;
|
|
87
187
|
}
|
|
88
|
-
input[type=text].code { letter-spacing: .4em; font: 600 1.25rem ui-monospace, monospace; text-align: center; }
|
|
89
188
|
button {
|
|
90
|
-
width: 100%; margin-top: 1rem;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
189
|
+
width: 100%; margin-top: 1rem; display: inline-flex; align-items: center;
|
|
190
|
+
justify-content: center; gap: .55rem; font-family: var(--display); font-weight: 700;
|
|
191
|
+
font-size: 1rem; color: #fff; background: var(--grad); border: 0; border-radius: 999px;
|
|
192
|
+
padding: .9rem 1.3rem; cursor: pointer; box-shadow: 0 10px 24px rgba(13, 147, 145, .32);
|
|
193
|
+
transition: transform .12s, box-shadow .12s, filter .12s;
|
|
194
|
+
}
|
|
195
|
+
button:hover { transform: translateY(-1px); filter: saturate(1.08); box-shadow: 0 14px 30px rgba(13, 147, 145, .4); }
|
|
196
|
+
button:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
197
|
+
button svg { width: 17px; height: 17px; }
|
|
198
|
+
/* Quiet secondary action (e.g. "resend SMS code") — a text-link-weight button
|
|
199
|
+
so it doesn't compete with the card's one primary gradient CTA. */
|
|
200
|
+
.btn-quiet {
|
|
201
|
+
width: auto; margin: 0; background: none; box-shadow: none; border: 0;
|
|
202
|
+
color: var(--accent-ink); font: 600 .85rem var(--display); padding: 0; cursor: pointer;
|
|
203
|
+
}
|
|
204
|
+
.btn-quiet:hover { transform: none; filter: none; text-decoration: underline; }
|
|
205
|
+
.err {
|
|
206
|
+
display: flex; gap: .55rem; align-items: flex-start; background: var(--warn-wash);
|
|
207
|
+
border: 1px solid var(--warn-line); color: var(--warn); border-radius: 11px;
|
|
208
|
+
padding: .75rem .9rem; font-size: .9rem; line-height: 1.5; margin: 0 0 1.15rem;
|
|
209
|
+
}
|
|
210
|
+
.dev {
|
|
211
|
+
background: var(--accent-wash); border: 1px solid var(--accent-line); color: var(--ink-2);
|
|
212
|
+
border-radius: 11px; padding: .65rem .85rem; font-size: .82rem; line-height: 1.5; margin: 0 0 1.15rem;
|
|
213
|
+
}
|
|
214
|
+
.dev code { font: 600 .8em var(--mono); color: var(--accent-ink); }
|
|
215
|
+
.note { font-size: .82rem; color: var(--faint); margin: 1.15rem 0 0; }
|
|
216
|
+
.note a { color: var(--accent-ink); font-weight: 600; text-decoration: none; }
|
|
217
|
+
.note a:hover { text-decoration: underline; }
|
|
218
|
+
.note a:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 3px; }
|
|
219
|
+
.who { font-weight: 600; color: var(--ink); }
|
|
220
|
+
.foot {
|
|
221
|
+
margin-top: 1.5rem; font-family: var(--mono); font-size: .7rem; letter-spacing: .04em;
|
|
222
|
+
color: var(--faint); text-align: center;
|
|
223
|
+
}
|
|
224
|
+
@media (prefers-reduced-motion: reduce) {
|
|
225
|
+
.card, .mark::after { animation: none !important; }
|
|
226
|
+
button { transition: none; }
|
|
227
|
+
}
|
|
106
228
|
</style>
|
|
107
229
|
</head>
|
|
108
230
|
<body>
|
|
109
231
|
<main class="card">
|
|
232
|
+
<span class="mark" aria-hidden="true">
|
|
233
|
+
<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>
|
|
234
|
+
</span>
|
|
110
235
|
<p class="eyebrow">Token of Trust© · Regulated Storefront</p>
|
|
111
236
|
{devMode && (devAuthOn ? (
|
|
112
237
|
<p class="dev"><strong>Local dev — console bypass</strong> (<code>DEV_AUTH=1</code>). No email is sent;
|
|
@@ -118,7 +243,32 @@ if (devMode) {
|
|
|
118
243
|
))}
|
|
119
244
|
{errorMessage && <p class="err">{errorMessage}</p>}
|
|
120
245
|
|
|
121
|
-
{
|
|
246
|
+
{stepUpRequired || successPending ? (
|
|
247
|
+
<>
|
|
248
|
+
<h1>{successPending ? "Almost there" : "Verify with SMS to continue"}</h1>
|
|
249
|
+
<p class="sub">
|
|
250
|
+
{successPending ? "Your identity is verified — sign-in just needs one more step. " : null}
|
|
251
|
+
We sent {stepUpFactorLabel} to the phone on file for <span class="who">{email}</span>. Enter it below to finish signing in.
|
|
252
|
+
</p>
|
|
253
|
+
<form method="post" action="/api/auth/step-up-verify" autocomplete="one-time-code">
|
|
254
|
+
<input type="hidden" name="identifier" value={email} />
|
|
255
|
+
<input type="hidden" name="next" value={next} />
|
|
256
|
+
<label for="stepup-code">SMS code</label>
|
|
257
|
+
<input class="code" id="stepup-code" name="code" type="text" inputmode="numeric"
|
|
258
|
+
pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required autofocus />
|
|
259
|
+
<button type="submit">
|
|
260
|
+
Verify & continue
|
|
261
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
262
|
+
</button>
|
|
263
|
+
</form>
|
|
264
|
+
<p class="note" style="margin-top:1.15rem;">Didn't get a code?</p>
|
|
265
|
+
<form method="post" action="/api/auth/step-up-send">
|
|
266
|
+
<input type="hidden" name="identifier" value={email} />
|
|
267
|
+
<button type="submit" class="btn-quiet">Resend SMS code</button>
|
|
268
|
+
</form>
|
|
269
|
+
<p class="note"><a href={`/auth/login${next !== "/" ? `?next=${encodeURIComponent(next)}` : ""}`}>Use a different email</a></p>
|
|
270
|
+
</>
|
|
271
|
+
) : showCodeStep ? (
|
|
122
272
|
<>
|
|
123
273
|
<h1>Check your email</h1>
|
|
124
274
|
<p class="sub">We sent a 6-digit code to <span class="who">{email}</span> if it has access to this preview. Enter it below.</p>
|
|
@@ -128,7 +278,10 @@ if (devMode) {
|
|
|
128
278
|
<label for="code">Access code</label>
|
|
129
279
|
<input class="code" id="code" name="code" type="text" inputmode="numeric"
|
|
130
280
|
pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required autofocus />
|
|
131
|
-
<button type="submit">
|
|
281
|
+
<button type="submit">
|
|
282
|
+
Verify & view preview
|
|
283
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
284
|
+
</button>
|
|
132
285
|
</form>
|
|
133
286
|
<p class="note"><a href={`/auth/login${next !== "/" ? `?next=${encodeURIComponent(next)}` : ""}`}>Use a different email</a> · <a href="https://mcp.tokenoftrust.com/early-access/storefront">Don't have access? Request early access →</a></p>
|
|
134
287
|
</>
|
|
@@ -141,7 +294,10 @@ if (devMode) {
|
|
|
141
294
|
<label for="email">Work email</label>
|
|
142
295
|
<input id="email" name="email" type="email" value={email}
|
|
143
296
|
autocomplete="email" placeholder="you@company.com" required autofocus />
|
|
144
|
-
<button type="submit">
|
|
297
|
+
<button type="submit">
|
|
298
|
+
Email me a code
|
|
299
|
+
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M4 6.5h16v11H4z" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/><path d="m4.5 7 7.5 5.5L19.5 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
300
|
+
</button>
|
|
145
301
|
</form>
|
|
146
302
|
<p class="note">Access is granted by role — no signup needed. Don't have access yet? <a href="https://mcp.tokenoftrust.com/early-access/storefront">Request early access →</a></p>
|
|
147
303
|
</>
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
* in a hidden form field and leaves only via the POST.
|
|
11
11
|
*/
|
|
12
12
|
import { normalizeEmailHint, redactEmailForHint } from "@/lib/privacy/emailHint";
|
|
13
|
+
// Self-hosted web fonts (audit finding 6) — same-origin @fontsource, replacing
|
|
14
|
+
// the render-blocking fonts.googleapis.com stylesheet. Covers Plus Jakarta Sans,
|
|
15
|
+
// Inter, and JetBrains Mono used by this page's inline styles.
|
|
16
|
+
import "@/styles/fonts.css";
|
|
13
17
|
|
|
14
18
|
export const prerender = false;
|
|
15
19
|
|
|
@@ -44,6 +48,22 @@ const next = (() => {
|
|
|
44
48
|
return n.startsWith("/") && !n.startsWith("//") ? n : "/dashboard";
|
|
45
49
|
})();
|
|
46
50
|
|
|
51
|
+
// RFC 9470 step-up: /api/auth/magic-exchange redirects here (not to /auth/login)
|
|
52
|
+
// with `reason` distinct from `error` — see `magicRedirect` there. Unlike the
|
|
53
|
+
// email-code path, this redirect never carries the real email (only the
|
|
54
|
+
// redacted `email_hint`), so we can't render the SMS-completion card here
|
|
55
|
+
// no-JS-safe with a hidden identifier — hand off to /auth/login, which has the
|
|
56
|
+
// full email and the step-up card, once the human re-enters it.
|
|
57
|
+
const reason = url.searchParams.get("reason");
|
|
58
|
+
const requiredAcr = url.searchParams.get("requiredAcr") ?? "";
|
|
59
|
+
const stepUpRequired = reason === "step-up-required";
|
|
60
|
+
const successPending = reason === "success-pending";
|
|
61
|
+
const ACR_LABELS: Record<string, string> = {
|
|
62
|
+
"tot:aal:sms": "a text message (SMS) code",
|
|
63
|
+
};
|
|
64
|
+
const stepUpFactorLabel = requiredAcr ? (ACR_LABELS[requiredAcr] ?? "an additional verification step") : "an additional verification step";
|
|
65
|
+
const loginHref = `/auth/login${next !== "/dashboard" ? `?next=${encodeURIComponent(next)}` : ""}`;
|
|
66
|
+
|
|
47
67
|
const ERROR_MESSAGES: Record<string, string> = {
|
|
48
68
|
invalid: "This sign-in link isn't valid or has already been used.",
|
|
49
69
|
expired: "This sign-in link has expired.",
|
|
@@ -77,23 +97,22 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
77
97
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
78
98
|
<meta name="robots" content="noindex, nofollow" />
|
|
79
99
|
<title>Confirm sign in — Regulated Storefront by Token of Trust©</title>
|
|
80
|
-
|
|
81
|
-
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
82
|
-
<link
|
|
83
|
-
rel="stylesheet"
|
|
84
|
-
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@500;600&display=swap"
|
|
85
|
-
/>
|
|
100
|
+
{/* Web fonts are self-hosted via `@/styles/fonts.css` (imported above). */}
|
|
86
101
|
<style nonce={cspNonce}>
|
|
87
|
-
/*
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
/* storefront.tokenoftrust.store visual system — the sign-in door wears the
|
|
103
|
+
same skin as the marketing storefront it belongs to: warm cream + deep
|
|
104
|
+
navy, the signature teal→blue gradient (--grad), Plus Jakarta / Inter /
|
|
105
|
+
JetBrains. Token values mirror /tenants/home/pages/storefront.css. Theme
|
|
106
|
+
via the triple: :root default light, @media dark (the site's dark-hero
|
|
107
|
+
palette), and [data-theme] overrides. */
|
|
90
108
|
:root {
|
|
91
|
-
--bg: #
|
|
92
|
-
--ink: #
|
|
93
|
-
--accent: #
|
|
94
|
-
--border: #
|
|
95
|
-
--warn: #
|
|
96
|
-
--
|
|
109
|
+
--bg: #faf8f4; --surface: #ffffff;
|
|
110
|
+
--ink: #10294b; --ink-2: #1b2a40; --muted: #4d5a70; --faint: #8593a8;
|
|
111
|
+
--accent: #0d9391; --accent-ink: #0b6360; --accent-wash: #e4f4f2; --accent-line: #c1e7e2;
|
|
112
|
+
--border: #e6e2d9;
|
|
113
|
+
--warn: #bf3d2c; --warn-wash: #fbecea; --warn-line: #f0ccc6;
|
|
114
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
115
|
+
--sh: 0 18px 50px rgba(16, 41, 75, .13);
|
|
97
116
|
--display: "Plus Jakarta Sans", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
98
117
|
--body: "Inter", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
99
118
|
--mono: "JetBrains Mono", ui-monospace, "SF Mono", "Cascadia Code", Menlo, monospace;
|
|
@@ -101,31 +120,34 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
101
120
|
}
|
|
102
121
|
@media (prefers-color-scheme: dark) {
|
|
103
122
|
:root {
|
|
104
|
-
--bg: #
|
|
105
|
-
--ink: #
|
|
106
|
-
--accent: #
|
|
107
|
-
--border: #
|
|
123
|
+
--bg: #121118; --surface: #1b1a26;
|
|
124
|
+
--ink: #f3f5f9; --ink-2: #c7cede; --muted: #98a1b5; --faint: #6f7994;
|
|
125
|
+
--accent: #5fe0cf; --accent-ink: #7fe3d6; --accent-wash: #10302d; --accent-line: #1f524c;
|
|
126
|
+
--border: #2a2836;
|
|
108
127
|
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
109
|
-
--
|
|
128
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
129
|
+
--sh: 0 18px 44px -18px rgba(0, 0, 0, .6);
|
|
110
130
|
color-scheme: dark;
|
|
111
131
|
}
|
|
112
132
|
}
|
|
113
133
|
:root[data-theme="dark"] {
|
|
114
|
-
--bg: #
|
|
115
|
-
--ink: #
|
|
116
|
-
--accent: #
|
|
117
|
-
--border: #
|
|
134
|
+
--bg: #121118; --surface: #1b1a26;
|
|
135
|
+
--ink: #f3f5f9; --ink-2: #c7cede; --muted: #98a1b5; --faint: #6f7994;
|
|
136
|
+
--accent: #5fe0cf; --accent-ink: #7fe3d6; --accent-wash: #10302d; --accent-line: #1f524c;
|
|
137
|
+
--border: #2a2836;
|
|
118
138
|
--warn: #f2857f; --warn-wash: #2a1414; --warn-line: #5a2626;
|
|
119
|
-
--
|
|
139
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
140
|
+
--sh: 0 18px 44px -18px rgba(0, 0, 0, .6);
|
|
120
141
|
color-scheme: dark;
|
|
121
142
|
}
|
|
122
143
|
:root[data-theme="light"] {
|
|
123
|
-
--bg: #
|
|
124
|
-
--ink: #
|
|
125
|
-
--accent: #
|
|
126
|
-
--border: #
|
|
127
|
-
--warn: #
|
|
128
|
-
--
|
|
144
|
+
--bg: #faf8f4; --surface: #ffffff;
|
|
145
|
+
--ink: #10294b; --ink-2: #1b2a40; --muted: #4d5a70; --faint: #8593a8;
|
|
146
|
+
--accent: #0d9391; --accent-ink: #0b6360; --accent-wash: #e4f4f2; --accent-line: #c1e7e2;
|
|
147
|
+
--border: #e6e2d9;
|
|
148
|
+
--warn: #bf3d2c; --warn-wash: #fbecea; --warn-line: #f0ccc6;
|
|
149
|
+
--grad: linear-gradient(115deg, #0d9391, #3b82c4);
|
|
150
|
+
--sh: 0 18px 50px rgba(16, 41, 75, .13);
|
|
129
151
|
color-scheme: light;
|
|
130
152
|
}
|
|
131
153
|
|
|
@@ -148,14 +170,14 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
148
170
|
}
|
|
149
171
|
.card::before {
|
|
150
172
|
content: ""; position: absolute; inset: 0 0 auto 0; height: 3px;
|
|
151
|
-
background:
|
|
173
|
+
background: var(--grad);
|
|
152
174
|
}
|
|
153
175
|
@keyframes rise { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } }
|
|
154
176
|
|
|
155
177
|
.mark {
|
|
156
178
|
width: 46px; height: 46px; border-radius: 13px; display: grid; place-items: center;
|
|
157
|
-
color: #fff; background:
|
|
158
|
-
box-shadow: 0
|
|
179
|
+
color: #fff; background: var(--grad);
|
|
180
|
+
box-shadow: 0 8px 20px -6px rgba(13, 147, 145, .6); position: relative;
|
|
159
181
|
}
|
|
160
182
|
.mark::after {
|
|
161
183
|
content: ""; position: absolute; inset: -5px; border-radius: 17px;
|
|
@@ -194,12 +216,12 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
194
216
|
form { margin: 0; }
|
|
195
217
|
.cta {
|
|
196
218
|
width: 100%; display: inline-flex; align-items: center; justify-content: center; gap: .55rem;
|
|
197
|
-
font-family: var(--display); font-weight: 700; font-size: 1rem; color: #fff; background: var(--
|
|
219
|
+
font-family: var(--display); font-weight: 700; font-size: 1rem; color: #fff; background: var(--grad);
|
|
198
220
|
border: 0; border-radius: 999px; padding: .9rem 1.3rem; cursor: pointer;
|
|
199
|
-
box-shadow: 0 10px
|
|
200
|
-
transition: transform .12s,
|
|
221
|
+
box-shadow: 0 10px 24px rgba(13, 147, 145, .32);
|
|
222
|
+
transition: transform .12s, box-shadow .12s, filter .12s;
|
|
201
223
|
}
|
|
202
|
-
.cta:hover { transform: translateY(-1px);
|
|
224
|
+
.cta:hover { transform: translateY(-1px); filter: saturate(1.08); box-shadow: 0 14px 30px rgba(13, 147, 145, .4); }
|
|
203
225
|
.cta:focus-visible { outline: 2px solid var(--accent); outline-offset: 3px; }
|
|
204
226
|
.cta svg { width: 17px; height: 17px; }
|
|
205
227
|
|
|
@@ -253,7 +275,17 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
253
275
|
<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>
|
|
254
276
|
</span>
|
|
255
277
|
<p class="eyebrow">Token of Trust© · Regulated Storefront</p>
|
|
256
|
-
{
|
|
278
|
+
{stepUpRequired || successPending ? (
|
|
279
|
+
<>
|
|
280
|
+
<h1>{successPending ? "Almost there" : "Verify with SMS to continue"}</h1>
|
|
281
|
+
<p class="sub">
|
|
282
|
+
{successPending
|
|
283
|
+
? "Your identity is verified — sign-in just needs one more step: verify with SMS to continue."
|
|
284
|
+
: `Your email is confirmed. For extra security, please verify ${stepUpFactorLabel} before we can finish signing you in.`}
|
|
285
|
+
</p>
|
|
286
|
+
<p class="altrow"><a class="altlink" href={loginHref}>Continue with SMS verification →</a></p>
|
|
287
|
+
</>
|
|
288
|
+
) : resendStatus === "sent" ? (
|
|
257
289
|
<>
|
|
258
290
|
<h1>Check your email</h1>
|
|
259
291
|
<p class="sub">If that address is still on this store's invite list, a fresh sign-in link is on the way.</p>
|
|
@@ -286,8 +318,8 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
286
318
|
</>
|
|
287
319
|
) : (
|
|
288
320
|
<>
|
|
289
|
-
<h1>
|
|
290
|
-
<p class="sub">Confirm it's you to finish signing in.</p>
|
|
321
|
+
<h1>Accept your invitation</h1>
|
|
322
|
+
<p class="sub">Confirm it's you to accept this invitation and finish signing in.</p>
|
|
291
323
|
{appLabel ? (
|
|
292
324
|
<div class="ident">
|
|
293
325
|
<span class="ava" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none"><path d="M4 9.5 5.2 5h13.6L20 9.5M4 9.5V19h16V9.5M4 9.5h16M9 19v-5h6v5" stroke="currentColor" stroke-width="2" stroke-linejoin="round"/></svg></span>
|
|
@@ -310,13 +342,13 @@ const showResendForm = !!errorMessage && !!tenant && resendStatus !== "sent";
|
|
|
310
342
|
<input type="hidden" name="email_hint" value={emailHint} />
|
|
311
343
|
<input type="hidden" name="next" value={next} />
|
|
312
344
|
<button class="cta" type="submit">
|
|
313
|
-
|
|
345
|
+
Accept & sign in
|
|
314
346
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
|
|
315
347
|
</button>
|
|
316
348
|
</form>
|
|
317
349
|
<p class="note">
|
|
318
350
|
<svg viewBox="0 0 24 24" fill="none" aria-hidden="true"><rect x="5" y="11" width="14" height="9" rx="2" stroke="currentColor" stroke-width="2"/><path d="M8 11V8a4 4 0 0 1 8 0v3" stroke="currentColor" stroke-width="2"/></svg>
|
|
319
|
-
This is a single-use link —
|
|
351
|
+
This is a single-use link — accepting completes your sign-in.
|
|
320
352
|
</p>
|
|
321
353
|
</>
|
|
322
354
|
)}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
import Layout from "@/layouts/Layout.astro";
|
|
3
|
+
import { getBlogPost, resolveBlogRedirect } from "@/lib/blog/provider.js";
|
|
4
|
+
import { breadcrumbLd } from "@/lib/jsonld.js";
|
|
5
|
+
|
|
6
|
+
const { tenant, canonicalBase, basePath } = Astro.locals;
|
|
7
|
+
const slug = Astro.params.slug ?? "";
|
|
8
|
+
const post = await getBlogPost(tenant.tenant_id, slug);
|
|
9
|
+
if (!post) {
|
|
10
|
+
const redirect = await resolveBlogRedirect(tenant.tenant_id, slug);
|
|
11
|
+
if (redirect) return Astro.redirect(`${basePath}/blog/${redirect}/`, 301);
|
|
12
|
+
return new Response(null, { status: 404 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const canonical = new URL(`/blog/${post.slug}/`, canonicalBase + "/").toString();
|
|
16
|
+
const articleLd = {
|
|
17
|
+
"@context": "https://schema.org",
|
|
18
|
+
"@type": "BlogPosting",
|
|
19
|
+
headline: post.title,
|
|
20
|
+
description: post.description,
|
|
21
|
+
url: canonical,
|
|
22
|
+
mainEntityOfPage: canonical,
|
|
23
|
+
datePublished: post.publishedAt,
|
|
24
|
+
dateModified: post.modifiedAt,
|
|
25
|
+
author: { "@type": "Person", name: post.author },
|
|
26
|
+
publisher: { "@type": "Organization", name: tenant.displayName, url: canonicalBase },
|
|
27
|
+
...(post.featuredImage ? { image: new URL(post.featuredImage.src, canonicalBase + "/").toString() } : {}),
|
|
28
|
+
};
|
|
29
|
+
const crumbs = breadcrumbLd([
|
|
30
|
+
{ label: tenant.displayName, href: "/" },
|
|
31
|
+
{ label: "Blog", href: "/blog/" },
|
|
32
|
+
{ label: post.title, href: `/blog/${post.slug}/` },
|
|
33
|
+
], canonicalBase);
|
|
34
|
+
const blogHref = `${basePath}/blog/`;
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
<Layout
|
|
38
|
+
title={post.seoTitle || post.title}
|
|
39
|
+
description={post.description}
|
|
40
|
+
path={`/blog/${post.slug}/`}
|
|
41
|
+
ogType="article"
|
|
42
|
+
ogImage={post.featuredImage?.src}
|
|
43
|
+
jsonLd={[crumbs, articleLd]}
|
|
44
|
+
>
|
|
45
|
+
<article class="article-shell">
|
|
46
|
+
<nav class="breadcrumbs" aria-label="Breadcrumb"><a href={blogHref}>Blog</a><span aria-hidden="true">/</span><span>{post.categories[0] ?? "Article"}</span></nav>
|
|
47
|
+
<header>
|
|
48
|
+
{post.tags[0] && <p class="eyebrow eyebrow--tick">{post.tags[0]}</p>}
|
|
49
|
+
<h1>{post.title}</h1>
|
|
50
|
+
<p class="dek">{post.excerpt}</p>
|
|
51
|
+
<div class="byline">
|
|
52
|
+
<span>By {post.author}</span>
|
|
53
|
+
<span aria-hidden="true">·</span>
|
|
54
|
+
<time datetime={post.publishedAt}>{new Date(post.publishedAt).toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric" })}</time>
|
|
55
|
+
</div>
|
|
56
|
+
</header>
|
|
57
|
+
{post.featuredImage && (
|
|
58
|
+
<img
|
|
59
|
+
class="featured"
|
|
60
|
+
src={post.featuredImage.src}
|
|
61
|
+
alt={post.featuredImage.alt}
|
|
62
|
+
width={post.featuredImage.width}
|
|
63
|
+
height={post.featuredImage.height}
|
|
64
|
+
fetchpriority="high"
|
|
65
|
+
decoding="async"
|
|
66
|
+
/>
|
|
67
|
+
)}
|
|
68
|
+
<div class="article-grid">
|
|
69
|
+
<aside>
|
|
70
|
+
<a href={blogHref}>← All articles</a>
|
|
71
|
+
{post.tags.length > 0 && <div class="article-tags">{post.tags.map((tag) => <span>{tag}</span>)}</div>}
|
|
72
|
+
</aside>
|
|
73
|
+
<div class="blog-body" set:html={post.contentHtml}></div>
|
|
74
|
+
</div>
|
|
75
|
+
</article>
|
|
76
|
+
</Layout>
|
|
77
|
+
|
|
78
|
+
<style>
|
|
79
|
+
.article-shell { width: min(1120px, calc(100% - 2rem)); margin-inline: auto; padding-block: clamp(2rem, 5vw, 5rem); }
|
|
80
|
+
.breadcrumbs { display: flex; gap: .55rem; align-items: center; color: var(--color-muted); font-size: .84rem; }
|
|
81
|
+
.breadcrumbs a { color: var(--color-accent-ink); font-weight: 700; }
|
|
82
|
+
header { max-width: 880px; margin: clamp(2.5rem, 6vw, 5rem) auto clamp(2rem, 5vw, 3.5rem); text-align: center; }
|
|
83
|
+
header .eyebrow { justify-content: center; }
|
|
84
|
+
h1 { margin-top: .9rem; font-size: clamp(2.5rem, 6vw, 5rem); line-height: 1.02; }
|
|
85
|
+
.dek { max-width: 68ch; margin: 1.5rem auto 0; color: var(--color-muted); font-size: clamp(1rem, 2vw, 1.2rem); }
|
|
86
|
+
.byline { display: flex; justify-content: center; gap: .55rem; margin-top: 1.35rem; color: var(--color-muted); font-size: .85rem; }
|
|
87
|
+
.featured { display: block; width: 100%; max-height: 620px; object-fit: cover; border-radius: var(--radius-lg); box-shadow: var(--shadow-md); }
|
|
88
|
+
.article-grid { display: grid; grid-template-columns: 190px minmax(0, 720px); justify-content: center; gap: clamp(2rem, 6vw, 5rem); margin-top: clamp(3rem, 7vw, 5.5rem); }
|
|
89
|
+
aside { align-self: start; position: sticky; top: 7rem; }
|
|
90
|
+
aside > a { color: var(--color-accent-ink); font-size: .88rem; font-weight: 700; }
|
|
91
|
+
.article-tags { display: flex; flex-wrap: wrap; gap: .45rem; margin-top: 1.2rem; }
|
|
92
|
+
.article-tags span { padding: .3rem .55rem; border: 1px solid var(--color-border); border-radius: var(--radius-full); color: var(--color-muted); font-size: .7rem; }
|
|
93
|
+
.blog-body { min-width: 0; font-size: 1.05rem; line-height: 1.78; }
|
|
94
|
+
.blog-body :global(p), .blog-body :global(ul), .blog-body :global(ol), .blog-body :global(blockquote), .blog-body :global(figure), .blog-body :global(table), .blog-body :global(pre) { margin-block: 1.25rem; }
|
|
95
|
+
.blog-body :global(h2), .blog-body :global(h3), .blog-body :global(h4) { scroll-margin-top: 7rem; margin: 2.8rem 0 1rem; line-height: 1.18; }
|
|
96
|
+
.blog-body :global(h2) { font-size: clamp(1.75rem, 3vw, 2.35rem); }
|
|
97
|
+
.blog-body :global(h3) { font-size: clamp(1.35rem, 2.4vw, 1.75rem); }
|
|
98
|
+
.blog-body :global(a) { color: var(--color-accent-ink); text-decoration: underline; text-underline-offset: .18em; }
|
|
99
|
+
.blog-body :global(ul), .blog-body :global(ol) { padding-left: 1.4rem; }
|
|
100
|
+
.blog-body :global(li) { margin-block: .45rem; }
|
|
101
|
+
.blog-body :global(blockquote) { padding: .8rem 1.25rem; border-left: 4px solid var(--color-accent); background: var(--color-surface); color: var(--color-muted); }
|
|
102
|
+
.blog-body :global(img) { display: block; width: auto; max-width: 100%; margin-inline: auto; border-radius: var(--radius-md); }
|
|
103
|
+
.blog-body :global(figcaption) { margin-top: .5rem; color: var(--color-muted); text-align: center; font-size: .78rem; }
|
|
104
|
+
.blog-body :global(table) { display: block; width: 100%; overflow-x: auto; border-collapse: collapse; }
|
|
105
|
+
.blog-body :global(th), .blog-body :global(td) { padding: .7rem; border: 1px solid var(--color-border); text-align: left; }
|
|
106
|
+
@media (max-width: 760px) { .article-grid { grid-template-columns: 1fr; } aside { position: static; } .article-tags { display: none; } }
|
|
107
|
+
</style>
|