@bytescale/sdk 3.57.0 → 3.58.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/dist/browser/cjs/main.js +13 -5
- package/dist/browser/esm/main.mjs +13 -5
- package/dist/types/private/dtos/AuthSwSetConfigDto.d.ts +2 -0
- package/dist/types/private/model/AuthManagerInterface.d.ts +13 -1
- package/dist/types/public/browser/AuthManagerBrowser.d.ts +1 -1
- package/dist/types/public/node/AuthManagerNode.d.ts +1 -1
- package/package.json +1 -1
- package/tests/AuthManagerBrowser.test.ts +64 -8
- package/tests/AuthServiceWorkerRewrite.test.ts +276 -0
package/dist/browser/cjs/main.js
CHANGED
|
@@ -3513,9 +3513,15 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3513
3513
|
}))) {
|
|
3514
3514
|
throw new Error("The 'sourceUrlPrefixes' field returned by 'serviceWorkerConfig' must be an array of strings.");
|
|
3515
3515
|
}
|
|
3516
|
+
if (result.urlRewriteRules !== undefined && (!Array.isArray(result.urlRewriteRules) || !result.urlRewriteRules.every(function (rule) {
|
|
3517
|
+
return rule !== null && AuthManagerBrowser_typeof(rule) === "object" && typeof rule.fromUrlPrefix === "string" && typeof rule.toUrlPrefix === "string";
|
|
3518
|
+
}))) {
|
|
3519
|
+
throw new Error("The 'urlRewriteRules' field returned by 'serviceWorkerConfig' must be an array of URL rewrite rules.");
|
|
3520
|
+
}
|
|
3516
3521
|
var config = {
|
|
3517
3522
|
additionalConfig: result.additionalConfig,
|
|
3518
|
-
sourceUrlPrefixes: result.sourceUrlPrefixes
|
|
3523
|
+
sourceUrlPrefixes: result.sourceUrlPrefixes,
|
|
3524
|
+
urlRewriteRules: result.urlRewriteRules
|
|
3519
3525
|
};
|
|
3520
3526
|
return AuthManagerBrowser_invoke(function () {
|
|
3521
3527
|
if (session.primaryAuthSwConfig !== undefined) {
|
|
@@ -3555,7 +3561,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3555
3561
|
var _a;
|
|
3556
3562
|
return AuthManagerBrowser_await(AuthManagerBrowser_awaitIgnored(_this5.sendServiceWorkerConfig(session, [Object.assign(Object.assign({}, primaryConfig), {
|
|
3557
3563
|
sourceUrlPrefixes: serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.sourceUrlPrefixes
|
|
3558
|
-
})].concat(AuthManagerBrowser_toConsumableArray((_a = serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.additionalConfig) !== null && _a !== void 0 ? _a : [])))));
|
|
3564
|
+
})].concat(AuthManagerBrowser_toConsumableArray((_a = serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.additionalConfig) !== null && _a !== void 0 ? _a : [])), serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.urlRewriteRules)));
|
|
3559
3565
|
} catch (e) {
|
|
3560
3566
|
return Promise.reject(e);
|
|
3561
3567
|
}
|
|
@@ -3582,20 +3588,22 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3582
3588
|
}
|
|
3583
3589
|
}, {
|
|
3584
3590
|
key: "sendServiceWorkerConfig",
|
|
3585
|
-
value: function sendServiceWorkerConfig(session, config) {
|
|
3591
|
+
value: function sendServiceWorkerConfig(session, config, urlRewriteRules) {
|
|
3586
3592
|
try {
|
|
3587
3593
|
var _this6 = this;
|
|
3588
3594
|
if (session.authServiceWorker === undefined) {
|
|
3589
3595
|
throw new Error("Service worker configuration cannot be applied because service workers are unavailable.");
|
|
3590
3596
|
}
|
|
3591
|
-
return AuthManagerBrowser_await(_this6.serviceWorkerUtils.sendMessage({
|
|
3597
|
+
return AuthManagerBrowser_await(_this6.serviceWorkerUtils.sendMessage(Object.assign({
|
|
3592
3598
|
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
3593
3599
|
config: config.map(function (entry) {
|
|
3594
3600
|
return Object.assign(Object.assign({}, entry), {
|
|
3595
3601
|
urlPrefix: "".concat(entry.sourceUrlPrefixes === undefined ? "" : _this6.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
|
|
3596
3602
|
});
|
|
3597
3603
|
})
|
|
3598
|
-
},
|
|
3604
|
+
}, urlRewriteRules === undefined ? {} : {
|
|
3605
|
+
urlRewriteRules: urlRewriteRules
|
|
3606
|
+
}), session.authServiceWorker, _this6.serviceWorkerScriptFieldName), function (_this6$serviceWorkerU) {
|
|
3599
3607
|
session.authServiceWorker = _this6$serviceWorkerU;
|
|
3600
3608
|
});
|
|
3601
3609
|
} catch (e) {
|
|
@@ -3473,9 +3473,15 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3473
3473
|
}))) {
|
|
3474
3474
|
throw new Error("The 'sourceUrlPrefixes' field returned by 'serviceWorkerConfig' must be an array of strings.");
|
|
3475
3475
|
}
|
|
3476
|
+
if (result.urlRewriteRules !== undefined && (!Array.isArray(result.urlRewriteRules) || !result.urlRewriteRules.every(function (rule) {
|
|
3477
|
+
return rule !== null && AuthManagerBrowser_typeof(rule) === "object" && typeof rule.fromUrlPrefix === "string" && typeof rule.toUrlPrefix === "string";
|
|
3478
|
+
}))) {
|
|
3479
|
+
throw new Error("The 'urlRewriteRules' field returned by 'serviceWorkerConfig' must be an array of URL rewrite rules.");
|
|
3480
|
+
}
|
|
3476
3481
|
var config = {
|
|
3477
3482
|
additionalConfig: result.additionalConfig,
|
|
3478
|
-
sourceUrlPrefixes: result.sourceUrlPrefixes
|
|
3483
|
+
sourceUrlPrefixes: result.sourceUrlPrefixes,
|
|
3484
|
+
urlRewriteRules: result.urlRewriteRules
|
|
3479
3485
|
};
|
|
3480
3486
|
return AuthManagerBrowser_invoke(function () {
|
|
3481
3487
|
if (session.primaryAuthSwConfig !== undefined) {
|
|
@@ -3515,7 +3521,7 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3515
3521
|
var _a;
|
|
3516
3522
|
return AuthManagerBrowser_await(AuthManagerBrowser_awaitIgnored(_this5.sendServiceWorkerConfig(session, [Object.assign(Object.assign({}, primaryConfig), {
|
|
3517
3523
|
sourceUrlPrefixes: serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.sourceUrlPrefixes
|
|
3518
|
-
})].concat(AuthManagerBrowser_toConsumableArray((_a = serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.additionalConfig) !== null && _a !== void 0 ? _a : [])))));
|
|
3524
|
+
})].concat(AuthManagerBrowser_toConsumableArray((_a = serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.additionalConfig) !== null && _a !== void 0 ? _a : [])), serviceWorkerConfig === null || serviceWorkerConfig === void 0 ? void 0 : serviceWorkerConfig.urlRewriteRules)));
|
|
3519
3525
|
} catch (e) {
|
|
3520
3526
|
return Promise.reject(e);
|
|
3521
3527
|
}
|
|
@@ -3542,20 +3548,22 @@ var AuthManagerImpl = /*#__PURE__*/function () {
|
|
|
3542
3548
|
}
|
|
3543
3549
|
}, {
|
|
3544
3550
|
key: "sendServiceWorkerConfig",
|
|
3545
|
-
value: function sendServiceWorkerConfig(session, config) {
|
|
3551
|
+
value: function sendServiceWorkerConfig(session, config, urlRewriteRules) {
|
|
3546
3552
|
try {
|
|
3547
3553
|
var _this6 = this;
|
|
3548
3554
|
if (session.authServiceWorker === undefined) {
|
|
3549
3555
|
throw new Error("Service worker configuration cannot be applied because service workers are unavailable.");
|
|
3550
3556
|
}
|
|
3551
|
-
return AuthManagerBrowser_await(_this6.serviceWorkerUtils.sendMessage({
|
|
3557
|
+
return AuthManagerBrowser_await(_this6.serviceWorkerUtils.sendMessage(Object.assign({
|
|
3552
3558
|
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
3553
3559
|
config: config.map(function (entry) {
|
|
3554
3560
|
return Object.assign(Object.assign({}, entry), {
|
|
3555
3561
|
urlPrefix: "".concat(entry.sourceUrlPrefixes === undefined ? "" : _this6.sourceScopedUrlPrefixMarker).concat(entry.urlPrefix)
|
|
3556
3562
|
});
|
|
3557
3563
|
})
|
|
3558
|
-
},
|
|
3564
|
+
}, urlRewriteRules === undefined ? {} : {
|
|
3565
|
+
urlRewriteRules: urlRewriteRules
|
|
3566
|
+
}), session.authServiceWorker, _this6.serviceWorkerScriptFieldName), function (_this6$serviceWorkerU) {
|
|
3559
3567
|
session.authServiceWorker = _this6$serviceWorkerU;
|
|
3560
3568
|
});
|
|
3561
3569
|
} catch (e) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AuthSwConfigDto } from "./AuthSwConfigDto";
|
|
2
|
+
import { UrlRewriteRule } from "../model/AuthManagerInterface";
|
|
2
3
|
export interface AuthSwSetConfigDto {
|
|
3
4
|
config: AuthSwConfigDto;
|
|
4
5
|
/**
|
|
@@ -8,4 +9,5 @@ export interface AuthSwSetConfigDto {
|
|
|
8
9
|
* with the user's events.
|
|
9
10
|
*/
|
|
10
11
|
type: "SET_BYTESCALE_AUTH_CONFIG";
|
|
12
|
+
urlRewriteRules?: UrlRewriteRule[];
|
|
11
13
|
}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { BytescaleApiClientConfig } from "../../public/shared";
|
|
2
2
|
import { AuthSwConfigEntryDto } from "../dtos/AuthSwConfigEntryDto";
|
|
3
|
+
export interface UrlRewriteRule {
|
|
4
|
+
/** URL prefix to replace. This URL must be covered by the service worker's origin and scope. */
|
|
5
|
+
fromUrlPrefix: string;
|
|
6
|
+
/** Replacement URL prefix. The remainder of the original URL is appended unchanged. */
|
|
7
|
+
toUrlPrefix: string;
|
|
8
|
+
}
|
|
3
9
|
export interface AuthManagerServiceWorkerConfig {
|
|
4
10
|
/**
|
|
5
11
|
* Additional download-only service-worker authorization rules.
|
|
@@ -12,6 +18,11 @@ export interface AuthManagerServiceWorkerConfig {
|
|
|
12
18
|
* History API changes do not update the matched client URL.
|
|
13
19
|
*/
|
|
14
20
|
sourceUrlPrefixes?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Rewrites matching service-worker requests before applying authorization rules. The first matching rule wins and
|
|
23
|
+
* rewritten URLs are not processed recursively.
|
|
24
|
+
*/
|
|
25
|
+
urlRewriteRules?: UrlRewriteRule[];
|
|
15
26
|
}
|
|
16
27
|
export interface BeginAuthSessionParams {
|
|
17
28
|
/**
|
|
@@ -34,7 +45,8 @@ export interface BeginAuthSessionParams {
|
|
|
34
45
|
options?: Pick<BytescaleApiClientConfig, "fetchApi" | "cdnUrl">;
|
|
35
46
|
/**
|
|
36
47
|
* Returns configuration for service-worker authorization. The primary JWT returned by `authUrl` is restricted using
|
|
37
|
-
* `sourceUrlPrefixes`; `additionalConfig` contains download-only rules and never authenticates SDK API operations
|
|
48
|
+
* `sourceUrlPrefixes`; `additionalConfig` contains download-only rules and never authenticates SDK API operations;
|
|
49
|
+
* `urlRewriteRules` rewrites in-scope URLs before download authorization is evaluated.
|
|
38
50
|
* Called when the session begins and again 20 seconds before the earliest additional entry expires. Entries without
|
|
39
51
|
* an expiry do not trigger a refresh.
|
|
40
52
|
*
|
|
@@ -3,7 +3,7 @@ import { AuthSwSetConfigDto } from "../../private/dtos/AuthSwSetConfigDto";
|
|
|
3
3
|
import { ServiceWorkerUtils } from "../../private/ServiceWorkerUtils";
|
|
4
4
|
export type { AuthSwConfigEntryDto } from "../../private/dtos/AuthSwConfigEntryDto";
|
|
5
5
|
export type { AuthSwHeaderDto } from "../../private/dtos/AuthSwHeaderDto";
|
|
6
|
-
export type { AuthManagerServiceWorkerConfig, BeginAuthSessionParams } from "../../private/model/AuthManagerInterface";
|
|
6
|
+
export type { AuthManagerServiceWorkerConfig, BeginAuthSessionParams, UrlRewriteRule } from "../../private/model/AuthManagerInterface";
|
|
7
7
|
declare class AuthManagerImpl implements AuthManagerInterface {
|
|
8
8
|
private readonly serviceWorkerUtils;
|
|
9
9
|
private readonly authSessionMutex;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AuthManagerInterface, BeginAuthSessionParams } from "../../private/model/AuthManagerInterface";
|
|
2
2
|
export type { AuthSwConfigEntryDto } from "../../private/dtos/AuthSwConfigEntryDto";
|
|
3
3
|
export type { AuthSwHeaderDto } from "../../private/dtos/AuthSwHeaderDto";
|
|
4
|
-
export type { AuthManagerServiceWorkerConfig, BeginAuthSessionParams } from "../../private/model/AuthManagerInterface";
|
|
4
|
+
export type { AuthManagerServiceWorkerConfig, BeginAuthSessionParams, UrlRewriteRule } from "../../private/model/AuthManagerInterface";
|
|
5
5
|
declare class AuthManagerImpl implements AuthManagerInterface {
|
|
6
6
|
beginAuthSession(_params: BeginAuthSessionParams): Promise<void>;
|
|
7
7
|
endAuthSession(): Promise<void>;
|
package/package.json
CHANGED
|
@@ -4,7 +4,8 @@ import { AuthSessionState } from "../src/private/AuthSessionState";
|
|
|
4
4
|
import type {
|
|
5
5
|
AuthManagerServiceWorkerConfig,
|
|
6
6
|
AuthSwConfigEntryDto,
|
|
7
|
-
BeginAuthSessionParams
|
|
7
|
+
BeginAuthSessionParams,
|
|
8
|
+
UrlRewriteRule
|
|
8
9
|
} from "../src/index.browser";
|
|
9
10
|
|
|
10
11
|
type FetchApi = NonNullable<NonNullable<BeginAuthSessionParams["options"]>["fetchApi"]>;
|
|
@@ -130,10 +131,17 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
130
131
|
urlPrefix: "https://upcdn.io/account-c/"
|
|
131
132
|
}
|
|
132
133
|
];
|
|
134
|
+
const urlRewriteRules: UrlRewriteRule[] = [
|
|
135
|
+
{
|
|
136
|
+
fromUrlPrefix: "https://app.example.com/__authenticated-download/",
|
|
137
|
+
toUrlPrefix: "https://upcdn.io/account-b/"
|
|
138
|
+
}
|
|
139
|
+
];
|
|
133
140
|
const serviceWorkerConfig = jest.fn(
|
|
134
141
|
async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
135
142
|
additionalConfig,
|
|
136
|
-
sourceUrlPrefixes: ["https://app.example.com/"]
|
|
143
|
+
sourceUrlPrefixes: ["https://app.example.com/"],
|
|
144
|
+
urlRewriteRules
|
|
137
145
|
})
|
|
138
146
|
);
|
|
139
147
|
|
|
@@ -159,7 +167,8 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
159
167
|
},
|
|
160
168
|
additionalConfig[1]
|
|
161
169
|
],
|
|
162
|
-
type: "SET_BYTESCALE_AUTH_CONFIG"
|
|
170
|
+
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
171
|
+
urlRewriteRules
|
|
163
172
|
});
|
|
164
173
|
expect(additionalConfig[0].urlPrefix).toBe("https://upcdn.io/account-b/");
|
|
165
174
|
expect(AuthSessionState.getSession()?.accessToken).toBe("access-a");
|
|
@@ -184,11 +193,23 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
184
193
|
.fn<() => Promise<AuthManagerServiceWorkerConfig>>()
|
|
185
194
|
.mockResolvedValueOnce({
|
|
186
195
|
additionalConfig: initialAdditionalConfig,
|
|
187
|
-
sourceUrlPrefixes: ["https://app.example.com/initial/"]
|
|
196
|
+
sourceUrlPrefixes: ["https://app.example.com/initial/"],
|
|
197
|
+
urlRewriteRules: [
|
|
198
|
+
{
|
|
199
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
200
|
+
toUrlPrefix: "https://upcdn.io/account-b/"
|
|
201
|
+
}
|
|
202
|
+
]
|
|
188
203
|
})
|
|
189
204
|
.mockResolvedValueOnce({
|
|
190
205
|
additionalConfig: [],
|
|
191
|
-
sourceUrlPrefixes: ["https://app.example.com/refreshed/"]
|
|
206
|
+
sourceUrlPrefixes: ["https://app.example.com/refreshed/"],
|
|
207
|
+
urlRewriteRules: [
|
|
208
|
+
{
|
|
209
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
210
|
+
toUrlPrefix: "https://upcdn.io/account-c/"
|
|
211
|
+
}
|
|
212
|
+
]
|
|
192
213
|
});
|
|
193
214
|
|
|
194
215
|
await AuthManager.beginAuthSession({
|
|
@@ -208,7 +229,13 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
208
229
|
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
209
230
|
}
|
|
210
231
|
],
|
|
211
|
-
type: "SET_BYTESCALE_AUTH_CONFIG"
|
|
232
|
+
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
233
|
+
urlRewriteRules: [
|
|
234
|
+
{
|
|
235
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
236
|
+
toUrlPrefix: "https://upcdn.io/account-c/"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
212
239
|
});
|
|
213
240
|
expect(fetchApi.mock.calls.map(([, init]) => init?.method)).toEqual(["GET", "PUT"]);
|
|
214
241
|
expect(AuthManager.isAuthSessionReady()).toBe(true);
|
|
@@ -225,7 +252,13 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
225
252
|
];
|
|
226
253
|
const serviceWorkerConfig = jest.fn(
|
|
227
254
|
async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
228
|
-
additionalConfig
|
|
255
|
+
additionalConfig,
|
|
256
|
+
urlRewriteRules: [
|
|
257
|
+
{
|
|
258
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
259
|
+
toUrlPrefix: "https://upcdn.io/account-b/"
|
|
260
|
+
}
|
|
261
|
+
]
|
|
229
262
|
})
|
|
230
263
|
);
|
|
231
264
|
|
|
@@ -254,8 +287,31 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
254
287
|
},
|
|
255
288
|
additionalConfig[0]
|
|
256
289
|
],
|
|
257
|
-
type: "SET_BYTESCALE_AUTH_CONFIG"
|
|
290
|
+
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
291
|
+
urlRewriteRules: [
|
|
292
|
+
{
|
|
293
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
294
|
+
toUrlPrefix: "https://upcdn.io/account-b/"
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("rejects malformed URL rewrite rules", async () => {
|
|
301
|
+
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
302
|
+
const fetchApi = createPrimaryFetchApi();
|
|
303
|
+
|
|
304
|
+
await AuthManager.beginAuthSession({
|
|
305
|
+
...createParams(fetchApi),
|
|
306
|
+
serviceWorkerConfig: async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
307
|
+
additionalConfig: [],
|
|
308
|
+
urlRewriteRules: [{ fromUrlPrefix: "https://app.example.com/download/" }] as UrlRewriteRule[]
|
|
309
|
+
}),
|
|
310
|
+
serviceWorkerScript: "/bytescale-auth-sw.js"
|
|
258
311
|
});
|
|
312
|
+
|
|
313
|
+
expect(postMessage).not.toHaveBeenCalled();
|
|
314
|
+
expect(AuthManager.isAuthSessionReady()).toBe(false);
|
|
259
315
|
});
|
|
260
316
|
|
|
261
317
|
test("fails closed until the initial service-worker config callback succeeds", async () => {
|
|
@@ -0,0 +1,276 @@
|
|
|
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");
|
|
10
|
+
|
|
11
|
+
describe("Auth service-worker URL rewriting", () => {
|
|
12
|
+
test("preserves the remaining path, query string, and fragment", async () => {
|
|
13
|
+
const harness = new AuthServiceWorkerHarness();
|
|
14
|
+
const rules = [rewriteRule("download/", "account-a/")];
|
|
15
|
+
await harness.setConfig([], rules);
|
|
16
|
+
|
|
17
|
+
const result = await harness.dispatchFetch(
|
|
18
|
+
"https://dashboard.example.com/download/path/to/file.pdf?download=true&version=2"
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/path/to/file.pdf?download=true&version=2");
|
|
22
|
+
expect(harness.getRewrittenUrl("https://dashboard.example.com/download/file.pdf?download=true#page=2", rules)).toBe(
|
|
23
|
+
"https://upcdn.io/account-a/file.pdf?download=true#page=2"
|
|
24
|
+
);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("matches authentication against the rewritten URL and supports navigation requests", async () => {
|
|
28
|
+
const upstreamResponse = new NodeFetchResponse("streamed-body", {
|
|
29
|
+
headers: {
|
|
30
|
+
"Accept-Ranges": "bytes",
|
|
31
|
+
"Content-Disposition": 'attachment; filename="file.pdf"',
|
|
32
|
+
"Content-Length": "13",
|
|
33
|
+
"Content-Range": "bytes 0-12/13",
|
|
34
|
+
"Content-Type": "application/pdf"
|
|
35
|
+
},
|
|
36
|
+
status: 206
|
|
37
|
+
});
|
|
38
|
+
const blob = jest.spyOn(upstreamResponse, "blob");
|
|
39
|
+
const arrayBuffer = jest.spyOn(upstreamResponse, "arrayBuffer");
|
|
40
|
+
const harness = new AuthServiceWorkerHarness(upstreamResponse);
|
|
41
|
+
await harness.setConfig([authConfig("account-a/", "token-a")], [rewriteRule("download/", "account-a/")]);
|
|
42
|
+
|
|
43
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf", {
|
|
44
|
+
headers: {
|
|
45
|
+
"If-Modified-Since": "Wed, 21 Oct 2015 07:28:00 GMT",
|
|
46
|
+
"If-None-Match": '"etag"',
|
|
47
|
+
"If-Range": '"range-etag"',
|
|
48
|
+
"Range": "bytes=0-12"
|
|
49
|
+
},
|
|
50
|
+
navigation: true
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
54
|
+
expect(result.outboundRequest?.mode).toBe("cors");
|
|
55
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
56
|
+
expect(result.outboundRequest?.headers.get("Range")).toBe("bytes=0-12");
|
|
57
|
+
expect(result.outboundRequest?.headers.get("If-Range")).toBe('"range-etag"');
|
|
58
|
+
expect(result.outboundRequest?.headers.get("If-None-Match")).toBe('"etag"');
|
|
59
|
+
expect(result.outboundRequest?.headers.get("If-Modified-Since")).toBe("Wed, 21 Oct 2015 07:28:00 GMT");
|
|
60
|
+
expect(result.response).toBe(upstreamResponse);
|
|
61
|
+
expect(result.response?.status).toBe(206);
|
|
62
|
+
expect(result.response?.headers.get("Content-Disposition")).toBe('attachment; filename="file.pdf"');
|
|
63
|
+
expect(result.response?.headers.get("Accept-Ranges")).toBe("bytes");
|
|
64
|
+
expect(blob).not.toHaveBeenCalled();
|
|
65
|
+
expect(arrayBuffer).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("selects different authentication configurations for different rewrite destinations", async () => {
|
|
69
|
+
const harness = new AuthServiceWorkerHarness();
|
|
70
|
+
await harness.setConfig(
|
|
71
|
+
[authConfig("account-a/", "token-a"), authConfig("account-b/", "token-b")],
|
|
72
|
+
[rewriteRule("download-a/", "account-a/"), rewriteRule("download-b/", "account-b/")]
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
const first = await harness.dispatchFetch("https://dashboard.example.com/download-a/file.pdf");
|
|
76
|
+
const second = await harness.dispatchFetch("https://dashboard.example.com/download-b/file.pdf");
|
|
77
|
+
|
|
78
|
+
expect(first.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
79
|
+
expect(second.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("never selects a token from the original URL", async () => {
|
|
83
|
+
const harness = new AuthServiceWorkerHarness();
|
|
84
|
+
await harness.setConfig(
|
|
85
|
+
[authConfigForUrl("https://dashboard.example.com/download/", "original-token")],
|
|
86
|
+
[rewriteRule("download/", "unconfigured-account/")]
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
90
|
+
|
|
91
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/unconfigured-account/file.pdf");
|
|
92
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("fetches rewritten requests even when no authentication configuration matches", async () => {
|
|
96
|
+
const harness = new AuthServiceWorkerHarness();
|
|
97
|
+
await harness.setConfig([], [rewriteRule("download/", "public-account/")]);
|
|
98
|
+
|
|
99
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/public.pdf");
|
|
100
|
+
|
|
101
|
+
expect(result.responded).toBe(true);
|
|
102
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/public-account/public.pdf");
|
|
103
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("uses only the first matching rewrite rule", async () => {
|
|
107
|
+
const harness = new AuthServiceWorkerHarness();
|
|
108
|
+
await harness.setConfig(
|
|
109
|
+
[authConfig("account-a/", "token-a"), authConfig("account-b/", "token-b")],
|
|
110
|
+
[rewriteRule("download/", "account-a/"), rewriteRule("download/", "account-b/")]
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
114
|
+
|
|
115
|
+
expect(result.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
116
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("does not recursively rewrite the rewritten URL", async () => {
|
|
120
|
+
const harness = new AuthServiceWorkerHarness();
|
|
121
|
+
await harness.setConfig(
|
|
122
|
+
[authConfig("account-a/", "token-a")],
|
|
123
|
+
[
|
|
124
|
+
{
|
|
125
|
+
fromUrlPrefix: "https://dashboard.example.com/download/",
|
|
126
|
+
toUrlPrefix: "https://proxy.example.com/download/"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
fromUrlPrefix: "https://proxy.example.com/download/",
|
|
130
|
+
toUrlPrefix: "https://upcdn.io/account-a/"
|
|
131
|
+
}
|
|
132
|
+
]
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const result = await harness.dispatchFetch("https://dashboard.example.com/download/file.pdf");
|
|
136
|
+
|
|
137
|
+
expect(result.outboundRequest?.url).toBe("https://proxy.example.com/download/file.pdf");
|
|
138
|
+
expect(result.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("retains existing behavior when no rewrite rule matches", async () => {
|
|
142
|
+
const harness = new AuthServiceWorkerHarness();
|
|
143
|
+
await harness.setConfig([authConfig("account-a/", "token-a")], [rewriteRule("download/", "account-a/")]);
|
|
144
|
+
|
|
145
|
+
const authenticated = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf");
|
|
146
|
+
const untouched = await harness.dispatchFetch("https://dashboard.example.com/ordinary-page");
|
|
147
|
+
|
|
148
|
+
expect(authenticated.outboundRequest?.url).toBe("https://upcdn.io/account-a/file.pdf");
|
|
149
|
+
expect(authenticated.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
150
|
+
expect(untouched.responded).toBe(false);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
interface FetchOptions {
|
|
155
|
+
headers?: HeadersInit;
|
|
156
|
+
navigation?: boolean;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface FetchResult {
|
|
160
|
+
outboundRequest: TestRequest | undefined;
|
|
161
|
+
responded: boolean;
|
|
162
|
+
response: NodeFetchResponse | undefined;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
type WorkerEventListener = (event: unknown) => void;
|
|
166
|
+
|
|
167
|
+
class AuthServiceWorkerHarness {
|
|
168
|
+
private readonly context: {
|
|
169
|
+
getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
|
|
170
|
+
setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
private readonly fetchListener: WorkerEventListener;
|
|
174
|
+
private readonly fetchMock: jest.MockedFunction<(request: TestRequest) => Promise<NodeFetchResponse>>;
|
|
175
|
+
|
|
176
|
+
constructor(private readonly upstreamResponse: NodeFetchResponse = new NodeFetchResponse("ok")) {
|
|
177
|
+
const listeners = new Map<string, WorkerEventListener>();
|
|
178
|
+
const cacheEntries = new Map<string, NodeFetchResponse>();
|
|
179
|
+
this.fetchMock = jest.fn(async (_request: TestRequest): Promise<NodeFetchResponse> => this.upstreamResponse);
|
|
180
|
+
|
|
181
|
+
const self = {
|
|
182
|
+
addEventListener: (type: string, listener: WorkerEventListener): void => {
|
|
183
|
+
listeners.set(type, listener);
|
|
184
|
+
},
|
|
185
|
+
clients: {
|
|
186
|
+
claim: async (): Promise<void> => {},
|
|
187
|
+
get: async (): Promise<undefined> => undefined
|
|
188
|
+
},
|
|
189
|
+
skipWaiting: async (): Promise<void> => {}
|
|
190
|
+
};
|
|
191
|
+
const cache = {
|
|
192
|
+
match: async (key: string): Promise<NodeFetchResponse | undefined> => cacheEntries.get(key)?.clone(),
|
|
193
|
+
put: async (key: string, value: NodeFetchResponse): Promise<void> => {
|
|
194
|
+
cacheEntries.set(key, value.clone());
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
const sandbox = {
|
|
198
|
+
caches: { open: async (): Promise<typeof cache> => cache },
|
|
199
|
+
console: { error: jest.fn(), log: jest.fn() },
|
|
200
|
+
fetch: this.fetchMock,
|
|
201
|
+
Headers: NodeFetchHeaders,
|
|
202
|
+
Promise,
|
|
203
|
+
Request: TestRequest,
|
|
204
|
+
Response: NodeFetchResponse,
|
|
205
|
+
self,
|
|
206
|
+
setTimeout
|
|
207
|
+
};
|
|
208
|
+
runInNewContext(workerSource, sandbox);
|
|
209
|
+
|
|
210
|
+
this.context = sandbox as typeof sandbox & AuthServiceWorkerHarness["context"];
|
|
211
|
+
const fetchListener = listeners.get("fetch");
|
|
212
|
+
if (fetchListener === undefined) {
|
|
213
|
+
throw new Error("Auth service worker did not register a fetch listener.");
|
|
214
|
+
}
|
|
215
|
+
this.fetchListener = fetchListener;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async setConfig(config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]): Promise<void> {
|
|
219
|
+
await this.context.setConfig(config, rules);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
getRewrittenUrl(url: string, rules: UrlRewriteRule[]): string | undefined {
|
|
223
|
+
return this.context.getRewrittenUrl(url, rules);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
|
|
227
|
+
const request = new TestRequest(url, { headers: options.headers as NodeFetchRequestInit["headers"] });
|
|
228
|
+
if (options.navigation === true) {
|
|
229
|
+
Object.defineProperty(request, "mode", { configurable: true, value: "navigate" });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let responsePromise: Promise<NodeFetchResponse> | undefined;
|
|
233
|
+
this.fetchListener({
|
|
234
|
+
clientId: "",
|
|
235
|
+
request,
|
|
236
|
+
respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
|
|
237
|
+
responsePromise = Promise.resolve(response);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const response = await responsePromise;
|
|
242
|
+
return {
|
|
243
|
+
outboundRequest: this.fetchMock.mock.calls.at(-1)?.[0],
|
|
244
|
+
responded: responsePromise !== undefined,
|
|
245
|
+
response
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
class TestRequest extends NodeFetchRequest {
|
|
251
|
+
readonly mode: RequestMode;
|
|
252
|
+
|
|
253
|
+
constructor(input: string | NodeFetchRequest, init: NodeFetchRequestInit & { mode?: RequestMode } = {}) {
|
|
254
|
+
super(input, init);
|
|
255
|
+
this.mode = init.mode ?? (input instanceof TestRequest ? input.mode : "cors");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function authConfig(accountPath: string, token: string): AuthSwConfigEntryDto {
|
|
260
|
+
return authConfigForUrl(`https://upcdn.io/${accountPath}`, token);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function authConfigForUrl(urlPrefix: string, token: string): AuthSwConfigEntryDto {
|
|
264
|
+
return {
|
|
265
|
+
expires: undefined,
|
|
266
|
+
headers: [{ key: "Authorization", value: `Bearer ${token}` }],
|
|
267
|
+
urlPrefix
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function rewriteRule(fromPath: string, toPath: string): UrlRewriteRule {
|
|
272
|
+
return {
|
|
273
|
+
fromUrlPrefix: `https://dashboard.example.com/${fromPath}`,
|
|
274
|
+
toUrlPrefix: `https://upcdn.io/${toPath}`
|
|
275
|
+
};
|
|
276
|
+
}
|