@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.
Files changed (80) hide show
  1. package/agency-kit/tools/lib/public-binaries.mjs +95 -0
  2. package/apps/storefront/astro.config.mjs +7 -0
  3. package/apps/storefront/env.d.ts +3 -0
  4. package/apps/storefront/src/components/catalog/storefront/ProductCard.astro +5 -4
  5. package/apps/storefront/src/components/catalog/storefront/commerce/PriceDisplay.astro +2 -1
  6. package/apps/storefront/src/components/catalog/storefront/islands/ImageGallery.tsx +20 -3
  7. package/apps/storefront/src/components/catalog/storefront/islands/SearchTypeahead.tsx +17 -20
  8. package/apps/storefront/src/components/catalog/storefront/islands/VariantSelector.tsx +13 -21
  9. package/apps/storefront/src/components/catalog/storefront/plp/Pagination.astro +9 -5
  10. package/apps/storefront/src/components/checkout/storefront/commerce/CheckoutLoader.astro +18 -6
  11. package/apps/storefront/src/components/compliance/storefront/islands/CheckoutComplianceGate.tsx +19 -4
  12. package/apps/storefront/src/components/content/storefront/Badge.astro +1 -0
  13. package/apps/storefront/src/components/content/storefront/blog/BlogIndexView.astro +10 -1
  14. package/apps/storefront/src/components/publication/storefront/islands/CatalogQueryView.tsx +321 -0
  15. package/apps/storefront/src/config/declaredTenantConfig.ts +59 -27
  16. package/apps/storefront/src/config/devTenantSeed.ts +2 -1
  17. package/apps/storefront/src/layouts/Layout.astro +1 -1
  18. package/apps/storefront/src/lib/access/auth/gatePage.ts +29 -14
  19. package/apps/storefront/src/lib/access/shared/publicAccess.ts +95 -0
  20. package/apps/storefront/src/lib/access/shared/tenantRouting.ts +19 -0
  21. package/apps/storefront/src/lib/catalog/shared/catalogIndex.ts +109 -0
  22. package/apps/storefront/src/lib/catalog/shared/catalogQueryView.ts +60 -0
  23. package/apps/storefront/src/lib/catalog/shared/collectionPaging.ts +78 -0
  24. package/apps/storefront/src/lib/catalog/shared/stockStatus.ts +23 -0
  25. package/apps/storefront/src/lib/catalog/tot/query.ts +8 -0
  26. package/apps/storefront/src/lib/checkout/browser/checkoutRequirementsForm.ts +54 -0
  27. package/apps/storefront/src/lib/checkout/commerce/checkoutHandoffRoute.ts +103 -35
  28. package/apps/storefront/src/lib/checkout/commerce/checkoutRequirements.ts +88 -0
  29. package/apps/storefront/src/lib/checkout/commerce/finalCartAuthorization.ts +2 -0
  30. package/apps/storefront/src/lib/checkout/commerce/sharedStoreAdmission.ts +95 -0
  31. package/apps/storefront/src/lib/checkout/shared/lazyCheckoutLoader.ts +191 -0
  32. package/apps/storefront/src/lib/compliance/browser/checkoutVerificationFlow.ts +9 -0
  33. package/apps/storefront/src/lib/compliance/enforcement.ts +7 -5
  34. package/apps/storefront/src/lib/content/seo/structuredData.ts +12 -3
  35. package/apps/storefront/src/lib/content/storyblok/provider.ts +14 -7
  36. package/apps/storefront/src/lib/observability/shared/serverTiming.ts +110 -0
  37. package/apps/storefront/src/lib/publication/shared/edgeCache.ts +3 -4
  38. package/apps/storefront/src/lib/publication/shared/staticQuery.ts +64 -0
  39. package/apps/storefront/src/middleware/controlPlane.ts +14 -11
  40. package/apps/storefront/src/middleware/index.ts +33 -12
  41. package/apps/storefront/src/middleware/renderChain.ts +52 -26
  42. package/apps/storefront/src/pages/[...slug].astro +3 -0
  43. package/apps/storefront/src/pages/api/commerce/final-cart-authorization.ts +112 -42
  44. package/apps/storefront/src/pages/api/compliance/preflight.ts +61 -13
  45. package/apps/storefront/src/pages/blog/author/[author].astro +1 -1
  46. package/apps/storefront/src/pages/blog/category/[category].astro +1 -1
  47. package/apps/storefront/src/pages/blog/tag/[tag].astro +1 -1
  48. package/apps/storefront/src/pages/catalog-index.json.ts +20 -0
  49. package/apps/storefront/src/pages/collections/[handle]/page/[n].astro +28 -0
  50. package/apps/storefront/src/pages/collections/[handle].astro +13 -198
  51. package/apps/storefront/src/pages/collections/_CollectionView.astro +251 -0
  52. package/apps/storefront/src/pages/collections/index.astro +1 -1
  53. package/apps/storefront/src/pages/products/[handle].astro +18 -8
  54. package/apps/storefront/src/pages/search.astro +33 -33
  55. package/apps/storefront/src/pages/sitemap.xml.ts +7 -1
  56. package/apps/storefront/vitest.config.cts +3 -0
  57. package/package.json +1 -1
  58. package/packages/cli/src/declared-config.mjs +318 -32
  59. package/packages/cli/src/publication-spaces.mjs +1 -1
  60. package/packages/public-runtime/package.json +1 -0
  61. package/packages/public-runtime/schemas/publication-space/v1/manifest.schema.json +1 -1
  62. package/packages/public-runtime/schemas/publication-space/v1/normalized-mount.schema.json +1 -1
  63. package/packages/public-runtime/src/activity/catalog.ts +8 -1
  64. package/packages/public-runtime/src/candidate-index.ts +5 -0
  65. package/packages/public-runtime/src/capabilities.ts +12 -9
  66. package/packages/public-runtime/src/capture-rewrites.ts +687 -0
  67. package/packages/public-runtime/src/checkout-orders.ts +6 -7
  68. package/packages/public-runtime/src/checkout.ts +190 -6
  69. package/packages/public-runtime/src/compliance/evaluate.ts +7 -3
  70. package/packages/public-runtime/src/compliance/profile.ts +90 -14
  71. package/packages/public-runtime/src/compliance-coherence.d.mts +10 -1
  72. package/packages/public-runtime/src/compliance-coherence.mjs +129 -29
  73. package/packages/public-runtime/src/customization-runtime.ts +64 -2
  74. package/packages/public-runtime/src/declared-tenant-config.d.mts +13 -2
  75. package/packages/public-runtime/src/declared-tenant-config.mjs +83 -1
  76. package/packages/public-runtime/src/image-preview-rewrite-overlay.ts +13 -37
  77. package/packages/public-runtime/src/membership.ts +3 -0
  78. package/packages/public-runtime/src/publication-space.ts +1 -0
  79. package/packages/public-runtime/src/tenant.ts +28 -4
  80. 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
