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