@bytescale/sdk 3.60.0 → 3.62.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.
@@ -1,12 +1,6 @@
1
1
  import { jest } from "@jest/globals";
2
- import { readFileSync } from "node:fs";
3
- import { resolve } from "node:path";
4
- import { runInNewContext } from "node:vm";
5
- import { Headers as NodeFetchHeaders, Request as NodeFetchRequest, Response as NodeFetchResponse } from "node-fetch";
6
- import type { RequestInit as NodeFetchRequestInit } from "node-fetch";
7
- import type { AuthSwConfigEntryDto, UrlRewriteRule } from "../src/index.browser";
8
-
9
- const workerSource = readFileSync(resolve(process.cwd(), "src/index.auth-sw.js"), "utf8");
2
+ import { Response as NodeFetchResponse } from "node-fetch";
3
+ import { AuthServiceWorkerHarness, authConfig, authConfigForUrl, rewriteRule } from "./utils/AuthServiceWorkerHarness";
10
4
 
11
5
  describe("Auth service-worker URL rewriting", () => {
12
6
  test("preserves the remaining path, query string, and fragment", async () => {
@@ -24,7 +18,11 @@ describe("Auth service-worker URL rewriting", () => {
24
18
  );
25
19
  });
26
20
 
27
- test("matches authentication against the rewritten URL and supports navigation requests", async () => {
21
+ test.each(
22
+ [undefined, ["https://dashboard.example.com/download/"]].map((prefixes): { prefixes: typeof prefixes } => ({
23
+ prefixes
24
+ }))
25
+ )("preserves streaming, range, and navigation behavior with request prefixes $prefixes", async ({ prefixes }) => {
28
26
  const upstreamResponse = new NodeFetchResponse("streamed-body", {
29
27
  headers: {
30
28
  "Accept-Ranges": "bytes",
@@ -38,7 +36,16 @@ describe("Auth service-worker URL rewriting", () => {
38
36
  const blob = jest.spyOn(upstreamResponse, "blob");
39
37
  const arrayBuffer = jest.spyOn(upstreamResponse, "arrayBuffer");
40
38
  const harness = new AuthServiceWorkerHarness(upstreamResponse);
41
- await harness.setConfig([authConfig("account-a/", "token-a")], [rewriteRule("download/", "account-a/")]);
39
+ await harness.setConfig(
40
+ [
41
+ {
42
+ ...authConfig("account-a/", "token-a"),
43
+ requestUrlPrefixes: prefixes,
44
+ urlPrefix: `${prefixes === undefined ? "" : "!bytescale-request-scoped!"}https://upcdn.io/account-a/`
45
+ }
46
+ ],
47
+ [rewriteRule("download/", "account-a/")]
48
+ );
42
49
 
43
50
  const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf", {
44
51
  headers: {
@@ -213,145 +220,3 @@ describe("Auth service-worker URL rewriting", () => {
213
220
  expect(result.outboundRequest?.headers.get("X-Trace-Id")).toBe("trace");
214
221
  });
215
222
  });
216
-
217
- interface FetchOptions {
218
- clientId?: string;
219
- headers?: HeadersInit;
220
- navigation?: boolean;
221
- }
222
-
223
- interface FetchResult {
224
- outboundRequest: TestRequest | undefined;
225
- responded: boolean;
226
- response: NodeFetchResponse | undefined;
227
- }
228
-
229
- type WorkerEventListener = (event: unknown) => void;
230
-
231
- class AuthServiceWorkerHarness {
232
- private readonly clientsById = new Map<string, { type: "window"; url: string }>();
233
- private readonly cacheEntries = new Map<string, NodeFetchResponse>();
234
- private cacheReadDelayMilliseconds = 0;
235
- private readonly context: {
236
- getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
237
- setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
238
- };
239
-
240
- private readonly fetchListener: WorkerEventListener;
241
- private readonly fetchMock: jest.MockedFunction<(request: TestRequest) => Promise<NodeFetchResponse>>;
242
-
243
- constructor(private readonly upstreamResponse: NodeFetchResponse = new NodeFetchResponse("ok")) {
244
- const listeners = new Map<string, WorkerEventListener>();
245
- this.fetchMock = jest.fn(async (_request: TestRequest): Promise<NodeFetchResponse> => this.upstreamResponse);
246
-
247
- const self = {
248
- addEventListener: (type: string, listener: WorkerEventListener): void => {
249
- listeners.set(type, listener);
250
- },
251
- clients: {
252
- claim: async (): Promise<void> => {},
253
- get: async (clientId: string): Promise<{ type: "window"; url: string } | undefined> =>
254
- this.clientsById.get(clientId)
255
- },
256
- skipWaiting: async (): Promise<void> => {}
257
- };
258
- const cache = {
259
- match: async (key: string): Promise<NodeFetchResponse | undefined> => {
260
- if (this.cacheReadDelayMilliseconds > 0) {
261
- await new Promise(resolve => setTimeout(resolve, this.cacheReadDelayMilliseconds));
262
- }
263
- return this.cacheEntries.get(key)?.clone();
264
- },
265
- put: async (key: string, value: NodeFetchResponse): Promise<void> => {
266
- this.cacheEntries.set(key, value.clone());
267
- }
268
- };
269
- const sandbox = {
270
- caches: { open: async (): Promise<typeof cache> => cache },
271
- console: { error: jest.fn(), log: jest.fn() },
272
- fetch: this.fetchMock,
273
- Headers: NodeFetchHeaders,
274
- Promise,
275
- Request: TestRequest,
276
- Response: NodeFetchResponse,
277
- self,
278
- setTimeout
279
- };
280
- runInNewContext(workerSource, sandbox);
281
-
282
- this.context = sandbox as typeof sandbox & AuthServiceWorkerHarness["context"];
283
- const fetchListener = listeners.get("fetch");
284
- if (fetchListener === undefined) {
285
- throw new Error("Auth service worker did not register a fetch listener.");
286
- }
287
- this.fetchListener = fetchListener;
288
- }
289
-
290
- async setConfig(config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]): Promise<void> {
291
- await this.context.setConfig(config, rules);
292
- }
293
-
294
- getRewrittenUrl(url: string, rules: UrlRewriteRule[]): string | undefined {
295
- return this.context.getRewrittenUrl(url, rules);
296
- }
297
-
298
- setWindowClient(clientId: string, url: string): void {
299
- this.clientsById.set(clientId, { type: "window", url });
300
- }
301
-
302
- setPersistentConfig(config: AuthSwConfigEntryDto[], cacheReadDelayMilliseconds: number): void {
303
- this.cacheEntries.set("config", new NodeFetchResponse(JSON.stringify(config)));
304
- this.cacheReadDelayMilliseconds = cacheReadDelayMilliseconds;
305
- }
306
-
307
- async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
308
- const request = new TestRequest(url, { headers: options.headers as NodeFetchRequestInit["headers"] });
309
- if (options.navigation === true) {
310
- Object.defineProperty(request, "mode", { configurable: true, value: "navigate" });
311
- }
312
-
313
- let responsePromise: Promise<NodeFetchResponse> | undefined;
314
- this.fetchListener({
315
- clientId: options.clientId ?? "",
316
- request,
317
- respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
318
- responsePromise = Promise.resolve(response);
319
- }
320
- });
321
-
322
- const response = await responsePromise;
323
- return {
324
- outboundRequest: this.fetchMock.mock.calls.at(-1)?.[0],
325
- responded: responsePromise !== undefined,
326
- response
327
- };
328
- }
329
- }
330
-
331
- class TestRequest extends NodeFetchRequest {
332
- readonly mode: RequestMode;
333
-
334
- constructor(input: string | NodeFetchRequest, init: NodeFetchRequestInit & { mode?: RequestMode } = {}) {
335
- super(input, init);
336
- this.mode = init.mode ?? (input instanceof TestRequest ? input.mode : "cors");
337
- }
338
- }
339
-
340
- function authConfig(accountPath: string, token: string): AuthSwConfigEntryDto {
341
- return authConfigForUrl(`https://upcdn.io/${accountPath}`, token);
342
- }
343
-
344
- function authConfigForUrl(urlPrefix: string, token: string): AuthSwConfigEntryDto {
345
- return {
346
- expires: undefined,
347
- headers: [{ key: "Authorization", value: `Bearer ${token}` }],
348
- urlPrefix
349
- };
350
- }
351
-
352
- function rewriteRule(fromPath: string, toPath: string): UrlRewriteRule {
353
- return {
354
- fromUrlPrefix: `https://dashboard.example.com/${fromPath}`,
355
- toUrlPrefix: `https://upcdn.io/${toPath}`
356
- };
357
- }
@@ -0,0 +1,135 @@
1
+ import { jest } from "@jest/globals";
2
+ import { Scheduler } from "../src/private/Scheduler";
3
+
4
+ class TestEventTarget {
5
+ private readonly listeners = new Map<string, Set<EventListener>>();
6
+ visibilityState: DocumentVisibilityState = "visible";
7
+
8
+ addEventListener(type: string, listener: EventListener): void {
9
+ const listeners = this.listeners.get(type) ?? new Set<EventListener>();
10
+ listeners.add(listener);
11
+ this.listeners.set(type, listeners);
12
+ }
13
+
14
+ removeEventListener(type: string, listener: EventListener): void {
15
+ this.listeners.get(type)?.delete(listener);
16
+ }
17
+
18
+ dispatch(type: string): void {
19
+ for (const listener of this.listeners.get(type) ?? []) {
20
+ listener(new Event(type));
21
+ }
22
+ }
23
+
24
+ listenerCount(type: string): number {
25
+ return this.listeners.get(type)?.size ?? 0;
26
+ }
27
+ }
28
+
29
+ describe("Scheduler", () => {
30
+ const originalDocument = Object.getOwnPropertyDescriptor(globalThis, "document");
31
+ const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
32
+ let documentEvents: TestEventTarget;
33
+ let windowEvents: TestEventTarget;
34
+ let now: number;
35
+
36
+ beforeEach(() => {
37
+ jest.useFakeTimers();
38
+ now = 1000;
39
+ jest.spyOn(Date, "now").mockImplementation(() => now);
40
+ documentEvents = new TestEventTarget();
41
+ windowEvents = new TestEventTarget();
42
+ Object.defineProperty(globalThis, "document", { configurable: true, value: documentEvents });
43
+ Object.defineProperty(globalThis, "window", { configurable: true, value: windowEvents });
44
+ });
45
+
46
+ afterEach(() => {
47
+ jest.useRealTimers();
48
+ jest.restoreAllMocks();
49
+ restoreGlobal("document", originalDocument);
50
+ restoreGlobal("window", originalWindow);
51
+ });
52
+
53
+ test.each(["focus", "online", "pageshow"])("runs overdue callbacks immediately on %s", eventName => {
54
+ const scheduler = new Scheduler();
55
+ const callback = jest.fn();
56
+ scheduler.schedule(2000, callback);
57
+
58
+ now = 3000;
59
+ windowEvents.dispatch(eventName);
60
+
61
+ expect(callback).toHaveBeenCalledTimes(1);
62
+ });
63
+
64
+ test("runs overdue callbacks when a visible document resumes", () => {
65
+ const scheduler = new Scheduler();
66
+ const callback = jest.fn();
67
+ scheduler.schedule(2000, callback);
68
+
69
+ now = 3000;
70
+ documentEvents.visibilityState = "hidden";
71
+ documentEvents.dispatch("visibilitychange");
72
+ expect(callback).not.toHaveBeenCalled();
73
+
74
+ documentEvents.visibilityState = "visible";
75
+ documentEvents.dispatch("visibilitychange");
76
+ expect(callback).toHaveBeenCalledTimes(1);
77
+ });
78
+
79
+ test("runs scheduled callbacks immediately after a material backwards clock change", () => {
80
+ const scheduler = new Scheduler();
81
+ const callback = jest.fn();
82
+ now = 10_000;
83
+ scheduler.schedule(20_000, callback);
84
+
85
+ now = 8000;
86
+ documentEvents.dispatch("resume");
87
+
88
+ expect(callback).toHaveBeenCalledTimes(1);
89
+ });
90
+
91
+ test("retains normal polling behavior", () => {
92
+ const scheduler = new Scheduler();
93
+ const callback = jest.fn();
94
+ scheduler.schedule(2000, callback);
95
+
96
+ now = 1999;
97
+ jest.advanceTimersByTime(1000);
98
+ expect(callback).not.toHaveBeenCalled();
99
+
100
+ now = 2000;
101
+ jest.advanceTimersByTime(1000);
102
+ expect(callback).toHaveBeenCalledTimes(1);
103
+ });
104
+
105
+ test("does not refresh early for a small clock correction", () => {
106
+ const scheduler = new Scheduler();
107
+ const callback = jest.fn();
108
+ const handle = scheduler.schedule(20_000, callback);
109
+
110
+ now = 500;
111
+ windowEvents.dispatch("focus");
112
+
113
+ expect(callback).not.toHaveBeenCalled();
114
+ scheduler.unschedule(handle);
115
+ });
116
+
117
+ test("removes lifecycle listeners after the final callback is removed", () => {
118
+ const scheduler = new Scheduler();
119
+ const handle = scheduler.schedule(20_000, jest.fn());
120
+
121
+ expect(windowEvents.listenerCount("focus")).toBe(1);
122
+ expect(documentEvents.listenerCount("resume")).toBe(1);
123
+ scheduler.unschedule(handle);
124
+ expect(windowEvents.listenerCount("focus")).toBe(0);
125
+ expect(documentEvents.listenerCount("resume")).toBe(0);
126
+ });
127
+ });
128
+
129
+ function restoreGlobal(name: "document" | "window", descriptor: PropertyDescriptor | undefined): void {
130
+ if (descriptor === undefined) {
131
+ Reflect.deleteProperty(globalThis, name);
132
+ } else {
133
+ Object.defineProperty(globalThis, name, descriptor);
134
+ }
135
+ }
@@ -80,6 +80,35 @@ describe("UploadManager AuthManager configuration", () => {
80
80
  await expect(manager.upload({ data: "x" })).rejects.toThrow("provide an API key");
81
81
  expect(uploadApi.beginMultipartUpload).not.toHaveBeenCalled();
82
82
  });
83
+
84
+ test("awaits manager-owned authentication before starting an upload", async () => {
85
+ const configState = state("customer", accountA);
86
+ configState.expiresAt = Date.now();
87
+ let completeRefresh = (): void => {
88
+ throw new Error("Refresh completion callback was not initialized.");
89
+ };
90
+ const refreshPromise = new Promise<void>(resolve => {
91
+ completeRefresh = () => {
92
+ configState.accessToken = "access-token-new";
93
+ configState.expiresAt = Date.now() + 60_000;
94
+ configState.jwt = "jwt-new";
95
+ configState.refreshPromise = undefined;
96
+ resolve();
97
+ };
98
+ });
99
+ configState.authenticationPromise = refreshPromise;
100
+ configState.refreshPromise = refreshPromise;
101
+ setSession(configState);
102
+ const manager = new TestUploadManager({ authConfigId: "customer" });
103
+ const uploadApi = installUploadApiMock(manager);
104
+
105
+ const upload = manager.upload({ data: "x" });
106
+ expect(uploadApi.beginMultipartUpload).not.toHaveBeenCalled();
107
+ completeRefresh();
108
+ await upload;
109
+
110
+ expect(uploadApi.beginMultipartUpload).toHaveBeenCalled();
111
+ });
83
112
  });
84
113
 
85
114
  interface UploadApiMock {
@@ -127,6 +156,7 @@ function installUploadApiMock(manager: TestUploadManager): UploadApiMock {
127
156
  function state(authConfigId: string, accountId: string): AuthSessionConfigState {
128
157
  return {
129
158
  accessToken: "access-token",
159
+ authenticationPromise: Promise.resolve(),
130
160
  config: {
131
161
  accountId,
132
162
  authConfigId,
@@ -0,0 +1,272 @@
1
+ // Frozen worker from SDK 3.61.0 (ee6a3a2d), retained for compatibility tests.
2
+ /* eslint-disable no-undef */
3
+ /**
4
+ * Bytescale Auth Service Worker (SW)
5
+ *
6
+ * This script should be referenced by the "serviceWorkerScript" field in the "AuthManager.beginAuthSession" method of
7
+ * the Bytescale JavaScript SDK to append "Authorization" headers to HTTP requests sent to the Bytescale CDN. This
8
+ * approach serves as an alternative to cookie-based authentication, which is incompatible with certain modern browsers.
9
+ *
10
+ * Documentation:
11
+ * - https://www.bytescale.com/docs/types/BeginAuthSessionParams#serviceWorkerScript
12
+ */
13
+ let transientCache; // [{urlPrefix, headers, expires?}] (See: AuthSwConfigDto)
14
+ let transientUrlRewriteRules;
15
+ const maxSourceUrlCacheEntries = 1000;
16
+ const persistentCacheName = "bytescale-sw-config";
17
+ const persistentCacheKey = "config";
18
+ const persistentUrlRewriteRulesCacheKey = "url-rewrite-rules";
19
+ const sourceScopedUrlPrefixMarker = "!bytescale-source-scoped!";
20
+ const sourceUrlsByClientId = new Map();
21
+
22
+ console.log(`[bytescale] Auth SW Registered`);
23
+
24
+ self.addEventListener("install", function (event) {
25
+ // Typically service workers go: 'installing' -> 'waiting' -> 'activated'.
26
+ // However, we skip the 'waiting' phase as we want this service worker to be used immediately after it's installed,
27
+ // instead of requiring a page refresh if the browser already has an old version of the service worker installed.
28
+ event.waitUntil(self.skipWaiting());
29
+ });
30
+
31
+ self.addEventListener("activate", function (event) {
32
+ // Immediately allow the service worker to intercept "fetch" events (instead of requiring a page refresh) if this is
33
+ // the first time this service worker is being installed.
34
+ event.waitUntil(self.clients.claim());
35
+ });
36
+
37
+ self.addEventListener("message", event => {
38
+ // Allows communication with the windows/tabs that have are able to generate the JWT (as they have the auth session with the user's API).
39
+ // See: AuthSwSetConfigDto
40
+ if (event.data) {
41
+ switch (event.data.type) {
42
+ // Auth sessions are started/ended by calling SET_CONFIG with auth config or with 'undefined' config, respectively.
43
+ // We use 'undefined' to end the auth session instead of unregistering the worker, as there may be multiple tabs
44
+ // in the user's application, so while the user may sign out in one tab, they may remain signed in to another tab,
45
+ // which may subsequently send a follow-up 'SET_CONFIG' which will resume auth.
46
+ case "SET_BYTESCALE_AUTH_CONFIG":
47
+ setConfig(event.data.config, event.data.urlRewriteRules).then(
48
+ () => {},
49
+ e => console.error(`[bytescale] Auth SW failed to persist config.`, e)
50
+ );
51
+ break;
52
+ }
53
+ }
54
+ });
55
+
56
+ self.addEventListener("fetch", function (event) {
57
+ // Faster and intercepts only the required requests.
58
+ // Called in almost all cases.
59
+ const interceptSync = config => {
60
+ const newRequest = interceptRequest(event, config, transientUrlRewriteRules);
61
+ if (newRequest instanceof Promise) {
62
+ event.respondWith(
63
+ newRequest.then(
64
+ request => handleRequestErrors(request ?? event.request),
65
+ () => handleRequestErrors(event.request)
66
+ )
67
+ );
68
+ } else if (newRequest !== undefined) {
69
+ event.respondWith(handleRequestErrors(newRequest));
70
+ }
71
+ };
72
+
73
+ // Slower and intercepts all requests (while still only rewriting the relevant requests).
74
+ // Called only for the initial request after this Service Worker is restarted after going idle (e.g. after 30s on Firefox/Windows).
75
+ const interceptAsync = async () =>
76
+ await handleRequestErrors(
77
+ (await getState()
78
+ .then(state => (state === undefined ? undefined : interceptRequest(event, state.config, state.urlRewriteRules)))
79
+ .catch(() => undefined)) ?? event.request
80
+ );
81
+
82
+ // Makes it clearer to developers that the request failed for normal reasons (not reasons caused by this script).
83
+ const handleRequestErrors = async request => {
84
+ try {
85
+ return await fetch(request);
86
+ } catch (e) {
87
+ throw new Error("Network request failed: see previous browser errors for the cause.");
88
+ }
89
+ };
90
+
91
+ // Optimization: avoids running async code (which necessitates intercepting all requests) when the config is already cached locally.
92
+ if (transientCache !== undefined && transientUrlRewriteRules !== undefined) {
93
+ interceptSync(transientCache);
94
+ } else {
95
+ event.respondWith(interceptAsync());
96
+ }
97
+ });
98
+
99
+ function interceptRequest(event, config, urlRewriteRules) {
100
+ const rewrittenUrl = getRewrittenUrl(event.request.url, urlRewriteRules);
101
+ const url = rewrittenUrl === undefined ? event.request.url : rewrittenUrl;
102
+ const fallbackRequest =
103
+ rewrittenUrl === undefined ? undefined : createCorsRequest(event.request, rewrittenUrl, event.request.headers);
104
+
105
+ if (config !== undefined) {
106
+ // Config is an array to support multiple different accounts within a single website, if needed.
107
+ for (const { expires, urlPrefix, headers, sourceUrlPrefixes } of config) {
108
+ const makeNewRequest = overwrite => {
109
+ const newHeaders = new Headers(event.request.headers);
110
+ if (overwrite) {
111
+ newHeaders.delete("Authorization");
112
+ newHeaders.delete("Authorization-Token");
113
+ }
114
+ for (const { key, value } of headers) {
115
+ if (overwrite || !newHeaders.has(key)) {
116
+ newHeaders.set(key, value);
117
+ }
118
+ }
119
+ return createCorsRequest(event.request, url, newHeaders);
120
+ };
121
+
122
+ if (expires === undefined || expires > Date.now()) {
123
+ const isSourceScoped = sourceUrlPrefixes !== undefined;
124
+
125
+ // AuthManager adds the 'sourceScopedUrlPrefixMarker' prefix to the 'urlPrefix' when 'sourceUrlPrefixes' is provided,
126
+ // as this prevents old versions of the service worker (that don't support 'sourceUrlPrefixes') from intercepting the requests,
127
+ // since it would end up intercepting ALL requests, whereas the user's intention is to intercept only source-filtered requests.
128
+ const actualUrlPrefix = isSourceScoped
129
+ ? urlPrefix.startsWith(sourceScopedUrlPrefixMarker)
130
+ ? urlPrefix.substring(sourceScopedUrlPrefixMarker.length)
131
+ : undefined
132
+ : urlPrefix;
133
+
134
+ if (
135
+ actualUrlPrefix !== undefined &&
136
+ url.startsWith(actualUrlPrefix) &&
137
+ event.request.method.toUpperCase() === "GET"
138
+ ) {
139
+ if (isSourceScoped) {
140
+ if (!Array.isArray(sourceUrlPrefixes) || sourceUrlPrefixes.length === 0) {
141
+ return fallbackRequest;
142
+ }
143
+ return getSourceUrl(event.clientId).then(sourceUrl => {
144
+ if (sourceUrl === undefined || !sourceUrlPrefixes.some(prefix => sourceUrl.startsWith(prefix))) {
145
+ return fallbackRequest;
146
+ }
147
+
148
+ // Overwrite existing auth headers that may have been set by other broad-match authorizers (i.e. AuthManager
149
+ // instances that were configured without any sourceUrlPrefixes specified).
150
+ return makeNewRequest(true);
151
+ });
152
+ }
153
+
154
+ // Do not overwrite existing auth headers, as this is a broad-match authorizer (i.e. AuthManager was run
155
+ // without any 'sourceUrlPrefixes' specified), so give priority to AuthManagers where sourceUrlPrefixes is specified.
156
+ return makeNewRequest(false);
157
+ }
158
+ }
159
+ }
160
+ }
161
+
162
+ return fallbackRequest;
163
+ }
164
+
165
+ function getRewrittenUrl(url, urlRewriteRules) {
166
+ if (Array.isArray(urlRewriteRules)) {
167
+ for (const rule of urlRewriteRules) {
168
+ if (
169
+ rule !== null &&
170
+ typeof rule === "object" &&
171
+ typeof rule.fromUrlPrefix === "string" &&
172
+ typeof rule.toUrlPrefix === "string" &&
173
+ url.startsWith(rule.fromUrlPrefix)
174
+ ) {
175
+ return `${rule.toUrlPrefix}${url.substring(rule.fromUrlPrefix.length)}`;
176
+ }
177
+ }
178
+ }
179
+ return undefined;
180
+ }
181
+
182
+ function createCorsRequest(originalRequest, url, headers) {
183
+ if (url === originalRequest.url) {
184
+ return new Request(originalRequest, {
185
+ mode: "cors", // Required for adding custom HTTP headers.
186
+ headers
187
+ });
188
+ }
189
+
190
+ const method = originalRequest.method.toUpperCase();
191
+ const requestInit = {
192
+ cache: originalRequest.cache,
193
+ credentials: originalRequest.credentials,
194
+ headers,
195
+ integrity: originalRequest.integrity,
196
+ keepalive: originalRequest.keepalive,
197
+ method: originalRequest.method,
198
+ mode: "cors",
199
+ redirect: originalRequest.redirect,
200
+ referrer: originalRequest.referrer,
201
+ referrerPolicy: originalRequest.referrerPolicy
202
+ };
203
+ if (method !== "GET" && method !== "HEAD") {
204
+ requestInit.body = originalRequest.body;
205
+ }
206
+ return new Request(url, requestInit);
207
+ }
208
+
209
+ function getSourceUrl(clientId) {
210
+ if (typeof clientId !== "string" || clientId.length === 0) {
211
+ return Promise.resolve(undefined);
212
+ }
213
+ const cached = sourceUrlsByClientId.get(clientId);
214
+ if (cached !== undefined) {
215
+ return cached;
216
+ }
217
+ const lookup = withTimeout(self.clients.get(clientId))
218
+ .then(client =>
219
+ client !== undefined &&
220
+ client !== null &&
221
+ client.type === "window" &&
222
+ typeof client.url === "string" &&
223
+ client.url.length > 0
224
+ ? client.url
225
+ : undefined
226
+ )
227
+ .catch(() => undefined);
228
+ sourceUrlsByClientId.set(clientId, lookup);
229
+ if (sourceUrlsByClientId.size > maxSourceUrlCacheEntries) {
230
+ sourceUrlsByClientId.delete(sourceUrlsByClientId.keys().next().value);
231
+ }
232
+ return lookup;
233
+ }
234
+
235
+ function withTimeout(promise) {
236
+ return Promise.race([promise, new Promise(resolve => setTimeout(() => resolve(undefined), 250))]);
237
+ }
238
+
239
+ async function getState() {
240
+ if (transientCache !== undefined && transientUrlRewriteRules !== undefined) {
241
+ return { config: transientCache, urlRewriteRules: transientUrlRewriteRules };
242
+ }
243
+
244
+ const cache = await getCache();
245
+ const responses = await Promise.all([
246
+ cache.match(persistentCacheKey),
247
+ cache.match(persistentUrlRewriteRulesCacheKey)
248
+ ]);
249
+ const config = responses[0] === undefined ? [] : await responses[0].json();
250
+ const urlRewriteRules = responses[1] === undefined ? [] : await responses[1].json();
251
+
252
+ transientCache = Array.isArray(config) ? config : [];
253
+ transientUrlRewriteRules = Array.isArray(urlRewriteRules) ? urlRewriteRules : [];
254
+ return { config: transientCache, urlRewriteRules: transientUrlRewriteRules };
255
+ }
256
+
257
+ async function setConfig(config, urlRewriteRules) {
258
+ // Ensures "fetch" events can start seeing the config immediately. Persistent config is only required for when this
259
+ // service worker expires (after 30s on some browsers, like FireFox on Windows).
260
+ transientCache = config;
261
+ transientUrlRewriteRules = Array.isArray(urlRewriteRules) ? urlRewriteRules : [];
262
+
263
+ const cache = await getCache();
264
+ await Promise.all([
265
+ cache.put(persistentCacheKey, new Response(JSON.stringify(config))),
266
+ cache.put(persistentUrlRewriteRulesCacheKey, new Response(JSON.stringify(transientUrlRewriteRules)))
267
+ ]);
268
+ }
269
+
270
+ function getCache() {
271
+ return caches.open(persistentCacheName);
272
+ }