@tstdl/base 0.93.139 → 0.93.141
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.
- package/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.d.ts +1 -1
- package/application/application.js +3 -3
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/README.md +267 -0
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/README.md +249 -0
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +3 -4
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +9 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/README.md +305 -0
- package/reflection/decorator-data.js +11 -12
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +293 -0
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +4 -13
- package/task-queue/postgres/task-queue.js +462 -355
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +8 -8
- package/task-queue/task-queue.d.ts +53 -19
- package/task-queue/task-queue.js +121 -55
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +45 -229
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +28 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +128 -8
- package/task-queue/tests/worker.test.js +39 -16
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/templates/README.md +287 -0
- package/test5.js +5 -5
- package/testing/README.md +157 -0
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/README.md +346 -0
- package/text/localization.service.js +2 -2
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/file-reader.js +1 -2
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
package/audit/module.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inject, Injector } from '../injector/index.js';
|
|
2
|
-
import { Database, migrate } from '../orm/server/index.js';
|
|
2
|
+
import { Database, migrate, registerDatabaseMigration } from '../orm/server/index.js';
|
|
3
3
|
/**
|
|
4
4
|
* Configuration for {@link configureAudit}.
|
|
5
5
|
*/
|
|
@@ -9,6 +9,11 @@ export class AuditModuleConfig {
|
|
|
9
9
|
* If not provided, the global database configuration is used.
|
|
10
10
|
*/
|
|
11
11
|
database;
|
|
12
|
+
/**
|
|
13
|
+
* Whether to automatically run database migrations when the application starts.
|
|
14
|
+
* Defaults to `true`.
|
|
15
|
+
*/
|
|
16
|
+
autoMigrate;
|
|
12
17
|
}
|
|
13
18
|
/**
|
|
14
19
|
* Configures audit server services.
|
|
@@ -17,6 +22,9 @@ export class AuditModuleConfig {
|
|
|
17
22
|
export function configureAudit({ injector, ...config } = {}) {
|
|
18
23
|
const targetInjector = injector ?? Injector;
|
|
19
24
|
targetInjector.register(AuditModuleConfig, { useValue: config });
|
|
25
|
+
if (config.autoMigrate != false) {
|
|
26
|
+
registerDatabaseMigration('Audit', migrateAuditSchema, { injector });
|
|
27
|
+
}
|
|
20
28
|
}
|
|
21
29
|
/**
|
|
22
30
|
* Migrates the audit database schema to the latest version.
|
|
@@ -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$:
|
|
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$:
|
|
59
|
+
readonly token$: Observable<TokenPayload<AdditionalTokenPayload> | undefined>;
|
|
59
60
|
/** Emits when token is available (not undefined) */
|
|
60
|
-
readonly definedToken$:
|
|
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$:
|
|
63
|
+
readonly validToken$: Observable<Exclude<TokenPayload<AdditionalTokenPayload>, void | undefined>>;
|
|
63
64
|
/** Current subject */
|
|
64
|
-
readonly subjectId$:
|
|
65
|
+
readonly subjectId$: Observable<string | undefined>;
|
|
65
66
|
/** Emits when subject is available */
|
|
66
|
-
readonly definedSubjectId$:
|
|
67
|
+
readonly definedSubjectId$: Observable<string>;
|
|
67
68
|
/** Current session id */
|
|
68
|
-
readonly sessionId$:
|
|
69
|
+
readonly sessionId$: Observable<string | undefined>;
|
|
69
70
|
/** Emits when session id is available */
|
|
70
|
-
readonly definedSessionId$:
|
|
71
|
+
readonly definedSessionId$: Observable<string>;
|
|
71
72
|
/** Whether the user is logged in */
|
|
72
|
-
readonly isLoggedIn$:
|
|
73
|
+
readonly isLoggedIn$: Observable<boolean>;
|
|
73
74
|
/** Emits when the user logs out */
|
|
74
|
-
readonly loggedOut$:
|
|
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
|
|
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 =
|
|
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).
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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) && (
|
|
409
|
+
const stillNeedsRefresh = isDefined(currentToken) && (this.forceRefreshRequested() || (currentNow >= (currentToken.exp - currentRefreshBufferSeconds)));
|
|
410
410
|
if (stillNeedsRefresh) {
|
|
411
411
|
await this.refresh();
|
|
412
412
|
}
|
|
413
|
-
if (
|
|
414
|
-
this.
|
|
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.
|
|
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.
|
|
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
|
|
452
|
+
from(this.disposeSignal),
|
|
453
453
|
this.token$.pipe(filter((t) => t != currentToken)),
|
|
454
|
-
this.
|
|
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 -
|
|
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.
|
|
20
|
+
request.cancellationSignal,
|
|
21
21
|
]);
|
|
22
22
|
await firstValueFrom(race$.pipe(timeout(30000))).catch(() => undefined);
|
|
23
|
-
if (request.
|
|
23
|
+
if (request.cancellationSignal.isSet) {
|
|
24
24
|
break;
|
|
25
25
|
}
|
|
26
26
|
}
|
|
@@ -25,6 +25,11 @@ export declare class AuthenticationModuleConfig {
|
|
|
25
25
|
* Override default {@link AuthenticationAncillaryService}.
|
|
26
26
|
*/
|
|
27
27
|
authenticationAncillaryService?: InjectionToken<AuthenticationAncillaryService<any, any, any>>;
|
|
28
|
+
/**
|
|
29
|
+
* Whether to automatically run database migrations when the application starts.
|
|
30
|
+
* Defaults to `true`.
|
|
31
|
+
*/
|
|
32
|
+
autoMigrate?: boolean;
|
|
28
33
|
}
|
|
29
34
|
/**
|
|
30
35
|
* Configures authentication server services.
|
|
@@ -2,7 +2,7 @@ import { ApiRequestTokenProvider } from '../../api/server/api-request-token.prov
|
|
|
2
2
|
import { inject } from '../../injector/index.js';
|
|
3
3
|
import { Injector } from '../../injector/injector.js';
|
|
4
4
|
import { isProvider } from '../../injector/provider.js';
|
|
5
|
-
import { Database, migrate } from '../../orm/server/index.js';
|
|
5
|
+
import { Database, migrate, registerDatabaseMigration } from '../../orm/server/index.js';
|
|
6
6
|
import { isDefined } from '../../utils/type-guards.js';
|
|
7
7
|
import { AuthenticationAncillaryService } from './authentication-ancillary.service.js';
|
|
8
8
|
import { AuthenticationApiRequestTokenProvider } from './authentication-api-request-token.provider.js';
|
|
@@ -28,6 +28,11 @@ export class AuthenticationModuleConfig {
|
|
|
28
28
|
* Override default {@link AuthenticationAncillaryService}.
|
|
29
29
|
*/
|
|
30
30
|
authenticationAncillaryService;
|
|
31
|
+
/**
|
|
32
|
+
* Whether to automatically run database migrations when the application starts.
|
|
33
|
+
* Defaults to `true`.
|
|
34
|
+
*/
|
|
35
|
+
autoMigrate;
|
|
31
36
|
}
|
|
32
37
|
/**
|
|
33
38
|
* Configures authentication server services.
|
|
@@ -44,6 +49,9 @@ export function configureAuthenticationServer({ injector, ...config }) {
|
|
|
44
49
|
if (isDefined(config.authenticationAncillaryService)) {
|
|
45
50
|
targetInjector.registerSingleton(AuthenticationAncillaryService, { useToken: config.authenticationAncillaryService });
|
|
46
51
|
}
|
|
52
|
+
if (config.autoMigrate != false) {
|
|
53
|
+
registerDatabaseMigration('Authentication', migrateAuthenticationSchema, { injector });
|
|
54
|
+
}
|
|
47
55
|
}
|
|
48
56
|
/**
|
|
49
57
|
* Migrates the authentication schema.
|
|
@@ -25,7 +25,7 @@ describe('AuthenticationApiController Integration', () => {
|
|
|
25
25
|
clear: vi.fn(() => storage.clear()),
|
|
26
26
|
};
|
|
27
27
|
({ injector, database } = await setupIntegrationTest({
|
|
28
|
-
modules: { authentication: true, audit: true, keyValueStore: true,
|
|
28
|
+
modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true, webServer: true },
|
|
29
29
|
authenticationAncillaryService: DefaultAuthenticationAncillaryService,
|
|
30
30
|
}));
|
|
31
31
|
await runInInjectionContext(injector, async () => {
|
|
@@ -11,7 +11,7 @@ describe('AuthenticationApiRequestTokenProvider', () => {
|
|
|
11
11
|
let tokenProvider;
|
|
12
12
|
const tenantId = crypto.randomUUID();
|
|
13
13
|
beforeAll(async () => {
|
|
14
|
-
({ injector } = await setupIntegrationTest({ modules: { authentication: true,
|
|
14
|
+
({ injector } = await setupIntegrationTest({ modules: { authentication: true, signals: true } }));
|
|
15
15
|
authenticationService = await injector.resolveAsync(AuthenticationService);
|
|
16
16
|
subjectService = await injector.resolveAsync(SubjectService);
|
|
17
17
|
tokenProvider = injector.resolve(AuthenticationApiRequestTokenProvider);
|
|
@@ -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
|
-
|
|
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
|
-
|
|
60
|
+
disposeToken = new CancellationToken();
|
|
60
61
|
injector.register(CancellationSignal, { useValue: disposeToken.signal });
|
|
61
62
|
});
|
|
62
63
|
afterEach(async () => {
|
|
63
|
-
|
|
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.
|
|
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);
|
|
@@ -25,7 +25,7 @@ describe('AuthenticationClientService Integration', () => {
|
|
|
25
25
|
clear: vi.fn(() => storage.clear()),
|
|
26
26
|
};
|
|
27
27
|
({ injector, database } = await setupIntegrationTest({
|
|
28
|
-
modules: { authentication: true, audit: true, keyValueStore: true,
|
|
28
|
+
modules: { authentication: true, audit: true, keyValueStore: true, signals: true, api: true, webServer: true },
|
|
29
29
|
authenticationAncillaryService: DefaultAuthenticationAncillaryService,
|
|
30
30
|
}));
|
|
31
31
|
await runInInjectionContext(injector, async () => {
|