@tokenoftrust/storefront-runner 3.3.0 → 3.3.1
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/storefront-runner",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.1",
|
|
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
|
-
|
|
508
|
-
|
|
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);
|