@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
// Frozen worker from SDK 3.61.0 (ee6a3a2d), retained for compatibility tests.
|
|
2
|
+
/* eslint-disable no-undef */
|
|
3
|
+
/**
|
|
4
|
+
* Bytescale Auth Service Worker (SW)
|
|
5
|
+
*
|
|
6
|
+
* This script should be referenced by the "serviceWorkerScript" field in the "AuthManager.beginAuthSession" method of
|
|
7
|
+
* the Bytescale JavaScript SDK to append "Authorization" headers to HTTP requests sent to the Bytescale CDN. This
|
|
8
|
+
* approach serves as an alternative to cookie-based authentication, which is incompatible with certain modern browsers.
|
|
9
|
+
*
|
|
10
|
+
* Documentation:
|
|
11
|
+
* - https://www.bytescale.com/docs/types/BeginAuthSessionParams#serviceWorkerScript
|
|
12
|
+
*/
|
|
13
|
+
let transientCache; // [{urlPrefix, headers, expires?}] (See: AuthSwConfigDto)
|
|
14
|
+
let transientUrlRewriteRules;
|
|
15
|
+
const maxSourceUrlCacheEntries = 1000;
|
|
16
|
+
const persistentCacheName = "bytescale-sw-config";
|
|
17
|
+
const persistentCacheKey = "config";
|
|
18
|
+
const persistentUrlRewriteRulesCacheKey = "url-rewrite-rules";
|
|
19
|
+
const sourceScopedUrlPrefixMarker = "!bytescale-source-scoped!";
|
|
20
|
+
const sourceUrlsByClientId = new Map();
|
|
21
|
+
|
|
22
|
+
console.log(`[bytescale] Auth SW Registered`);
|
|
23
|
+
|
|
24
|
+
self.addEventListener("install", function (event) {
|
|
25
|
+
// Typically service workers go: 'installing' -> 'waiting' -> 'activated'.
|
|
26
|
+
// However, we skip the 'waiting' phase as we want this service worker to be used immediately after it's installed,
|
|
27
|
+
// instead of requiring a page refresh if the browser already has an old version of the service worker installed.
|
|
28
|
+
event.waitUntil(self.skipWaiting());
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
self.addEventListener("activate", function (event) {
|
|
32
|
+
// Immediately allow the service worker to intercept "fetch" events (instead of requiring a page refresh) if this is
|
|
33
|
+
// the first time this service worker is being installed.
|
|
34
|
+
event.waitUntil(self.clients.claim());
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
self.addEventListener("message", event => {
|
|
38
|
+
// Allows communication with the windows/tabs that have are able to generate the JWT (as they have the auth session with the user's API).
|
|
39
|
+
// See: AuthSwSetConfigDto
|
|
40
|
+
if (event.data) {
|
|
41
|
+
switch (event.data.type) {
|
|
42
|
+
// Auth sessions are started/ended by calling SET_CONFIG with auth config or with 'undefined' config, respectively.
|
|
43
|
+
// We use 'undefined' to end the auth session instead of unregistering the worker, as there may be multiple tabs
|
|
44
|
+
// in the user's application, so while the user may sign out in one tab, they may remain signed in to another tab,
|
|
45
|
+
// which may subsequently send a follow-up 'SET_CONFIG' which will resume auth.
|
|
46
|
+
case "SET_BYTESCALE_AUTH_CONFIG":
|
|
47
|
+
setConfig(event.data.config, event.data.urlRewriteRules).then(
|
|
48
|
+
() => {},
|
|
49
|
+
e => console.error(`[bytescale] Auth SW failed to persist config.`, e)
|
|
50
|
+
);
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
self.addEventListener("fetch", function (event) {
|
|
57
|
+
// Faster and intercepts only the required requests.
|
|
58
|
+
// Called in almost all cases.
|
|
59
|
+
const interceptSync = config => {
|
|
60
|
+
const newRequest = interceptRequest(event, config, transientUrlRewriteRules);
|
|
61
|
+
if (newRequest instanceof Promise) {
|
|
62
|
+
event.respondWith(
|
|
63
|
+
newRequest.then(
|
|
64
|
+
request => handleRequestErrors(request ?? event.request),
|
|
65
|
+
() => handleRequestErrors(event.request)
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
} else if (newRequest !== undefined) {
|
|
69
|
+
event.respondWith(handleRequestErrors(newRequest));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// Slower and intercepts all requests (while still only rewriting the relevant requests).
|
|
74
|
+
// Called only for the initial request after this Service Worker is restarted after going idle (e.g. after 30s on Firefox/Windows).
|
|
75
|
+
const interceptAsync = async () =>
|
|
76
|
+
await handleRequestErrors(
|
|
77
|
+
(await getState()
|
|
78
|
+
.then(state => (state === undefined ? undefined : interceptRequest(event, state.config, state.urlRewriteRules)))
|
|
79
|
+
.catch(() => undefined)) ?? event.request
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Makes it clearer to developers that the request failed for normal reasons (not reasons caused by this script).
|
|
83
|
+
const handleRequestErrors = async request => {
|
|
84
|
+
try {
|
|
85
|
+
return await fetch(request);
|
|
86
|
+
} catch (e) {
|
|
87
|
+
throw new Error("Network request failed: see previous browser errors for the cause.");
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// Optimization: avoids running async code (which necessitates intercepting all requests) when the config is already cached locally.
|
|
92
|
+
if (transientCache !== undefined && transientUrlRewriteRules !== undefined) {
|
|
93
|
+
interceptSync(transientCache);
|
|
94
|
+
} else {
|
|
95
|
+
event.respondWith(interceptAsync());
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
function interceptRequest(event, config, urlRewriteRules) {
|
|
100
|
+
const rewrittenUrl = getRewrittenUrl(event.request.url, urlRewriteRules);
|
|
101
|
+
const url = rewrittenUrl === undefined ? event.request.url : rewrittenUrl;
|
|
102
|
+
const fallbackRequest =
|
|
103
|
+
rewrittenUrl === undefined ? undefined : createCorsRequest(event.request, rewrittenUrl, event.request.headers);
|
|
104
|
+
|
|
105
|
+
if (config !== undefined) {
|
|
106
|
+
// Config is an array to support multiple different accounts within a single website, if needed.
|
|
107
|
+
for (const { expires, urlPrefix, headers, sourceUrlPrefixes } of config) {
|
|
108
|
+
const makeNewRequest = overwrite => {
|
|
109
|
+
const newHeaders = new Headers(event.request.headers);
|
|
110
|
+
if (overwrite) {
|
|
111
|
+
newHeaders.delete("Authorization");
|
|
112
|
+
newHeaders.delete("Authorization-Token");
|
|
113
|
+
}
|
|
114
|
+
for (const { key, value } of headers) {
|
|
115
|
+
if (overwrite || !newHeaders.has(key)) {
|
|
116
|
+
newHeaders.set(key, value);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return createCorsRequest(event.request, url, newHeaders);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
if (expires === undefined || expires > Date.now()) {
|
|
123
|
+
const isSourceScoped = sourceUrlPrefixes !== undefined;
|
|
124
|
+
|
|
125
|
+
// AuthManager adds the 'sourceScopedUrlPrefixMarker' prefix to the 'urlPrefix' when 'sourceUrlPrefixes' is provided,
|
|
126
|
+
// as this prevents old versions of the service worker (that don't support 'sourceUrlPrefixes') from intercepting the requests,
|
|
127
|
+
// since it would end up intercepting ALL requests, whereas the user's intention is to intercept only source-filtered requests.
|
|
128
|
+
const actualUrlPrefix = isSourceScoped
|
|
129
|
+
? urlPrefix.startsWith(sourceScopedUrlPrefixMarker)
|
|
130
|
+
? urlPrefix.substring(sourceScopedUrlPrefixMarker.length)
|
|
131
|
+
: undefined
|
|
132
|
+
: urlPrefix;
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
actualUrlPrefix !== undefined &&
|
|
136
|
+
url.startsWith(actualUrlPrefix) &&
|
|
137
|
+
event.request.method.toUpperCase() === "GET"
|
|
138
|
+
) {
|
|
139
|
+
if (isSourceScoped) {
|
|
140
|
+
if (!Array.isArray(sourceUrlPrefixes) || sourceUrlPrefixes.length === 0) {
|
|
141
|
+
return fallbackRequest;
|
|
142
|
+
}
|
|
143
|
+
return getSourceUrl(event.clientId).then(sourceUrl => {
|
|
144
|
+
if (sourceUrl === undefined || !sourceUrlPrefixes.some(prefix => sourceUrl.startsWith(prefix))) {
|
|
145
|
+
return fallbackRequest;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Overwrite existing auth headers that may have been set by other broad-match authorizers (i.e. AuthManager
|
|
149
|
+
// instances that were configured without any sourceUrlPrefixes specified).
|
|
150
|
+
return makeNewRequest(true);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Do not overwrite existing auth headers, as this is a broad-match authorizer (i.e. AuthManager was run
|
|
155
|
+
// without any 'sourceUrlPrefixes' specified), so give priority to AuthManagers where sourceUrlPrefixes is specified.
|
|
156
|
+
return makeNewRequest(false);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return fallbackRequest;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function getRewrittenUrl(url, urlRewriteRules) {
|
|
166
|
+
if (Array.isArray(urlRewriteRules)) {
|
|
167
|
+
for (const rule of urlRewriteRules) {
|
|
168
|
+
if (
|
|
169
|
+
rule !== null &&
|
|
170
|
+
typeof rule === "object" &&
|
|
171
|
+
typeof rule.fromUrlPrefix === "string" &&
|
|
172
|
+
typeof rule.toUrlPrefix === "string" &&
|
|
173
|
+
url.startsWith(rule.fromUrlPrefix)
|
|
174
|
+
) {
|
|
175
|
+
return `${rule.toUrlPrefix}${url.substring(rule.fromUrlPrefix.length)}`;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function createCorsRequest(originalRequest, url, headers) {
|
|
183
|
+
if (url === originalRequest.url) {
|
|
184
|
+
return new Request(originalRequest, {
|
|
185
|
+
mode: "cors", // Required for adding custom HTTP headers.
|
|
186
|
+
headers
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const method = originalRequest.method.toUpperCase();
|
|
191
|
+
const requestInit = {
|
|
192
|
+
cache: originalRequest.cache,
|
|
193
|
+
credentials: originalRequest.credentials,
|
|
194
|
+
headers,
|
|
195
|
+
integrity: originalRequest.integrity,
|
|
196
|
+
keepalive: originalRequest.keepalive,
|
|
197
|
+
method: originalRequest.method,
|
|
198
|
+
mode: "cors",
|
|
199
|
+
redirect: originalRequest.redirect,
|
|
200
|
+
referrer: originalRequest.referrer,
|
|
201
|
+
referrerPolicy: originalRequest.referrerPolicy
|
|
202
|
+
};
|
|
203
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
204
|
+
requestInit.body = originalRequest.body;
|
|
205
|
+
}
|
|
206
|
+
return new Request(url, requestInit);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function getSourceUrl(clientId) {
|
|
210
|
+
if (typeof clientId !== "string" || clientId.length === 0) {
|
|
211
|
+
return Promise.resolve(undefined);
|
|
212
|
+
}
|
|
213
|
+
const cached = sourceUrlsByClientId.get(clientId);
|
|
214
|
+
if (cached !== undefined) {
|
|
215
|
+
return cached;
|
|
216
|
+
}
|
|
217
|
+
const lookup = withTimeout(self.clients.get(clientId))
|
|
218
|
+
.then(client =>
|
|
219
|
+
client !== undefined &&
|
|
220
|
+
client !== null &&
|
|
221
|
+
client.type === "window" &&
|
|
222
|
+
typeof client.url === "string" &&
|
|
223
|
+
client.url.length > 0
|
|
224
|
+
? client.url
|
|
225
|
+
: undefined
|
|
226
|
+
)
|
|
227
|
+
.catch(() => undefined);
|
|
228
|
+
sourceUrlsByClientId.set(clientId, lookup);
|
|
229
|
+
if (sourceUrlsByClientId.size > maxSourceUrlCacheEntries) {
|
|
230
|
+
sourceUrlsByClientId.delete(sourceUrlsByClientId.keys().next().value);
|
|
231
|
+
}
|
|
232
|
+
return lookup;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function withTimeout(promise) {
|
|
236
|
+
return Promise.race([promise, new Promise(resolve => setTimeout(() => resolve(undefined), 250))]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function getState() {
|
|
240
|
+
if (transientCache !== undefined && transientUrlRewriteRules !== undefined) {
|
|
241
|
+
return { config: transientCache, urlRewriteRules: transientUrlRewriteRules };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const cache = await getCache();
|
|
245
|
+
const responses = await Promise.all([
|
|
246
|
+
cache.match(persistentCacheKey),
|
|
247
|
+
cache.match(persistentUrlRewriteRulesCacheKey)
|
|
248
|
+
]);
|
|
249
|
+
const config = responses[0] === undefined ? [] : await responses[0].json();
|
|
250
|
+
const urlRewriteRules = responses[1] === undefined ? [] : await responses[1].json();
|
|
251
|
+
|
|
252
|
+
transientCache = Array.isArray(config) ? config : [];
|
|
253
|
+
transientUrlRewriteRules = Array.isArray(urlRewriteRules) ? urlRewriteRules : [];
|
|
254
|
+
return { config: transientCache, urlRewriteRules: transientUrlRewriteRules };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function setConfig(config, urlRewriteRules) {
|
|
258
|
+
// Ensures "fetch" events can start seeing the config immediately. Persistent config is only required for when this
|
|
259
|
+
// service worker expires (after 30s on some browsers, like FireFox on Windows).
|
|
260
|
+
transientCache = config;
|
|
261
|
+
transientUrlRewriteRules = Array.isArray(urlRewriteRules) ? urlRewriteRules : [];
|
|
262
|
+
|
|
263
|
+
const cache = await getCache();
|
|
264
|
+
await Promise.all([
|
|
265
|
+
cache.put(persistentCacheKey, new Response(JSON.stringify(config))),
|
|
266
|
+
cache.put(persistentUrlRewriteRulesCacheKey, new Response(JSON.stringify(transientUrlRewriteRules)))
|
|
267
|
+
]);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getCache() {
|
|
271
|
+
return caches.open(persistentCacheName);
|
|
272
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
import type { AuthSwSetConfigDto } from "../../src/private/dtos/AuthSwSetConfigDto";
|
|
9
|
+
|
|
10
|
+
const currentWorkerSource = readFileSync(resolve(process.cwd(), "src/index.auth-sw.js"), "utf8");
|
|
11
|
+
|
|
12
|
+
interface FetchOptions {
|
|
13
|
+
clientId?: string;
|
|
14
|
+
headers?: HeadersInit;
|
|
15
|
+
method?: string;
|
|
16
|
+
navigation?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface FetchResult {
|
|
20
|
+
outboundRequest: TestRequest | undefined;
|
|
21
|
+
responded: boolean;
|
|
22
|
+
response: NodeFetchResponse | undefined;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type WorkerEventListener = (event: unknown) => void;
|
|
26
|
+
|
|
27
|
+
export class AuthServiceWorkerHarness {
|
|
28
|
+
private readonly clientsById = new Map<string, { type: "window"; url: string }>();
|
|
29
|
+
private cacheReadDelayMilliseconds = 0;
|
|
30
|
+
private readonly context: {
|
|
31
|
+
getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
|
|
32
|
+
setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
private readonly messageListener: WorkerEventListener;
|
|
36
|
+
private readonly fetchListener: WorkerEventListener;
|
|
37
|
+
private readonly fetchMock: jest.MockedFunction<(request: TestRequest) => Promise<NodeFetchResponse>>;
|
|
38
|
+
|
|
39
|
+
constructor(
|
|
40
|
+
private readonly upstreamResponse: NodeFetchResponse = new NodeFetchResponse("ok"),
|
|
41
|
+
private readonly workerSource: string = currentWorkerSource,
|
|
42
|
+
private readonly cacheEntries = new Map<string, NodeFetchResponse>()
|
|
43
|
+
) {
|
|
44
|
+
const listeners = new Map<string, WorkerEventListener>();
|
|
45
|
+
this.fetchMock = jest.fn(async (_request: TestRequest): Promise<NodeFetchResponse> => this.upstreamResponse);
|
|
46
|
+
|
|
47
|
+
const self = {
|
|
48
|
+
addEventListener: (type: string, listener: WorkerEventListener): void => {
|
|
49
|
+
listeners.set(type, listener);
|
|
50
|
+
},
|
|
51
|
+
clients: {
|
|
52
|
+
claim: async (): Promise<void> => {},
|
|
53
|
+
get: async (clientId: string): Promise<{ type: "window"; url: string } | undefined> =>
|
|
54
|
+
this.clientsById.get(clientId)
|
|
55
|
+
},
|
|
56
|
+
skipWaiting: async (): Promise<void> => {}
|
|
57
|
+
};
|
|
58
|
+
const cache = {
|
|
59
|
+
match: async (key: string): Promise<NodeFetchResponse | undefined> => {
|
|
60
|
+
if (this.cacheReadDelayMilliseconds > 0) {
|
|
61
|
+
await new Promise(resolve => setTimeout(resolve, this.cacheReadDelayMilliseconds));
|
|
62
|
+
}
|
|
63
|
+
return this.cacheEntries.get(key)?.clone();
|
|
64
|
+
},
|
|
65
|
+
put: async (key: string, value: NodeFetchResponse): Promise<void> => {
|
|
66
|
+
this.cacheEntries.set(key, value.clone());
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const sandbox = {
|
|
70
|
+
caches: { open: async (): Promise<typeof cache> => cache },
|
|
71
|
+
console: { error: jest.fn(), log: jest.fn() },
|
|
72
|
+
fetch: this.fetchMock,
|
|
73
|
+
Headers: NodeFetchHeaders,
|
|
74
|
+
Promise,
|
|
75
|
+
Request: TestRequest,
|
|
76
|
+
Response: NodeFetchResponse,
|
|
77
|
+
self,
|
|
78
|
+
setTimeout
|
|
79
|
+
};
|
|
80
|
+
runInNewContext(workerSource, sandbox);
|
|
81
|
+
|
|
82
|
+
this.context = sandbox as typeof sandbox & AuthServiceWorkerHarness["context"];
|
|
83
|
+
const fetchListener = listeners.get("fetch");
|
|
84
|
+
if (fetchListener === undefined) {
|
|
85
|
+
throw new Error("Auth service worker did not register a fetch listener.");
|
|
86
|
+
}
|
|
87
|
+
this.fetchListener = fetchListener;
|
|
88
|
+
const messageListener = listeners.get("message");
|
|
89
|
+
if (messageListener === undefined) {
|
|
90
|
+
throw new Error("Auth service worker did not register a message listener.");
|
|
91
|
+
}
|
|
92
|
+
this.messageListener = messageListener;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async setConfig(config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]): Promise<void> {
|
|
96
|
+
await this.context.setConfig(config, rules);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async dispatchMessage(message: AuthSwSetConfigDto): Promise<void> {
|
|
100
|
+
let persistence: Promise<void> | undefined;
|
|
101
|
+
this.messageListener({
|
|
102
|
+
data: message,
|
|
103
|
+
waitUntil: (promise: Promise<void>): void => {
|
|
104
|
+
persistence = promise;
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
expect(persistence).toBeDefined();
|
|
108
|
+
await persistence;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
restart(workerSource = this.workerSource): AuthServiceWorkerHarness {
|
|
112
|
+
return new AuthServiceWorkerHarness(this.upstreamResponse, workerSource, this.cacheEntries);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getRewrittenUrl(url: string, rules: UrlRewriteRule[]): string | undefined {
|
|
116
|
+
return this.context.getRewrittenUrl(url, rules);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setWindowClient(clientId: string, url: string): void {
|
|
120
|
+
this.clientsById.set(clientId, { type: "window", url });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setPersistentConfig(config: AuthSwConfigEntryDto[], cacheReadDelayMilliseconds: number): void {
|
|
124
|
+
this.cacheEntries.set("config", new NodeFetchResponse(JSON.stringify(config)));
|
|
125
|
+
this.cacheReadDelayMilliseconds = cacheReadDelayMilliseconds;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
|
|
129
|
+
this.fetchMock.mockClear();
|
|
130
|
+
const request = new TestRequest(url, {
|
|
131
|
+
headers: options.headers as NodeFetchRequestInit["headers"],
|
|
132
|
+
method: options.method
|
|
133
|
+
});
|
|
134
|
+
if (options.navigation === true) {
|
|
135
|
+
Object.defineProperty(request, "mode", { configurable: true, value: "navigate" });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let responsePromise: Promise<NodeFetchResponse> | undefined;
|
|
139
|
+
this.fetchListener({
|
|
140
|
+
clientId: options.clientId ?? "",
|
|
141
|
+
request,
|
|
142
|
+
respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
|
|
143
|
+
responsePromise = Promise.resolve(response);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const response = await responsePromise;
|
|
148
|
+
expect(this.fetchMock).toHaveBeenCalledTimes(responsePromise === undefined ? 0 : 1);
|
|
149
|
+
return {
|
|
150
|
+
outboundRequest: this.fetchMock.mock.calls.at(-1)?.[0],
|
|
151
|
+
responded: responsePromise !== undefined,
|
|
152
|
+
response
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
class TestRequest extends NodeFetchRequest {
|
|
158
|
+
readonly mode: RequestMode;
|
|
159
|
+
|
|
160
|
+
constructor(input: string | NodeFetchRequest, init: NodeFetchRequestInit & { mode?: RequestMode } = {}) {
|
|
161
|
+
super(input, init);
|
|
162
|
+
this.mode = init.mode ?? (input instanceof TestRequest ? input.mode : "cors");
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function authConfig(accountPath: string, token: string): AuthSwConfigEntryDto {
|
|
167
|
+
return authConfigForUrl(`https://upcdn.io/${accountPath}`, token);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function authConfigForUrl(urlPrefix: string, token: string): AuthSwConfigEntryDto {
|
|
171
|
+
return {
|
|
172
|
+
expires: undefined,
|
|
173
|
+
headers: [{ key: "Authorization", value: `Bearer ${token}` }],
|
|
174
|
+
urlPrefix
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function rewriteRule(fromPath: string, toPath: string): UrlRewriteRule {
|
|
179
|
+
return {
|
|
180
|
+
fromUrlPrefix: `https://dashboard.example.com/${fromPath}`,
|
|
181
|
+
toUrlPrefix: `https://upcdn.io/${toPath}`
|
|
182
|
+
};
|
|
183
|
+
}
|