@tokenoftrust/storefront-runner 2.5.5 → 3.0.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/agency-kit/tools/lib/public-binaries.mjs +95 -0
- package/apps/storefront/astro.config.mjs +7 -0
- package/apps/storefront/env.d.ts +3 -0
- package/apps/storefront/src/components/catalog/storefront/ProductCard.astro +5 -4
- package/apps/storefront/src/components/catalog/storefront/commerce/PriceDisplay.astro +2 -1
- package/apps/storefront/src/components/catalog/storefront/islands/ImageGallery.tsx +20 -3
- package/apps/storefront/src/components/catalog/storefront/islands/SearchTypeahead.tsx +17 -20
- package/apps/storefront/src/components/catalog/storefront/islands/VariantSelector.tsx +13 -21
- package/apps/storefront/src/components/catalog/storefront/plp/Pagination.astro +9 -5
- package/apps/storefront/src/components/checkout/storefront/commerce/CheckoutLoader.astro +18 -6
- package/apps/storefront/src/components/compliance/storefront/islands/CheckoutComplianceGate.tsx +19 -4
- package/apps/storefront/src/components/content/storefront/Badge.astro +1 -0
- package/apps/storefront/src/components/content/storefront/blog/BlogIndexView.astro +10 -1
- package/apps/storefront/src/components/publication/storefront/islands/CatalogQueryView.tsx +321 -0
- package/apps/storefront/src/config/declaredTenantConfig.ts +59 -27
- package/apps/storefront/src/config/devTenantSeed.ts +2 -1
- package/apps/storefront/src/layouts/Layout.astro +1 -1
- package/apps/storefront/src/lib/access/auth/gatePage.ts +29 -14
- package/apps/storefront/src/lib/access/shared/publicAccess.ts +95 -0
- package/apps/storefront/src/lib/access/shared/tenantRouting.ts +19 -0
- package/apps/storefront/src/lib/catalog/shared/catalogIndex.ts +109 -0
- package/apps/storefront/src/lib/catalog/shared/catalogQueryView.ts +60 -0
- package/apps/storefront/src/lib/catalog/shared/collectionPaging.ts +78 -0
- package/apps/storefront/src/lib/catalog/shared/stockStatus.ts +23 -0
- package/apps/storefront/src/lib/catalog/tot/query.ts +8 -0
- package/apps/storefront/src/lib/checkout/browser/checkoutRequirementsForm.ts +54 -0
- package/apps/storefront/src/lib/checkout/commerce/checkoutHandoffRoute.ts +103 -35
- package/apps/storefront/src/lib/checkout/commerce/checkoutRequirements.ts +88 -0
- package/apps/storefront/src/lib/checkout/commerce/finalCartAuthorization.ts +2 -0
- package/apps/storefront/src/lib/checkout/commerce/sharedStoreAdmission.ts +95 -0
- package/apps/storefront/src/lib/checkout/shared/lazyCheckoutLoader.ts +191 -0
- package/apps/storefront/src/lib/compliance/browser/checkoutVerificationFlow.ts +9 -0
- package/apps/storefront/src/lib/compliance/enforcement.ts +7 -5
- package/apps/storefront/src/lib/content/seo/structuredData.ts +12 -3
- package/apps/storefront/src/lib/content/storyblok/provider.ts +14 -7
- package/apps/storefront/src/lib/observability/shared/serverTiming.ts +110 -0
- package/apps/storefront/src/lib/publication/shared/edgeCache.ts +3 -4
- package/apps/storefront/src/lib/publication/shared/staticQuery.ts +64 -0
- package/apps/storefront/src/middleware/controlPlane.ts +14 -11
- package/apps/storefront/src/middleware/index.ts +33 -12
- package/apps/storefront/src/middleware/renderChain.ts +52 -26
- package/apps/storefront/src/pages/[...slug].astro +3 -0
- package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +112 -42
- package/apps/storefront/src/pages/api/compliance/preflight.ts +61 -13
- package/apps/storefront/src/pages/blog/author/[author].astro +1 -1
- package/apps/storefront/src/pages/blog/category/[category].astro +1 -1
- package/apps/storefront/src/pages/blog/tag/[tag].astro +1 -1
- package/apps/storefront/src/pages/catalog-index.json.ts +20 -0
- package/apps/storefront/src/pages/collections/[handle]/page/[n].astro +28 -0
- package/apps/storefront/src/pages/collections/[handle].astro +13 -198
- package/apps/storefront/src/pages/collections/_CollectionView.astro +251 -0
- package/apps/storefront/src/pages/collections/index.astro +1 -1
- package/apps/storefront/src/pages/products/[handle].astro +18 -8
- package/apps/storefront/src/pages/search.astro +33 -33
- package/apps/storefront/src/pages/sitemap.xml.ts +7 -1
- package/apps/storefront/vitest.config.cts +3 -0
- package/package.json +1 -1
- package/packages/cli/src/declared-config.mjs +318 -32
- package/packages/cli/src/publication-spaces.mjs +1 -1
- package/packages/public-runtime/package.json +1 -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/activity/catalog.ts +8 -1
- package/packages/public-runtime/src/candidate-index.ts +5 -0
- package/packages/public-runtime/src/capabilities.ts +12 -9
- package/packages/public-runtime/src/capture-rewrites.ts +687 -0
- package/packages/public-runtime/src/checkout-orders.ts +6 -7
- package/packages/public-runtime/src/checkout.ts +190 -6
- package/packages/public-runtime/src/compliance/evaluate.ts +7 -3
- package/packages/public-runtime/src/compliance/profile.ts +90 -14
- package/packages/public-runtime/src/compliance-coherence.d.mts +10 -1
- package/packages/public-runtime/src/compliance-coherence.mjs +129 -29
- package/packages/public-runtime/src/customization-runtime.ts +64 -2
- package/packages/public-runtime/src/declared-tenant-config.d.mts +13 -2
- package/packages/public-runtime/src/declared-tenant-config.mjs +83 -1
- package/packages/public-runtime/src/image-preview-rewrite-overlay.ts +13 -37
- package/packages/public-runtime/src/membership.ts +3 -0
- package/packages/public-runtime/src/publication-space.ts +1 -0
- package/packages/public-runtime/src/tenant.ts +28 -4
- package/scripts/tenant/validate.mjs +17 -8
|
@@ -11,7 +11,7 @@ import { readEnv, readD1, readKv } from "@/lib/publication/shared/env";
|
|
|
11
11
|
import { fromD1, type D1Like } from "@/lib/catalog/d1/catalog";
|
|
12
12
|
import { readCurrentTenantConsentConfig } from "@/lib/measurement/consent/consentConfigRead";
|
|
13
13
|
import { loadActiveManagedInstallDeclarations } from "@/lib/integrations/integration/managedInstallDeclarations";
|
|
14
|
-
import { buildTenantConsentBroker, verifyPreviewToken } from "@tot/public-runtime";
|
|
14
|
+
import { buildTenantConsentBroker, verifyPreviewToken, storefrontModeRequiresSignIn } from "@tot/public-runtime";
|
|
15
15
|
import { tenantIsDeclaredPublic } from "@/config/tenants";
|
|
16
16
|
import type {
|
|
17
17
|
ConsentSnapshot,
|
|
@@ -37,6 +37,7 @@ import { gatedApiResponse, gatedHoldingResponse } from "@/lib/access/auth/gatePa
|
|
|
37
37
|
import { readSession, KvSessionStore, SESSION_COOKIE } from "@/lib/access/auth/session";
|
|
38
38
|
import { runRenderChain } from "./renderChain.js";
|
|
39
39
|
import { resolveCandidateContentPin } from "./candidatePin.js";
|
|
40
|
+
import { timed } from "@/lib/observability/shared/serverTiming";
|
|
40
41
|
import type { InternalContentGrant } from "@/lib/access/auth/internalContentAuthority";
|
|
41
42
|
|
|
42
43
|
export interface ControlPlaneInput {
|
|
@@ -92,13 +93,13 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
|
|
|
92
93
|
let viewerRecord = null;
|
|
93
94
|
if (!isAuthPath(url.pathname)) {
|
|
94
95
|
const sessionKv = await readKv("SESSION");
|
|
96
|
+
const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
|
|
95
97
|
if (sessionKv) {
|
|
96
|
-
|
|
97
|
-
viewerRecord = await readSession(
|
|
98
|
+
viewerRecord = await timed(locals.serverTiming, "session", () => readSession(
|
|
98
99
|
new KvSessionStore(sessionKv),
|
|
99
100
|
sid,
|
|
100
101
|
Math.floor(Date.now() / 1000),
|
|
101
|
-
);
|
|
102
|
+
));
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
@@ -193,9 +194,12 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
|
|
|
193
194
|
return context.redirect(decision.location, 303);
|
|
194
195
|
}
|
|
195
196
|
// An API caller gets an honest 401, never a 200 page it would read as success.
|
|
196
|
-
|
|
197
|
+
const gateReason = storefrontModeRequiresSignIn(tenant.storefront?.mode)
|
|
198
|
+
? "storefront-unpaid"
|
|
199
|
+
: "shared-preview-host";
|
|
200
|
+
if (isApiPath(url.pathname)) return gatedApiResponse(decision.location, request, gateReason);
|
|
197
201
|
// Withhold content but offer sign-in (carrying the intended destination).
|
|
198
|
-
return gatedHoldingResponse(decision.location);
|
|
202
|
+
return gatedHoldingResponse(decision.location, gateReason);
|
|
199
203
|
}
|
|
200
204
|
// Admitted to a store that is not public on this host: by session, preview
|
|
201
205
|
// grant, or QA bypass. Visitor routes read this to stay side-effect-free for an
|
|
@@ -251,10 +255,8 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
|
|
|
251
255
|
let managedIntegrationDeclarations: IntegrationDeclarationInput[] = [];
|
|
252
256
|
if (appsD1) {
|
|
253
257
|
try {
|
|
254
|
-
managedIntegrationDeclarations = await
|
|
255
|
-
fromD1(appsD1),
|
|
256
|
-
tenant.tenant_id,
|
|
257
|
-
);
|
|
258
|
+
managedIntegrationDeclarations = await timed(locals.serverTiming, "installs", () =>
|
|
259
|
+
loadActiveManagedInstallDeclarations(fromD1(appsD1), tenant.tenant_id));
|
|
258
260
|
} catch {
|
|
259
261
|
managedIntegrationDeclarations = [];
|
|
260
262
|
}
|
|
@@ -278,7 +280,8 @@ export async function serveControlPlane(input: ControlPlaneInput): Promise<Respo
|
|
|
278
280
|
publicationBucket: input.publicationBucket,
|
|
279
281
|
verifiedCandidatePin: candidatePin ?? undefined,
|
|
280
282
|
managedIntegrationDeclarations,
|
|
281
|
-
readTenantConsentSnapshot: () =>
|
|
283
|
+
readTenantConsentSnapshot: () =>
|
|
284
|
+
timed(locals.serverTiming, "consent", () => readSavedConsentSnapshot(tenant.tenant_id)),
|
|
282
285
|
// The D1 catalog serving projection, for a tenant that declares
|
|
283
286
|
// `catalog.serving: "d1"` (ADR 0033). Deferred to the chain's own catalog build
|
|
284
287
|
// so a request the static bundle or the edge cache answers never pays for it.
|
|
@@ -25,7 +25,7 @@ import { getTenantResolver } from "@/config/resolver";
|
|
|
25
25
|
import { readEnv, readKv, readR2 } from "@/lib/publication/shared/runtimeEnv";
|
|
26
26
|
import { resolveCdnForwardRouting, resolveTenantRouting } from "@/lib/access/shared/tenantRouting";
|
|
27
27
|
import { crossSiteFormRefusal, verifyCdnForward } from "@/lib/publication/shared/cdnForward";
|
|
28
|
-
import { compileRouteTierIndexV1, resolvePublicationRoutePlanV1, type RouteTierIndexV1 , storefrontModeRequiresSignIn } from "@tot/public-runtime";
|
|
28
|
+
import { SAMPLE_CART_HANDOFF_URL, compileRouteTierIndexV1, resolvePublicationRoutePlanV1, type RouteTierIndexV1 , storefrontModeRequiresSignIn } from "@tot/public-runtime";
|
|
29
29
|
import type { PublicationArtifactBucketV1, TenantConfig } from "@tot/public-runtime";
|
|
30
30
|
import { DEV_DRAFT_COOKIE } from "@/lib/developer-experience/dev/devDraft";
|
|
31
31
|
import { applySecurityHeaders } from "@/lib/access/shared/securityHeaders";
|
|
@@ -36,6 +36,7 @@ import { parseGateMode } from "@/lib/access/auth/loginGate";
|
|
|
36
36
|
import { evaluateDemoGate, hostInGateList } from "@/lib/access/shared/demoGate";
|
|
37
37
|
import { isMaintenanceOn, maintenanceResponse } from "@/lib/access/shared/maintenance";
|
|
38
38
|
import { HEALTH_PATH, healthResponse } from "@/lib/observability/shared/health";
|
|
39
|
+
import { ServerTiming, withServerTiming } from "@/lib/observability/shared/serverTiming";
|
|
39
40
|
import { observeDeployVersion } from "@/lib/activity/deployVersion";
|
|
40
41
|
import { primeActivityKillSwitch } from "@/lib/activity/killSwitch";
|
|
41
42
|
import {
|
|
@@ -73,20 +74,27 @@ function routeTierIndexFor(tenant: TenantConfig): RouteTierIndexV1 {
|
|
|
73
74
|
* appended after the branch has read or written any edge cache.
|
|
74
75
|
*/
|
|
75
76
|
export const onRequest = defineMiddleware(async (context, next) => {
|
|
77
|
+
// An in-process rewrite re-enters this middleware with the same locals; the outer pass owns
|
|
78
|
+
// the recorder and the header.
|
|
79
|
+
const outer = !context.locals.serverTiming;
|
|
80
|
+
const timing = context.locals.serverTiming ?? new ServerTiming();
|
|
81
|
+
context.locals.serverTiming = timing;
|
|
76
82
|
const seen: { cdnForwarded: boolean; assetBasePath?: string } = { cdnForwarded: false };
|
|
77
|
-
const response = await serveRequest(context, next, seen);
|
|
83
|
+
const response = await serveRequest(context, next, seen, timing);
|
|
78
84
|
const { scopePreviewPinnedImageUrls } = await import("@/lib/assets/preview-pinned-image-urls");
|
|
79
85
|
const scoped = await scopePreviewPinnedImageUrls(response, context.locals.apiBasePath ?? seen.assetBasePath ?? "");
|
|
80
|
-
|
|
86
|
+
const finished = withVisitorRegionCookie(
|
|
81
87
|
scoped,
|
|
82
88
|
visitorRegionFor(context.request, { cdnForwarded: seen.cdnForwarded, dev: import.meta.env.DEV }),
|
|
83
89
|
);
|
|
90
|
+
return outer ? withServerTiming(finished, timing) : finished;
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
async function serveRequest(
|
|
87
94
|
context: APIContext,
|
|
88
95
|
next: MiddlewareNext,
|
|
89
96
|
seen: { cdnForwarded: boolean; assetBasePath?: string },
|
|
97
|
+
timing: ServerTiming,
|
|
90
98
|
): Promise<Response> {
|
|
91
99
|
const { request, locals } = context;
|
|
92
100
|
const url = new URL(request.url);
|
|
@@ -137,6 +145,16 @@ async function serveRequest(
|
|
|
137
145
|
const crossSite = crossSiteFormRefusal(request, origin);
|
|
138
146
|
if (crossSite) return crossSite;
|
|
139
147
|
|
|
148
|
+
// The canonical TEST sample GET signs server catalog facts without shopper state.
|
|
149
|
+
const sampleHandoff = new URL(SAMPLE_CART_HANDOFF_URL);
|
|
150
|
+
if (request.method === "GET" && !cdn && url.origin === sampleHandoff.origin &&
|
|
151
|
+
host === sampleHandoff.host && url.pathname === sampleHandoff.pathname) {
|
|
152
|
+
const { GET } = await import("@/pages/api/commerce/checkout-handoff");
|
|
153
|
+
const response = await GET(context);
|
|
154
|
+
applySecurityHeaders(response.headers);
|
|
155
|
+
return response;
|
|
156
|
+
}
|
|
157
|
+
|
|
140
158
|
// Demo access gate for named hosts (DEMO_GATE_HOSTS). Two modes, switched by
|
|
141
159
|
// DEMO_GATE_MODE:
|
|
142
160
|
// "password" (default) — interim Basic-Auth gate, enforced HERE (host-level,
|
|
@@ -158,10 +176,12 @@ async function serveRequest(
|
|
|
158
176
|
// Host first; if the host owns no tenant, a leading /<domain>/ path segment
|
|
159
177
|
// selects one (DNS-free tenant selection on localhost/preview). The resolver
|
|
160
178
|
// is KV-backed in prod (many-tenant) and static in plain dev.
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
179
|
+
const { tenant, basePath, renderPath } = await timing.time("tenant", async () => {
|
|
180
|
+
const resolver = await getTenantResolver();
|
|
181
|
+
return cdn
|
|
182
|
+
? await resolveCdnForwardRouting(cdn, url.pathname, resolver)
|
|
183
|
+
: await resolveTenantRouting(host, url.pathname, resolver);
|
|
184
|
+
});
|
|
165
185
|
if (!tenant) {
|
|
166
186
|
return new Response("Unknown host", { status: 404 });
|
|
167
187
|
}
|
|
@@ -214,13 +234,14 @@ async function serveRequest(
|
|
|
214
234
|
let internalContentGrant: InternalContentGrant | undefined;
|
|
215
235
|
if (publication.action !== "fail-closed" && route.requiresAuthorization) {
|
|
216
236
|
const sessionKv = await readKv("SESSION");
|
|
217
|
-
const
|
|
237
|
+
const coreUrl = await readEnv("TOT_CORE_URL");
|
|
238
|
+
const decision = await timing.time("internal-auth", () => authorizeInternalContentRequest({
|
|
218
239
|
route,
|
|
219
240
|
sessionStore: sessionKv ? new KvSessionStore(sessionKv) : undefined,
|
|
220
241
|
sessionId: context.cookies.get(SESSION_COOKIE)?.value ?? null,
|
|
221
|
-
coreUrl
|
|
242
|
+
coreUrl,
|
|
222
243
|
mintAssertion: mintOpaqueBrokerAssertion,
|
|
223
|
-
});
|
|
244
|
+
}));
|
|
224
245
|
if (decision.allowed) internalContentGrant = decision;
|
|
225
246
|
if (!internalContentGrant) {
|
|
226
247
|
const response = new Response("Not Found", { status: 404 });
|
|
@@ -288,7 +309,7 @@ async function serveRequest(
|
|
|
288
309
|
(capturedRevisionId || renderPath.startsWith("/_a/"))
|
|
289
310
|
? ((await readR2("PRODUCT_IMAGES")) ?? (await readR2("TENANT_ASSETS")) ?? null)
|
|
290
311
|
: null;
|
|
291
|
-
const bundled = await tryServeStaticBundle({
|
|
312
|
+
const bundled = await timing.time("bundle", () => tryServeStaticBundle({
|
|
292
313
|
request,
|
|
293
314
|
url,
|
|
294
315
|
tenant,
|
|
@@ -309,7 +330,7 @@ async function serveRequest(
|
|
|
309
330
|
},
|
|
310
331
|
}
|
|
311
332
|
: {}),
|
|
312
|
-
});
|
|
333
|
+
}));
|
|
313
334
|
if (bundled) return bundled;
|
|
314
335
|
const rootLocation = platformStaticRootLocationV1({
|
|
315
336
|
origin,
|
|
@@ -104,9 +104,10 @@ import {
|
|
|
104
104
|
resolveIntegrationRenderState,
|
|
105
105
|
} from "@/lib/integrations/integration/renderIntegrationState";
|
|
106
106
|
import { recordActivity } from "@/lib/activity/recordActivity";
|
|
107
|
+
import { timed } from "@/lib/observability/shared/serverTiming";
|
|
107
108
|
import { middlewareRewriteTargetV1 } from "./routeBranch.js";
|
|
108
109
|
import { declaredTenantOrigins } from "@/lib/access/shared/tenantOrigins";
|
|
109
|
-
import { withDeclaredTenantConfig } from "@/config/declaredTenantConfig";
|
|
110
|
+
import { DeclaredTenantConfigUnreadError, withDeclaredTenantConfig } from "@/config/declaredTenantConfig";
|
|
110
111
|
import {
|
|
111
112
|
applyInternalContentResponsePolicy,
|
|
112
113
|
internalContentGrantMatchesPlan,
|
|
@@ -226,14 +227,15 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
226
227
|
});
|
|
227
228
|
}
|
|
228
229
|
|
|
229
|
-
const
|
|
230
|
+
const timing = locals.serverTiming;
|
|
231
|
+
const publicationSource = await timed(timing, "source", () => resolvePublicationRuntimeSourceV1({
|
|
230
232
|
index: input.routeTierIndex,
|
|
231
233
|
requestPath: input.publicationRequestPath,
|
|
232
234
|
plan: input.publicationRoute,
|
|
233
235
|
channel: channelFromEnv(customization.env),
|
|
234
236
|
store: kv ? { get: (key) => kv.get(key) } : undefined,
|
|
235
237
|
bucket: input.publicationBucket,
|
|
236
|
-
});
|
|
238
|
+
}));
|
|
237
239
|
if (publicationSource.status === "unavailable") {
|
|
238
240
|
const response = new Response("Not Found", {
|
|
239
241
|
status: 404,
|
|
@@ -264,12 +266,26 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
264
266
|
// features). Everything below — static bundle, notices, gates, features,
|
|
265
267
|
// capabilities — reads this, never the bare record. A candidate-pinned render
|
|
266
268
|
// takes the candidate version's declaration, so a reviewer sees the notices the
|
|
267
|
-
// change under review declares.
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
269
|
+
// change under review declares. A declaration that cannot be read refuses the
|
|
270
|
+
// render: the store's compliance is unknown, and its gates and notices with it.
|
|
271
|
+
let tenant: TenantConfig;
|
|
272
|
+
try {
|
|
273
|
+
tenant = await timed(timing, "config", () => withDeclaredTenantConfig(
|
|
274
|
+
tenantRecord,
|
|
275
|
+
kv ? (key: string) => kv.get(key) : async () => null,
|
|
276
|
+
{ env: customization.env, ...(candidatePin ? { pinnedVersionId: candidatePin.versionId } : {}) },
|
|
277
|
+
));
|
|
278
|
+
} catch (err) {
|
|
279
|
+
if (!(err instanceof DeclaredTenantConfigUnreadError)) throw err;
|
|
280
|
+
console.error(err.message);
|
|
281
|
+
const response = new Response("Service Unavailable", {
|
|
282
|
+
status: 503,
|
|
283
|
+
headers: { "Cache-Control": "private, no-store", "Retry-After": "30" },
|
|
284
|
+
});
|
|
285
|
+
applySecurityHeaders(response.headers);
|
|
286
|
+
if (isInternalPublication) applyInternalContentResponsePolicy(response.headers);
|
|
287
|
+
return response;
|
|
288
|
+
}
|
|
273
289
|
// Request-scoped read memo: every content read resolves the same pointer and
|
|
274
290
|
// version manifest, and a catalog render reads one artifact per catalog file.
|
|
275
291
|
const kvReads = new Map<string, Promise<string | null>>();
|
|
@@ -351,12 +367,18 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
351
367
|
secret: previewSecret,
|
|
352
368
|
nowSeconds,
|
|
353
369
|
});
|
|
354
|
-
|
|
370
|
+
// The shared preview sets no version cookie, yet its documents name `/_a/` captures of the
|
|
371
|
+
// version the preview channel serves (capture overlay); those resolve against that version.
|
|
372
|
+
const sharedPreviewRevision = !candidatePin && !cookieRevision && customization.env === "test" &&
|
|
373
|
+
renderPath.startsWith("/_a/")
|
|
374
|
+
? (await resolveServableChannelVersion(kvGet, tenant.tenant_id, "preview")).versionId
|
|
375
|
+
: null;
|
|
376
|
+
const capturedRevisionId = candidatePin?.versionId ?? cookieRevision ?? sharedPreviewRevision;
|
|
355
377
|
const originalsBucket =
|
|
356
378
|
(capturedRevisionId || renderPath.startsWith("/_a/"))
|
|
357
379
|
? ((await readR2("PRODUCT_IMAGES")) ?? (await readR2("TENANT_ASSETS")) ?? null)
|
|
358
380
|
: null;
|
|
359
|
-
const bundled = publicationSource.status === "legacy-root" ? await tryServeStaticBundle({
|
|
381
|
+
const bundled = publicationSource.status === "legacy-root" ? await timed(timing, "bundle", () => tryServeStaticBundle({
|
|
360
382
|
request,
|
|
361
383
|
url,
|
|
362
384
|
tenant,
|
|
@@ -376,7 +398,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
376
398
|
},
|
|
377
399
|
}
|
|
378
400
|
: {}),
|
|
379
|
-
}) : null;
|
|
401
|
+
})) : null;
|
|
380
402
|
if (bundled) return bundled;
|
|
381
403
|
|
|
382
404
|
// ---- Tenant-local integration materialization (ext-kernel-09) --------------
|
|
@@ -496,10 +518,10 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
496
518
|
!candidatePin &&
|
|
497
519
|
!draftCookie &&
|
|
498
520
|
!isInternalPublication &&
|
|
499
|
-
isCacheablePath(url.pathname
|
|
521
|
+
isCacheablePath(url.pathname);
|
|
500
522
|
let pageKey: Request | null = null;
|
|
501
523
|
if (pageCacheEligible && cache && kv) {
|
|
502
|
-
const version = await readCacheVersion(kv, tenant.tenant_id);
|
|
524
|
+
const version = await timed(timing, "page-cache", () => readCacheVersion(kv, tenant.tenant_id));
|
|
503
525
|
// Fold the resolved integration plan revision into the key so a warm entry is
|
|
504
526
|
// version-scoped: a rollback/uninstall resolves a new revision → old entry
|
|
505
527
|
// unreachable → clean re-render without the removed origins (never a TTL wait).
|
|
@@ -514,7 +536,9 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
514
536
|
locals.integrationCsp?.planRevision,
|
|
515
537
|
publicationSource.status === "selected" ? publicationSource.cacheScope : undefined,
|
|
516
538
|
);
|
|
517
|
-
const
|
|
539
|
+
const key = pageKey;
|
|
540
|
+
const hit = await timed(timing, "page-cache", () => getCachedPage(cache, key));
|
|
541
|
+
timing?.note("cache", hit ? "hit" : "miss");
|
|
518
542
|
// The cached artifact is nonce-free (carries the placeholder). Stamp a fresh
|
|
519
543
|
// per-response nonce into its body + CSP header so a warm hit is served with a
|
|
520
544
|
// brand-new nonce, never the one an earlier response rendered with.
|
|
@@ -544,7 +568,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
544
568
|
// below still run).
|
|
545
569
|
if (kv && !candidatePin) {
|
|
546
570
|
const channel = channelFromEnv(customization.env);
|
|
547
|
-
await resolveServableChannelVersion(kvGet, tenant.tenant_id, channel, ({ versionId }) => {
|
|
571
|
+
await timed(timing, "channel", () => resolveServableChannelVersion(kvGet, tenant.tenant_id, channel, ({ versionId }) => {
|
|
548
572
|
recordActivity({
|
|
549
573
|
action: "preview.build.requested",
|
|
550
574
|
actor: { kind: "system", id: "storefront" },
|
|
@@ -553,7 +577,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
553
577
|
scope: { tenantId: tenant.tenant_id },
|
|
554
578
|
payload: { args: { changeId: versionId } },
|
|
555
579
|
});
|
|
556
|
-
}).catch(() => {
|
|
580
|
+
})).catch(() => {
|
|
557
581
|
/* fail-open: self-heal detection never breaks the request */
|
|
558
582
|
});
|
|
559
583
|
}
|
|
@@ -570,7 +594,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
570
594
|
draftClaims && draftClaims.tenantId === tenant.tenant_id ? draftClaims : null;
|
|
571
595
|
|
|
572
596
|
let theme = mergeTheme(referenceTheme, tenant.theme_tokens);
|
|
573
|
-
const publishedTheme = await readRuntimeArtifact(themeArtifactPath(tenant.tenant_id));
|
|
597
|
+
const publishedTheme = await timed(timing, "artifacts", () => readRuntimeArtifact(themeArtifactPath(tenant.tenant_id)));
|
|
574
598
|
if (publishedTheme) {
|
|
575
599
|
try {
|
|
576
600
|
theme = mergeTheme(theme, JSON.parse(publishedTheme) as DeepPartial<ThemeTokens>);
|
|
@@ -596,7 +620,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
596
620
|
// commit (localhost=working tree, preview=PR head, live=promoted). So a
|
|
597
621
|
// capability change is PR-gated and separable per environment.
|
|
598
622
|
let publishedCaps: Partial<CapabilityConfig> | null = null;
|
|
599
|
-
const publishedCapsRaw = await readRuntimeArtifact(capabilitiesArtifactPath(tenant.tenant_id));
|
|
623
|
+
const publishedCapsRaw = await timed(timing, "artifacts", () => readRuntimeArtifact(capabilitiesArtifactPath(tenant.tenant_id)));
|
|
600
624
|
if (publishedCapsRaw) {
|
|
601
625
|
try {
|
|
602
626
|
publishedCaps = JSON.parse(publishedCapsRaw) as Partial<CapabilityConfig>;
|
|
@@ -612,7 +636,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
612
636
|
// tenant enables a catalogued embed by committing embeds.json — no platform
|
|
613
637
|
// change — but can never introduce a non-catalog origin.
|
|
614
638
|
let publishedEmbeds: string[] | null = null;
|
|
615
|
-
const publishedEmbedsRaw = await readRuntimeArtifact(embedsArtifactPath(tenant.tenant_id));
|
|
639
|
+
const publishedEmbedsRaw = await timed(timing, "artifacts", () => readRuntimeArtifact(embedsArtifactPath(tenant.tenant_id)));
|
|
616
640
|
if (publishedEmbedsRaw) {
|
|
617
641
|
try {
|
|
618
642
|
const parsed = JSON.parse(publishedEmbedsRaw);
|
|
@@ -648,7 +672,7 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
648
672
|
// loader + signed add-to-cart, and the CSP additions below.
|
|
649
673
|
const commerce =
|
|
650
674
|
locals.features.catalog && capabilities.cartCheckout.enabled
|
|
651
|
-
? await resolveTenantCommerce(tenant)
|
|
675
|
+
? await timed(timing, "commerce", () => resolveTenantCommerce(tenant))
|
|
652
676
|
: null;
|
|
653
677
|
locals.commerce = commerce ?? undefined;
|
|
654
678
|
// Developer-draft content overlay — serves the AI-edited artifacts to the draft
|
|
@@ -666,7 +690,8 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
666
690
|
// are read through the SAME content source just resolved, so they follow the
|
|
667
691
|
// request's selected version exactly as pages and the blog do. Only the control
|
|
668
692
|
// plane supplies the D1 projection; the public branch never reads D1.
|
|
669
|
-
locals.tot = await
|
|
693
|
+
locals.tot = await timed(timing, "catalog", async () =>
|
|
694
|
+
createToTClient(tenant, await input.readCatalogD1?.(), locals.content));
|
|
670
695
|
locals.themeStyle = themeToCssVars(theme);
|
|
671
696
|
locals.canonicalBase = canonicalBase;
|
|
672
697
|
locals.customizationEnv = customization.env;
|
|
@@ -733,9 +758,9 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
733
758
|
// request. Rewriting there discards the route it already chose.
|
|
734
759
|
routePattern: context.routePattern,
|
|
735
760
|
});
|
|
736
|
-
let response = rewriteTarget !== undefined
|
|
737
|
-
?
|
|
738
|
-
:
|
|
761
|
+
let response = await timed(timing, "render", () => rewriteTarget !== undefined
|
|
762
|
+
? next(rewriteTarget)
|
|
763
|
+
: next());
|
|
739
764
|
|
|
740
765
|
// A nested pass of this middleware (an in-process `rewrite()`) already decorated
|
|
741
766
|
// and nonce-stamped the response. Serve it verbatim: decorating again would inject
|
|
@@ -869,7 +894,8 @@ export async function runRenderChain(input: RenderChainInput): Promise<Response>
|
|
|
869
894
|
);
|
|
870
895
|
// Store the nonce-free artifact (the placeholder is still in the body + CSP
|
|
871
896
|
// header) BEFORE stamping — so the cache never holds a live/reusable nonce.
|
|
872
|
-
|
|
897
|
+
const key = pageKey;
|
|
898
|
+
await timed(timing, "page-cache", () => putCachedPage(cache, key, response));
|
|
873
899
|
}
|
|
874
900
|
// MATERIALIZE-ONLY capture (ADR 0013): expose the tenant's resolved route-document
|
|
875
901
|
// Cache-Control so the publish/materialize plane can bake it into the bundle
|
|
@@ -40,6 +40,9 @@ if (
|
|
|
40
40
|
// dir, so the pages live under `/style-guide/…` and we rewrite the underscored URL
|
|
41
41
|
// onto them, under the same tenant prefix. These are noindex + sitemap-excluded and
|
|
42
42
|
// never reskin the live site.
|
|
43
|
+
// Its query, like the `__preview` flag below, only matters to a Worker render: the
|
|
44
|
+
// static copy of a page is the published, non-preview render.
|
|
45
|
+
export const STATIC_QUERY = "render-only";
|
|
43
46
|
const styleGuideTarget = styleGuideAliasRewrite(basePath, slug, Astro.url.search);
|
|
44
47
|
if (styleGuideTarget !== undefined) {
|
|
45
48
|
return Astro.rewrite(styleGuideTarget);
|