@bytescale/sdk 3.61.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.
package/README.md CHANGED
@@ -432,6 +432,97 @@ With JWTs, the user can also perform API requests, such as file deletions, as th
432
432
 
433
433
  [Learn more about the `AuthManager` and JWTs »](https://www.bytescale.com/docs/auth)
434
434
 
435
+ ### Authenticate private URL aliases while keeping public CDN requests cacheable
436
+
437
+ Automatic (`AuthSessionConfigAuto`) and manual (`AuthSessionConfigManual`) configurations both accept
438
+ `requestUrlPrefixes?: string[]`. This restricts **service-worker authentication** by matching the original
439
+ `event.request.url`, before `urlRewriteRules` are applied. The rewritten destination must still match the
440
+ configuration's CDN URL and account, and the configuration must not have expired.
441
+
442
+ - Omit the field (or set it to `undefined`) to preserve existing behavior.
443
+ - Set it to `[]` to authenticate no requests through the service worker.
444
+ - Provide multiple prefixes to match any of them using case-sensitive `startsWith` comparisons.
445
+
446
+ Prefixes follow the same string-array validation as `sourceUrlPrefixes`: they are not parsed or normalized.
447
+ Use complete URL prefixes with a trailing `/` when restricting an origin or directory. An empty string matches
448
+ any URL. `sourceUrlPrefixes` independently restricts the initiating page or iframe URL; when both fields are
449
+ provided, both must match. Request prefixes alone do not require an initiating client, so new-tab downloads
450
+ can use them.
451
+
452
+ Serve the current Bytescale auth service worker as `/auth-sw.js` on your application's origin, then initialize
453
+ an automatic configuration:
454
+
455
+ ```javascript
456
+ import { AuthManager } from "@bytescale/sdk";
457
+
458
+ await AuthManager.beginAuthSession({
459
+ serviceWorkerScript: "/auth-sw.js",
460
+ urlRewriteRules: [
461
+ { fromUrlPrefix: "https://app.example.com/media-auth/", toUrlPrefix: "https://upcdn.io/" },
462
+ { fromUrlPrefix: "https://app.example.com/download/", toUrlPrefix: "https://upcdn.io/" }
463
+ ],
464
+ authConfigs: async () => [
465
+ {
466
+ accountId: "A123abc", // Replace with your Bytescale account ID.
467
+ authConfigId: undefined,
468
+ authUrl: "https://app.example.com/auth", // Your endpoint returning a JWT as text/plain.
469
+ authHeaders: async () => ({}),
470
+ requestUrlPrefixes: ["https://app.example.com/media-auth/", "https://app.example.com/download/"],
471
+ enableCookieAuth: false
472
+ }
473
+ ]
474
+ });
475
+ ```
476
+
477
+ For a manual configuration, replace `authUrl` and `authHeaders` with your `getAuthorizationToken` callback;
478
+ keep `requestUrlPrefixes` and the other fields:
479
+
480
+ ```javascript
481
+ const manualConfig = {
482
+ accountId: "A123abc",
483
+ authConfigId: undefined,
484
+ requestUrlPrefixes: ["https://app.example.com/media-auth/", "https://app.example.com/download/"],
485
+ getAuthorizationToken: async () => {
486
+ const response = await fetch("/auth");
487
+ return await response.text();
488
+ }
489
+ };
490
+ ```
491
+
492
+ With the example configuration:
493
+
494
+ | Original resource URL | Final destination | AuthManager-injected headers |
495
+ | ------------------------------------------------------------------------ | -------------------------------------------------------- | ---------------------------- |
496
+ | `https://upcdn.io/A123abc/image/example.jpg` | Unchanged | None |
497
+ | `https://app.example.com/media-auth/A123abc/image/example.jpg` | `https://upcdn.io/A123abc/image/example.jpg` | Account A123abc's JWT |
498
+ | `https://app.example.com/download/A123abc/raw/example.jpg?download=true` | `https://upcdn.io/A123abc/raw/example.jpg?download=true` | Account A123abc's JWT |
499
+ | `https://app.example.com/media-auth/B123abc/image/example.jpg` | `https://upcdn.io/B123abc/image/example.jpg` | None |
500
+
501
+ The download alias supports a normal link after initialization:
502
+
503
+ ```html
504
+ <a href="https://app.example.com/download/A123abc/raw/example.jpg?download=true" target="_blank" rel="noopener">
505
+ Download
506
+ </a>
507
+ ```
508
+
509
+ Rewriting uses the first matching rule once and continues even when authentication does not match. Responses
510
+ remain streamed, and range headers, query parameters, and navigation behavior are preserved. A rejected request
511
+ restriction allows the worker to consider later authentication entries. Existing rules preventing duplicate
512
+ service-worker destinations within an AuthManager session still apply.
513
+
514
+ This field does not change API-client authentication, cookie authentication, token acquisition, or refresh.
515
+ It does not remove caller-provided headers or existing CDN cookies. Leave cookie authentication disabled and
516
+ avoid other broad authentication configurations for CDN requests you want to keep unauthenticated.
517
+
518
+ **Compatibility and rollout:** deploy the auth worker shipped with SDK **3.62.0 or later** before enabling
519
+ `requestUrlPrefixes` in the application. Update any self-hosted worker copies and allow the updated worker to
520
+ activate in existing browser sessions. A distinct compatibility marker makes older workers skip the entire
521
+ request-restricted configuration, even when `sourceUrlPrefixes` is also present. They cannot silently turn it
522
+ into broad authentication. Private downloads may remain unauthenticated until the worker is updated; versions
523
+ without rewrite support also cannot serve aliases. The updated worker can recover these restrictions and
524
+ rewrite rules from persisted state after restart, and continues to support older configurations.
525
+
435
526
  ## UrlBuilder
436
527
 
437
528
  Use the `UrlBuilder` to construct URLs for your uploaded files:
@@ -3860,6 +3860,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
3860
3860
  this.refreshBeforeExpirySeconds = 20;
3861
3861
  this.scheduler = new Scheduler();
3862
3862
  this.sourceScopedUrlPrefixMarker = "!bytescale-source-scoped!";
3863
+ this.requestScopedUrlPrefixMarker = "!bytescale-request-scoped!";
3863
3864
  this.authSessionMutex = AuthSessionState.getMutex();
3864
3865
  }
