@tokenoftrust/storefront-runner 2.0.41 → 2.0.43
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/integrations/lifecycle-host-asset.mjs +20 -6
- package/apps/storefront/package.json +1 -0
- package/apps/storefront/platform-reconcile-tenants.json +3 -0
- package/apps/storefront/public/favicon.ico +0 -0
- package/apps/storefront/public/shared/commerce-chrome.css +147 -16
- package/apps/storefront/src/components/AsyncStylesheet.astro +26 -0
- package/apps/storefront/src/components/Breadcrumbs.astro +8 -1
- package/apps/storefront/src/components/Img.astro +6 -5
- package/apps/storefront/src/components/ProductCard.astro +6 -4
- package/apps/storefront/src/components/Seo.astro +2 -1
- package/apps/storefront/src/components/chrome/SiteFooter.astro +45 -11
- package/apps/storefront/src/components/chrome/SiteHeader.astro +83 -13
- package/apps/storefront/src/components/commerce/RatingStars.astro +1 -1
- package/apps/storefront/src/components/compliance/ComplianceNotice.astro +2 -2
- package/apps/storefront/src/components/content/PromoTiles.astro +107 -0
- package/apps/storefront/src/components/content/TrustStrip.astro +30 -14
- package/apps/storefront/src/components/home/Hero.astro +156 -4
- package/apps/storefront/src/components/islands/AgeGate.tsx +135 -26
- package/apps/storefront/src/components/islands/ImageGallery.tsx +6 -9
- package/apps/storefront/src/components/islands/IsolatedAgeGate.tsx +194 -46
- package/apps/storefront/src/components/islands/SearchTypeahead.tsx +10 -4
- package/apps/storefront/src/components/marketing/Faq.astro +9 -2
- package/apps/storefront/src/config/devTenantSeed.ts +1 -1
- package/apps/storefront/src/config/kvTenantResolver.ts +7 -1
- package/apps/storefront/src/config/publicationSpaceConfig.ts +12 -1
- package/apps/storefront/src/config/publicationSpaceRegistry.ts +5 -0
- package/apps/storefront/src/config/tenants.ts +22 -0
- package/apps/storefront/src/layouts/Layout.astro +69 -44
- package/apps/storefront/src/lib/activity/changeActorAttribution.ts +24 -8
- package/apps/storefront/src/lib/activity/recordActivity.ts +15 -0
- package/apps/storefront/src/lib/asyncCss.ts +23 -0
- package/apps/storefront/src/lib/auth/loginGate.ts +22 -11
- package/apps/storefront/src/lib/basePath.ts +70 -9
- package/apps/storefront/src/lib/chrome/managedCart.ts +50 -0
- package/apps/storefront/src/lib/chrome/model.ts +4 -0
- package/apps/storefront/src/lib/chrome/newsletterAgeAttestation.ts +9 -0
- package/apps/storefront/src/lib/content/trustStrip.ts +38 -3
- package/apps/storefront/src/lib/cspStaticHashes.ts +4 -1
- package/apps/storefront/src/lib/diag/capture.ts +161 -0
- package/apps/storefront/src/lib/diag/consoleLine.ts +76 -0
- package/apps/storefront/src/lib/diag/debugConsole.ts +118 -0
- package/apps/storefront/src/lib/diag/debugMode.ts +163 -0
- package/apps/storefront/src/lib/diag/debugWindow.ts +111 -0
- package/apps/storefront/src/lib/diag/reference.ts +148 -0
- package/apps/storefront/src/lib/diag/referenceStore.ts +113 -0
- package/apps/storefront/src/lib/diag/report.ts +109 -0
- package/apps/storefront/src/lib/diag/session.ts +93 -0
- package/apps/storefront/src/lib/featuredProductsPresentation.ts +5 -0
- package/apps/storefront/src/lib/heroSlideshow.ts +41 -0
- package/apps/storefront/src/lib/homeVisualParity.ts +107 -0
- package/apps/storefront/src/lib/imageAttrs.ts +9 -6
- package/apps/storefront/src/lib/integration/nonceStamp.ts +23 -1
- package/apps/storefront/src/lib/integration/renderIntegrationState.ts +36 -0
- package/apps/storefront/src/lib/integration/tenantAnalyticsDeclarations.ts +17 -12
- package/apps/storefront/src/lib/jsonScript.ts +13 -0
- package/apps/storefront/src/lib/pinnedRouteRewriteBoundary.ts +31 -0
- package/apps/storefront/src/lib/previewReload.ts +53 -9
- package/apps/storefront/src/lib/rawChrome.ts +148 -19
- package/apps/storefront/src/lib/rawMarketingHtml.ts +120 -7
- package/apps/storefront/src/lib/storyblok/content-model.ts +46 -3
- package/apps/storefront/src/lib/styleGuideThemes.ts +107 -0
- package/apps/storefront/src/lib/tenantRouting.ts +15 -4
- package/apps/storefront/src/lib/tot/syncState.ts +21 -0
- package/apps/storefront/src/lib/tracking/publishSafetyNet.ts +29 -3
- package/apps/storefront/src/middleware/controlPlane.ts +9 -5
- package/apps/storefront/src/middleware/index.ts +13 -1
- package/apps/storefront/src/middleware/renderChain.ts +54 -11
- package/apps/storefront/src/middleware/routeBranch.ts +58 -12
- package/apps/storefront/src/middleware/staticBundle.ts +37 -3
- package/apps/storefront/src/pages/[...slug].astro +6 -2
- package/apps/storefront/src/pages/api/request-access.ts +5 -4
- package/apps/storefront/src/pages/blog/[slug].astro +11 -1
- package/apps/storefront/src/pages/cart.ts +23 -0
- package/apps/storefront/src/pages/index.astro +64 -85
- package/apps/storefront/src/pages/products/[handle].astro +8 -6
- package/apps/storefront/src/pages/sitemap.xml.ts +8 -2
- package/apps/storefront/src/pages/style-guide/[tenant]/[theme].astro +14 -25
- package/apps/storefront/src/pages/style-guide/[tenant]/chrome/[theme].astro +10 -17
- package/apps/storefront/src/pages/style-guide/[tenant]/guide/[theme].astro +9 -16
- package/apps/storefront/src/pages/style-guide/[tenant]/index.astro +9 -15
- package/apps/storefront/src/pages/the-build/[slug].astro +7 -1
- package/apps/storefront/src/styles/fonts.css +7 -0
- package/apps/storefront/src/styles/global.css +397 -3
- package/package.json +4 -1
- package/packages/cli/src/visual-parity.mjs +321 -0
- package/packages/public-runtime/package.json +7 -0
- package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
- package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
- package/packages/public-runtime/src/chrome.ts +44 -7
- package/packages/public-runtime/src/csp.ts +12 -1
- package/packages/public-runtime/src/extension-contract-values.d.mts +19 -0
- package/packages/public-runtime/src/extension-contract-values.mjs +62 -0
- package/packages/public-runtime/src/extension-contract.ts +20 -6
- package/packages/public-runtime/src/image-dependencies.ts +1438 -0
- package/packages/public-runtime/src/index.ts +8 -0
- package/packages/public-runtime/src/integration/consent-broker.ts +10 -7
- package/packages/public-runtime/src/integration/consent-config.ts +9 -0
- package/packages/public-runtime/src/integration/contact-sales-action-source.ts +56 -10
- package/packages/public-runtime/src/integration/governed-event-properties.ts +62 -0
- package/packages/public-runtime/src/integration/index.ts +1 -0
- package/packages/public-runtime/src/integration/lifecycle-boot.ts +30 -2
- package/packages/public-runtime/src/integration/lifecycle-host-asset.d.mts +2 -0
- package/packages/public-runtime/src/integration/lifecycle-host-asset.mjs +17 -3
- package/packages/public-runtime/src/integration/native-amplitude.ts +12 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox-asset.mjs +9 -0
- package/packages/public-runtime/src/integration/native-ga4-sandbox-entry.ts +27 -3
- package/packages/public-runtime/src/integration/native-ga4-sandbox.ts +13 -0
- package/packages/public-runtime/src/integration/native-ga4.ts +11 -0
- package/packages/public-runtime/src/integration/platform-adapters.ts +5 -1
- package/packages/public-runtime/src/integration/render-phases.ts +28 -2
- package/packages/public-runtime/src/publication-redirects.d.mts +45 -0
- package/packages/public-runtime/src/publication-redirects.mjs +379 -0
- package/packages/public-runtime/src/publication-space.ts +24 -3
- package/packages/public-runtime/src/static-bundle.ts +145 -26
- package/packages/public-runtime/src/tenant-embed-defaults.d.mts +3 -0
- package/packages/public-runtime/src/tenant-embed-defaults.mjs +40 -0
- package/packages/public-runtime/src/tenant.ts +59 -1
- package/pnpm-lock.runner.yaml +19 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/dev/git-status.mjs +1 -1
- package/scripts/dev/publish.mjs +6 -0
- package/scripts/lib/tenant-asset-copy.mjs +4 -2
- package/scripts/tenant/materialize-tenant-content.mjs +1 -0
- package/scripts/tenant/validate.mjs +171 -8
- package/apps/storefront/src/lib/cfImage.ts +0 -55
- package/apps/storefront/src/workers/pruneRollupWorker.ts +0 -120
- 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/js/storefront.js +0 -129
- package/tenants/home/public/logo-wordmark.png +0 -0
- package/tenants/home/public/pages/storefront.css +0 -235
- package/tenants/home/theme.json +0 -6
|
@@ -59,13 +59,18 @@ export async function resolveTenantRouting(
|
|
|
59
59
|
pathname: string,
|
|
60
60
|
resolver: TenantResolver,
|
|
61
61
|
): Promise<TenantRouting> {
|
|
62
|
-
// 1. Host wins when it explicitly owns a tenant
|
|
62
|
+
// 1. Host wins when it explicitly owns a tenant — UNLESS it declares
|
|
63
|
+
// `pathPrefixTenants`, the platform's shared preview host, which serves its
|
|
64
|
+
// own content at the root AND dispatches other tenants by path prefix.
|
|
65
|
+
// Returning early for that host turns every /<domain>/* URL into a path on
|
|
66
|
+
// its own site, which is how giving it a tenant of its own silently 404'd
|
|
67
|
+
// the dev loop, the admin preview links and the post-deploy chrome canary.
|
|
63
68
|
const byHost = await resolver.resolveStrict(host);
|
|
64
|
-
if (byHost) {
|
|
69
|
+
if (byHost && !byHost.pathPrefixTenants) {
|
|
65
70
|
return { tenant: byHost, basePath: "", renderPath: normalizePath(pathname) };
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
// 2. Unknown host -> try a /<domain>/*
|
|
73
|
+
// 2. Unknown host, or a declared path-prefix host -> try a /<domain>/* prefix.
|
|
69
74
|
const segments = pathname.replace(/^\/+/, "").split("/");
|
|
70
75
|
const firstRaw = segments[0] ?? "";
|
|
71
76
|
let first = firstRaw;
|
|
@@ -85,7 +90,13 @@ export async function resolveTenantRouting(
|
|
|
85
90
|
}
|
|
86
91
|
}
|
|
87
92
|
|
|
88
|
-
// 3.
|
|
93
|
+
// 3. A path-prefix host whose prefix named no known tenant serves its OWN
|
|
94
|
+
// content — that is the root case, and any other path on it.
|
|
95
|
+
if (byHost) {
|
|
96
|
+
return { tenant: byHost, basePath: "", renderPath: normalizePath(pathname) };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 4. Fall back to the registry default for this (unknown) host.
|
|
89
100
|
const fallback = await resolver.resolveByHost(host);
|
|
90
101
|
return { tenant: fallback, basePath: "", renderPath: normalizePath(pathname) };
|
|
91
102
|
}
|
|
@@ -57,6 +57,20 @@ export interface SyncStateSweepChanges {
|
|
|
57
57
|
collections: SyncStateEntityChangeSet;
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
/** Mirrors packages/migration/src/load/sync-state.ts's `ImageRehostRecord`. */
|
|
61
|
+
export interface SyncStateImageRehost {
|
|
62
|
+
computedAt: string;
|
|
63
|
+
mode: "rehost" | "reference";
|
|
64
|
+
sink: string | null;
|
|
65
|
+
considered: number;
|
|
66
|
+
rehosted: number;
|
|
67
|
+
alreadyRehosted: number;
|
|
68
|
+
referenced: number;
|
|
69
|
+
errors: number;
|
|
70
|
+
products: string[];
|
|
71
|
+
collections: string[];
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/** The on-disk shape written by the migration CLI (schema "tot.sync-state/1"). */
|
|
61
75
|
export interface CatalogSyncState {
|
|
62
76
|
schema: string;
|
|
@@ -74,6 +88,13 @@ export interface CatalogSyncState {
|
|
|
74
88
|
* migrated before hsby-B2 (change detection) has no `lastChanges`.
|
|
75
89
|
*/
|
|
76
90
|
lastChanges?: SyncStateSweepChanges;
|
|
91
|
+
/**
|
|
92
|
+
* What the most recent sync's image rehost moved. Absent when that run moved
|
|
93
|
+
* no imagery. Recorded separately from `lastChanges` because a rehost rewrites
|
|
94
|
+
* image urls without touching `source_checksum` — so the content-hash delta
|
|
95
|
+
* reports those records as unchanged, and this is the only account of the move.
|
|
96
|
+
*/
|
|
97
|
+
lastImageRehost?: SyncStateImageRehost;
|
|
77
98
|
}
|
|
78
99
|
|
|
79
100
|
// Vite resolves this glob at build time and bundles every tenant's sync-state
|
|
@@ -119,7 +119,21 @@ function attrsOf(element: RewriterElement): Record<string, string> {
|
|
|
119
119
|
* shipping no identity for this content at all.
|
|
120
120
|
*
|
|
121
121
|
* No-op (returns `html` unchanged) when `HTMLRewriter` isn't available in
|
|
122
|
-
* this runtime.
|
|
122
|
+
* this runtime, and equally when the rewrite itself cannot complete.
|
|
123
|
+
*
|
|
124
|
+
* FAIL-OPEN, ALWAYS. This is a safety net over content the platform does not
|
|
125
|
+
* author, so it must never be able to take a page down. `onEndTag` registers a
|
|
126
|
+
* callback for an end tag the parser is not obliged to produce: `<li>`, `<p>`,
|
|
127
|
+
* `<td>` and friends may legally omit theirs, and `HTMLRewriter` then aborts the
|
|
128
|
+
* stream with `TypeError: Parser error: No end tag.` — mid-transform, so no
|
|
129
|
+
* guard at registration can catch it. That throw propagated out of
|
|
130
|
+
* `getHomeHtml`/`getPageHtml` and cost the whole route: `/` and `/sitemap.xml`
|
|
131
|
+
* served a zero-byte 404 for a tenant whose HTML was valid.
|
|
132
|
+
*
|
|
133
|
+
* An unstamped document loses derived click identity — the exact loss the
|
|
134
|
+
* absence of any stamp already represents, and what `reportTrackingChainHealth`
|
|
135
|
+
* exists to surface. A 404 is not a smaller loss than that, so the original HTML
|
|
136
|
+
* wins whenever the pass cannot finish.
|
|
123
137
|
*/
|
|
124
138
|
export async function stampTrackingIdsAtPublish(html: string): Promise<string> {
|
|
125
139
|
const HTMLRewriter = getHtmlRewriterCtor();
|
|
@@ -157,6 +171,18 @@ export async function stampTrackingIdsAtPublish(html: string): Promise<string> {
|
|
|
157
171
|
},
|
|
158
172
|
});
|
|
159
173
|
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
try {
|
|
175
|
+
const rewritten = rewriter.transform(new Response(html));
|
|
176
|
+
return await rewritten.text();
|
|
177
|
+
} catch (error) {
|
|
178
|
+
// One structured line, never a throw — the same posture the raw-surface
|
|
179
|
+
// health report takes. The document still serves; only its derived ids are
|
|
180
|
+
// absent, and that absence is what the health signal already reports.
|
|
181
|
+
console.error(
|
|
182
|
+
`[tracking] publish-time id stamping skipped — the document is served unstamped: ${
|
|
183
|
+
error instanceof Error ? error.message : String(error)
|
|
184
|
+
}`,
|
|
185
|
+
);
|
|
186
|
+
return html;
|
|
187
|
+
}
|
|
162
188
|
}
|
|
@@ -12,6 +12,7 @@ import { fromD1, type D1Like } from "@/lib/d1/catalog";
|
|
|
12
12
|
import { readCurrentTenantConsentConfig } from "@/lib/apps/consentConfigRead";
|
|
13
13
|
import { loadActiveManagedInstallDeclarations } from "@/lib/integration/managedInstallDeclarations";
|
|
14
14
|
import { buildTenantConsentBroker, verifyPreviewToken } from "@tot/public-runtime";
|
|
15
|
+
import { tenantIsDeclaredPublic } from "@/config/tenants";
|
|
15
16
|
import type {
|
|
16
17
|
ConsentSnapshot,
|
|
17
18
|
IntegrationDeclarationInput,
|
|
@@ -26,7 +27,6 @@ import {
|
|
|
26
27
|
isAdminPath,
|
|
27
28
|
sessionAdmitsHost,
|
|
28
29
|
sessionHostCapability,
|
|
29
|
-
PUBLIC_HOME_TENANT_ID,
|
|
30
30
|
type GateMode,
|
|
31
31
|
} from "@/lib/auth/loginGate";
|
|
32
32
|
import { resolveAdminLanding, isAdminSelectPath } from "@/lib/auth/adminLanding";
|
|
@@ -64,15 +64,19 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
|
|
|
64
64
|
const { request, locals } = context;
|
|
65
65
|
|
|
66
66
|
// Login-gate enforcement (DEMO_GATE_MODE="login"): runs before the render so a
|
|
67
|
-
// gated page is never served to an unauthenticated viewer.
|
|
68
|
-
//
|
|
69
|
-
// other tenant on a gated host is withheld behind sign-in. A valid,
|
|
67
|
+
// gated page is never served to an unauthenticated viewer. A tenant that
|
|
68
|
+
// DECLARES `access.public` is EXEMPT — it renders publicly (browse + sign up);
|
|
69
|
+
// every other tenant on a gated host is withheld behind sign-in. A valid,
|
|
70
70
|
// resource-bound tot_session sets locals.viewer (email + roles) for chrome;
|
|
71
71
|
// otherwise an unauthenticated visitor gets a "Coming soon" holding page with
|
|
72
72
|
// a Sign-in button (auth paths are exempt so sign-in itself is reachable). The
|
|
73
73
|
// grant `resource` is the tenant's appDomain — never input.
|
|
74
74
|
if (input.hostLoginGated && !input.internalContentGrant) {
|
|
75
|
-
|
|
75
|
+
// From the platform registry by id, NOT off `tenant` — that object resolves
|
|
76
|
+
// from TENANT_CACHE KV first, so a record written before this field existed
|
|
77
|
+
// would silently gate a page the platform declares public, and a KV write
|
|
78
|
+
// must never be able to make a store public. See tenantIsDeclaredPublic.
|
|
79
|
+
const tenantExempt = tenantIsDeclaredPublic(tenant.tenant_id);
|
|
76
80
|
// UNIFIED on the federated session: read the FULL session (no resource filter —
|
|
77
81
|
// federated sign-in's aud is the storefront app, not this tenant) and admit the
|
|
78
82
|
// viewer iff they hold a MEMBERSHIP for the host's tenant. One session model for
|
|
@@ -36,6 +36,7 @@ import { HEALTH_PATH, healthResponse } from "@/lib/health";
|
|
|
36
36
|
import { observeDeployVersion } from "@/lib/activity/deployVersion";
|
|
37
37
|
import { primeActivityKillSwitch } from "@/lib/activity/killSwitch";
|
|
38
38
|
import {
|
|
39
|
+
platformStaticRootLocationV1,
|
|
39
40
|
publicationRedirectLocationV1,
|
|
40
41
|
resolveMiddlewareBranchV1,
|
|
41
42
|
} from "./routeBranch.js";
|
|
@@ -245,7 +246,18 @@ export const onRequest = defineMiddleware(async (context, next) => {
|
|
|
245
246
|
hasManagedInstalls: false,
|
|
246
247
|
});
|
|
247
248
|
if (bundled) return bundled;
|
|
248
|
-
const
|
|
249
|
+
const rootLocation = platformStaticRootLocationV1({
|
|
250
|
+
origin: url.origin,
|
|
251
|
+
basePath,
|
|
252
|
+
renderPath,
|
|
253
|
+
search: url.search,
|
|
254
|
+
});
|
|
255
|
+
if (rootLocation) {
|
|
256
|
+
const response = new Response(null, { status: 308, headers: { location: rootLocation } });
|
|
257
|
+
applySecurityHeaders(response.headers);
|
|
258
|
+
return response;
|
|
259
|
+
}
|
|
260
|
+
const asset = await next();
|
|
249
261
|
applySecurityHeaders(asset.headers);
|
|
250
262
|
return asset;
|
|
251
263
|
}
|
|
@@ -51,6 +51,8 @@ import {
|
|
|
51
51
|
resolveEnvironmentOnlyPlan,
|
|
52
52
|
INTEGRATION_NONCE_PLACEHOLDER,
|
|
53
53
|
defaultConsentSnapshot,
|
|
54
|
+
defaultAnalyticsConsentConfig,
|
|
55
|
+
buildTenantConsentBroker,
|
|
54
56
|
exactPublicationTimestamp,
|
|
55
57
|
parseConsentSnapshot,
|
|
56
58
|
CONSENT_SNAPSHOT_INPUT_HEADER,
|
|
@@ -58,6 +60,7 @@ import {
|
|
|
58
60
|
CONSENT_SNAPSHOT_HMAC_SECRET_BINDING,
|
|
59
61
|
MATERIALIZE_MODE_BINDING,
|
|
60
62
|
verifyConsentSnapshotSig,
|
|
63
|
+
resolveNativeGa4SandboxUrl,
|
|
61
64
|
} from "@tot/public-runtime";
|
|
62
65
|
import { readBakedManagedInstalls } from "@/lib/integration/bakedManagedInstalls";
|
|
63
66
|
import type {
|
|
@@ -86,7 +89,11 @@ import {
|
|
|
86
89
|
cspHeaderName,
|
|
87
90
|
} from "@/lib/integration/browserEnvironment";
|
|
88
91
|
import { freshNonce, stampResponseNonce } from "@/lib/integration/nonceStamp";
|
|
89
|
-
import {
|
|
92
|
+
import { takePinnedRouteRewriteResponse } from "@/lib/pinnedRouteRewriteBoundary";
|
|
93
|
+
import {
|
|
94
|
+
composeIntegrationDeclarations,
|
|
95
|
+
resolveIntegrationRenderState,
|
|
96
|
+
} from "@/lib/integration/renderIntegrationState";
|
|
90
97
|
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
91
98
|
import { middlewareRewriteTargetV1 } from "./routeBranch.js";
|
|
92
99
|
import {
|
|
@@ -119,8 +126,8 @@ export interface RenderChainInput {
|
|
|
119
126
|
managedIntegrationDeclarations: readonly IntegrationDeclarationInput[];
|
|
120
127
|
/**
|
|
121
128
|
* Reads the tenant's saved consent revision (D1). `null` on the public branch,
|
|
122
|
-
* which falls back to the
|
|
123
|
-
*
|
|
129
|
+
* which falls back to the configured-analytics platform default when no saved
|
|
130
|
+
* tenant override exists.
|
|
124
131
|
*/
|
|
125
132
|
readTenantConsentSnapshot: (() => Promise<ConsentSnapshot | null>) | null;
|
|
126
133
|
/**
|
|
@@ -270,10 +277,10 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
270
277
|
// is byte-identical to the non-integration form) and the environment-only plan is
|
|
271
278
|
// resolved below exactly as before — zero cost.
|
|
272
279
|
let resolvedBrowserEnvironment: BrowserEnvironment | null = null;
|
|
273
|
-
const integrationDeclarations =
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
280
|
+
const integrationDeclarations = composeIntegrationDeclarations(
|
|
281
|
+
loadTenantIntegrationDeclarations(tenant),
|
|
282
|
+
managedIntegrationDeclarations,
|
|
283
|
+
);
|
|
277
284
|
locals.integrationDeclarations = integrationDeclarations;
|
|
278
285
|
if (integrationDeclarations.length > 0) {
|
|
279
286
|
const declFeatures = resolveTenantFeatures(tenant);
|
|
@@ -304,8 +311,10 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
304
311
|
|
|
305
312
|
// ---- Consent snapshot: the tenant's ACTUAL initial consent state, not a
|
|
306
313
|
// hardcoded default. Both document mounts (Layout.astro, rawHtmlResponse) inline
|
|
307
|
-
// THIS value
|
|
314
|
+
// THIS value. Configured analytics defaults on unless an authoritative saved or
|
|
315
|
+
// baked tenant consent snapshot overrides it.
|
|
308
316
|
locals.consentSnapshot = defaultConsentSnapshot();
|
|
317
|
+
let tenantConsentResolved = false;
|
|
309
318
|
// BAKED-SNAPSHOT INPUT (ADR 0013), honored ONLY on the materialize render, ONLY
|
|
310
319
|
// under a valid tenant-bound signature. The go-live bake pipeline resolves the
|
|
311
320
|
// tenant's REAL snapshot from D1 ONCE on the Worker, signs it, and provides
|
|
@@ -338,9 +347,20 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
338
347
|
}
|
|
339
348
|
if (bakedConsent) {
|
|
340
349
|
locals.consentSnapshot = bakedConsent;
|
|
350
|
+
tenantConsentResolved = true;
|
|
341
351
|
} else if (input.readTenantConsentSnapshot) {
|
|
342
352
|
const saved = await input.readTenantConsentSnapshot();
|
|
343
|
-
if (saved)
|
|
353
|
+
if (saved) {
|
|
354
|
+
locals.consentSnapshot = saved;
|
|
355
|
+
tenantConsentResolved = true;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (
|
|
359
|
+
!tenantConsentResolved &&
|
|
360
|
+
integrationDeclarations.some((declaration) =>
|
|
361
|
+
declaration.manifest.browser.consent.purposes.includes("analytics"))
|
|
362
|
+
) {
|
|
363
|
+
locals.consentSnapshot = buildTenantConsentBroker(defaultAnalyticsConsentConfig()).getSnapshot();
|
|
344
364
|
}
|
|
345
365
|
|
|
346
366
|
// ---- Edge page cache: short-circuit production browse reads on a hit,
|
|
@@ -523,7 +543,10 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
523
543
|
// matching nonce/CSP pair.
|
|
524
544
|
const cspNonce = INTEGRATION_NONCE_PLACEHOLDER;
|
|
525
545
|
|
|
526
|
-
|
|
546
|
+
// Components read non-CSS brand choices (for example hero/header variants)
|
|
547
|
+
// from the tenant object. Expose the same effective theme that produced the
|
|
548
|
+
// CSS variables so a reconciled theme.json controls both surfaces.
|
|
549
|
+
locals.tenant = { ...tenant, theme_tokens: theme };
|
|
527
550
|
// One immutable clock for every visibility consumer in this render. Selected
|
|
528
551
|
// Worker content uses its audited pointer transition; materialization uses the
|
|
529
552
|
// signed build input; an ordinary legacy request has no clock and schedules stay hidden.
|
|
@@ -621,14 +644,25 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
621
644
|
basePath,
|
|
622
645
|
renderPath,
|
|
623
646
|
search: url.search,
|
|
647
|
+
// Astro re-runs this middleware for its error route, against the ORIGINAL
|
|
648
|
+
// request. Rewriting there discards the route it already chose.
|
|
649
|
+
routePattern: context.routePattern,
|
|
624
650
|
});
|
|
625
651
|
let response = rewriteTarget !== undefined
|
|
626
652
|
? await next(rewriteTarget)
|
|
627
653
|
: await next();
|
|
628
654
|
|
|
655
|
+
// A pinned generic-route fallback uses `context.rewrite()`, which runs the
|
|
656
|
+
// rewritten route through this middleware and hands back a fully decorated,
|
|
657
|
+
// CSP-stamped response. Preserve that inner response verbatim. Running the
|
|
658
|
+
// outer decoration path again would inject preview helpers twice and replace
|
|
659
|
+
// the CSP nonce without replacing the already-live nonce in the document.
|
|
660
|
+
const pinnedRewriteResponse = takePinnedRouteRewriteResponse(response);
|
|
661
|
+
if (pinnedRewriteResponse) return pinnedRewriteResponse;
|
|
662
|
+
|
|
629
663
|
// On-publish auto-refresh: inject the live-reload client into SHARED preview
|
|
630
664
|
// (`test`) HTML pages ONLY — never `live` (keeps live clean + edge-cacheable).
|
|
631
|
-
// It polls /
|
|
665
|
+
// It polls /api/preview/version and reloads when this tenant's cache-version
|
|
632
666
|
// bumps on publish/reconcile. Injection happens before the CSP header is set so
|
|
633
667
|
// the script's per-request nonce (cspNonce) matches the enforced policy.
|
|
634
668
|
response = await maybeInjectPreviewReload(response, {
|
|
@@ -717,6 +751,15 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
717
751
|
...widgetCsp.frameSrc,
|
|
718
752
|
...embedCsp.frameSrc,
|
|
719
753
|
],
|
|
754
|
+
// The native GA4 sandbox mounts on this tenant's OTHER apex/www host (see
|
|
755
|
+
// resolveNativeGa4SandboxUrl) so gtag.js runs on a real, non-opaque origin.
|
|
756
|
+
// frame-src (above) lets this page frame that sandbox; frame-ancestors is
|
|
757
|
+
// evaluated by the FRAMED document, so the sandbox origin must separately be
|
|
758
|
+
// allowed to frame this one back, or the site's own strict `frame-ancestors
|
|
759
|
+
// 'self'` blocks its own sandbox (apex and www are different origins).
|
|
760
|
+
// Derived from this request's own hostname — never a hardcoded tenant literal
|
|
761
|
+
// — so it's correct for every custom-domain tenant, not just one (ADR 0032).
|
|
762
|
+
frameAncestors: [new URL(resolveNativeGa4SandboxUrl(url.href)).origin],
|
|
720
763
|
formAction: checkoutCsp?.formAction ?? [],
|
|
721
764
|
reportGroup: CSP_REPORT_GROUP,
|
|
722
765
|
reportUri: CSP_REPORT_PATH,
|
|
@@ -15,10 +15,9 @@
|
|
|
15
15
|
* - {@link PLATFORM_STATIC_ASSET_PREFIXES} — built and public/ bytes. Identical
|
|
16
16
|
* for every visitor, no tenant state at all. Serving them through origin
|
|
17
17
|
* compute is exactly what ADR 0013 exists to prevent.
|
|
18
|
-
* - {@link PLATFORM_PUBLIC_DISCOVERY_PATHS} —
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* live on a reserved mount.
|
|
18
|
+
* - {@link PLATFORM_PUBLIC_DISCOVERY_PATHS} — anonymous-identical discovery
|
|
19
|
+
* output. Production serves materialized bytes from content-addressed
|
|
20
|
+
* storage; local and preview rendering keeps the same Tier 0 route shape.
|
|
22
21
|
* - {@link PLATFORM_PUBLIC_APP_ROUTE_PREFIXES} — the app's own catalog browse
|
|
23
22
|
* pages. Reserved so a tenant mount cannot claim them, which is a different
|
|
24
23
|
* question from who may read them: they are the storefront's most public
|
|
@@ -27,6 +26,7 @@
|
|
|
27
26
|
* Contract: docs/architecture/decisions/0013-public-serving-contract.md
|
|
28
27
|
*/
|
|
29
28
|
import {
|
|
29
|
+
PUBLICATION_DISCOVERY_OUTPUT_PATHS_V1,
|
|
30
30
|
resolvePublicationRoutePlanV1,
|
|
31
31
|
type PublicationRoutePlanV1,
|
|
32
32
|
type PublicationRouteRedirectPlanV1,
|
|
@@ -50,6 +50,7 @@ export type MiddlewareBranchV1 = "static-passthrough" | "public-render" | "contr
|
|
|
50
50
|
* front of bytes that never vary.
|
|
51
51
|
*/
|
|
52
52
|
export const PLATFORM_STATIC_ASSET_PREFIXES = [
|
|
53
|
+
"/_a",
|
|
53
54
|
"/_astro",
|
|
54
55
|
"/apps",
|
|
55
56
|
"/assets",
|
|
@@ -61,15 +62,13 @@ export const PLATFORM_STATIC_ASSET_PREFIXES = [
|
|
|
61
62
|
] as const;
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* Per-tenant discovery documents.
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* passed through.
|
|
65
|
+
* Per-tenant public discovery documents. Production publication materializes
|
|
66
|
+
* these routes into content-addressed storage; local and preview rendering use
|
|
67
|
+
* the public render chain. They never enter the authorization control plane.
|
|
68
68
|
*/
|
|
69
69
|
export const PLATFORM_PUBLIC_DISCOVERY_PATHS = [
|
|
70
|
-
|
|
70
|
+
...PUBLICATION_DISCOVERY_OUTPUT_PATHS_V1,
|
|
71
71
|
"/robots.txt",
|
|
72
|
-
"/sitemap.xml",
|
|
73
72
|
] as const;
|
|
74
73
|
|
|
75
74
|
/**
|
|
@@ -105,7 +104,8 @@ export function isPlatformStaticAssetPath(renderPath: string): boolean {
|
|
|
105
104
|
}
|
|
106
105
|
|
|
107
106
|
export function isPlatformPublicDiscoveryPath(renderPath: string): boolean {
|
|
108
|
-
|
|
107
|
+
const caseFolded = renderPath.toLowerCase();
|
|
108
|
+
return PLATFORM_PUBLIC_DISCOVERY_PATHS.some((entry) => caseFolded === entry);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
export function isPlatformPublicAppRoutePath(renderPath: string): boolean {
|
|
@@ -142,18 +142,64 @@ export interface ResolvedMiddlewareBranchV1 {
|
|
|
142
142
|
publication: PublicationRoutePlanV1;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Astro route patterns the error pipeline pins. When Astro renders one of these it
|
|
147
|
+
* has ALREADY chosen the route, and re-runs this middleware against the ORIGINAL
|
|
148
|
+
* request — same URL, so the same tenant prefix, so the same rewrite target.
|
|
149
|
+
* Rewriting then throws that choice away and sends the render back to the
|
|
150
|
+
* catch-all, which 404s again with a null body. The error page never renders.
|
|
151
|
+
*/
|
|
152
|
+
const ASTRO_ERROR_ROUTE_PATTERNS: readonly string[] = ["/404", "/500"];
|
|
153
|
+
|
|
154
|
+
/** True when Astro is rendering its error route, not the request's own route. */
|
|
155
|
+
export function isAstroErrorRouteV1(routePattern: string | undefined): boolean {
|
|
156
|
+
return routePattern !== undefined && ASTRO_ERROR_ROUTE_PATTERNS.includes(routePattern);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Astro rewrite target for tenant-prefix stripping or canonical render aliases.
|
|
161
|
+
*
|
|
162
|
+
* Returns `undefined` — do not rewrite — while the error route is rendering.
|
|
163
|
+
* Astro's error handler builds a state pinned to `/404` but carrying the ORIGINAL
|
|
164
|
+
* pathname, then re-runs middleware over it; a prefix rewrite there re-dispatches
|
|
165
|
+
* to the tenant path, which resolves to the catch-all and answers 404 with a null
|
|
166
|
+
* body. The merged result is a zero-byte 404: every path-routed 404 on this
|
|
167
|
+
* deployment, while the same 404 host-routed returns the full page.
|
|
168
|
+
*/
|
|
146
169
|
export function middlewareRewriteTargetV1(input: {
|
|
147
170
|
requestPath: string;
|
|
148
171
|
basePath: string;
|
|
149
172
|
renderPath: string;
|
|
150
173
|
search: string;
|
|
174
|
+
/** `context.routePattern` — the route Astro is currently rendering. */
|
|
175
|
+
routePattern?: string;
|
|
151
176
|
}): string | undefined {
|
|
177
|
+
if (isAstroErrorRouteV1(input.routePattern)) return undefined;
|
|
152
178
|
return input.basePath !== "" || input.renderPath !== input.requestPath
|
|
153
179
|
? `${input.renderPath}${input.search}`
|
|
154
180
|
: undefined;
|
|
155
181
|
}
|
|
156
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Where a path-routed platform static request belongs: its root-served URL.
|
|
185
|
+
*
|
|
186
|
+
* Platform bytes live at the origin root under every routing mode, and the
|
|
187
|
+
* platform's static handler answers them only there. `/<tenant>/shared/x.css` is
|
|
188
|
+
* not a file, and an Astro rewrite to `/shared/x.css` does not reach the static
|
|
189
|
+
* handler either: rewrites dispatch through the ROUTER, whose only match is the
|
|
190
|
+
* `[...slug]` catch-all. So the prefixed request is redirected to the root, which
|
|
191
|
+
* is correct on every substrate. Returns `undefined` when host-routed.
|
|
192
|
+
*/
|
|
193
|
+
export function platformStaticRootLocationV1(input: {
|
|
194
|
+
origin: string;
|
|
195
|
+
basePath: string;
|
|
196
|
+
renderPath: string;
|
|
197
|
+
search: string;
|
|
198
|
+
}): string | undefined {
|
|
199
|
+
if (!input.basePath) return undefined;
|
|
200
|
+
return `${input.origin}${input.renderPath}${input.search}`;
|
|
201
|
+
}
|
|
202
|
+
|
|
157
203
|
/** Absolute 308 location for host- and path-routed publication aliases. */
|
|
158
204
|
export function publicationRedirectLocationV1(input: {
|
|
159
205
|
origin: string;
|
|
@@ -9,7 +9,12 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readR2 } from "@/lib/runtimeEnv";
|
|
11
11
|
import { getCachedPage, putCachedPage, staticBundleCacheKey } from "@/lib/edgeCache";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
resolveChannelDigest,
|
|
14
|
+
serveStaticBundle,
|
|
15
|
+
serveVersionIndependentAsset,
|
|
16
|
+
versionIndependentAssetKey,
|
|
17
|
+
} from "@tot/public-runtime";
|
|
13
18
|
import type { StaticBundleR2BucketLike, TenantConfig } from "@tot/public-runtime";
|
|
14
19
|
|
|
15
20
|
export interface StaticBundleAttemptInput {
|
|
@@ -46,11 +51,38 @@ export async function tryServeStaticBundle(
|
|
|
46
51
|
input: StaticBundleAttemptInput,
|
|
47
52
|
): Promise<Response | null> {
|
|
48
53
|
const { request, url, tenant, cache, kv } = input;
|
|
54
|
+
const method = request.method.toUpperCase();
|
|
55
|
+
|
|
56
|
+
// Version-independent assets (`/_a/<sha256>`, `/_astro/<name>`) are exact addresses: the same
|
|
57
|
+
// bytes for every viewer, preview or not, whatever the live pointer says. A page from an
|
|
58
|
+
// older version — or a preview of one — keeps resolving its own sub-resources.
|
|
59
|
+
if ((method === "GET" || method === "HEAD") && versionIndependentAssetKey(url.pathname)) {
|
|
60
|
+
const bucket = await readR2("STATIC_BUNDLES");
|
|
61
|
+
if (bucket) {
|
|
62
|
+
const pinnedKey = cache && method === "GET"
|
|
63
|
+
? staticBundleCacheKey(tenant.tenant_id, "pinned", url.pathname, "")
|
|
64
|
+
: null;
|
|
65
|
+
if (pinnedKey && cache) {
|
|
66
|
+
const hit = await getCachedPage(cache, pinnedKey);
|
|
67
|
+
if (hit) return hit;
|
|
68
|
+
}
|
|
69
|
+
const pinned = await serveVersionIndependentAsset(
|
|
70
|
+
bucket as unknown as StaticBundleR2BucketLike,
|
|
71
|
+
url.pathname,
|
|
72
|
+
method,
|
|
73
|
+
);
|
|
74
|
+
if (pinned) {
|
|
75
|
+
if (pinnedKey && cache && pinned.ok) await putCachedPage(cache, pinnedKey, pinned);
|
|
76
|
+
return pinned;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
49
81
|
const eligible =
|
|
50
82
|
tenant.staticPublish != null &&
|
|
51
83
|
tenant.staticPublish.enabled !== false &&
|
|
52
84
|
input.basePath === "" &&
|
|
53
|
-
|
|
85
|
+
(method === "GET" || method === "HEAD") &&
|
|
54
86
|
!input.isPreviewHost &&
|
|
55
87
|
!input.hasPreviewToken &&
|
|
56
88
|
!input.hasCandidatePin &&
|
|
@@ -80,7 +112,7 @@ export async function tryServeStaticBundle(
|
|
|
80
112
|
return null;
|
|
81
113
|
}
|
|
82
114
|
|
|
83
|
-
const bundleCacheKey = cache
|
|
115
|
+
const bundleCacheKey = cache && method === "GET"
|
|
84
116
|
? staticBundleCacheKey(tenant.tenant_id, digest, url.pathname, url.search)
|
|
85
117
|
: null;
|
|
86
118
|
if (bundleCacheKey && cache) {
|
|
@@ -98,6 +130,8 @@ export async function tryServeStaticBundle(
|
|
|
98
130
|
},
|
|
99
131
|
},
|
|
100
132
|
url.pathname,
|
|
133
|
+
url.search,
|
|
134
|
+
method,
|
|
101
135
|
);
|
|
102
136
|
// `null` = the manifest proves this path is outside the bundle's static surface
|
|
103
137
|
// (an app surface like /admin) — the caller owns it.
|
|
@@ -11,7 +11,8 @@ import ProseSections from "@/components/content/ProseSections.astro";
|
|
|
11
11
|
import ProseToc from "@/components/content/ProseToc.astro";
|
|
12
12
|
import { breadcrumbLd } from "@/lib/jsonld";
|
|
13
13
|
import { truncate } from "@/lib/format";
|
|
14
|
-
import { rawHtmlPolicy, rawHtmlResponse } from "@/lib/rawMarketingHtml";
|
|
14
|
+
import { rawHtmlPolicy, rawHtmlResponse, rawPageCanonicalUrl } from "@/lib/rawMarketingHtml";
|
|
15
|
+
import { markPinnedRouteRewriteComplete } from "@/lib/pinnedRouteRewriteBoundary";
|
|
15
16
|
|
|
16
17
|
const { tenant, canonicalBase, content, basePath } = Astro.locals;
|
|
17
18
|
const slug = (Astro.params.slug ?? "").replace(/^\/+|\/+$/g, "");
|
|
@@ -32,7 +33,9 @@ if (
|
|
|
32
33
|
// dir, so the pages live under `/style-guide/…` and we rewrite the underscored URL
|
|
33
34
|
// onto them. These are noindex + sitemap-excluded and never reskin the live site.
|
|
34
35
|
if (slug === "_style-guide" || slug.startsWith("_style-guide/")) {
|
|
35
|
-
return
|
|
36
|
+
return markPinnedRouteRewriteComplete(
|
|
37
|
+
await Astro.rewrite("/style-guide/" + slug.slice("_style-guide".length + 1)),
|
|
38
|
+
);
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
// Raw tenant-authored marketing subpage (vendor-injected HTML) — serve the
|
|
@@ -50,6 +53,7 @@ if (rawHtml && rawHtmlPolicy(tenant).allowed) {
|
|
|
50
53
|
chromeWrapper,
|
|
51
54
|
plan: Astro.locals.integrationPlan,
|
|
52
55
|
consentSnapshot: Astro.locals.consentSnapshot,
|
|
56
|
+
canonicalUrl: rawPageCanonicalUrl(canonicalBase, slug),
|
|
53
57
|
});
|
|
54
58
|
}
|
|
55
59
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* POST /api/request-access — early-access lead capture for the
|
|
2
|
+
* POST /api/request-access — early-access lead capture for the storefront landing page.
|
|
3
3
|
*
|
|
4
|
-
* The browser form (tenants/
|
|
4
|
+
* The browser form (tenants/storefront.tokenoftrust.store) POSTs here same-origin (CSP `connect-src 'self'`
|
|
5
5
|
* + `form-action 'self'`). This route forwards the lead to tot20's generic
|
|
6
6
|
* messaging endpoint SERVER-SIDE — the storefront's ToT credentials never reach
|
|
7
7
|
* the browser. The message emails the Trello Success board so a card is created.
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* the server-side send just works and no mailto is returned.
|
|
19
19
|
*
|
|
20
20
|
* Deliberately NOT gated on `context.locals.features.*` — unlike newsletter.ts,
|
|
21
|
-
* this must work on the
|
|
21
|
+
* this must work on the storefront marketing tenant.
|
|
22
22
|
*/
|
|
23
23
|
import type { APIContext } from "astro";
|
|
24
24
|
import { readEnv } from "@/lib/env";
|
|
@@ -26,7 +26,8 @@ import { readEnv } from "@/lib/env";
|
|
|
26
26
|
export const prerender = false;
|
|
27
27
|
|
|
28
28
|
// Trello Success board — a card is created from the forwarded email.
|
|
29
|
-
// Kept in sync with the mailto: fallback in
|
|
29
|
+
// Kept in sync with the mailto: fallback in
|
|
30
|
+
// tenants/storefront.tokenoftrust.store/public/js/storefront.js.
|
|
30
31
|
const BOARD_EMAIL = "darrin84+l6gajmvvoo1x4edew5qs@boards.trello.com";
|
|
31
32
|
|
|
32
33
|
// Permissive email shape — tot20 / the mail system is the real deliverability authority.
|
|
@@ -11,12 +11,22 @@ import { buildAlternates } from "@/lib/seo/alternates.js";
|
|
|
11
11
|
import { languageLabel } from "@/lib/i18n.js";
|
|
12
12
|
import { breadcrumbLd, blogPostingLd } from "@/lib/jsonld.js";
|
|
13
13
|
|
|
14
|
+
// These redirects also exist in the published artifact: the publication composition lifts this
|
|
15
|
+
// file into the space's `routes.aliases`, which reach the CDN plane through the live pointer. The
|
|
16
|
+
// redirect below is the pointer-served (`worker-r2-pointer`) plane's IMPLEMENTATION of those same
|
|
17
|
+
// declared redirects — that plane resolves routing from tenant config, so without it a renamed
|
|
18
|
+
// slug would redirect on the CDN plane and 404 here. The materialization rail verifies this names
|
|
19
|
+
// a source the composition actually materializes.
|
|
20
|
+
export const MATERIALIZED_REDIRECT_SOURCE = "content/blog/redirects.json";
|
|
21
|
+
|
|
14
22
|
const { tenant, canonicalBase, basePath, content, publicationTimestamp } = Astro.locals;
|
|
15
23
|
const slug = Astro.params.slug ?? "";
|
|
16
24
|
const post = await getBlogPost(tenant.tenant_id, slug, content, publicationTimestamp);
|
|
17
25
|
if (!post) {
|
|
18
26
|
const redirect = await resolveBlogRedirect(tenant.tenant_id, slug, content);
|
|
19
|
-
|
|
27
|
+
// 308, matching the `redirect-308` alias contract the published plane serves, so a renamed slug
|
|
28
|
+
// behaves identically on both substrates.
|
|
29
|
+
if (redirect) return Astro.redirect(`${basePath}/blog/${redirect}/`, 308);
|
|
20
30
|
return new Response(null, { status: 404 });
|
|
21
31
|
}
|
|
22
32
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { APIRoute } from "astro";
|
|
2
|
+
import { managedCartHref } from "@/lib/chrome/managedCart.js";
|
|
3
|
+
|
|
4
|
+
export const prerender = false;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Backward-compatible entry for bookmarked or tenant-authored `/cart` links.
|
|
8
|
+
* Chrome now links straight to managed checkout, but this route prevents stale
|
|
9
|
+
* links from falling through the content catch-all to a misleading storefront
|
|
10
|
+
* 404. Tenant resolution and preview authorization have already run in
|
|
11
|
+
* middleware before this redirect is selected.
|
|
12
|
+
*/
|
|
13
|
+
export const GET: APIRoute = ({ locals }) => {
|
|
14
|
+
const location = managedCartHref(locals.commerce);
|
|
15
|
+
if (!location) return new Response(null, { status: 404 });
|
|
16
|
+
return new Response(null, {
|
|
17
|
+
status: 302,
|
|
18
|
+
headers: {
|
|
19
|
+
location,
|
|
20
|
+
"cache-control": "private, no-store",
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
};
|