@tstdl/base 0.93.90 → 0.93.92

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.
@@ -9,7 +9,6 @@ var __metadata = (this && this.__metadata) || function (k, v) {
9
9
  };
10
10
  import { Subject, filter, firstValueFrom, race, timer } from 'rxjs';
11
11
  import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
12
- import { isNode } from '../../environment.js';
13
12
  import { BadRequestError } from '../../errors/bad-request.error.js';
14
13
  import { ForbiddenError } from '../../errors/forbidden.error.js';
15
14
  import { InvalidTokenError } from '../../errors/invalid-token.error.js';
@@ -24,7 +23,7 @@ import { computed, signal, toObservable } from '../../signals/api.js';
24
23
  import { currentTimestampSeconds } from '../../utils/date-time.js';
25
24
  import { formatError } from '../../utils/format-error.js';
26
25
  import { timeout } from '../../utils/timing.js';
27
- import { assertDefinedPass, isDefined, isNullOrUndefined, isUndefined } from '../../utils/type-guards.js';
26
+ import { assertDefinedPass, isDefined, isNotFunction, isNullOrUndefined, isUndefined } from '../../utils/type-guards.js';
28
27
  import { millisecondsPerSecond } from '../../utils/units.js';
29
28
  import { AUTHENTICATION_API_CLIENT, INITIAL_AUTHENTICATION_DATA } from './tokens.js';
30
29
  const tokenStorageKey = 'AuthenticationService:token';
@@ -47,7 +46,6 @@ const unrecoverableErrors = [
47
46
  NotSupportedError,
48
47
  UnauthorizedError,
49
48
  ];
