@tokenoftrust/storefront-runner 1.3.2 → 1.3.4-rc.2
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 +59 -0
- package/apps/storefront/env.d.ts +24 -8
- package/apps/storefront/package.json +1 -1
- package/apps/storefront/src/components/admin/{AdminFoxyCartSettingsPanel.astro → AdminCheckoutSettingsPanel.astro} +50 -51
- package/apps/storefront/src/components/admin/AdminSettingsTab.astro +2 -2
- package/apps/storefront/src/components/admin/AdminSubscriptionsTab.astro +613 -0
- package/apps/storefront/src/components/admin/orders/OrdersWorkspace.astro +3 -3
- package/apps/storefront/src/components/chrome/SiteHeader.astro +3 -4
- package/apps/storefront/src/components/commerce/{FoxySidecartStyles.astro → CartDrawerStyles.astro} +13 -14
- package/apps/storefront/src/components/commerce/CheckoutLoader.astro +21 -0
- package/apps/storefront/src/components/islands/VariantSelector.tsx +101 -20
- package/apps/storefront/src/config/adminTargetTenant.ts +2 -2
- package/apps/storefront/src/config/devTenantSeed.ts +65 -16
- package/apps/storefront/src/config/resolver.ts +48 -1
- package/apps/storefront/src/layouts/Layout.astro +14 -12
- package/apps/storefront/src/lib/admin/README.md +2 -2
- package/apps/storefront/src/lib/admin/checkoutStyleInput.ts +5 -5
- package/apps/storefront/src/lib/admin/orderClient.ts +20 -20
- package/apps/storefront/src/lib/admin/services/mock/{foxyCartSettings.ts → checkoutSettings.ts} +21 -22
- package/apps/storefront/src/lib/admin/services/mock/index.ts +3 -2
- package/apps/storefront/src/lib/admin/services/mock/settings.ts +1 -1
- package/apps/storefront/src/lib/admin/services/mock/subscriptions.ts +398 -0
- package/apps/storefront/src/lib/admin/services/orders.ts +21 -21
- package/apps/storefront/src/lib/admin/viewmodels/subscriptions.ts +106 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +14 -3
- package/apps/storefront/src/lib/{foxyCommerce.ts → checkoutCommerce.ts} +20 -18
- package/apps/storefront/src/lib/commerce/{foxyCartSettings.ts → checkoutSettings.ts} +65 -66
- package/apps/storefront/src/lib/dev/activityIngestToken.ts +9 -0
- package/apps/storefront/src/lib/dev/activityStore.ts +12 -0
- package/apps/storefront/src/lib/dev/cliSignInCode.ts +23 -4
- package/apps/storefront/src/lib/dev/runtimeStore.ts +193 -22
- package/apps/storefront/src/lib/edgeCache.ts +7 -6
- package/apps/storefront/src/lib/health.ts +53 -0
- package/apps/storefront/src/lib/privacy/emailHint.ts +33 -0
- package/apps/storefront/src/lib/rawMarketingHtml.ts +1 -0
- package/apps/storefront/src/lib/sections.ts +19 -23
- package/apps/storefront/src/lib/securityHeaders.ts +1 -1
- package/apps/storefront/src/lib/storyblok/content-model.ts +3 -2
- package/apps/storefront/src/middleware/index.ts +29 -12
- package/apps/storefront/src/pages/[...slug].astro +5 -6
- package/apps/storefront/src/pages/admin.astro +10 -3
- package/apps/storefront/src/pages/api/admin/orders/[id].ts +4 -4
- package/apps/storefront/src/pages/api/admin/orders.ts +5 -5
- package/apps/storefront/src/pages/api/auth/magic-exchange.ts +14 -7
- package/apps/storefront/src/pages/api/checkout-style.css.ts +5 -5
- package/apps/storefront/src/pages/auth/login.astro +4 -1
- package/apps/storefront/src/pages/auth/magic.astro +263 -43
- package/apps/storefront/src/pages/capabilities.astro +1 -1
- package/apps/storefront/src/pages/cockpit.astro +2365 -0
- package/apps/storefront/src/pages/dashboard/[appDomain]/team.astro +19 -0
- package/apps/storefront/src/pages/index.astro +23 -29
- package/apps/storefront/src/pages/products/[handle].astro +50 -20
- package/package.json +6 -2
- package/packages/public-runtime/src/capabilities.ts +97 -9
- package/packages/public-runtime/src/{foxy-orders.ts → checkout-orders.ts} +28 -27
- package/packages/public-runtime/src/checkout-style-publish.ts +25 -25
- package/packages/public-runtime/src/checkout-style.ts +13 -13
- package/packages/public-runtime/src/{foxy.ts → checkout.ts} +111 -34
- package/packages/public-runtime/src/csp.ts +11 -11
- package/packages/public-runtime/src/customization-scripts.ts +28 -0
- package/packages/public-runtime/src/extension-contract.ts +73 -0
- package/packages/public-runtime/src/index.ts +4 -2
- package/packages/public-runtime/src/product.ts +57 -0
- package/packages/public-runtime/src/raw-html-policy.ts +35 -0
- package/packages/public-runtime/src/tenant.ts +14 -11
- package/pnpm-workspace.yaml +1 -0
- package/scripts/dev/checkout-watch.mjs +9 -0
- package/scripts/dev/file-browser.mjs +5 -1
- package/scripts/dev/transient-files.mjs +34 -0
- package/scripts/tot-dev.mjs +16 -3
- package/apps/storefront/src/components/commerce/FoxyLoader.astro +0 -23
- package/apps/storefront/src/pages/api/dev/activity.ts +0 -124
- package/apps/storefront/src/pages/api/dev/cli-signin-code.ts +0 -73
- package/apps/storefront/src/pages/api/dev/diff.ts +0 -68
- package/apps/storefront/src/pages/api/dev/file.ts +0 -57
- package/apps/storefront/src/pages/api/dev/locate.ts +0 -125
- package/apps/storefront/src/pages/api/dev/publish.ts +0 -128
- package/apps/storefront/src/pages/api/dev/tree.ts +0 -70
- package/apps/storefront/src/pages/api/dev/write.ts +0 -116
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
resolveCustomizationEnv,
|
|
30
30
|
buildCsp,
|
|
31
31
|
STOREFRONT_CSP_ALLOWLIST,
|
|
32
|
-
|
|
32
|
+
checkoutCspAdditions,
|
|
33
33
|
CSP_REPORT_GROUP,
|
|
34
34
|
CSP_REPORT_PATH,
|
|
35
35
|
readPublishedArtifact,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
capabilitiesArtifactPath,
|
|
40
40
|
} from "@tot/public-runtime";
|
|
41
41
|
import type { CapabilityConfig } from "@tot/public-runtime";
|
|
42
|
-
import { resolveTenantCommerce } from "@/lib/
|
|
42
|
+
import { resolveTenantCommerce } from "@/lib/checkoutCommerce";
|
|
43
43
|
import { evaluateDemoGate, hostInGateList } from "@/lib/demoGate";
|
|
44
44
|
import {
|
|
45
45
|
evaluateLoginGate,
|
|
@@ -51,6 +51,7 @@ import {
|
|
|
51
51
|
import { gatedHoldingResponse } from "@/lib/auth/gatePage";
|
|
52
52
|
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/auth/session";
|
|
53
53
|
import { isMaintenanceOn, maintenanceResponse } from "@/lib/maintenance";
|
|
54
|
+
import { HEALTH_PATH, healthResponse } from "@/lib/health";
|
|
54
55
|
import { membershipFor } from "@/lib/dashboard/tenantSelection";
|
|
55
56
|
import type { DeepPartial, ThemeTokens } from "@tot/public-runtime";
|
|
56
57
|
|
|
@@ -60,6 +61,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
60
61
|
const host =
|
|
61
62
|
request.headers.get("host") ?? url.host ?? "localhost";
|
|
62
63
|
|
|
64
|
+
// ---- /health — liveness + deploy-identity probe ---------------------------
|
|
65
|
+
// Answered FIRST: before tenant resolution, the access gate, the maintenance
|
|
66
|
+
// kill-switch, and the edge cache — so it reports the running build's commit SHA
|
|
67
|
+
// regardless of Host, an unresolved tenant, or a closed site. See lib/health.
|
|
68
|
+
if (url.pathname === HEALTH_PATH) {
|
|
69
|
+
return healthResponse();
|
|
70
|
+
}
|
|
71
|
+
|
|
63
72
|
// ---- Site maintenance kill-switch (fail-safe full close) ------------------
|
|
64
73
|
// MAINTENANCE_MODE=1 takes the ENTIRE site offline (every host + path) with a
|
|
65
74
|
// static "Coming Soon" holding page, BEFORE tenant resolution, the access
|
|
@@ -324,7 +333,7 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
324
333
|
// read these instead of hard-coding tenant identity checks.
|
|
325
334
|
locals.features = resolveTenantFeatures(tenant);
|
|
326
335
|
locals.capabilities = capabilities;
|
|
327
|
-
//
|
|
336
|
+
// Cart/checkout facts (Track A) — only when the tenant has a catalog
|
|
328
337
|
// AND the cartCheckout capability is enabled for THIS environment. Drives the
|
|
329
338
|
// loader + signed add-to-cart, and the CSP additions below.
|
|
330
339
|
const commerce =
|
|
@@ -356,6 +365,14 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
356
365
|
// the tenant prefix on the client.
|
|
357
366
|
locals.basePath = basePath;
|
|
358
367
|
|
|
368
|
+
// /dev was renamed to /cockpit (2026-07-20). 302-redirect any old /dev link —
|
|
369
|
+
// host-routed (/dev) or path-routed (/<vendor>/dev) — to the new path, preserving
|
|
370
|
+
// the tenant prefix + query. Temporary (302) while the cockpit UX is still evolving.
|
|
371
|
+
if (renderPath === "/dev" || renderPath.startsWith("/dev/")) {
|
|
372
|
+
const rest = renderPath.slice("/dev".length); // "" or "/sub…"
|
|
373
|
+
return Response.redirect(`${url.origin}${basePath}/cockpit${rest}${url.search}`, 302);
|
|
374
|
+
}
|
|
375
|
+
|
|
359
376
|
// When a /<domain>/ prefix selected the tenant, rewrite to the stripped route
|
|
360
377
|
// so the existing file-based routes match. next(path) rewrites WITHOUT
|
|
361
378
|
// re-running middleware, so the locals set above are preserved.
|
|
@@ -406,10 +423,10 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
406
423
|
"Reporting-Endpoints",
|
|
407
424
|
`${CSP_REPORT_GROUP}="${CSP_REPORT_PATH}"`,
|
|
408
425
|
);
|
|
409
|
-
//
|
|
426
|
+
// Checkout origins (loader CDN + the store's cart/checkout host) added to the
|
|
410
427
|
// strict policy only when this tenant has a store — so add-to-cart, the
|
|
411
428
|
// sidecart iframe/XHR, and the cross-origin form POST all pass.
|
|
412
|
-
const
|
|
429
|
+
const checkoutCsp = commerce ? checkoutCspAdditions(commerce.origin) : null;
|
|
413
430
|
response.headers.set(
|
|
414
431
|
cspHeader,
|
|
415
432
|
buildCsp({
|
|
@@ -420,21 +437,21 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
420
437
|
scriptSrc: [
|
|
421
438
|
...STOREFRONT_CSP_ALLOWLIST.scriptSrc,
|
|
422
439
|
...STATIC_SCRIPT_HASHES,
|
|
423
|
-
...(
|
|
440
|
+
...(checkoutCsp?.scriptSrc ?? []),
|
|
424
441
|
],
|
|
425
442
|
styleSrc: [
|
|
426
443
|
...STOREFRONT_CSP_ALLOWLIST.styleSrc,
|
|
427
444
|
...STATIC_STYLE_HASHES,
|
|
428
|
-
...(
|
|
445
|
+
...(checkoutCsp?.styleSrc ?? []),
|
|
429
446
|
],
|
|
430
|
-
fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(
|
|
447
|
+
fontSrc: [...STOREFRONT_CSP_ALLOWLIST.fontSrc, ...(checkoutCsp?.fontSrc ?? [])],
|
|
431
448
|
connectSrc: [
|
|
432
449
|
...STOREFRONT_CSP_ALLOWLIST.connectSrc,
|
|
433
|
-
...(
|
|
450
|
+
...(checkoutCsp?.connectSrc ?? []),
|
|
434
451
|
],
|
|
435
|
-
imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(
|
|
436
|
-
frameSrc:
|
|
437
|
-
formAction:
|
|
452
|
+
imgSrc: [...STOREFRONT_CSP_ALLOWLIST.imgSrc, ...(checkoutCsp?.imgSrc ?? [])],
|
|
453
|
+
frameSrc: checkoutCsp?.frameSrc ?? [],
|
|
454
|
+
formAction: checkoutCsp?.formAction ?? [],
|
|
438
455
|
reportGroup: CSP_REPORT_GROUP,
|
|
439
456
|
reportUri: CSP_REPORT_PATH,
|
|
440
457
|
}),
|
|
@@ -9,7 +9,7 @@ import Layout from "@/layouts/Layout.astro";
|
|
|
9
9
|
import Breadcrumbs from "@/components/Breadcrumbs.astro";
|
|
10
10
|
import { breadcrumbLd } from "@/lib/jsonld";
|
|
11
11
|
import { truncate } from "@/lib/format";
|
|
12
|
-
import { rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
12
|
+
import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
13
13
|
|
|
14
14
|
const { tenant, canonicalBase, content, basePath } = Astro.locals;
|
|
15
15
|
const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
@@ -34,12 +34,11 @@ if (slug === "_style-guide" || slug.startsWith("_style-guide/")) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
// Raw tenant-authored marketing subpage (vendor-injected HTML) — serve the
|
|
37
|
-
// self-contained document VERBATIM
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// headers, CSP, and (prod) the edge cache.
|
|
37
|
+
// self-contained document VERBATIM only when rawHtmlPolicy says this tenant may
|
|
38
|
+
// bypass Layout. Regulated commerce tenants fall through to platform-composed
|
|
39
|
+
// pages so compliance UI cannot be removed by raw HTML.
|
|
41
40
|
const rawHtml = await content.getPageHtml(slug);
|
|
42
|
-
if (rawHtml) {
|
|
41
|
+
if (rawHtml && rawHtmlPolicy(tenant).allowed) {
|
|
43
42
|
// Shared chrome: a body-only fragment gets wrapped in the tenant's chrome.html;
|
|
44
43
|
// a full document serves verbatim (backward-compatible). See rawMarketingHtml.ts.
|
|
45
44
|
const chromeWrapper = await content.getChromeHtml();
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*
|
|
13
13
|
* SHELL CONTRACT — every AdminXTab.astro component mounted below must preserve:
|
|
14
14
|
* - A `<section id="{tab}" data-admin-panel data-agent-scope="{tab}">` root,
|
|
15
|
-
* where {tab} is one of: orders, products, customers, fulfillment,
|
|
15
|
+
* where {tab} is one of: orders, products, subscriptions, customers, fulfillment,
|
|
16
16
|
* reporting, ai-workflows, settings. The shell's hash router shows/hides
|
|
17
17
|
* panels by matching `html[data-admin-tab]` to this id, and the AI Ask
|
|
18
18
|
* envelope reads `data-agent-scope` to label its active context.
|
|
@@ -35,6 +35,7 @@ import AdminFulfillmentTab from "../components/admin/AdminFulfillmentTab.astro";
|
|
|
35
35
|
import AdminAIWorkflowsTab from "../components/admin/AdminAIWorkflowsTab.astro";
|
|
36
36
|
import OrdersWorkspace from "../components/admin/orders/OrdersWorkspace.astro";
|
|
37
37
|
import AdminProductsTab from "../components/admin/AdminProductsTab.astro";
|
|
38
|
+
import AdminSubscriptionsTab from "../components/admin/AdminSubscriptionsTab.astro";
|
|
38
39
|
import AdminReportingTab from "../components/admin/AdminReportingTab.astro";
|
|
39
40
|
import AdminSettingsTab from "../components/admin/AdminSettingsTab.astro";
|
|
40
41
|
import { readViewerSession } from "@/lib/auth/route";
|
|
@@ -79,7 +80,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
79
80
|
<title>{tenantLabel} Admin</title>
|
|
80
81
|
<script is:inline nonce={cspNonce}>
|
|
81
82
|
(() => {
|
|
82
|
-
const adminTabs = ["orders", "products", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
83
|
+
const adminTabs = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
83
84
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
84
85
|
const defaultTab = canViewInternalGuide ? "orders" : "settings";
|
|
85
86
|
const hash = window.location.hash.slice(1);
|
|
@@ -502,6 +503,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
502
503
|
|
|
503
504
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="orders"] #orders,
|
|
504
505
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="products"] #products,
|
|
506
|
+
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="subscriptions"] #subscriptions,
|
|
505
507
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="customers"] #customers,
|
|
506
508
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="fulfillment"] #fulfillment,
|
|
507
509
|
html.admin-tabs-ready[data-admin-mode="work-area"][data-admin-tab="reporting"] #reporting,
|
|
@@ -1126,6 +1128,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1126
1128
|
<nav class="nav">
|
|
1127
1129
|
<a href="#orders" data-admin-tab-link><svg><use href="#i-file"></use></svg><span>Orders</span></a>
|
|
1128
1130
|
<a href="#products" data-admin-tab-link><svg><use href="#i-box"></use></svg><span>Products</span></a>
|
|
1131
|
+
<a href="#subscriptions" data-admin-tab-link><svg><use href="#i-refresh"></use></svg><span>Subscriptions</span></a>
|
|
1129
1132
|
<a href="#customers" data-admin-tab-link><svg><use href="#i-users"></use></svg><span>Customers</span></a>
|
|
1130
1133
|
<a href="#fulfillment" data-admin-tab-link><svg><use href="#i-truck"></use></svg><span>Fulfillment</span></a>
|
|
1131
1134
|
<a href="#reporting" data-admin-tab-link><svg><use href="#i-chart"></use></svg><span>Reporting</span></a>
|
|
@@ -1151,6 +1154,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1151
1154
|
<div class="tabs mobile-tabbar" role="tablist" aria-label="Admin work areas">
|
|
1152
1155
|
<a class="tab" href="#orders" role="tab" aria-controls="orders" aria-selected="false" data-admin-tab-link>Orders</a>
|
|
1153
1156
|
<a class="tab" href="#products" role="tab" aria-controls="products" aria-selected="false" data-admin-tab-link>Products</a>
|
|
1157
|
+
<a class="tab" href="#subscriptions" role="tab" aria-controls="subscriptions" aria-selected="false" data-admin-tab-link>Subscriptions</a>
|
|
1154
1158
|
<a class="tab" href="#customers" role="tab" aria-controls="customers" aria-selected="false" data-admin-tab-link>Customers</a>
|
|
1155
1159
|
<a class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1156
1160
|
<a class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
@@ -1197,6 +1201,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1197
1201
|
<div class="tabs admin-tabs" role="tablist" aria-label="Admin work areas">
|
|
1198
1202
|
<a id="admin-tab-orders" class="tab active" href="#orders" role="tab" aria-controls="orders" aria-selected="true" data-admin-tab-link>Orders</a>
|
|
1199
1203
|
<a id="admin-tab-products" class="tab" href="#products" role="tab" aria-controls="products" aria-selected="false" data-admin-tab-link>Products</a>
|
|
1204
|
+
<a id="admin-tab-subscriptions" class="tab" href="#subscriptions" role="tab" aria-controls="subscriptions" aria-selected="false" data-admin-tab-link>Subscriptions</a>
|
|
1200
1205
|
<a id="admin-tab-customers" class="tab" href="#customers" role="tab" aria-controls="customers" aria-selected="false" data-admin-tab-link>Customers</a>
|
|
1201
1206
|
<a id="admin-tab-fulfillment" class="tab" href="#fulfillment" role="tab" aria-controls="fulfillment" aria-selected="false" data-admin-tab-link>Fulfillment</a>
|
|
1202
1207
|
<a id="admin-tab-reporting" class="tab" href="#reporting" role="tab" aria-controls="reporting" aria-selected="false" data-admin-tab-link>Reporting</a>
|
|
@@ -1321,6 +1326,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1321
1326
|
|
|
1322
1327
|
<OrdersWorkspace envelope={tenantEnvelope} />
|
|
1323
1328
|
<AdminProductsTab envelope={tenantEnvelope} />
|
|
1329
|
+
<AdminSubscriptionsTab envelope={tenantEnvelope} />
|
|
1324
1330
|
<AdminCustomersTab envelope={tenantEnvelope} />
|
|
1325
1331
|
<AdminFulfillmentTab envelope={tenantEnvelope} />
|
|
1326
1332
|
<AdminReportingTab envelope={tenantEnvelope} />
|
|
@@ -1441,7 +1447,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1441
1447
|
|
|
1442
1448
|
<script is:inline nonce={cspNonce}>
|
|
1443
1449
|
(() => {
|
|
1444
|
-
const tabIds = ["orders", "products", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
1450
|
+
const tabIds = ["orders", "products", "subscriptions", "customers", "fulfillment", "reporting", "ai-workflows", "settings"];
|
|
1445
1451
|
const tabSet = new Set(tabIds);
|
|
1446
1452
|
const tabHashPattern = new RegExp(`^#(${tabIds.join("|")})$`);
|
|
1447
1453
|
const canViewInternalGuide = document.documentElement.dataset.internalAdminGuide === "true";
|
|
@@ -1449,6 +1455,7 @@ const { envelope: tenantEnvelope, isStaff, pickerTenants, deniedTarget } = admin
|
|
|
1449
1455
|
const askContexts = {
|
|
1450
1456
|
orders: "Orders",
|
|
1451
1457
|
products: "Products",
|
|
1458
|
+
subscriptions: "Subscriptions",
|
|
1452
1459
|
customers: "Customers",
|
|
1453
1460
|
fulfillment: "Fulfillment",
|
|
1454
1461
|
reporting: "Reporting",
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `GET /api/admin/orders/:id` — one order's full detail for the Orders tab (U2).
|
|
3
3
|
*
|
|
4
|
-
* Server-side proxy to
|
|
5
|
-
* resolved tenant + env. Bearer stays server-side; response is the
|
|
4
|
+
* Server-side proxy to checkout `GET /commerce/orders/:orderId` for the
|
|
5
|
+
* resolved tenant + env. Bearer stays server-side; response is the CheckoutOrderDetail
|
|
6
6
|
* DTO. `verificationDetail` is empty until U4 fills it — consumers tolerate that.
|
|
7
7
|
* Read-only (scope-locks).
|
|
8
8
|
*/
|
|
9
9
|
import type { APIContext } from "astro";
|
|
10
10
|
import { readEnv } from "@/lib/env";
|
|
11
11
|
import {
|
|
12
|
-
|
|
12
|
+
createCheckoutOrderClient,
|
|
13
13
|
orderApiBaseUrl,
|
|
14
14
|
OrderApiError,
|
|
15
15
|
} from "@/lib/admin/orderClient";
|
|
@@ -37,7 +37,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
37
37
|
const bearer = await readEnv("FOXY_ORDER_API_KEY");
|
|
38
38
|
if (!bearer) return jsonError("not_provisioned", 503);
|
|
39
39
|
|
|
40
|
-
const client =
|
|
40
|
+
const client = createCheckoutOrderClient({
|
|
41
41
|
baseUrl: orderApiBaseUrl(customizationEnv),
|
|
42
42
|
appDomain: tenant.appDomain,
|
|
43
43
|
bearer,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `GET /api/admin/orders` — internal admin route backing the Orders tab (U2).
|
|
3
3
|
*
|
|
4
|
-
* Server-side proxy to
|
|
4
|
+
* Server-side proxy to the checkout order READ API for the resolved tenant +
|
|
5
5
|
* environment. The per-tenant bearer key stays on the Worker (never in the
|
|
6
6
|
* response); the browser only ever sees order DTO JSON. Read-only (scope-locks).
|
|
7
7
|
* U3 consumes this and maps the DTO → OrdersViewModel.
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
* host/approver layer) — same posture as `api/evidence.json.ts`; no new gate here.
|
|
11
11
|
*/
|
|
12
12
|
import type { APIContext } from "astro";
|
|
13
|
-
import type {
|
|
13
|
+
import type { CheckoutOrderListFilters } from "@tot/public-runtime";
|
|
14
14
|
import { readEnv } from "@/lib/env";
|
|
15
15
|
import {
|
|
16
|
-
|
|
16
|
+
createCheckoutOrderClient,
|
|
17
17
|
orderApiBaseUrl,
|
|
18
18
|
OrderApiError,
|
|
19
19
|
} from "@/lib/admin/orderClient";
|
|
@@ -46,7 +46,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
46
46
|
const url = new URL(context.request.url);
|
|
47
47
|
const q = url.searchParams;
|
|
48
48
|
const limitRaw = q.get("limit");
|
|
49
|
-
const filters:
|
|
49
|
+
const filters: CheckoutOrderListFilters = {
|
|
50
50
|
queue: q.get("queue") ?? undefined,
|
|
51
51
|
status: q.get("status") ?? undefined,
|
|
52
52
|
from: q.get("from") ?? undefined,
|
|
@@ -55,7 +55,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
55
55
|
cursor: q.get("cursor") ?? undefined,
|
|
56
56
|
};
|
|
57
57
|
|
|
58
|
-
const client =
|
|
58
|
+
const client = createCheckoutOrderClient({
|
|
59
59
|
baseUrl: orderApiBaseUrl(customizationEnv),
|
|
60
60
|
appDomain: tenant.appDomain,
|
|
61
61
|
bearer,
|
|
@@ -24,16 +24,19 @@ import {
|
|
|
24
24
|
sessionFromFederatedClaims,
|
|
25
25
|
} from "@/lib/auth/identityToken";
|
|
26
26
|
import { createSession, DEFAULT_SESSION_TTL_SECONDS } from "@/lib/auth/session";
|
|
27
|
+
import { normalizeEmailHint } from "@/lib/privacy/emailHint";
|
|
27
28
|
|
|
28
29
|
export const prerender = false;
|
|
29
30
|
|
|
30
31
|
/** Back to the landing page carrying an error (no token in the URL on failure). */
|
|
31
32
|
function magicRedirect(
|
|
32
33
|
context: APIContext,
|
|
33
|
-
params: { error?: string; next?: string },
|
|
34
|
+
params: { error?: string; next?: string; tenant?: string; emailHint?: string },
|
|
34
35
|
): Response {
|
|
35
36
|
const q = new URLSearchParams();
|
|
36
37
|
if (params.error) q.set("error", params.error);
|
|
38
|
+
if (params.tenant) q.set("tenant", params.tenant);
|
|
39
|
+
if (params.emailHint) q.set("email_hint", params.emailHint);
|
|
37
40
|
if (params.next && params.next !== "/dashboard") q.set("next", params.next);
|
|
38
41
|
const qs = q.toString();
|
|
39
42
|
return context.redirect(`/auth/magic${qs ? `?${qs}` : ""}`, 303);
|
|
@@ -42,19 +45,23 @@ function magicRedirect(
|
|
|
42
45
|
export async function POST(context: APIContext): Promise<Response> {
|
|
43
46
|
let token = "";
|
|
44
47
|
let next = "/dashboard";
|
|
48
|
+
let tenant = "";
|
|
49
|
+
let emailHint = "";
|
|
45
50
|
try {
|
|
46
51
|
const form = await context.request.formData();
|
|
47
52
|
token = String(form.get("token") ?? "").trim();
|
|
53
|
+
tenant = String(form.get("tenant") ?? "").trim().toLowerCase();
|
|
54
|
+
emailHint = normalizeEmailHint(String(form.get("email_hint") ?? "")) ?? "";
|
|
48
55
|
next = safeNext(String(form.get("next") ?? "/dashboard"));
|
|
49
56
|
} catch {
|
|
50
57
|
return magicRedirect(context, { error: "invalid", next });
|
|
51
58
|
}
|
|
52
|
-
if (!token) return magicRedirect(context, { error: "missing", next });
|
|
59
|
+
if (!token) return magicRedirect(context, { error: "missing", tenant, emailHint, next });
|
|
53
60
|
|
|
54
61
|
const client = await getAccessClient();
|
|
55
62
|
const store = await getSessionStore();
|
|
56
63
|
if (!client || !store) {
|
|
57
|
-
return magicRedirect(context, { error: "unconfigured", next });
|
|
64
|
+
return magicRedirect(context, { error: "unconfigured", tenant, emailHint, next });
|
|
58
65
|
}
|
|
59
66
|
|
|
60
67
|
// Consume the token (single-use; burned server-side by tot20).
|
|
@@ -62,19 +69,19 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
62
69
|
if (!result.ok) {
|
|
63
70
|
// reason ∈ invalid|expired|error (tot20's "used" normalizes to invalid).
|
|
64
71
|
const error = result.reason === "expired" ? "expired" : "invalid";
|
|
65
|
-
return magicRedirect(context, { error, next });
|
|
72
|
+
return magicRedirect(context, { error, tenant, emailHint, next });
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
// Prove authorization offline — fail closed with no flat-JSON fallback.
|
|
69
76
|
const idToken = result.id_token;
|
|
70
77
|
if (!idToken) {
|
|
71
78
|
console.error("auth/magic-exchange: no id_token on exchange; failing closed");
|
|
72
|
-
return magicRedirect(context, { error: "verify_failed", next });
|
|
79
|
+
return magicRedirect(context, { error: "verify_failed", tenant, emailHint, next });
|
|
73
80
|
}
|
|
74
81
|
const verifier = await getFederatedVerifier();
|
|
75
82
|
if (!verifier) {
|
|
76
83
|
console.error("auth/magic-exchange: federated verifier unconfigured");
|
|
77
|
-
return magicRedirect(context, { error: "unconfigured", next });
|
|
84
|
+
return magicRedirect(context, { error: "unconfigured", tenant, emailHint, next });
|
|
78
85
|
}
|
|
79
86
|
|
|
80
87
|
let derived;
|
|
@@ -85,7 +92,7 @@ export async function POST(context: APIContext): Promise<Response> {
|
|
|
85
92
|
console.error("auth/magic-exchange: id_token verification failed; failing closed", {
|
|
86
93
|
error: err instanceof Error ? err.message : String(err),
|
|
87
94
|
});
|
|
88
|
-
return magicRedirect(context, { error: "verify_failed", next });
|
|
95
|
+
return magicRedirect(context, { error: "verify_failed", tenant, emailHint, next });
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `GET /api/checkout-style.css` — the stable, per-tenant storefront URL that hosts
|
|
3
3
|
* the published checkout styling as `text/css`. This URL is what U11's publisher
|
|
4
|
-
* points
|
|
5
|
-
*
|
|
4
|
+
* points hosted checkout's `content_url` at and what checkout fetches server-side
|
|
5
|
+
* when it (re)caches the checkout template.
|
|
6
6
|
*
|
|
7
|
-
* The tenant + environment are self-identifying from the request Host (
|
|
7
|
+
* The tenant + environment are self-identifying from the request Host (checkout's
|
|
8
8
|
* fetch carries no session): the middleware resolves `locals.tenant` +
|
|
9
9
|
* `locals.customizationEnv`, and we serve exactly that tenant's published style
|
|
10
10
|
* for that env (live on prod hosts, test behind a preview token). Falls back to
|
|
11
11
|
* the neutral DEFAULT_CHECKOUT_STYLE tokens when nothing is published, so the URL
|
|
12
12
|
* is always a valid, non-empty stylesheet.
|
|
13
13
|
*
|
|
14
|
-
* ASCII-safety (
|
|
14
|
+
* ASCII-safety (checkout cache is not Unicode-safe) is enforced inside
|
|
15
15
|
* buildCheckoutStyleCss. No secrets, no cross-service call — read-only from KV.
|
|
16
16
|
*/
|
|
17
17
|
import type { APIContext } from "astro";
|
|
@@ -34,7 +34,7 @@ export async function GET(context: APIContext): Promise<Response> {
|
|
|
34
34
|
return new Response(css, {
|
|
35
35
|
headers: {
|
|
36
36
|
"content-type": "text/css; charset=utf-8",
|
|
37
|
-
//
|
|
37
|
+
// Checkout caches on its side via the theme refresh; keep the origin fresh so
|
|
38
38
|
// a re-cache always pulls the latest published styling.
|
|
39
39
|
"cache-control": "no-store",
|
|
40
40
|
"x-robots-tag": "noindex, nofollow",
|
|
@@ -10,12 +10,15 @@
|
|
|
10
10
|
* (tot20 sends/verifies the code, the storefront mints the session). No secrets
|
|
11
11
|
* reach the browser. Inline style carries the per-request CSP nonce.
|
|
12
12
|
*/
|
|
13
|
+
import { normalizeEmailHint } from "@/lib/privacy/emailHint";
|
|
14
|
+
|
|
13
15
|
export const prerender = false;
|
|
14
16
|
|
|
15
17
|
const cspNonce = Astro.locals.cspNonce;
|
|
16
18
|
const url = new URL(Astro.request.url);
|
|
17
19
|
const sent = url.searchParams.get("sent") === "1";
|
|
18
20
|
const email = (url.searchParams.get("email") ?? "").trim();
|
|
21
|
+
const emailHint = normalizeEmailHint(url.searchParams.get("email_hint")) ?? "";
|
|
19
22
|
const errorCode = url.searchParams.get("error");
|
|
20
23
|
const next = (() => {
|
|
21
24
|
const n = url.searchParams.get("next") ?? "/";
|
|
@@ -132,7 +135,7 @@ if (devMode) {
|
|
|
132
135
|
) : (
|
|
133
136
|
<>
|
|
134
137
|
<h1>Sign in</h1>
|
|
135
|
-
<p class="sub"><span class="who">Token of Trust© Regulated Storefront</span> is in early access. Enter your email — if you have access, we'll send a one-time code
|
|
138
|
+
<p class="sub"><span class="who">Token of Trust© Regulated Storefront</span> is in early access. Enter your email — if you have access, we'll send a one-time code.{emailHint ? <> Use the invite email hinted as <span class="who">{emailHint}</span>.</> : null}</p>
|
|
136
139
|
<form method="post" action="/api/auth/send">
|
|
137
140
|
<input type="hidden" name="next" value={next} />
|
|
138
141
|
<label for="email">Work email</label>
|