@salesforce/storefront-next-runtime 1.0.0-alpha.0 → 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 (41) hide show
  1. package/dist/ComponentContext.js +199 -4
  2. package/dist/ComponentContext.js.map +1 -1
  3. package/dist/DesignComponent.js +2 -2
  4. package/dist/DesignRegion.js +2 -2
  5. package/dist/RegionContext.js +9 -0
  6. package/dist/RegionContext.js.map +1 -0
  7. package/dist/component.types.d.ts +1 -1
  8. package/dist/config.d.ts +6 -5
  9. package/dist/config.d.ts.map +1 -1
  10. package/dist/config.js +2 -1
  11. package/dist/config.js.map +1 -1
  12. package/dist/data-store.d.ts +3 -3
  13. package/dist/data-store.d.ts.map +1 -1
  14. package/dist/defaults.d.ts +106 -0
  15. package/dist/defaults.d.ts.map +1 -0
  16. package/dist/defaults.js +67 -0
  17. package/dist/defaults.js.map +1 -0
  18. package/dist/design-data.d.ts +10 -332
  19. package/dist/design-data.d.ts.map +1 -1
  20. package/dist/design-data.js +67 -23
  21. package/dist/design-data.js.map +1 -1
  22. package/dist/design-react-core.d.ts +5 -15
  23. package/dist/design-react-core.d.ts.map +1 -1
  24. package/dist/design-react-core.js +2 -2
  25. package/dist/design-react.d.ts +2 -2
  26. package/dist/design.d.ts +2 -2
  27. package/dist/scapi.d.ts.map +1 -1
  28. package/dist/security-react.d.ts +34 -0
  29. package/dist/security-react.d.ts.map +1 -0
  30. package/dist/security-react.js +21 -0
  31. package/dist/security-react.js.map +1 -0
  32. package/dist/security.d.ts +61 -0
  33. package/dist/security.d.ts.map +1 -0
  34. package/dist/security.js +304 -0
  35. package/dist/security.js.map +1 -0
  36. package/dist/site-context.d.ts +2 -2
  37. package/dist/types3.d.ts +1 -35
  38. package/dist/types3.d.ts.map +1 -1
  39. package/package.json +15 -2
  40. package/dist/DesignFrame.js +0 -204
  41. package/dist/DesignFrame.js.map +0 -1
@@ -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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"}
@@ -1,6 +1,6 @@
1
1
  import { n as Site$1, r as Url, t as Locale$1 } from "./types.js";
2
2
  import { PropsWithChildren } from "react";
3
- import * as react_jsx_runtime2 from "react/jsx-runtime";
3
+ import * as react_jsx_runtime0 from "react/jsx-runtime";
4
4
  import * as react_router10 from "react-router";
5
5
  import { Cookie, CookieOptions, MiddlewareFunction, RouterContextProvider } from "react-router";
6
6
  import { RouteConfigEntry } from "@react-router/dev/routes";
@@ -102,7 +102,7 @@ declare function SiteProvider({
102
102
  language,
103
103
  currency,
104
104
  children
105
- }: PropsWithChildren<SiteContextValue>): react_jsx_runtime2.JSX.Element;
105
+ }: PropsWithChildren<SiteContextValue>): react_jsx_runtime0.JSX.Element;
106
106
  /**
107
107
  * React hook to get the current site context.
108
108
  * Returns `{ site, locale, language, currency }`.
package/dist/types3.d.ts CHANGED
@@ -44,40 +44,6 @@ interface ComponentModule<TProps, TFrameworkComponent = unknown> {
44
44
  /** Any additional exports (loaders, etc.) */
45
45
  [key: string]: unknown;
46
46
  }