50
- const localStorage = isNode ? undefined : globalThis.localStorage;
51
49
  /**
52
50
  * Handles authentication on client side.
53
51
  *
@@ -380,39 +378,51 @@ let AuthenticationClientService = class AuthenticationClientService {
380
378
  try {
381
379
  const token = this.token();
382
380
  if (isUndefined(token)) {
383
- // Wait for login, dispose, or forced refresh
384
- await firstValueFrom(race([this.definedToken$, this.disposeToken, this.forceRefreshToken]));
381
+ // Wait for login or dispose.
382
+ // We ignore forceRefreshToken here because we can't refresh without a token.
383
+ await firstValueFrom(race([this.definedToken$, this.disposeToken]), { defaultValue: undefined });
385
384
  continue;
386
385
  }
387
386
  const now = this.estimatedServerTimestampSeconds();
388
- const needsRefresh = this.forceRefreshToken.isSet || (now >= (token.exp - refreshBufferSeconds));
387
+ const forceRefresh = this.forceRefreshToken.isSet;
388
+ const needsRefresh = forceRefresh || (now >= (token.exp - refreshBufferSeconds));
389
389
  if (needsRefresh) {
390
- // Only take the lock when we actually intend to refresh.
391
- // Using tryUse(undefined, ...) ensures we try once and don't block if another instance is already refreshing.
390
+ let lockAcquired = false;
392
391
  await this.lock.tryUse(undefined, async () => {
393
- // Re-check conditions inside the lock to avoid redundant refreshes if another instance just did it.
392
+ lockAcquired = true;
394
393
  const currentToken = this.token();
395
394
  const currentNow = this.estimatedServerTimestampSeconds();
396
395
  const stillNeedsRefresh = isDefined(currentToken) && (this.forceRefreshToken.isSet || (currentNow >= (currentToken.exp - refreshBufferSeconds)));
397
396
  if (stillNeedsRefresh) {
398
- this.forceRefreshToken.unset();
399
397
  await this.refresh();
398
+ this.forceRefreshToken.unset();
400
399
  }
401
400
  });
401
+ if (!lockAcquired) {
402
+ // Lock held by another instance, wait 5 seconds or until state/token changes.
403
+ // We ignore forceRefreshToken here to avoid a busy loop if it is already set.
404
+ await firstValueFrom(race([timer(5000), this.disposeToken, this.token$.pipe(filter((t) => t !== token))]), { defaultValue: undefined });
405
+ continue;
406
+ }
402
407
  }
403
408
  const delay = ((this.token()?.exp ?? 0) - this.estimatedServerTimestampSeconds() - refreshBufferSeconds) * millisecondsPerSecond;
404
- // Ensure delay is at least 0 to avoid tight loop, or wait longer if not logged in.
405
- // If not logged in after refresh attempt (e.g. session invalidated), we wait for login.
406
- if (isUndefined(this.token()) || (delay < 0)) {
407
- await firstValueFrom(race([this.definedToken$, this.disposeToken, this.forceRefreshToken, timer(5000)]));
409
+ const wakeUpSignals = [
410
+ this.disposeToken,
411
+ this.token$.pipe(filter((t) => t != token)),
412
+ ];
413
+ if (!forceRefresh) {
414
+ wakeUpSignals.push(this.forceRefreshToken);
415
+ }
416
+ if (delay > 0) {
417
+ await firstValueFrom(race([timer(delay), ...wakeUpSignals]), { defaultValue: undefined });
408
418
  }
409
419
  else {
410
- await firstValueFrom(race([timer(delay), this.disposeToken, this.forceRefreshToken]));
420
+ await firstValueFrom(race([timer(5000), ...wakeUpSignals]), { defaultValue: undefined });
411
421
  }
412
422
  }
413
423
  catch (error) {
414
424
  this.logger.error(error);
415
- await firstValueFrom(race([timer(5000), this.disposeToken, this.forceRefreshToken]));
425
+ await firstValueFrom(race([timer(5000), this.disposeToken, this.token$.pipe(filter((t) => t !== this.token()))]), { defaultValue: undefined });
416
426
  }
417
427
  }
418
428
  }
@@ -454,7 +464,11 @@ let AuthenticationClientService = class AuthenticationClientService {
454
464
  }
455
465
  readFromStorage(key) {
456
466
  try {
457
- const serialized = localStorage?.getItem(key);
467
+ const storage = globalThis.localStorage;
468
+ if (isUndefined(storage) || (isNotFunction(storage.getItem))) {
469
+ return undefined;
470
+ }
471
+ const serialized = storage.getItem(key);
458
472
  if (isNullOrUndefined(serialized)) {
459
473
  return undefined;
460
474
  }
@@ -467,12 +481,16 @@ let AuthenticationClientService = class AuthenticationClientService {
467
481
  }
468
482
  writeToStorage(key, value) {
469
483
  try {
484
+ const storage = globalThis.localStorage;
485
+ if (isUndefined(storage) || (isNotFunction(storage.setItem)) || (isNotFunction(storage.removeItem))) {
486
+ return;
487
+ }
470
488
  if (isUndefined(value)) {
471
- localStorage?.removeItem(key);
489
+ storage.removeItem(key);
472
490
  }
473
491
  else {
474
492
  const serialized = JSON.stringify(value);
475
- localStorage?.setItem(key, serialized);
493
+ storage.setItem(key, serialized);
476
494
  }
477
495
  }
478
496
  catch (error) {
@@ -7,13 +7,13 @@ 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 { match } from 'ts-pattern';
10
11
  import { defineEnum } from '../../enumeration/enumeration.js';
11
12
  import { formatPersonName } from '../../formats/formats.js';
12
13
  import { TenantEntity } from '../../orm/entity.js';
13
14
  import { Inheritance, Table, Unique } from '../../orm/index.js';
14
15
  import { TimestampProperty } from '../../orm/schemas/timestamp.js';
15
16
  import { Enumeration } from '../../schema/index.js';
16
- import { match } from 'ts-pattern';
17
17
  export const SubjectType = defineEnum('SubjectType', {
18
18
  System: 'system',
19
19
  User: 'user',
@@ -0,0 +1,131 @@
1
+ import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
2
+ import { Subject } from 'rxjs';
3
+ import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
4
+ import { AUTHENTICATION_API_CLIENT } from '../../authentication/client/tokens.js';
5
+ import { Lock } from '../../lock/index.js';
6
+ import { Logger } from '../../logger/index.js';
7
+ import { MessageBus } from '../../message-bus/index.js';
8
+ import { Injector } from '../../injector/index.js';
9
+ import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
10
+ import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
11
+ describe('AuthenticationClientService Methods', () => {
12
+ let injector;
13
+ let service;
14
+ let mockApiClient;
15
+ let mockLock;
16
+ let mockMessageBus;
17
+ let mockLogger;
18
+ beforeEach(() => {
19
+ const storage = new Map();
20
+ globalThis.localStorage = {
21
+ getItem: vi.fn((key) => storage.get(key) ?? null),
22
+ setItem: vi.fn((key, value) => storage.set(key, value)),
23
+ removeItem: vi.fn((key) => storage.delete(key)),
24
+ clear: vi.fn(() => storage.clear()),
25
+ };
26
+ configureDefaultSignalsImplementation();
27
+ injector = new Injector('TestInjector');
28
+ mockApiClient = {
29
+ login: vi.fn(),
30
+ refresh: vi.fn(),
31
+ impersonate: vi.fn(),
32
+ unimpersonate: vi.fn(),
33
+ changeSecret: vi.fn(),
34
+ initSecretReset: vi.fn(),
35
+ resetSecret: vi.fn(),
36
+ checkSecret: vi.fn(),
37
+ timestamp: vi.fn().mockResolvedValue(Math.floor(Date.now() / 1000)),
38
+ endSession: vi.fn().mockResolvedValue(undefined),
39
+ };
40
+ mockLock = {
41
+ tryUse: vi.fn(async (_timeout, callback) => {
42
+ const result = await callback({ lost: false });
43
+ return { success: true, result };
44
+ }),
45
+ use: vi.fn(async (_timeout, callback) => {
46
+ return await callback({ lost: false });
47
+ }),
48
+ };
49
+ mockMessageBus = {
50
+ publishAndForget: vi.fn(),
51
+ messages$: new Subject(),
52
+ dispose: vi.fn(),
53
+ };
54
+ mockLogger = {
55
+ error: vi.fn(),
56
+ warn: vi.fn(),
57
+ info: vi.fn(),
58
+ debug: vi.fn(),
59
+ };
60
+ injector.register(AUTHENTICATION_API_CLIENT, { useValue: mockApiClient });
61
+ injector.register(Lock, { useValue: mockLock });
62
+ injector.register(MessageBus, { useValue: mockMessageBus });
63
+ injector.register(Logger, { useValue: mockLogger });
64
+ const disposeToken = new CancellationToken();
65
+ injector.register(CancellationSignal, { useValue: disposeToken.signal });
66
+ service = injector.resolve(AuthenticationClientService);
67
+ });
68
+ afterEach(async () => {
69
+ await service.dispose();
70
+ });
71
+ test('impersonate should acquire lock and call api', async () => {
72
+ const token = { exp: Date.now() + 3600, jti: 'impersonated-token', subject: 'sub', impersonator: 'admin' };
73
+ mockApiClient.impersonate.mockResolvedValue(token);
74
+ await service.impersonate('target-subject');
75
+ expect(mockLock.use).toHaveBeenCalled();
76
+ expect(mockApiClient.impersonate).toHaveBeenCalledWith({ subject: 'target-subject', data: undefined });
77
+ expect(service.token()).toEqual(token);
78
+ expect(service.impersonated()).toBe(true);
79
+ });
80
+ test('impersonate should fail if already impersonating', async () => {
81
+ const token = { exp: Date.now() + 3600, jti: 'impersonated-token', subject: 'sub', impersonator: 'admin' };
82
+ service.setNewToken(token); // Force state
83
+ await expect(service.impersonate('another-target')).rejects.toThrow('Already impersonating');
84
+ });
85
+ test('unimpersonate should acquire lock and call api', async () => {
86
+ const token = { exp: Date.now() + 3600, jti: 'original-token', subject: 'admin' };
87
+ mockApiClient.unimpersonate.mockResolvedValue(token);
88
+ await service.unimpersonate();
89
+ expect(mockLock.use).toHaveBeenCalled();
90
+ expect(mockApiClient.unimpersonate).toHaveBeenCalled();
91
+ expect(service.token()).toEqual(token);
92
+ });
93
+ test('changeSecret should call api', async () => {
94
+ await service.changeSecret({ tenantId: 't', subject: 's' }, 'old', 'new');
95
+ expect(mockApiClient.changeSecret).toHaveBeenCalledWith({ tenantId: 't', subject: 's', currentSecret: 'old', newSecret: 'new' });
96
+ });
97
+ test('initResetSecret should call api', async () => {
98
+ await service.initResetSecret({ tenantId: 't', subject: 's' }, { some: 'data' });
99
+ expect(mockApiClient.initSecretReset).toHaveBeenCalledWith({ tenantId: 't', subject: 's', data: { some: 'data' } });
100
+ });
101
+ test('resetSecret should call api', async () => {
102
+ await service.resetSecret('token', 'new-secret');
103
+ expect(mockApiClient.resetSecret).toHaveBeenCalledWith({ token: 'token', newSecret: 'new-secret' });
104
+ });
105
+ test('updateRawTokens should update signals and storage', async () => {
106
+ service.updateRawTokens('raw', 'refresh', 'impersonator');
107
+ expect(service.rawToken()).toBe('raw');
108
+ expect(service.rawRefreshToken()).toBe('refresh');
109
+ expect(service.rawImpersonatorRefreshToken()).toBe('impersonator');
110
+ expect(globalThis.localStorage.setItem).toHaveBeenCalledWith('AuthenticationService:raw-token', JSON.stringify('raw'));
111
+ });
112
+ test('impersonate should rollback data on failure', async () => {
113
+ service.setAdditionalData({ role: 'admin' });
114
+ const originalData = service.authenticationData;
115
+ mockApiClient.impersonate.mockRejectedValue(new Error('Impersonation failed'));
116
+ await expect(service.impersonate('target')).rejects.toThrow('Impersonation failed');
117
+ // Should have restored original data
118
+ expect(service.authenticationData).toEqual(originalData);
119
+ expect(service.impersonatorAuthenticationData).toBeUndefined();
120
+ });
121
+ test('unimpersonate should handle failure', async () => {
122
+ mockApiClient.unimpersonate.mockRejectedValue(new Error('Unimpersonation failed'));
123
+ await expect(service.unimpersonate()).rejects.toThrow('Unimpersonation failed');
124
+ });
125
+ test('syncClock should handle errors gracefully', async () => {
126
+ mockApiClient.timestamp.mockRejectedValue(new Error('Time sync failed'));
127
+ await service.syncClock();
128
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Failed to synchronize clock'));
129
+ expect(service.clockOffset).toBe(0);
130
+ });
131
+ });
@@ -0,0 +1,124 @@
1
+ import { Subject } from 'rxjs';
2
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
3
+ import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
4
+ import { AUTHENTICATION_API_CLIENT } from '../../authentication/client/tokens.js';
5
+ import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
6
+ import { Injector } from '../../injector/index.js';
7
+ import { Lock } from '../../lock/index.js';
8
+ import { Logger } from '../../logger/index.js';
9
+ import { MessageBus } from '../../message-bus/index.js';
10
+ import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
11
+ import { timeout } from '../../utils/timing.js';
12
+ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
13
+ let injector;
14
+ let service;
15
+ let mockApiClient;
16
+ let mockLock;
17
+ let mockMessageBus;
18
+ let mockLogger;
19
+ beforeEach(() => {
20
+ const storage = new Map();
21
+ globalThis.localStorage = {
22
+ getItem: vi.fn((key) => storage.get(key) ?? null),
23
+ setItem: vi.fn((key, value) => storage.set(key, value)),
24
+ removeItem: vi.fn((key) => storage.delete(key)),
25
+ clear: vi.fn(() => storage.clear()),
26
+ };
27
+ configureDefaultSignalsImplementation();
28
+ injector = new Injector('Test');
29
+ mockApiClient = {
30
+ login: vi.fn(),
31
+ refresh: vi.fn(),
32
+ timestamp: vi.fn().mockResolvedValue(Math.floor(Date.now() / 1000)),
33
+ endSession: vi.fn().mockResolvedValue(undefined),
34
+ };
35
+ mockLock = {
36
+ tryUse: vi.fn(async (_timeout, callback) => {
37
+ const result = await callback({ lost: false });
38
+ return { success: true, result };
39
+ }),
40
+ use: vi.fn(async (_timeout, callback) => {
41
+ return await callback({ lost: false });
42
+ }),
43
+ };
44
+ mockMessageBus = {
45
+ publishAndForget: vi.fn(),
46
+ messages$: new Subject(),
47
+ dispose: vi.fn(),
48
+ };
49
+ mockLogger = {
50
+ error: vi.fn(),
51
+ warn: vi.fn(),
52
+ info: vi.fn(),
53
+ debug: vi.fn(),
54
+ };
55
+ injector.register(AUTHENTICATION_API_CLIENT, { useValue: mockApiClient });
56
+ injector.register(Lock, { useValue: mockLock });
57
+ injector.register(MessageBus, { useValue: mockMessageBus });
58
+ injector.register(Logger, { useValue: mockLogger });
59
+ const disposeToken = new CancellationToken();
60
+ injector.register(CancellationSignal, { useValue: disposeToken.signal });
61
+ });
62
+ afterEach(async () => {
63
+ await service.dispose();
64
+ });
65
+ test('Zombie Timer: loop should wake up immediately when token changes', async () => {
66
+ // 1. Mock a long expiration
67
+ const now = Math.floor(Date.now() / 1000);
68
+ const initialToken = { exp: now + 3600, jti: 'initial' };
69
+ // Set in storage so initialize() (called by resolve) loads it
70
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
71
+ service = injector.resolve(AuthenticationClientService);
72
+ // Wait for loop to enter the race condition (wait phase)
73
+ await timeout(100);
74
+ // 2. Change token
75
+ const newToken = { exp: now + 3600, jti: 'new' };
76
+ mockApiClient.refresh.mockResolvedValue(newToken);
77
+ service.requestRefresh(); // This should trigger immediate wake up
78
+ // Wait for loop to process
79
+ await timeout(100);
80
+ expect(mockApiClient.refresh).toHaveBeenCalled();
81
+ });
82
+ test('Forced Refresh Loss: forceRefreshToken should not be cleared on failure', async () => {
83
+ const now = Math.floor(Date.now() / 1000);
84
+ const initialToken = { exp: now + 3600, jti: 'initial' };
85
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
86
+ service = injector.resolve(AuthenticationClientService);
87
+ await timeout(100);
88
+ // 1. Mock refresh failure
89
+ mockApiClient.refresh.mockRejectedValue(new Error('Network Error'));
90
+ service.requestRefresh();
91
+ // Wait for loop to attempt refresh and fail
92
+ await timeout(200);
93
+ expect(mockApiClient.refresh).toHaveBeenCalled();
94
+ expect(service.forceRefreshToken.isSet).toBe(true); // Should STILL be set
95
+ });
96
+ test('Lock Contention Backoff: should wait 5 seconds and not busy-loop', async () => {
97
+ const now = Math.floor(Date.now() / 1000);
98
+ const initialToken = { exp: now + 5, jti: 'initial' }; // Expiring soon
99
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
100
+ // 1. Mock lock already held
101
+ mockLock.tryUse.mockResolvedValue(undefined); // lockAcquired = false
102
+ const startTime = Date.now();
103
+ service = injector.resolve(AuthenticationClientService);
104
+ // We expect it to try once, fail to get lock, and then wait 5 seconds.
105
+ await timeout(300);
106
+ expect(mockLock.tryUse).toHaveBeenCalledTimes(1);
107
+ // Check if it's still waiting (not finished loop)
108
+ const duration = Date.now() - startTime;
109
+ expect(duration).toBeLessThan(1000);
110
+ });
111
+ test('Busy Loop: should not busy loop when forceRefreshToken is set and lock is held', async () => {
112
+ const now = Math.floor(Date.now() / 1000);
113
+ const initialToken = { exp: now + 3600, jti: 'initial' };
114
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
115
+ // Mock lock already held
116
+ mockLock.tryUse.mockResolvedValue(undefined);
117
+ service = injector.resolve(AuthenticationClientService);
118
+ await timeout(100);
119
+ service.requestRefresh(); // Set the flag
120
+ await timeout(300);
121
+ // If it busy loops, this will be much higher than 1.
122
+ expect(mockLock.tryUse.mock.calls.length).toBeLessThan(5);
123
+ });
124
+ });
@@ -7,8 +7,14 @@ export type HttpServerRequestContext<Context = unknown> = {
7
7
  };
8
8
  export declare abstract class HttpServer<Context = unknown> implements AsyncIterable<HttpServerRequestContext<Context>>, AsyncDisposable {
9
9
  abstract readonly connectedSocketsCount: number;
10
- abstract readonly port: number;
11
- abstract readonly address: string;
10
+ /**
11
+ * The port the server is listening on, or `null` if not listening.
12
+ */
13
+ abstract readonly port: number | null;
14
+ /**
15
+ * The address the server is listening on, or `null` if not listening.
16
+ */
17
+ abstract readonly address: string | null;
12
18
  abstract listen(port: number): Promise<void>;