3865
3866
  return AuthManagerBrowser_createClass(AuthManagerImpl, [{
@@ -4138,6 +4139,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4138
4139
  key: "Authorization",
4139
4140
  value: "Bearer ".concat(state.jwt)
4140
4141
  }],
4142
+ requestUrlPrefixes: state.config.requestUrlPrefixes,
4141
4143
  sourceUrlPrefixes: state.config.sourceUrlPrefixes,
4142
4144
  urlPrefix: "".concat(_this6.getConfigCdnUrl(session.params, state.config), "/").concat(state.config.accountId, "/")
4143
4145
  }];
@@ -4159,7 +4161,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4159
4161
  type: "SET_BYTESCALE_AUTH_CONFIG",
4160
4162
  config: config.map(function (entry) {
4161
4163
  return Object.assign(Object.assign({}, entry), {
4162
- urlPrefix: "".concat(entry.sourceUrlPrefixes === undefined ? "" : _this7.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
4164
+ urlPrefix: "".concat(entry.requestUrlPrefixes === undefined ? "" : _this7.requestScopedUrlPrefixMarker).concat(entry.sourceUrlPrefixes === undefined ? "" : _this7.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
4163
4165
  });
4164
4166
  })
4165
4167
  }, urlRewriteRules === undefined ? {} : {
@@ -4181,6 +4183,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4181
4183
  }
4182
4184
  return configs.map(function (config) {
4183
4185
  return config === null || AuthManagerBrowser_typeof(config) !== "object" ? config : Object.assign(Object.assign({}, config), {
4186
+ requestUrlPrefixes: Array.isArray(config.requestUrlPrefixes) ? AuthManagerBrowser_toConsumableArray(config.requestUrlPrefixes) : config.requestUrlPrefixes,
4184
4187
  sourceUrlPrefixes: Array.isArray(config.sourceUrlPrefixes) ? AuthManagerBrowser_toConsumableArray(config.sourceUrlPrefixes) : config.sourceUrlPrefixes
4185
4188
  });
4186
4189
  });
@@ -4238,11 +4241,8 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4238
4241
  if (config.enableCookieAuth !== undefined && typeof config.enableCookieAuth !== "boolean" || config.enableServiceWorkerAuth !== undefined && typeof config.enableServiceWorkerAuth !== "boolean") {
4239
4242
  throw new Error("Authentication enablement flags must be booleans when provided.");
4240
4243
  }
4241
- if (config.sourceUrlPrefixes !== undefined && (!Array.isArray(config.sourceUrlPrefixes) || !config.sourceUrlPrefixes.every(function (prefix) {
4242
- return typeof prefix === "string";
4243
- }))) {
4244
- throw new Error("The 'sourceUrlPrefixes' field must be an array of strings.");
4245
- }
4244
+ this.validateUrlPrefixes(config.sourceUrlPrefixes, "sourceUrlPrefixes");
4245
+ this.validateUrlPrefixes(config.requestUrlPrefixes, "requestUrlPrefixes");
4246
4246
  var isManual = typeof config.getAuthorizationToken === "function";
4247
4247
  var isAutomatic = typeof config.authUrl === "string" && typeof config.authHeaders === "function";
4248
4248
  if (isManual === isAutomatic || isManual && (config.authUrl !== undefined || config.authHeaders !== undefined)) {
@@ -4284,6 +4284,15 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4284
4284
  }
4285
4285
  }
