@salesforce/storefront-next-runtime 0.4.2 → 1.0.0-alpha.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.
Files changed (87) hide show
  1. package/README.md +9 -3
  2. package/dist/ComponentContext.js +199 -4
  3. package/dist/ComponentContext.js.map +1 -1
  4. package/dist/DesignComponent.js +2 -2
  5. package/dist/DesignRegion.js +2 -2
  6. package/dist/RegionContext.js +9 -0
  7. package/dist/RegionContext.js.map +1 -0
  8. package/dist/component.types.d.ts +1 -1
  9. package/dist/config.d.ts +34 -221
  10. package/dist/config.d.ts.map +1 -1
  11. package/dist/config.js +35 -116
  12. package/dist/config.js.map +1 -1
  13. package/dist/data-store.d.ts +185 -15
  14. package/dist/data-store.d.ts.map +1 -1
  15. package/dist/data-store.js +412 -10
  16. package/dist/data-store.js.map +1 -1
  17. package/dist/defaults.d.ts +106 -0
  18. package/dist/defaults.d.ts.map +1 -0
  19. package/dist/defaults.js +67 -0
  20. package/dist/defaults.js.map +1 -0
  21. package/dist/design-data.d.ts +238 -356
  22. package/dist/design-data.d.ts.map +1 -1
  23. package/dist/design-data.js +459 -30
  24. package/dist/design-data.js.map +1 -1
  25. package/dist/design-mode.d.ts +3 -2
  26. package/dist/design-mode.d.ts.map +1 -1
  27. package/dist/design-react-core.d.ts +5 -15
  28. package/dist/design-react-core.d.ts.map +1 -1
  29. package/dist/design-react-core.js +2 -2
  30. package/dist/design-react.d.ts +2 -2
  31. package/dist/design.d.ts +2 -2
  32. package/dist/events.d.ts +32 -6
  33. package/dist/events.d.ts.map +1 -1
  34. package/dist/i18n-client.d.ts.map +1 -1
  35. package/dist/i18n-client.js.map +1 -1
  36. package/dist/i18n.d.ts +1 -2
  37. package/dist/i18n.d.ts.map +1 -1
  38. package/dist/modeDetection.js +0 -18
  39. package/dist/modeDetection.js.map +1 -1
  40. package/dist/scapi.d.ts +2185 -466
  41. package/dist/scapi.d.ts.map +1 -1
  42. package/dist/scapi.js +1 -1
  43. package/dist/scapi.js.map +1 -1
  44. package/dist/schema.d.ts +17 -15
  45. package/dist/schema.d.ts.map +1 -1
  46. package/dist/security-react.d.ts +34 -0
  47. package/dist/security-react.d.ts.map +1 -0
  48. package/dist/security-react.js +21 -0
  49. package/dist/security-react.js.map +1 -0
  50. package/dist/security.d.ts +61 -0
  51. package/dist/security.d.ts.map +1 -0
  52. package/dist/security.js +304 -0
  53. package/dist/security.js.map +1 -0
  54. package/dist/site-context.d.ts +43 -27
  55. package/dist/site-context.d.ts.map +1 -1
  56. package/dist/site-context.js +2 -2
  57. package/dist/site-context2.js +41 -31
  58. package/dist/site-context2.js.map +1 -1
  59. package/dist/types.d.ts +19 -3
  60. package/dist/types.d.ts.map +1 -1
  61. package/dist/types2.d.ts +89 -63
  62. package/dist/types2.d.ts.map +1 -1
  63. package/dist/types3.d.ts +1 -35
  64. package/dist/types3.d.ts.map +1 -1
  65. package/package.json +15 -20
  66. package/dist/DesignFrame.js +0 -204
  67. package/dist/DesignFrame.js.map +0 -1
  68. package/dist/custom-global-preferences.d.ts +0 -20
  69. package/dist/custom-global-preferences.d.ts.map +0 -1
  70. package/dist/custom-global-preferences.js +0 -31
  71. package/dist/custom-global-preferences.js.map +0 -1
  72. package/dist/custom-site-preferences.d.ts +0 -20
  73. package/dist/custom-site-preferences.d.ts.map +0 -1
  74. package/dist/custom-site-preferences.js +0 -31
  75. package/dist/custom-site-preferences.js.map +0 -1
  76. package/dist/data-store-custom-global-preferences.d.ts +0 -2
  77. package/dist/data-store-custom-global-preferences.js +0 -6
  78. package/dist/data-store-custom-site-preferences.d.ts +0 -2
  79. package/dist/data-store-custom-site-preferences.js +0 -6
  80. package/dist/data-store-gcp-preferences.d.ts +0 -2
  81. package/dist/data-store-gcp-preferences.js +0 -6
  82. package/dist/gcp-preferences.d.ts +0 -52
  83. package/dist/gcp-preferences.d.ts.map +0 -1
  84. package/dist/gcp-preferences.js +0 -64
  85. package/dist/gcp-preferences.js.map +0 -1
  86. package/dist/utils.js +0 -90
  87. package/dist/utils.js.map +0 -1
package/dist/schema.d.ts CHANGED
@@ -4,17 +4,16 @@
4
4
  *
5
5
  * Generic parameter `App` represents the template's application config shape.
6
6
  * The SDK does not prescribe what fields `app` must contain — templates define
7
- * their own `AppConfig` type with SCAPI credentials, pages, features, etc.
8
- * and pass it as `BaseConfig<AppConfig>`.
7
+ * their own `AppConfig` type and pass it as `BaseConfig<AppConfig>`.
9
8
  *
10
- * The SDK accesses specific `app` fields (e.g., `commerce.api.clientId`) at
11
- * runtime via the middleware validation, not via compile-time type constraints.
9
+ * Validation of `app` (e.g., required credentials, required collections) is
10
+ * the template's responsibility, typically handled in its server middleware.
12
11
  *
13
12
  * @typeParam App - The template's application config shape (defaults to `Record<string, unknown>`)
14
13
  *
15
14
  * @example
16
15
  * // In the template's types file:
17
- * type AppConfig = { commerce: { api: {...} }; pages: {...}; features: {...} };
16
+ * type AppConfig = { ... };
18
17
  * type Config = BaseConfig<AppConfig>;
19
18
  *
20
19
  * // In config.server.ts:
