@tstdl/base 0.93.127 → 0.93.129

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 (57) hide show
  1. package/api/client/client.js +45 -9
  2. package/api/client/tests/api-client.test.d.ts +1 -0
  3. package/api/client/tests/api-client.test.js +194 -0
  4. package/api/types.d.ts +35 -3
  5. package/authentication/client/authentication.service.js +30 -11
  6. package/authentication/client/http-client.middleware.js +10 -3
  7. package/authentication/server/authentication.service.d.ts +12 -0
  8. package/authentication/server/authentication.service.js +14 -2
  9. package/authentication/tests/authentication.client-error-handling.test.js +23 -66
  10. package/authentication/tests/authentication.client-service-refresh.test.js +14 -14
  11. package/cancellation/token.d.ts +6 -0
  12. package/cancellation/token.js +8 -0
  13. package/document-management/server/services/document-file.service.js +10 -9
  14. package/document-management/server/services/document-management-ancillary.service.d.ts +12 -1
  15. package/document-management/server/services/document-management-ancillary.service.js +9 -0
  16. package/file/server/temporary-file.d.ts +2 -1
  17. package/file/server/temporary-file.js +5 -1
  18. package/http/client/adapters/undici.adapter.js +0 -2
  19. package/http/client/http-client-request.d.ts +2 -0
  20. package/http/client/http-client-request.js +4 -0
  21. package/http/client/http-client-response.d.ts +1 -1
  22. package/http/client/http-client-response.js +3 -2
  23. package/http/utils.d.ts +6 -0
  24. package/http/utils.js +71 -0
  25. package/injector/injector.js +2 -0
  26. package/mail/drizzle/0000_numerous_the_watchers.sql +8 -0
  27. package/mail/drizzle/meta/0000_snapshot.json +1 -32
  28. package/mail/drizzle/meta/_journal.json +2 -9
  29. package/object-storage/s3/s3.object-storage.js +6 -6
  30. package/object-storage/s3/tests/s3.object-storage.integration.test.js +22 -53
  31. package/orm/tests/repository-expiration.test.js +3 -3
  32. package/package.json +1 -1
  33. package/pdf/utils.d.ts +24 -3
  34. package/pdf/utils.js +89 -30
  35. package/process/spawn.d.ts +1 -1
  36. package/rate-limit/tests/postgres-rate-limiter.test.js +9 -7
  37. package/renderer/typst.d.ts +5 -0
  38. package/renderer/typst.js +9 -5
  39. package/task-queue/tests/complex.test.js +22 -22
  40. package/task-queue/tests/dependencies.test.js +15 -13
  41. package/task-queue/tests/queue.test.js +13 -13
  42. package/task-queue/tests/worker.test.js +12 -12
  43. package/testing/integration-setup.d.ts +2 -0
  44. package/testing/integration-setup.js +13 -7
  45. package/utils/backoff.d.ts +27 -3
  46. package/utils/backoff.js +31 -9
  47. package/utils/index.d.ts +1 -0
  48. package/utils/index.js +1 -0
  49. package/utils/retry-with-backoff.d.ts +22 -0
  50. package/utils/retry-with-backoff.js +64 -0
  51. package/utils/tests/backoff.test.d.ts +1 -0
  52. package/utils/tests/backoff.test.js +41 -0
  53. package/utils/tests/retry-with-backoff.test.d.ts +1 -0
  54. package/utils/tests/retry-with-backoff.test.js +49 -0
  55. package/mail/drizzle/0000_previous_malcolm_colcord.sql +0 -13
  56. package/mail/drizzle/0001_flimsy_bloodscream.sql +0 -5
  57. package/mail/drizzle/meta/0001_snapshot.json +0 -69
@@ -1,3 +1,4 @@
1
+ import { CancellationToken } from '../../cancellation/index.js';
1
2
  import { HttpClient, HttpClientRequest } from '../../http/client/index.js';
2
3
  import { bustCache, normalizeSingleHttpValue } from '../../http/index.js';
3
4
  import { inject } from '../../injector/inject.js';
