@fuzdev/fuz_ui 0.191.4 → 0.193.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/dist/ApiIndex.svelte +3 -3
- package/dist/ApiModule.svelte +2 -2
- package/dist/ApiModulesList.svelte +3 -3
- package/dist/ColorSchemeInput.svelte +6 -6
- package/dist/ContextmenuLinkEntry.svelte +1 -1
- package/dist/ContextmenuRoot.svelte +10 -10
- package/dist/ContextmenuRootForSafariCompatibility.svelte +19 -19
- package/dist/ContextmenuSeparator.svelte +3 -3
- package/dist/ContextmenuSubmenu.svelte +3 -3
- package/dist/CopyToClipboard.svelte +3 -3
- package/dist/DeclarationLink.svelte +1 -1
- package/dist/Dialog.svelte +16 -16
- package/dist/Docs.svelte +12 -12
- package/dist/DocsFooter.svelte +2 -2
- package/dist/DocsList.svelte +7 -7
- package/dist/DocsMenu.svelte +4 -4
- package/dist/DocsMenu.svelte.d.ts.map +1 -1
- package/dist/DocsPageLinks.svelte +4 -4
- package/dist/DocsPrimaryNav.svelte +2 -2
- package/dist/DocsSecondaryNav.svelte +4 -4
- package/dist/DocsTertiaryNav.svelte +13 -13
- package/dist/DocsTertiaryNav.svelte.d.ts +1 -1
- package/dist/GithubLink.svelte +1 -1
- package/dist/Hashlink.svelte +2 -2
- package/dist/HueInput.svelte +5 -5
- package/dist/LibraryDetail.svelte +8 -8
- package/dist/LibrarySummary.svelte +4 -4
- package/dist/MdnLink.svelte +1 -1
- package/dist/ModuleLink.svelte +1 -1
- package/dist/PasteFromClipboard.svelte +1 -1
- package/dist/PendingAnimation.svelte +2 -2
- package/dist/PendingButton.svelte +2 -2
- package/dist/ProjectLinks.svelte +4 -4
- package/dist/Teleport.svelte +2 -2
- package/dist/ThemeInput.svelte +5 -5
- package/dist/TomeContent.svelte +3 -3
- package/dist/TomeHeader.svelte +15 -14
- package/dist/TomeHeader.svelte.d.ts.map +1 -1
- package/dist/TomeLink.svelte +13 -6
- package/dist/TomeLink.svelte.d.ts +1 -1
- package/dist/TomeLink.svelte.d.ts.map +1 -1
- package/dist/TomeSection.svelte +1 -1
- package/dist/TomeSectionHeader.svelte +3 -3
- package/dist/api_search.svelte.js +2 -2
- package/dist/contextmenu_state.svelte.d.ts.map +1 -1
- package/dist/contextmenu_state.svelte.js +11 -10
- package/dist/csp.d.ts +81 -90
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +145 -163
- package/dist/csp_of_fuzdev.d.ts +13 -3
- package/dist/csp_of_fuzdev.d.ts.map +1 -1
- package/dist/csp_of_fuzdev.js +23 -7
- package/dist/dimensions.svelte.d.ts.map +1 -1
- package/dist/dimensions.svelte.js +2 -2
- package/dist/library.svelte.d.ts +1 -1
- package/dist/library_generate.js +1 -1
- package/dist/style_variable_helpers.svelte.d.ts.map +1 -1
- package/dist/style_variable_helpers.svelte.js +1 -1
- package/dist/theme_state.svelte.d.ts.map +1 -1
- package/dist/theme_state.svelte.js +2 -2
- package/dist/tome.d.ts +8 -3
- package/dist/tome.d.ts.map +1 -1
- package/dist/tome.js +24 -8
- package/dist/tsdoc_helpers.d.ts +9 -0
- package/dist/tsdoc_helpers.d.ts.map +1 -1
- package/dist/tsdoc_helpers.js +15 -5
- package/package.json +5 -5
- package/src/lib/api_search.svelte.ts +2 -2
- package/src/lib/contextmenu_state.svelte.ts +11 -10
- package/src/lib/csp.ts +224 -258
- package/src/lib/csp_of_fuzdev.ts +24 -8
- package/src/lib/dimensions.svelte.ts +2 -2
- package/src/lib/library_generate.ts +1 -1
- package/src/lib/style_variable_helpers.svelte.ts +1 -1
- package/src/lib/theme_state.svelte.ts +2 -2
- package/src/lib/tome.ts +25 -8
- package/src/lib/tsdoc_helpers.ts +18 -7
package/src/lib/csp.ts
CHANGED
|
@@ -1,176 +1,217 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {Defined} from '@fuzdev/fuz_util/types.js';
|
|
2
2
|
|
|
3
3
|
// TODO schemas, but I may be moving to ArkType from Zod if precompilation looks good
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
| CspDirectiveValue<K> // Static value replacement
|
|
13
|
-
| null // Removes the directive
|
|
14
|
-
// Transform function returning one of the previous types
|
|
15
|
-
| ((value: CspDirectiveValue<K>) => CspDirectiveValue<K> | null);
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Sources to include based on their trust levels.
|
|
20
|
-
*/
|
|
21
|
-
trusted_sources?: Array<CspSourceSpec>;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Override default values for specific directives,
|
|
25
|
-
* merging with `value_defaults_base` (or replacing if that directive is null in the base).
|
|
26
|
-
*/
|
|
27
|
-
value_defaults?: Partial<typeof csp_directive_value_defaults>;
|
|
5
|
+
/**
|
|
6
|
+
* Per-directive map of source arrays — accepted as `extend` layer entries.
|
|
7
|
+
* Excludes directives like `'upgrade-insecure-requests'` (boolean) that can't be appended to.
|
|
8
|
+
*/
|
|
9
|
+
export type CspDirectiveSourcesMap = {
|
|
10
|
+
[K in CspDirective as CspDirectives[K] extends ReadonlyArray<any> ? K : never]?: CspDirectives[K];
|
|
11
|
+
};
|
|
28
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Options for `create_csp_directives`.
|
|
15
|
+
*
|
|
16
|
+
* The pipeline runs in three stages:
|
|
17
|
+
* 1. `replace_defaults` sets the starting state (defaults to `csp_directive_value_defaults`).
|
|
18
|
+
* 2. `extend` appends sources per directive, layered left to right.
|
|
19
|
+
* 3. `overrides` replaces or removes per-directive values as a final pass.
|
|
20
|
+
*/
|
|
21
|
+
export interface CreateCspDirectivesOptions {
|
|
29
22
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
23
|
+
* Starting values per directive — *wholesale replaces* the library defaults.
|
|
24
|
+
*
|
|
25
|
+
* - Omitted: uses `csp_directive_value_defaults` (the curated library defaults).
|
|
26
|
+
* - Provided: exactly the directives you list, nothing else inherited.
|
|
27
|
+
* Anything not listed is **absent** from the starting state — including security defaults
|
|
28
|
+
* like `default-src: 'none'`. To tweak a single directive while keeping the rest, use
|
|
29
|
+
* `extend` (to append) or `overrides` (to replace per-key) instead.
|
|
30
|
+
* - `{}`: starts blank with no directives.
|
|
31
|
+
*
|
|
32
|
+
* `null` is not accepted (top-level or per-key) — omit the option to use library defaults,
|
|
33
|
+
* pass `{}` to start blank, or use `overrides` to remove a specific directive.
|
|
34
|
+
*
|
|
35
|
+
* Per-key `undefined` is treated as omitted (no-op).
|
|
33
36
|
*/
|
|
34
|
-
|
|
37
|
+
replace_defaults?: Partial<typeof csp_directive_value_defaults>;
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
40
|
+
* Sources to append per directive, layered left to right.
|
|
41
|
+
* Each entry is a partial map; values append to the result of `replace_defaults` and prior entries.
|
|
42
|
+
* Values are deduplicated within and across layers.
|
|
43
|
+
*
|
|
44
|
+
* Only array-typed directives can be extended (boolean directives like `upgrade-insecure-requests`
|
|
45
|
+
* are excluded by the type). Throws if any entry attempts to extend a directive whose current
|
|
46
|
+
* value is `['none']` — use `replace_defaults` or `overrides` to opt into default-deny directives.
|
|
47
|
+
*
|
|
48
|
+
* Per-key `undefined` is treated as omitted (no-op) — supports conditional patterns like
|
|
49
|
+
* `{'connect-src': is_prod ? [API_URL] : undefined}`. Per-key `null` throws — `extend` only
|
|
50
|
+
* appends; use `overrides: { 'X': null }` to remove a directive.
|
|
39
51
|
*/
|
|
40
|
-
|
|
52
|
+
extend?: ReadonlyArray<CspDirectiveSourcesMap>;
|
|
41
53
|
|
|
42
54
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
55
|
+
* Final-pass per-directive overrides. Replaces the directive value or removes it entirely.
|
|
56
|
+
* Pass `null` to remove a directive from the output.
|
|
57
|
+
*
|
|
58
|
+
* Highest precedence — wins over `replace_defaults` and `extend`.
|
|
59
|
+
*
|
|
60
|
+
* Per-key `undefined` is treated as omitted (no-op) — distinct from `null`, which removes.
|
|
46
61
|
*/
|
|
47
|
-
|
|
62
|
+
overrides?: {
|
|
63
|
+
[K in CspDirective]?: CspDirectiveValue<K> | null;
|
|
64
|
+
};
|
|
48
65
|
}
|
|
49
66
|
|
|
50
67
|
/**
|
|
51
|
-
*
|
|
52
|
-
* and maps to the `KitConfig` `directives` option.
|
|
53
|
-
* The goal is to provide an ergonomic, modern, and safe API
|
|
54
|
-
* for Content Security Policy (CSP) creation
|
|
55
|
-
* that's simple to write and audit, and isn't error-prone.
|
|
68
|
+
* Builds a CSP directives map for use with SvelteKit's `kit.csp.directives` option.
|
|
56
69
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
70
|
+
* Restrictive by default; opt into specific permissions via `extend` (append) or
|
|
71
|
+
* `overrides` (replace). Designed to read as an audit log: every user-added source
|
|
72
|
+
* is named at exactly one site in the source code. Library defaults are inherited
|
|
73
|
+
* unless you opt out via `replace_defaults`.
|
|
74
|
+
*
|
|
75
|
+
* Validation:
|
|
76
|
+
* - Unknown directive keys throw.
|
|
77
|
+
* - Extending a `['none']` directive throws (use `replace_defaults`/`overrides` to opt in).
|
|
78
|
+
* - `null` for `replace_defaults` (top-level or per-key) throws — omit the option for library
|
|
79
|
+
* defaults, pass `{}` to start blank, or use `overrides` to remove a specific directive.
|
|
80
|
+
* - `null` per-key in `extend` throws (use `overrides` for removal).
|
|
81
|
+
* - `undefined` per-key is treated as omitted in all three stages.
|
|
82
|
+
* - Non-object entries in `extend` (`null`, `undefined`, primitives) throw with a friendly error.
|
|
83
|
+
* - Output is validated to ensure `'none'` never appears alongside other tokens,
|
|
84
|
+
* that no directive ends up with an empty array (use `['none']` to forbid all),
|
|
85
|
+
* and that every source array contains only strings.
|
|
86
|
+
*
|
|
87
|
+
* Things like rendering to a string are out of scope and left to SvelteKit.
|
|
59
88
|
*/
|
|
60
|
-
export
|
|
61
|
-
const {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
89
|
+
export const create_csp_directives = (options: CreateCspDirectivesOptions = {}): CspDirectives => {
|
|
90
|
+
const {replace_defaults = csp_directive_value_defaults, extend, overrides} = options;
|
|
91
|
+
|
|
92
|
+
if (replace_defaults === null) {
|
|
93
|
+
throw new Error(
|
|
94
|
+
`Invalid value 'null' for options.replace_defaults. ` +
|
|
95
|
+
`Omit the option to use library defaults, or pass {} to start with no directives.`,
|
|
96
|
+
);
|
|
97
|
+
}
|
|
69
98
|
|
|
70
99
|
const directives: CspDirectives = {};
|
|
71
100
|
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
101
|
+
// `Object.entries` widens to `[string, unknown]` and TS can't re-narrow per-key, so
|
|
102
|
+
// writes via the validated `directive` need a single trusted-bridge cast — kept inside
|
|
103
|
+
// this helper so the call sites stay free of type assertions. Clones arrays so callers
|
|
104
|
+
// don't have to worry about user-supplied arrays leaking into the output.
|
|
105
|
+
const assign = (directive: CspDirective, value: unknown): void => {
|
|
106
|
+
(directives as Record<CspDirective, unknown>)[directive] = Array.isArray(value)
|
|
107
|
+
? [...value]
|
|
108
|
+
: value;
|
|
79
109
|
};
|
|
80
110
|
|
|
81
|
-
//
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// Get trust requirements (with overrides applied)
|
|
92
|
-
const trust_requirements: Map<CspDirective, CspTrustLevel | null> = new Map();
|
|
93
|
-
for (const spec of csp_directive_specs) {
|
|
94
|
-
const required_trust = required_trust_defaults[spec.name];
|
|
95
|
-
if (required_trust == null) continue; // omit null and undefined
|
|
96
|
-
|
|
97
|
-
trust_requirements.set(spec.name, required_trust);
|
|
98
|
-
}
|
|
111
|
+
// Stage 1: starting state from `replace_defaults`.
|
|
112
|
+
// `{}` starts blank — every directive must come from `extend`/`overrides`.
|
|
113
|
+
for_each_directive(replace_defaults, 'replace_defaults', (directive, value) => {
|
|
114
|
+
if (value === null) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`Invalid value 'null' for directive '${directive}' in options.replace_defaults. ` +
|
|
117
|
+
`Omit the key instead, or use \`overrides: { '${directive}': null }\` to remove.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
121
|
+
if (value === undefined) return;
|
|
122
|
+
assign(directive, value);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Stage 2: append sources per layer in `extend`.
|
|
126
|
+
if (extend?.length) {
|
|
127
|
+
for (const layer of extend) {
|
|
128
|
+
for_each_directive(layer, 'extend', (directive, value) => {
|
|
129
|
+
// `undefined` is treated as omitted, matching `replace_defaults`/`overrides`.
|
|
130
|
+
// Lets callers write `{'connect-src': cond ? [API] : undefined}` naturally.
|
|
131
|
+
if (value === undefined) return;
|
|
132
|
+
// `null` is meaningful enough to deserve its own error — users reaching for null
|
|
133
|
+
// in extend almost always want removal, which lives on `overrides`.
|
|
134
|
+
if (value === null) {
|
|
135
|
+
throw new Error(
|
|
136
|
+
`Cannot extend directive '${directive}' with null. ` +
|
|
137
|
+
`extend can only append sources — to remove a directive from the output, ` +
|
|
138
|
+
`use \`overrides: { '${directive}': null }\`.`,
|
|
139
|
+
);
|
|
108
140
|
}
|
|
109
|
-
|
|
141
|
+
if (!Array.isArray(value)) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`Cannot extend directive '${directive}': value must be an array of sources, got ${typeof value}.`,
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
if (!value.length) return;
|
|
147
|
+
const current = directives[directive];
|
|
148
|
+
if (is_none(current)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`Cannot extend directive '${directive}' while its current value is ['none']. ` +
|
|
151
|
+
`The pipeline runs replace_defaults → extend → overrides, so an \`overrides\` ` +
|
|
152
|
+
`entry for '${directive}' cannot rescue this — extend sees the ['none'] starting ` +
|
|
153
|
+
`value first. Opt in via \`replace_defaults: { '${directive}': [...] }\` or move ` +
|
|
154
|
+
`the sources into \`overrides: { '${directive}': [...] }\`.`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
if (current === undefined) {
|
|
158
|
+
assign(directive, [...new Set(value)]);
|
|
159
|
+
} else if (Array.isArray(current)) {
|
|
160
|
+
assign(directive, [...new Set([...current, ...value])]);
|
|
161
|
+
} else {
|
|
162
|
+
throw new Error(`Cannot extend directive '${directive}': it has a non-array value.`);
|
|
163
|
+
}
|
|
164
|
+
});
|
|
110
165
|
}
|
|
111
166
|
}
|
|
112
167
|
|
|
113
|
-
//
|
|
114
|
-
if (
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
// Skip if directive is ['none'] or not an array
|
|
122
|
-
if (is_none_directive(value) || !Array.isArray(value)) {
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Get required trust for this directive
|
|
127
|
-
const required_trust = trust_requirements.get(directive);
|
|
128
|
-
if (required_trust == null) continue;
|
|
129
|
-
|
|
130
|
-
// Add matching sources - separate the filtering into trust-based and directive-based inclusion
|
|
131
|
-
const sources_to_add = trusted_sources
|
|
132
|
-
.filter((spec) => {
|
|
133
|
-
// Check for explicit inclusion in directives list
|
|
134
|
-
const explicitly_included = spec.directives?.includes(directive) ?? false;
|
|
135
|
-
|
|
136
|
-
// Check for trust level based inclusion
|
|
137
|
-
const has_trust_level = spec.trust !== undefined;
|
|
138
|
-
const include_by_trust = has_trust_level && is_csp_trusted(required_trust, spec.trust);
|
|
139
|
-
|
|
140
|
-
// Include the source if either condition is met
|
|
141
|
-
return explicitly_included || include_by_trust;
|
|
142
|
-
})
|
|
143
|
-
.map((spec) => spec.source);
|
|
144
|
-
|
|
145
|
-
if (sources_to_add.length > 0) {
|
|
146
|
-
directives[directive] = [...value, ...sources_to_add] as CspDirectiveValue<any>;
|
|
168
|
+
// Stage 3: final-pass `overrides` — replace value or remove key.
|
|
169
|
+
if (overrides) {
|
|
170
|
+
for_each_directive(overrides, 'overrides', (directive, value) => {
|
|
171
|
+
if (value === null) {
|
|
172
|
+
delete directives[directive]; // eslint-disable-line @typescript-eslint/no-dynamic-delete
|
|
173
|
+
} else if (value !== undefined) {
|
|
174
|
+
assign(directive, value);
|
|
147
175
|
}
|
|
148
|
-
}
|
|
176
|
+
});
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
179
|
+
// Stage 4: output validation — empty arrays and mixed `'none'` are invalid CSP.
|
|
180
|
+
for (const [key, value] of Object.entries(directives)) {
|
|
181
|
+
if (typeof value === 'boolean') continue;
|
|
182
|
+
if (!Array.isArray(value)) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`Directive '${key}' has an invalid value: expected an array of sources or a boolean, ` +
|
|
185
|
+
`got ${value === null ? 'null' : typeof value}.`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
if (value.length === 0) {
|
|
189
|
+
throw new Error(
|
|
190
|
+
`Directive '${key}' has an empty array. ` +
|
|
191
|
+
`Use ['none'] to forbid all sources, or omit the directive entirely.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
// Element-level type check — the `CspSource` template-string type gates this at the
|
|
195
|
+
// type layer, but `as any` callers can slip non-strings through. A non-string source
|
|
196
|
+
// would render as `undefined` / `[object Object]` in the emitted CSP header.
|
|
197
|
+
for (let i = 0; i < value.length; i++) {
|
|
198
|
+
const v = (value as Array<unknown>)[i];
|
|
199
|
+
if (typeof v !== 'string') {
|
|
200
|
+
throw new Error(
|
|
201
|
+
`Directive '${key}' has a non-string source at index ${i}: got ${v === null ? 'null' : typeof v}.`,
|
|
202
|
+
);
|
|
168
203
|
}
|
|
169
204
|
}
|
|
205
|
+
if (value.length > 1 && (value as Array<unknown>).includes('none')) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Directive '${key}' has 'none' alongside other tokens (${value.join(', ')}). ` +
|
|
208
|
+
`'none' must appear alone in CSP.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
170
211
|
}
|
|
171
212
|
|
|
172
213
|
return directives;
|
|
173
|
-
}
|
|
214
|
+
};
|
|
174
215
|
|
|
175
216
|
export type CspDirective = keyof CspDirectives;
|
|
176
217
|
|
|
@@ -181,99 +222,57 @@ export const parse_csp_directive = (directive: unknown): CspDirective | null =>
|
|
|
181
222
|
|
|
182
223
|
export type CspDirectiveValue<T extends CspDirective> = Defined<CspDirectives[T]>;
|
|
183
224
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* Numeric values for CSP trust levels. See `csp_trust_levels`.
|
|
188
|
-
* Lower is less trusted.
|
|
189
|
-
* Includes `undefined` in the type for safety.
|
|
190
|
-
*/
|
|
191
|
-
export const csp_trust_level_value: Record<CspTrustLevel, number | undefined> = {
|
|
192
|
-
low: 0,
|
|
193
|
-
medium: 1,
|
|
194
|
-
high: 2,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* Trust levels for CSP sources.
|
|
199
|
-
*
|
|
200
|
-
* With the base defaults, trust levels roughly correspond to:
|
|
201
|
-
*
|
|
202
|
-
* - `low` – Passive resources only (no script execution, no styling or UI control).
|
|
203
|
-
* Examples: `img-src`, `font-src`.
|
|
204
|
-
* - `medium` – Content that may affect layout, styling, or embed external browsing contexts,
|
|
205
|
-
* but cannot directly run code in the page's JS execution environment or
|
|
206
|
-
* perform other high-risk actions. Examples: `style-src`, `frame-src`, `frame-ancestors`.
|
|
207
|
-
* - `high` – Sources that can execute code in the page's context or open powerful network
|
|
208
|
-
* channels. Examples: `script-src`, `connect-src`, `child-src`.
|
|
209
|
-
* - `null` – No trust. This is used for directives that don't support sources.
|
|
210
|
-
*
|
|
211
|
-
*/
|
|
212
|
-
export type CspTrustLevel = ArrayElement<typeof csp_trust_levels>;
|
|
213
|
-
|
|
214
|
-
/**
|
|
215
|
-
* Validates and extracts a CSP trust level from an unknown value.
|
|
216
|
-
*/
|
|
217
|
-
export const parse_csp_trust_level = (trust: unknown): CspTrustLevel | null =>
|
|
218
|
-
csp_trust_levels.includes(trust as any) ? (trust as CspTrustLevel) : null;
|
|
219
|
-
|
|
220
|
-
export interface CspSourceSpec {
|
|
221
|
-
source: CspSource;
|
|
222
|
-
trust?: CspTrustLevel;
|
|
223
|
-
directives?: Array<CspDirective>;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
export interface CspDirectiveSpec {
|
|
227
|
-
name: CspDirective;
|
|
228
|
-
fallback: Array<CspDirective> | null;
|
|
229
|
-
fallback_of: Array<CspDirective> | null;
|
|
230
|
-
}
|
|
225
|
+
const is_none = (value: unknown): boolean =>
|
|
226
|
+
Array.isArray(value) && value.length === 1 && value[0] === 'none';
|
|
231
227
|
|
|
232
228
|
/**
|
|
233
|
-
*
|
|
229
|
+
* Iterate over a per-directive options map, validating that every key is a known directive.
|
|
230
|
+
* Throws if any key fails to parse as a `CspDirective`, mentioning `source_label` so the
|
|
231
|
+
* error pinpoints which option (`replace_defaults`, `extend`, or `overrides`) was bad.
|
|
234
232
|
*
|
|
235
|
-
*
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
* - 'low' sources can only be used in low trust directives (lowest privilege)
|
|
233
|
+
* Also guards against non-object `source` values (e.g. `extend: [undefined]`, `extend: ['oops']`)
|
|
234
|
+
* so callers get a friendly library error instead of a cryptic native `TypeError` from
|
|
235
|
+
* `Object.entries`.
|
|
239
236
|
*/
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
const for_each_directive = <V>(
|
|
238
|
+
source: Record<string, V>,
|
|
239
|
+
source_label: 'replace_defaults' | 'extend' | 'overrides',
|
|
240
|
+
fn: (directive: CspDirective, value: V) => void,
|
|
241
|
+
): void => {
|
|
242
|
+
if (source === null || typeof source !== 'object') {
|
|
243
|
+
const got = source === null ? 'null' : typeof source;
|
|
244
|
+
throw new Error(`Invalid entry in options.${source_label}: expected an object, got ${got}.`);
|
|
245
|
+
}
|
|
246
|
+
for (const [key, value] of Object.entries(source)) {
|
|
247
|
+
const directive = parse_csp_directive(key);
|
|
248
|
+
if (directive === null) {
|
|
249
|
+
throw new Error(`Invalid directive in options.${source_label}: ${key}`);
|
|
250
|
+
}
|
|
251
|
+
fn(directive, value);
|
|
249
252
|
}
|
|
250
|
-
|
|
251
|
-
// A source with higher trust privilege (higher value)
|
|
252
|
-
// can be used in a directive with less privilege (lower value).
|
|
253
|
-
return granted_value >= required_value;
|
|
254
253
|
};
|
|
255
254
|
|
|
256
|
-
/**
|
|
257
|
-
* Helper to check if a directive value is `['none']`,
|
|
258
|
-
* or more precisely for robustness with malformed values, checks for an array with `'none'`.
|
|
259
|
-
*/
|
|
260
|
-
const is_none_directive = (value: unknown): boolean =>
|
|
261
|
-
Array.isArray(value) && value.includes('none');
|
|
262
|
-
|
|
263
255
|
export const COLOR_SCHEME_SCRIPT_HASH = 'sha256-QOxqn7EUzb3ydF9SALJoJGWSvywW9R0AfTDSenB83Z8=';
|
|
264
256
|
|
|
265
257
|
/**
|
|
266
|
-
* The
|
|
258
|
+
* The library CSP directive defaults — directives enabled out of the box.
|
|
267
259
|
* Prioritizes safety but loosens around media and styles, relying on defense-in-depth.
|
|
268
|
-
*
|
|
260
|
+
* WASM compile is allowed (`'wasm-unsafe-eval'` on `script-src` and `worker-src`); `eval` is not.
|
|
261
|
+
*
|
|
262
|
+
* Directives not listed here (`report-to`, `require-trusted-types-for`, `trusted-types`,
|
|
263
|
+
* `sandbox`) are intentionally absent by default — opt in via `replace_defaults` or `overrides`.
|
|
264
|
+
*
|
|
265
|
+
* Customizable via `CreateCspDirectivesOptions.replace_defaults`.
|
|
269
266
|
*/
|
|
270
|
-
export const csp_directive_value_defaults:
|
|
271
|
-
CspDirective
|
|
272
|
-
|
|
273
|
-
> = {
|
|
267
|
+
export const csp_directive_value_defaults: Partial<{
|
|
268
|
+
[K in CspDirective]: CspDirectiveValue<K>;
|
|
269
|
+
}> = {
|
|
274
270
|
'default-src': ['none'],
|
|
275
|
-
'
|
|
276
|
-
|
|
271
|
+
// `'wasm-unsafe-eval'` permits WASM compile/instantiate only — `eval` and `new Function`
|
|
272
|
+
// remain blocked. Needed for `@fuzdev/fuz_util/hash_blake3` and any other WASM in the page.
|
|
273
|
+
// Pre-2022 browsers ignore the keyword and block WASM; if you need them, override with `'unsafe-eval'`.
|
|
274
|
+
'script-src': ['self', 'wasm-unsafe-eval', COLOR_SCHEME_SCRIPT_HASH],
|
|
275
|
+
'script-src-elem': ['self', COLOR_SCHEME_SCRIPT_HASH], // Block script elements except for self and the color scheme loader (WASM compile is gated by script-src, not script-src-elem)
|
|
277
276
|
'script-src-attr': ['none'], // Block scripts in HTML attributes
|
|
278
277
|
'style-src': ['self', 'unsafe-inline'], // Main style directive (uses unsafe-inline but network connections are disallowed by other directives)
|
|
279
278
|
'style-src-elem': ['self', 'unsafe-inline'], // Style elements (standalone stylesheets)
|
|
@@ -287,57 +286,24 @@ export const csp_directive_value_defaults: Record<
|
|
|
287
286
|
'frame-src': ['self'], // Frames/iframes
|
|
288
287
|
'frame-ancestors': ['self'], // Control what can embed this page
|
|
289
288
|
'form-action': ['self'], // Form submission targets
|
|
290
|
-
'
|
|
289
|
+
// `'wasm-unsafe-eval'` mirrors the script-src allowance so WASM compiled inside a Web Worker also works.
|
|
290
|
+
'worker-src': ['self', 'blob:', 'wasm-unsafe-eval'], // Web workers
|
|
291
291
|
'object-src': ['none'], // Block plugins (Flash, Java, etc.)
|
|
292
292
|
'base-uri': ['none'], // Prevent base tag hijacking
|
|
293
293
|
'upgrade-insecure-requests': true, // Upgrade http to https
|
|
294
|
-
'report-to': null, // Report violations (e.g. `'/csp-violation-report'`)
|
|
295
|
-
'require-trusted-types-for': null,
|
|
296
|
-
'trusted-types': null,
|
|
297
|
-
sandbox: null,
|
|
298
294
|
};
|
|
299
295
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
* Feedback is welcome, please see the issues - https://github.com/fuzdev/fuz_ui/issues
|
|
306
|
-
*/
|
|
307
|
-
export const csp_directive_required_trust_defaults: Record<CspDirective, CspTrustLevel | null> = {
|
|
308
|
-
'default-src': null,
|
|
309
|
-
'script-src': 'high',
|
|
310
|
-
'script-src-elem': 'high',
|
|
311
|
-
'script-src-attr': null,
|
|
312
|
-
'style-src': 'medium',
|
|
313
|
-
'style-src-elem': 'medium',
|
|
314
|
-
'style-src-attr': 'medium',
|
|
315
|
-
'img-src': 'low',
|
|
316
|
-
'media-src': 'low',
|
|
317
|
-
'font-src': 'low',
|
|
318
|
-
'manifest-src': null,
|
|
319
|
-
'child-src': null,
|
|
320
|
-
'connect-src': 'medium',
|
|
321
|
-
'frame-src': 'medium',
|
|
322
|
-
'frame-ancestors': 'medium',
|
|
323
|
-
'form-action': 'medium',
|
|
324
|
-
'worker-src': 'medium',
|
|
325
|
-
'object-src': null,
|
|
326
|
-
'base-uri': null,
|
|
327
|
-
'upgrade-insecure-requests': null,
|
|
328
|
-
'report-to': null,
|
|
329
|
-
'require-trusted-types-for': null,
|
|
330
|
-
'trusted-types': null,
|
|
331
|
-
sandbox: null,
|
|
332
|
-
};
|
|
296
|
+
export interface CspDirectiveSpec {
|
|
297
|
+
name: CspDirective;
|
|
298
|
+
fallback: Array<CspDirective> | null;
|
|
299
|
+
fallback_of: Array<CspDirective> | null;
|
|
300
|
+
}
|
|
333
301
|
|
|
334
302
|
/**
|
|
335
303
|
* Static data descriptors for the CSP directives.
|
|
336
304
|
* Fuz excludes deprecated directives, so those are intentionally omitted,
|
|
337
305
|
* but any newer missing directives are bugs.
|
|
338
306
|
*
|
|
339
|
-
* Could be co-located but is currently here to keep that module smaller.
|
|
340
|
-
*
|
|
341
307
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Security-Policy}
|
|
342
308
|
*/
|
|
343
309
|
export const csp_directive_specs: Array<CspDirectiveSpec> = [
|
package/src/lib/csp_of_fuzdev.ts
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {CspDirectives} from './csp.ts';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* Per-directive sources that allow full interop between fuzdev/sister sites
|
|
5
|
+
* (`*.fuz.dev`, `*.zzz.software`) — loading assets and APIs from each other,
|
|
6
|
+
* and iframing in both directions.
|
|
7
|
+
*
|
|
8
|
+
* Pass into `create_csp_directives({extend: [csp_directives_of_fuzdev]})`.
|
|
9
|
+
* Intended for sites within the ecosystem; external apps usually want a
|
|
10
|
+
* narrower, hand-written allow-list.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately scoped — `script-src`, `style-src`, `worker-src`, etc. are not
|
|
13
|
+
* granted, even between sister sites. Add at the call site if a specific page
|
|
14
|
+
* needs them.
|
|
5
15
|
*/
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
]
|
|
16
|
+
export const csp_directives_of_fuzdev: Partial<CspDirectives> = {
|
|
17
|
+
'img-src': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
18
|
+
'media-src': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
19
|
+
'font-src': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
20
|
+
'connect-src': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
21
|
+
'frame-src': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
22
|
+
'frame-ancestors': ['https://*.fuz.dev/', 'https://*.zzz.software/'],
|
|
23
|
+
// `child-src` is intentionally omitted — it defaults to `['none']`, and `extend` cannot
|
|
24
|
+
// append to a `'none'`-valued directive. Including it here would break the common
|
|
25
|
+
// `create_csp_directives({extend: [csp_directives_of_fuzdev]})` call. `frame-src` is its
|
|
26
|
+
// fallback target anyway, so direct opt-in via `frame-src` is the supported path.
|
|
27
|
+
};
|
|
@@ -141,7 +141,7 @@ export const library_generate = (input: LibraryGenerateInput): LibraryGenerateRe
|
|
|
141
141
|
// Phase 1: Analyze all modules and collect re-exports
|
|
142
142
|
const source_json: SourceJson = {
|
|
143
143
|
name: package_json.name,
|
|
144
|
-
version: package_json.version,
|
|
144
|
+
version: package_json.version || '',
|
|
145
145
|
modules,
|
|
146
146
|
};
|
|
147
147
|
|
|
@@ -4,7 +4,7 @@ import {create_context} from './context_helpers.js';
|
|
|
4
4
|
|
|
5
5
|
// TODO maybe change this to a generic wrapper class for any value?
|
|
6
6
|
export class SelectedStyleVariable {
|
|
7
|
-
value: StyleVariable | null = $state()!;
|
|
7
|
+
value: StyleVariable | null = $state.raw()!;
|
|
8
8
|
|
|
9
9
|
constructor(initial: StyleVariable | null = null) {
|
|
10
10
|
this.value = initial;
|
|
@@ -13,8 +13,8 @@ export interface ThemeStateJson {
|
|
|
13
13
|
export type ThemeStateOptions = Partial<ThemeStateJson>;
|
|
14
14
|
|
|
15
15
|
export class ThemeState {
|
|
16
|
-
theme: Theme = $state()!;
|
|
17
|
-
color_scheme: ColorScheme = $state()!;
|
|
16
|
+
theme: Theme = $state.raw()!;
|
|
17
|
+
color_scheme: ColorScheme = $state.raw()!;
|
|
18
18
|
|
|
19
19
|
constructor(options?: ThemeStateOptions) {
|
|
20
20
|
const theme = options?.theme ?? default_themes[0]!;
|