@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 +91 -0
- package/dist/browser/cjs/main.js +15 -6
- package/dist/browser/esm/main.mjs +15 -6
- package/dist/types/private/dtos/AuthSwConfigEntryDto.d.ts +3 -1
- package/dist/types/private/model/AuthSessionConfigBase.d.ts +8 -0
- package/dist/types/public/browser/AuthManagerBrowser.d.ts +2 -0
- package/package.json +1 -1
- package/tests/ApiClientAuth.test.ts +14 -0
- package/tests/AuthManagerBrowser.test.ts +98 -2
- package/tests/AuthServiceWorkerRequestScope.test.ts +259 -0
- package/tests/AuthServiceWorkerRewrite.test.ts +17 -152
- package/tests/fixtures/auth-sw-3.61.0.js +272 -0
- package/tests/utils/AuthServiceWorkerHarness.ts +183 -0
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:
|
package/dist/browser/cjs/main.js
CHANGED
|
@@ -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
|
-
|
|
4242
|
-
|
|
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
|
-
|
|
4202
|
-
|
|
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
|
-
/**
|
|
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
|
@@ -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:
|
|
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
|
],
|