@bytescale/sdk 3.58.0 → 3.60.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 +730 -316
- package/dist/browser/esm/main.mjs +730 -316
- package/dist/node/cjs/main.js +123 -29
- package/dist/node/esm/main.mjs +123 -29
- package/dist/types/private/AuthSessionState.d.ts +7 -0
- package/dist/types/private/UploadManagerBase.d.ts +0 -1
- package/dist/types/private/dtos/AuthSwSetConfigDto.d.ts +1 -1
- package/dist/types/private/model/AuthManagerInterface.d.ts +1 -105
- package/dist/types/private/model/AuthSession.d.ts +12 -5
- package/dist/types/private/model/AuthSessionConfig.d.ts +3 -0
- package/dist/types/private/model/AuthSessionConfigAuto.d.ts +6 -0
- package/dist/types/private/model/AuthSessionConfigBase.d.ts +14 -0
- package/dist/types/private/model/AuthSessionConfigManual.d.ts +7 -0
- package/dist/types/private/model/BeginAuthSessionParams.d.ts +3 -0
- package/dist/types/private/model/BeginAuthSessionParamsOptions.d.ts +2 -0
- package/dist/types/private/model/BeginAuthSessionParamsV1.d.ts +11 -0
- package/dist/types/private/model/BeginAuthSessionParamsV2.d.ts +16 -0
- package/dist/types/private/model/NonEmptyArray.d.ts +1 -0
- package/dist/types/private/model/UrlRewriteRule.d.ts +6 -0
- package/dist/types/public/browser/AuthManagerBrowser.d.ts +28 -11
- package/dist/types/public/node/AuthManagerNode.d.ts +12 -2
- package/dist/types/public/shared/generated/runtime.d.ts +13 -6
- package/dist/worker/cjs/main.js +123 -29
- package/dist/worker/esm/main.mjs +123 -29
- package/package.json +1 -1
- package/tests/ApiClientAuth.test.ts +222 -0
- package/tests/AuthManagerBrowser.test.ts +360 -234
- package/tests/AuthServiceWorkerRewrite.test.ts +86 -5
- package/tests/UploadManagerAuth.test.ts +156 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jest } from "@jest/globals";
|
|
2
2
|
import { Response as NodeFetchResponse } from "node-fetch";
|
|
3
3
|
import { AuthSessionState } from "../src/private/AuthSessionState";
|
|
4
|
+
import { AuthSessionConfigState } from "../src/private/model/AuthSession";
|
|
4
5
|
import type {
|
|
5
|
-
|
|
6
|
-
AuthSwConfigEntryDto,
|
|
6
|
+
AuthSessionConfig,
|
|
7
7
|
BeginAuthSessionParams,
|
|
8
|
+
BeginAuthSessionParamsV2,
|
|
8
9
|
UrlRewriteRule
|
|
9
10
|
} from "../src/index.browser";
|
|
10
11
|
|
|
@@ -18,14 +19,21 @@ interface AuthManagerApi {
|
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
interface AuthManagerInternals extends AuthManagerApi {
|
|
21
|
-
|
|
22
|
+
refreshAuthConfig: (
|
|
22
23
|
session: NonNullable<ReturnType<typeof AuthSessionState.getSession>>,
|
|
23
|
-
|
|
24
|
+
state: AuthSessionConfigState
|
|
24
25
|
) => Promise<void>;
|
|
25
26
|
scheduler: { unschedule: (handle: number) => void };
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
|
|
29
|
+
const accountA = "A123abc";
|
|
30
|
+
const accountB = "B123abc";
|
|
31
|
+
const accountC = "C123abc";
|
|
32
|
+
const jwtA = "e30.e30.jwt-a";
|
|
33
|
+
const jwtB = "e30.e30.jwt-b";
|
|
34
|
+
const jwtC = "e30.e30.jwt-c";
|
|
35
|
+
|
|
36
|
+
describe("AuthManager browser multi-configuration sessions", () => {
|
|
29
37
|
const originalNavigator = Object.getOwnPropertyDescriptor(globalThis, "navigator");
|
|
30
38
|
const originalFetch = Object.getOwnPropertyDescriptor(globalThis, "fetch");
|
|
31
39
|
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
@@ -63,6 +71,12 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
63
71
|
serviceWorkerApi.register.mockClear();
|
|
64
72
|
});
|
|
65
73
|
|
|
74
|
+
afterEach(async () => {
|
|
75
|
+
navigatorValue.serviceWorker = serviceWorkerApi;
|
|
76
|
+
await AuthManager.endAuthSession();
|
|
77
|
+
jest.restoreAllMocks();
|
|
78
|
+
});
|
|
79
|
+
|
|
66
80
|
afterAll(() => {
|
|
67
81
|
for (const [key, descriptor] of [
|
|
68
82
|
["fetch", originalFetch],
|
|
@@ -77,322 +91,434 @@ describe("AuthManager browser service-worker config", () => {
|
|
|
77
91
|
}
|
|
78
92
|
});
|
|
79
93
|
|
|
80
|
-
|
|
81
|
-
navigatorValue.serviceWorker = serviceWorkerApi;
|
|
82
|
-
await AuthManager.endAuthSession();
|
|
83
|
-
jest.restoreAllMocks();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test("retains the existing cookie fallback when no additional config is requested", async () => {
|
|
94
|
+
test("preserves the complete V1 cookie flow and legacy default token", async () => {
|
|
87
95
|
delete navigatorValue.serviceWorker;
|
|
88
|
-
const fetchApi =
|
|
96
|
+
const fetchApi = createFetchApi();
|
|
89
97
|
|
|
90
|
-
await AuthManager.beginAuthSession(
|
|
98
|
+
await AuthManager.beginAuthSession(v1Params(fetchApi));
|
|
91
99
|
|
|
92
|
-
expect((fetchApi.mock.calls[1][0] as string).endsWith("?set-cookie=true")).toBe(true);
|
|
93
|
-
expect(postMessage).not.toHaveBeenCalled();
|
|
94
100
|
expect(AuthManager.isAuthSessionReady()).toBe(true);
|
|
101
|
+
expect(AuthSessionState.getSession()?.accessToken).toBe("access-a");
|
|
102
|
+
expect(fetchUrls(fetchApi)).toEqual([
|
|
103
|
+
"https://app.example.com/auth-a",
|
|
104
|
+
`https://upcdn.io/api/v1/access_tokens/${accountA}?set-cookie=true`
|
|
105
|
+
]);
|
|
106
|
+
expect(postMessage).not.toHaveBeenCalled();
|
|
107
|
+
|
|
108
|
+
await AuthManager.endAuthSession();
|
|
109
|
+
expect(fetchUrls(fetchApi).at(-1)).toBe(`https://upcdn.io/api/v1/access_tokens/${accountA}?set-cookie=true`);
|
|
95
110
|
});
|
|
96
111
|
|
|
97
|
-
test("
|
|
98
|
-
const
|
|
112
|
+
test("preserves V1 service-worker auth and unsupported-browser cookie fallback", async () => {
|
|
113
|
+
const supportedFetch = createFetchApi();
|
|
114
|
+
await AuthManager.beginAuthSession({ ...v1Params(supportedFetch), serviceWorkerScript: "/auth-sw.js" });
|
|
99
115
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
116
|
+
expect((postMessage.mock.calls.at(-1)?.[0] as any).config).toEqual([
|
|
117
|
+
{
|
|
118
|
+
expires: expect.any(Number),
|
|
119
|
+
headers: [{ key: "Authorization", value: `Bearer ${jwtA}` }],
|
|
120
|
+
sourceUrlPrefixes: undefined,
|
|
121
|
+
urlPrefix: `https://upcdn.io/${accountA}/`
|
|
122
|
+
}
|
|
123
|
+
]);
|
|
124
|
+
expect(fetchUrls(supportedFetch)[1]).toContain("set-cookie=false");
|
|
125
|
+
await AuthManager.endAuthSession();
|
|
104
126
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
113
|
-
],
|
|
114
|
-
type: "SET_BYTESCALE_AUTH_CONFIG"
|
|
115
|
-
});
|
|
116
|
-
expect(fetchApi.mock.calls.map(([, init]) => init?.method)).toEqual(["GET", "PUT"]);
|
|
127
|
+
postMessage.mockClear();
|
|
128
|
+
delete navigatorValue.serviceWorker;
|
|
129
|
+
const fallbackFetch = createFetchApi();
|
|
130
|
+
await AuthManager.beginAuthSession({ ...v1Params(fallbackFetch), serviceWorkerScript: "/auth-sw.js" });
|
|
131
|
+
|
|
132
|
+
expect(fetchUrls(fallbackFetch)[1]).toContain("set-cookie=true");
|
|
133
|
+
expect(postMessage).not.toHaveBeenCalled();
|
|
117
134
|
});
|
|
118
135
|
|
|
119
|
-
test("
|
|
120
|
-
const fetchApi =
|
|
121
|
-
const
|
|
136
|
+
test("initializes automatic and manual V2 configs and sends one aggregate worker model", async () => {
|
|
137
|
+
const fetchApi = createFetchApi();
|
|
138
|
+
const manualB = jest.fn(async () => jwtB);
|
|
139
|
+
const manualC = jest.fn(async () => jwtC);
|
|
140
|
+
const configs: [AuthSessionConfig, ...AuthSessionConfig[]] = [
|
|
141
|
+
{
|
|
142
|
+
accountId: accountA,
|
|
143
|
+
authConfigId: undefined,
|
|
144
|
+
authHeaders: async (): Promise<Record<string, string>> => ({ "X-App-Auth": "app-token" }),
|
|
145
|
+
authUrl: "https://app.example.com/auth-a",
|
|
146
|
+
sourceUrlPrefixes: ["https://app.example.com/"]
|
|
147
|
+
},
|
|
122
148
|
{
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
urlPrefix: "https://upcdn.io/account-b/"
|
|
149
|
+
accountId: accountB,
|
|
150
|
+
authConfigId: "customer-b",
|
|
151
|
+
getAuthorizationToken: manualB
|
|
127
152
|
},
|
|
128
153
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
154
|
+
accountId: accountC,
|
|
155
|
+
authConfigId: "cookie-c",
|
|
156
|
+
enableCookieAuth: true,
|
|
157
|
+
enableServiceWorkerAuth: false,
|
|
158
|
+
getAuthorizationToken: manualC
|
|
132
159
|
}
|
|
133
160
|
];
|
|
161
|
+
const authConfigs = jest.fn(async () => configs);
|
|
134
162
|
const urlRewriteRules: UrlRewriteRule[] = [
|
|
135
163
|
{
|
|
136
|
-
fromUrlPrefix: "https://app.example.com/
|
|
137
|
-
toUrlPrefix:
|
|
164
|
+
fromUrlPrefix: "https://app.example.com/download/",
|
|
165
|
+
toUrlPrefix: `https://upcdn.io/${accountB}/`
|
|
138
166
|
}
|
|
139
167
|
];
|
|
140
|
-
const serviceWorkerConfig = jest.fn(
|
|
141
|
-
async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
142
|
-
additionalConfig,
|
|
143
|
-
sourceUrlPrefixes: ["https://app.example.com/"],
|
|
144
|
-
urlRewriteRules
|
|
145
|
-
})
|
|
146
|
-
);
|
|
147
168
|
|
|
148
169
|
await AuthManager.beginAuthSession({
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
serviceWorkerScript: "/
|
|
170
|
+
authConfigs,
|
|
171
|
+
options: { fetchApi },
|
|
172
|
+
serviceWorkerScript: "/auth-sw.js",
|
|
173
|
+
urlRewriteRules
|
|
152
174
|
});
|
|
153
175
|
|
|
154
176
|
expect(AuthManager.isAuthSessionReady()).toBe(true);
|
|
155
|
-
expect(
|
|
156
|
-
expect(
|
|
177
|
+
expect(authConfigs).toHaveBeenCalledTimes(1);
|
|
178
|
+
expect(manualB).toHaveBeenCalledTimes(1);
|
|
179
|
+
expect(manualC).toHaveBeenCalledTimes(1);
|
|
180
|
+
const session = AuthSessionState.getSession();
|
|
181
|
+
expect(session?.authConfigs?.map(state => state.accessToken)).toEqual(["access-a", "access-b", "access-c"]);
|
|
182
|
+
expect(session?.accessToken).toBeUndefined();
|
|
183
|
+
expect(fetchUrls(fetchApi).filter(url => url.includes("set-cookie=true"))).toEqual([
|
|
184
|
+
`https://upcdn.io/api/v1/access_tokens/${accountC}?set-cookie=true`
|
|
185
|
+
]);
|
|
186
|
+
expect(postMessage.mock.calls.at(-1)?.[0]).toEqual({
|
|
157
187
|
config: [
|
|
158
188
|
{
|
|
159
189
|
expires: expect.any(Number),
|
|
160
|
-
headers: [{ key: "Authorization", value:
|
|
190
|
+
headers: [{ key: "Authorization", value: `Bearer ${jwtA}` }],
|
|
161
191
|
sourceUrlPrefixes: ["https://app.example.com/"],
|
|
162
|
-
urlPrefix:
|
|
192
|
+
urlPrefix: `!bytescale-source-scoped!https://upcdn.io/${accountA}/`
|
|
163
193
|
},
|
|
164
194
|
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
195
|
+
expires: expect.any(Number),
|
|
196
|
+
headers: [{ key: "Authorization", value: `Bearer ${jwtB}` }],
|
|
197
|
+
sourceUrlPrefixes: undefined,
|
|
198
|
+
urlPrefix: `https://upcdn.io/${accountB}/`
|
|
199
|
+
}
|
|
169
200
|
],
|
|
170
201
|
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
171
202
|
urlRewriteRules
|
|
172
203
|
});
|
|
173
|
-
|
|
174
|
-
expect(AuthSessionState.getSession()?.accessToken).toBe("access-a");
|
|
175
|
-
expect(fetchApi.mock.calls.map(([, init]) => init?.method)).toEqual(["GET", "PUT"]);
|
|
204
|
+
});
|
|
176
205
|
|
|
177
|
-
|
|
206
|
+
test("uses each config's effective CDN URL for registration, worker prefixes, cleanup, and collisions", async () => {
|
|
207
|
+
const fetchApi = createFetchApi();
|
|
208
|
+
const defaultCdnUrl = "https://downloads-default.example.com";
|
|
209
|
+
const customCdnUrl = "https://downloads-custom.example.com";
|
|
210
|
+
const cookieCdnUrl = "https://downloads-cookie.example.com";
|
|
178
211
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
212
|
+
await AuthManager.beginAuthSession({
|
|
213
|
+
authConfigs: async () => [
|
|
214
|
+
{
|
|
215
|
+
...apiOnlyConfig("custom-worker", accountA, async () => jwtA),
|
|
216
|
+
cdnUrl: customCdnUrl,
|
|
217
|
+
enableServiceWorkerAuth: true
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
...apiOnlyConfig("default-worker", accountA, async () => jwtB),
|
|
221
|
+
enableServiceWorkerAuth: true
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
...apiOnlyConfig("cookie", accountA, async () => jwtC),
|
|
225
|
+
cdnUrl: cookieCdnUrl,
|
|
226
|
+
enableCookieAuth: true
|
|
227
|
+
}
|
|
228
|
+
],
|
|
229
|
+
options: { cdnUrl: defaultCdnUrl, fetchApi },
|
|
230
|
+
serviceWorkerScript: "/auth-sw.js"
|
|
231
|
+
});
|
|
182
232
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
233
|
+
expect(fetchApi.mock.calls.filter(([, init]) => init?.method === "PUT").map(([input]) => inputUrl(input))).toEqual([
|
|
234
|
+
`${customCdnUrl}/api/v1/access_tokens/${accountA}?set-cookie=false`,
|
|
235
|
+
`${defaultCdnUrl}/api/v1/access_tokens/${accountA}?set-cookie=false`,
|
|
236
|
+
`${cookieCdnUrl}/api/v1/access_tokens/${accountA}?set-cookie=true`
|
|
237
|
+
]);
|
|
238
|
+
expect((postMessage.mock.calls.at(-1)?.[0] as any).config).toEqual([
|
|
239
|
+
{
|
|
240
|
+
expires: expect.any(Number),
|
|
241
|
+
headers: [{ key: "Authorization", value: `Bearer ${jwtA}` }],
|
|
242
|
+
sourceUrlPrefixes: undefined,
|
|
243
|
+
urlPrefix: `${customCdnUrl}/${accountA}/`
|
|
244
|
+
},
|
|
186
245
|
{
|
|
187
|
-
expires:
|
|
188
|
-
headers: [{ key: "Authorization", value:
|
|
189
|
-
|
|
246
|
+
expires: expect.any(Number),
|
|
247
|
+
headers: [{ key: "Authorization", value: `Bearer ${jwtB}` }],
|
|
248
|
+
sourceUrlPrefixes: undefined,
|
|
249
|
+
urlPrefix: `${defaultCdnUrl}/${accountA}/`
|
|
190
250
|
}
|
|
191
|
-
];
|
|
192
|
-
const serviceWorkerConfig = jest
|
|
193
|
-
.fn<() => Promise<AuthManagerServiceWorkerConfig>>()
|
|
194
|
-
.mockResolvedValueOnce({
|
|
195
|
-
additionalConfig: initialAdditionalConfig,
|
|
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
|
-
]
|
|
203
|
-
})
|
|
204
|
-
.mockResolvedValueOnce({
|
|
205
|
-
additionalConfig: [],
|
|
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
|
-
]
|
|
213
|
-
});
|
|
251
|
+
]);
|
|
214
252
|
|
|
215
|
-
await AuthManager.
|
|
216
|
-
...createParams(fetchApi),
|
|
217
|
-
serviceWorkerConfig,
|
|
218
|
-
serviceWorkerScript: "/bytescale-auth-sw.js"
|
|
219
|
-
});
|
|
220
|
-
await new Promise(resolve => setTimeout(resolve, 1_500));
|
|
253
|
+
await AuthManager.endAuthSession();
|
|
221
254
|
|
|
222
|
-
expect(
|
|
223
|
-
|
|
224
|
-
|
|
255
|
+
expect(
|
|
256
|
+
fetchApi.mock.calls.filter(([, init]) => init?.method === "DELETE").map(([input]) => inputUrl(input))
|
|
257
|
+
).toEqual([`${cookieCdnUrl}/api/v1/access_tokens/${accountA}?set-cookie=true`]);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("supports a manual cookie-only V2 config without a service worker", async () => {
|
|
261
|
+
delete navigatorValue.serviceWorker;
|
|
262
|
+
const fetchApi = createFetchApi();
|
|
263
|
+
const provider = jest.fn(async () => jwtA);
|
|
264
|
+
|
|
265
|
+
await AuthManager.beginAuthSession({
|
|
266
|
+
authConfigs: async () => [
|
|
225
267
|
{
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
268
|
+
accountId: accountA,
|
|
269
|
+
authConfigId: undefined,
|
|
270
|
+
enableCookieAuth: true,
|
|
271
|
+
enableServiceWorkerAuth: false,
|
|
272
|
+
getAuthorizationToken: provider
|
|
230
273
|
}
|
|
231
274
|
],
|
|
232
|
-
|
|
233
|
-
urlRewriteRules: [
|
|
234
|
-
{
|
|
235
|
-
fromUrlPrefix: "https://app.example.com/download/",
|
|
236
|
-
toUrlPrefix: "https://upcdn.io/account-c/"
|
|
237
|
-
}
|
|
238
|
-
]
|
|
275
|
+
options: { fetchApi }
|
|
239
276
|
});
|
|
240
|
-
|
|
277
|
+
|
|
241
278
|
expect(AuthManager.isAuthSessionReady()).toBe(true);
|
|
279
|
+
expect(provider).toHaveBeenCalledTimes(1);
|
|
280
|
+
expect(fetchUrls(fetchApi)).toEqual([`https://upcdn.io/api/v1/access_tokens/${accountA}?set-cookie=true`]);
|
|
281
|
+
expect(postMessage).not.toHaveBeenCalled();
|
|
242
282
|
});
|
|
243
283
|
|
|
244
|
-
test("
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
headers: [{ key: "Authorization", value: "Bearer jwt-b" }],
|
|
250
|
-
urlPrefix: "https://upcdn.io/account-b/"
|
|
251
|
-
}
|
|
252
|
-
];
|
|
253
|
-
const serviceWorkerConfig = jest.fn(
|
|
254
|
-
async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
255
|
-
additionalConfig,
|
|
256
|
-
urlRewriteRules: [
|
|
257
|
-
{
|
|
258
|
-
fromUrlPrefix: "https://app.example.com/download/",
|
|
259
|
-
toUrlPrefix: "https://upcdn.io/account-b/"
|
|
260
|
-
}
|
|
261
|
-
]
|
|
262
|
-
})
|
|
263
|
-
);
|
|
284
|
+
test("refreshes configs independently and preserves a still-valid token after failure", async () => {
|
|
285
|
+
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
286
|
+
const fetchApi = createFetchApi();
|
|
287
|
+
const providerA = jest.fn(async () => jwtA);
|
|
288
|
+
const providerB = jest.fn<() => Promise<string>>().mockResolvedValueOnce(jwtB).mockRejectedValueOnce("offline");
|
|
264
289
|
|
|
265
290
|
await AuthManager.beginAuthSession({
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
serviceWorkerScript: "/bytescale-auth-sw.js"
|
|
291
|
+
authConfigs: async () => [apiOnlyConfig(undefined, accountA, providerA), apiOnlyConfig("b", accountB, providerB)],
|
|
292
|
+
options: { fetchApi }
|
|
269
293
|
});
|
|
270
|
-
|
|
271
294
|
const session = AuthSessionState.getSession();
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
const stateB = session?.authConfigs?.[1];
|
|
296
|
+
if (session === undefined || stateB === undefined) {
|
|
297
|
+
throw new Error("Expected initialized auth state.");
|
|
274
298
|
}
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
await
|
|
299
|
+
const previousExpiry = stateB.expiresAt;
|
|
300
|
+
const internals = AuthManager as AuthManagerInternals;
|
|
301
|
+
await internals.refreshAuthConfig(session, stateB);
|
|
302
|
+
|
|
303
|
+
expect(providerA).toHaveBeenCalledTimes(1);
|
|
304
|
+
expect(providerB).toHaveBeenCalledTimes(2);
|
|
305
|
+
expect(stateB.accessToken).toBe("access-b");
|
|
306
|
+
expect(stateB.jwt).toBe(jwtB);
|
|
307
|
+
expect(stateB.expiresAt).toBe(previousExpiry);
|
|
308
|
+
expect(AuthManager.isAuthSessionReady()).toBe(true);
|
|
309
|
+
});
|
|
278
310
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
311
|
+
test("clears the cookie-enabled config and the complete worker config on end", async () => {
|
|
312
|
+
const fetchApi = createFetchApi();
|
|
313
|
+
await AuthManager.beginAuthSession({
|
|
314
|
+
authConfigs: async () => [
|
|
283
315
|
{
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
316
|
+
accountId: accountA,
|
|
317
|
+
authConfigId: "worker",
|
|
318
|
+
getAuthorizationToken: async () => jwtA
|
|
287
319
|
},
|
|
288
|
-
additionalConfig[0]
|
|
289
|
-
],
|
|
290
|
-
type: "SET_BYTESCALE_AUTH_CONFIG",
|
|
291
|
-
urlRewriteRules: [
|
|
292
320
|
{
|
|
293
|
-
|
|
294
|
-
|
|
321
|
+
accountId: accountB,
|
|
322
|
+
authConfigId: "cookie",
|
|
323
|
+
enableCookieAuth: true,
|
|
324
|
+
enableServiceWorkerAuth: false,
|
|
325
|
+
getAuthorizationToken: async () => jwtB
|
|
295
326
|
}
|
|
296
|
-
]
|
|
327
|
+
],
|
|
328
|
+
options: { fetchApi },
|
|
329
|
+
serviceWorkerScript: "/auth-sw.js"
|
|
297
330
|
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test("rejects malformed URL rewrite rules", async () => {
|
|
301
|
-
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
302
|
-
const fetchApi = createPrimaryFetchApi();
|
|
303
331
|
|
|
304
|
-
await AuthManager.
|
|
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"
|
|
311
|
-
});
|
|
332
|
+
await AuthManager.endAuthSession();
|
|
312
333
|
|
|
313
|
-
expect(
|
|
314
|
-
expect(
|
|
334
|
+
expect(fetchApi.mock.calls.filter(([, init]) => init?.method === "DELETE")).toHaveLength(1);
|
|
335
|
+
expect(fetchUrls(fetchApi).at(-1)).toBe(`https://upcdn.io/api/v1/access_tokens/${accountB}?set-cookie=true`);
|
|
336
|
+
expect(postMessage.mock.calls.at(-1)?.[0]).toEqual({ config: [], type: "SET_BYTESCALE_AUTH_CONFIG" });
|
|
337
|
+
expect(AuthManager.isAuthSessionActive()).toBe(false);
|
|
338
|
+
await expect(AuthManager.endAuthSession()).resolves.toBeUndefined();
|
|
315
339
|
});
|
|
316
340
|
|
|
317
|
-
test(
|
|
318
|
-
|
|
319
|
-
|
|
341
|
+
test.each([
|
|
342
|
+
{
|
|
343
|
+
name: "empty auth config array",
|
|
344
|
+
params: (fetchApi: FetchApi) =>
|
|
345
|
+
({ authConfigs: async () => [], options: { fetchApi } } as unknown as BeginAuthSessionParamsV2),
|
|
346
|
+
error: "non-empty array"
|
|
347
|
+
},
|
|
348
|
+
{
|
|
349
|
+
name: "duplicate named IDs",
|
|
350
|
+
params: (fetchApi: FetchApi) =>
|
|
351
|
+
v2Params(fetchApi, [
|
|
352
|
+
apiOnlyConfig("same", accountA, async () => jwtA),
|
|
353
|
+
apiOnlyConfig("same", accountB, async () => jwtB)
|
|
354
|
+
]),
|
|
355
|
+
error: "Duplicate auth configuration ID"
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
name: "multiple defaults",
|
|
359
|
+
params: (fetchApi: FetchApi) =>
|
|
360
|
+
v2Params(fetchApi, [
|
|
361
|
+
apiOnlyConfig(undefined, accountA, async () => jwtA),
|
|
362
|
+
apiOnlyConfig(undefined, accountB, async () => jwtB)
|
|
363
|
+
]),
|
|
364
|
+
error: "Only one default"
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: "multiple cookie configs",
|
|
368
|
+
params: (fetchApi: FetchApi) =>
|
|
369
|
+
v2Params(fetchApi, [
|
|
370
|
+
{ ...apiOnlyConfig("a", accountA, async () => jwtA), enableCookieAuth: true },
|
|
371
|
+
{ ...apiOnlyConfig("b", accountB, async () => jwtB), enableCookieAuth: true }
|
|
372
|
+
]),
|
|
373
|
+
error: "Only one auth configuration may enable cookie"
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "missing service-worker script",
|
|
377
|
+
params: (fetchApi: FetchApi) =>
|
|
378
|
+
v2Params(fetchApi, [{ ...apiOnlyConfig("a", accountA, async () => jwtA), enableServiceWorkerAuth: true }]),
|
|
379
|
+
error: "serviceWorkerScript"
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: "duplicate worker destination",
|
|
383
|
+
params: (fetchApi: FetchApi): BeginAuthSessionParamsV2 => ({
|
|
384
|
+
...v2Params(fetchApi, [
|
|
385
|
+
{ ...apiOnlyConfig("a", accountA, async () => jwtA), enableServiceWorkerAuth: true },
|
|
386
|
+
{ ...apiOnlyConfig("b", accountA, async () => jwtB), enableServiceWorkerAuth: true }
|
|
387
|
+
]),
|
|
388
|
+
serviceWorkerScript: "/auth-sw.js"
|
|
389
|
+
}),
|
|
390
|
+
error: "same URL prefix"
|
|
391
|
+
},
|
|
392
|
+
{
|
|
393
|
+
name: "invalid account ID",
|
|
394
|
+
params: (fetchApi: FetchApi) => v2Params(fetchApi, [apiOnlyConfig("a", "A12/abc", async () => jwtA)]),
|
|
395
|
+
error: "Invalid Bytescale account ID"
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
name: "invalid config CDN URL",
|
|
399
|
+
params: (fetchApi: FetchApi) =>
|
|
400
|
+
v2Params(fetchApi, [
|
|
401
|
+
{ ...apiOnlyConfig("a", accountA, async () => jwtA), cdnUrl: 123 } as unknown as AuthSessionConfig
|
|
402
|
+
]),
|
|
403
|
+
error: "cdnUrl"
|
|
404
|
+
},
|
|
405
|
+
{
|
|
406
|
+
name: "overlapping cookie and worker accounts",
|
|
407
|
+
params: (fetchApi: FetchApi): BeginAuthSessionParamsV2 => ({
|
|
408
|
+
...v2Params(fetchApi, [
|
|
409
|
+
{ ...apiOnlyConfig("cookie", accountA, async () => jwtA), enableCookieAuth: true },
|
|
410
|
+
{ ...apiOnlyConfig("worker", accountA, async () => jwtB), enableServiceWorkerAuth: true }
|
|
411
|
+
]),
|
|
412
|
+
serviceWorkerScript: "/auth-sw.js"
|
|
413
|
+
}),
|
|
414
|
+
error: "Cookie and service-worker authentication"
|
|
415
|
+
}
|
|
416
|
+
])("rejects $name before invoking a provider", async ({ params, error }) => {
|
|
417
|
+
const fetchApi = createFetchApi();
|
|
320
418
|
|
|
321
|
-
await AuthManager.beginAuthSession(
|
|
322
|
-
...createParams(fetchApi),
|
|
323
|
-
serviceWorkerConfig: async () => null as unknown as AuthManagerServiceWorkerConfig,
|
|
324
|
-
serviceWorkerScript: "/bytescale-auth-sw.js"
|
|
325
|
-
});
|
|
419
|
+
await expect(AuthManager.beginAuthSession(params(fetchApi))).rejects.toThrow(error);
|
|
326
420
|
|
|
327
|
-
expect(fetchApi.
|
|
421
|
+
expect(fetchApi).not.toHaveBeenCalled();
|
|
328
422
|
expect(postMessage).not.toHaveBeenCalled();
|
|
329
|
-
expect(AuthManager.
|
|
423
|
+
expect(AuthManager.isAuthSessionActive()).toBe(false);
|
|
330
424
|
});
|
|
331
425
|
|
|
332
|
-
test("
|
|
333
|
-
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
additionalConfig: []
|
|
337
|
-
})
|
|
338
|
-
);
|
|
426
|
+
test("rejects V2 service-worker features when the browser cannot enforce them", async () => {
|
|
427
|
+
delete navigatorValue.serviceWorker;
|
|
428
|
+
const fetchApi = createFetchApi();
|
|
429
|
+
const provider = jest.fn(async () => jwtA);
|
|
339
430
|
|
|
340
|
-
await expect(
|
|
341
|
-
|
|
342
|
-
|
|
431
|
+
await expect(
|
|
432
|
+
AuthManager.beginAuthSession({
|
|
433
|
+
authConfigs: async () => [
|
|
434
|
+
{
|
|
435
|
+
accountId: accountA,
|
|
436
|
+
authConfigId: undefined,
|
|
437
|
+
getAuthorizationToken: provider
|
|
438
|
+
}
|
|
439
|
+
],
|
|
440
|
+
options: { fetchApi },
|
|
441
|
+
serviceWorkerScript: "/auth-sw.js"
|
|
442
|
+
})
|
|
443
|
+
).rejects.toThrow("does not support");
|
|
343
444
|
|
|
344
|
-
expect(
|
|
445
|
+
expect(provider).not.toHaveBeenCalled();
|
|
345
446
|
expect(fetchApi).not.toHaveBeenCalled();
|
|
346
|
-
expect(AuthManager.isAuthSessionActive()).toBe(false);
|
|
347
447
|
});
|
|
348
448
|
|
|
349
|
-
test("rejects
|
|
350
|
-
|
|
351
|
-
const fetchApi =
|
|
352
|
-
const serviceWorkerConfig = jest.fn(
|
|
353
|
-
async (): Promise<AuthManagerServiceWorkerConfig> => ({
|
|
354
|
-
additionalConfig: []
|
|
355
|
-
})
|
|
356
|
-
);
|
|
449
|
+
test.each(["", "not-a-jwt"])("rejects a malformed manual token and disposes the partial V2 session", async token => {
|
|
450
|
+
jest.spyOn(console, "warn").mockImplementation(() => {});
|
|
451
|
+
const fetchApi = createFetchApi();
|
|
357
452
|
|
|
358
453
|
await expect(
|
|
359
454
|
AuthManager.beginAuthSession({
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
serviceWorkerScript: "/bytescale-auth-sw.js"
|
|
455
|
+
authConfigs: async () => [apiOnlyConfig(undefined, accountA, async () => token)],
|
|
456
|
+
options: { fetchApi }
|
|
363
457
|
})
|
|
364
|
-
).rejects.toThrow("
|
|
458
|
+
).rejects.toThrow("malformed");
|
|
365
459
|
|
|
366
|
-
expect(serviceWorkerConfig).not.toHaveBeenCalled();
|
|
367
460
|
expect(fetchApi).not.toHaveBeenCalled();
|
|
368
461
|
expect(AuthManager.isAuthSessionActive()).toBe(false);
|
|
369
462
|
});
|
|
370
463
|
});
|
|
371
464
|
|
|
372
|
-
function
|
|
465
|
+
function apiOnlyConfig(
|
|
466
|
+
authConfigId: string | undefined,
|
|
467
|
+
accountId: string,
|
|
468
|
+
provider: () => Promise<string>
|
|
469
|
+
): AuthSessionConfig {
|
|
373
470
|
return {
|
|
374
|
-
accountId
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
471
|
+
accountId,
|
|
472
|
+
authConfigId,
|
|
473
|
+
enableServiceWorkerAuth: false,
|
|
474
|
+
getAuthorizationToken: provider
|
|
378
475
|
};
|
|
379
476
|
}
|
|
380
477
|
|
|
381
|
-
function
|
|
382
|
-
return jest.fn<FetchApi>(async (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
JSON.stringify({ accessToken: "access-a", ttlSeconds: 3600 })
|
|
391
|
-
) as unknown as Response;
|
|
392
|
-
case "DELETE":
|
|
393
|
-
return new NodeFetchResponse(null, { status: 204 }) as unknown as Response;
|
|
394
|
-
default:
|
|
395
|
-
throw new Error(`Unexpected method: ${init?.method ?? "undefined"}`);
|
|
478
|
+
function createFetchApi(): jest.MockedFunction<FetchApi> {
|
|
479
|
+
return jest.fn<FetchApi>(async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
480
|
+
const url = inputUrl(input);
|
|
481
|
+
if (init?.method === "GET") {
|
|
482
|
+
const suffix = url.split("auth-")[1] ?? "a";
|
|
483
|
+
const jwt = suffix === "a" ? jwtA : suffix === "b" ? jwtB : jwtC;
|
|
484
|
+
return new NodeFetchResponse(jwt, {
|
|
485
|
+
headers: { "Content-Type": "text/plain" }
|
|
486
|
+
}) as unknown as Response;
|
|
396
487
|
}
|
|
488
|
+
if (init?.method === "PUT") {
|
|
489
|
+
const accountId = url.split("/access_tokens/")[1]?.split("?")[0];
|
|
490
|
+
const suffix = accountId === accountA ? "a" : accountId === accountB ? "b" : "c";
|
|
491
|
+
return new NodeFetchResponse(
|
|
492
|
+
JSON.stringify({ accessToken: `access-${suffix}`, ttlSeconds: 3600 })
|
|
493
|
+
) as unknown as Response;
|
|
494
|
+
}
|
|
495
|
+
if (init?.method === "DELETE") {
|
|
496
|
+
return new NodeFetchResponse(null, { status: 204 }) as unknown as Response;
|
|
497
|
+
}
|
|
498
|
+
throw new Error(`Unexpected request: ${init?.method ?? "undefined"} ${url}`);
|
|
397
499
|
});
|
|
398
500
|
}
|
|
501
|
+
|
|
502
|
+
function fetchUrls(fetchApi: jest.MockedFunction<FetchApi>): string[] {
|
|
503
|
+
return fetchApi.mock.calls.map(([input]) => inputUrl(input));
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function v1Params(fetchApi: FetchApi): BeginAuthSessionParams {
|
|
507
|
+
return {
|
|
508
|
+
accountId: accountA,
|
|
509
|
+
authHeaders: async (): Promise<Record<string, string>> => ({ "X-App-Authorization": "app-token" }),
|
|
510
|
+
authUrl: "https://app.example.com/auth-a",
|
|
511
|
+
options: { fetchApi }
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function inputUrl(input: RequestInfo | URL): string {
|
|
516
|
+
return typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function v2Params(fetchApi: FetchApi, configs: AuthSessionConfig[]): BeginAuthSessionParamsV2 {
|
|
520
|
+
return {
|
|
521
|
+
authConfigs: async () => configs as [AuthSessionConfig, ...AuthSessionConfig[]],
|
|
522
|
+
options: { fetchApi }
|
|
523
|
+
};
|
|
524
|
+
}
|