From 514d0dd8e222ef316693ba1f488a13b639bae805 Mon Sep 17 00:00:00 2001 From: Sorin Davidoi Date: Wed, 26 Aug 2026 09:06:39 +0200 Subject: [PATCH] feat(workers): add support for trusted types Using `fuse.js/worker` currently fails for projects that use the `require-trusted-types-for` Content Security Policy directive: ``` This document requires 'TrustedScriptURL' assignment. The action has been blocked. _spawnWorker @fuse-worker.mjs:73 _init @fuse-worker.mjs:107 _ensureInit @fuse-worker.mjs:69 search @fuse-worker.mjs:130 ``` This patch fixes this by creating a `TrustedTypePolicy` called `fuse-trusted-worker-url` and spawning the web workers with a `TrustedTypeURL` instance created from the policy. It also changes the public API to allow `workerUrl` to be passed as a `TrustedScriptURL`, such that consumers can use their own policies. This change has should have no impact on consumers that don't make use of the `require-trusted-types-for` directive. See https://developer.mozilla.org/en-US/docs/Web/API/Trusted_Types_API. Inspired by https://github.com/sveltejs/svelte/pull/16271. --- dist/fuse-worker.cjs | 7 ++++++- dist/fuse-worker.d.cts | 17 ++++++++++++++++- dist/fuse-worker.d.ts | 17 ++++++++++++++++- dist/fuse-worker.mjs | 7 ++++++- dist/fuse.d.cts | 15 +++++++++++++++ dist/fuse.d.ts | 15 +++++++++++++++ src/types.ts | 18 ++++++++++++++++++ src/workers/FuseWorker.ts | 27 +++++++++++++++++++++++---- 8 files changed, 115 insertions(+), 8 deletions(-) diff --git a/dist/fuse-worker.cjs b/dist/fuse-worker.cjs index 05f6f32f6..8f0910c88 100644 --- a/dist/fuse-worker.cjs +++ b/dist/fuse-worker.cjs @@ -15,6 +15,7 @@ const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTok //#endregion //#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; +const TRUSTED_TYPES_POLICY = globalThis?.window?.trustedTypes?.createPolicy("fuse-trusted-worker-url", { createScriptURL: (url) => url }); function getDefaultWorkerCount() { const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4; return Math.min(hw, DEFAULT_MAX_WORKERS); @@ -41,7 +42,11 @@ var FuseWorker = class FuseWorker { this._workerOptions = workerOptions || {}; FuseWorker._assertNoFunctionOptions(this._options); if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); - this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl(); + if (this._workerOptions.workerUrl) this._workerUrl = this._workerOptions.workerUrl; + else { + const defaultWorkerUrl = resolveDefaultWorkerUrl(); + this._workerUrl = TRUSTED_TYPES_POLICY ? TRUSTED_TYPES_POLICY.createScriptURL(defaultWorkerUrl.toString()) : defaultWorkerUrl; + } } static _assertNoFunctionOptions(options) { if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn")); diff --git a/dist/fuse-worker.d.cts b/dist/fuse-worker.d.cts index c6c78963a..e8bfea893 100644 --- a/dist/fuse-worker.d.cts +++ b/dist/fuse-worker.d.cts @@ -201,13 +201,28 @@ type Expression = string | KeyedLeaf | PathLeafString | { } | { $or?: ChildExpression[]; }; +/** https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL */ +type TrustedScriptURL = { + toString(): string; +}; +declare global { + interface Window { + trustedTypes: { + createPolicy: (name: string, options: { + createScriptURL: (url: string) => string; + }) => { + createScriptURL: (url: string) => TrustedScriptURL; + }; + }; + } +} //#endregion //#region src/workers/FuseWorker.d.ts interface FuseWorkerOptions { /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */ numWorkers?: number; /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */ - workerUrl?: string | URL; + workerUrl?: string | URL | TrustedScriptURL; } declare class FuseWorker { private _options; diff --git a/dist/fuse-worker.d.ts b/dist/fuse-worker.d.ts index c6c78963a..e8bfea893 100644 --- a/dist/fuse-worker.d.ts +++ b/dist/fuse-worker.d.ts @@ -201,13 +201,28 @@ type Expression = string | KeyedLeaf | PathLeafString | { } | { $or?: ChildExpression[]; }; +/** https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL */ +type TrustedScriptURL = { + toString(): string; +}; +declare global { + interface Window { + trustedTypes: { + createPolicy: (name: string, options: { + createScriptURL: (url: string) => string; + }) => { + createScriptURL: (url: string) => TrustedScriptURL; + }; + }; + } +} //#endregion //#region src/workers/FuseWorker.d.ts interface FuseWorkerOptions { /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */ numWorkers?: number; /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */ - workerUrl?: string | URL; + workerUrl?: string | URL | TrustedScriptURL; } declare class FuseWorker { private _options; diff --git a/dist/fuse-worker.mjs b/dist/fuse-worker.mjs index 1a6ddc4ec..691006f4f 100644 --- a/dist/fuse-worker.mjs +++ b/dist/fuse-worker.mjs @@ -13,6 +13,7 @@ const FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED = "FuseWorker does not support useTok //#endregion //#region src/workers/FuseWorker.ts const DEFAULT_MAX_WORKERS = 8; +const TRUSTED_TYPES_POLICY = globalThis?.window?.trustedTypes?.createPolicy("fuse-trusted-worker-url", { createScriptURL: (url) => url }); function getDefaultWorkerCount() { const hw = typeof navigator !== "undefined" ? navigator.hardwareConcurrency || 4 : 4; return Math.min(hw, DEFAULT_MAX_WORKERS); @@ -38,7 +39,11 @@ var FuseWorker = class FuseWorker { this._workerOptions = workerOptions || {}; FuseWorker._assertNoFunctionOptions(this._options); if (this._options.useTokenSearch) throw new Error(FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED); - this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl(); + if (this._workerOptions.workerUrl) this._workerUrl = this._workerOptions.workerUrl; + else { + const defaultWorkerUrl = resolveDefaultWorkerUrl(); + this._workerUrl = TRUSTED_TYPES_POLICY ? TRUSTED_TYPES_POLICY.createScriptURL(defaultWorkerUrl.toString()) : defaultWorkerUrl; + } } static _assertNoFunctionOptions(options) { if (typeof options.sortFn === "function") throw new Error(FUSE_WORKER_UNSUPPORTED_FN_OPTION("sortFn")); diff --git a/dist/fuse.d.cts b/dist/fuse.d.cts index 2d123f90e..b91508d29 100644 --- a/dist/fuse.d.cts +++ b/dist/fuse.d.cts @@ -288,6 +288,21 @@ type Expression = string | KeyedLeaf | PathLeafString | { } | { $or?: ChildExpression[]; }; +/** https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL */ +type TrustedScriptURL = { + toString(): string; +}; +declare global { + interface Window { + trustedTypes: { + createPolicy: (name: string, options: { + createScriptURL: (url: string) => string; + }) => { + createScriptURL: (url: string) => TrustedScriptURL; + }; + }; + } +} //#endregion //#region src/tools/FuseIndex.d.ts declare class FuseIndex { diff --git a/dist/fuse.d.ts b/dist/fuse.d.ts index 295642824..aa2e9c179 100644 --- a/dist/fuse.d.ts +++ b/dist/fuse.d.ts @@ -288,6 +288,21 @@ type Expression = string | KeyedLeaf | PathLeafString | { } | { $or?: ChildExpression[]; }; +/** https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL */ +type TrustedScriptURL = { + toString(): string; +}; +declare global { + interface Window { + trustedTypes: { + createPolicy: (name: string, options: { + createScriptURL: (url: string) => string; + }) => { + createScriptURL: (url: string) => TrustedScriptURL; + }; + }; + } +} //#endregion //#region src/tools/FuseIndex.d.ts declare class FuseIndex { diff --git a/src/types.ts b/src/types.ts index cacaf5d8d..a4402dd70 100644 --- a/src/types.ts +++ b/src/types.ts @@ -369,3 +369,21 @@ export type Expression = | PathLeafString | { $and?: ChildExpression[] } | { $or?: ChildExpression[] } + +/** https://developer.mozilla.org/en-US/docs/Web/API/TrustedScriptURL */ +export type TrustedScriptURL = { + toString(): string; +} + +declare global { +interface Window { + trustedTypes: { + createPolicy: (name: string, options: { + createScriptURL: (url: string) => string, + })=> { + + createScriptURL: (url: string) => TrustedScriptURL, + }, + } +} +} diff --git a/src/workers/FuseWorker.ts b/src/workers/FuseWorker.ts index 428327eb7..a48a02367 100644 --- a/src/workers/FuseWorker.ts +++ b/src/workers/FuseWorker.ts @@ -6,14 +6,15 @@ import type { FuseOptionKey, FuseResult, FuseSearchOptions, - Expression + Expression, + TrustedScriptURL } from '../types' export interface FuseWorkerOptions { /** Number of parallel workers. Defaults to navigator.hardwareConcurrency (max 8). */ numWorkers?: number /** Custom URL to the worker script. If not provided, resolves automatically via import.meta.url. */ - workerUrl?: string | URL + workerUrl?: string | URL | TrustedScriptURL } interface PendingCall { @@ -28,6 +29,13 @@ interface Shard { const DEFAULT_MAX_WORKERS = 8 +const TRUSTED_TYPES_POLICY = globalThis?.window?.trustedTypes?.createPolicy( + 'fuse-trusted-worker-url', + { + createScriptURL: (url) => url + } +) + function getDefaultWorkerCount(): number { const hw = typeof navigator !== 'undefined' ? navigator.hardwareConcurrency || 4 : 4 @@ -89,7 +97,7 @@ export default class FuseWorker { private _initPromise: Promise | null = null private _pending: Map = new Map() private _nextId = 0 - private _workerUrl: string | URL + private _workerUrl: string | URL | TrustedScriptURL constructor( docs: ReadonlyArray, @@ -108,7 +116,17 @@ export default class FuseWorker { if (this._options.useTokenSearch) { throw new Error(ErrorMsg.FUSE_WORKER_TOKEN_SEARCH_UNSUPPORTED) } - this._workerUrl = this._workerOptions.workerUrl || resolveDefaultWorkerUrl() + + if (this._workerOptions.workerUrl) { + // Don't apply the trusted types policy as workerUrl is outside our control + this._workerUrl = this._workerOptions.workerUrl + } else { + const defaultWorkerUrl = resolveDefaultWorkerUrl() + + this._workerUrl = TRUSTED_TYPES_POLICY + ? TRUSTED_TYPES_POLICY.createScriptURL(defaultWorkerUrl.toString()) + : defaultWorkerUrl + } } private static _assertNoFunctionOptions(options: IFuseOptions): void { @@ -152,6 +170,7 @@ export default class FuseWorker { } private _spawnWorker(): Worker { + // @ts-expect-error the types for Worker are not updated yet to accept TrustedScriptURL const worker = new Worker(this._workerUrl, { type: 'module' }) worker.onmessage = (e: MessageEvent) => {