@@ -64,8 +65,25 @@ export function compileClient(definition, options = defaultOptions) {
64
65
  let resource;
65
66
  const hasGet = methods.includes('GET');
66
67
  const fallbackMethod = methods.find((method) => method != 'GET') ?? 'GET';
68
+ const hasParameters = isDefined(endpoint.parameters);
69
+ const hasBody = isDefined(endpoint.body);
67
70
  const apiEndpointFunction = {
68
- async [name](parameters, requestBody) {
71
+ async [name](...args) {
72
+ let parameters;
73
+ let requestBody;
74
+ let requestOptions;
75
+ if (hasBody) {
76
+ parameters = args[0];
77
+ requestBody = args[1];
78
+ requestOptions = args[2];
79
+ }
80
+ else if (hasParameters) {
81
+ parameters = args[0];
82
+ requestOptions = args[1];
83
+ }
84
+ else {
85
+ requestOptions = args[0];
86
+ }
69
87
  resource ??= getFullApiEndpointResource({ api: definition, endpoint, defaultPrefix: options.prefix });
70
88
  const context = { endpoint };
71
89
  const method = (hasGet && isUndefined(parameters)) ? 'GET' : fallbackMethod;
@@ -73,15 +91,15 @@ export function compileClient(definition, options = defaultOptions) {
73
91
  if (isDefined(requestBody)) {
74
92
  throw new Error('Body not supported for Server Sent Events.');
75
93
  }
76
- return getServerSentEvents(this[httpClientSymbol].options.baseUrl, resource, endpoint, parameters);
94
+ return getServerSentEvents(this[httpClientSymbol].options.baseUrl, resource, endpoint, parameters, requestOptions);
77
95
  }
78
96
  if (endpoint.result == DataStream) {
79
97
  if (isDefined(requestBody)) {
80
98
  throw new Error('Body not supported for DataStream.');
81
99
  }
82
- return getDataStream(this[httpClientSymbol].options.baseUrl, resource, endpoint, parameters);
100
+ return getDataStream(this[httpClientSymbol].options.baseUrl, resource, endpoint, parameters, requestOptions);
83
101
  }
84
- if (context.endpoint.data?.[bustCache] == true) {
102
+ if (context.endpoint.data?.[bustCache] == true || requestOptions?.bustCache == true) {
85
103
  context[bustCache] = true;
86
104
  }
87
105
  const request = new HttpClientRequest({
@@ -90,7 +108,12 @@ export function compileClient(definition, options = defaultOptions) {
90
108
  parameters,
91
109
  body: getRequestBody(requestBody),
92
110
  credentials: (endpoint.credentials == true) ? 'include' : undefined,
93
- context,
111
+ context: { ...context, ...requestOptions?.context },
112
+ abortSignal: getCancellationSignal(requestOptions?.abortSignal),
113
+ timeout: requestOptions?.timeout,
114
+ headers: requestOptions?.headers,
115
+ authorization: requestOptions?.authorization,
116
+ priority: requestOptions?.priority,
94
117
  });
95
118
  const response = await this[httpClientSymbol].rawRequest(request);
96
119
  return await getResponseBody(response, endpoint.result);
@@ -133,11 +156,11 @@ async function getResponseBody(response, schema) {
133
156
  : undefined;
134
157
  return Schema.parse(schema, body, { mask: true });
135
158
  }
136
- function getDataStream(baseUrl, resource, endpoint, parameters) {
137
- const sse = getServerSentEvents(baseUrl, resource, endpoint, parameters);
159
+ function getDataStream(baseUrl, resource, endpoint, parameters, options) {
160
+ const sse = getServerSentEvents(baseUrl, resource, endpoint, parameters, options);
138
161
  return DataStream.parse(sse);
139
162
  }
140
- function getServerSentEvents(baseUrl, resource, endpoint, parameters) {
163
+ function getServerSentEvents(baseUrl, resource, endpoint, parameters, options) {
141
164
  const { parsedUrl, parametersRest } = buildUrl(resource, parameters, { arraySeparator: ',' });
142
165
  const url = new URL(parsedUrl, baseUrl);
143
166
  for (const [parameter, value] of objectEntries(parametersRest)) {
@@ -148,7 +171,20 @@ function getServerSentEvents(baseUrl, resource, endpoint, parameters) {
148
171
  url.searchParams.append(parameter, normalizeSingleHttpValue(val));
149
172
  }
150
173
  }
151
- return new ServerSentEvents(url.toString(), { withCredentials: endpoint.credentials });
174
+ const sse = new ServerSentEvents(url.toString(), { withCredentials: endpoint.credentials });
175
+ if (isDefined(options?.abortSignal)) {
176
+ options.abortSignal.addEventListener('abort', () => sse.close(), { once: true });
177
+ }
178
+ return sse;
179
+ }
180
+ function getCancellationSignal(signal) {
181
+ if (isUndefined(signal)) {
182
+ return undefined;
183
+ }
184
+ if (signal instanceof AbortSignal) {
185
+ return CancellationToken.from(signal).signal;
186
+ }
187
+ return signal;
152
188
  }
153
189
  export function getHttpClientOfApiClient(apiClient) {
154
190
  return apiClient[httpClientSymbol];
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,194 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { CancellationToken } from '../../../cancellation/index.js';
3
+ import { HttpClient } from '../../../http/client/index.js';
4
+ import { bustCache } from '../../../http/index.js';
5
+ import { object } from '../../../schema/index.js';
6
+ import { DataStream } from '../../../sse/data-stream.js';
7
+ import { ServerSentEvents } from '../../../sse/server-sent-events.js';
8
+ import { defineApi } from '../../types.js';
9
+ import { compileClient } from '../client.js';
10
+ describe('ApiClient', () => {
11
+ it('should pass abortSignal and handle positional arguments', async () => {
12
+ const apiDefinition = defineApi({
13
+ resource: 'test',
14
+ endpoints: {
15
+ noParams: {
16
+ method: 'GET',
17
+ resource: 'no-params',
18
+ },
19
+ onlyParams: {
20
+ method: 'GET',
21
+ resource: 'only-params',
22
+ parameters: object({
23
+ id: String,
24
+ }),
25
+ },
26
+ paramsAndBody: {
27
+ method: 'POST',
28
+ resource: 'params-and-body',
29
+ parameters: object({
30
+ id: String,
31
+ }),
32
+ body: object({
33
+ data: String,
34
+ }),
35
+ },
36
+ },
37
+ });
38
+ const mockHttpClient = Object.assign(Object.create(HttpClient.prototype), {
39
+ rawRequest: vi.fn().mockResolvedValue({
40
+ statusCode: 200,
41
+ hasBody: false,
42
+ close: vi.fn(),
43
+ }),
44
+ options: { baseUrl: 'http://localhost/' },
45
+ });
46
+ const Client = compileClient(apiDefinition);
47
+ const client = new Client(mockHttpClient);
48
+ const abortController = new AbortController();
49
+ const cancellationToken = new CancellationToken();
50
+ // 1. No params
51
+ await client.noParams({ abortSignal: abortController.signal });
52
+ expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
53
+ url: expect.stringContaining('no-params'),
54
+ }));
55
+ let lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
56
+ expect(lastRequest.abortSignal.isSet).toBe(false);
57
+ abortController.abort();
58
+ expect(lastRequest.abortSignal.isSet).toBe(true);
59
+ // 2. Only params
60
+ await client.onlyParams({ id: '123' }, { abortSignal: cancellationToken.abortSignal });
61
+ expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
62
+ url: expect.stringContaining('only-params'),
63
+ parameters: { id: '123' },
64
+ }));
65
+ lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
66
+ expect(lastRequest.abortSignal.isSet).toBe(false);
67
+ cancellationToken.set();
68
+ expect(lastRequest.abortSignal.isSet).toBe(true);
69
+ // 3. Params and body
70
+ const abortController3 = new AbortController();
71
+ await client.paramsAndBody({ id: '456' }, { data: 'val' }, { abortSignal: abortController3.signal });
72
+ expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
73
+ url: expect.stringContaining('params-and-body'),
74
+ parameters: { id: '456' },
75
+ body: { json: { data: 'val' } },
76
+ }));
77
+ lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
78
+ expect(lastRequest.abortSignal.isSet).toBe(false);
79
+ abortController3.abort();
80
+ expect(lastRequest.abortSignal.isSet).toBe(true);
81
+ // 4. Omitted requestOptions
82
+ await client.onlyParams({ id: '789' });
83
+ expect(mockHttpClient.rawRequest).toHaveBeenLastCalledWith(expect.objectContaining({
84
+ url: expect.stringContaining('only-params'),
85
+ parameters: { id: '789' },
86
+ }));
87
+ lastRequest = mockHttpClient.rawRequest.mock.calls.at(-1)[0];
88
+ expect(lastRequest.abortSignal.isSet).toBe(false);
89
+ });
90
+ it('should handle different HTTP methods', async () => {
91
+ const apiDefinition = defineApi({
92
+ resource: 'methods',
93
+ endpoints: {
94
+ get: { method: 'GET' },
95
+ post: { method: 'POST' },
96
+ put: { method: 'PUT' },
97
+ patch: { method: 'PATCH' },
98
+ delete: { method: 'DELETE' },
99
+ },
100
+ });
101
+ const mockHttpClient = Object.assign(Object.create(HttpClient.prototype), {
102
+ rawRequest: vi.fn().mockResolvedValue({ statusCode: 200, hasBody: false, close: vi.fn() }),
103
+ options: { baseUrl: 'http://localhost/' },
104
+ });
105
+ const Client = compileClient(apiDefinition);
106
+ const client = new Client(mockHttpClient);
107
+ await client.get();
108
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].method).toBe('GET');
109
+ await client.post();
110
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].method).toBe('POST');
111
+ await client.put();
112
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].method).toBe('PUT');
113
+ await client.patch();
114
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].method).toBe('PATCH');
115
+ await client.delete();
116
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].method).toBe('DELETE');
117
+ });
118
+ it('should handle body types', async () => {
119
+ const apiDefinition = defineApi({
120
+ resource: 'body',
121
+ endpoints: {
122
+ json: { method: 'POST', body: object({ foo: String }) },
123
+ text: { method: 'POST', body: String },
124
+ binary: { method: 'POST', body: Uint8Array },
125
+ },
126
+ });
127
+ const mockHttpClient = Object.assign(Object.create(HttpClient.prototype), {
128
+ rawRequest: vi.fn().mockResolvedValue({ statusCode: 200, hasBody: false, close: vi.fn() }),
129
+ options: { baseUrl: 'http://localhost/' },
130
+ });
131
+ const Client = compileClient(apiDefinition);
132
+ const client = new Client(mockHttpClient);
133
+ await client.json(undefined, { foo: 'bar' });
134
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].body).toEqual({ json: { foo: 'bar' } });
135
+ await client.text(undefined, 'hello');
136
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].body).toEqual({ text: 'hello' });
137
+ const buffer = new Uint8Array([1, 2, 3]);
138
+ await client.binary(undefined, buffer);
139
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].body).toEqual({ binary: buffer });
140
+ });
141
+ it('should handle credentials, bustCache and resource methods', async () => {
142
+ const apiDefinition = defineApi({
143
+ resource: 'features',
144
+ endpoints: {
145
+ withCredentials: { method: 'GET', credentials: true },
146
+ withBustCache: { method: 'GET', data: { [bustCache]: true } },
147
+ },
148
+ });
149
+ const mockHttpClient = Object.assign(Object.create(HttpClient.prototype), {
150
+ rawRequest: vi.fn().mockResolvedValue({ statusCode: 200, hasBody: false, close: vi.fn() }),
151
+ options: { baseUrl: 'http://localhost/' },
152
+ });
153
+ const Client = compileClient(apiDefinition);
154
+ const client = new Client(mockHttpClient);
155
+ await client.withCredentials();
156
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].credentials).toBe('include');
157
+ await client.withBustCache();
158
+ expect(mockHttpClient.rawRequest.mock.calls.at(-1)[0].context[bustCache]).toBe(true);
159
+ expect(Client.getEndpointResource('withCredentials')).not.toContain('withCredentials');
160
+ expect(Client.getEndpointUrl('withCredentials').href).toBe('http://baseurl/api/v1/features');
161
+ expect(client.getEndpointUrl('withCredentials').href).toBe('http://localhost/api/v1/features');
162
+ });
163
+ it('should handle Server Sent Events and DataStream', async () => {
164
+ const apiDefinition = defineApi({
165
+ resource: 'sse',
166
+ endpoints: {
167
+ events: { method: 'GET', result: ServerSentEvents },
168
+ stream: { method: 'GET', result: DataStream },
169
+ },
170
+ });
171
+ const mockEventSource = {
172
+ addEventListener: vi.fn(),
173
+ removeEventListener: vi.fn(),
174
+ close: vi.fn(),
175
+ readyState: 0,
176
+ };
177
+ const EventSourceMock = vi.fn().mockImplementation(function () {
178
+ return mockEventSource;
179
+ });
180
+ vi.stubGlobal('EventSource', EventSourceMock);
181
+ const mockHttpClient = Object.assign(Object.create(HttpClient.prototype), {
182
+ options: { baseUrl: 'http://localhost/' },
183
+ });
184
+ const Client = compileClient(apiDefinition);
185
+ const client = new Client(mockHttpClient);
186
+ const sse = await client.events();
187
+ expect(sse).toBeInstanceOf(ServerSentEvents);
188
+ expect(EventSourceMock).toHaveBeenCalledWith(expect.stringContaining('sse'), expect.any(Object));
189
+ const stream = await client.stream();
190
+ expect(stream).toBeInstanceOf(Object); // It's an Observable
191
+ expect(EventSourceMock).toHaveBeenCalledWith(expect.stringContaining('sse'), expect.any(Object));
192
+ vi.unstubAllGlobals();
193
+ });
194
+ });
package/api/types.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { Observable } from 'rxjs';
2
2
  import type { Auditor } from '../audit/index.js';