- const sid = context.cookies.get(SESSION_COOKIE)?.value ?? null;
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
- if (isApiPath(url.pathname)) return gatedApiResponse(decision.location, request);
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 loadActiveManagedInstallDeclarations(
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: () => readSavedConsentSnapshot(tenant.tenant_id),
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
- return withVisitorRegionCookie(
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 resolver = await getTenantResolver();
162
- const { tenant, basePath, renderPath } = cdn
163
- ? await resolveCdnForwardRouting(cdn, url.pathname, resolver)
164
- : await resolveTenantRouting(host, url.pathname, resolver);
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 decision = await authorizeInternalContentRequest({
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: await readEnv("TOT_CORE_URL"),
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 publicationSource = await resolvePublicationRuntimeSourceV1({
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
- const tenant = await withDeclaredTenantConfig(
269
- tenantRecord,
270
- kv ? (key: string) => kv.get(key) : async () => null,
271
- { env: customization.env, ...(candidatePin ? { pinnedVersionId: candidatePin.versionId } : {}) },
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
- const capturedRevisionId = candidatePin?.versionId ?? cookieRevision;
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, url.searchParams);
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 hit = await getCachedPage(cache, pageKey);
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 createToTClient(tenant, await input.readCatalogD1?.(), locals.content);
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
- ? await next(rewriteTarget)
738
- : await next();
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
- await putCachedPage(cache, pageKey, response);
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);