@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.
Files changed (29) hide show
  1. package/dist/browser/cjs/main.js +719 -316
  2. package/dist/browser/esm/main.mjs +719 -316
  3. package/dist/node/cjs/main.js +123 -29
  4. package/dist/node/esm/main.mjs +123 -29
  5. package/dist/types/private/AuthSessionState.d.ts +7 -0
  6. package/dist/types/private/UploadManagerBase.d.ts +0 -1
  7. package/dist/types/private/dtos/AuthSwSetConfigDto.d.ts +1 -1
  8. package/dist/types/private/model/AuthManagerInterface.d.ts +1 -105
  9. package/dist/types/private/model/AuthSession.d.ts +12 -5
  10. package/dist/types/private/model/AuthSessionConfig.d.ts +3 -0
  11. package/dist/types/private/model/AuthSessionConfigAuto.d.ts +6 -0
  12. package/dist/types/private/model/AuthSessionConfigBase.d.ts +12 -0
  13. package/dist/types/private/model/AuthSessionConfigManual.d.ts +7 -0
  14. package/dist/types/private/model/BeginAuthSessionParams.d.ts +3 -0
  15. package/dist/types/private/model/BeginAuthSessionParamsOptions.d.ts +2 -0
  16. package/dist/types/private/model/BeginAuthSessionParamsV1.d.ts +11 -0
  17. package/dist/types/private/model/BeginAuthSessionParamsV2.d.ts +16 -0
  18. package/dist/types/private/model/NonEmptyArray.d.ts +1 -0
  19. package/dist/types/private/model/UrlRewriteRule.d.ts +6 -0
  20. package/dist/types/public/browser/AuthManagerBrowser.d.ts +27 -11
  21. package/dist/types/public/node/AuthManagerNode.d.ts +12 -2
  22. package/dist/types/public/shared/generated/runtime.d.ts +13 -6
  23. package/dist/worker/cjs/main.js +123 -29
  24. package/dist/worker/esm/main.mjs +123 -29
  25. package/package.json +1 -1
  26. package/tests/ApiClientAuth.test.ts +222 -0
  27. package/tests/AuthManagerBrowser.test.ts +301 -237
  28. package/tests/AuthServiceWorkerRewrite.test.ts +63 -2
  29. 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> => 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
+ }