13
19
  abstract close(timeout: number): Promise<void>;
14
20
  abstract [Symbol.asyncIterator](): AsyncIterator<HttpServerRequestContext<Context>>;
@@ -9,8 +9,8 @@ export declare class NodeHttpServer extends HttpServer<NodeHttpServerContext> im
9
9
  #private;
10
10
  private untrackConnectedSockets?;
11
11
  get connectedSocketsCount(): number;
12
- get port(): number;
13
- get address(): string;
12
+ get port(): number | null;
13
+ get address(): string | null;
14
14
  [afterResolve](): void;
15
15
  [Symbol.asyncDispose](): Promise<void>;
16
16
  listen(port: number): Promise<void>;
@@ -35,10 +35,10 @@ let NodeHttpServer = NodeHttpServer_1 = class NodeHttpServer extends HttpServer
35
35
  return this.#sockets.size;
36
36
  }
37
37
  get port() {
38
- return this.#httpServer.address().port;
38
+ return this.#httpServer.address()?.port ?? null;
39
39
  }
40
40
  get address() {
41
- return this.#httpServer.address().address;
41
+ return this.#httpServer.address()?.address ?? null;
42
42
  }
43
43
  [afterResolve]() {
44
44
  this.#httpServer.on('request', (request, response) => this.#requestIterable.feed({ request, response }));
@@ -3,7 +3,7 @@ import { Injector } from '../../injector/injector.js';
3
3
  import type { resolveArgumentType } from '../../injector/interfaces.js';
4
4
  import { Module } from '../module.js';
5
5
  export declare class WebServerModuleConfiguration {
6
- port: number;
6
+ port?: number;
7
7
  }
8
8
  export declare class WebServerModule extends Module {
9
9
  private readonly config;
@@ -37,7 +37,7 @@ let WebServerModule = class WebServerModule extends Module {
37
37
  }
38
38
  async _run(cancellationSignal) {
39
39
  this.initialize();
40
- await this.httpServer.listen(this.config.port);
40
+ await this.httpServer.listen(this.config.port ?? 8000);
41
41
  const closePromise = cancellationSignal.$set.then(async () => {
42
42
  await this.httpServer[Symbol.asyncDispose]();
43
43
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.90",
3
+ "version": "0.93.92",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -21,7 +21,7 @@ import { configureDefaultSignalsImplementation } from '../signals/implementation
21
21
  import { configurePostgresTaskQueue, migratePostgresTaskQueueSchema } from '../task-queue/postgres/index.js';
22
22
  import * as configParser from '../utils/config-parser.js';
23
23
  import { objectEntries } from '../utils/object/object.js';
24
- import { isDefined } from '../utils/type-guards.js';
24
+ import { isDefined, isNotNull } from '../utils/type-guards.js';
25
25
  /**
26
26
  * Standard setup for integration tests.
27
27
  */
@@ -109,7 +109,7 @@ export async function setupIntegrationTest(options = {}) {
109
109
  authenticationApiClient: AuthenticationApiClient,
110
110
  registerMiddleware: true,
111
111
  }, injector);
112
- if (options.modules?.webServer ?? options.modules?.api ?? options.modules?.authentication) {
112
+ if (options.modules.webServer ?? options.modules.api ?? options.modules.authentication) {
113
113
  const port = options.api?.port ?? 0;
114
114
  configureWebServerModule({ port, injector });
115
115
  const webServerModule = await injector.resolveAsync(WebServerModule);
@@ -118,7 +118,7 @@ export async function setupIntegrationTest(options = {}) {
118
118
  // Wait for server to be listening
119
119
  while (true) {
120
120
  try {
121
- if (isDefined(httpServer.port)) {
121
+ if (isNotNull(httpServer.port)) {
122
122
  break;
123
123
  }
124
124
  }