@tstdl/base 0.93.139 → 0.93.140

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 (133) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.js +2 -2
  28. package/audit/README.md +267 -0
  29. package/authentication/README.md +288 -0
  30. package/authentication/client/authentication.service.d.ts +12 -11
  31. package/authentication/client/authentication.service.js +21 -21
  32. package/authentication/client/http-client.middleware.js +2 -2
  33. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  34. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  35. package/browser/README.md +401 -0
  36. package/cancellation/README.md +156 -0
  37. package/cancellation/tests/coverage.test.d.ts +1 -0
  38. package/cancellation/tests/coverage.test.js +49 -0
  39. package/cancellation/tests/leak.test.js +24 -29
  40. package/cancellation/tests/token.test.d.ts +1 -0
  41. package/cancellation/tests/token.test.js +136 -0
  42. package/cancellation/token.d.ts +53 -177
  43. package/cancellation/token.js +132 -208
  44. package/context/README.md +174 -0
  45. package/cookie/README.md +161 -0
  46. package/css/README.md +157 -0
  47. package/data-structures/README.md +320 -0
  48. package/decorators/README.md +140 -0
  49. package/distributed-loop/README.md +231 -0
  50. package/distributed-loop/distributed-loop.js +1 -1
  51. package/document-management/README.md +403 -0
  52. package/document-management/server/services/document-management.service.js +9 -7
  53. package/document-management/tests/document-management-core.test.js +2 -7
  54. package/document-management/tests/document-management.api.test.js +6 -7
  55. package/document-management/tests/document-statistics.service.test.js +11 -12
  56. package/document-management/tests/document.service.test.js +3 -3
  57. package/document-management/tests/enum-helpers.test.js +2 -3
  58. package/dom/README.md +213 -0
  59. package/enumerable/README.md +259 -0
  60. package/enumeration/README.md +121 -0
  61. package/errors/README.md +267 -0
  62. package/file/README.md +191 -0
  63. package/formats/README.md +210 -0
  64. package/function/README.md +144 -0
  65. package/http/README.md +318 -0
  66. package/http/client/adapters/undici.adapter.js +1 -1
  67. package/http/client/http-client-request.d.ts +6 -5
  68. package/http/client/http-client-request.js +8 -9
  69. package/http/server/node/node-http-server.js +1 -2
  70. package/image-service/README.md +137 -0
  71. package/injector/README.md +491 -0
  72. package/intl/README.md +113 -0
  73. package/json-path/README.md +182 -0
  74. package/jsx/README.md +154 -0
  75. package/key-value-store/README.md +191 -0
  76. package/lock/README.md +249 -0
  77. package/lock/web/web-lock.js +119 -47
  78. package/logger/README.md +287 -0
  79. package/mail/README.md +256 -0
  80. package/memory/README.md +144 -0
  81. package/message-bus/README.md +244 -0
  82. package/message-bus/message-bus-base.js +1 -1
  83. package/module/README.md +182 -0
  84. package/module/module.d.ts +1 -1
  85. package/module/module.js +77 -17
  86. package/module/modules/web-server.module.js +1 -1
  87. package/notification/tests/notification-type.service.test.js +24 -15
  88. package/object-storage/README.md +300 -0
  89. package/openid-connect/README.md +274 -0
  90. package/orm/README.md +423 -0
  91. package/package.json +8 -6
  92. package/password/README.md +164 -0
  93. package/pdf/README.md +246 -0
  94. package/polyfills.js +1 -0
  95. package/pool/README.md +198 -0
  96. package/process/README.md +237 -0
  97. package/promise/README.md +252 -0
  98. package/promise/cancelable-promise.js +1 -1
  99. package/random/README.md +193 -0
  100. package/reflection/README.md +305 -0
  101. package/rpc/README.md +386 -0
  102. package/rxjs-utils/README.md +262 -0
  103. package/schema/README.md +342 -0
  104. package/serializer/README.md +342 -0
  105. package/signals/implementation/README.md +134 -0
  106. package/sse/README.md +278 -0
  107. package/task-queue/README.md +300 -0
  108. package/task-queue/postgres/task-queue.d.ts +2 -1
  109. package/task-queue/postgres/task-queue.js +32 -2
  110. package/task-queue/task-context.js +1 -1
  111. package/task-queue/task-queue.d.ts +17 -0
  112. package/task-queue/task-queue.js +103 -45
  113. package/task-queue/tests/complex.test.js +4 -4
  114. package/task-queue/tests/dependencies.test.js +4 -2
  115. package/task-queue/tests/queue.test.js +111 -0
  116. package/task-queue/tests/worker.test.js +21 -13
  117. package/templates/README.md +287 -0
  118. package/testing/README.md +157 -0
  119. package/text/README.md +346 -0
  120. package/threading/README.md +238 -0
  121. package/types/README.md +311 -0
  122. package/utils/README.md +322 -0
  123. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  124. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  125. package/utils/async-iterable-helpers/take-until.js +4 -4
  126. package/utils/backoff.js +89 -30
  127. package/utils/retry-with-backoff.js +1 -1
  128. package/utils/timer.d.ts +1 -1
  129. package/utils/timer.js +5 -7
  130. package/utils/timing.d.ts +1 -1
  131. package/utils/timing.js +2 -4
  132. package/utils/z-base32.d.ts +1 -0
  133. package/utils/z-base32.js +1 -0
