@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
@@ -210,6 +210,55 @@ var AuthSessionState = /*#__PURE__*/function () {
210
210
  }
211
211
  return window[AuthSessionState.stateKey];
212
212
  }
213
+ /** Resolves request-time auth while remaining compatible with the single-token session used by SDK 3.54.0. */
214
+ }, {
215
+ key: "resolveAuthConfig",
216
+ value: function resolveAuthConfig(authConfigId, requireReadyDefault) {
217
+ if (authConfigId === false) {
218
+ return undefined;
219
+ }
220
+ var session = AuthSessionState.getSession();
221
+ if (session === undefined || !session.isActive) {
222
+ if (typeof authConfigId === "string") {
223
+ throw new Error("No active AuthManager configuration has ID '".concat(authConfigId, "'."));
224
+ }
225
+ return undefined;
226
+ }
227
+ if (Array.isArray(session.authConfigs)) {
228
+ var state = session.authConfigs.find(function (config) {
229
+ return config.config.authConfigId === authConfigId;
230
+ });
231
+ if (state === undefined) {
232
+ if (typeof authConfigId === "string") {
233
+ throw new Error("No active AuthManager configuration has ID '".concat(authConfigId, "'."));
234
+ }
235
+ return undefined;
236
+ }
237
+ if (state.accessToken === undefined || state.expiresAt === undefined || state.expiresAt <= Date.now() || state.jwt === undefined) {
238
+ var isV2Session = typeof session.params.authConfigs === "function";
239
+ if (typeof authConfigId === "string" || requireReadyDefault || isV2Session) {
240
+ throw new Error("AuthManager configuration '".concat(authConfigId !== null && authConfigId !== void 0 ? authConfigId : "default", "' is not ready."));
241
+ }
242
+ return undefined;
243
+ }
244
+ return {
245
+ accessToken: state.accessToken,
246
+ accountId: state.config.accountId,
247
+ jwt: state.jwt
248
+ };
249
+ }
250
+ if (typeof authConfigId === "string") {
251
+ throw new Error("No active AuthManager configuration has ID '".concat(authConfigId, "'."));
252
+ }
253
+ if (session.accessToken === undefined || typeof session.params.accountId !== "string") {
254
+ return undefined;
255
+ }
256
+ return {
257
+ accessToken: session.accessToken,
258
+ accountId: session.params.accountId,
259
+ jwt: undefined
260
+ };
261
+ }
213
262
  }]);
214
263
  }();
215
264
  AuthSessionState.stateKey = "BytescaleSessionState";
