@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.
Files changed (29) hide show
  1. package/dist/browser/cjs/main.js +730 -316
  2. package/dist/browser/esm/main.mjs +730 -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 +14 -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 +28 -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 +360 -234
  28. package/tests/AuthServiceWorkerRewrite.test.ts +86 -5
  29. 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> => 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> => cacheEntries.get(key)?.clone(),
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
+ }