@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
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { assetContentType } from "./tenant-assets.js";
|
|
44
44
|
import { formatHtmlCacheControl } from "./html-cache-policy.js";
|
|
45
|
+
import { NATIVE_GA4_SANDBOX_ASSET_PATH } from "./integration/native-ga4-sandbox-asset.mjs";
|
|
45
46
|
|
|
46
47
|
// ---------------------------------------------------------------------------
|
|
47
48
|
// R2 seam — same structural subset tenant-assets.ts uses (no workers-types dep)
|
|
@@ -167,6 +168,24 @@ export interface StaticBundleManifest {
|
|
|
167
168
|
htmlCacheControl?: string;
|
|
168
169
|
/** T6 classification: relPath -> {hash, kind}. Absent on a pre-T6 manifest — every lookup then falls back to the old versions/<digest>/<rel> key (see `resolveStaticBundleObjectKey`). */
|
|
169
170
|
files?: Record<string, StaticBundleFileEntry>;
|
|
171
|
+
/** Exact, version-bound whole-site redirects materialized from content/redirects.json. */
|
|
172
|
+
redirects?: Array<{ from: string; to: string }>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function isManifestRedirectPath(value: string): boolean {
|
|
176
|
+
return value.startsWith("/") && !value.startsWith("//") &&
|
|
177
|
+
!value.includes("\\") && !value.includes("?") && !value.includes("#") &&
|
|
178
|
+
!value.split("/").includes("..");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isManifestRedirectDestination(value: string): boolean {
|
|
182
|
+
if (isManifestRedirectPath(value)) return true;
|
|
183
|
+
try {
|
|
184
|
+
const url = new URL(value);
|
|
185
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.search && !url.hash;
|
|
186
|
+
} catch {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
170
189
|
}
|
|
171
190
|
|
|
172
191
|
function parseManifest(raw: string): StaticBundleManifest | null {
|
|
@@ -193,8 +212,20 @@ function parseManifest(raw: string): StaticBundleManifest | null {
|
|
|
193
212
|
}
|
|
194
213
|
}
|
|
195
214
|
}
|
|
215
|
+
let redirects: StaticBundleManifest["redirects"];
|
|
216
|
+
if (m.redirects !== undefined) {
|
|
217
|
+
if (!Array.isArray(m.redirects)) return null;
|
|
218
|
+
redirects = [];
|
|
219
|
+
for (const value of m.redirects) {
|
|
220
|
+
const redirect = value as { from?: unknown; to?: unknown };
|
|
221
|
+
if (!redirect || typeof redirect.from !== "string" || typeof redirect.to !== "string" ||
|
|
222
|
+
!isManifestRedirectPath(redirect.from) ||
|
|
223
|
+
!isManifestRedirectDestination(redirect.to)) return null;
|
|
224
|
+
redirects.push({ from: redirect.from, to: redirect.to });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
196
227
|
const htmlCacheControl = typeof m.htmlCacheControl === "string" ? m.htmlCacheControl : undefined;
|
|
197
|
-
return { treeDigest: m.treeDigest, pinnedNonce: m.pinnedNonce, csp, htmlCacheControl, files };
|
|
228
|
+
return { treeDigest: m.treeDigest, pinnedNonce: m.pinnedNonce, csp, htmlCacheControl, files, redirects };
|
|
198
229
|
} catch {
|
|
199
230
|
return null;
|
|
200
231
|
}
|
|
@@ -260,8 +291,82 @@ function failClosed(deps: ServeStaticBundleDeps, detail: string): Response {
|
|
|
260
291
|
});
|
|
261
292
|
}
|
|
262
293
|
|
|
263
|
-
/** Cache-Control for
|
|
264
|
-
const
|
|
294
|
+
/** Cache-Control for a content-addressed or hash-named object — its URL can never name different bytes. */
|
|
295
|
+
const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Cache-Control for an asset served at its STABLE path (`/shared/x.css`, `/brand/logo.png`). A
|
|
299
|
+
* republish can put different bytes at the same path, so it revalidates — the same tier the S3
|
|
300
|
+
* plane stamps (`s3-publish.mjs` `CACHE_CONTROL.ASSET`). Pages fetch their sub-resources by
|
|
301
|
+
* content address; the stable path serves links, crawlers and runtime-computed URLs.
|
|
302
|
+
*/
|
|
303
|
+
const STABLE_ASSET_CACHE_CONTROL = "public, max-age=86400, stale-while-revalidate=604800";
|
|
304
|
+
|
|
305
|
+
/** Bundle prefixes whose stable paths are still immutable: hash-named build output and path-versioned platform code. */
|
|
306
|
+
const IMMUTABLE_STABLE_PREFIXES = ["_astro/", "platform/"];
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* URL prefix of a content-addressed asset (`/_a/<sha256><ext>`) — kept in lockstep with
|
|
310
|
+
* `scripts/publish/lib/asset-pool.mjs` `PINNED_ASSET_PREFIX` and the CloudFront function
|
|
311
|
+
* (`live-pointer-rewrite.mjs` `versionIndependentAssetUri`).
|
|
312
|
+
*/
|
|
313
|
+
export const PINNED_ASSET_URL_PREFIX = "/_a/";
|
|
314
|
+
|
|
315
|
+
/** URL prefix of hash-named build output, served from the version-independent named store. */
|
|
316
|
+
export const HASH_NAMED_URL_PREFIX = "/_astro/";
|
|
317
|
+
|
|
318
|
+
/** R2 key of a hash-named file in the named store (`asset-pool.mjs` `namedStoreKey`). */
|
|
319
|
+
export function staticBundleNamedStoreKey(rel: string): string {
|
|
320
|
+
return `assets/${rel}`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* The R2 key a version-independent asset URL names, or null for any other path. These URLs
|
|
325
|
+
* resolve WITHOUT the live pointer (ADR 0013, "Sub-resources are version-independent"): a page
|
|
326
|
+
* from an older version keeps fetching its own bytes after the pointer moves.
|
|
327
|
+
* `/_a/<sha256><ext>` → `assets/<sha256><ext>` (the content-addressed pool)
|
|
328
|
+
* `/_astro/<name>` → `assets/_astro/<name>` (the named store)
|
|
329
|
+
*/
|
|
330
|
+
export function versionIndependentAssetKey(pathname: string): string | null {
|
|
331
|
+
if (/^\/_a\/[0-9a-f]{64}(\.[A-Za-z0-9]+)?$/.test(pathname)) return `assets/${pathname.slice(4)}`;
|
|
332
|
+
if (/^\/_astro(\/[A-Za-z0-9._@-]+)+$/.test(pathname) && !pathname.includes("/.")) {
|
|
333
|
+
return staticBundleNamedStoreKey(pathname.slice(1));
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Serve a version-independent asset straight from R2, or return null when `pathname` is not one
|
|
340
|
+
* (or, for `/_astro/`, when the named store does not hold it — the Worker's own build also serves
|
|
341
|
+
* that prefix, so a miss belongs to the caller). A missing `/_a/` object is a 404: the address is
|
|
342
|
+
* exact, so there is nothing else it could mean.
|
|
343
|
+
*/
|
|
344
|
+
export async function serveVersionIndependentAsset(
|
|
345
|
+
bucket: StaticBundleR2BucketLike,
|
|
346
|
+
pathname: string,
|
|
347
|
+
method = "GET",
|
|
348
|
+
): Promise<Response | null> {
|
|
349
|
+
const key = versionIndependentAssetKey(pathname);
|
|
350
|
+
if (!key) return null;
|
|
351
|
+
const object = await bucket.get(key);
|
|
352
|
+
if (!object) {
|
|
353
|
+
if (pathname.startsWith(HASH_NAMED_URL_PREFIX)) return null;
|
|
354
|
+
return new Response("Not found", {
|
|
355
|
+
status: 404,
|
|
356
|
+
headers: { "cache-control": "no-store", "x-tot-static-bundle": "pinned-miss" },
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
const headers: Record<string, string> = {
|
|
360
|
+
"content-type": assetContentType(key),
|
|
361
|
+
"cache-control": IMMUTABLE_CACHE_CONTROL,
|
|
362
|
+
"x-tot-static-bundle": "pinned",
|
|
363
|
+
};
|
|
364
|
+
if (typeof object.size === "number") headers["content-length"] = String(object.size);
|
|
365
|
+
const body: BodyInit | null = method.toUpperCase() === "HEAD"
|
|
366
|
+
? null
|
|
367
|
+
: object.body ?? (await object.arrayBuffer());
|
|
368
|
+
return new Response(body, { status: 200, headers });
|
|
369
|
+
}
|
|
265
370
|
|
|
266
371
|
/**
|
|
267
372
|
* Serve `pathname` from the digest-addressed static bundle in R2.
|
|
@@ -292,15 +397,20 @@ const ASSET_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
|
292
397
|
* manifest's file `kind` (long-lived immutable for the shared `asset` pool —
|
|
293
398
|
* safe because the key itself is the content hash; short/revalidating for a
|
|
294
399
|
* `route` doc, rendered fresh per version at a STABLE, non-digest url) and,
|
|
295
|
-
* for
|
|
296
|
-
* same manifest read — never the Worker's per-request nonce middleware,
|
|
297
|
-
* the ADR's CSP-consequences note.
|
|
400
|
+
* for a tenant HTML response, the bundle's PINNED CSP header read back from
|
|
401
|
+
* the same manifest read — never the Worker's per-request nonce middleware,
|
|
402
|
+
* per the ADR's CSP-consequences note. The platform-owned GA4 sandbox is the
|
|
403
|
+
* exact-path exception because its generated document owns a hash-pinned meta CSP.
|
|
298
404
|
*/
|
|
299
405
|
export async function serveStaticBundle(
|
|
300
406
|
deps: ServeStaticBundleDeps,
|
|
301
407
|
pathname: string,
|
|
408
|
+
search = "",
|
|
409
|
+
method = "GET",
|
|
302
410
|
): Promise<Response | null> {
|
|
303
411
|
const { bucket, digest } = deps;
|
|
412
|
+
const pinned = await serveVersionIndependentAsset(bucket, pathname, method);
|
|
413
|
+
if (pinned) return pinned;
|
|
304
414
|
if (!digest) {
|
|
305
415
|
return failClosed(deps, "pointer-serving tenant has no resolved live digest");
|
|
306
416
|
}
|
|
@@ -315,6 +425,18 @@ export async function serveStaticBundle(
|
|
|
315
425
|
return failClosed(deps, `missing/unreadable manifest for digest ${digest}`);
|
|
316
426
|
}
|
|
317
427
|
|
|
428
|
+
const redirectPath = pathname === "/" || pathname.split("/").pop()?.includes(".")
|
|
429
|
+
? pathname
|
|
430
|
+
: pathname.endsWith("/") ? pathname : `${pathname}/`;
|
|
431
|
+
const redirect = manifest.redirects?.find(({ from }) => from === pathname || from === redirectPath);
|
|
432
|
+
if (redirect) {
|
|
433
|
+
const query = redirect.to.startsWith("/") ? search : "";
|
|
434
|
+
return new Response(null, {
|
|
435
|
+
status: 308,
|
|
436
|
+
headers: { location: `${redirect.to}${query}`, "cache-control": "public, max-age=300" },
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
|
|
318
440
|
const entry = manifest.files?.[rel];
|
|
319
441
|
if (manifest.files && !entry) {
|
|
320
442
|
// Provably not in this bundle's static surface — an app surface (/admin,
|
|
@@ -333,38 +455,35 @@ export async function serveStaticBundle(
|
|
|
333
455
|
// shared DEFAULT_HTML_CACHE_POLICY string — the SAME source the S3 plane reads, so
|
|
334
456
|
// both planes emit a byte-identical header.
|
|
335
457
|
const routeCacheControl = manifest.htmlCacheControl ?? formatHtmlCacheControl();
|
|
336
|
-
// Cache tier:
|
|
337
|
-
//
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
? ASSET_CACHE_CONTROL
|
|
344
|
-
: routeCacheControl
|
|
345
|
-
: isHtml
|
|
346
|
-
? routeCacheControl
|
|
347
|
-
: ASSET_CACHE_CONTROL;
|
|
458
|
+
// Cache tier: route documents take the tenant's policy; an asset at its stable path
|
|
459
|
+
// revalidates unless its path itself is immutable (hash-named or path-versioned).
|
|
460
|
+
const cacheControl = isHtml || entry?.kind === "route"
|
|
461
|
+
? routeCacheControl
|
|
462
|
+
: IMMUTABLE_STABLE_PREFIXES.some((prefix) => rel.startsWith(prefix))
|
|
463
|
+
? IMMUTABLE_CACHE_CONTROL
|
|
464
|
+
: STABLE_ASSET_CACHE_CONTROL;
|
|
348
465
|
const headers: Record<string, string> = {
|
|
349
466
|
"content-type": assetContentType(rel),
|
|
350
|
-
// Cache-key-by-digest lives in the caller's edge-cache layer
|
|
351
|
-
//
|
|
352
|
-
//
|
|
353
|
-
// old one); `asset` keys are ADDITIONALLY digest-independent (content-
|
|
354
|
-
// addressed by hash), so they're safe to cache forever downstream too.
|
|
355
|
-
// This header governs downstream/browser caching of the STABLE URL:
|
|
467
|
+
// Cache-key-by-digest lives in the caller's edge-cache layer (a pointer flip
|
|
468
|
+
// is a clean miss on the NEW digest's keys). This header governs downstream/
|
|
469
|
+
// browser caching of the STABLE URL:
|
|
356
470
|
"cache-control": cacheControl,
|
|
357
471
|
"x-tot-static-bundle": "hit",
|
|
358
472
|
"x-tot-static-bundle-digest": digest,
|
|
359
473
|
};
|
|
360
474
|
|
|
361
|
-
|
|
475
|
+
// The reviewed GA4 transport frame is a platform-owned security boundary with
|
|
476
|
+
// its own generated, hash-pinned meta CSP. Applying the tenant page CSP as a
|
|
477
|
+
// second policy would intersect the two and block the reviewed gtag.js origin.
|
|
478
|
+
if (isHtml && rel !== NATIVE_GA4_SANDBOX_ASSET_PATH && manifest.csp) {
|
|
362
479
|
headers[manifest.csp.headerName] = manifest.csp.value;
|
|
363
480
|
}
|
|
364
481
|
|
|
365
482
|
if (typeof object.size === "number") {
|
|
366
483
|
headers["content-length"] = String(object.size);
|
|
367
484
|
}
|
|
368
|
-
const body: BodyInit
|
|
485
|
+
const body: BodyInit | null = method.toUpperCase() === "HEAD"
|
|
486
|
+
? null
|
|
487
|
+
: object.body ?? (await object.arrayBuffer());
|
|
369
488
|
return new Response(body, { status: 200, headers });
|
|
370
489
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build-time embed opt-in defaults, keyed by tenant id.
|
|
3
|
+
*
|
|
4
|
+
* Plain JS ON PURPOSE — the same reason as embed-catalog.mjs: this data is read
|
|
5
|
+
* both by TypeScript (`tenants.ts`, which spreads it into each TenantConfig) AND
|
|
6
|
+
* by plain-`node` tooling that cannot import TS. The static-publish pipeline
|
|
7
|
+
* (scripts/publish/lib/tenant-csp-profile.mjs) builds a CloudFront CSP off the
|
|
8
|
+
* tenant's opted-in embeds, and a CloudFront policy is applied by a dependency-light
|
|
9
|
+
* script with no TS toolchain. Keeping the declaration HERE gives both readers ONE
|
|
10
|
+
* copy: `tenants.ts` is the only consumer that turns it into a TenantConfig field,
|
|
11
|
+
* and no second constant exists to drift from it.
|
|
12
|
+
*
|
|
13
|
+
* This is the PLATFORM default only. A tenant's effective opt-in is this unioned
|
|
14
|
+
* with the tenant's own published `embeds.json` and filtered to the vetted catalog
|
|
15
|
+
* — see `resolveEffectiveEmbeds` (csp.ts), which every reader goes through. A
|
|
16
|
+
* registered tenant self-serves entirely through `embeds.json` and needs no entry
|
|
17
|
+
* here; an entry exists only for a tenant whose opt-in predates (or is independent
|
|
18
|
+
* of) its own content repo.
|
|
19
|
+
*
|
|
20
|
+
* Slugs must exist in EMBED_PROVIDERS — the platform owns each provider's exact
|
|
21
|
+
* origins, so a tenant lists a slug and never a raw host. An unknown slug is
|
|
22
|
+
* dropped by `resolveEffectiveEmbeds` (fail-safe) and flagged by the embeds lint.
|
|
23
|
+
*
|
|
24
|
+
* @type {Readonly<Record<string, readonly string[]>>}
|
|
25
|
+
*/
|
|
26
|
+
export const TENANT_EMBED_DEFAULTS = {
|
|
27
|
+
// The corporate site's /contact/contact-sales/ page embeds a Pipedrive Web Form.
|
|
28
|
+
"tokenoftrust.com": ["pipedrive"],
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* This tenant's build-time embed defaults, or `[]` when it declares none. Always
|
|
33
|
+
* returns a fresh array so a caller can't mutate the shared declaration.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} tenantId
|
|
36
|
+
* @returns {string[]}
|
|
37
|
+
*/
|
|
38
|
+
export function tenantEmbedDefaults(tenantId) {
|
|
39
|
+
return [...(TENANT_EMBED_DEFAULTS[tenantId] ?? [])];
|
|
40
|
+
}
|
|
@@ -83,6 +83,20 @@ export interface ThemeColorTokens {
|
|
|
83
83
|
"field-border"?: string;
|
|
84
84
|
/** Review/rating star fill (trust/UGC surfaces). Default: `accent`. */
|
|
85
85
|
star?: string;
|
|
86
|
+
|
|
87
|
+
// --- Conversion-role aliases. These name the JOB a color does on a
|
|
88
|
+
// conversion surface rather than its place in the palette, so a tenant can
|
|
89
|
+
// move the buy button without dragging nav chrome, headings and
|
|
90
|
+
// ::selection (all of which read `primary`) along with it. ---
|
|
91
|
+
/** Primary call-to-action fill (buy button, hero CTA). Default: `primary`. */
|
|
92
|
+
cta?: string;
|
|
93
|
+
/** Hover/active shade of `cta`. Default: `primary-hover`. */
|
|
94
|
+
"cta-hover"?: string;
|
|
95
|
+
/** Readable text/icon color ON `cta`. Default: `primary-contrast`. */
|
|
96
|
+
"cta-contrast"?: string;
|
|
97
|
+
/** Trust / reassurance accent — guarantee callouts, security and compliance
|
|
98
|
+
* badges, proof strips. Default: `success`. */
|
|
99
|
+
trust?: string;
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export interface ThemeFontFamily {
|
|
@@ -140,8 +154,12 @@ export interface ThemeBrandTokens {
|
|
|
140
154
|
* background with the brand color used only as an accent on the headline/CTA —
|
|
141
155
|
* matches merchants whose own site is a light page with color accents. The
|
|
142
156
|
* brand adopter sets "light" when it captures a real hero image.
|
|
157
|
+
*
|
|
158
|
+
* "banner" renders a merchant-supplied wide hero asset edge-to-edge. It is
|
|
159
|
+
* intended for visual-parity migrations where the source artwork already
|
|
160
|
+
* contains the campaign copy and CTA treatment.
|
|
143
161
|
*/
|
|
144
|
-
heroVariant?: "bold" | "light";
|
|
162
|
+
heroVariant?: "bold" | "light" | "banner";
|
|
145
163
|
}
|
|
146
164
|
|
|
147
165
|
export interface ThemeTokens {
|
|
@@ -388,6 +406,17 @@ export function siteTypeDefaults(siteType: SiteType): TenantFeatures {
|
|
|
388
406
|
return { ...(siteType === "marketing" ? MARKETING_FEATURES : COMMERCE_FEATURES) };
|
|
389
407
|
}
|
|
390
408
|
|
|
409
|
+
/**
|
|
410
|
+
* Does this tenant render publicly on a login-gated host? Deny-closed: only an
|
|
411
|
+
* explicit `access.public` declaration exempts a tenant from the gate, so an
|
|
412
|
+
* undeclared tenant stays behind sign-in.
|
|
413
|
+
*/
|
|
414
|
+
export function tenantRendersPublicly(
|
|
415
|
+
tenant: Pick<TenantConfig, "access">,
|
|
416
|
+
): boolean {
|
|
417
|
+
return tenant.access?.public === true;
|
|
418
|
+
}
|
|
419
|
+
|
|
391
420
|
/**
|
|
392
421
|
* Resolve a tenant's effective features: siteType defaults, with any explicit
|
|
393
422
|
* `features` overrides layered on top (undefined entries ignored, so a tenant
|
|
@@ -434,6 +463,35 @@ export interface TenantConfig {
|
|
|
434
463
|
siteType?: SiteType;
|
|
435
464
|
/** Explicit feature overrides layered atop the siteType defaults. */
|
|
436
465
|
features?: Partial<TenantFeatures>;
|
|
466
|
+
/**
|
|
467
|
+
* Public-access declaration read by the login gate. A tenant on a gated host
|
|
468
|
+
* (`DEMO_GATE_HOSTS`) is withheld behind sign-in unless it declares itself
|
|
469
|
+
* public here. Omitted == gated, so a new tenant is private by default and
|
|
470
|
+
* going public is an explicit, reviewable line in the tenant's config —
|
|
471
|
+
* never a tenant id the gate recognizes.
|
|
472
|
+
*/
|
|
473
|
+
access?: {
|
|
474
|
+
/**
|
|
475
|
+
* Render publicly on a gated host: no Coming-Soon interstitial and no
|
|
476
|
+
* sign-in requirement, so anyone can browse and sign up.
|
|
477
|
+
*/
|
|
478
|
+
public?: boolean;
|
|
479
|
+
};
|
|
480
|
+
/**
|
|
481
|
+
* This tenant's host ALSO dispatches other tenants under a `/<domain>/*` path
|
|
482
|
+
* prefix, while still serving its own content at the root. Declared by the
|
|
483
|
+
* platform's shared preview host, which is reached as `<host>/<tenant-domain>/…`
|
|
484
|
+
* by the dev loop, the admin preview links and the post-deploy chrome canary.
|
|
485
|
+
*
|
|
486
|
+
* Routing is otherwise host-first: a host owning a tenant serves only that
|
|
487
|
+
* tenant, so `/<domain>/*` is an ordinary path on its own site. That default is
|
|
488
|
+
* load-bearing — without it any merchant's hostname could serve a different
|
|
489
|
+
* tenant's storefront under a path — so this stays opt-in and platform-declared
|
|
490
|
+
* (ADR 0035), never inferred and never a tenant's to set.
|
|
491
|
+
*
|
|
492
|
+
* Omitted == the host serves only its own tenant.
|
|
493
|
+
*/
|
|
494
|
+
pathPrefixTenants?: boolean;
|
|
437
495
|
/**
|
|
438
496
|
* Third-party embeds this tenant is allowed to render, as provider slugs from
|
|
439
497
|
* the platform's `EMBED_PROVIDERS` catalog (e.g. `["pipedrive"]`) — NOT raw
|
package/pnpm-lock.runner.yaml
CHANGED
|
@@ -7,6 +7,10 @@ settings:
|
|
|
7
7
|
importers:
|
|
8
8
|
|
|
9
9
|
.:
|
|
10
|
+
dependencies:
|
|
11
|
+
sharp:
|
|
12
|
+
specifier: ^0.34.5
|
|
13
|
+
version: 0.34.5
|
|
10
14
|
devDependencies:
|
|
11
15
|
typescript:
|
|
12
16
|
specifier: ^5.6.3
|
|
@@ -20,6 +24,9 @@ importers:
|
|
|
20
24
|
'@astrojs/preact':
|
|
21
25
|
specifier: ^6.0.0
|
|
22
26
|
version: 6.0.0(@babel/core@7.29.7)(@types/node@24.13.3)(esbuild@0.28.1)(jiti@2.7.0)(preact@10.29.2)(rollup@4.62.2)(tsx@4.22.4)(yaml@2.9.0)
|
|
27
|
+
'@fontsource/anuphan':
|
|
28
|
+
specifier: 5.3.0
|
|
29
|
+
version: 5.3.0
|
|
23
30
|
'@fontsource/inter':
|
|
24
31
|
specifier: 5.3.0
|
|
25
32
|
version: 5.3.0
|
|
@@ -104,6 +111,13 @@ importers:
|
|
|
104
111
|
version: 4.105.0
|
|
105
112
|
|
|
106
113
|
packages/public-runtime:
|
|
114
|
+
dependencies:
|
|
115
|
+
lightningcss:
|
|
116
|
+
specifier: ^1.32.0
|
|
117
|
+
version: 1.32.0
|
|
118
|
+
parse5:
|
|
119
|
+
specifier: ^7.2.1
|
|
120
|
+
version: 7.3.0
|
|
107
121
|
devDependencies:
|
|
108
122
|
ajv:
|
|
109
123
|
specifier: ^8.20.0
|
|
@@ -1109,6 +1123,9 @@ packages:
|
|
|
1109
1123
|
'@floating-ui/utils@0.2.11':
|
|
1110
1124
|
resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==}
|
|
1111
1125
|
|
|
1126
|
+
'@fontsource/anuphan@5.3.0':
|
|
1127
|
+
resolution: {integrity: sha512-1vWJrY2shVTVE4Bv1BheXKQKWI4DxNSWF64++zt7wbRuYef8QV1tJItvHHxN6+1fknwanIM8RKsXiR0RuHPlhA==}
|
|
1128
|
+
|
|
1112
1129
|
'@fontsource/inter@5.3.0':
|
|
1113
1130
|
resolution: {integrity: sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==}
|
|
1114
1131
|
|
|
@@ -4524,6 +4541,8 @@ snapshots:
|
|
|
4524
4541
|
|
|
4525
4542
|
'@floating-ui/utils@0.2.11': {}
|
|
4526
4543
|
|
|
4544
|
+
'@fontsource/anuphan@5.3.0': {}
|
|
4545
|
+
|
|
4527
4546
|
'@fontsource/inter@5.3.0': {}
|
|
4528
4547
|
|
|
4529
4548
|
'@fontsource/jetbrains-mono@5.3.0': {}
|
package/pnpm-workspace.yaml
CHANGED
|
@@ -85,7 +85,7 @@ export function classify(statusMap, relPathPosix) {
|
|
|
85
85
|
*/
|
|
86
86
|
export function fileStatus(gitCwd, relPath) {
|
|
87
87
|
const posix = relPath.split(sep).join("/");
|
|
88
|
-
const res = git(gitCwd, ["status", "--porcelain", "--", posix]);
|
|
88
|
+
const res = git(gitCwd, ["status", "--porcelain", "--untracked-files=all", "--", posix]);
|
|
89
89
|
if (!res.ok) return "unknown";
|
|
90
90
|
const line = res.stdout.split("\n").find((l) => l.length >= 2);
|
|
91
91
|
if (!line) return "unchanged";
|
package/scripts/dev/publish.mjs
CHANGED
|
@@ -117,6 +117,12 @@ export async function publishFile(env, sourceRoot, tenantId, relPath, deps = {})
|
|
|
117
117
|
const changeId = deriveChangeId(tenantId, session.email || session.token);
|
|
118
118
|
|
|
119
119
|
try {
|
|
120
|
+
// Bind the active tenant before the candidate call, the same way `tot submit` does.
|
|
121
|
+
// candidate_open confines the target repo to the tenants the session holds, so an
|
|
122
|
+
// unbound session refuses with a cross-tenant error that reads as a permission
|
|
123
|
+
// problem rather than a missing bind. An invited developer's session already holds
|
|
124
|
+
// this tenant, so this is the explicit form of a precondition we were relying on.
|
|
125
|
+
await client.callTool("client_switch", { tenant: tenantId });
|
|
120
126
|
const result = await client.callTool("candidate_open", {
|
|
121
127
|
repo,
|
|
122
128
|
changeId,
|
|
@@ -57,13 +57,15 @@ export function copyTreeSkippingUnreadable(from, to, deps) {
|
|
|
57
57
|
const childRel = rel ? join(rel, entry.name) : entry.name;
|
|
58
58
|
const childSrc = join(src, entry.name);
|
|
59
59
|
const childDst = join(dst, entry.name);
|
|
60
|
+
if (entry.isSymbolicLink()) {
|
|
61
|
+
skipped.push({ path: childRel, reason: "SYMLINK" });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
60
64
|
if (entry.isDirectory()) {
|
|
61
65
|
walk(childSrc, childDst, childRel);
|
|
62
66
|
continue;
|
|
63
67
|
}
|
|
64
68
|
try {
|
|
65
|
-
// copyFile follows symlinks, so a DANGLING link throws here and is skipped —
|
|
66
|
-
// which is the common shape of "referenced but missing".
|
|
67
69
|
copyFile(childSrc, childDst);
|
|
68
70
|
copied++;
|
|
69
71
|
} catch (err) {
|
|
@@ -133,6 +133,7 @@ export function resolveTargetPath(root, tenant, repoPath) {
|
|
|
133
133
|
function canonicalMappings(tenant) {
|
|
134
134
|
return [
|
|
135
135
|
{ workspace: "theme.json", repo: `tenants/${tenant}/theme.json`, kind: "file" },
|
|
136
|
+
{ workspace: "analytics.json", repo: `tenants/${tenant}/analytics.json`, kind: "file" },
|
|
136
137
|
{ workspace: "content/", repo: `tenants/${tenant}/content/`, kind: "tree" },
|
|
137
138
|
{ workspace: "public/", repo: `tenants/${tenant}/public/`, kind: "tree" },
|
|
138
139
|
];
|