@@ -38,23 +37,27 @@ interface DefineConfigOptions {
38
37
  /**
39
38
  * Config paths that cannot be overridden by environment variables.
40
39
  * Paths use double underscore separators and are matched case-insensitively.
40
+ * Any env var targeting a protected path or a sub-path of it will throw.
41
41
  *
42
- * @example ['app__engagement'] — prevents PUBLIC__app__engagement__* from being set via env
42
+ * @example ['app__analytics'] — prevents PUBLIC__app__analytics__* from being set via env
43
43
  */
44
44
  protectedPaths?: string[];
45
45
  }
46
46
  /**
47
47
  * Define a type-safe storefront configuration with IDE autocomplete.
48
48
  *
49
- * Automatically merges `PUBLIC__` prefixed environment variables into the config
50
- * at load time. Validates env vars against the base config structure (strict mode
51
- * only allows overriding existing paths).
49
+ * Reads `process.env` at call time and merges any `PUBLIC__`-prefixed
50
+ * variables into the config (validated against the base config structure —
51
+ * env vars targeting paths that don't exist in the base config are ignored
52
+ * with a warning). This is a server-only side effect by design; calling
53
+ * `defineConfig` from a browser bundle silently no-ops because `PUBLIC__`
54
+ * vars are not present in the client environment.
52
55
  *
53
56
  * Environment variables:
54
57
  * - `PUBLIC__<path>` (optional): Override any config path using double underscore separators.
55
- * e.g. `PUBLIC__app__commerce__api__clientId=abc123` maps to `config.app.commerce.api.clientId`
56
- * - `PUBLIC__app__pages__cart__quantityUpdateDebounce=1000` maps to a number (optimistic JSON parsing)
57
- * - `PUBLIC__app__features__socialLogin__providers=["Apple","Google"]` maps to an array
58
+ * e.g. `PUBLIC__app__some__nested__value=abc123` maps to `config.app.some.nested.value`
59
+ * - JSON values are parsed optimistically: numbers, booleans, arrays, and objects all work.
60
+ * `PUBLIC__app__features__providers=["A","B"]` parses to an array.
58
61
  *
59
62
  * @param config - The base configuration object with all defaults
60
63
  * @param options - Optional settings (e.g., protectedPaths to prevent env var overrides)
@@ -67,10 +70,9 @@ interface DefineConfigOptions {
67
70
  * export default defineConfig({
68
71
  * metadata: { projectName: 'My Store', projectSlug: 'my-store' },
69
72
  * app: {
70
- * commerce: { api: { clientId: '', organizationId: '', shortCode: '' }, sites: [] },
71
- * defaultSiteId: 'RefArch',
73
+ * // template-specific shape
72
74
  * },
73
- * }, { protectedPaths: ['app__engagement'] });
75
+ * }, { protectedPaths: ['app__analytics'] });
74
76
  */
75
77
  declare function defineConfig<T extends BaseConfig>(config: T, options?: DefineConfigOptions): T;