3
3
  import type { Token } from '../authentication/index.js';
4
+ import type { HttpHeaders, HttpHeadersObject, HttpRequestAuthorization } from '../http/index.js';
4
5
  import type { HttpServerRequest, HttpServerResponse } from '../http/server/index.js';
5
6
  import type { HttpMethod } from '../http/types.js';
6
7
  import type { SchemaOutput, SchemaTestable } from '../schema/index.js';
@@ -53,7 +54,7 @@ export type ApiEndpointDefinition = {
53
54
  *
54
55
  * results in
55
56
  * ```ts
56
- * ${endpoint.rootResource ?? api.ressource}/${endpoint.resource}
57
+ * ${endpoint.rootResource ?? api.resource}/${endpoint.resource}
57
58
  * ```
58
59
  * @default name of endpoint property
59
60
  */
@@ -118,7 +119,38 @@ export type ApiRequestContext<T extends ApiDefinition = ApiDefinition, K extends
118
119
  getAuditor(): Promise<Auditor>;
119
120
  };
120
121
  export type ApiEndpointServerImplementation<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = (context: ApiRequestContext<T, K>) => ApiServerResult<T, K> | Promise<ApiServerResult<T, K>>;
121
- export type ApiEndpointClientImplementation<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = ApiClientBody<T, K> extends never ? ApiParameters<T, K> extends never ? () => Promise<ApiClientResult<T, K>> : (parameters: ApiParameters<T, K>) => Promise<ApiClientResult<T, K>> : (parameters: ApiParameters<T, K> extends never ? undefined | Record<never, never> : ApiParameters<T, K>, body: ApiClientBody<T, K>) => Promise<ApiClientResult<T, K>>;
122
+ export type ApiClientRequestOptions = {
123
+ /**
124
+ * AbortSignal to cancel the request.
125
+ */
126
+ abortSignal?: AbortSignal;
127
+ /**
128
+ * Request timeout in milliseconds.
129
+ * @default 30000
130
+ */
131
+ timeout?: number;
132
+ /**
133
+ * Additional headers to send with the request.
134
+ */
135
+ headers?: HttpHeadersObject | HttpHeaders;
136
+ /**
137
+ * Can be used to store data for middleware etc.
138
+ */
139
+ context?: Record;
140
+ /**
141
+ * If true, adds a cache-busting parameter to the request URL.
142
+ */
143
+ bustCache?: boolean;
144
+ /**
145
+ * Authorization for the request.
146
+ */
147
+ authorization?: HttpRequestAuthorization;
148
+ /**
149
+ * Fetch priority for the request.
150
+ */
151
+ priority?: RequestPriority;
152
+ };
153
+ export type ApiEndpointClientImplementation<T extends ApiDefinition = ApiDefinition, K extends ApiEndpointKeys<T> = ApiEndpointKeys<T>> = ApiClientBody<T, K> extends never ? ApiParameters<T, K> extends never ? (options?: ApiClientRequestOptions) => Promise<ApiClientResult<T, K>> : (parameters: ApiParameters<T, K>, options?: ApiClientRequestOptions) => Promise<ApiClientResult<T, K>> : (parameters: ApiParameters<T, K> extends never ? undefined | Record<never, never> : ApiParameters<T, K>, body: ApiClientBody<T, K>, options?: ApiClientRequestOptions) => Promise<ApiClientResult<T, K>>;
122
154
  export type ApiController<T extends ApiDefinition = any> = {
123
155
  [P in ApiEndpointKeys<T>]: ApiEndpointServerImplementation<T, P>;
124
156
  };
