@tokenoftrust/storefront-runner 3.3.0 → 3.3.2

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.
@@ -98,7 +98,7 @@ export class StaticTenantResolver implements TenantResolver {
98
98
 
99
99
  export const tenantResolver: TenantResolver = new StaticTenantResolver();
100
100
 
101
- const COMPOSED_PLATFORM_FIELDS = ["access", "pathPrefixTenants", "capabilities"] as const;
101
+ const COMPOSED_PLATFORM_FIELDS = ["access", "pathPrefixTenants", "capabilities", "siteType", "features"] as const;
102
102
 
103
103
  /** Same contract as the real registry's withPlatformPolicy — kept in sync by hand. */
104
104
  export function withPlatformPolicy(record: TenantConfig): TenantConfig {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/storefront-runner",
3
- "version": "3.3.0",
3
+ "version": "3.3.2",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "World-shareable storefront runner: multi-tenant renderer on Astro/Cloudflare. No control plane.",
6
6
  "packageManager": "pnpm@11.9.0",
@@ -0,0 +1,5 @@
1
+ /** Types for the shared (S3 publish + Cloudflare serve) per-page CSP resolution. */
2
+ export declare function pageCspFor(
3
+ manifest: { csp?: unknown; perRouteCsp?: Record<string, unknown> | null } | null | undefined,
4
+ relPath: string,
5
+ ): { headerName: string; value: string } | null;
@@ -0,0 +1,27 @@
1
+ import { NATIVE_GA4_SANDBOX_ASSET_PATH } from "./integration/native-ga4-sandbox-asset.mjs";
2
+
3
+ /** @param {unknown} value */
4
+ function captureOrNull(value) {
5
+ const capture = /** @type {{headerName?: unknown, value?: unknown} | null | undefined} */ (value);
6
+ return capture && typeof capture.headerName === "string" && typeof capture.value === "string"
7
+ ? /** @type {{headerName: string, value: string}} */ (capture)
8
+ : null;
9
+ }
10
+
11
+ /**
12
+ * The tenant CSP one HTML document carries, shared by every plane that applies it: the S3 version
13
+ * upload stamps it as object metadata (`planVersionUpload`) and the Cloudflare static-bundle serve
14
+ * emits it as a response header (`serveStaticBundle`), so the two cannot answer differently.
15
+ *
16
+ * A page's own captured policy (`manifest.perRouteCsp[<bundlePath>]`) wins; the home-route
17
+ * `manifest.csp` is only the fallback for a document with no entry of its own. The platform-owned
18
+ * GA4 sandbox document enforces its own hash-pinned meta CSP and takes none.
19
+ * @param {{csp?: unknown, perRouteCsp?: Record<string, unknown> | null} | null | undefined} manifest
20
+ * @param {string} relPath the bundle-relative path of the document
21
+ * @returns {{headerName: string, value: string} | null}
22
+ */
23
+ export function pageCspFor(manifest, relPath) {
24
+ if (relPath === NATIVE_GA4_SANDBOX_ASSET_PATH) return null;
25
+ const own = manifest?.perRouteCsp ? captureOrNull(manifest.perRouteCsp[relPath]) : null;
26
+ return own ?? captureOrNull(manifest?.csp);
27
+ }
@@ -42,9 +42,9 @@
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";
46
45
  import { CONTENT_NAMED_ASSET_PREFIXES } from "./content-named-asset-prefixes.mjs";
47
46
  import { resolveRedirect } from "./publication-redirects.mjs";
47
+ import { pageCspFor } from "./page-csp.mjs";
48
48
  import { POOL_ASSET_SECURITY_HEADERS } from "./pool-asset-headers.js";
49
49
 
50
50
  // ---------------------------------------------------------------------------
@@ -161,6 +161,8 @@ export interface StaticBundleManifest {
161
161
  treeDigest: string;
162
162
  pinnedNonce: string;
163
163
  csp: { headerName: string; value: string } | null;
164
+ /** Each HTML document's OWN captured CSP, keyed by bundle path; `csp` is the fallback (see `pageCspFor`). */
165
+ perRouteCsp?: Record<string, { headerName: string; value: string }>;
164
166
  /**
165
167
  * The route-document `Cache-Control` this bundle's tenant declared (ADR 0013
166
168
  * `serving.htmlCachePolicy`, formatted by `formatHtmlCacheControl` at materialize).
@@ -222,6 +224,16 @@ function parseManifest(raw: string): StaticBundleManifest | null {
222
224
  typeof (m.csp as { value?: unknown }).value === "string"
223
225
  ? (m.csp as { headerName: string; value: string })
224
226
  : null;
227
+ let perRouteCsp: StaticBundleManifest["perRouteCsp"];
228
+ if (m.perRouteCsp && typeof m.perRouteCsp === "object") {
229
+ perRouteCsp = {};
230
+ for (const [rel, value] of Object.entries(m.perRouteCsp as Record<string, unknown>)) {
231
+ const capture = value as { headerName?: unknown; value?: unknown } | null;
232
+ if (capture && typeof capture.headerName === "string" && typeof capture.value === "string") {
233
+ perRouteCsp[rel] = { headerName: capture.headerName, value: capture.value };
234
+ }
235
+ }
236
+ }
225
237
  let files: StaticBundleManifest["files"];
226
238
  if (m.files && typeof m.files === "object") {
227
239
  files = {};
@@ -253,7 +265,7 @@ function parseManifest(raw: string): StaticBundleManifest | null {
253
265
  }
254
266
  }
255
267
  const htmlCacheControl = typeof m.htmlCacheControl === "string" ? m.htmlCacheControl : undefined;
256
- return { treeDigest: m.treeDigest, pinnedNonce: m.pinnedNonce, csp, htmlCacheControl, files, redirects };
268
+ return { treeDigest: m.treeDigest, pinnedNonce: m.pinnedNonce, csp, perRouteCsp, htmlCacheControl, files, redirects };
257
269
  } catch {
258
270
  return null;
259
271
  }
@@ -504,9 +516,8 @@ export async function serveStaticBundle(
504
516
  // The reviewed GA4 transport frame is a platform-owned security boundary with
505
517
  // its own generated, hash-pinned meta CSP. Applying the tenant page CSP as a
506
518
  // second policy would intersect the two and block the reviewed gtag.js origin.
507
- if (isHtml && rel !== NATIVE_GA4_SANDBOX_ASSET_PATH && manifest.csp) {
508
- headers[manifest.csp.headerName] = manifest.csp.value;
509
- }
519
+ const pageCsp = isHtml ? pageCspFor(manifest, rel) : null;
520
+ if (pageCsp) headers[pageCsp.headerName] = pageCsp.value;
510
521
 
511
522
  if (typeof object.size === "number") {
512
523
  headers["content-length"] = String(object.size);