4286
4286
  }
4287
+ }, {
4288
+ key: "validateUrlPrefixes",
4289
+ value: function validateUrlPrefixes(prefixes, fieldName) {
4290
+ if (prefixes !== undefined && (!Array.isArray(prefixes) || !prefixes.every(function (prefix) {
4291
+ return typeof prefix === "string";
4292
+ }))) {
4293
+ throw new Error("The '".concat(fieldName, "' field must be an array of strings."));
4294
+ }
4295
+ }
4287
4296
  }, {
4288
4297
  key: "validateUrlRewriteRules",
4289
4298
  value: function validateUrlRewriteRules(rules) {
@@ -3820,6 +3820,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
3820
3820
  this.refreshBeforeExpirySeconds = 20;
3821
3821
  this.scheduler = new Scheduler();
3822
3822
  this.sourceScopedUrlPrefixMarker = "!bytescale-source-scoped!";
3823
+ this.requestScopedUrlPrefixMarker = "!bytescale-request-scoped!";
3823
3824
  this.authSessionMutex = AuthSessionState.getMutex();
3824
3825
  }
3825
3826
  return AuthManagerBrowser_createClass(AuthManagerImpl, [{
@@ -4098,6 +4099,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4098
4099
  key: "Authorization",
4099
4100
  value: "Bearer ".concat(state.jwt)
4100
4101
  }],
4102
+ requestUrlPrefixes: state.config.requestUrlPrefixes,
4101
4103
  sourceUrlPrefixes: state.config.sourceUrlPrefixes,
4102
4104
  urlPrefix: "".concat(_this6.getConfigCdnUrl(session.params, state.config), "/").concat(state.config.accountId, "/")
4103
4105
  }];
@@ -4119,7 +4121,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4119
4121
  type: "SET_BYTESCALE_AUTH_CONFIG",
4120
4122
  config: config.map(function (entry) {
4121
4123
  return Object.assign(Object.assign({}, entry), {
4122
- urlPrefix: "".concat(entry.sourceUrlPrefixes === undefined ? "" : _this7.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
4124
+ urlPrefix: "".concat(entry.requestUrlPrefixes === undefined ? "" : _this7.requestScopedUrlPrefixMarker).concat(entry.sourceUrlPrefixes === undefined ? "" : _this7.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
4123
4125
  });
4124
4126
  })
4125
4127
  }, urlRewriteRules === undefined ? {} : {
@@ -4141,6 +4143,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4141
4143
  }
4142
4144
  return configs.map(function (config) {
4143
4145
  return config === null || AuthManagerBrowser_typeof(config) !== "object" ? config : Object.assign(Object.assign({}, config), {
4146
+ requestUrlPrefixes: Array.isArray(config.requestUrlPrefixes) ? AuthManagerBrowser_toConsumableArray(config.requestUrlPrefixes) : config.requestUrlPrefixes,
4144
4147
  sourceUrlPrefixes: Array.isArray(config.sourceUrlPrefixes) ? AuthManagerBrowser_toConsumableArray(config.sourceUrlPrefixes) : config.sourceUrlPrefixes
4145
4148
  });
4146
4149
  });
@@ -4198,11 +4201,8 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4198
4201
  if (config.enableCookieAuth !== undefined && typeof config.enableCookieAuth !== "boolean" || config.enableServiceWorkerAuth !== undefined && typeof config.enableServiceWorkerAuth !== "boolean") {
4199
4202
  throw new Error("Authentication enablement flags must be booleans when provided.");
4200
4203
  }
4201
- if (config.sourceUrlPrefixes !== undefined && (!Array.isArray(config.sourceUrlPrefixes) || !config.sourceUrlPrefixes.every(function (prefix) {
4202
- return typeof prefix === "string";
4203
- }))) {
4204
- throw new Error("The 'sourceUrlPrefixes' field must be an array of strings.");
4205
- }
4204
+ this.validateUrlPrefixes(config.sourceUrlPrefixes, "sourceUrlPrefixes");
4205
+ this.validateUrlPrefixes(config.requestUrlPrefixes, "requestUrlPrefixes");
4206
4206
  var isManual = typeof config.getAuthorizationToken === "function";
4207
4207
  var isAutomatic = typeof config.authUrl === "string" && typeof config.authHeaders === "function";
4208
4208
  if (isManual === isAutomatic || isManual && (config.authUrl !== undefined || config.authHeaders !== undefined)) {
@@ -4244,6 +4244,15 @@ var AuthManagerImpl = /*#__PURE__*/function () {
4244
4244
  }
4245
4245
  }
4246
4246
  }