@@ -126,7 +158,7 @@ export type ApiClientImplementation<T extends ApiDefinition = any> = {
126
158
  [P in ApiEndpointKeys<T>]: ApiEndpointClientImplementation<T, P>;
127
159
  } & {
128
160
  getEndpointResource<E extends ApiEndpointKeys<T>>(endpoint: E, parameters?: ApiParameters<T, E>): string;
129
- getEndpointUrl<E extends ApiEndpointKeys<T>>(endpoint: E, parameters?: ApiParameters<T, E>): string;
161
+ getEndpointUrl<E extends ApiEndpointKeys<T>>(endpoint: E, parameters?: ApiParameters<T, E>): URL;
130
162
  };
131
163
  export declare function defineApi<T extends ApiDefinition>(definition: T): T;
132
164
  export declare function resolveApiEndpointDataProvider<T>(request: HttpServerRequest, context: ApiGatewayMiddlewareContext, provider: ApiEndpointDataProvider<T>): Promise<T>;
@@ -7,7 +7,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  var __metadata = (this && this.__metadata) || function (k, v) {
8
8
  if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
9
  };
10
- import { Subject, filter, firstValueFrom, map, race, timer } from 'rxjs';
10
+ import { Subject, filter, firstValueFrom, map, race, skip, timer } from 'rxjs';
11
11
  import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
