@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
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The storefront (cockpit) half of the browserless CLI sign-in RENDEZVOUS — the
|
|
3
|
+
* server-to-server calls into the MCP's device-rendezvous endpoints, authenticated
|
|
4
|
+
* by the storefront's SIGNED CLIENT ASSERTION (private_key_jwt, RFC 7523), NOT the
|
|
5
|
+
* retired `x-mcp-service-secret`. The developer's cockpit (already authenticated as
|
|
6
|
+
* that developer) drives these; the MCP verifies the assertion against the
|
|
7
|
+
* storefront's published JWKS and admits only a client registered to broker CLI
|
|
8
|
+
* sign-in.
|
|
9
|
+
*
|
|
10
|
+
* createRendezvous → POST /oauth/device/rendezvous (start; returns the
|
|
11
|
+
* NON-SECRET rendezvous_code rendered into `tot login --code`)
|
|
12
|
+
* rendezvousStatus → POST /oauth/device/rendezvous/status (poll for a terminal
|
|
13
|
+
* attaching → { state, fingerprint })
|
|
14
|
+
* approveRendezvous → POST /oauth/device/rendezvous/approve (the human confirmed
|
|
15
|
+
* the fingerprint; carries the dev's fresh subject_token proof)
|
|
16
|
+
* denyRendezvous → POST /oauth/device/rendezvous/deny
|
|
17
|
+
*
|
|
18
|
+
* All four endpoints verify the SAME assertion audience — `<mcpBase>/oauth/device/rendezvous`
|
|
19
|
+
* (the MCP derives the expected audience from that fixed path, not the sub-path), so
|
|
20
|
+
* one minted assertion shape covers every call. SERVER-SIDE ONLY.
|
|
21
|
+
*/
|
|
22
|
+
import { readEnv } from "@/lib/env";
|
|
23
|
+
import { mintMcpClientAssertion, McpClientAssertionError } from "@/lib/auth/mcpClientAssertion";
|
|
24
|
+
|
|
25
|
+
const JWT_BEARER_ASSERTION_TYPE = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer";
|
|
26
|
+
/** The path whose audience the MCP binds EVERY rendezvous assertion to. */
|
|
27
|
+
const RENDEZVOUS_AUDIENCE_PATH = "/oauth/device/rendezvous";
|
|
28
|
+
|
|
29
|
+
/** Injectable transport so the broker calls unit-test with no network. */
|
|
30
|
+
export interface HttpTransport {
|
|
31
|
+
post(
|
|
32
|
+
url: string,
|
|
33
|
+
headers: Record<string, string>,
|
|
34
|
+
body: unknown,
|
|
35
|
+
): Promise<{ status: number; json(): Promise<unknown> }>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Bound every rendezvous call to the MCP. The MCP runs on AppRunner (scale-to-zero),
|
|
39
|
+
// so the FIRST call after idle hits a cold instance that can take many seconds to
|
|
40
|
+
// answer. Without a bound the storefront Worker's subrequest hangs and the browser's
|
|
41
|
+
// POST to /api/dev/cli-signin-code eventually dies at the transport layer — the cockpit
|
|
42
|
+
// shows the opaque catch-branch "Couldn't reach the sign-in service" and the button is
|
|
43
|
+
// simply dead. Aborting at a fixed deadline turns that silent hang into a fast, RETRYABLE
|
|
44
|
+
// 502 ("having a moment — try again"); the retry lands on a now-warm MCP and succeeds.
|
|
45
|
+
const RENDEZVOUS_FETCH_TIMEOUT_MS = 12_000;
|
|
46
|
+
|
|
47
|
+
export class FetchHttpTransport implements HttpTransport {
|
|
48
|
+
async post(url: string, headers: Record<string, string>, body: unknown) {
|
|
49
|
+
const res = await fetch(url, {
|
|
50
|
+
method: "POST",
|
|
51
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
// A timeout/abort rejects the fetch → callRendezvous's catch → a 502 the cockpit
|
|
54
|
+
// renders as a retryable error, instead of a hung request the browser gives up on.
|
|
55
|
+
signal: AbortSignal.timeout(RENDEZVOUS_FETCH_TIMEOUT_MS),
|
|
56
|
+
});
|
|
57
|
+
return { status: res.status, json: () => res.json() };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface RendezvousDeps {
|
|
62
|
+
transport?: HttpTransport;
|
|
63
|
+
/** Override the assertion minter in tests (defaults to the real private-key signer). */
|
|
64
|
+
mintAssertion?: (audience: string) => Promise<string>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A resolved MCP target: the origin to POST to + the assertion audience. */
|
|
68
|
+
interface McpTarget {
|
|
69
|
+
origin: string;
|
|
70
|
+
audience: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the MCP origin from `MCP_BASE_URL` (its ORIGIN). MCP_BASE_URL is the MCP
|
|
75
|
+
* this storefront authenticates to with its private_key_jwt client assertion (kid
|
|
76
|
+
* storefront-mcp-client-1) — the SAME credential + tier the confidential broker
|
|
77
|
+
* client (canBrokerCliSignin) is registered under (e.g. mcp.qa for preview). NOT
|
|
78
|
+
* MCP_APP_BINDING_URL, which the retired shared-secret shim used and which may point
|
|
79
|
+
* at a different MCP tier. Returns null when unset/malformed (route maps that to 503).
|
|
80
|
+
*/
|
|
81
|
+
export async function resolveMcpTarget(): Promise<McpTarget | null> {
|
|
82
|
+
const baseUrl = await readEnv("MCP_BASE_URL");
|
|
83
|
+
if (!baseUrl) return null;
|
|
84
|
+
try {
|
|
85
|
+
const origin = new URL(baseUrl).origin;
|
|
86
|
+
return { origin, audience: `${origin}${RENDEZVOUS_AUDIENCE_PATH}` };
|
|
87
|
+
} catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function callRendezvous(
|
|
93
|
+
target: McpTarget,
|
|
94
|
+
path: string,
|
|
95
|
+
extra: Record<string, unknown>,
|
|
96
|
+
deps: RendezvousDeps,
|
|
97
|
+
): Promise<{ status: number; body: Record<string, unknown> }> {
|
|
98
|
+
const mint = deps.mintAssertion ?? ((aud: string) => mintMcpClientAssertion(aud));
|
|
99
|
+
const clientAssertion = await mint(target.audience);
|
|
100
|
+
const transport = deps.transport ?? new FetchHttpTransport();
|
|
101
|
+
const res = await transport.post(
|
|
102
|
+
`${target.origin}${path}`,
|
|
103
|
+
{},
|
|
104
|
+
{
|
|
105
|
+
client_assertion: clientAssertion,
|
|
106
|
+
client_assertion_type: JWT_BEARER_ASSERTION_TYPE,
|
|
107
|
+
...extra,
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
let body: Record<string, unknown> = {};
|
|
111
|
+
try {
|
|
112
|
+
body = (await res.json()) as Record<string, unknown>;
|
|
113
|
+
} catch {
|
|
114
|
+
body = {};
|
|
115
|
+
}
|
|
116
|
+
return { status: res.status, body };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export type RendezvousCreateResult =
|
|
120
|
+
| { ok: true; rendezvousCode: string; expiresIn: number | null }
|
|
121
|
+
| { ok: false; status: 502 | 503; reason: string };
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Start a rendezvous for the signed-in developer's OWN email (+ their vendor tenant).
|
|
125
|
+
* Fail-closed mapping mirrors the retired mint: an unconfigured seam or a 401/403
|
|
126
|
+
* (assertion rejected / client not entitled to broker) is a 503 ("not wired up on
|
|
127
|
+
* this environment"); any other fault is a 502.
|
|
128
|
+
*/
|
|
129
|
+
export async function createRendezvous(
|
|
130
|
+
email: string,
|
|
131
|
+
tenant: string | null,
|
|
132
|
+
deps: RendezvousDeps = {},
|
|
133
|
+
): Promise<RendezvousCreateResult> {
|
|
134
|
+
const target = await resolveMcpTarget();
|
|
135
|
+
if (!target) return { ok: false, status: 503, reason: "not-configured" };
|
|
136
|
+
let res: { status: number; body: Record<string, unknown> };
|
|
137
|
+
try {
|
|
138
|
+
res = await callRendezvous(
|
|
139
|
+
target,
|
|
140
|
+
RENDEZVOUS_AUDIENCE_PATH,
|
|
141
|
+
{ email: email.toLowerCase(), ...(tenant ? { tenant: tenant.toLowerCase() } : {}) },
|
|
142
|
+
deps,
|
|
143
|
+
);
|
|
144
|
+
} catch (cause) {
|
|
145
|
+
if (cause instanceof McpClientAssertionError) return { ok: false, status: 503, reason: "assertion-unconfigured" };
|
|
146
|
+
return { ok: false, status: 502, reason: "mcp request failed: " + (cause instanceof Error ? cause.message : String(cause)) };
|
|
147
|
+
}
|
|
148
|
+
if (res.status < 200 || res.status >= 300) {
|
|
149
|
+
// 401 invalid_client / 403 unauthorized_client|not_entitled = "not wired up /
|
|
150
|
+
// not entitled on this environment" → 503; a fault is 502.
|
|
151
|
+
const notConfigured = res.status === 401 || res.status === 403;
|
|
152
|
+
return { ok: false, status: notConfigured ? 503 : 502, reason: `rendezvous create returned ${res.status}` };
|
|
153
|
+
}
|
|
154
|
+
const rendezvousCode = typeof res.body.rendezvous_code === "string" ? res.body.rendezvous_code : "";
|
|
155
|
+
if (!rendezvousCode) return { ok: false, status: 502, reason: "rendezvous create returned no code" };
|
|
156
|
+
return {
|
|
157
|
+
ok: true,
|
|
158
|
+
rendezvousCode,
|
|
159
|
+
expiresIn: typeof res.body.expires_in === "number" ? res.body.expires_in : null,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export type RendezvousState = "pending" | "attached" | "approved" | "denied" | "expired";
|
|
164
|
+
export type RendezvousStatusResult =
|
|
165
|
+
| { ok: true; state: RendezvousState; fingerprint: string | null }
|
|
166
|
+
| { ok: false; status: 502 | 503; reason: string };
|
|
167
|
+
|
|
168
|
+
/** Poll for a terminal attaching. `fingerprint` is present once state is `attached`. */
|
|
169
|
+
export async function rendezvousStatus(
|
|
170
|
+
rendezvousCode: string,
|
|
171
|
+
deps: RendezvousDeps = {},
|
|
172
|
+
): Promise<RendezvousStatusResult> {
|
|
173
|
+
const target = await resolveMcpTarget();
|
|
174
|
+
if (!target) return { ok: false, status: 503, reason: "not-configured" };
|
|
175
|
+
let res: { status: number; body: Record<string, unknown> };
|
|
176
|
+
try {
|
|
177
|
+
res = await callRendezvous(target, `${RENDEZVOUS_AUDIENCE_PATH}/status`, { rendezvous_code: rendezvousCode }, deps);
|
|
178
|
+
} catch (cause) {
|
|
179
|
+
if (cause instanceof McpClientAssertionError) return { ok: false, status: 503, reason: "assertion-unconfigured" };
|
|
180
|
+
return { ok: false, status: 502, reason: "mcp request failed" };
|
|
181
|
+
}
|
|
182
|
+
if (res.status < 200 || res.status >= 300) {
|
|
183
|
+
const notConfigured = res.status === 401 || res.status === 403;
|
|
184
|
+
return { ok: false, status: notConfigured ? 503 : 502, reason: `rendezvous status returned ${res.status}` };
|
|
185
|
+
}
|
|
186
|
+
const state = String(res.body.state ?? "expired") as RendezvousState;
|
|
187
|
+
return { ok: true, state, fingerprint: typeof res.body.fingerprint === "string" ? res.body.fingerprint : null };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export type RendezvousDecisionResult =
|
|
191
|
+
| { ok: true }
|
|
192
|
+
| { ok: false; status: 403 | 409 | 502 | 503; reason: string };
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Approve the attached terminal. Carries the developer's fresh `subject_token`
|
|
196
|
+
* (Part A / Finding 3) so the MCP can bind the approval to the dev's own identity —
|
|
197
|
+
* a 403 `approver_mismatch` means the proof didn't match the rendezvous developer;
|
|
198
|
+
* a 409 `not_approvable` means the fingerprint didn't match a waiting terminal.
|
|
199
|
+
*/
|
|
200
|
+
export async function approveRendezvous(
|
|
201
|
+
rendezvousCode: string,
|
|
202
|
+
fingerprint: string,
|
|
203
|
+
subjectToken: string,
|
|
204
|
+
deps: RendezvousDeps = {},
|
|
205
|
+
): Promise<RendezvousDecisionResult> {
|
|
206
|
+
const target = await resolveMcpTarget();
|
|
207
|
+
if (!target) return { ok: false, status: 503, reason: "not-configured" };
|
|
208
|
+
let res: { status: number; body: Record<string, unknown> };
|
|
209
|
+
try {
|
|
210
|
+
res = await callRendezvous(
|
|
211
|
+
target,
|
|
212
|
+
`${RENDEZVOUS_AUDIENCE_PATH}/approve`,
|
|
213
|
+
{ rendezvous_code: rendezvousCode, fingerprint, subject_token: subjectToken },
|
|
214
|
+
deps,
|
|
215
|
+
);
|
|
216
|
+
} catch (cause) {
|
|
217
|
+
if (cause instanceof McpClientAssertionError) return { ok: false, status: 503, reason: "assertion-unconfigured" };
|
|
218
|
+
return { ok: false, status: 502, reason: "mcp request failed" };
|
|
219
|
+
}
|
|
220
|
+
if (res.status >= 200 && res.status < 300) return { ok: true };
|
|
221
|
+
if (res.status === 403) return { ok: false, status: 403, reason: String(res.body.error ?? "approver_mismatch") };
|
|
222
|
+
if (res.status === 409) return { ok: false, status: 409, reason: String(res.body.error ?? "not_approvable") };
|
|
223
|
+
const notConfigured = res.status === 401;
|
|
224
|
+
return { ok: false, status: notConfigured ? 503 : 502, reason: `rendezvous approve returned ${res.status}` };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Reject the attached terminal (the human said "not me"). */
|
|
228
|
+
export async function denyRendezvous(
|
|
229
|
+
rendezvousCode: string,
|
|
230
|
+
deps: RendezvousDeps = {},
|
|
231
|
+
): Promise<RendezvousDecisionResult> {
|
|
232
|
+
const target = await resolveMcpTarget();
|
|
233
|
+
if (!target) return { ok: false, status: 503, reason: "not-configured" };
|
|
234
|
+
try {
|
|
235
|
+
const res = await callRendezvous(target, `${RENDEZVOUS_AUDIENCE_PATH}/deny`, { rendezvous_code: rendezvousCode }, deps);
|
|
236
|
+
if (res.status >= 200 && res.status < 300) return { ok: true };
|
|
237
|
+
return { ok: false, status: res.status === 401 ? 503 : 502, reason: `rendezvous deny returned ${res.status}` };
|
|
238
|
+
} catch {
|
|
239
|
+
return { ok: false, status: 502, reason: "mcp request failed" };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-issue a FRESH tot20-signed id_token for an already-signed-in developer — the
|
|
3
|
+
* `subject_token` proof the cockpit sends when APPROVING a browserless CLI sign-in
|
|
4
|
+
* rendezvous (Part A / Finding 3).
|
|
5
|
+
*
|
|
6
|
+
* WHY re-issue: the storefront verifies tot20's id_token OFFLINE at sign-in and then
|
|
7
|
+
* DISCARDS it (only derived claims live in the KV session), and it's short-lived —
|
|
8
|
+
* long expired by the time the developer approves. So at approve time we MINT a fresh
|
|
9
|
+
* single-use sign-in token for the session's VERIFIED email and exchange it for a
|
|
10
|
+
* freshly-signed id_token.
|
|
11
|
+
*
|
|
12
|
+
* WHY it closes Finding 3: the mint + exchange authenticate with the storefront's tot20
|
|
13
|
+
* CORE machine credential (TOT_API_KEY/TOT_SECRET_KEY) — a DIFFERENT secret from the MCP
|
|
14
|
+
* broker key. A compromised broker key alone cannot produce this proof.
|
|
15
|
+
*
|
|
16
|
+
* WHY mint, NOT developerInvites: tot20's access-code `/mint` is NOT operator-grant-gated
|
|
17
|
+
* per tenant, whereas developerInvites is (it 403s `notGranted` for a non-staff developer
|
|
18
|
+
* re-attesting themselves on a tenant the storefront doesn't operate). `/mint` binds the
|
|
19
|
+
* token to (email, aud = the storefront's OWN appDomain) — which equals the storefront's
|
|
20
|
+
* MCP client_id, exactly what the MCP binds the subject_token audience to. This is the
|
|
21
|
+
* same non-gated mint the MCP itself uses to power rendezvous creation.
|
|
22
|
+
*/
|
|
23
|
+
import { getAccessClient } from "@/lib/auth/route";
|
|
24
|
+
import type { TotAccessClient } from "@/lib/auth/totAccessClient";
|
|
25
|
+
|
|
26
|
+
/** Short — the proof is minted and used immediately at approve time. */
|
|
27
|
+
const SUBJECT_TOKEN_TTL_SECONDS = 120;
|
|
28
|
+
|
|
29
|
+
export type SubjectReissueResult =
|
|
30
|
+
| { ok: true; idToken: string }
|
|
31
|
+
| { ok: false; reason: string };
|
|
32
|
+
|
|
33
|
+
export interface SubjectReissueDeps {
|
|
34
|
+
accessClient?: TotAccessClient | null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Produce a fresh subject id_token for `email`. Fail-closed: any gap (unconfigured seam,
|
|
39
|
+
* mint refused, exchange failed / step-up required) returns a typed `{ ok:false, reason }`
|
|
40
|
+
* and NEVER throws — the caller maps that to a refusal to approve. The id_token is returned
|
|
41
|
+
* only to the server; it is never logged.
|
|
42
|
+
*/
|
|
43
|
+
export async function reissueSubjectToken(
|
|
44
|
+
email: string,
|
|
45
|
+
deps: SubjectReissueDeps = {},
|
|
46
|
+
): Promise<SubjectReissueResult> {
|
|
47
|
+
if (!email) return { ok: false, reason: "missing-identity" };
|
|
48
|
+
|
|
49
|
+
const accessClient =
|
|
50
|
+
deps.accessClient !== undefined ? deps.accessClient : await getAccessClient();
|
|
51
|
+
if (!accessClient) return { ok: false, reason: "access-client-unconfigured" };
|
|
52
|
+
|
|
53
|
+
// Mint a fresh single-use sign-in token for the developer's OWN email (bound to the
|
|
54
|
+
// storefront's appDomain as audience — not grant-gated per tenant).
|
|
55
|
+
const minted = await accessClient.mintSignInToken(email, SUBJECT_TOKEN_TTL_SECONDS);
|
|
56
|
+
if (!minted.ok || !minted.token) {
|
|
57
|
+
return { ok: false, reason: minted.ok ? "no-signin-token" : `mint-${minted.reason}` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Exchange (single-use; tot20 burns it) → a signed federated id_token whose aud is the
|
|
61
|
+
// storefront appDomain and whose email is this developer.
|
|
62
|
+
const exchanged = await accessClient.exchangeSignInToken(minted.token);
|
|
63
|
+
if (!exchanged.ok || !exchanged.id_token) {
|
|
64
|
+
return { ok: false, reason: exchanged.ok ? "no-id-token" : `exchange-${exchanged.reason}` };
|
|
65
|
+
}
|
|
66
|
+
return { ok: true, idToken: exchanged.id_token };
|
|
67
|
+
}
|
|
@@ -40,20 +40,20 @@ export function renderMagicLinkInviteEmail(input: MagicLinkInviteEmailInput): {
|
|
|
40
40
|
const html = `<!doctype html>
|
|
41
41
|
<html lang="en">
|
|
42
42
|
<head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
|
|
43
|
-
<body style="margin:0;padding:0;background:#
|
|
44
|
-
<div style="max-width:520px;margin:0 auto;padding:32px 20px;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#
|
|
45
|
-
<div style="font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em;text-transform:uppercase;color:#
|
|
43
|
+
<body style="margin:0;padding:0;background:#f7f9fc;">
|
|
44
|
+
<div style="max-width:520px;margin:0 auto;padding:32px 20px;font:16px/1.6 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Helvetica,Arial,sans-serif;color:#33415b;">
|
|
45
|
+
<div style="font:600 12px/1 ui-monospace,SFMono-Regular,Menlo,monospace;letter-spacing:.12em;text-transform:uppercase;color:#0b7a6d;margin:0 0 24px;">
|
|
46
46
|
Token of Trust© · ${esc(store)}
|
|
47
47
|
</div>
|
|
48
|
-
<h1 style="font-size:22px;line-height:1.3;letter-spacing:-.
|
|
49
|
-
<p style="margin:0 0 8px;color:#
|
|
50
|
-
${note ? `<div style="margin:16px 0;padding:12px 14px;background:#
|
|
48
|
+
<h1 style="font-size:22px;line-height:1.3;letter-spacing:-.02em;margin:0 0 12px;color:#0a2540;">You've been invited</h1>
|
|
49
|
+
<p style="margin:0 0 8px;color:#33415b;">You've been given developer access to <strong>${esc(store)}</strong>. Click below to sign in — no password needed.</p>
|
|
50
|
+
${note ? `<div style="margin:16px 0;padding:12px 14px;background:#e7f6f4;border:1px solid #b8e6df;border-radius:10px;color:#0a2540;font-size:14px;"><strong>Note from your inviter:</strong><br>${note}</div>` : ""}
|
|
51
51
|
<div style="margin:28px 0;">
|
|
52
|
-
<a href="${url}" style="display:inline-block;background:#
|
|
52
|
+
<a href="${url}" style="display:inline-block;background:#0b7a6d;color:#ffffff;text-decoration:none;font-weight:600;font-size:16px;padding:14px 28px;border-radius:999px;">Sign in to ${esc(store)}</a>
|
|
53
53
|
</div>
|
|
54
|
-
<p style="margin:0 0 4px;font-size:13px;color:#
|
|
55
|
-
<p style="margin:0 0 24px;font-size:13px;word-break:break-all;"><a href="${url}" style="color:#
|
|
56
|
-
<p style="margin:0;font-size:12px;color:#
|
|
54
|
+
<p style="margin:0 0 4px;font-size:13px;color:#55637a;">Or paste this link into your browser:</p>
|
|
55
|
+
<p style="margin:0 0 24px;font-size:13px;word-break:break-all;"><a href="${url}" style="color:#0b7a6d;">${url}</a></p>
|
|
56
|
+
<p style="margin:0;font-size:12px;color:#8593a8;border-top:1px solid #e3e8ef;padding-top:16px;">This is a single-use sign-in link. If you didn't expect this invitation, you can ignore this email.</p>
|
|
57
57
|
</div>
|
|
58
58
|
</body>
|
|
59
59
|
</html>`;
|
|
@@ -58,3 +58,15 @@ export async function readR2(binding: string): Promise<R2Bucket | undefined> {
|
|
|
58
58
|
const w = await getWorkerEnv();
|
|
59
59
|
return (w?.[binding] as R2Bucket | undefined) ?? undefined;
|
|
60
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Cloudflare Queue producer binding (e.g. WEBHOOK_DELIVERY_QUEUE), if present.
|
|
64
|
+
* Untyped (`unknown`) deliberately — callers cast to their own minimal
|
|
65
|
+
* queue-shape interface (see `webhooks/cloudflareQueueDispatcher.ts`'s
|
|
66
|
+
* `CloudflareQueueSender`) rather than this module taking a types dep on a
|
|
67
|
+
* specific binding's shape.
|
|
68
|
+
*/
|
|
69
|
+
export async function readQueue(binding: string): Promise<unknown | undefined> {
|
|
70
|
+
const w = await getWorkerEnv();
|
|
71
|
+
return w?.[binding] ?? undefined;
|
|
72
|
+
}
|
|
@@ -9,8 +9,13 @@ export function organizationLd(tenant: TenantConfig, base: string) {
|
|
|
9
9
|
// Logo sourced from the migrated brand assets (og image / logo), so the
|
|
10
10
|
// Organization schema reflects the real merchant brand, not a generic favicon.
|
|
11
11
|
const brand = tenant.theme_tokens?.brand;
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
// Absolutize the logo: schema.org wants an absolute URL, and tenant brand
|
|
13
|
+
// assets are stored as site-relative paths (e.g. /tenants/<id>/brand/og.svg).
|
|
14
|
+
// new URL() leaves an already-absolute value untouched.
|
|
15
|
+
const logo = new URL(
|
|
16
|
+
brand?.ogImage ?? brand?.logoLight ?? "/favicon.svg",
|
|
17
|
+
base + "/",
|
|
18
|
+
).toString();
|
|
14
19
|
return {
|
|
15
20
|
"@context": "https://schema.org",
|
|
16
21
|
"@type": "Organization",
|
|
@@ -97,10 +102,11 @@ export function productLd(
|
|
|
97
102
|
url,
|
|
98
103
|
}));
|
|
99
104
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
)
|
|
103
|
-
|
|
105
|
+
// NO aggregateRating/review here — those are emitted ONLY from real sourced
|
|
106
|
+
// numbers by reviewLd(), which callers merge over this Product graph. Emitting
|
|
107
|
+
// a rating from a bare `reviews.rating` metafield (with a placeholder count)
|
|
108
|
+
// fabricates rich-result markup for stores with no real reviews (e.g. PVC),
|
|
109
|
+
// which violates the review-markup policy (docs/architecture/review-markup-policy.md).
|
|
104
110
|
return {
|
|
105
111
|
"@context": "https://schema.org",
|
|
106
112
|
"@type": "Product",
|
|
@@ -121,17 +127,6 @@ export function productLd(
|
|
|
121
127
|
: "https://schema.org/OutOfStock",
|
|
122
128
|
offers,
|
|
123
129
|
},
|
|
124
|
-
...(ratingMf
|
|
125
|
-
? {
|
|
126
|
-
aggregateRating: {
|
|
127
|
-
"@type": "AggregateRating",
|
|
128
|
-
ratingValue: ratingMf.value,
|
|
129
|
-
bestRating: "5",
|
|
130
|
-
// ASSUMPTION: review count placeholder; real capture is Phase 2.
|
|
131
|
-
ratingCount: 24,
|
|
132
|
-
},
|
|
133
|
-
}
|
|
134
|
-
: {}),
|
|
135
130
|
};
|
|
136
131
|
}
|
|
137
132
|
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shopper membership eligibility — the request-scoped seam that resolves whether
|
|
3
|
+
* the current shopper is a member, and which tier (F10).
|
|
4
|
+
*
|
|
5
|
+
* Phase 1 storefronts have NO shopper-login/account source (member accounts are
|
|
6
|
+
* a Phase-2 managed-checkout capability), so this resolves anonymous
|
|
7
|
+
* (`{ isMember: false }`) by default. The pricing engine in
|
|
8
|
+
* `@tot/public-runtime` is real and unit-tested regardless; member pricing only
|
|
9
|
+
* RENDERS once a real eligibility signal flows through here.
|
|
10
|
+
*
|
|
11
|
+
* Production wires the signal from the resolved account claim (e.g. a
|
|
12
|
+
* member-tier cookie/JWT set by the Phase-2 login), so the whole storefront gets
|
|
13
|
+
* member pricing by threading one function — no per-page auth logic.
|
|
14
|
+
*/
|
|
15
|
+
import type { MemberContext } from "@tot/public-runtime";
|
|
16
|
+
import { ANONYMOUS_MEMBER } from "@tot/public-runtime";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The minimal membership signal a request carries. Phase 1 never populates it;
|
|
20
|
+
* production derives it from the shopper's resolved account (a member-tier
|
|
21
|
+
* claim). Kept tiny + framework-free so it is trivially testable.
|
|
22
|
+
*/
|
|
23
|
+
export interface MembershipSignal {
|
|
24
|
+
/** The shopper's membership tier id, if they are a signed-in member. */
|
|
25
|
+
memberTierId?: string | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the shopper's {@link MemberContext} from an optional membership signal.
|
|
30
|
+
* No signal (Phase 1, or an anonymous shopper) → non-member.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveMemberContext(signal?: MembershipSignal | null): MemberContext {
|
|
33
|
+
if (signal?.memberTierId) {
|
|
34
|
+
return { isMember: true, tierId: signal.memberTierId };
|
|
35
|
+
}
|
|
36
|
+
return ANONYMOUS_MEMBER;
|
|
37
|
+
}
|