47
- /**
48
- * Generic design metadata interface - framework agnostic.
49
- * Different frameworks can extend this with their specific metadata.
50
- */
51
- interface DesignMetadata {
52
- /** Component identifier */
53
- id?: string;
54
- /** Component name for display */
55
- name?: string;
56
- /** Component group/category */
57
- group?: string;
58
- /** Component description */
59
- description?: string;
60
- /** Additional framework-specific metadata */
61
- [key: string]: any;
62
- }
63
- /**
64
- * Internal registry entry for a component.
65
- * Framework agnostic - no specific component types.
66
- */
67
- interface Entry<TProps, TFrameworkComponent = unknown> {
68
- /** Component identifier */
69
- id: ComponentId;
70
- /** Eagerly loaded component (if registered directly) */
71
- raw: TFrameworkComponent | null;
72
- /** Lazily loaded component (if discovered via dynamic import) */
73
- lazy?: TFrameworkComponent;
74
- /** Dynamic importer function */
75
- import?: () => Promise<ComponentModule<TProps, TFrameworkComponent>>;
76
- /** Fallback component for loading states */
77
- fallback?: TFrameworkComponent;
78
- /** Loader function names for external invocation */
79
- loaderNames?: LoaderNames;
80
- }
81
47
  /**
82
48
  * Framework adapter interface.
83
49
  * Each framework implements this to provide framework-specific behavior.
@@ -106,5 +72,5 @@ interface ComponentRegistryOptions<TProps, TFrameworkComponent> {
106
72
  adapter: FrameworkAdapter<TProps, TFrameworkComponent>;
107
73
  }
108
74
  //#endregion
109
- export { Entry as a, DesignMetadata as i, ComponentModule as n, FrameworkAdapter as o, ComponentRegistryOptions as r, LoaderNames as s, ComponentId as t };
75
+ export { LoaderNames as a, FrameworkAdapter as i, ComponentModule as n, ComponentRegistryOptions as r, ComponentId as t };
110
76
  //# sourceMappingURL=types3.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types3.d.ts","names":["ComponentId","LoaderNames","ComponentModule","TProps","TFrameworkComponent","DesignMetadata","Entry","Promise","FrameworkAdapter","ComponentRegistryOptions"],"sources":["../src/design/registry/types.d.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 */\n\n/* ==================== Framework Agnostic Types ==================== */\n\n/**\n * Unique identifier for a component type.\n */\nexport type ComponentId = string;\n\n/**\n * Loader and fallback function names for external invocation.\n */\nexport interface LoaderNames {\n /** Server-side loader function name */\n loader?: string;\n /** Client-side loader function name */\n clientLoader?: string;\n /** Fallback component function name */\n fallback?: string;\n}\n\n/**\n * Shape of a dynamically imported component module.\n * This is what import.meta.glob() returns for each component.\n */\nexport interface ComponentModule<TProps, TFrameworkComponent = unknown> {\n /** The main component export */\n default: TFrameworkComponent;\n /** Optional fallback component for Suspense boundaries */\n fallback?: TFrameworkComponent;\n /** Any additional exports (loaders, etc.) */\n [key: string]: unknown;\n}\n\n/**\n * Generic design metadata interface - framework agnostic.\n * Different frameworks can extend this with their specific metadata.\n */\nexport interface DesignMetadata {\n /** Component identifier */\n id?: string;\n /** Component name for display */\n name?: string;\n /** Component group/category */\n group?: string;\n /** Component description */\n description?: string;\n /** Additional framework-specific metadata */\n [key: string]: any;\n}\n\n/**\n * Internal registry entry for a component.\n * Framework agnostic - no specific component types.\n */\nexport interface Entry<TProps, TFrameworkComponent = unknown> {\n /** Component identifier */\n id: ComponentId;\n /** Eagerly loaded component (if registered directly) */\n raw: TFrameworkComponent | null;\n /** Lazily loaded component (if discovered via dynamic import) */\n lazy?: TFrameworkComponent;\n /** Dynamic importer function */\n import?: () => Promise<ComponentModule<TProps, TFrameworkComponent>>;\n /** Fallback component for loading states */\n fallback?: TFrameworkComponent;\n /** Loader function names for external invocation */\n loaderNames?: LoaderNames;\n}\n\n/**\n * Framework adapter interface.\n * Each framework implements this to provide framework-specific behavior.\n */\nexport interface FrameworkAdapter<TProps, TFrameworkComponent = unknown> {\n /**\n * Creates a lazy-loaded component from an importer function.\n */\n createLazyComponent(importer: () => Promise<ComponentModule<TProps, TFrameworkComponent>>): TFrameworkComponent;\n\n /**\n * Decorates a component with design-time capabilities.\n * Each framework adapter implements its own decoration logic.\n */\n decorateComponent(component: TFrameworkComponent): TFrameworkComponent;\n}\n\n/**\n * Configuration options for ComponentRegistry.\n * Framework agnostic with adapter injection.\n */\nexport interface ComponentRegistryOptions<TProps, TFrameworkComponent> {\n /**\n * Framework adapter for framework-specific operations.\n * The adapter handles all framework-specific behavior including decoration.\n */\n adapter: FrameworkAdapter<TProps, TFrameworkComponent>;\n}\n"],"mappings":";;AAqBA;AAKA;AAaA;AAaA;AAiBA;;;;;;;;;;;AAmBA;;;;;AAIgGI,KAvEpFJ,WAAAA,GAuEoFI,MAAAA;;;AAahG;AAK8BD,UApFbF,WAAAA,CAoFaE;EAAQC;EAAzBI,MAAAA,CAAAA,EAAAA,MAAAA;EAAgB;;;;;;;;;UAvEZN;;WAEJE;;aAEEA;;;;;;;;UASEC,cAAAA;;;;;;;;;;;;;;;;UAiBAC;;MAETN;;OAECI;;SAEEA;;iBAEQG,QAAQL,gBAAgBC,QAAQC;;aAEpCA;;gBAEGH;;;;;;UAODO;;;;sCAIuBD,QAAQL,gBAAgBC,QAAQC,wBAAwBA;;;;;;+BAM/DA,sBAAsBA;;;;;;UAOtCK;;;;;WAKJD,iBAAiBL,QAAQC"}