76
78
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","names":[],"sources":["../src/config/schema.ts"],"sourcesContent":[],"mappings":";;AAsCA;;;;;;AAeA;AAuCA;;;;;;;;;;;;;KAtDY,uBAAuB,0BAA0B;;;;;;;;;;oBAUrC;;OAEf;;UAGQ,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAuCD,uBAAuB,oBAAoB,aAAa,sBAAsB"}
1
+ {"version":3,"file":"schema.d.ts","names":[],"sources":["../src/config/schema.ts"],"sourcesContent":[],"mappings":";;AAqCA;;;;;;AAeA;AA0CA;;;;;;;;;;;;KAzDY,uBAAuB,0BAA0B;;;;;;;;;;oBAUrC;;OAEf;;UAGQ,mBAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA0CD,uBAAuB,oBAAoB,aAAa,sBAAsB"}
@@ -0,0 +1,34 @@
1
+ import * as react0 from "react";
2
+
3
+ //#region src/security/nonce-context.d.ts
4
+
5
+ /**
6
+ * Copyright 2026 Salesforce, Inc.
7
+ *
8
+ * Licensed under the Apache License, Version 2.0 (the "License");
9
+ * you may not use this file except in compliance with the License.
10
+ * You may obtain a copy of the License at
11
+ *
12
+ * http://www.apache.org/licenses/LICENSE-2.0
13
+ *
14
+ * Unless required by applicable law or agreed to in writing, software
15
+ * distributed under the License is distributed on an "AS IS" BASIS,
16
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17
+ * See the License for the specific language governing permissions and
18
+ * limitations under the License.
19
+ */
20
+ /** React Context carrying the per-request CSP nonce through the SSR React tree. */
21
+ declare const NonceContext: react0.Context<string | undefined>;
22
+ /**
23
+ * React component-side reader for the per-request nonce.
24
+ *
25
+ * Returns `undefined` if no `NonceContext.Provider` is in the tree
26
+ * (e.g. in tests, or a customer who hasn't ejected `entry.server.tsx`).
27
+ * Callers should coerce `undefined` to omit the `nonce` attribute on
28
+ * rendered `<script>` tags (React 19 omits attributes whose value is
29
+ * `undefined`).
30
+ */
31
+ declare function useSecurityNonceFromContext(): string | undefined;
32
+ //#endregion
33
+ export { NonceContext, useSecurityNonceFromContext };
34
+ //# sourceMappingURL=security-react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-react.d.ts","names":[],"sources":["../src/security/nonce-context.tsx"],"sourcesContent":[],"mappings":";;;;;;;AAwCA;AAWA;;;;;;;;;;;;cAXa,cAA2D,MAAA,CAA/C;;;;;;;;;;iBAWT,2BAAA,CAAA"}
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext } from "react";
2
+
3
+ //#region src/security/nonce-context.tsx
4
+ /** React Context carrying the per-request CSP nonce through the SSR React tree. */
5
+ const NonceContext = createContext(void 0);
6
+ /**
7
+ * React component-side reader for the per-request nonce.
8
+ *
9
+ * Returns `undefined` if no `NonceContext.Provider` is in the tree
10
+ * (e.g. in tests, or a customer who hasn't ejected `entry.server.tsx`).
11
+ * Callers should coerce `undefined` to omit the `nonce` attribute on
12
+ * rendered `<script>` tags (React 19 omits attributes whose value is
13
+ * `undefined`).
14
+ */
15
+ function useSecurityNonceFromContext() {
16
+ return useContext(NonceContext);
17
+ }
18
+
19
+ //#endregion
20
+ export { NonceContext, useSecurityNonceFromContext };
21
+ //# sourceMappingURL=security-react.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security-react.js","names":[],"sources":["../src/security/nonce-context.tsx"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/**\n * Client-safe React Context for the per-request CSP nonce.\n *\n * Lives in its own file (separate from `nonce.ts`) because `nonce.ts`\n * imports `node:crypto` for `generateNonce()`. Components in the React\n * tree (rendered on both server and client) need to read the nonce\n * without dragging Node-only modules into the client bundle.\n *\n * Why a React Context (not just the route loader's return value): when\n * the root loader throws, `useRouteLoaderData('root')` returns\n * `undefined`, so any nonce surfaced through it is lost on the error\n * path — `Layout` and `ErrorBoundary` would then render inline\n * `<script>` tags without a nonce, and a strict CSP would block them,\n * killing client hydration on the error page.\n *\n * The custom server entry (`entry.server.tsx`) reads the nonce from\n * `securityContext` (which the middleware sets *before* `next()`,\n * regardless of whether the loader succeeds or throws) and wraps\n * `<ServerRouter>` with `<NonceContext.Provider>`. Both happy and\n * error paths can then read the nonce via `useSecurityNonceFromContext()`.\n */\nimport { createContext, useContext } from 'react';\n\n/** React Context carrying the per-request CSP nonce through the SSR React tree. */\nexport const NonceContext = createContext<string | undefined>(undefined);\n\n/**\n * React component-side reader for the per-request nonce.\n *\n * Returns `undefined` if no `NonceContext.Provider` is in the tree\n * (e.g. in tests, or a customer who hasn't ejected `entry.server.tsx`).\n * Callers should coerce `undefined` to omit the `nonce` attribute on\n * rendered `<script>` tags (React 19 omits attributes whose value is\n * `undefined`).\n */\nexport function useSecurityNonceFromContext(): string | undefined {\n return useContext(NonceContext);\n}\n"],"mappings":";;;;AAwCA,MAAa,eAAe,cAAkC,OAAU;;;;;;;;;;AAWxE,SAAgB,8BAAkD;AAC9D,QAAO,WAAW,aAAa"}
@@ -0,0 +1,61 @@
1
+ import { a as HstsConfig, c as SecurityConfig, i as CspDirectives, n as defaultSecurityHeaders, o as ReferrerPolicyValue, r as CspConfig, s as ResolvedSecurityConfig, t as defaultCspDirectives } from "./defaults.js";
2
+ import * as react_router15 from "react-router";
3
+ import { MiddlewareFunction, RouterContextProvider } from "react-router";
4
+
5
+ //#region src/security/middleware.d.ts
6
+
7
+ /**
8
+ * Create the React Router middleware that applies default security
9
+ * response headers.
10
+ *
11
+ * - Validates customer config via zod at factory call (boot). Throws on
12
+ * invalid directive names with a clear message.
13
+ * - Generates a fresh CSP nonce per request (16 bytes / 24 base64 chars).
14
+ * Sets it on `securityContext` for `getSecurityNonce()` consumers.
15
+ * - Merges customer directives over SDK defaults (per-directive replace).
16
+ * - HSTS is suppressed locally — emitted only when running on MRT
17
+ * (BUNDLE_ID set and not 'local').
18
+ *
19
+ * @param input - Customer security config from `config.server.ts`. Any
20
+ * field omitted falls back to the SDK default.
21
+ *
22
+ * Reads (at boot, once):
23
+ * - `process.env.BUNDLE_ID` — when set and not 'local', emit HSTS.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const mw = createSecurityHeadersMiddleware(config.security);
28
+ * // register in root.tsx middleware chain before appConfigMiddleware
29
+ * ```
30
+ */
31
+ declare function createSecurityHeadersMiddleware(input?: SecurityConfig): MiddlewareFunction<Response>;
32
+ //#endregion
33
+ //#region src/security/nonce.d.ts
34
+ /** React Router context carrying the current request's CSP nonce. */
35
+ declare const securityContext: react_router15.RouterContext<{
36
+ nonce: string;
37
+ } | null>;
38
+ /**
39
+ * Read the current request's CSP nonce. Returns `null` when the security
40
+ * middleware is disabled. Server-only — call from a loader or action.
41
+ *
42
+ * Naming: `get*` (not `use*`) because this is not a React hook — it reads
43
+ * the React Router context directly. Mirrors `getLocale` / `getTranslation`
44
+ * in the i18n module.
45
+ *
46
+ * The nonce is meaningful only on the SSR-rendered inline script. On
47
+ * client navigations, the loader runs again and returns a fresh nonce,
48
+ * but no new CSP header is emitted, so the loader-returned value should
49
+ * not be applied to scripts injected client-side.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // In root.tsx loader:
54
+ * const nonce = getSecurityNonce(args.context);
55
+ * return { nonce, ...other };
56
+ * ```
57
+ */
58
+ declare function getSecurityNonce(context: Readonly<RouterContextProvider>): string | null;
59
+ //#endregion
60
+ export { type CspConfig, type CspDirectives, type HstsConfig, type ReferrerPolicyValue, type ResolvedSecurityConfig, type SecurityConfig, createSecurityHeadersMiddleware, defaultCspDirectives, defaultSecurityHeaders, getSecurityNonce, securityContext };
61
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","names":[],"sources":["../src/security/middleware.ts","../src/security/nonce.ts"],"sourcesContent":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA6HgB,+BAAA,SAAuC,iBAAsB,mBAAmB;;;;cClGnF,iBAA+D,cAAA,CAAhD;EDkGZ,KAAA,EAAA,MAAA;CAAuC,GAAA,IAAA,CAAA;;;;;;;AClGvD;AAsBA;;;;;;;;;;;;;iBAAgB,gBAAA,UAA0B,SAAS"}
@@ -0,0 +1,304 @@
1
+ import { n as defaultSecurityHeaders, t as defaultCspDirectives } from "./defaults.js";
2
+ import { createContext } from "react-router";
3
+ import { randomBytes } from "node:crypto";
4
+ import { z } from "zod";
5
+
6
+ //#region src/security/nonce.ts
7
+ /** 16 bytes (128 bits) of CSPRNG-grade entropy, base64-encoded → 24 chars. */
8
+ const NONCE_BYTES = 16;
9
+ /** Generate a fresh CSP nonce. Each request must call this exactly once. */
10
+ function generateNonce() {
11
+ return randomBytes(NONCE_BYTES).toString("base64");
12
+ }
13
+ /** React Router context carrying the current request's CSP nonce. */
14
+ const securityContext = createContext(null);
15
+ /**
16
+ * Read the current request's CSP nonce. Returns `null` when the security
17
+ * middleware is disabled. Server-only — call from a loader or action.
18
+ *
19
+ * Naming: `get*` (not `use*`) because this is not a React hook — it reads
20
+ * the React Router context directly. Mirrors `getLocale` / `getTranslation`
21
+ * in the i18n module.
22
+ *
23
+ * The nonce is meaningful only on the SSR-rendered inline script. On
24
+ * client navigations, the loader runs again and returns a fresh nonce,
25
+ * but no new CSP header is emitted, so the loader-returned value should
26
+ * not be applied to scripts injected client-side.
27
+ *
28
+ * @example
29
+ * ```ts
30
+ * // In root.tsx loader:
31
+ * const nonce = getSecurityNonce(args.context);
32
+ * return { nonce, ...other };
33
+ * ```
34
+ */
35
+ function getSecurityNonce(context) {
36
+ return context.get(securityContext)?.nonce ?? null;
37
+ }
38
+
39
+ //#endregion
40
+ //#region src/security/schema.ts
41
+ const VALID_CSP_DIRECTIVES = [
42
+ "default-src",
43
+ "script-src",
44
+ "style-src",
45
+ "img-src",
46
+ "font-src",
47
+ "connect-src",
48
+ "frame-src",
49
+ "frame-ancestors",
50
+ "form-action",
51
+ "base-uri",
52
+ "object-src",
53
+ "manifest-src",
54
+ "media-src",
55
+ "worker-src",
56
+ "child-src",
57
+ "report-uri",
58
+ "report-to",
59
+ "upgrade-insecure-requests"
60
+ ];
61
+ const cspDirectivesSchema = z.record(z.string(), z.union([z.array(z.string()), z.literal(true)])).superRefine((directives, ctx) => {
62
+ for (const name of Object.keys(directives)) {
63
+ if (!VALID_CSP_DIRECTIVES.includes(name)) ctx.addIssue({
64
+ code: "custom",
65
+ message: `Invalid CSP directive name "${name}". Valid: ${VALID_CSP_DIRECTIVES.join(", ")}`,
66
+ path: [name]
67
+ });
68
+ if (name === "upgrade-insecure-requests") {
69
+ if (directives[name] !== true) ctx.addIssue({
70
+ code: "custom",
71
+ message: `'upgrade-insecure-requests' must be the literal value true`,
72
+ path: [name]
73
+ });
74
+ } else if (!Array.isArray(directives[name])) ctx.addIssue({
75
+ code: "custom",
76
+ message: `Directive "${name}" must be a string array`,
77
+ path: [name]
78
+ });
79
+ }
80
+ });
81
+ const cspConfigSchema = z.object({
82
+ directives: cspDirectivesSchema.optional(),
83
+ reportOnly: z.boolean().optional()
84
+ });
85
+ const hstsConfigSchema = z.object({
86
+ maxAge: z.number().int().nonnegative().optional(),
87
+ includeSubDomains: z.boolean().optional(),
88
+ preload: z.boolean().optional()
89
+ });
90
+ const referrerPolicySchema = z.enum([
91
+ "no-referrer",
92
+ "no-referrer-when-downgrade",
93
+ "origin",
94
+ "origin-when-cross-origin",
95
+ "same-origin",
96
+ "strict-origin",
97
+ "strict-origin-when-cross-origin",
98
+ "unsafe-url"
99
+ ]);
100
+ const securityConfigSchema = z.object({
101
+ enabled: z.boolean().optional(),
102
+ csp: z.union([cspConfigSchema, z.literal(false)]).optional(),
103
+ hsts: z.union([hstsConfigSchema, z.literal(false)]).optional(),
104
+ frameOptions: z.union([z.enum(["DENY", "SAMEORIGIN"]), z.literal(false)]).optional(),
105
+ contentTypeOptions: z.union([z.literal("nosniff"), z.literal(false)]).optional(),
106
+ referrerPolicy: z.union([referrerPolicySchema, z.literal(false)]).optional(),
107
+ permissionsPolicy: z.union([z.record(z.string(), z.array(z.string())), z.literal(false)]).optional()
108
+ });
109
+ /**
110
+ * Validate a `SecurityConfig`. Throws a `ZodError` with a clear message on
111
+ * invalid directive names or value shapes. Called once at server boot.
112
+ */
113
+ function parseSecurityConfig(input) {
114
+ return securityConfigSchema.parse(input);
115
+ }
116
+
117
+ //#endregion
118
+ //#region src/security/serialize.ts
119
+ /**
120
+ * Serialize a `CspDirectives` map to a CSP header string.
121
+ *
122
+ * If `nonce` is provided, it is appended to `script-src` (creating it
123
+ * if absent). Empty directive arrays are omitted.
124
+ * `upgrade-insecure-requests` is serialized as a bare keyword.
125
+ */
126
+ function serializeCsp(directives, options) {
127
+ const parts = [];
128
+ let scriptSrcEmitted = false;
129
+ for (const [name, value] of Object.entries(directives)) {
130
+ if (name === "upgrade-insecure-requests") {
131
+ if (value === true) parts.push("upgrade-insecure-requests");
132
+ continue;
133
+ }
134
+ const sources = value;
135
+ if (!sources || sources.length === 0) continue;
136
+ if (name === "script-src" && options?.nonce) {
137
+ parts.push(`script-src ${[...sources, `'nonce-${options.nonce}'`].join(" ")}`);
138
+ scriptSrcEmitted = true;
139
+ } else parts.push(`${name} ${sources.join(" ")}`);
140
+ }
141
+ if (options?.nonce && !scriptSrcEmitted) parts.push(`script-src 'nonce-${options.nonce}'`);
142
+ return parts.join("; ");
143
+ }
144
+ /**
145
+ * Serialize a Permissions-Policy map to a header string.
146
+ *
147
+ * Per the W3C structured-field grammar, the keywords `self` and `*` are
148
+ * emitted unquoted; all other allowlist entries are emitted as quoted
149
+ * strings (the schema rejects malformed origins before they reach here).
150
+ * Empty allowlists serialize to `name=()` (deny).
151
+ *
152
+ * Reference: https://www.w3.org/TR/permissions-policy/#permissions-policy-http-header-field
153
+ */
154
+ function serializePermissionsPolicy(policy) {
155
+ return Object.entries(policy).map(([feature, allowlist]) => {
156
+ if (allowlist.length === 0) return `${feature}=()`;
157
+ return `${feature}=(${allowlist.map((origin) => origin === "self" || origin === "*" ? origin : `"${origin}"`).join(" ")})`;
158
+ }).join(", ");
159
+ }
160
+ /**
161
+ * Serialize an HSTS config to a header string.
162
+ */
163
+ function serializeHsts(hsts) {
164
+ const parts = [`max-age=${hsts.maxAge}`];
165
+ if (hsts.includeSubDomains) parts.push("includeSubDomains");
166
+ if (hsts.preload) parts.push("preload");
167
+ return parts.join("; ");
168
+ }
169
+
170
+ //#endregion
171
+ //#region src/security/middleware.ts
172
+ /**
173
+ * Read at boot. HSTS is suppressed when running locally (BUNDLE_ID unset
174
+ * or 'local') because HSTS pins the host in browser caches — pinning
175
+ * `localhost` would force HTTPS on every developer's `pnpm dev`.
176
+ */
177
+ function isRemote() {
178
+ const id = process.env.BUNDLE_ID;
179
+ return Boolean(id) && id !== "local";
180
+ }
181
+ /**
182
+ * Merge customer config with SDK defaults. Per-directive replace: any
183
+ * directive the customer sets fully replaces the SDK default for that key
184
+ * (object spread semantics).
185
+ *
186
+ * Narrows defaults via a runtime check rather than `as Required<...>` casts,
187
+ * so a future change that sets `defaults.csp = false` or `defaults.hsts = false`
188
+ * is caught here instead of producing `max-age=undefined` at the wire.
189
+ */
190
+ function resolve(input) {
191
+ const defaultsCsp = defaultSecurityHeaders.csp === false ? null : defaultSecurityHeaders.csp;
192
+ const defaultsHsts = defaultSecurityHeaders.hsts === false ? null : defaultSecurityHeaders.hsts;
193
+ return {
194
+ enabled: input.enabled ?? defaultSecurityHeaders.enabled,
195
+ csp: input.csp === false ? false : {
196
+ directives: {
197
+ ...defaultsCsp?.directives ?? {},
198
+ ...input.csp?.directives ?? {}
199
+ },
200
+ reportOnly: input.csp?.reportOnly ?? false
201
+ },
202
+ hsts: input.hsts === false ? false : input.hsts === void 0 ? defaultsHsts ?? false : {
203
+ ...defaultsHsts ?? {
204
+ maxAge: 0,
205
+ includeSubDomains: false,
206
+ preload: false
207
+ },
208
+ ...input.hsts
209
+ },
210
+ frameOptions: input.frameOptions ?? defaultSecurityHeaders.frameOptions,
211
+ contentTypeOptions: input.contentTypeOptions ?? defaultSecurityHeaders.contentTypeOptions,
212
+ referrerPolicy: input.referrerPolicy ?? defaultSecurityHeaders.referrerPolicy,
213
+ permissionsPolicy: input.permissionsPolicy ?? defaultSecurityHeaders.permissionsPolicy
214
+ };
215
+ }
216
+ /**
217
+ * Boot-time warnings. Logged once per server start when potentially
218
+ * unsafe configurations are active.
219
+ */
220
+ function warnIfUnsafe(resolved) {
221
+ if (!resolved.enabled) {
222
+ console.warn("[security] All security headers disabled via config. This is not recommended for production.");
223
+ return;
224
+ }
225
+ if (resolved.csp === false) console.warn("[security] CSP disabled via config. Other headers still applied.");
226
+ else if (resolved.csp.reportOnly) console.warn("[security] CSP is in report-only mode. This is intended for migration only. Set csp.reportOnly to false before going to production.");
227
+ if (resolved.csp !== false) {
228
+ const scriptSrc = resolved.csp.directives["script-src"];
229
+ if (Array.isArray(scriptSrc) && !scriptSrc.includes("'self'")) console.warn("[security] CSP script-src does not include 'self'. The inline window.__APP_CONFIG__ script may fail to execute.");
230
+ }
231
+ }
232
+ /**
233
+ * Create the React Router middleware that applies default security
234
+ * response headers.
235
+ *
236
+ * - Validates customer config via zod at factory call (boot). Throws on
237
+ * invalid directive names with a clear message.
238
+ * - Generates a fresh CSP nonce per request (16 bytes / 24 base64 chars).
239
+ * Sets it on `securityContext` for `getSecurityNonce()` consumers.
240
+ * - Merges customer directives over SDK defaults (per-directive replace).
241
+ * - HSTS is suppressed locally — emitted only when running on MRT
242
+ * (BUNDLE_ID set and not 'local').
243
+ *
244
+ * @param input - Customer security config from `config.server.ts`. Any
245
+ * field omitted falls back to the SDK default.
246
+ *
247
+ * Reads (at boot, once):
248
+ * - `process.env.BUNDLE_ID` — when set and not 'local', emit HSTS.
249
+ *
250
+ * @example
251
+ * ```ts
252
+ * const mw = createSecurityHeadersMiddleware(config.security);
253
+ * // register in root.tsx middleware chain before appConfigMiddleware
254
+ * ```
255
+ */
256
+ function createSecurityHeadersMiddleware(input = {}) {
257
+ parseSecurityConfig(input);
258
+ const resolved = resolve(input);
259
+ warnIfUnsafe(resolved);
260
+ const staticHsts = isRemote() && resolved.hsts !== false ? serializeHsts(resolved.hsts) : null;
261
+ const permissionsHeader = resolved.permissionsPolicy === false ? null : serializePermissionsPolicy(resolved.permissionsPolicy);
262
+ const cspHeaderName = resolved.csp !== false && resolved.csp.reportOnly ? "Content-Security-Policy-Report-Only" : "Content-Security-Policy";
263
+ let staticCspBody = null;
264
+ let baseScriptSrc = "";
265
+ if (resolved.csp !== false) {
266
+ const { "script-src": scriptSrc,...rest } = resolved.csp.directives;
267
+ staticCspBody = serializeCsp(rest);
268
+ baseScriptSrc = (scriptSrc ?? []).join(" ");
269
+ }
270
+ /**
271
+ * Apply the resolved security headers to a response. Pulled into a helper
272
+ * so we can run it on the success path AND on a thrown Response (RR
273
+ * loaders/actions throw `Response` for 404/redirect/etc.). Without this,
274
+ * a 404 error response would ship without security headers.
275
+ */
276
+ const applyHeaders = (response, nonce) => {
277
+ if (staticCspBody !== null && nonce !== null) {
278
+ const scriptSrcClause = baseScriptSrc.length > 0 ? `script-src ${baseScriptSrc} 'nonce-${nonce}'` : `script-src 'nonce-${nonce}'`;
279
+ const csp = staticCspBody.length > 0 ? `${staticCspBody}; ${scriptSrcClause}` : scriptSrcClause;
280
+ response.headers.set(cspHeaderName, csp);
281
+ }
282
+ if (staticHsts !== null) response.headers.set("Strict-Transport-Security", staticHsts);
283
+ if (resolved.frameOptions !== false) response.headers.set("X-Frame-Options", resolved.frameOptions);
284
+ if (resolved.contentTypeOptions !== false) response.headers.set("X-Content-Type-Options", resolved.contentTypeOptions);
285
+ if (resolved.referrerPolicy !== false) response.headers.set("Referrer-Policy", resolved.referrerPolicy);
286
+ if (permissionsHeader !== null) response.headers.set("Permissions-Policy", permissionsHeader);
287
+ return response;
288
+ };
289
+ return async (args, next) => {
290
+ if (!resolved.enabled) return next();
291
+ const nonce = resolved.csp === false ? null : generateNonce();
292
+ if (nonce !== null) args.context.set(securityContext, { nonce });
293
+ try {
294
+ return applyHeaders(await next(), nonce);
295
+ } catch (err) {
296
+ if (err instanceof Response) throw applyHeaders(err, nonce);
297
+ throw err;
298
+ }
299
+ };
300
+ }
301
+
302
+ //#endregion
303
+ export { createSecurityHeadersMiddleware, defaultCspDirectives, defaultSecurityHeaders, getSecurityNonce, securityContext };
304
+ //# sourceMappingURL=security.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.js","names":["parts: string[]","defaultsHsts: Required<HstsConfig> | null","staticCspBody: string | null"],"sources":["../src/security/nonce.ts","../src/security/schema.ts","../src/security/serialize.ts","../src/security/middleware.ts"],"sourcesContent":["/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { randomBytes } from 'node:crypto';\nimport { createContext, type RouterContextProvider } from 'react-router';\n\n/** 16 bytes (128 bits) of CSPRNG-grade entropy, base64-encoded → 24 chars. */\nconst NONCE_BYTES = 16;\n\n/** Generate a fresh CSP nonce. Each request must call this exactly once. */\nexport function generateNonce(): string {\n return randomBytes(NONCE_BYTES).toString('base64');\n}\n\n/** React Router context carrying the current request's CSP nonce. */\nexport const securityContext = createContext<{ nonce: string } | null>(null);\n\n/**\n * Read the current request's CSP nonce. Returns `null` when the security\n * middleware is disabled. Server-only — call from a loader or action.\n *\n * Naming: `get*` (not `use*`) because this is not a React hook — it reads\n * the React Router context directly. Mirrors `getLocale` / `getTranslation`\n * in the i18n module.\n *\n * The nonce is meaningful only on the SSR-rendered inline script. On\n * client navigations, the loader runs again and returns a fresh nonce,\n * but no new CSP header is emitted, so the loader-returned value should\n * not be applied to scripts injected client-side.\n *\n * @example\n * ```ts\n * // In root.tsx loader:\n * const nonce = getSecurityNonce(args.context);\n * return { nonce, ...other };\n * ```\n */\nexport function getSecurityNonce(context: Readonly<RouterContextProvider>): string | null {\n return context.get(securityContext)?.nonce ?? null;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { z } from 'zod';\nimport type { SecurityConfig } from './types.js';\n\nconst VALID_CSP_DIRECTIVES = [\n 'default-src',\n 'script-src',\n 'style-src',\n 'img-src',\n 'font-src',\n 'connect-src',\n 'frame-src',\n 'frame-ancestors',\n 'form-action',\n 'base-uri',\n 'object-src',\n 'manifest-src',\n 'media-src',\n 'worker-src',\n 'child-src',\n 'report-uri',\n 'report-to',\n 'upgrade-insecure-requests',\n] as const;\n\nconst cspDirectivesSchema = z\n .record(z.string(), z.union([z.array(z.string()), z.literal(true)]))\n .superRefine((directives, ctx) => {\n for (const name of Object.keys(directives)) {\n if (!(VALID_CSP_DIRECTIVES as readonly string[]).includes(name)) {\n ctx.addIssue({\n code: 'custom',\n message: `Invalid CSP directive name \"${name}\". Valid: ${VALID_CSP_DIRECTIVES.join(', ')}`,\n path: [name],\n });\n }\n if (name === 'upgrade-insecure-requests') {\n if (directives[name] !== true) {\n ctx.addIssue({\n code: 'custom',\n message: `'upgrade-insecure-requests' must be the literal value true`,\n path: [name],\n });\n }\n } else if (!Array.isArray(directives[name])) {\n ctx.addIssue({\n code: 'custom',\n message: `Directive \"${name}\" must be a string array`,\n path: [name],\n });\n }\n }\n });\n\nconst cspConfigSchema = z.object({\n directives: cspDirectivesSchema.optional(),\n reportOnly: z.boolean().optional(),\n});\n\nconst hstsConfigSchema = z.object({\n maxAge: z.number().int().nonnegative().optional(),\n includeSubDomains: z.boolean().optional(),\n preload: z.boolean().optional(),\n});\n\nconst referrerPolicySchema = z.enum([\n 'no-referrer',\n 'no-referrer-when-downgrade',\n 'origin',\n 'origin-when-cross-origin',\n 'same-origin',\n 'strict-origin',\n 'strict-origin-when-cross-origin',\n 'unsafe-url',\n]);\n\nconst securityConfigSchema = z.object({\n enabled: z.boolean().optional(),\n csp: z.union([cspConfigSchema, z.literal(false)]).optional(),\n hsts: z.union([hstsConfigSchema, z.literal(false)]).optional(),\n frameOptions: z.union([z.enum(['DENY', 'SAMEORIGIN']), z.literal(false)]).optional(),\n contentTypeOptions: z.union([z.literal('nosniff'), z.literal(false)]).optional(),\n referrerPolicy: z.union([referrerPolicySchema, z.literal(false)]).optional(),\n permissionsPolicy: z.union([z.record(z.string(), z.array(z.string())), z.literal(false)]).optional(),\n});\n\n/**\n * Validate a `SecurityConfig`. Throws a `ZodError` with a clear message on\n * invalid directive names or value shapes. Called once at server boot.\n */\nexport function parseSecurityConfig(input: unknown): SecurityConfig {\n return securityConfigSchema.parse(input) as SecurityConfig;\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport type { CspDirectives, HstsConfig } from './types.js';\n\n/**\n * Serialize a `CspDirectives` map to a CSP header string.\n *\n * If `nonce` is provided, it is appended to `script-src` (creating it\n * if absent). Empty directive arrays are omitted.\n * `upgrade-insecure-requests` is serialized as a bare keyword.\n */\nexport function serializeCsp(directives: CspDirectives, options?: { nonce?: string }): string {\n const parts: string[] = [];\n let scriptSrcEmitted = false;\n\n for (const [name, value] of Object.entries(directives)) {\n if (name === 'upgrade-insecure-requests') {\n if (value === true) parts.push('upgrade-insecure-requests');\n continue;\n }\n const sources = value as string[] | undefined;\n if (!sources || sources.length === 0) continue;\n\n if (name === 'script-src' && options?.nonce) {\n parts.push(`script-src ${[...sources, `'nonce-${options.nonce}'`].join(' ')}`);\n scriptSrcEmitted = true;\n } else {\n parts.push(`${name} ${sources.join(' ')}`);\n }\n }\n\n if (options?.nonce && !scriptSrcEmitted) {\n parts.push(`script-src 'nonce-${options.nonce}'`);\n }\n\n return parts.join('; ');\n}\n\n/**\n * Serialize a Permissions-Policy map to a header string.\n *\n * Per the W3C structured-field grammar, the keywords `self` and `*` are\n * emitted unquoted; all other allowlist entries are emitted as quoted\n * strings (the schema rejects malformed origins before they reach here).\n * Empty allowlists serialize to `name=()` (deny).\n *\n * Reference: https://www.w3.org/TR/permissions-policy/#permissions-policy-http-header-field\n */\nexport function serializePermissionsPolicy(policy: Record<string, string[]>): string {\n return Object.entries(policy)\n .map(([feature, allowlist]) => {\n if (allowlist.length === 0) return `${feature}=()`;\n const tokens = allowlist.map((origin) => (origin === 'self' || origin === '*' ? origin : `\"${origin}\"`));\n return `${feature}=(${tokens.join(' ')})`;\n })\n .join(', ');\n}\n\n/**\n * Serialize an HSTS config to a header string.\n */\nexport function serializeHsts(hsts: Required<HstsConfig>): string {\n const parts = [`max-age=${hsts.maxAge}`];\n if (hsts.includeSubDomains) parts.push('includeSubDomains');\n if (hsts.preload) parts.push('preload');\n return parts.join('; ');\n}\n","/**\n * Copyright 2026 Salesforce, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { type MiddlewareFunction } from 'react-router';\nimport { defaultSecurityHeaders } from './defaults.js';\nimport { generateNonce, securityContext } from './nonce.js';\nimport { parseSecurityConfig } from './schema.js';\nimport { serializeCsp, serializeHsts, serializePermissionsPolicy } from './serialize.js';\nimport type { CspDirectives, HstsConfig, ResolvedSecurityConfig, SecurityConfig } from './types.js';\n\n/**\n * Read at boot. HSTS is suppressed when running locally (BUNDLE_ID unset\n * or 'local') because HSTS pins the host in browser caches — pinning\n * `localhost` would force HTTPS on every developer's `pnpm dev`.\n */\nfunction isRemote(): boolean {\n const id = process.env.BUNDLE_ID;\n return Boolean(id) && id !== 'local';\n}\n\n/**\n * Merge customer config with SDK defaults. Per-directive replace: any\n * directive the customer sets fully replaces the SDK default for that key\n * (object spread semantics).\n *\n * Narrows defaults via a runtime check rather than `as Required<...>` casts,\n * so a future change that sets `defaults.csp = false` or `defaults.hsts = false`\n * is caught here instead of producing `max-age=undefined` at the wire.\n */\nfunction resolve(input: SecurityConfig): ResolvedSecurityConfig {\n const defaultsCsp = defaultSecurityHeaders.csp === false ? null : defaultSecurityHeaders.csp;\n const defaultsHsts: Required<HstsConfig> | null =\n defaultSecurityHeaders.hsts === false ? null : defaultSecurityHeaders.hsts;\n\n return {\n enabled: input.enabled ?? defaultSecurityHeaders.enabled,\n csp:\n input.csp === false\n ? false\n : {\n directives: {\n ...(defaultsCsp?.directives ?? {}),\n ...(input.csp?.directives ?? {}),\n },\n reportOnly: input.csp?.reportOnly ?? false,\n },\n hsts:\n input.hsts === false\n ? false\n : input.hsts === undefined\n ? (defaultsHsts ?? false)\n : { ...(defaultsHsts ?? { maxAge: 0, includeSubDomains: false, preload: false }), ...input.hsts },\n frameOptions: input.frameOptions ?? defaultSecurityHeaders.frameOptions,\n contentTypeOptions: input.contentTypeOptions ?? defaultSecurityHeaders.contentTypeOptions,\n referrerPolicy: input.referrerPolicy ?? defaultSecurityHeaders.referrerPolicy,\n permissionsPolicy: input.permissionsPolicy ?? defaultSecurityHeaders.permissionsPolicy,\n };\n}\n\n/**\n * Boot-time warnings. Logged once per server start when potentially\n * unsafe configurations are active.\n */\nfunction warnIfUnsafe(resolved: ResolvedSecurityConfig): void {\n if (!resolved.enabled) {\n // eslint-disable-next-line no-console\n console.warn('[security] All security headers disabled via config. This is not recommended for production.');\n return;\n }\n if (resolved.csp === false) {\n // eslint-disable-next-line no-console\n console.warn('[security] CSP disabled via config. Other headers still applied.');\n } else if (resolved.csp.reportOnly) {\n // eslint-disable-next-line no-console\n console.warn(\n '[security] CSP is in report-only mode. This is intended for migration only. Set csp.reportOnly to false before going to production.'\n );\n }\n if (resolved.csp !== false) {\n const scriptSrc = resolved.csp.directives['script-src'];\n if (Array.isArray(scriptSrc) && !scriptSrc.includes(\"'self'\")) {\n // eslint-disable-next-line no-console\n console.warn(\n \"[security] CSP script-src does not include 'self'. The inline window.__APP_CONFIG__ script may fail to execute.\"\n );\n }\n }\n}\n\n/**\n * Create the React Router middleware that applies default security\n * response headers.\n *\n * - Validates customer config via zod at factory call (boot). Throws on\n * invalid directive names with a clear message.\n * - Generates a fresh CSP nonce per request (16 bytes / 24 base64 chars).\n * Sets it on `securityContext` for `getSecurityNonce()` consumers.\n * - Merges customer directives over SDK defaults (per-directive replace).\n * - HSTS is suppressed locally — emitted only when running on MRT\n * (BUNDLE_ID set and not 'local').\n *\n * @param input - Customer security config from `config.server.ts`. Any\n * field omitted falls back to the SDK default.\n *\n * Reads (at boot, once):\n * - `process.env.BUNDLE_ID` — when set and not 'local', emit HSTS.\n *\n * @example\n * ```ts\n * const mw = createSecurityHeadersMiddleware(config.security);\n * // register in root.tsx middleware chain before appConfigMiddleware\n * ```\n */\nexport function createSecurityHeadersMiddleware(input: SecurityConfig = {}): MiddlewareFunction<Response> {\n parseSecurityConfig(input); // throws on invalid input\n const resolved = resolve(input);\n warnIfUnsafe(resolved);\n\n const remote = isRemote();\n\n // Pre-compute everything that doesn't depend on the per-request nonce.\n // The CSP serializer iterates ~11 directives + does string joins; doing\n // that once at boot instead of per-request saves ~10-25µs per response.\n const staticHsts = remote && resolved.hsts !== false ? serializeHsts(resolved.hsts) : null;\n const permissionsHeader =\n resolved.permissionsPolicy === false ? null : serializePermissionsPolicy(resolved.permissionsPolicy);\n const cspHeaderName =\n resolved.csp !== false && resolved.csp.reportOnly\n ? 'Content-Security-Policy-Report-Only'\n : 'Content-Security-Policy';\n\n // Pre-build the static CSP body with everything except script-src.\n // Per request we append `; script-src <baseScriptSrc> 'nonce-<value>'`.\n let staticCspBody: string | null = null;\n let baseScriptSrc = '';\n if (resolved.csp !== false) {\n const { 'script-src': scriptSrc, ...rest } = resolved.csp.directives;\n staticCspBody = serializeCsp(rest as CspDirectives);\n baseScriptSrc = (scriptSrc ?? []).join(' ');\n }\n\n /**\n * Apply the resolved security headers to a response. Pulled into a helper\n * so we can run it on the success path AND on a thrown Response (RR\n * loaders/actions throw `Response` for 404/redirect/etc.). Without this,\n * a 404 error response would ship without security headers.\n */\n const applyHeaders = (response: Response, nonce: string | null): Response => {\n if (staticCspBody !== null && nonce !== null) {\n const scriptSrcClause =\n baseScriptSrc.length > 0\n ? `script-src ${baseScriptSrc} 'nonce-${nonce}'`\n : `script-src 'nonce-${nonce}'`;\n const csp = staticCspBody.length > 0 ? `${staticCspBody}; ${scriptSrcClause}` : scriptSrcClause;\n response.headers.set(cspHeaderName, csp);\n }\n if (staticHsts !== null) response.headers.set('Strict-Transport-Security', staticHsts);\n if (resolved.frameOptions !== false) response.headers.set('X-Frame-Options', resolved.frameOptions);\n if (resolved.contentTypeOptions !== false)\n response.headers.set('X-Content-Type-Options', resolved.contentTypeOptions);\n if (resolved.referrerPolicy !== false) response.headers.set('Referrer-Policy', resolved.referrerPolicy);\n if (permissionsHeader !== null) response.headers.set('Permissions-Policy', permissionsHeader);\n return response;\n };\n\n return async (args, next) => {\n if (!resolved.enabled) return next();\n\n // Generate nonce + put on context BEFORE next() so render can read it.\n const nonce = resolved.csp === false ? null : generateNonce();\n if (nonce !== null) args.context.set(securityContext, { nonce });\n\n try {\n return applyHeaders(await next(), nonce);\n } catch (err) {\n // RR loaders/actions throw `Response` instances (e.g. 404, redirect).\n // Apply security headers to the thrown response and re-throw so RR\n // continues to handle it (e.g. render the error boundary).\n if (err instanceof Response) {\n // RR's contract: loaders/actions throw `Response` for 404/redirect.\n // eslint-disable-next-line @typescript-eslint/only-throw-error\n throw applyHeaders(err, nonce);\n }\n // For non-Response errors (unexpected exceptions), RR synthesizes a\n // 500 response. We can't reach that response here, but we re-throw\n // unchanged so the host can decide. The synthesized 500 will lack\n // our security headers — the host should ensure error responses go\n // through this middleware too (they do, in the default RR pipeline).\n throw err;\n }\n };\n}\n"],"mappings":"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"}