12
12
  import { BadRequestError } from '../../errors/bad-request.error.js';
13
13
  import { ForbiddenError } from '../../errors/forbidden.error.js';
@@ -36,7 +36,6 @@ const impersonatorAuthenticationDataStorageKey = 'AuthenticationService:imperson
36
36
  const tokenUpdateBusName = 'AuthenticationService:tokenUpdate';
37
37
  const loggedOutBusName = 'AuthenticationService:loggedOut';
38
38
  const refreshLockResource = 'AuthenticationService:refresh';
39
- const refreshBufferSeconds = 15;
40
39
  const maxRefreshDelay = 15 * millisecondsPerMinute;
41
40
  const lockTimeout = 10000;
42
41
  const logoutTimeout = 150;
@@ -377,8 +376,9 @@ let AuthenticationClientService = class AuthenticationClientService {
377
376
  await this.syncClock();
378
377
  }
379
378
  while (this.disposeSignal.isUnset) {
379
+ const iterationToken = this.token();
380
380
  try {
381
- const token = this.token();
381
+ const token = iterationToken;
382
382
  if (isUndefined(token)) {
383
383
  // Wait for login or dispose.
384
384
  // We ignore forceRefreshToken here because we can't refresh without a token.
@@ -387,6 +387,7 @@ let AuthenticationClientService = class AuthenticationClientService {
387
387
  }
388
388
  const now = this.estimatedServerTimestampSeconds();
389
389
  const forceRefresh = this.forceRefreshToken.isSet;
390
+ const refreshBufferSeconds = calculateRefreshBufferSeconds(token);
390
391
  const needsRefresh = forceRefresh || (now >= (token.exp - refreshBufferSeconds));
391
392
  if (needsRefresh) {
392
393
  let lockAcquired = false;
@@ -394,18 +395,24 @@ let AuthenticationClientService = class AuthenticationClientService {
394
395
  lockAcquired = true;
395
396
  const currentToken = this.token();
396
397
  const currentNow = this.estimatedServerTimestampSeconds();
397
- const stillNeedsRefresh = isDefined(currentToken) && (this.forceRefreshToken.isSet || (currentNow >= (currentToken.exp - refreshBufferSeconds)));
398
+ const currentRefreshBufferSeconds = isDefined(currentToken) ? calculateRefreshBufferSeconds(currentToken) : 0;
399
+ // Passive Sync: Check if another tab refreshed the token while we were waiting for the lock (or trying to get it)
400
+ const stillNeedsRefresh = isDefined(currentToken) && (forceRefresh || (currentNow >= (currentToken.exp - currentRefreshBufferSeconds)));
398
401
  if (stillNeedsRefresh) {
399
402
  await this.refresh();
403
+ if (forceRefresh && (this.token() == currentToken)) {
404
+ this.forceRefreshToken.unset();
405
+ }
406
+ }
407
+ else if (forceRefresh && (this.token() != currentToken)) {
400
408
  this.forceRefreshToken.unset();
401
409
  }
402
410
  });
403
411
  if (!lockAcquired) {
404
- // Lock held by another instance, wait 5 seconds or until state/token changes.
405
- // We ignore forceRefreshToken here to avoid a busy loop if it is already set.
412
+ // Lock held by another instance, wait 5 seconds or until token changes (Passive Sync)
406
413
  const changeReason = await firstValueFrom(race([
407
414
  timer(5000).pipe(map(() => 'timer')),
408
- this.token$.pipe(filter((t) => t !== token), map(() => 'token')),
415
+ this.token$.pipe(filter((t) => t != token), map(() => 'token')),
409
416
  this.disposeSignal,
410
417
  ]), { defaultValue: undefined });
411
418
  if (changeReason == 'token') {
@@ -414,7 +421,8 @@ let AuthenticationClientService = class AuthenticationClientService {
414
421
  continue;
415
422
  }
416
423
  }
417
- const delay = Math.min(maxRefreshDelay, ((this.token()?.exp ?? 0) - this.estimatedServerTimestampSeconds() - refreshBufferSeconds) * millisecondsPerSecond);
424
+ const currentRefreshBufferSeconds = calculateRefreshBufferSeconds(token);
425
+ const delay = Math.min(maxRefreshDelay, ((this.token()?.exp ?? 0) - this.estimatedServerTimestampSeconds() - currentRefreshBufferSeconds) * millisecondsPerSecond);
418
426
  const wakeUpSignals = [
419
427
  this.disposeSignal,
420
428
  this.token$.pipe(filter((t) => t != token)),
@@ -431,9 +439,15 @@ let AuthenticationClientService = class AuthenticationClientService {
431
439
  }
432
440
  catch (error) {
433
441
  this.logger.error(error);
434
- const initialToken = this.token();
435
- await firstValueFrom(race([timer(5000), this.disposeSignal, this.token$.pipe(filter((t) => t != initialToken))]), { defaultValue: undefined });
436
- await timeout(2500);
442
+ if (this.token() != iterationToken) {
443
+ continue;
444
+ }
445
+ await firstValueFrom(race([
446
+ timer(2500),
447
+ this.disposeSignal.set$,
448
+ this.token$.pipe(filter((t) => t != iterationToken)),
449
+ this.forceRefreshToken.set$.pipe(skip(this.forceRefreshToken.isSet ? 1 : 0)),
450
+ ]), { defaultValue: undefined });
437
451
  }
438
452
  }
439
453
  }
@@ -516,3 +530,8 @@ AuthenticationClientService = __decorate([
516
530
  __metadata("design:paramtypes", [])
517
531
  ], AuthenticationClientService);
518
532
  export { AuthenticationClientService };
533
+ function calculateRefreshBufferSeconds(token) {
534
+ const iat = token.iat ?? (token.exp - 3600);
535
+ const lifetime = token.exp - iat;
536
+ return (lifetime * 0.1) + 5;
537
+ }
@@ -1,4 +1,4 @@
1
- import { firstValueFrom, timeout } from 'rxjs';
1
+ import { firstValueFrom, race, timeout } from 'rxjs';
2
2
  import { HttpError } from '../../http/index.js';
3
3
  import { isDefined } from '../../utils/type-guards.js';
4
4
  import { cacheValueOrAsyncProvider } from '../../utils/value-or-provider.js';
@@ -14,8 +14,15 @@ export function waitForAuthenticationCredentialsMiddleware(authenticationService
14
14
  const endpoint = request.context?.endpoint;
15
15
  if ((endpoint?.credentials == true) && (endpoint.data?.[dontWaitForValidToken] != true)) {
16
16
  const authenticationService = await getAuthenticationService();
17
- while (!authenticationService.hasValidToken) {
18
- await firstValueFrom(authenticationService.validToken$.pipe(timeout(30000)));
17
+ while (!authenticationService.hasValidToken && authenticationService.isLoggedIn()) {
18
+ const race$ = race([
19
+ authenticationService.validToken$,
20
+ request.abortSignal,
21
+ ]);
22
+ await firstValueFrom(race$.pipe(timeout(30000))).catch(() => undefined);
23
+ if (request.abortSignal.isSet) {
24
+ break;
25
+ }
19
26
  }
20
27
  }
21
28
  await next();
@@ -65,6 +65,18 @@ export declare class AuthenticationServiceOptions {
65
65
  * @default 10 minutes
66
66
  */
67
67
  secretResetTokenTimeToLive?: number;
68
+ /**
69
+ * Number of iterations for password hashing.
70
+ *
71
+ * @default 250000
72
+ */
73
+ hashIterations?: number;
74
+ /**
75
+ * Number of iterations for signing secrets derivation.
76
+ *
77
+ * @default 500000
78
+ */
79
+ signingSecretsDerivationIterations?: number;
68
80
  }
69
81
  /**
70
82
  * Result of an authentication attempt.
@@ -63,6 +63,18 @@ export class AuthenticationServiceOptions {
63
63
  * @default 10 minutes
64
64
  */
65
65
  secretResetTokenTimeToLive;
66
+ /**
67
+ * Number of iterations for password hashing.
68
+ *
69
+ * @default 250000
70
+ */
71
+ hashIterations;
72
+ /**
73
+ * Number of iterations for signing secrets derivation.
74
+ *
75
+ * @default 500000
76
+ */
77
+ signingSecretsDerivationIterations;
66
78
  }
67
79
  const HASH_ITERATIONS = 250000;
68
80
  const HASH_LENGTH_BITS = 512;
@@ -747,7 +759,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
747
759
  const key = await importPbkdf2Key(secret);
748
760
  const saltBase64 = await this.#keyValueStore.getOrSet('derivationSalt', encodeBase64(getRandomBytes(SALT_LENGTH)));
749
761
  const salt = decodeBase64(saltBase64);
750
- const algorithm = { name: 'PBKDF2', hash: 'SHA-512', iterations: SIGNING_SECRETS_DERIVATION_ITERATIONS, salt };
762
+ const algorithm = { name: 'PBKDF2', hash: 'SHA-512', iterations: this.#options.signingSecretsDerivationIterations ?? SIGNING_SECRETS_DERIVATION_ITERATIONS, salt };
751
763
  const [derivedTokenSigningSecret, derivedRefreshTokenSigningSecret, derivedSecretResetTokenSigningSecret] = await deriveBytesMultiple(algorithm, key, 3, SIGNING_SECRETS_LENGTH);
752
764
  this.derivedTokenSigningSecret = derivedTokenSigningSecret;
753
765
  this.derivedRefreshTokenSigningSecret = derivedRefreshTokenSigningSecret;
@@ -755,7 +767,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
755
767
  }
756
768
  async getHash(secret, salt) {
757
769
  const key = await importPbkdf2Key(secret);
758
- const hash = await globalThis.crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-512', iterations: HASH_ITERATIONS, salt }, key, HASH_LENGTH_BITS);
770
+ const hash = await globalThis.crypto.subtle.deriveBits({ name: 'PBKDF2', hash: 'SHA-512', iterations: this.#options.hashIterations ?? HASH_ITERATIONS, salt }, key, HASH_LENGTH_BITS);
759
771
  return new Uint8Array(hash);
760
772
  }
761
773
  };