1
+ {"version":3,"file":"types3.d.ts","names":["ComponentId","LoaderNames","ComponentModule","TProps","TFrameworkComponent","DesignMetadata","Entry","Promise","FrameworkAdapter","ComponentRegistryOptions"],"sources":["../src/design/registry/types.d.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 */\n\n/* ==================== Framework Agnostic Types ==================== */\n\n/**\n * Unique identifier for a component type.\n */\nexport type ComponentId = string;\n\n/**\n * Loader and fallback function names for external invocation.\n */\nexport interface LoaderNames {\n /** Server-side loader function name */\n loader?: string;\n /** Client-side loader function name */\n clientLoader?: string;\n /** Fallback component function name */\n fallback?: string;\n}\n\n/**\n * Shape of a dynamically imported component module.\n * This is what import.meta.glob() returns for each component.\n */\nexport interface ComponentModule<TProps, TFrameworkComponent = unknown> {\n /** The main component export */\n default: TFrameworkComponent;\n /** Optional fallback component for Suspense boundaries */\n fallback?: TFrameworkComponent;\n /** Any additional exports (loaders, etc.) */\n [key: string]: unknown;\n}\n\n/**\n * Generic design metadata interface - framework agnostic.\n * Different frameworks can extend this with their specific metadata.\n */\nexport interface DesignMetadata {\n /** Component identifier */\n id?: string;\n /** Component name for display */\n name?: string;\n /** Component group/category */\n group?: string;\n /** Component description */\n description?: string;\n /** Additional framework-specific metadata */\n [key: string]: any;\n}\n\n/**\n * Internal registry entry for a component.\n * Framework agnostic - no specific component types.\n */\nexport interface Entry<TProps, TFrameworkComponent = unknown> {\n /** Component identifier */\n id: ComponentId;\n /** Eagerly loaded component (if registered directly) */\n raw: TFrameworkComponent | null;\n /** Lazily loaded component (if discovered via dynamic import) */\n lazy?: TFrameworkComponent;\n /** Dynamic importer function */\n import?: () => Promise<ComponentModule<TProps, TFrameworkComponent>>;\n /** Fallback component for loading states */\n fallback?: TFrameworkComponent;\n /** Loader function names for external invocation */\n loaderNames?: LoaderNames;\n}\n\n/**\n * Framework adapter interface.\n * Each framework implements this to provide framework-specific behavior.\n */\nexport interface FrameworkAdapter<TProps, TFrameworkComponent = unknown> {\n /**\n * Creates a lazy-loaded component from an importer function.\n */\n createLazyComponent(importer: () => Promise<ComponentModule<TProps, TFrameworkComponent>>): TFrameworkComponent;\n\n /**\n * Decorates a component with design-time capabilities.\n * Each framework adapter implements its own decoration logic.\n */\n decorateComponent(component: TFrameworkComponent): TFrameworkComponent;\n}\n\n/**\n * Configuration options for ComponentRegistry.\n * Framework agnostic with adapter injection.\n */\nexport interface ComponentRegistryOptions<TProps, TFrameworkComponent> {\n /**\n * Framework adapter for framework-specific operations.\n * The adapter handles all framework-specific behavior including decoration.\n */\n adapter: FrameworkAdapter<TProps, TFrameworkComponent>;\n}\n"],"mappings":";;AAqBA;AAKA;AAaA;AAiDA;;;;;;;;;AAiBA;;;;;;;;KApFYA,WAAAA;;;;UAKKC,WAAAA;;;;;;;;;;;;UAaAC;;WAEJE;;aAEEA;;;;;;;;UA6CEI;;;;sCAIuBD,QAAQL,gBAAgBC,QAAQC,wBAAwBA;;;;;;+BAM/DA,sBAAsBA;;;;;;UAOtCK;;;;;WAKJD,iBAAiBL,QAAQC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/storefront-next-runtime",
3
- "version": "1.0.0-alpha.0",
3
+ "version": "1.0.0-alpha.1",
4
4
  "description": "Runtime agnostic libraries for SFCC Storefront Next",
5
5
  "type": "module",
6
6
  "exports": {
@@ -109,6 +109,18 @@
109
109
  "types": "./dist/i18n-client.d.ts",
110
110
  "default": "./dist/i18n-client.js"
111
111
  }
112
+ },
113
+ "./security": {
114
+ "import": {
115
+ "types": "./dist/security.d.ts",
116
+ "default": "./dist/security.js"
117
+ }
118
+ },
119
+ "./security/react": {
120
+ "import": {
121
+ "types": "./dist/security-react.d.ts",
122
+ "default": "./dist/security-react.js"
123
+ }
112
124
  }
113
125
  },
114
126
  "files": [
@@ -125,7 +137,8 @@
125
137
  "dependencies": {
126
138
  "@salesforce/mrt-utilities": "0.2.1",
127
139
  "jiti": "^2.6.1",
128
- "openapi-fetch": "0.15.0"
140
+ "openapi-fetch": "0.15.0",
141
+ "zod": "4.1.13"
129
142
  },
130
143
  "devDependencies": {
131
144
  "@react-router/dev": "7.12.0",