@@ -353,12 +402,47 @@ var BytescaleApiClientConfigUtils = /*#__PURE__*/function () {
353
402
  }, {
354
403
  key: "getAccountId",
355
404
  value: function getAccountId(config) {
405
+ return BytescaleApiClientConfigUtils.resolveAuthentication(config).accountId;
406
+ }
407
+ }, {
408
+ key: "resolveAuthentication",
409
+ value: function resolveAuthentication(config) {
410
+ var _a, _b;
411
+ var apiKey = (_a = config.apiKey) !== null && _a !== void 0 ? _a : undefined;
412
+ var authConfig = AuthSessionState.resolveAuthConfig(config.authConfigId, apiKey === undefined);
413
+ var apiKeyAccountId = apiKey === undefined ? undefined : BytescaleApiClientConfigUtils.getApiKeyAccountId(apiKey);
414
+ if (apiKeyAccountId !== undefined && authConfig !== undefined && apiKeyAccountId !== authConfig.accountId) {
415
+ throw new Error("The API key belongs to account '".concat(apiKeyAccountId, "', but AuthManager configuration '").concat((_b = config.authConfigId) !== null && _b !== void 0 ? _b : "default", "' belongs to account '").concat(authConfig.accountId, "'."));
416
+ }
417
+ if (apiKey !== undefined) {
418
+ return {
419
+ accountId: apiKeyAccountId,
420
+ headers: Object.assign({
421
+ Authorization: "Bearer ".concat(apiKey)
422
+ }, authConfig === undefined ? {} : {
423
+ "Authorization-Token": authConfig.accessToken
424
+ })
425
+ };
426
+ }
427
+ if ((authConfig === null || authConfig === void 0 ? void 0 : authConfig.jwt) !== undefined) {
428
+ return {
429
+ accountId: authConfig.accountId,
430
+ headers: {
431
+ Authorization: "Bearer ".concat(authConfig.jwt)
432
+ }
433
+ };
434
+ }
435
+ throw new Error("Please provide an API key via the 'apiKey' config parameter.");
436
+ }
437
+ }, {
438
+ key: "getApiKeyAccountId",
439
+ value: function getApiKeyAccountId(apiKey) {
356
440
  var _a, _b;
357
441
  var accountId;
358
- if (BytescaleApiClientConfigUtils.specialApiKeys.includes(config.apiKey)) {
442
+ if (BytescaleApiClientConfigUtils.specialApiKeys.includes(apiKey)) {
359
443
  accountId = BytescaleApiClientConfigUtils.specialApiKeyAccountId;
360
444
  } else {
361
- accountId = (_b = (_a = config.apiKey.split("_")[1]) === null || _a === void 0 ? void 0 : _a.substr(0, BytescaleApiClientConfigUtils.accountIdLength)) !== null && _b !== void 0 ? _b : "";
445
+ accountId = (_b = (_a = apiKey.split("_")[1]) === null || _a === void 0 ? void 0 : _a.substr(0, BytescaleApiClientConfigUtils.accountIdLength)) !== null && _b !== void 0 ? _b : "";
362
446
  if (accountId.length !== BytescaleApiClientConfigUtils.accountIdLength) {
363
447
  throw new Error("Invalid Bytescale API key.");
364
448
  }
@@ -368,21 +452,24 @@ var BytescaleApiClientConfigUtils = /*#__PURE__*/function () {
368
452
  }, {
369
453
  key: "validate",
370
454
  value: function validate(config) {
371
- var _a;
372
455
  // Defensive programming, for users not using TypeScript. Mainly because this is used by UploadWidget users.
373
456
  if ((config !== null && config !== void 0 ? config : undefined) === undefined) {
374
457
  throw new Error("Config parameter required.");
375
458
  }
376
- if (((_a = config.apiKey) !== null && _a !== void 0 ? _a : undefined) === undefined) {
377
- throw new Error("Please provide an API key via the 'apiKey' config parameter.");
459
+ if (config.authConfigId !== undefined && config.authConfigId !== false && typeof config.authConfigId !== "string") {
460
+ throw new Error("The 'authConfigId' config parameter must be a string, false, or undefined.");
378
461
  }
379
- if (config.apiKey.trim() !== config.apiKey) {
462
+ if (config.apiKey !== undefined && typeof config.apiKey !== "string") {
463
+ throw new Error("The 'apiKey' config parameter must be a string when provided.");
464
+ }
465
+ if (config.apiKey !== undefined && config.apiKey.trim() !== config.apiKey) {
380
466
  // We do not support API keys with whitespace (by trimming ourselves) because otherwise we'd need to support this
381
467
  // everywhere in perpetuity (since removing the trimming would be a breaking change).
382
468
  throw new Error("API key needs trimming (whitespace detected).");
383
469
  }
384
- // This performs futher validation on the API key...
385
- BytescaleApiClientConfigUtils.getAccountId(config);
470
+ if (config.apiKey !== undefined) {
471
+ BytescaleApiClientConfigUtils.getApiKeyAccountId(config.apiKey);
472
+ }
386
473
  }
387
474
  }]);
388
475
  }();
@@ -409,12 +496,7 @@ var BaseAPI = /*#__PURE__*/function () {
409
496
  try {
410
497
  var _this = this;
411
498
  var _a;
412
- var apiKey = _this.config.apiKey;
413
- context.headers["Authorization"] = "Bearer ".concat(apiKey); // authorization-header authentication
414
- var session = AuthSessionState.getSession();
415
- if ((session === null || session === void 0 ? void 0 : session.accessToken) !== undefined) {
416
- context.headers["Authorization-Token"] = session.accessToken;
417
- }
499
+ Object.assign(context.headers, BytescaleApiClientConfigUtils.resolveAuthentication(_this.config).headers);
418
500
  // Key: any possible value for 'baseUrlOverride'
419
501
  // Value: user-overridden value for that base URL from the config.
420
502
  var nonDefaultBasePaths = _defineProperty({}, BytescaleApiClientConfigUtils.defaultCdnUrl, BytescaleApiClientConfigUtils.getCdnUrl(_this.config));
@@ -454,9 +536,21 @@ var BaseAPI = /*#__PURE__*/function () {
454
536
  url += "?" + querystring(context.query);
455
537
  }
456
538
  var configHeaders = _this2.config.headers;
457
- var _Object$assign = Object.assign({}, context.headers);
458
- return runtime_await(runtime_await(configHeaders === undefined ? {} : typeof configHeaders === "function" ? configHeaders() : configHeaders, function (_configHeaders) {
459
- var headers = Object.assign(_Object$assign, configHeaders === undefined ? _configHeaders : _configHeaders);
539
+ return runtime_await(runtime_await(configHeaders === undefined ? {} : typeof configHeaders === "function" ? configHeaders() : configHeaders, function (resolvedConfigHeaders) {
540
+ for (var _i = 0, _Object$keys = Object.keys(resolvedConfigHeaders); _i < _Object$keys.length; _i++) {
541
+ var key = _Object$keys[_i];
542
+ if (key.toLowerCase() === "authorization" || key.toLowerCase() === "authorization-token") {
543
+ for (var _i2 = 0, _Object$keys2 = Object.keys(context.headers); _i2 < _Object$keys2.length; _i2++) {
544
+ var generatedKey = _Object$keys2[_i2];
545
+ if (generatedKey.toLowerCase() === key.toLowerCase()) {
546
+ // Custom auth headers have documented precedence; remove the generated spelling to make that deterministic.
547
+ // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
548
+ delete context.headers[generatedKey];
549
+ }
550
+ }
551
+ }
552
+ }
553
+ var headers = Object.assign(Object.assign({}, context.headers), resolvedConfigHeaders);
460
554
  Object.keys(headers).forEach(function (key) {
461
555
  return headers[key] === undefined ? delete headers[key] : {};
462
556
  });
@@ -468,12 +562,12 @@ var BaseAPI = /*#__PURE__*/function () {
468
562
  headers: headers,
469
563
  body: context.body
470
564
  };
471
- var _Object$assign2 = Object.assign({}, initParams);
565
+ var _Object$assign = Object.assign({}, initParams);
472
566
  return runtime_await(initOverrideFn({
473
567
  init: initParams,
474
568
  context: context
475
569
  }), function (_initOverrideFn) {
476
- var overriddenInit = Object.assign(_Object$assign2, _initOverrideFn);
570
+ var overriddenInit = Object.assign(_Object$assign, _initOverrideFn);
477
571
  var init = Object.assign(Object.assign({}, overriddenInit), {
478
572
  body: JSON.stringify(overriddenInit.body)
479
573
  });
@@ -2098,7 +2192,6 @@ var UploadManagerBase = /*#__PURE__*/function () {
2098
2192
  this.defaultMaxConcurrentUploadParts = 4;
2099
2193
  this.intervalMs = 500;
2100
2194
  this.uploadApi = new UploadApi(config);
2101
- this.accountId = BytescaleApiClientConfigUtils.getAccountId(config);
2102
2195
  }
2103
2196
  return UploadManagerBase_createClass(UploadManagerBase, [{
2104
2197
  key: "upload",
@@ -2106,6 +2199,7 @@ var UploadManagerBase = /*#__PURE__*/function () {
2106
2199
  try {
2107
2200
  var _this = this;
2108
2201
  _this.assertNotCancelled(request);
2202
+ var accountId = BytescaleApiClientConfigUtils.getAccountId(_this.config);
2109
2203
  var source = _this.processUploadSource(request.data);
2110
2204
  var preUploadInfo = _this.getPreUploadInfo(request, source);
2111
2205
  var bytesTotal = preUploadInfo.size;
@@ -2115,7 +2209,7 @@ var UploadManagerBase = /*#__PURE__*/function () {
2115
2209
  if (request.onProgress !== undefined) {
2116
2210
  request.onProgress(_this.makeProgressEvent(0, bytesTotal));
2117
2211
  }
2118
- return UploadManagerBase_await(_this.beginUpload(request, preUploadInfo), function (uploadInfo) {
2212
+ return UploadManagerBase_await(_this.beginUpload(request, preUploadInfo, accountId), function (uploadInfo) {
2119
2213
  var partCount = uploadInfo.uploadParts.count;
2120
2214
  var parts = UploadManagerBase_toConsumableArray(Array(partCount).keys());
2121
2215
  var _this$makeCancellatio = _this.makeCancellationMethods(),
@@ -2125,7 +2219,7 @@ var UploadManagerBase = /*#__PURE__*/function () {
2125
2219
  var uploadedParts;
2126
2220
  return UploadManagerBase_continue(UploadManagerBase_finallyRethrows(function () {
2127
2221
  return UploadManagerBase_await(_this.mapAsync(parts, preUploadInfo.maxConcurrentUploadParts, _async(function (part) {
2128
- return _this.uploadPart(request, source, part, uploadInfo, makeOnProgressForPart(), addCancellationHandler);
2222
+ return _this.uploadPart(request, source, part, uploadInfo, makeOnProgressForPart(), addCancellationHandler, accountId);
2129
2223
  })), function (_this$mapAsync) {
2130
2224
  uploadedParts = _this$mapAsync;
2131
2225
  return UploadManagerBase_awaitIgnored(_this.postUpload(init));
@@ -2237,14 +2331,14 @@ var UploadManagerBase = /*#__PURE__*/function () {
2237
2331
  }
2238
2332
  }, {
2239
2333
  key: "beginUpload",
2240
- value: function beginUpload(request, _ref2) {
2334
+ value: function beginUpload(request, _ref2, accountId) {
2241
2335
  var size = _ref2.size,
2242
2336
  mime = _ref2.mime,
2243
2337
  originalFileName = _ref2.originalFileName;
2244
2338
  try {
2245
2339
  var _this4 = this;
2246
2340
  return UploadManagerBase_await(_this4.uploadApi.beginMultipartUpload({
2247
- accountId: _this4.accountId,
2341
+ accountId: accountId,
2248
2342
  beginMultipartUploadRequest: {
2249
2343
  metadata: request.metadata,
2250
2344
  mime: mime,
@@ -2261,16 +2355,16 @@ var UploadManagerBase = /*#__PURE__*/function () {
2261
2355
  }
2262
2356
  }, {
2263
2357
  key: "uploadPart",
2264
- value: function uploadPart(request, source, partIndex, uploadInfo, onProgress, addCancellationHandler) {
2358
+ value: function uploadPart(request, source, partIndex, uploadInfo, onProgress, addCancellationHandler, accountId) {
2265
2359
  try {
2266
2360
  var _this5 = this;
2267
2361
  _this5.assertNotCancelled(request);
2268
- return UploadManagerBase_await(_this5.getUploadPart(partIndex, uploadInfo), function (part) {
2362
+ return UploadManagerBase_await(_this5.getUploadPart(partIndex, uploadInfo, accountId), function (part) {
2269
2363
  _this5.assertNotCancelled(request);
2270
2364
  return UploadManagerBase_await(_this5.putUploadPart(part, source, onProgress, addCancellationHandler), function (etag) {
2271
2365
  _this5.assertNotCancelled(request);
2272
2366
  return UploadManagerBase_await(_this5.uploadApi.completeUploadPart({
2273
- accountId: _this5.accountId,
2367
+ accountId: accountId,
2274
2368
  uploadId: uploadInfo.uploadId,
2275
2369
  uploadPartIndex: partIndex,
2276
2370
  completeUploadPartRequest: {
@@ -2311,7 +2405,7 @@ var UploadManagerBase = /*#__PURE__*/function () {
2311
2405
  }
2312
2406
  }, {
2313
2407
  key: "getUploadPart",
2314
- value: function getUploadPart(partIndex, uploadInfo) {
2408
+ value: function getUploadPart(partIndex, uploadInfo, accountId) {
2315
2409
  try {
2316
2410
  var _this7 = this;
2317
2411
  if (partIndex === 0) {
@@ -2319,7 +2413,7 @@ var UploadManagerBase = /*#__PURE__*/function () {
2319
2413
  }
2320
2414
  return UploadManagerBase_await(_this7.uploadApi.getUploadPart({
2321
2415
  uploadId: uploadInfo.uploadId,
2322
- accountId: _this7.accountId,
2416
+ accountId: accountId,
2323
2417
  uploadPartIndex: partIndex
2324
2418
  }));
2325
2419
  } catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytescale/sdk",
3
- "version": "3.58.0",
3
+ "version": "3.59.0",
4
4
  "description": "Bytescale JavaScript SDK",
5
5
  "author": "Bytescale <hello@bytescale.com> (https://www.bytescale.com)",
6
6
  "license": "MIT",
@@ -0,0 +1,222 @@
1
+ import { jest } from "@jest/globals";
2
+ import { Headers as NodeFetchHeaders, Response as NodeFetchResponse } from "node-fetch";
3
+ import { AuthSessionState } from "../src/private/AuthSessionState";
4
+ import { AuthSession, AuthSessionConfigState } from "../src/private/model/AuthSession";
5
+ import { BaseAPI, BytescaleApiClientConfig, FetchAPI, RequestOpts } from "../src/public/shared/generated/runtime";
6
+ import { BeginAuthSessionParams } from "../src/private/model/BeginAuthSessionParams";
7
+ import { AuthSessionConfig } from "../src/private/model/AuthSessionConfig";
8
+
9
+ const accountA = "A123abc";
10
+ const accountB = "B123abc";
11
+ const apiKeyA = `public_${accountA}_test`;
12
+
13
+ class TestApi extends BaseAPI {
14
+ async get(): Promise<Response> {
15
+ const request: RequestOpts = { headers: {}, method: "GET", path: "/test" };
16
+ return await this.request(request, undefined, undefined);
17
+ }
18
+ }
19
+
20
+ describe("API-client AuthManager configuration", () => {
21
+ const originalWindow = Object.getOwnPropertyDescriptor(globalThis, "window");
22
+
23
+ beforeAll(() => {
24
+ Object.defineProperty(globalThis, "window", { configurable: true, value: {} });
25
+ });
26
+
27
+ afterEach(() => {
28
+ AuthSessionState.setSession(undefined);
29
+ });
30
+
31
+ afterAll(() => {
32
+ if (originalWindow === undefined) {
33
+ Reflect.deleteProperty(globalThis, "window");
34
+ } else {
35
+ Object.defineProperty(globalThis, "window", originalWindow);
36
+ }
37
+ });
38
+
39
+ test("retains API-key-only authentication when no default AuthManager config exists", async () => {
40
+ setModernSession([state("named", accountA, "jwt-a", "access-a")]);
41
+ const { api, fetchApi } = createApi({ apiKey: apiKeyA });
42
+
43
+ await api.get();
44
+
45
+ expect(requestHeaders(fetchApi).get("Authorization")).toBe(`Bearer ${apiKeyA}`);
46
+ expect(requestHeaders(fetchApi).has("Authorization-Token")).toBe(false);
47
+ });
48
+
49
+ test("supplements an API key with the selected registered access token on every request", async () => {
50
+ const defaultState = state(undefined, accountA, "jwt-a", "access-a");
51
+ setModernSession([defaultState]);
52
+ const { api, fetchApi } = createApi({ apiKey: apiKeyA });
53
+
54
+ await api.get();
55
+ defaultState.accessToken = "access-refreshed";
56
+ await api.get();
57
+
58
+ expect(requestHeaders(fetchApi, 0).get("Authorization")).toBe(`Bearer ${apiKeyA}`);
59
+ expect(requestHeaders(fetchApi, 0).get("Authorization-Token")).toBe("access-a");
60
+ expect(requestHeaders(fetchApi, 1).get("Authorization-Token")).toBe("access-refreshed");
61
+ });
62
+
63
+ test("uses the raw JWT as sole authentication for an API-key-less named client", async () => {
64
+ setModernSession([state("customer", accountB, "jwt-b", "access-b")]);
65
+ const { api, fetchApi } = createApi({ authConfigId: "customer" });
66
+
67
+ await api.get();
68
+
69
+ expect(requestHeaders(fetchApi).get("Authorization")).toBe("Bearer jwt-b");
70
+ expect(requestHeaders(fetchApi).has("Authorization-Token")).toBe(false);
71
+ });
72
+
73
+ test("supports an API-key-less default client", async () => {
74
+ setModernSession([state(undefined, accountA, "jwt-a", "access-a")]);
75
+ const { api, fetchApi } = createApi({});
76
+
77
+ await api.get();
78
+
79
+ expect(requestHeaders(fetchApi).get("Authorization")).toBe("Bearer jwt-a");
80
+ });
81
+
82
+ test("fails clearly for opt-out without an API key and for an unknown named config", async () => {
83
+ setModernSession([state(undefined, accountA, "jwt-a", "access-a")]);
84
+
85
+ await expect(createApi({ authConfigId: false }).api.get()).rejects.toThrow("provide an API key");
86
+ await expect(createApi({ apiKey: apiKeyA, authConfigId: "missing" }).api.get()).rejects.toThrow(
87
+ "No active AuthManager configuration has ID 'missing'"
88
+ );
89
+ });
90
+
91
+ test("fails before fetch when API-key and AuthManager accounts differ", async () => {
92
+ setModernSession([state(undefined, accountB, "jwt-b", "access-b")]);
93
+ const { api, fetchApi } = createApi({ apiKey: apiKeyA });
94
+
95
+ await expect(api.get()).rejects.toThrow("belongs to account");
96
+ expect(fetchApi).not.toHaveBeenCalled();
97
+ });
98
+
99
+ test("allows construction before an API-key-less AuthManager config becomes available", async () => {
100
+ const { api, fetchApi } = createApi({ authConfigId: "later" });
101
+ await expect(api.get()).rejects.toThrow("No active AuthManager configuration");
102
+
103
+ setModernSession([state("later", accountA, "jwt-a", "access-a")]);
104
+ await api.get();
105
+
106
+ expect(requestHeaders(fetchApi).get("Authorization")).toBe("Bearer jwt-a");
107
+ });
108
+
109
+ test("fails closed for an expired selected configuration", async () => {
110
+ const expired = state("expired", accountA, "jwt-a", "access-a");
111
+ expired.expiresAt = Date.now() - 1;
112
+ setModernSession([expired]);
113
+
114
+ await expect(createApi({ apiKey: apiKeyA, authConfigId: "expired" }).api.get()).rejects.toThrow("not ready");
115
+ await expect(createApi({ authConfigId: "expired" }).api.get()).rejects.toThrow("not ready");
116
+ });
117
+
118
+ test("fails closed for an expired V2 default unless AuthManager is explicitly disabled", async () => {
119
+ const expired = state(undefined, accountA, "jwt-a", "access-a");
120
+ expired.expiresAt = Date.now() - 1;
121
+ setModernSession([expired]);
122
+
123
+ await expect(createApi({ apiKey: apiKeyA }).api.get()).rejects.toThrow("not ready");
124
+ const optedOut = createApi({ apiKey: apiKeyA, authConfigId: false });
125
+ await optedOut.api.get();
126
+ expect(requestHeaders(optedOut.fetchApi).get("Authorization")).toBe(`Bearer ${apiKeyA}`);
127
+ });
128
+
129
+ test("recognizes a 3.54 session as the default supplemental token", async () => {
130
+ AuthSessionState.setSession({
131
+ accessToken: "legacy-access",
132
+ accessTokenRefreshHandle: undefined,
133
+ authServiceWorker: undefined,
134
+ isActive: true,
135
+ params: {
136
+ accountId: accountA,
137
+ authHeaders: async (): Promise<Record<string, string>> => ({}),
138
+ authUrl: "https://app.example.com/auth"
139
+ }
140
+ });
141
+ const withKey = createApi({ apiKey: apiKeyA });
142
+
143
+ await withKey.api.get();
144
+
145
+ expect(requestHeaders(withKey.fetchApi).get("Authorization-Token")).toBe("legacy-access");
146
+ await expect(createApi({}).api.get()).rejects.toThrow("provide an API key");
147
+ });
148
+
149
+ test("retains deterministic custom authentication-header precedence", async () => {
150
+ setModernSession([state(undefined, accountA, "jwt-a", "access-a")]);
151
+ const { api, fetchApi } = createApi({
152
+ apiKey: apiKeyA,
153
+ headers: async (): Promise<Record<string, string>> => ({
154
+ "authorization": "Custom authorization",
155
+ "AUTHORIZATION-TOKEN": "custom-token"
156
+ })
157
+ });
158
+
159
+ await api.get();
160
+
161
+ const headers = requestHeaders(fetchApi);
162
+ expect(headers.get("Authorization")).toBe("Custom authorization");
163
+ expect(headers.get("Authorization-Token")).toBe("custom-token");
164
+ expect(headers.has("authConfigId")).toBe(false);
165
+ });
166
+ });
167
+
168
+ function createApi(config: Omit<BytescaleApiClientConfig, "fetchApi">): {
169
+ api: TestApi;
170
+ fetchApi: jest.MockedFunction<FetchAPI>;
171
+ } {
172
+ const fetchApi = jest.fn<FetchAPI>(async () => new NodeFetchResponse("{}") as unknown as Response);
173
+ return { api: new TestApi({ ...config, fetchApi }), fetchApi };
174
+ }
175
+
176
+ function requestHeaders(
177
+ fetchApi: jest.MockedFunction<FetchAPI>,
178
+ call = fetchApi.mock.calls.length - 1
179
+ ): NodeFetchHeaders {
180
+ return new NodeFetchHeaders(fetchApi.mock.calls[call][1]?.headers as Record<string, string>);
181
+ }
182
+
183
+ function state(
184
+ authConfigId: string | undefined,
185
+ accountId: string,
186
+ jwt: string,
187
+ accessToken: string
188
+ ): AuthSessionConfigState {
189
+ const config: AuthSessionConfig = {
190
+ accountId,
191
+ authConfigId,
192
+ enableServiceWorkerAuth: false,
193
+ getAuthorizationToken: async () => jwt
194
+ };
195
+ return {
196
+ accessToken,
197
+ config,
198
+ expiresAt: Date.now() + 60_000,
199
+ jwt,
200
+ refreshHandle: undefined
201
+ };
202
+ }
203
+
204
+ function setModernSession(states: AuthSessionConfigState[]): void {
205
+ const configs = states.map(value => value.config);
206
+ const nonEmptyConfigs: [AuthSessionConfig, ...AuthSessionConfig[]] = [configs[0], ...configs.slice(1)];
207
+ const params: BeginAuthSessionParams = {
208
+ authConfigs: async () => nonEmptyConfigs,
209
+ serviceWorkerScript: undefined
210
+ };
211
+ const session: AuthSession = {
212
+ accessToken: undefined,
213
+ accessTokenRefreshHandle: undefined,
214
+ authConfigs: states,
215
+ authServiceWorker: undefined,
216
+ isActive: true,
217
+ isReady: true,
218
+ params,
219
+ serviceWorkerConfigured: false
220
+ };
221
+ AuthSessionState.setSession(session);
222
+ }