4247
+ }, {
4248
+ key: "validateUrlPrefixes",
4249
+ value: function validateUrlPrefixes(prefixes, fieldName) {
4250
+ if (prefixes !== undefined && (!Array.isArray(prefixes) || !prefixes.every(function (prefix) {
4251
+ return typeof prefix === "string";
4252
+ }))) {
4253
+ throw new Error("The '".concat(fieldName, "' field must be an array of strings."));
4254
+ }
4255
+ }
4247
4256
  }, {
4248
4257
  key: "validateUrlRewriteRules",
4249
4258
  value: function validateUrlRewriteRules(rules) {
@@ -4,8 +4,10 @@ export interface AuthSwConfigEntryDto {
4
4
  expires: number | undefined;
5
5
  /** Headers to add to matching requests. */
6
6
  headers: AuthSwHeaderDto[];
7
+ /** Optional original resource URL prefixes, before rewriting. An empty array matches no requests. */
8
+ requestUrlPrefixes?: string[];
7
9
  /** Optional page or iframe URL prefixes. An empty array matches no clients. */
8
10
  sourceUrlPrefixes?: string[];
9
- /** Request URL prefix. Use the actual URL; AuthManager applies any internal compatibility marker. */
11
+ /** Final destination URL prefix. Use the actual URL; AuthManager applies any internal compatibility marker. */
10
12
  urlPrefix: string;
11
13
  }
@@ -9,6 +9,14 @@ export interface AuthSessionConfigBase {
9
9
  enableCookieAuth?: boolean;
10
10
  /** Enables CDN service-worker authentication. Defaults to true. */
11
11
  enableServiceWorkerAuth?: boolean;
12
+ /**
13
+ * Restricts service-worker authentication to original resource URLs, before URL rewriting.
14
+ * Uses case-sensitive startsWith matching against any prefix. Undefined allows all requests; [] allows none.
15
+ * The final destination must still match this configuration's CDN and account.
16
+ * Does not affect API-client or cookie authentication, token acquisition, or refresh.
17
+ * Requires the auth service worker shipped with SDK 3.62.0 or later; older workers skip this config.
18
+ */
19
+ requestUrlPrefixes?: string[];
12
20
  /** Restricts service-worker authentication to matching page or iframe URLs. */
13
21
  sourceUrlPrefixes?: string[];
14
22
  }
@@ -16,6 +16,7 @@ declare class AuthManagerImpl implements AuthManagerInterface {
16
16
  private readonly refreshBeforeExpirySeconds;
17
17
  private readonly scheduler;
18
18
  private readonly sourceScopedUrlPrefixMarker;
19
+ private readonly requestScopedUrlPrefixMarker;
19
20
  constructor(serviceWorkerUtils: ServiceWorkerUtils<AuthSwSetConfigDto>);
20
21
  isAuthSessionActive(): boolean;
21
22
  isAuthSessionReady(): boolean;
@@ -28,6 +29,7 @@ declare class AuthManagerImpl implements AuthManagerInterface {
28
29
  private getV2Configs;
29
30
  private normalizeV1Config;
30
31
  private validateSessionConfig;
32
+ private validateUrlPrefixes;
31
33
  private validateUrlRewriteRules;
32
34
  private updateSessionState;
33
35
  private isConfigUsable;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytescale/sdk",
3
- "version": "3.61.0",
3
+ "version": "3.62.0",
4
4
  "description": "Bytescale JavaScript SDK",
5
5
  "author": "Bytescale <hello@bytescale.com> (https://www.bytescale.com)",
6
6
  "license": "MIT",
@@ -60,6 +60,20 @@ 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
+
63
77
  test("awaits manager-owned authentication at the expiry boundary", async () => {
64
78
  const configState = state("customer", accountA, "jwt-old", "access-old");
65
79
  configState.expiresAt = Date.now();
@@ -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
  ],