@tstdl/base 0.93.89 → 0.93.91

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,52 @@ 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]));
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))]));
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]));
408
418
  }
409
419
  else {
410
- await firstValueFrom(race([timer(delay), this.disposeToken, this.forceRefreshToken]));
420
+ // If expired (or within buffer) and we didn't refresh (e.g. refresh failed or lock contention), wait a bit to avoid tight loop
421
+ await firstValueFrom(race([timer(5000), ...wakeUpSignals]));
411
422
  }
412
423
  }
413
424
  catch (error) {
414
425
  this.logger.error(error);
415
- await firstValueFrom(race([timer(5000), this.disposeToken, this.forceRefreshToken]));
426
+ await firstValueFrom(race([timer(5000), this.disposeToken, this.token$.pipe(filter((t) => t !== this.token()))]));
416
427
  }
417
428
  }
418
429
  }
@@ -454,7 +465,11 @@ let AuthenticationClientService = class AuthenticationClientService {
454
465
  }
455
466
  readFromStorage(key) {
456
467
  try {
457
- const serialized = localStorage?.getItem(key);
468
+ const storage = globalThis.localStorage;
469
+ if (isUndefined(storage) || (isNotFunction(storage.getItem))) {
470
+ return undefined;
471
+ }
472
+ const serialized = storage.getItem(key);
458
473
  if (isNullOrUndefined(serialized)) {
459
474
  return undefined;
460
475
  }
@@ -467,12 +482,16 @@ let AuthenticationClientService = class AuthenticationClientService {
467
482
  }
468
483
  writeToStorage(key, value) {
469
484
  try {
485
+ const storage = globalThis.localStorage;
486
+ if (isUndefined(storage) || (isNotFunction(storage.setItem)) || (isNotFunction(storage.removeItem))) {
487
+ return;
488
+ }
470
489
  if (isUndefined(value)) {
471
- localStorage?.removeItem(key);
490
+ storage.removeItem(key);
472
491
  }
473
492
  else {
474
493
  const serialized = JSON.stringify(value);
475
- localStorage?.setItem(key, serialized);
494
+ storage.setItem(key, serialized);
476
495
  }
477
496
  }
478
497
  catch (error) {
@@ -28,3 +28,4 @@ export declare class Subject extends TenantEntity {
28
28
  status: SubjectStatus;
29
29
  lastActivityTimestamp: Timestamp | null;
30
30
  }
31
+ export declare function getSubjectDisplayName(subject: Subject): string;
@@ -7,7 +7,9 @@ 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';
12
+ import { formatPersonName } from '../../formats/formats.js';
11
13
  import { TenantEntity } from '../../orm/entity.js';
12
14
  import { Inheritance, Table, Unique } from '../../orm/index.js';
13
15
  import { TimestampProperty } from '../../orm/schemas/timestamp.js';
@@ -55,3 +57,10 @@ Subject = __decorate([
55
57
  Unique(['id']) // for external systems that might not support composite identities
56
58
  ], Subject);
57
59
  export { Subject };
60
+ export function getSubjectDisplayName(subject) {
61
+ return match(subject.type)
62
+ .with(SubjectType.User, () => formatPersonName(subject))
63
+ .with(SubjectType.System, () => subject.displayName)
64
+ .with(SubjectType.ServiceAccount, () => subject.displayName)
65
+ .exhaustive();
66
+ }
@@ -1,13 +1,13 @@
1
1
  import type { LoadOptions } from '../../orm/repository.types.js';
2
2
  import { ServiceAccount, Subject, SystemAccount, User } from '../models/index.js';
3
- export type CreateUser = Pick<User, 'tenantId' | 'email' | 'firstName' | 'lastName'> & Partial<Pick<User, 'status'>>;
4
- export type CreateServiceAccount = Pick<ServiceAccount, 'tenantId' | 'displayName' | 'description' | 'parent'> & Partial<Pick<ServiceAccount, 'status'>>;
3
+ export type CreateUserData = Pick<User, 'tenantId' | 'email' | 'firstName' | 'lastName'> & Partial<Pick<User, 'status'>>;
4
+ export type CreateServiceAccountData = Pick<ServiceAccount, 'tenantId' | 'displayName' | 'description' | 'parent'> & Partial<Pick<ServiceAccount, 'status'>>;
5
5
  export declare class SubjectService {
6
6
  #private;
7
7
  getSubject(id: string, options?: LoadOptions<Subject>): Promise<Subject>;
8
8
  tryGetSubject(id: string, options?: LoadOptions<Subject>): Promise<Subject | undefined>;
9
9
  getSystemAccount(tenantId: string, identifier: string): Promise<SystemAccount>;
10
- createUser(data: CreateUser): Promise<User>;
10
+ createUser(data: CreateUserData): Promise<User>;
11
11
  updateUser(tenantId: string, userId: string, data: Partial<Pick<User, 'firstName' | 'lastName' | 'status'>>): Promise<void>;
12
12
  updateUserEmail(tenantId: string, userId: string, email: string): Promise<void>;
13
13
  getUser(tenantId: string, userId: string): Promise<User>;
@@ -16,7 +16,7 @@ export declare class SubjectService {
16
16
  hasUserByEmail(tenantId: string, email: string): Promise<boolean>;
17
17
  getUserBySubject(subject: Subject): Promise<User>;
18
18
  loadManyUsersByEmails(tenantId: string, emails: string[]): Promise<User[]>;
19
- createServiceAccount(data: CreateServiceAccount): Promise<ServiceAccount>;
19
+ createServiceAccount(data: CreateServiceAccountData): Promise<ServiceAccount>;
20
20
  updateServiceAccount(tenantId: string, serviceAccountId: string, data: Partial<Pick<ServiceAccount, 'description' | 'displayName' | 'status'>>): Promise<void>;
21
21
  getServiceAccount(tenantId: string, serviceAccountId: string): Promise<ServiceAccount>;
22
22
  getServiceAccountBySubject(subject: Subject): Promise<ServiceAccount>;
@@ -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.89",
3
+ "version": "0.93.91",
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
  }