@baukit/csp 0.19.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,10 @@ const policy = contentSecurityPolicy({
15
15
 
16
16
  `contentSecurityPolicy` throws on any script source that is not a `'sha256-...'` hash, and leaves
17
17
  out `upgrade-insecure-requests` only when an origin is plain `http:`.
18
+ `vendorScripts` adds origins to `script-src`, `vendorFrames` adds them to `frame-src`, and
19
+ `blobFrames: true` adds `blob:` to `frame-src`. `assertProductionOrigins(origins)` throws for `http:`
20
+ origins, embedded credentials and loopback hosts, so a production export fails when a local `.env`
21
+ leaks into the build.
18
22
  `developmentContentSecurityPolicy` adds inline styles and the HMR socket for the dev server.
19
23
  `unhashedInlineScripts` lists inline scripts a built page carries that the policy does not hash.
20
24
 
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
1
  export { SECURITY_HEADERS } from './headers.js';
2
- export { clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, type PolicyOptions, type PolicyOrigins, type StyleSources, } from './policy.js';
2
+ export { assertProductionOrigins, clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, type PolicyOptions, type PolicyOrigins, type StyleSources, } from './policy.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,EACrB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,YAAY,GAClB,MAAM,aAAa,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
1
  export { SECURITY_HEADERS } from './headers.js';
2
- export { clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, } from './policy.js';
2
+ export { assertProductionOrigins, clerkFrontendApiOrigin, contentSecurityPolicy, developmentContentSecurityPolicy, hashSource, inlineScripts, originOf, unhashedInlineScripts, } from './policy.js';
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,GAItB,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EACtB,qBAAqB,EACrB,gCAAgC,EAChC,UAAU,EACV,aAAa,EACb,QAAQ,EACR,qBAAqB,GAItB,MAAM,aAAa,CAAC"}
package/dist/policy.d.ts CHANGED
@@ -12,12 +12,18 @@ export interface PolicyOrigins {
12
12
  readonly suiteHub?: string | undefined;
13
13
  readonly vendorConnect?: readonly string[];
14
14
  readonly vendorImages?: readonly string[];
15
+ /** Origins added to `script-src`, for vendor scripts such as the Clerk frontend API. */
16
+ readonly vendorScripts?: readonly string[];
17
+ /** Origins added to `frame-src` next to the issuer. */
18
+ readonly vendorFrames?: readonly string[];
15
19
  }
16
20
  export interface PolicyOptions {
17
21
  readonly styles: StyleSources;
18
22
  readonly origins: PolicyOrigins;
19
23
  /** `'sha256-...'` sources for every inline `<script>` the build emits. */
20
24
  readonly scriptHashes: readonly string[];
25
+ /** Adds `blob:` to `frame-src`, for in-app previews that render a blob URL in an iframe. */
26
+ readonly blobFrames?: boolean;
21
27
  }
22
28
  /** Returns the origin of a configured URL, or `undefined` when it is unset. */
23
29
  export declare function originOf(value: string | undefined): string | undefined;
@@ -39,4 +45,10 @@ export declare function developmentContentSecurityPolicy(options: PolicyOptions,
39
45
  export declare function unhashedInlineScripts(html: string, policy: string, hash: (script: string) => string): string[];
40
46
  /** Decodes Clerk's frontend API host from a publishable key (`pk_<env>_<base64 host$>`). */
41
47
  export declare function clerkFrontendApiOrigin(publishableKey: string | undefined): string | undefined;
48
+ /**
49
+ * Throws when a configured origin is not a plain HTTPS origin on a public host.
50
+ * A production export calls it so a leaked local `.env` cannot ship `http:` or
51
+ * loopback origins in the policy.
52
+ */
53
+ export declare function assertProductionOrigins(origins: PolicyOrigins): void;
42
54
  //# sourceMappingURL=policy.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1C;AAQD,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMtE;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpD;AAED,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMvD;AAyDD;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAOpE;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAMR;AAED,sFAAsF;AACtF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAC/B,MAAM,EAAE,CASV;AAED,4FAA4F;AAC5F,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO7F"}
1
+ {"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,mGAAmG;AACnG,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,kBAAkB,CAAC;AAExD,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAClC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACrC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACxC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC1C,wFAAwF;IACxF,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C,uDAAuD;IACvD,QAAQ,CAAC,YAAY,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC3C;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,YAAY,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,0EAA0E;IAC1E,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,4FAA4F;IAC5F,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAC;CAC/B;AAQD,+EAA+E;AAC/E,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAMtE;AAED,0DAA0D;AAC1D,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAEpD;AAED,uDAAuD;AACvD,wBAAgB,UAAU,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAMvD;AAqED;;;GAGG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,aAAa,GAAG,MAAM,CAOpE;AAED;;;GAGG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,SAAS,MAAM,EAAE,GAC5B,MAAM,CAMR;AAED,sFAAsF;AACtF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,MAAM,GAC/B,MAAM,EAAE,CASV;AAED,4FAA4F;AAC5F,wBAAgB,sBAAsB,CAAC,cAAc,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAO7F;AAiBD;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI,CAapE"}
package/dist/policy.js CHANGED
@@ -39,17 +39,24 @@ function originList(origins) {
39
39
  origins.suiteHub,
40
40
  ...(origins.vendorConnect ?? []),
41
41
  ...(origins.vendorImages ?? []),
42
+ ...(origins.vendorScripts ?? []),
43
+ ...(origins.vendorFrames ?? []),
42
44
  ];
43
45
  }
44
46
  function usesPlainHttp(origins) {
45
47
  return originList(origins).some((origin) => origin?.startsWith('http:') === true);
46
48
  }
49
+ function frameSources(options) {
50
+ const { origins } = options;
51
+ const list = sources(origins.issuer, ...(origins.vendorFrames ?? []), options.blobFrames === true ? 'blob:' : undefined);
52
+ return list.length === 0 ? [NONE] : list;
53
+ }
47
54
  function directives(options) {
48
55
  const { origins } = options;
49
56
  const issuer = origins.issuer;
50
57
  return [
51
58
  ['default-src', [SELF]],
52
- ['script-src', sources(SELF, ...options.scriptHashes)],
59
+ ['script-src', sources(SELF, ...(origins.vendorScripts ?? []), ...options.scriptHashes)],
53
60
  ['style-src', options.styles === 'files' ? [SELF] : [SELF, INLINE]],
54
61
  ['img-src', sources(SELF, 'data:', 'blob:', origins.media, ...(origins.vendorImages ?? []))],
55
62
  ['media-src', sources(SELF, 'blob:', origins.media)],
@@ -57,7 +64,7 @@ function directives(options) {
57
64
  'connect-src',
58
65
  sources(SELF, origins.api, issuer, origins.analytics, origins.suiteHub, ...(origins.vendorConnect ?? [])),
59
66
  ],
60
- ['frame-src', issuer === undefined ? [NONE] : [issuer]],
67
+ ['frame-src', frameSources(options)],
61
68
  ['worker-src', [SELF]],
62
69
  ['manifest-src', [SELF]],
63
70
  ['object-src', [NONE]],
@@ -110,4 +117,34 @@ export function clerkFrontendApiOrigin(publishableKey) {
110
117
  const host = atob(encoded).replace(/\$$/u, '');
111
118
  return originOf(`https://${host}`);
112
119
  }
120
+ const IPV4_MAPPED_LOOPBACK = /^\[::ffff:7f[0-9a-f]{2}:/u;
121
+ function isLoopbackHost(hostname) {
122
+ const host = hostname.toLowerCase().replace(/\.$/u, '');
123
+ return (host === 'localhost' ||
124
+ host.endsWith('.localhost') ||
125
+ host === '[::1]' ||
126
+ host === '[::]' ||
127
+ host === '0.0.0.0' ||
128
+ host.startsWith('127.') ||
129
+ IPV4_MAPPED_LOOPBACK.test(host));
130
+ }
131
+ /**
132
+ * Throws when a configured origin is not a plain HTTPS origin on a public host.
133
+ * A production export calls it so a leaked local `.env` cannot ship `http:` or
134
+ * loopback origins in the policy.
135
+ */
136
+ export function assertProductionOrigins(origins) {
137
+ for (const origin of originList(origins)) {
138
+ if (origin === undefined) {
139
+ continue;
140
+ }
141
+ const url = new URL(origin);
142
+ if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
143
+ throw new Error(`not an HTTPS origin without credentials: ${origin}`);
144
+ }
145
+ if (isLoopbackHost(url.hostname)) {
146
+ throw new Error(`loopback origin in a production policy: ${origin}`);
147
+ }
148
+ }
149
+ }
113
150
  //# sourceMappingURL=policy.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"policy.js","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@baukit/csp",
3
- "version": "0.19.2",
3
+ "version": "0.20.0",
4
4
  "description": "Content Security Policy builder and security headers for Baukit web builds.",
5
5
  "license": "MIT",
6
6
  "author": "Patrick Koss",
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { SECURITY_HEADERS } from './headers.js';
2
2
  export {
3
+ assertProductionOrigins,
3
4
  clerkFrontendApiOrigin,
4
5
  contentSecurityPolicy,
5
6
  developmentContentSecurityPolicy,
package/src/policy.ts CHANGED
@@ -14,6 +14,10 @@ export interface PolicyOrigins {
14
14
  readonly suiteHub?: string | undefined;
15
15
  readonly vendorConnect?: readonly string[];
16
16
  readonly vendorImages?: readonly string[];
17
+ /** Origins added to `script-src`, for vendor scripts such as the Clerk frontend API. */
18
+ readonly vendorScripts?: readonly string[];
19
+ /** Origins added to `frame-src` next to the issuer. */
20
+ readonly vendorFrames?: readonly string[];
17
21
  }
18
22
 
19
23
  export interface PolicyOptions {
@@ -21,6 +25,8 @@ export interface PolicyOptions {
21
25
  readonly origins: PolicyOrigins;
22
26
  /** `'sha256-...'` sources for every inline `<script>` the build emits. */
23
27
  readonly scriptHashes: readonly string[];
28
+ /** Adds `blob:` to `frame-src`, for in-app previews that render a blob URL in an iframe. */
29
+ readonly blobFrames?: boolean;
24
30
  }
25
31
 
26
32
  const SELF = "'self'";
@@ -65,6 +71,8 @@ function originList(origins: PolicyOrigins): (string | undefined)[] {
65
71
  origins.suiteHub,
66
72
  ...(origins.vendorConnect ?? []),
67
73
  ...(origins.vendorImages ?? []),
74
+ ...(origins.vendorScripts ?? []),
75
+ ...(origins.vendorFrames ?? []),
68
76
  ];
69
77
  }
70
78
 
@@ -72,12 +80,22 @@ function usesPlainHttp(origins: PolicyOrigins): boolean {
72
80
  return originList(origins).some((origin) => origin?.startsWith('http:') === true);
73
81
  }
74
82
 
83
+ function frameSources(options: PolicyOptions): string[] {
84
+ const { origins } = options;
85
+ const list = sources(
86
+ origins.issuer,
87
+ ...(origins.vendorFrames ?? []),
88
+ options.blobFrames === true ? 'blob:' : undefined,
89
+ );
90
+ return list.length === 0 ? [NONE] : list;
91
+ }
92
+
75
93
  function directives(options: PolicyOptions): [string, string[]][] {
76
94
  const { origins } = options;
77
95
  const issuer = origins.issuer;
78
96
  return [
79
97
  ['default-src', [SELF]],
80
- ['script-src', sources(SELF, ...options.scriptHashes)],
98
+ ['script-src', sources(SELF, ...(origins.vendorScripts ?? []), ...options.scriptHashes)],
81
99
  ['style-src', options.styles === 'files' ? [SELF] : [SELF, INLINE]],
82
100
  ['img-src', sources(SELF, 'data:', 'blob:', origins.media, ...(origins.vendorImages ?? []))],
83
101
  ['media-src', sources(SELF, 'blob:', origins.media)],
@@ -92,7 +110,7 @@ function directives(options: PolicyOptions): [string, string[]][] {
92
110
  ...(origins.vendorConnect ?? []),
93
111
  ),
94
112
  ],
95
- ['frame-src', issuer === undefined ? [NONE] : [issuer]],
113
+ ['frame-src', frameSources(options)],
96
114
  ['worker-src', [SELF]],
97
115
  ['manifest-src', [SELF]],
98
116
  ['object-src', [NONE]],
@@ -160,3 +178,38 @@ export function clerkFrontendApiOrigin(publishableKey: string | undefined): stri
160
178
  const host = atob(encoded).replace(/\$$/u, '');
161
179
  return originOf(`https://${host}`);
162
180
  }
181
+
182
+ const IPV4_MAPPED_LOOPBACK = /^\[::ffff:7f[0-9a-f]{2}:/u;
183
+
184
+ function isLoopbackHost(hostname: string): boolean {
185
+ const host = hostname.toLowerCase().replace(/\.$/u, '');
186
+ return (
187
+ host === 'localhost' ||
188
+ host.endsWith('.localhost') ||
189
+ host === '[::1]' ||
190
+ host === '[::]' ||
191
+ host === '0.0.0.0' ||
192
+ host.startsWith('127.') ||
193
+ IPV4_MAPPED_LOOPBACK.test(host)
194
+ );
195
+ }
196
+
197
+ /**
198
+ * Throws when a configured origin is not a plain HTTPS origin on a public host.
199
+ * A production export calls it so a leaked local `.env` cannot ship `http:` or
200
+ * loopback origins in the policy.
201
+ */
202
+ export function assertProductionOrigins(origins: PolicyOrigins): void {
203
+ for (const origin of originList(origins)) {
204
+ if (origin === undefined) {
205
+ continue;
206
+ }
207
+ const url = new URL(origin);
208
+ if (url.protocol !== 'https:' || url.username !== '' || url.password !== '') {
209
+ throw new Error(`not an HTTPS origin without credentials: ${origin}`);
210
+ }
211
+ if (isLoopbackHost(url.hostname)) {
212
+ throw new Error(`loopback origin in a production policy: ${origin}`);
213
+ }
214
+ }
215
+ }
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import { assertProductionOrigins, contentSecurityPolicy } from './index.js';
4
+
5
+ function directive(policy: string, name: string): string[] | undefined {
6
+ return policy
7
+ .split(';')
8
+ .map((part) => part.trim().split(' '))
9
+ .find(([directiveName]) => directiveName === name)
10
+ ?.slice(1);
11
+ }
12
+
13
+ const base = { styles: 'files', scriptHashes: [] } as const;
14
+
15
+ describe('vendor script and frame sources', () => {
16
+ it('adds vendor script origins to script-src before the hashes', () => {
17
+ const policy = contentSecurityPolicy({
18
+ ...base,
19
+ scriptHashes: ["'sha256-Ym9vdA=='"],
20
+ origins: { vendorScripts: ['https://clerk.example.test'] },
21
+ });
22
+
23
+ expect(directive(policy, 'script-src')).toEqual([
24
+ "'self'",
25
+ 'https://clerk.example.test',
26
+ "'sha256-Ym9vdA=='",
27
+ ]);
28
+ });
29
+
30
+ it('adds vendor frames and blob: to frame-src next to the issuer', () => {
31
+ const policy = contentSecurityPolicy({
32
+ ...base,
33
+ blobFrames: true,
34
+ origins: { issuer: 'https://login.example.test', vendorFrames: ['https://pay.example.test'] },
35
+ });
36
+
37
+ expect(directive(policy, 'frame-src')).toEqual([
38
+ 'https://login.example.test',
39
+ 'https://pay.example.test',
40
+ 'blob:',
41
+ ]);
42
+ });
43
+
44
+ it('keeps frame-src at none without an issuer, vendor frame or blob frames', () => {
45
+ expect(directive(contentSecurityPolicy({ ...base, origins: {} }), 'frame-src')).toEqual([
46
+ "'none'",
47
+ ]);
48
+ expect(
49
+ directive(contentSecurityPolicy({ ...base, blobFrames: true, origins: {} }), 'frame-src'),
50
+ ).toEqual(['blob:']);
51
+ });
52
+
53
+ it('treats a plain http vendor script origin as a local build', () => {
54
+ const policy = contentSecurityPolicy({
55
+ ...base,
56
+ origins: { vendorScripts: ['http://localhost:9000'] },
57
+ });
58
+
59
+ expect(policy).not.toContain('upgrade-insecure-requests');
60
+ });
61
+ });
62
+
63
+ describe('production origin guard', () => {
64
+ it('accepts public HTTPS origins', () => {
65
+ expect(() => {
66
+ assertProductionOrigins({
67
+ api: 'https://api.example.test',
68
+ vendorScripts: ['https://clerk.example.test'],
69
+ });
70
+ }).not.toThrow();
71
+ });
72
+
73
+ it.each([
74
+ 'http://api.example.test',
75
+ 'https://user:secret@api.example.test',
76
+ 'https://localhost:8080',
77
+ 'https://app.localhost',
78
+ 'https://127.0.0.1',
79
+ 'https://127.1.2.3:9',
80
+ 'https://0.0.0.0',
81
+ 'https://[::1]',
82
+ 'https://[::ffff:7f00:1]',
83
+ 'https://localhost.',
84
+ ])('rejects %s', (origin) => {
85
+ expect(() => {
86
+ assertProductionOrigins({ api: origin });
87
+ }).toThrow();
88
+ expect(() => {
89
+ assertProductionOrigins({ vendorFrames: [origin] });
90
+ }).toThrow();
91
+ });
92
+
93
+ it('skips unset origins', () => {
94
+ expect(() => {
95
+ assertProductionOrigins({ api: undefined, issuer: undefined });
96
+ }).not.toThrow();
97
+ });
98
+ });