@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
|
@@ -149,9 +149,73 @@ describe("Auth service-worker URL rewriting", () => {
|
|
|
149
149
|
expect(authenticated.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
150
150
|
expect(untouched.responded).toBe(false);
|
|
151
151
|
});
|
|
152
|
+
|
|
153
|
+
test("waits for delayed persistent authentication state on a cold start", async () => {
|
|
154
|
+
const harness = new AuthServiceWorkerHarness();
|
|
155
|
+
harness.setPersistentConfig([authConfig("account-a/", "token-a")], 300);
|
|
156
|
+
|
|
157
|
+
const result = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf");
|
|
158
|
+
|
|
159
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("keeps source-page authorization independent across multiple client IDs", async () => {
|
|
163
|
+
const harness = new AuthServiceWorkerHarness();
|
|
164
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
165
|
+
harness.setWindowClient("client-b", "https://app.example.com/b/");
|
|
166
|
+
await harness.setConfig([
|
|
167
|
+
{
|
|
168
|
+
...authConfig("account-a/", "token-a"),
|
|
169
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
170
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
...authConfig("account-b/", "token-b"),
|
|
174
|
+
sourceUrlPrefixes: ["https://app.example.com/b/"],
|
|
175
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-b/"
|
|
176
|
+
}
|
|
177
|
+
]);
|
|
178
|
+
|
|
179
|
+
const a = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", { clientId: "client-a" });
|
|
180
|
+
const wrongSource = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
181
|
+
clientId: "client-b"
|
|
182
|
+
});
|
|
183
|
+
const b = await harness.dispatchFetch("https://upcdn.io/account-b/file.pdf", { clientId: "client-b" });
|
|
184
|
+
|
|
185
|
+
expect(a.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
186
|
+
expect(wrongSource.responded).toBe(true);
|
|
187
|
+
expect(wrongSource.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
188
|
+
expect(b.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("source-scoped auth replaces both authentication headers and preserves unrelated headers", async () => {
|
|
192
|
+
const harness = new AuthServiceWorkerHarness();
|
|
193
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
194
|
+
await harness.setConfig([
|
|
195
|
+
{
|
|
196
|
+
...authConfig("account-a/", "token-a"),
|
|
197
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
198
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
199
|
+
}
|
|
200
|
+
]);
|
|
201
|
+
|
|
202
|
+
const result = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
203
|
+
clientId: "client-a",
|
|
204
|
+
headers: {
|
|
205
|
+
"Authorization": "Bearer stale",
|
|
206
|
+
"Authorization-Token": "stale-token",
|
|
207
|
+
"X-Trace-Id": "trace"
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
212
|
+
expect(result.outboundRequest?.headers.has("Authorization-Token")).toBe(false);
|
|
213
|
+
expect(result.outboundRequest?.headers.get("X-Trace-Id")).toBe("trace");
|
|
214
|
+
});
|
|
152
215
|
});
|
|
153
216
|
|
|
154
217
|
interface FetchOptions {
|
|
218
|
+
clientId?: string;
|
|
155
219
|
headers?: HeadersInit;
|
|
156
220
|
navigation?: boolean;
|
|
157
221
|
}
|
|
@@ -165,6 +229,9 @@ interface FetchResult {
|
|
|
165
229
|
type WorkerEventListener = (event: unknown) => void;
|
|
166
230
|
|
|
167
231
|
class AuthServiceWorkerHarness {
|
|
232
|
+
private readonly clientsById = new Map<string, { type: "window"; url: string }>();
|
|
233
|
+
private readonly cacheEntries = new Map<string, NodeFetchResponse>();
|
|
234
|
+
private cacheReadDelayMilliseconds = 0;
|
|
168
235
|
private readonly context: {
|
|
169
236
|
getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
|
|
170
237
|
setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
|
|
@@ -175,7 +242,6 @@ class AuthServiceWorkerHarness {
|
|
|
175
242
|
|
|
176
243
|
constructor(private readonly upstreamResponse: NodeFetchResponse = new NodeFetchResponse("ok")) {
|
|
177
244
|
const listeners = new Map<string, WorkerEventListener>();
|
|
178
|
-
const cacheEntries = new Map<string, NodeFetchResponse>();
|
|
179
245
|
this.fetchMock = jest.fn(async (_request: TestRequest): Promise<NodeFetchResponse> => this.upstreamResponse);
|
|
180
246
|
|
|
181
247
|
const self = {
|
|
@@ -184,14 +250,20 @@ class AuthServiceWorkerHarness {
|
|
|
184
250
|
},
|
|
185
251
|
clients: {
|
|
186
252
|
claim: async (): Promise<void> => {},
|
|
187
|
-
get: async (): Promise<undefined> =>
|
|
253
|
+
get: async (clientId: string): Promise<{ type: "window"; url: string } | undefined> =>
|
|
254
|
+
this.clientsById.get(clientId)
|
|
188
255
|
},
|
|
189
256
|
skipWaiting: async (): Promise<void> => {}
|
|
190
257
|
};
|
|
191
258
|
const cache = {
|
|
192
|
-
match: async (key: string): Promise<NodeFetchResponse | undefined> =>
|
|
259
|
+
match: async (key: string): Promise<NodeFetchResponse | undefined> => {
|
|
260
|
+
if (this.cacheReadDelayMilliseconds > 0) {
|
|
261
|
+
await new Promise(resolve => setTimeout(resolve, this.cacheReadDelayMilliseconds));
|
|
262
|
+
}
|
|
263
|
+
return this.cacheEntries.get(key)?.clone();
|
|
264
|
+
},
|
|
193
265
|
put: async (key: string, value: NodeFetchResponse): Promise<void> => {
|
|
194
|
-
cacheEntries.set(key, value.clone());
|
|
266
|
+
this.cacheEntries.set(key, value.clone());
|
|
195
267
|
}
|
|
196
268
|
};
|
|
197
269
|
const sandbox = {
|
|
@@ -223,6 +295,15 @@ class AuthServiceWorkerHarness {
|
|
|
223
295
|
return this.context.getRewrittenUrl(url, rules);
|
|
224
296
|
}
|
|
225
297
|
|
|
298
|
+
setWindowClient(clientId: string, url: string): void {
|
|
299
|
+
this.clientsById.set(clientId, { type: "window", url });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
setPersistentConfig(config: AuthSwConfigEntryDto[], cacheReadDelayMilliseconds: number): void {
|
|
303
|
+
this.cacheEntries.set("config", new NodeFetchResponse(JSON.stringify(config)));
|
|
304
|
+
this.cacheReadDelayMilliseconds = cacheReadDelayMilliseconds;
|
|
305
|
+
}
|
|
306
|
+
|
|
226
307
|
async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
|
|
227
308
|
const request = new TestRequest(url, { headers: options.headers as NodeFetchRequestInit["headers"] });
|
|
228
309
|
if (options.navigation === true) {
|
|
@@ -231,7 +312,7 @@ class AuthServiceWorkerHarness {
|
|
|
231
312
|
|
|
232
313
|
let responsePromise: Promise<NodeFetchResponse> | undefined;
|
|
233
314
|
this.fetchListener({
|
|
234
|
-
clientId: "",
|
|
315
|
+
clientId: options.clientId ?? "",
|
|
235
316
|
request,
|
|
236
317
|
respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
|
|
237
318
|
responsePromise = Promise.resolve(response);
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jest } from "@jest/globals";
|
|
2
|
+
import { AuthSessionState } from "../src/private/AuthSessionState";
|
|
3
|
+
import { UploadManagerBase } from "../src/private/UploadManagerBase";
|
|
4
|
+
import { AddCancellationHandler } from "../src/private/model/AddCancellationHandler";
|
|
5
|
+
import { AuthSessionConfigState } from "../src/private/model/AuthSession";
|
|
6
|
+
import { OnPartProgress } from "../src/private/model/OnPartProgress";
|
|
7
|
+
import { PreUploadInfo } from "../src/private/model/PreUploadInfo";
|
|
8
|
+
import { PutUploadPartResult } from "../src/private/model/PutUploadPartResult";
|
|
9
|
+
import { UploadManagerParams, UploadSource } from "../src/public/shared/CommonTypes";
|
|
10
|
+
import {
|
|
11
|
+
BeginMultipartUploadResponse,
|
|
12
|
+
CompleteMultipartUploadResponse,
|
|
13
|
+
UploadPart
|
|
14
|
+
} from "../src/public/shared/generated";
|
|
15
|
+
|
|
16
|
+
const accountA = "A123abc";
|
|
17
|
+
|
|
18
|
+
class TestUploadManager extends UploadManagerBase<string, undefined> {
|
|
19
|
+
protected processUploadSource(data: UploadSource): string {
|
|
20
|
+
return String(data);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
protected getPreUploadInfoPartial(
|
|
24
|
+
_request: UploadManagerParams,
|
|
25
|
+
_source: string
|
|
26
|
+
): Partial<PreUploadInfo> & { size: number } {
|
|
27
|
+
return { size: 1 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
protected preUpload(_source: string): undefined {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected async postUpload(_init: undefined): Promise<void> {}
|
|
35
|
+
|
|
36
|
+
protected async doPutUploadPart(
|
|
37
|
+
_part: UploadPart,
|
|
38
|
+
_contentLength: number,
|
|
39
|
+
_source: string,
|
|
40
|
+
_onProgress: OnPartProgress,
|
|
41
|
+
_addCancellationHandler: AddCancellationHandler
|
|
42
|
+
): Promise<PutUploadPartResult> {
|
|
43
|
+
return { etag: "etag", status: 200 };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("UploadManager AuthManager configuration", () => {
|
|
48
|
+
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
|
|
49
|
+
|
|
50
|
+
beforeAll(() => {
|
|
51
|
+
Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => AuthSessionState.setSession(undefined));
|
|
55
|
+
|
|
56
|
+
afterAll(() => {
|
|
57
|
+
if (originalWindow === undefined) {
|
|
58
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
59
|
+
} else {
|
|
60
|
+
Object.defineProperty(globalThis, "window", originalWindow);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("allows keyless construction and resolves the selected account once upload begins", async () => {
|
|
65
|
+
const manager = new TestUploadManager({ authConfigId: "customer" });
|
|
66
|
+
await expect(manager.upload({ data: "x" })).rejects.toThrow("No active AuthManager configuration");
|
|
67
|
+
|
|
68
|
+
setSession(state("customer", accountA));
|
|
69
|
+
const uploadApi = installUploadApiMock(manager);
|
|
70
|
+
await manager.upload({ data: "x" });
|
|
71
|
+
|
|
72
|
+
expect(uploadApi.beginMultipartUpload).toHaveBeenCalledWith(expect.objectContaining({ accountId: accountA }));
|
|
73
|
+
expect(uploadApi.completeUploadPart).toHaveBeenCalledWith(expect.objectContaining({ accountId: accountA }));
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("fails before upload network work when keyless AuthManager auth is explicitly disabled", async () => {
|
|
77
|
+
const manager = new TestUploadManager({ authConfigId: false });
|
|
78
|
+
const uploadApi = installUploadApiMock(manager);
|
|
79
|
+
|
|
80
|
+
await expect(manager.upload({ data: "x" })).rejects.toThrow("provide an API key");
|
|
81
|
+
expect(uploadApi.beginMultipartUpload).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
interface UploadApiMock {
|
|
86
|
+
beginMultipartUpload: jest.MockedFunction<(request: { accountId: string }) => Promise<BeginMultipartUploadResponse>>;
|
|
87
|
+
completeUploadPart: jest.MockedFunction<(request: { accountId: string }) => Promise<CompleteMultipartUploadResponse>>;
|
|
88
|
+
getUploadPart: jest.MockedFunction<(request: { accountId: string }) => Promise<UploadPart>>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function installUploadApiMock(manager: TestUploadManager): UploadApiMock {
|
|
92
|
+
const firstPart: UploadPart = {
|
|
93
|
+
range: { inclusiveEnd: 0, inclusiveStart: 0 },
|
|
94
|
+
uploadId: "upload-id",
|
|
95
|
+
uploadPartIndex: 0,
|
|
96
|
+
uploadUrl: "https://upload.example.com/part"
|
|
97
|
+
};
|
|
98
|
+
const beginResponse: BeginMultipartUploadResponse = {
|
|
99
|
+
file: {
|
|
100
|
+
accountId: accountA,
|
|
101
|
+
etag: null,
|
|
102
|
+
filePath: "/file.txt",
|
|
103
|
+
fileUrl: `https://upcdn.io/${accountA}/raw/file.txt`,
|
|
104
|
+
lastModified: { tYPE: "EpochMillis" },
|
|
105
|
+
metadata: {},
|
|
106
|
+
mime: "text/plain",
|
|
107
|
+
originalFileName: null,
|
|
108
|
+
size: 1,
|
|
109
|
+
tags: []
|
|
110
|
+
},
|
|
111
|
+
uploadId: "upload-id",
|
|
112
|
+
uploadParts: { count: 1, first: firstPart }
|
|
113
|
+
};
|
|
114
|
+
const uploadApi: UploadApiMock = {
|
|
115
|
+
beginMultipartUpload: jest.fn<(request: { accountId: string }) => Promise<BeginMultipartUploadResponse>>(
|
|
116
|
+
async _request => beginResponse
|
|
117
|
+
),
|
|
118
|
+
completeUploadPart: jest.fn<(request: { accountId: string }) => Promise<CompleteMultipartUploadResponse>>(
|
|
119
|
+
async (_request): Promise<CompleteMultipartUploadResponse> => ({ etag: "etag", status: "Completed" })
|
|
120
|
+
),
|
|
121
|
+
getUploadPart: jest.fn<(request: { accountId: string }) => Promise<UploadPart>>(async _request => firstPart)
|
|
122
|
+
};
|
|
123
|
+
(manager as unknown as { uploadApi: UploadApiMock }).uploadApi = uploadApi;
|
|
124
|
+
return uploadApi;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function state(authConfigId: string, accountId: string): AuthSessionConfigState {
|
|
128
|
+
return {
|
|
129
|
+
accessToken: "access-token",
|
|
130
|
+
config: {
|
|
131
|
+
accountId,
|
|
132
|
+
authConfigId,
|
|
133
|
+
enableServiceWorkerAuth: false,
|
|
134
|
+
getAuthorizationToken: async () => "jwt"
|
|
135
|
+
},
|
|
136
|
+
expiresAt: Date.now() + 60_000,
|
|
137
|
+
jwt: "jwt",
|
|
138
|
+
refreshHandle: undefined
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function setSession(configState: AuthSessionConfigState): void {
|
|
143
|
+
AuthSessionState.setSession({
|
|
144
|
+
accessToken: undefined,
|
|
145
|
+
accessTokenRefreshHandle: undefined,
|
|
146
|
+
authConfigs: [configState],
|
|
147
|
+
authServiceWorker: undefined,
|
|
148
|
+
isActive: true,
|
|
149
|
+
isReady: true,
|
|
150
|
+
params: {
|
|
151
|
+
authConfigs: async () => [configState.config],
|
|
152
|
+
serviceWorkerScript: undefined
|
|
153
|
+
},
|
|
154
|
+
serviceWorkerConfigured: false
|
|
155
|
+
});
|
|
156
|
+
}
|