@bytescale/sdk 3.58.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 +719 -316
- package/dist/browser/esm/main.mjs +719 -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 +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 +301 -237
- package/tests/AuthServiceWorkerRewrite.test.ts +63 -2
- package/tests/UploadManagerAuth.test.ts +156 -0
|
@@ -149,9 +149,64 @@ 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("keeps source-page authorization independent across multiple client IDs", async () => {
|
|
154
|
+
const harness = new AuthServiceWorkerHarness();
|
|
155
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
156
|
+
harness.setWindowClient("client-b", "https://app.example.com/b/");
|
|
157
|
+
await harness.setConfig([
|
|
158
|
+
{
|
|
159
|
+
...authConfig("account-a/", "token-a"),
|
|
160
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
161
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
...authConfig("account-b/", "token-b"),
|
|
165
|
+
sourceUrlPrefixes: ["https://app.example.com/b/"],
|
|
166
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-b/"
|
|
167
|
+
}
|
|
168
|
+
]);
|
|
169
|
+
|
|
170
|
+
const a = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", { clientId: "client-a" });
|
|
171
|
+
const wrongSource = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
172
|
+
clientId: "client-b"
|
|
173
|
+
});
|
|
174
|
+
const b = await harness.dispatchFetch("https://upcdn.io/account-b/file.pdf", { clientId: "client-b" });
|
|
175
|
+
|
|
176
|
+
expect(a.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
177
|
+
expect(wrongSource.responded).toBe(true);
|
|
178
|
+
expect(wrongSource.outboundRequest?.headers.has("Authorization")).toBe(false);
|
|
179
|
+
expect(b.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-b");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("source-scoped auth replaces both authentication headers and preserves unrelated headers", async () => {
|
|
183
|
+
const harness = new AuthServiceWorkerHarness();
|
|
184
|
+
harness.setWindowClient("client-a", "https://app.example.com/a/");
|
|
185
|
+
await harness.setConfig([
|
|
186
|
+
{
|
|
187
|
+
...authConfig("account-a/", "token-a"),
|
|
188
|
+
sourceUrlPrefixes: ["https://app.example.com/a/"],
|
|
189
|
+
urlPrefix: "!bytescale-source-scoped!https://upcdn.io/account-a/"
|
|
190
|
+
}
|
|
191
|
+
]);
|
|
192
|
+
|
|
193
|
+
const result = await harness.dispatchFetch("https://upcdn.io/account-a/file.pdf", {
|
|
194
|
+
clientId: "client-a",
|
|
195
|
+
headers: {
|
|
196
|
+
"Authorization": "Bearer stale",
|
|
197
|
+
"Authorization-Token": "stale-token",
|
|
198
|
+
"X-Trace-Id": "trace"
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(result.outboundRequest?.headers.get("Authorization")).toBe("Bearer token-a");
|
|
203
|
+
expect(result.outboundRequest?.headers.has("Authorization-Token")).toBe(false);
|
|
204
|
+
expect(result.outboundRequest?.headers.get("X-Trace-Id")).toBe("trace");
|
|
205
|
+
});
|
|
152
206
|
});
|
|
153
207
|
|
|
154
208
|
interface FetchOptions {
|
|
209
|
+
clientId?: string;
|
|
155
210
|
headers?: HeadersInit;
|
|
156
211
|
navigation?: boolean;
|
|
157
212
|
}
|
|
@@ -165,6 +220,7 @@ interface FetchResult {
|
|
|
165
220
|
type WorkerEventListener = (event: unknown) => void;
|
|
166
221
|
|
|
167
222
|
class AuthServiceWorkerHarness {
|
|
223
|
+
private readonly clientsById = new Map<string, { type: "window"; url: string }>();
|
|
168
224
|
private readonly context: {
|
|
169
225
|
getRewrittenUrl: (url: string, rules: UrlRewriteRule[]) => string | undefined;
|
|
170
226
|
setConfig: (config: AuthSwConfigEntryDto[], rules?: UrlRewriteRule[]) => Promise<void>;
|
|
@@ -184,7 +240,8 @@ class AuthServiceWorkerHarness {
|
|
|
184
240
|
},
|
|
185
241
|
clients: {
|
|
186
242
|
claim: async (): Promise<void> => {},
|
|
187
|
-
get: async (): Promise<undefined> =>
|
|
243
|
+
get: async (clientId: string): Promise<{ type: "window"; url: string } | undefined> =>
|
|
244
|
+
this.clientsById.get(clientId)
|
|
188
245
|
},
|
|
189
246
|
skipWaiting: async (): Promise<void> => {}
|
|
190
247
|
};
|
|
@@ -223,6 +280,10 @@ class AuthServiceWorkerHarness {
|
|
|
223
280
|
return this.context.getRewrittenUrl(url, rules);
|
|
224
281
|
}
|
|
225
282
|
|
|
283
|
+
setWindowClient(clientId: string, url: string): void {
|
|
284
|
+
this.clientsById.set(clientId, { type: "window", url });
|
|
285
|
+
}
|
|
286
|
+
|
|
226
287
|
async dispatchFetch(url: string, options: FetchOptions = {}): Promise<FetchResult> {
|
|
227
288
|
const request = new TestRequest(url, { headers: options.headers as NodeFetchRequestInit["headers"] });
|
|
228
289
|
if (options.navigation === true) {
|
|
@@ -231,7 +292,7 @@ class AuthServiceWorkerHarness {
|
|
|
231
292
|
|
|
232
293
|
let responsePromise: Promise<NodeFetchResponse> | undefined;
|
|
233
294
|
this.fetchListener({
|
|
234
|
-
clientId: "",
|
|
295
|
+
clientId: options.clientId ?? "",
|
|
235
296
|
request,
|
|
236
297
|
respondWith: (response: NodeFetchResponse | Promise<NodeFetchResponse>): void => {
|
|
237
298
|
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
|
+
}
|