@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.
@@ -60,6 +60,94 @@ describe("API-client AuthManager configuration", () => {
60
60
  expect(requestHeaders(fetchApi, 1).get("Authorization-Token")).toBe("access-refreshed");
61
61
  });
62
62
 
63
+ test.each(
64
+ [[], ["https://app.example.com/media-auth/"]].map((prefixes): { prefixes: typeof prefixes } => ({ prefixes }))
65
+ )("ignores service-worker request restrictions in API clients: $prefixes", async ({ prefixes }) => {
66
+ const configState = state(undefined, accountA, "jwt-a", "access-a");
67
+ configState.config.requestUrlPrefixes = prefixes;
68
+ setModernSession([configState]);
69
+ const withKey = createApi({ apiKey: apiKeyA });
70
+ const withoutKey = createApi({});
71
+ await withKey.api.get();
72
+ await withoutKey.api.get();
73
+ expect(requestHeaders(withKey.fetchApi).get("Authorization-Token")).toBe("access-a");
74
+ expect(requestHeaders(withoutKey.fetchApi).get("Authorization")).toBe("Bearer jwt-a");
75
+ });
76
+
77
+ test("awaits manager-owned authentication at the expiry boundary", async () => {
78
+ const configState = state("customer", accountA, "jwt-old", "access-old");
79
+ configState.expiresAt = Date.now();
80
+ let completeRefresh = (): void => {
81
+ throw new Error("Refresh completion callback was not initialized.");
82
+ };
83
+ const authenticationPromise = new Promise<void>(resolve => {
84
+ completeRefresh = () => {
85
+ configState.accessToken = "access-new";
86
+ configState.expiresAt = Date.now() + 60_000;
87
+ configState.jwt = "jwt-new";
88
+ configState.refreshPromise = undefined;
89
+ resolve();
90
+ };
91
+ });
92
+ configState.authenticationPromise = authenticationPromise;
93
+ configState.refreshPromise = authenticationPromise;
94
+ setModernSession([configState]);
95
+ const { api, fetchApi } = createApi({ apiKey: apiKeyA, authConfigId: "customer" });
96
+
97
+ const requests = [api.get(), api.get()];
98
+ expect(fetchApi).not.toHaveBeenCalled();
99
+ completeRefresh();
100
+ await Promise.all(requests);
101
+
102
+ expect(requestHeaders(fetchApi, 0).get("Authorization-Token")).toBe("access-new");
103
+ expect(requestHeaders(fetchApi, 1).get("Authorization-Token")).toBe("access-new");
104
+ });
105
+
106
+ test("observes authentication that starts while resolving custom headers", async () => {
107
+ const configState = state("customer", accountB, "jwt-old", "access-old");
108
+ let completeRefresh = (): void => {
109
+ throw new Error("Refresh completion callback was not initialized.");
110
+ };
111
+ const authenticationPromise = new Promise<void>(resolve => {
112
+ completeRefresh = () => {
113
+ configState.accessToken = "access-new";
114
+ configState.expiresAt = Date.now() + 60_000;
115
+ configState.jwt = "jwt-new";
116
+ configState.refreshPromise = undefined;
117
+ resolve();
118
+ };
119
+ });
120
+ setModernSession([configState]);
121
+ const { api, fetchApi } = createApi({
122
+ authConfigId: "customer",
123
+ headers: async (): Promise<Record<string, string>> => {
124
+ configState.authenticationPromise = authenticationPromise;
125
+ configState.refreshPromise = authenticationPromise;
126
+ return {};
127
+ }
128
+ });
129
+
130
+ const request = api.get();
131
+ expect(fetchApi).not.toHaveBeenCalled();
132
+ completeRefresh();
133
+ await request;
134
+
135
+ expect(requestHeaders(fetchApi).get("Authorization")).toBe("Bearer jwt-new");
136
+ });
137
+
138
+ test("never invokes the token provider when authentication is not in progress", async () => {
139
+ const getAuthorizationToken = jest.fn(async () => "jwt-new");
140
+ const configState = state("customer", accountB, "jwt-old", "access-old", getAuthorizationToken);
141
+ configState.expiresAt = Date.now();
142
+ setModernSession([configState]);
143
+ const { api, fetchApi } = createApi({ authConfigId: "customer" });
144
+
145
+ await expect(api.get()).rejects.toThrow("not ready");
146
+
147
+ expect(getAuthorizationToken).not.toHaveBeenCalled();
148
+ expect(fetchApi).not.toHaveBeenCalled();
149
+ });
150
+
63
151
  test("uses the raw JWT as sole authentication for an API-key-less named client", async () => {
64
152
  setModernSession([state("customer", accountB, "jwt-b", "access-b")]);
65
153
  const { api, fetchApi } = createApi({ authConfigId: "customer" });
@@ -126,6 +214,29 @@ describe("API-client AuthManager configuration", () => {
126
214
  expect(requestHeaders(optedOut.fetchApi).get("Authorization")).toBe(`Bearer ${apiKeyA}`);
127
215
  });
128
216
 
217
+ test("fails closed for an expired V1 default instead of falling back to its API key", async () => {
218
+ const expired = state(undefined, accountA, "jwt-a", "access-a");
219
+ expired.expiresAt = Date.now() - 1;
220
+ AuthSessionState.setSession({
221
+ accessToken: undefined,
222
+ accessTokenRefreshHandle: undefined,
223
+ authConfigs: [expired],
224
+ authServiceWorker: undefined,
225
+ isActive: true,
226
+ isReady: false,
227
+ params: {
228
+ accountId: accountA,
229
+ authHeaders: async (): Promise<Record<string, string>> => ({}),
230
+ authUrl: "https://app.example.com/auth"
231
+ },
232
+ serviceWorkerConfigured: false
233
+ });
234
+ const { api, fetchApi } = createApi({ apiKey: apiKeyA });
235
+
236
+ await expect(api.get()).rejects.toThrow("not ready");
237
+ expect(fetchApi).not.toHaveBeenCalled();
238
+ });
239
+
129
240
  test("recognizes a 3.54 session as the default supplemental token", async () => {
130
241
  AuthSessionState.setSession({
131
242
  accessToken: "legacy-access",
@@ -184,16 +295,18 @@ function state(
184
295
  authConfigId: string | undefined,
185
296
  accountId: string,
186
297
  jwt: string,
187
- accessToken: string
298
+ accessToken: string,
299
+ getAuthorizationToken: () => Promise<string> = async () => jwt
188
300
  ): AuthSessionConfigState {
189
301
  const config: AuthSessionConfig = {
190
302
  accountId,
191
303
  authConfigId,
192
304
  enableServiceWorkerAuth: false,
193
- getAuthorizationToken: async () => jwt
305
+ getAuthorizationToken
194
306
  };
195
307
  return {
196
308
  accessToken,
309
+ authenticationPromise: Promise.resolve(),
197
310
  config,
198
311
  expiresAt: Date.now() + 60_000,
199
312
  jwt,
@@ -4,11 +4,14 @@ import { AuthSessionState } from "../src/private/AuthSessionState";
4
4
  import { AuthSessionConfigState } from "../src/private/model/AuthSession";
5
5
  import type {
6
6
  AuthSessionConfig,
7
+ AuthSwConfigEntryDto,
7
8
  BeginAuthSessionParams,
8
9
  BeginAuthSessionParamsV2,
9
10
  UrlRewriteRule
10
11
  } from "../src/index.browser";
11
12
 
13
+ import { AuthServiceWorkerHarness } from "./utils/AuthServiceWorkerHarness";
14
+
12
15
  type FetchApi = NonNullable<NonNullable<BeginAuthSessionParams["options"]>["fetchApi"]>;
13
16
 
14
17
  interface AuthManagerApi {
@@ -143,11 +146,13 @@ describe("AuthManager browser multi-configuration sessions", () => {
143
146
  authConfigId: undefined,
144
147
  authHeaders: async (): Promise<Record<string, string>> => ({ "X-App-Auth": "app-token" }),
145
148
  authUrl: "https://app.example.com/auth-a",
149
+ requestUrlPrefixes: ["https://app.example.com/media-auth/"],
146
150
  sourceUrlPrefixes: ["https://app.example.com/"]
147
151
  },
148
152
  {
149
153
  accountId: accountB,
150
154
  authConfigId: "customer-b",
155
+ requestUrlPrefixes: ["https://app.example.com/download/"],
151
156
  getAuthorizationToken: manualB
152
157
  },
153
158
  {
@@ -188,14 +193,16 @@ describe("AuthManager browser multi-configuration sessions", () => {
188
193
  {
189
194
  expires: expect.any(Number),
190
195
  headers: [{ key: "Authorization", value: `Bearer ${jwtA}` }],
196
+ requestUrlPrefixes: ["https://app.example.com/media-auth/"],
191
197
  sourceUrlPrefixes: ["https://app.example.com/"],
192
- urlPrefix: `!bytescale-source-scoped!https://upcdn.io/${accountA}/`
198
+ urlPrefix: `!bytescale-request-scoped!!bytescale-source-scoped!https://upcdn.io/${accountA}/`
193
199
  },
194
200
  {
195
201
  expires: expect.any(Number),
196
202
  headers: [{ key: "Authorization", value: `Bearer ${jwtB}` }],
203
+ requestUrlPrefixes: ["https://app.example.com/download/"],
197
204
  sourceUrlPrefixes: undefined,
198
- urlPrefix: `https://upcdn.io/${accountB}/`
205
+ urlPrefix: `!bytescale-request-scoped!https://upcdn.io/${accountB}/`
199
206
  }
200
207
  ],
201
208
  type: "SET_BYTESCALE_AUTH_CONFIG",
@@ -203,6 +210,94 @@ describe("AuthManager browser multi-configuration sessions", () => {
203
210
  });
204
211
  });
205
212
 
213
+ test.each(
214
+ [undefined, [], ["https://app.example.com/media-auth/", "https://app.example.com/download/"]].map(
215
+ (prefixes): { prefixes: typeof prefixes } => ({
216
+ prefixes
217
+ })
218
+ )
219
+ )("sends request prefixes through messages and restores them in the worker: $prefixes", async ({ prefixes }) => {
220
+ const fetchApi = createFetchApi();
221
+ await AuthManager.beginAuthSession({
222
+ authConfigs: async () => [
223
+ {
224
+ accountId: accountA,
225
+ authConfigId: undefined,
226
+ getAuthorizationToken: async () => jwtA,
227
+ requestUrlPrefixes: prefixes
228
+ }
229
+ ],
230
+ options: { fetchApi },
231
+ serviceWorkerScript: "/auth-sw.js",
232
+ urlRewriteRules: [{ fromUrlPrefix: "https://app.example.com/media-auth/", toUrlPrefix: "https://upcdn.io/" }]
233
+ });
234
+ const message = postMessage.mock.calls.at(-1)?.[0] as {
235
+ config: AuthSwConfigEntryDto[];
236
+ type: "SET_BYTESCALE_AUTH_CONFIG";
237
+ urlRewriteRules: UrlRewriteRule[];
238
+ };
239
+ expect(message.config[0].requestUrlPrefixes).toEqual(prefixes);
240
+ expect(message.config[0].urlPrefix).toBe(
241
+ `${prefixes === undefined ? "" : "!bytescale-request-scoped!"}https://upcdn.io/${accountA}/`
242
+ );
243
+ const worker = new AuthServiceWorkerHarness();
244
+ await worker.dispatchMessage(message);
245
+ const restarted = worker.restart();
246
+ const result = await restarted.dispatchFetch(`https://app.example.com/media-auth/${accountA}/image/example.jpg`);
247
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe(prefixes?.length === 0 ? null : `Bearer ${jwtA}`);
248
+ const direct = await restarted.dispatchFetch(`https://upcdn.io/${accountA}/image/example.jpg`);
249
+ expect(direct.responded).toBe(prefixes === undefined);
250
+ });
251
+
252
+ test("snapshots request prefixes and keeps them through token refresh", async () => {
253
+ const requestUrlPrefixes = ["https://app.example.com/media-auth/"];
254
+ const provider = jest.fn<() => Promise<string>>().mockResolvedValueOnce(jwtA).mockResolvedValueOnce(jwtB);
255
+ await AuthManager.beginAuthSession({
256
+ authConfigs: async () => [
257
+ { accountId: accountA, authConfigId: undefined, getAuthorizationToken: provider, requestUrlPrefixes }
258
+ ],
259
+ options: { fetchApi: createFetchApi() },
260
+ serviceWorkerScript: "/auth-sw.js"
261
+ });
262
+ requestUrlPrefixes.length = 0;
263
+ const session = AuthSessionState.getSession();
264
+ const state = session?.authConfigs?.[0];
265
+ if (session === undefined || state === undefined) {
266
+ throw new Error("Expected initialized auth state.");
267
+ }
268
+ await (AuthManager as AuthManagerInternals).refreshAuthConfig(session, state);
269
+ expect(provider).toHaveBeenCalledTimes(2);
270
+ expect(state.config.requestUrlPrefixes).toEqual(["https://app.example.com/media-auth/"]);
271
+ expect(state.refreshHandle).toBeDefined();
272
+ expect((postMessage.mock.calls.at(-1)?.[0] as { config: AuthSwConfigEntryDto[] }).config[0]).toMatchObject({
273
+ headers: [{ key: "Authorization", value: `Bearer ${jwtB}` }],
274
+ requestUrlPrefixes: ["https://app.example.com/media-auth/"]
275
+ });
276
+ });
277
+
278
+ test.each(
279
+ [null, "https://app.example.com/", [42], ["https://app.example.com/", null]].map(
280
+ (prefixes): { prefixes: typeof prefixes } => ({ prefixes })
281
+ )
282
+ )("rejects malformed request prefixes before acquiring tokens: $prefixes", async ({ prefixes }) => {
283
+ const provider = jest.fn(async () => jwtA);
284
+ const fetchApi = createFetchApi();
285
+ await expect(
286
+ AuthManager.beginAuthSession(
287
+ v2Params(fetchApi, [
288
+ {
289
+ ...apiOnlyConfig(undefined, accountA, provider),
290
+ requestUrlPrefixes: prefixes as unknown as string[]
291
+ }
292
+ ])
293
+ )
294
+ ).rejects.toThrow("The 'requestUrlPrefixes' field must be an array of strings.");
295
+ expect(provider).not.toHaveBeenCalled();
296
+ expect(fetchApi).not.toHaveBeenCalled();
297
+ expect(postMessage).not.toHaveBeenCalled();
298
+ expect(AuthManager.isAuthSessionActive()).toBe(false);
299
+ });
300
+
206
301
  test("uses each config's effective CDN URL for registration, worker prefixes, cleanup, and collisions", async () => {
207
302
  const fetchApi = createFetchApi();
208
303
  const defaultCdnUrl = "https://downloads-default.example.com";
@@ -269,6 +364,7 @@ describe("AuthManager browser multi-configuration sessions", () => {
269
364
  authConfigId: undefined,
270
365
  enableCookieAuth: true,
271
366
  enableServiceWorkerAuth: false,
367
+ requestUrlPrefixes: [],
272
368
  getAuthorizationToken: provider
273
369
  }
274
370
  ],
@@ -308,6 +404,53 @@ describe("AuthManager browser multi-configuration sessions", () => {
308
404
  expect(AuthManager.isAuthSessionReady()).toBe(true);
309
405
  });
310
406
 
407
+ test("deduplicates concurrent refreshes for the same auth config", async () => {
408
+ const fetchApi = createFetchApi();
409
+ let resolveRefresh = (_jwt: string): void => {
410
+ throw new Error("Refresh resolver was not initialized.");
411
+ };
412
+ const refreshResult = new Promise<string>(resolve => {
413
+ resolveRefresh = resolve;
414
+ });
415
+ let markRefreshStarted = (): void => {
416
+ throw new Error("Refresh-start resolver was not initialized.");
417
+ };
418
+ const refreshStarted = new Promise<void>(resolve => {
419
+ markRefreshStarted = resolve;
420
+ });
421
+ const provider = jest
422
+ .fn<() => Promise<string>>()
423
+ .mockResolvedValueOnce(jwtA)
424
+ .mockImplementationOnce(async () => {
425
+ markRefreshStarted();
426
+ return await refreshResult;
427
+ });
428
+
429
+ await AuthManager.beginAuthSession({
430
+ authConfigs: async () => [apiOnlyConfig("customer", accountA, provider)],
431
+ options: { fetchApi }
432
+ });
433
+ const session = AuthSessionState.getSession();
434
+ const configState = session?.authConfigs?.[0];
435
+ if (session === undefined || configState === undefined) {
436
+ throw new Error("Expected initialized auth state.");
437
+ }
438
+ const internals = AuthManager as AuthManagerInternals;
439
+
440
+ const firstRefresh = internals.refreshAuthConfig(session, configState);
441
+ const secondRefresh = internals.refreshAuthConfig(session, configState);
442
+ await refreshStarted;
443
+ expect(provider).toHaveBeenCalledTimes(2);
444
+ expect(configState.authenticationPromise).toBe(configState.refreshPromise);
445
+
446
+ resolveRefresh(jwtB);
447
+ await Promise.all([firstRefresh, secondRefresh]);
448
+
449
+ expect(configState.jwt).toBe(jwtB);
450
+ expect(configState.authenticationPromise).toBeDefined();
451
+ expect(configState.refreshPromise).toBeUndefined();
452
+ });
453
+
311
454
  test("clears the cookie-enabled config and the complete worker config on end", async () => {
312
455
  const fetchApi = createFetchApi();
313
456
  await AuthManager.beginAuthSession({
@@ -0,0 +1,259 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import type { AuthSwConfigEntryDto } from "../src/index.browser";
4
+ import { AuthServiceWorkerHarness, authConfig, rewriteRule } from "./utils/AuthServiceWorkerHarness";
5
+
6
+ const mediaPrefix = "https://dashboard.example.com/media-auth/";
7
+ const downloadPrefix = "https://dashboard.example.com/download/";
8
+ const sourcePrefix = "https://app.example.com/private/";
9
+ const rules = [rewriteRule("media-auth/", ""), rewriteRule("download/", "")];
10
+ const oldWorkerSource = readFileSync(resolve(process.cwd(), "tests/fixtures/auth-sw-3.61.0.js"), "utf8");
11
+
12
+ describe("Auth service-worker original request URL restrictions", () => {
13
+ test.each([
14
+ { prefixes: undefined, directAuth: true, mediaAuth: true, downloadAuth: true },
15
+ { prefixes: [], directAuth: false, mediaAuth: false, downloadAuth: false },
16
+ { prefixes: [""], directAuth: true, mediaAuth: true, downloadAuth: true },
17
+ { prefixes: [mediaPrefix], directAuth: false, mediaAuth: true, downloadAuth: false },
18
+ { prefixes: [mediaPrefix, downloadPrefix], directAuth: false, mediaAuth: true, downloadAuth: true },
19
+ { prefixes: ["https://upcdn.io/account-a/"], directAuth: true, mediaAuth: false, downloadAuth: false }
20
+ ])("matches the original URL for $prefixes", async ({ prefixes, directAuth, mediaAuth, downloadAuth }) => {
21
+ const harness = new AuthServiceWorkerHarness();
22
+ await harness.setConfig([scopedConfig(prefixes)], rules);
23
+
24
+ const direct = await harness.dispatchFetch("https://upcdn.io/account-a/image/example.jpg");
25
+ expect(direct.responded).toBe(directAuth);
26
+ expect(direct.outboundRequest?.headers.get("Authorization")).toBe(directAuth ? "Bearer token-a" : undefined);
27
+
28
+ for (const [prefix, expectedAuth] of [
29
+ [mediaPrefix, mediaAuth],
30
+ [downloadPrefix, downloadAuth]
31
+ ] as const) {
32
+ const result = await harness.dispatchFetch(`${prefix}account-a/raw/example.jpg?download=true`, {
33
+ navigation: true
34
+ });
35
+ expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/raw/example.jpg?download=true");
36
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe(expectedAuth ? "Bearer token-a" : null);
37
+ }
38
+ });
39
+
40
+ test("still checks the final account and CDN for an allowed alias", async () => {
41
+ const harness = new AuthServiceWorkerHarness();
42
+ await harness.setConfig([scopedConfig([mediaPrefix])], rules);
43
+
44
+ const otherAccount = await harness.dispatchFetch(`${mediaPrefix}account-b/raw/example.jpg`);
45
+ const similarAccount = await harness.dispatchFetch(`${mediaPrefix}account-a-extra/raw/example.jpg`);
46
+ expect(otherAccount.outboundRequest?.headers.has("Authorization")).toBe(false);
47
+ expect(similarAccount.outboundRequest?.headers.has("Authorization")).toBe(false);
48
+
49
+ await harness.setConfig(
50
+ [scopedConfig([mediaPrefix])],
51
+ [{ fromUrlPrefix: mediaPrefix, toUrlPrefix: "https://other-cdn.example.com/" }]
52
+ );
53
+ const otherCdn = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
54
+ expect(otherCdn.outboundRequest?.url).toBe("https://other-cdn.example.com/account-a/raw/example.jpg");
55
+ expect(otherCdn.outboundRequest?.headers.has("Authorization")).toBe(false);
56
+ });
57
+
58
+ test.each([[], [downloadPrefix]].map((prefixes): { prefixes: typeof prefixes } => ({ prefixes })))(
59
+ "continues to another eligible config after rejecting $prefixes",
60
+ async ({ prefixes }) => {
61
+ const harness = new AuthServiceWorkerHarness();
62
+ await harness.setConfig(
63
+ [
64
+ { ...scopedConfig(prefixes), headers: [{ key: "X-Rejected", value: "must-not-leak" }] },
65
+ scopedConfig([mediaPrefix]),
66
+ { ...scopedConfig([mediaPrefix]), headers: [{ key: "Authorization", value: "Bearer later" }] }
67
+ ],
68
+ rules
69
+ );
70
+
71
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
72
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
73
+ expect(result.outboundRequest?.headers.has("X-Rejected")).toBe(false);
74
+ }
75
+ );
76
+
77
+ test("does not block an eligible destination config with a rejected original-prefix config", async () => {
78
+ const harness = new AuthServiceWorkerHarness();
79
+ await harness.setConfig(
80
+ [
81
+ scopedConfig([downloadPrefix]),
82
+ {
83
+ ...scopedConfig([mediaPrefix]),
84
+ headers: [{ key: "Authorization", value: "Bearer token-b" }],
85
+ urlPrefix: "!bytescale-request-scoped!https://upcdn.io/account-b/"
86
+ }
87
+ ],
88
+ rules
89
+ );
90
+
91
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-b/raw/example.jpg`);
92
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
93
+ });
94
+
95
+ test.each([
96
+ { requestPrefix: mediaPrefix, clientUrl: `${sourcePrefix}page`, authorized: true },
97
+ { requestPrefix: mediaPrefix, clientUrl: "https://app.example.com/public/", authorized: false },
98
+ { requestPrefix: downloadPrefix, clientUrl: `${sourcePrefix}page`, authorized: false },
99
+ { requestPrefix: mediaPrefix, clientUrl: undefined, authorized: false }
100
+ ])("combines resource and initiating-page restrictions: %j", async ({ requestPrefix, clientUrl, authorized }) => {
101
+ const harness = new AuthServiceWorkerHarness();
102
+ if (clientUrl !== undefined) {
103
+ harness.setWindowClient("client", clientUrl);
104
+ }
105
+ await harness.setConfig([scopedConfig([mediaPrefix], [sourcePrefix])], rules);
106
+
107
+ const result = await harness.dispatchFetch(`${requestPrefix}account-a/raw/example.jpg`, { clientId: "client" });
108
+ expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/raw/example.jpg");
109
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe(authorized ? "Bearer token-a" : null);
110
+ });
111
+
112
+ test("a rejected request restriction skips source lookup and permits a later config", async () => {
113
+ const harness = new AuthServiceWorkerHarness();
114
+ await harness.setConfig([scopedConfig([], [sourcePrefix]), scopedConfig([mediaPrefix])], rules);
115
+
116
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
117
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
118
+ });
119
+
120
+ test.each(["expired", "empty source", "HEAD"])("keeps rewriting when auth is excluded by %s", async reason => {
121
+ const harness = new AuthServiceWorkerHarness();
122
+ const config = scopedConfig([mediaPrefix], reason === "empty source" ? [] : undefined);
123
+ if (reason === "expired") {
124
+ config.expires = Date.now() - 1;
125
+ }
126
+ await harness.setConfig([config], rules);
127
+
128
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg?download=true`, {
129
+ method: reason === "HEAD" ? "HEAD" : "GET"
130
+ });
131
+ expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/raw/example.jpg?download=true");
132
+ expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
133
+ });
134
+
135
+ test("does not strip caller headers from requests excluded by the restriction", async () => {
136
+ const harness = new AuthServiceWorkerHarness();
137
+ await harness.setConfig([scopedConfig([])], rules);
138
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`, {
139
+ headers: { "Authorization": "Caller token", "Authorization-Token": "Caller access token", "Range": "bytes=0-9" }
140
+ });
141
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe("Caller token");
142
+ expect(result.outboundRequest?.headers.get("Authorization-Token")).toBe("Caller access token");
143
+ expect(result.outboundRequest?.headers.get("Range")).toBe("bytes=0-9");
144
+ });
145
+
146
+ test.each(
147
+ [undefined, [], [mediaPrefix, downloadPrefix]].map((prefixes): { prefixes: typeof prefixes } => ({ prefixes }))
148
+ )("recovers restrictions and rewrites after restart: $prefixes", async ({ prefixes }) => {
149
+ const harness = new AuthServiceWorkerHarness();
150
+ await harness.dispatchMessage({
151
+ type: "SET_BYTESCALE_AUTH_CONFIG",
152
+ config: [scopedConfig(prefixes)],
153
+ urlRewriteRules: rules
154
+ });
155
+
156
+ const direct = await harness.restart().dispatchFetch("https://upcdn.io/account-a/raw/example.jpg");
157
+ expect(direct.outboundRequest?.headers.get("Authorization")).toBe(prefixes === undefined ? "Bearer token-a" : null);
158
+ for (const prefix of [mediaPrefix, downloadPrefix]) {
159
+ const alias = await harness
160
+ .restart()
161
+ .dispatchFetch(`${prefix}account-a/raw/example.jpg?download=true`, { navigation: true });
162
+ expect(alias.outboundRequest?.url).toBe("https://upcdn.io/account-a/raw/example.jpg?download=true");
163
+ expect(alias.outboundRequest?.headers.get("Authorization")).toBe(
164
+ prefixes?.length === 0 ? null : "Bearer token-a"
165
+ );
166
+ }
167
+ });
168
+
169
+ test("recovers both restrictions after restart", async () => {
170
+ const harness = new AuthServiceWorkerHarness();
171
+ await harness.dispatchMessage({
172
+ type: "SET_BYTESCALE_AUTH_CONFIG",
173
+ config: [scopedConfig([mediaPrefix], [sourcePrefix])],
174
+ urlRewriteRules: rules
175
+ });
176
+ const restarted = harness.restart();
177
+ restarted.setWindowClient("client", `${sourcePrefix}page`);
178
+ const allowed = await restarted.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`, { clientId: "client" });
179
+ const rejected = await restarted.dispatchFetch(`${downloadPrefix}account-a/raw/example.jpg`, {
180
+ clientId: "client"
181
+ });
182
+ expect(allowed.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
183
+ expect(rejected.outboundRequest?.headers.has("Authorization")).toBe(false);
184
+ });
185
+
186
+ test.each([null, "https://", [42]].map((prefixes): { prefixes: typeof prefixes } => ({ prefixes })))(
187
+ "rejects malformed persisted request prefixes: $prefixes",
188
+ async ({ prefixes }) => {
189
+ const harness = new AuthServiceWorkerHarness();
190
+ await harness.setConfig([scopedConfig(prefixes as unknown as string[])], rules);
191
+ const result = await harness.restart().dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
192
+ expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
193
+ }
194
+ );
195
+
196
+ test("rejects restricted configs without the compatibility marker", async () => {
197
+ const harness = new AuthServiceWorkerHarness();
198
+ await harness.setConfig([{ ...authConfig("account-a/", "token-a"), requestUrlPrefixes: [mediaPrefix] }], rules);
199
+ const result = await harness.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
200
+ expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
201
+ });
202
+ });
203
+
204
+ describe("Auth service-worker version compatibility", () => {
205
+ test.each(
206
+ [undefined, [], [sourcePrefix]].map((sourcePrefixes): { sourcePrefixes: typeof sourcePrefixes } => ({
207
+ sourcePrefixes
208
+ }))
209
+ )("3.61.0 skips request-scoped auth with source prefixes $sourcePrefixes", async ({ sourcePrefixes }) => {
210
+ const oldWorker = new AuthServiceWorkerHarness(undefined, oldWorkerSource);
211
+ oldWorker.setWindowClient("client", `${sourcePrefix}page`);
212
+ await oldWorker.setConfig([scopedConfig([mediaPrefix, downloadPrefix], sourcePrefixes)], rules);
213
+ const direct = await oldWorker.dispatchFetch("https://upcdn.io/account-a/raw/example.jpg", {
214
+ clientId: "client"
215
+ });
216
+ expect(direct.responded).toBe(false);
217
+ const alias = await oldWorker.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`, { clientId: "client" });
218
+ expect(alias.outboundRequest?.url).toBe("https://upcdn.io/account-a/raw/example.jpg");
219
+ expect(alias.outboundRequest?.headers.has("Authorization")).toBe(false);
220
+
221
+ const recoveredOld = await oldWorker.restart().dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
222
+ expect(recoveredOld.outboundRequest?.headers.has("Authorization")).toBe(false);
223
+ const upgraded = oldWorker.restart(readFileSync(resolve(process.cwd(), "src/index.auth-sw.js"), "utf8"));
224
+ upgraded.setWindowClient("client", `${sourcePrefix}page`);
225
+ const recoveredNew = await upgraded.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`, {
226
+ clientId: "client"
227
+ });
228
+ expect(recoveredNew.outboundRequest?.headers.get("Authorization")).toBe(
229
+ sourcePrefixes?.length === 0 ? null : "Bearer token-a"
230
+ );
231
+ });
232
+
233
+ test("3.61.0 skips empty request prefixes before and after restart", async () => {
234
+ const oldWorker = new AuthServiceWorkerHarness(undefined, oldWorkerSource);
235
+ await oldWorker.setConfig([scopedConfig([])], rules);
236
+ const alias = await oldWorker.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
237
+ const direct = await oldWorker.restart().dispatchFetch("https://upcdn.io/account-a/raw/example.jpg");
238
+ expect(alias.outboundRequest?.headers.has("Authorization")).toBe(false);
239
+ expect(direct.outboundRequest?.headers.has("Authorization")).toBe(false);
240
+ });
241
+
242
+ test("3.61.0 still authenticates unrestricted configs from the new SDK", async () => {
243
+ const oldWorker = new AuthServiceWorkerHarness(undefined, oldWorkerSource);
244
+ await oldWorker.setConfig([scopedConfig(undefined)], rules);
245
+ const result = await oldWorker.dispatchFetch(`${mediaPrefix}account-a/raw/example.jpg`);
246
+ expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
247
+ });
248
+ });
249
+
250
+ function scopedConfig(requestUrlPrefixes: string[] | undefined, sourceUrlPrefixes?: string[]): AuthSwConfigEntryDto {
251
+ return {
252
+ ...authConfig("account-a/", "token-a"),
253
+ requestUrlPrefixes,
254
+ sourceUrlPrefixes,
255
+ urlPrefix: `${requestUrlPrefixes === undefined ? "" : "!bytescale-request-scoped!"}${
256
+ sourceUrlPrefixes === undefined ? "" : "!bytescale-source-scoped!"
257
+ }https://upcdn.io/account-a/`
258
+ };
259
+ }