@@ -0,0 +1,288 @@
1
+ # @tstdl/base/authentication
2
+
3
+ A comprehensive, secure, and type-safe authentication module providing JWT-based session management, credential handling, and extensible hooks for both server and client environments.
4
+
5
+ ## Table of Contents
6
+
7
+ - [✨ Features](#-features)
8
+ - [Core Concepts](#core-concepts)
9
+ - [Subject System](#subject-system)
10
+ - [Server-Side Architecture](#server-side-architecture)
11
+ - [Client-Side Architecture](#client-side-architecture)
12
+ - [Extensibility Hooks](#extensibility-hooks)
13
+ - [🚀 Basic Usage](#-basic-usage)
14
+ - [Server Setup](#server-setup)
15
+ - [Client Setup](#client-setup)
16
+ - [🔧 Advanced Topics](#-advanced-topics)
17
+ - [Custom Token Payloads & Authentication Data](#custom-token-payloads--authentication-data)
18
+ - [Impersonation](#impersonation)
19
+ - [Secret Validation](#secret-validation)
20
+ - [HTTP Client Middleware](#http-client-middleware)
21
+ - [📚 API](#-api)
22
+
23
+ ## ✨ Features
24
+
25
+ - **Full-Stack Solution**: Provides synchronized services for both Node.js servers and browser clients.
26
+ - **Secure by Design**: Uses PBKDF2 for password hashing, secure random salts, and timing-safe comparisons.
27
+ - **JWT Sessions**: Implements access tokens (short-lived) and refresh tokens (long-lived, database-backed) with automatic rotation.
28
+ - **Reactive Client State**: The client service exposes authentication state (token, session, subject) via Signals and RxJS Observables.
29
+ - **Impersonation**: Built-in support for administrators to securely log in as other users.
30
+ - **Subject Diversity**: Supports `User`, `ServiceAccount`, and `SystemAccount` types out of the box.
31
+ - **Secret Management**: Includes flows for changing passwords and secure, token-based password resets.
32
+ - **Audit Logging**: Automatically logs security events (login success/failure, password changes) via the `@tstdl/base/audit` system.
33
+ - **Extensible**: Abstract classes allow you to inject custom logic for subject resolution, token payload enrichment, and permission checks.
34
+
35
+ ## Core Concepts
36
+
37
+ ### Subject System
38
+
39
+ The module uses a polymorphic `Subject` system to represent different types of entities that can authenticate:
40
+
41
+ - **User**: A human user belonging to a tenant.
42
+ - **ServiceAccount**: A non-human account, typically for API integrations.
43
+ - **SystemAccount**: A built-in system account for internal tasks.
44
+
45
+ ### Server-Side Architecture
46
+
47
+ The server component revolves around the `AuthenticationService`. It handles:
48
+
49
+ - **Credential Storage**: Manages the `AuthenticationCredentials` entity (subject, hash, salt).
50
+ - **Session Tracking**: Manages the `AuthenticationSession` entity, allowing for server-side session revocation.
51
+ - **Token Issuance**: Generates signed JWTs using configured secrets.
52
+ - **Validation**: Verifies tokens and handles the refresh flow.
53
+
54
+ ### Client-Side Architecture
55
+
56
+ The `AuthenticationClientService` manages the lifecycle in the browser:
57
+
58
+ - **Storage**: Persists tokens securely in `localStorage`.
59
+ - **Auto-Refresh**: Automatically refreshes the access token before expiration using a synchronized lock mechanism (works across tabs).
60
+ - **State**: Provides reactive signals like `isLoggedIn`, `token`, and `subjectId`.
61
+
62
+ ### Extensibility Hooks
63
+
64
+ To integrate this module with your application, implement the `AuthenticationAncillaryService`. This service bridges generic authentication logic with your domain-specific subjects (Users, etc.).
65
+
66
+ ## 🚀 Basic Usage
67
+
68
+ ### Server Setup
69
+
70
+ 1. **Implement the Ancillary Service**
71
+ Create a service to resolve subjects and define token payloads.
72
+
73
+ ```typescript
74
+ import { AuthenticationAncillaryService, type GetTokenPayloadContext } from '@tstdl/base/authentication/server';
75
+ import { Subject, type SubjectInput, User } from '@tstdl/base/authentication';
76
+ import { Singleton, inject } from '@tstdl/base/injector';
77
+ import { UserService } from './user.service.js';
78
+
79
+ @Singleton()
80
+ export class AppAuthenticationAncillaryService extends AuthenticationAncillaryService {
81
+ readonly #userService = inject(UserService);
82
+
83
+ // Resolve a login identifier (e.g., email) to actual Subject entities
84
+ override async resolveSubjects({ tenantId, subject }: SubjectInput): Promise<Subject[]> {
85
+ const user = await this.#userService.findByEmail(subject, tenantId);
86
+ return user ? [user] : [];
87
+ }
88
+
89
+ // Add custom data to the JWT payload
90
+ override async getTokenPayload(subject: Subject, _data: void, _context: GetTokenPayloadContext): Promise<Record<string, unknown>> {
91
+ // You can check the type and add specific data
92
+ if (subject instanceof User) {
93
+ return { role: subject.role };
94
+ }
95
+
96
+ return {};
97
+ }
98
+
99
+ // Handle secret reset (e.g., send email)
100
+ override async handleInitSecretReset(data: any): Promise<void> {
101
+ console.log(`Send reset email to ${data.subject.id} with token ${data.token}`);
102
+ }
103
+
104
+ // Define impersonation rules
105
+ override async canImpersonate(token: any, subject: Subject): Promise<boolean> {
106
+ return token.role == 'admin';
107
+ }
108
+ }
109
+ ```
110
+
111
+ 2. **Configure the Server**
112
+ Register the module in your application bootstrap.
113
+
114
+ ```typescript
115
+ import { configureAuthenticationServer, migrateAuthenticationSchema } from '@tstdl/base/authentication/server';
116
+ import { AppAuthenticationAncillaryService } from './app-authentication-ancillary.service.js';
117
+
118
+ // Run migrations for authentication tables
119
+ await migrateAuthenticationSchema();
120
+
121
+ configureAuthenticationServer({
122
+ authenticationAncillaryService: AppAuthenticationAncillaryService,
123
+ serviceOptions: {
124
+ // In production, load these from environment variables!
125
+ secret: 'super-secure-random-string-for-signing-tokens-at-least-64-chars',
126
+ tokenTimeToLive: 15 * 60 * 1000, // 15 minutes
127
+ },
128
+ });
129
+ ```
130
+
131
+ ### Client Setup
132
+
133
+ 1. **Configure the Client**
134
+ Register the client module.
135
+
136
+ ```typescript
137
+ import { configureAuthenticationClient } from '@tstdl/base/authentication';
138
+
139
+ configureAuthenticationClient({
140
+ registerMiddleware: true, // Automatically attaches tokens to outgoing requests
141
+ });
142
+ ```
143
+
144
+ 2. **Use the Service**
145
+ Inject `AuthenticationClientService` to manage login state.
146
+
147
+ ```typescript
148
+ import { AuthenticationClientService } from '@tstdl/base/authentication';
149
+ import { inject } from '@tstdl/base/injector';
150
+ import { effect } from '@tstdl/base/signals';
151
+
152
+ const authService = inject(AuthenticationClientService);
153
+
154
+ // React to state changes using Signals
155
+ effect(() => {
156
+ if (authService.isLoggedIn()) {
157
+ console.log(`User logged in with ID: ${authService.subjectId()}`);
158
+ } else {
159
+ console.log('User is guest');
160
+ }
161
+ });
162
+
163
+ // Perform login
164
+ async function login(email: string, pass: string) {
165
+ try {
166
+ await authService.login(email, pass);
167
+ } catch (error) {
168
+ console.error('Login failed', error);
169
+ }
170
+ }
171
+ ```
172
+
173
+ ## 🔧 Advanced Topics
174
+
175
+ ### Custom Token Payloads & Authentication Data
176
+
177
+ You can strongly type the extra data in your JWTs and the data passed during login.
178
+
179
+ ```typescript
180
+ import { Subject } from '@tstdl/base/authentication';
181
+
182
+ // Define types
183
+ type MyTokenPayload = { role: 'admin' | 'user'; tenantId: string };
184
+ type MyAuthData = { tenantId: string }; // Data sent from client during login
185
+
186
+ // Server: Extend Ancillary Service
187
+ class MyAncillary extends AuthenticationAncillaryService<MyTokenPayload, MyAuthData> {
188
+ override async getTokenPayload(subject: Subject, data: MyAuthData): Promise<MyTokenPayload> {
189
+ // ... load data using subject.id or subject entity directly
190
+ return { role: 'admin', tenantId: data.tenantId };
191
+ }
192
+ }
193
+
194
+ // Client: Inject with generics
195
+ const authService = inject<AuthenticationClientService<MyTokenPayload, MyAuthData>>(AuthenticationClientService);
196
+
197
+ // Now typed:
198
+ await authService.login('user@example.com', 'password', { tenantId: '123' });
199
+ const role = authService.token()?.role; // Typed as 'admin' | 'user'
200
+ ```
201
+
202
+ ### Impersonation
203
+
204
+ Administrators can log in as other users without knowing their password. This creates a nested session where the original admin session is preserved.
205
+
206
+ ```typescript
207
+ // Client
208
+ const targetSubjectId = '...';
209
+ await authService.impersonate(targetSubjectId);
210
+
211
+ // Check status
212
+ if (authService.impersonated()) {
213
+ console.log(`Impersonating ${authService.subjectId()} by ${authService.impersonator()}`);
214
+ }
215
+
216
+ // Return to admin session
217
+ await authService.unimpersonate();
218
+ ```
219
+
220
+ ### Secret Validation
221
+
222
+ By default, the module checks for password strength and known data breaches (pwned passwords). You can override this by implementing `AuthenticationSecretRequirementsValidator`.
223
+
224
+ ```typescript
225
+ import { AuthenticationSecretRequirementsValidator, type SecretCheckResult, type SecretTestResult } from '@tstdl/base/authentication/server';
226
+ import { Singleton } from '@tstdl/base/injector';
227
+
228
+ @Singleton({ alias: AuthenticationSecretRequirementsValidator })
229
+ export class MySecretValidator extends AuthenticationSecretRequirementsValidator {
230
+ override async checkSecretRequirements(secret: string): Promise<SecretCheckResult> {
231
+ // Custom logic (e.g. using zxcvbn or similar)
232
+ return { strength: 4, pwned: 0, warnings: [], suggestions: [] };
233
+ }
234
+
235
+ override async testSecretRequirements(secret: string): Promise<SecretTestResult> {
236
+ if (secret.length < 10) {
237
+ return { success: false, reason: 'Too short' };
238
+ }
239
+ return { success: true };
240
+ }
241
+ }
242
+ ```
243
+
244
+ ### HTTP Client Middleware
245
+
246
+ If `registerMiddleware: true` is passed to `configureAuthenticationClient`, the `waitForAuthenticationCredentialsMiddleware` is registered.
247
+
248
+ This middleware intercepts all outgoing HTTP requests made via `@tstdl/base/http`. If the request endpoint requires credentials (as defined in the API definition), the middleware will:
249
+
250
+ 1. Check if a valid token exists.
251
+ 2. If the token is expired or missing, it waits for the `AuthenticationClientService` to refresh or acquire a token.
252
+ 3. Once valid, the request proceeds.
253
+
254
+ ## 📚 API
255
+
256
+ ### Server-Side (`@tstdl/base/authentication/server`)
257
+
258
+ | Class/Function | Description |
259
+ | :------------------------------------------ | :------------------------------------------------------------------------------------ |
260
+ | `AuthenticationService` | Main service for credential verification, token issuance, and session management. |
261
+ | `AuthenticationAncillaryService` | Abstract class for hooks (resolve subjects, payload generation, impersonation checks).|
262
+ | `AuthenticationSecretRequirementsValidator` | Abstract class for validating password strength/requirements. |
263
+ | `configureAuthenticationServer` | Configures the server module (secrets, options, ancillary service). |
264
+ | `migrateAuthenticationSchema` | Runs database migrations for authentication tables. |
265
+ | `AuthenticationApiController` | The API controller implementation (automatically registered). |
266
+ | `SubjectService` | Service for managing subjects (User, ServiceAccount, SystemAccount). |
267
+
268
+ ### Client-Side (`@tstdl/base/authentication`)
269
+
270
+ | Class/Function | Description |
271
+ | :------------------------------------------- | :---------------------------------------------------------------------- |
272
+ | `AuthenticationClientService` | Main client service. Handles login, logout, refresh, and state signals. |
273
+ | `configureAuthenticationClient` | Configures the client module and optional middleware. |
274
+ | `waitForAuthenticationCredentialsMiddleware` | HTTP middleware that pauses requests until a valid token is available. |
275
+
276
+ ### Models & Types (`@tstdl/base/authentication`)
277
+
278
+ | Type/Class | Description |
279
+ | :-------------------------- | :------------------------------------------ |
280
+ | `Subject` | Base entity for Users, ServiceAccounts, etc.|
281
+ | `User` | Subject type representing a person. |
282
+ | `ServiceAccount` | Subject type representing a non-human user. |
283
+ | `SystemAccount` | Subject type representing a system user. |
284
+ | `TokenPayload` | The structure of the JWT payload. |
285
+ | `InitSecretResetData` | Data required to initiate a password reset. |
286
+ | `SecretCheckResult` | Result of a password strength check. |
287
+ | `AuthenticationCredentials` | Database entity for user credentials. |
288
+ | `AuthenticationSession` | Database entity for active sessions. |
@@ -1,3 +1,4 @@
1
+ import { type Observable } from 'rxjs';
1
2
  import type { AfterResolve } from '../../injector/index.js';
2
3
  import { afterResolve } from '../../injector/index.js';
3
4
  import type { Record } from '../../types/index.js';
@@ -22,10 +23,10 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
22
23
  private readonly errorSubject;
23
24
  private readonly tokenUpdateBus;
24
25
  private readonly loggedOutBus;
25
- private readonly forceRefreshToken;
26
26
  private readonly lock;
27
27
  private readonly logger;
28
28
  private readonly disposeSignal;
29
+ private readonly forceRefreshRequested;
29
30
  private clockOffset;
30
31
  private initialized;
31
32
  private refreshLoopPromise;
@@ -33,7 +34,7 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
33
34
  * Observable for authentication errors.
34
35
  * Emits when a refresh fails.
35
36
  */
36
- readonly error$: import("rxjs").Observable<Error>;
37
+ readonly error$: Observable<Error>;
37
38
  /** Current token */
38
39
  readonly token: import("../../signals/api.js").WritableSignal<TokenPayload<AdditionalTokenPayload> | undefined>;
39
40
  /** Current raw token */
@@ -55,23 +56,23 @@ export declare class AuthenticationClientService<AdditionalTokenPayload extends
55
56
  /** Whether the user is impersonated */
56
57
  readonly impersonated: import("../../signals/api.js").Signal<boolean>;
57
58
  /** Current token */
58
- readonly token$: import("rxjs").Observable<TokenPayload<AdditionalTokenPayload> | undefined>;
59
+ readonly token$: Observable<TokenPayload<AdditionalTokenPayload> | undefined>;
59
60
  /** Emits when token is available (not undefined) */
60
- readonly definedToken$: import("rxjs").Observable<Exclude<TokenPayload<AdditionalTokenPayload>, void | undefined>>;
61
+ readonly definedToken$: Observable<Exclude<TokenPayload<AdditionalTokenPayload>, void | undefined>>;
61
62
  /** Emits when a valid token is available (not undefined and not expired) */
62
- readonly validToken$: import("rxjs").Observable<Exclude<TokenPayload<AdditionalTokenPayload>, void | undefined>>;
63
+ readonly validToken$: Observable<Exclude<TokenPayload<AdditionalTokenPayload>, void | undefined>>;
63
64
  /** Current subject */
64
- readonly subjectId$: import("rxjs").Observable<string | undefined>;
65
+ readonly subjectId$: Observable<string | undefined>;
65
66
  /** Emits when subject is available */
66
- readonly definedSubjectId$: import("rxjs").Observable<string>;
67
+ readonly definedSubjectId$: Observable<string>;
67
68
  /** Current session id */
68
- readonly sessionId$: import("rxjs").Observable<string | undefined>;
69
+ readonly sessionId$: Observable<string | undefined>;
69
70
  /** Emits when session id is available */
70
- readonly definedSessionId$: import("rxjs").Observable<string>;
71
+ readonly definedSessionId$: Observable<string>;
71
72
  /** Whether the user is logged in */
72
- readonly isLoggedIn$: import("rxjs").Observable<boolean>;
73
+ readonly isLoggedIn$: Observable<boolean>;
73
74
  /** Emits when the user logs out */
74
- readonly loggedOut$: import("rxjs").Observable<void>;
75
+ readonly loggedOut$: Observable<void>;
75
76
  private get authenticationData();
76
77
  private set authenticationData(value);
77
78
  private get impersonatorAuthenticationData();
@@ -7,8 +7,8 @@ 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, skip, takeUntil, timer } from 'rxjs';
11
- import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
10
+ import { Subject, filter, firstValueFrom, from, map, race, skip, takeUntil, timer } from 'rxjs';
11
+ import { CancellationSignal } from '../../cancellation/token.js';
12
12
  import { BadRequestError } from '../../errors/bad-request.error.js';
13
13
  import { ForbiddenError } from '../../errors/forbidden.error.js';
14
14
  import { formatError } from '../../errors/index.js';
@@ -25,7 +25,7 @@ import { computed, signal, toObservable } from '../../signals/api.js';
25
25
  import { currentTimestampSeconds } from '../../utils/date-time.js';
26
26
  import { timeout } from '../../utils/timing.js';
27
27
  import { assertDefinedPass, isDefined, isInstanceOf, isNotFunction, isNullOrUndefined, isUndefined } from '../../utils/type-guards.js';
28
- import { millisecondsPerMinute, millisecondsPerSecond } from '../../utils/units.js';
28
+ import { millisecondsPerMinute, millisecondsPerSecond, secondsPerHour } from '../../utils/units.js';
29
29
  import { AUTHENTICATION_API_CLIENT, INITIAL_AUTHENTICATION_DATA } from './tokens.js';
30
30
  const tokenStorageKey = 'AuthenticationService:token';
31
31
  const rawTokenStorageKey = 'AuthenticationService:raw-token';
@@ -37,7 +37,7 @@ const tokenUpdateBusName = 'AuthenticationService:tokenUpdate';
37
37
  const loggedOutBusName = 'AuthenticationService:loggedOut';
38
38
  const refreshLockResource = 'AuthenticationService:refresh';
39
39
  const maxRefreshDelay = 15 * millisecondsPerMinute;
40
- const lockTimeout = 10000;
40
+ const lockTimeout = 10_000;
41
41
  const logoutTimeout = 150;
42
42
  const unrecoverableErrors = [
43
43
  InvalidTokenError,
@@ -66,10 +66,10 @@ let AuthenticationClientService = class AuthenticationClientService {
66
66
  errorSubject = new Subject();
67
67
  tokenUpdateBus = inject((MessageBus), tokenUpdateBusName);
68
68
  loggedOutBus = inject((MessageBus), loggedOutBusName);
69
- forceRefreshToken = new CancellationToken();
70
69
  lock = inject(Lock, refreshLockResource);
71
70
  logger = inject(Logger, 'AuthenticationService');
72
- disposeSignal = inject(CancellationSignal).createChild();
71
+ disposeSignal = inject(CancellationSignal).fork();
72
+ forceRefreshRequested = signal(false);
73
73
  clockOffset = 0;
74
74
  initialized = false;
75
75
  refreshLoopPromise;
@@ -198,7 +198,7 @@ let AuthenticationClientService = class AuthenticationClientService {
198
198
  if (!this.initialized) {
199
199
  return;
200
200
  }
201
- this.disposeSignal.set();
201
+ this.disposeSignal.dispose();
202
202
  await this.refreshLoopPromise;
203
203
  this.errorSubject.complete();
204
204
  await this.loggedOutBus.dispose();
@@ -240,7 +240,7 @@ let AuthenticationClientService = class AuthenticationClientService {
240
240
  }
241
241
  finally {
242
242
  // Always clear the local token, even if the server call fails.
243
- this.forceRefreshToken.unset();
243
+ this.forceRefreshRequested.set(false);
244
244
  this.setNewToken(undefined);
245
245
  this.loggedOutBus.publishAndForget();
246
246
  }
@@ -253,7 +253,7 @@ let AuthenticationClientService = class AuthenticationClientService {
253
253
  if (isDefined(data)) {
254
254
  this.setAdditionalData(data);
255
255
  }
256
- this.forceRefreshToken.set();
256
+ this.forceRefreshRequested.set(true);
257
257
  }
258
258
  /**
259
259
  * Refresh the token.
@@ -393,11 +393,11 @@ let AuthenticationClientService = class AuthenticationClientService {
393
393
  if (isUndefined(token)) {
394
394
  // Wait for login or dispose.
395
395
  // We ignore forceRefreshToken here because we can't refresh without a token.
396
- await firstValueFrom(race([this.definedToken$, this.disposeSignal]), { defaultValue: undefined });
396
+ await firstValueFrom(race([this.definedToken$, from(this.disposeSignal)]), { defaultValue: undefined });
397
397
  continue;
398
398
  }
399
399
  const now = this.estimatedServerTimestampSeconds();
400
- const forceRefresh = this.forceRefreshToken.isSet;
400
+ const forceRefresh = this.forceRefreshRequested();
401
401
  const refreshBufferSeconds = calculateRefreshBufferSeconds(token);
402
402
  const needsRefresh = forceRefresh || (now >= (token.exp - refreshBufferSeconds));
403
403
  if (needsRefresh) {
@@ -406,12 +406,12 @@ let AuthenticationClientService = class AuthenticationClientService {
406
406
  const currentNow = this.estimatedServerTimestampSeconds();
407
407
  const currentRefreshBufferSeconds = isDefined(currentToken) ? calculateRefreshBufferSeconds(currentToken) : 0;
408
408
  // Passive Sync: Check if another tab refreshed the token while we were waiting for the lock (or trying to get it)
409
- const stillNeedsRefresh = isDefined(currentToken) && (forceRefresh || (currentNow >= (currentToken.exp - currentRefreshBufferSeconds)));
409
+ const stillNeedsRefresh = isDefined(currentToken) && (this.forceRefreshRequested() || (currentNow >= (currentToken.exp - currentRefreshBufferSeconds)));
410
410
  if (stillNeedsRefresh) {
411
411
  await this.refresh();
412
412
  }
413
- if (forceRefresh && (this.token() != currentToken)) {
414
- this.forceRefreshToken.unset();
413
+ if (this.forceRefreshRequested() && (this.token() != currentToken)) {
414
+ this.forceRefreshRequested.set(false);
415
415
  }
416
416
  });
417
417
  if (!lockResult.success) {
@@ -419,10 +419,10 @@ let AuthenticationClientService = class AuthenticationClientService {
419
419
  const changeReason = await firstValueFrom(race([
420
420
  timer(5000).pipe(map(() => 'timer')),
421
421
  this.token$.pipe(filter((t) => t != token), map(() => 'token')),
422
- this.disposeSignal,
422
+ from(this.disposeSignal),
423
423
  ]), { defaultValue: undefined });
424
424
  if (changeReason == 'token') {
425
- this.forceRefreshToken.unset();
425
+ this.forceRefreshRequested.set(false);
426
426
  }
427
427
  continue;
428
428
  }
@@ -431,11 +431,11 @@ let AuthenticationClientService = class AuthenticationClientService {
431
431
  const currentRefreshBufferSeconds = isDefined(currentToken) ? calculateRefreshBufferSeconds(currentToken) : 0;
432
432
  const delay = Math.min(maxRefreshDelay, ((currentToken?.exp ?? 0) - this.estimatedServerTimestampSeconds() - currentRefreshBufferSeconds) * millisecondsPerSecond);
433
433
  const wakeUpSignals = [
434
- this.disposeSignal,
434
+ from(this.disposeSignal),
435
435
  this.token$.pipe(filter((t) => t != currentToken)),
436
436
  ];
437
437
  if (!forceRefresh) {
438
- wakeUpSignals.push(this.forceRefreshToken);
438
+ wakeUpSignals.push(toObservable(this.forceRefreshRequested).pipe(filter((v) => v)));
439
439
  }
440
440
  if (delay > 0) {
441
441
  await firstValueFrom(race([timer(delay), ...wakeUpSignals]), { defaultValue: undefined });
@@ -449,9 +449,9 @@ let AuthenticationClientService = class AuthenticationClientService {
449
449
  const currentToken = this.token();
450
450
  await firstValueFrom(race([
451
451
  timer(2500),
452
- this.disposeSignal.set$,
452
+ from(this.disposeSignal),
453
453
  this.token$.pipe(filter((t) => t != currentToken)),
454
- this.forceRefreshToken.set$.pipe(skip(this.forceRefreshToken.isSet ? 1 : 0)),
454
+ toObservable(this.forceRefreshRequested).pipe(filter((requested) => requested), skip(this.forceRefreshRequested() ? 1 : 0)),
455
455
  ]), { defaultValue: undefined });
456
456
  }
457
457
  }
@@ -536,7 +536,7 @@ AuthenticationClientService = __decorate([
536
536
  ], AuthenticationClientService);
537
537
  export { AuthenticationClientService };
538
538
  function calculateRefreshBufferSeconds(token) {
539
- const iat = token.iat ?? (token.exp - 3600);
539
+ const iat = token.iat ?? (token.exp - secondsPerHour);
540
540
  const lifetime = token.exp - iat;
541
541
  return (lifetime * 0.1) + 5;
542
542
  }
@@ -17,10 +17,10 @@ export function waitForAuthenticationCredentialsMiddleware(authenticationService
17
17
  while (!authenticationService.hasValidToken && authenticationService.isLoggedIn()) {
18
18
  const race$ = race([
19
19
  authenticationService.validToken$,
20
- request.abortSignal,
20
+ request.cancellationSignal,
21
21
  ]);
22
22
  await firstValueFrom(race$.pipe(timeout(30000))).catch(() => undefined);
23
- if (request.abortSignal.isSet) {
23
+ if (request.cancellationSignal.isSet) {
24
24
  break;
25
25
  }
26
26
  }
@@ -68,7 +68,8 @@ describe('AuthenticationClientService Error Handling & Stuck States', () => {
68
68
  injector.register(CancellationSignal, { useValue: disposeToken.signal });
69
69
  });
70
70
  afterEach(async () => {
71
- await service.dispose();
71
+ disposeToken.set();
72
+ await injector.dispose();
72
73
  });
73
74
  function setupServiceWithToken(iatOffset = -10, expOffset = 5) {
74
75
  const now = Math.floor(Date.now() / 1000);
@@ -16,6 +16,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
16
16
  let mockLock;
17
17
  let mockMessageBus;
18
18
  let mockLogger;
19
+ let disposeToken;
19
20
  beforeEach(() => {
20
21
  const storage = new Map();
21
22
  globalThis.localStorage = {
@@ -56,11 +57,12 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
56
57
  injector.register(Lock, { useValue: mockLock });
57
58
  injector.register(MessageBus, { useValue: mockMessageBus });
58
59
  injector.register(Logger, { useValue: mockLogger });
59
- const disposeToken = new CancellationToken();
60
+ disposeToken = new CancellationToken();
60
61
  injector.register(CancellationSignal, { useValue: disposeToken.signal });
61
62
  });
62
63
  afterEach(async () => {
63
- await service.dispose();
64
+ disposeToken.set();
65
+ await injector.dispose();
64
66
  });
65
67
  test('Zombie Timer: loop should wake up immediately when token changes', async () => {
66
68
  // 1. Mock a long expiration
@@ -91,7 +93,7 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
91
93
  // Wait for loop to attempt refresh and fail
92
94
  await timeout(50);
93
95
  expect(mockApiClient.refresh).toHaveBeenCalled();
94
- expect(service.forceRefreshToken.isSet).toBe(true); // Should STILL be set
96
+ expect(service.forceRefreshRequested()).toBe(true); // Should STILL be set
95
97
  });
96
98
  test('Lock Contention Backoff: should wait 5 seconds and not busy-loop', async () => {
97
99
  const now = Math.floor(Date.now() / 1000);