@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.
- 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.js +2 -2
- package/audit/README.md +267 -0
- 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/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- 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/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/services/document-management.service.js +9 -7
- 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.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/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/lock/README.md +249 -0
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- 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 +1 -1
- 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/package.json +8 -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/reflection/README.md +305 -0
- 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 +300 -0
- package/task-queue/postgres/task-queue.d.ts +2 -1
- package/task-queue/postgres/task-queue.js +32 -2
- package/task-queue/task-context.js +1 -1
- package/task-queue/task-queue.d.ts +17 -0
- package/task-queue/task-queue.js +103 -45
- package/task-queue/tests/complex.test.js +4 -4
- package/task-queue/tests/dependencies.test.js +4 -2
- package/task-queue/tests/queue.test.js +111 -0
- package/task-queue/tests/worker.test.js +21 -13
- package/templates/README.md +287 -0
- package/testing/README.md +157 -0
- package/text/README.md +346 -0
- 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/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
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @tstdl/base/object-storage
|
|
2
|
+
|
|
3
|
+
A flexible and extensible module for handling object storage, providing a strong abstraction layer over concrete implementations like S3. It simplifies file management with module-based isolation, automatic lifecycle management, and seamless dependency injection integration.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [✨ Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [Injecting and Using ObjectStorage](#injecting-and-using-objectstorage)
|
|
12
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
13
|
+
- [Lifecycle Management (Expiration)](#lifecycle-management-expiration)
|
|
14
|
+
- [Pre-signed URLs](#pre-signed-urls)
|
|
15
|
+
- [Streaming I/O](#streaming-io)
|
|
16
|
+
- [Listing Objects](#listing-objects)
|
|
17
|
+
- [Moving and Copying](#moving-and-copying)
|
|
18
|
+
- [📚 API](#-api)
|
|
19
|
+
|
|
20
|
+
## ✨ Features
|
|
21
|
+
|
|
22
|
+
- **Abstract `ObjectStorage` Interface**: Decouples your application logic from specific storage vendors.
|
|
23
|
+
- **S3-Compatible Implementation**: Includes a robust `S3ObjectStorage` implementation working with AWS S3, MinIO, Google Cloud Storage (via S3 interoperability), etc.
|
|
24
|
+
- **Google Cloud Storage Implementation**: Native `GoogleObjectStorage` implementation for direct GCS support.
|
|
25
|
+
- **Module-based Isolation**: Organizes objects into logical "modules". These map to either key prefixes in a shared bucket or separate buckets per module.
|
|
26
|
+
- **Automatic Lifecycle Management**: Configure object expiration policies (e.g., delete temp files after 24h) directly via injection tokens.
|
|
27
|
+
- **Stream Support**: Native support for `ReadableStream` and `Uint8Array` for memory-efficient handling of large files.
|
|
28
|
+
- **Pre-signed URLs**: Generate secure, temporary URLs for direct client-side uploads and downloads.
|
|
29
|
+
- **Dependency Injection**: Designed for `@tstdl/base/injector`, allowing context-aware resolution of storage instances.
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### ObjectStorage
|
|
34
|
+
|
|
35
|
+
The central abstract class defining the contract for storage operations (upload, download, delete, exists, etc.). You rarely instantiate this directly; instead, you request it via dependency injection.
|
|
36
|
+
|
|
37
|
+
### Modules
|
|
38
|
+
|
|
39
|
+
A **Module** is a logical namespace for a collection of objects (e.g., `user-avatars`, `invoices`, `temp-uploads`).
|
|
40
|
+
|
|
41
|
+
- **Shared Bucket Mode**: Modules are treated as directory prefixes within a single S3 bucket (e.g., `my-bucket/user-avatars/image.png`).
|
|
42
|
+
- **Bucket-Per-Module Mode**: Each module gets its own dedicated S3 bucket (e.g., bucket `user-avatars` contains `image.png`).
|
|
43
|
+
|
|
44
|
+
### ObjectStorageProvider
|
|
45
|
+
|
|
46
|
+
A factory responsible for creating `ObjectStorage` instances for specific modules. The `S3ObjectStorageProvider` handles the creation of S3 clients and ensures buckets exist.
|
|
47
|
+
|
|
48
|
+
### ObjectStorageObject
|
|
49
|
+
|
|
50
|
+
Represents a stored file. It provides methods to access metadata, size, content, and resource URIs without loading the entire file into memory immediately.
|
|
51
|
+
|
|
52
|
+
## 🚀 Basic Usage
|
|
53
|
+
|
|
54
|
+
### Configuration
|
|
55
|
+
|
|
56
|
+
#### S3 (AWS, MinIO, etc.)
|
|
57
|
+
|
|
58
|
+
Configure the S3 provider at your application's entry point (e.g., `bootstrap.ts`).
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { configureS3ObjectStorage } from '@tstdl/base/object-storage';
|
|
62
|
+
|
|
63
|
+
// Option A: Single Shared Bucket (Recommended for most use cases)
|
|
64
|
+
configureS3ObjectStorage({
|
|
65
|
+
endpoint: 'http://localhost:9000', // S3 Endpoint
|
|
66
|
+
accessKey: 'minioadmin',
|
|
67
|
+
secretKey: 'minioadmin',
|
|
68
|
+
bucket: 'my-app-storage', // All modules will be subfolders in this bucket
|
|
69
|
+
forcePathStyle: true, // Required for local s3 server
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Option B: Bucket Per Module
|
|
73
|
+
// configureS3ObjectStorage({
|
|
74
|
+
// endpoint: '...',
|
|
75
|
+
// accessKey: '...',
|
|
76
|
+
// secretKey: '...',
|
|
77
|
+
// bucketPerModule: true // Each module creates a new bucket
|
|
78
|
+
// });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Google Cloud Storage
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { configureGoogleObjectStorage } from '@tstdl/base/object-storage/google';
|
|
85
|
+
|
|
86
|
+
configureGoogleObjectStorage({
|
|
87
|
+
projectId: 'my-project-id',
|
|
88
|
+
keyFilename: '/path/to/keyfile.json',
|
|
89
|
+
bucket: 'my-app-storage',
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Injecting and Using ObjectStorage
|
|
94
|
+
|
|
95
|
+
Inject `ObjectStorage` into your services using the module name as the injection argument.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
99
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
100
|
+
|
|
101
|
+
@Singleton()
|
|
102
|
+
export class ProfilePictureService {
|
|
103
|
+
// Inject storage specifically for the 'profile-pictures' module
|
|
104
|
+
readonly #storage = inject(ObjectStorage, 'profile-pictures');
|
|
105
|
+
|
|
106
|
+
async savePicture(userId: string, content: Uint8Array): Promise<void> {
|
|
107
|
+
const key = `${userId}.jpg`;
|
|
108
|
+
|
|
109
|
+
// Upload content
|
|
110
|
+
await this.#storage.uploadObject(key, content, {
|
|
111
|
+
contentType: 'image/jpeg',
|
|
112
|
+
metadata: { userId },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getPicture(userId: string): Promise<Uint8Array> {
|
|
117
|
+
const key = `${userId}.jpg`;
|
|
118
|
+
|
|
119
|
+
// Check existence
|
|
120
|
+
if (!(await this.#storage.exists(key))) {
|
|
121
|
+
throw new Error('Picture not found');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Download content
|
|
125
|
+
return this.#storage.getContent(key);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async deletePicture(userId: string): Promise<void> {
|
|
129
|
+
await this.#storage.deleteObject(`${userId}.jpg`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 🔧 Advanced Topics
|
|
135
|
+
|
|
136
|
+
### Lifecycle Management (Expiration)
|
|
137
|
+
|
|
138
|
+
You can configure objects to automatically expire (be deleted) after a certain duration. This is configured when injecting the storage instance. The implementation (e.g., S3) will apply lifecycle rules to the bucket.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
142
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
143
|
+
import { secondsPerDay } from '@tstdl/base/utils';
|
|
144
|
+
|
|
145
|
+
@Singleton()
|
|
146
|
+
export class TemporaryUploadService {
|
|
147
|
+
// Objects in 'temp-files' will be deleted 1 day after creation
|
|
148
|
+
readonly #tempStorage = inject(ObjectStorage, {
|
|
149
|
+
module: 'temp-files',
|
|
150
|
+
configuration: {
|
|
151
|
+
lifecycle: {
|
|
152
|
+
expiration: {
|
|
153
|
+
after: 1 * secondsPerDay,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
async saveTempFile(filename: string, data: Uint8Array): Promise<void> {
|
|
160
|
+
await this.#tempStorage.uploadObject(filename, data);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Pre-signed URLs
|
|
166
|
+
|
|
167
|
+
Offload bandwidth from your server by generating pre-signed URLs that allow clients to upload or download directly to/from the storage provider.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
171
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
172
|
+
import { millisecondsPerMinute, now } from '@tstdl/base/utils';
|
|
173
|
+
|
|
174
|
+
@Singleton()
|
|
175
|
+
export class DownloadService {
|
|
176
|
+
readonly #storage = inject(ObjectStorage, 'reports');
|
|
177
|
+
|
|
178
|
+
async getDownloadLink(reportId: string): Promise<string> {
|
|
179
|
+
const key = `${reportId}.pdf`;
|
|
180
|
+
const expiration = now().getTime() + 15 * millisecondsPerMinute;
|
|
181
|
+
|
|
182
|
+
// Generate a URL valid for 15 minutes
|
|
183
|
+
return this.#storage.getDownloadUrl(key, expiration, {
|
|
184
|
+
'response-content-disposition': 'attachment; filename="report.pdf"',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getUploadLink(reportId: string): Promise<string> {
|
|
189
|
+
const key = `${reportId}.pdf`;
|
|
190
|
+
const expiration = now().getTime() + 15 * millisecondsPerMinute;
|
|
191
|
+
|
|
192
|
+
// Generate a PUT URL for uploading
|
|
193
|
+
return this.#storage.getUploadUrl(key, expiration, {
|
|
194
|
+
contentType: 'application/pdf',
|
|
195
|
+
contentLength: 1024 * 1024 * 5, // 5MB expected size
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Streaming I/O
|
|
202
|
+
|
|
203
|
+
For large files, use streams to avoid loading the entire file into memory.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { inject } from '@tstdl/base/injector';
|
|
207
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
208
|
+
|
|
209
|
+
class LargeFileService {
|
|
210
|
+
readonly #storage = inject(ObjectStorage, 'backups');
|
|
211
|
+
|
|
212
|
+
async uploadStream(filename: string, stream: ReadableStream<Uint8Array>, size: number): Promise<void> {
|
|
213
|
+
await this.#storage.uploadObject(filename, stream, {
|
|
214
|
+
contentLength: size,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async streamDownload(filename: string): Promise<ReadableStream<Uint8Array>> {
|
|
219
|
+
return this.#storage.getContentStream(filename);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Listing Objects
|
|
225
|
+
|
|
226
|
+
Use `getObjectsCursor()` to iterate over objects efficiently using an async iterable.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
async function listAllFiles(storage: ObjectStorage) {
|
|
230
|
+
for await (const obj of storage.getObjectsCursor()) {
|
|
231
|
+
console.log(`Found file: ${obj.key}, Size: ${await obj.getContentLength()}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Moving and Copying
|
|
237
|
+
|
|
238
|
+
You can move or copy objects within the same module or across different modules (and buckets).
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { inject } from '@tstdl/base/injector';
|
|
242
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
243
|
+
|
|
244
|
+
class FileOrganizer {
|
|
245
|
+
readonly #inbox = inject(ObjectStorage, 'inbox');
|
|
246
|
+
readonly #archive = inject(ObjectStorage, 'archive');
|
|
247
|
+
|
|
248
|
+
async archiveFile(filename: string): Promise<void> {
|
|
249
|
+
// Move from 'inbox' module to 'archive' module
|
|
250
|
+
await this.#inbox.moveObject(filename, [this.#archive, `2024/${filename}`]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async duplicateInInbox(filename: string): Promise<void> {
|
|
254
|
+
// Copy within the same module
|
|
255
|
+
await this.#inbox.copyObject(filename, `copy_of_${filename}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 📚 API
|
|
261
|
+
|
|
262
|
+
### `ObjectStorage` (Abstract Class)
|
|
263
|
+
|
|
264
|
+
| Method | Description |
|
|
265
|
+
| :--------------------------------------------- | :------------------------------------------------------ |
|
|
266
|
+
| `exists(key: string): Promise<boolean>` | Checks if an object exists. |
|
|
267
|
+
| `uploadObject(key, content, options?)` | Uploads data (`Uint8Array` or `ReadableStream`). |
|
|
268
|
+
| `getContent(key): Promise<Uint8Array>` | Downloads the full object content into memory. |
|
|
269
|
+
| `getContentStream(key): ReadableStream` | Gets a stream of the object content. |
|
|
270
|
+
| `getDownloadUrl(key, expiration, headers?)` | Generates a pre-signed download URL. |
|
|
271
|
+
| `getUploadUrl(key, expiration, options?)` | Generates a pre-signed upload URL. |
|
|
272
|
+
| `deleteObject(key): Promise<void>` | Deletes a single object. |
|
|
273
|
+
| `deleteObjects(keys): Promise<void>` | Deletes multiple objects. |
|
|
274
|
+
| `copyObject(source, dest, options?)` | Copies an object (intra- or inter-module). |
|
|
275
|
+
| `moveObject(source, dest, options?)` | Moves an object (copy + delete). |
|
|
276
|
+
| `getObjects(): Promise<ObjectStorageObject[]>` | Lists all objects in the module. |
|
|
277
|
+
| `getObjectsCursor(): AsyncIterable` | Iterates over objects efficiently. |
|
|
278
|
+
| `getObject(key): Promise<ObjectStorageObject>` | Gets a handle to an object without downloading content. |
|
|
279
|
+
|
|
280
|
+
### `S3ObjectStorageProviderConfig`
|
|
281
|
+
|
|
282
|
+
| Property | Type | Description |
|
|
283
|
+
| :---------------- | :-------- | :----------------------------------------------------------------------------- |
|
|
284
|
+
| `endpoint` | `string` | S3 API endpoint (e.g., `https://s3.amazonaws.com` or `http://localhost:9000`). |
|
|
285
|
+
| `region` | `string` | S3 Region (e.g., `us-east-1`). |
|
|
286
|
+
| `accessKey` | `string` | S3 Access Key ID. |
|
|
287
|
+
| `secretKey` | `string` | S3 Secret Access Key. |
|
|
288
|
+
| `bucket` | `string` | Name of the shared bucket. Mutually exclusive with `bucketPerModule`. |
|
|
289
|
+
| `bucketPerModule` | `boolean` | If true, creates a separate bucket for each module. |
|
|
290
|
+
| `forcePathStyle` | `boolean` | Whether to use path-style addressing. Useful for local s3 server. |
|
|
291
|
+
|
|
292
|
+
### `ObjectStorageObject`
|
|
293
|
+
|
|
294
|
+
| Method | Description |
|
|
295
|
+
| :--------------------------------------- | :----------------------------------------- |
|
|
296
|
+
| `getContentLength(): Promise<number>` | Returns the size of the object in bytes. |
|
|
297
|
+
| `getMetadata(): Promise<ObjectMetadata>` | Returns user-defined metadata. |
|
|
298
|
+
| `getContent(): Promise<Uint8Array>` | Downloads content. |
|
|
299
|
+
| `getContentStream(): ReadableStream` | Streams content. |
|
|
300
|
+
| `getResourceUri(): Promise<string>` | Returns the URI (e.g., `s3://bucket/key`). |
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# OpenID Connect
|
|
2
|
+
|
|
3
|
+
A robust, type-safe OpenID Connect (OIDC) client implementation for TypeScript applications. It simplifies integration with OIDC providers by handling configuration discovery, secure state management, PKCE, and token exchanges.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [Prerequisites](#prerequisites)
|
|
10
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
11
|
+
- [1. Setup & Configuration](#1-setup--configuration)
|
|
12
|
+
- [2. Initiating Authorization (Login)](#2-initiating-authorization-login)
|
|
13
|
+
- [3. Handling the Callback](#3-handling-the-callback)
|
|
14
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
15
|
+
- [Client Credentials Flow](#client-credentials-flow)
|
|
16
|
+
- [Refreshing Tokens](#refreshing-tokens)
|
|
17
|
+
- [Fetching User Info](#fetching-user-info)
|
|
18
|
+
- [Custom State Data](#custom-state-data)
|
|
19
|
+
- [📚 API](#-api)
|
|
20
|
+
|
|
21
|
+
## ✨ Features
|
|
22
|
+
|
|
23
|
+
- **Auto-Discovery**: Automatically fetches and caches provider configuration from `.well-known/openid-configuration`.
|
|
24
|
+
- **Secure by Default**: Implements Authorization Code Flow with **PKCE** (Proof Key for Code Exchange, SHA-256) and cryptographically secure state generation.
|
|
25
|
+
- **State Persistence**: Persists authorization state via the ORM to securely validate callbacks and prevent CSRF/replay attacks.
|
|
26
|
+
- **Multiple Flows**: Supports Authorization Code and Client Credentials flows.
|
|
27
|
+
- **Automated Authentication**: Supports both `body` and `basic-auth` authentication methods for token endpoints.
|
|
28
|
+
- **Token Management**: Simple methods to exchange codes for tokens, refresh existing tokens, and fetch user info.
|
|
29
|
+
- **Type-Safe**: Full TypeScript support for token responses and custom state data.
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### OidcService
|
|
34
|
+
|
|
35
|
+
The central service class. It orchestrates the entire flow: fetching configuration, generating secure parameters, storing state in the database, and communicating with the OIDC provider to exchange tokens.
|
|
36
|
+
|
|
37
|
+
### OidcState
|
|
38
|
+
|
|
39
|
+
An entity model used to persist the state of an authentication attempt. It stores:
|
|
40
|
+
|
|
41
|
+
- The generated `state` string (for CSRF protection).
|
|
42
|
+
- The `codeVerifier` (for PKCE).
|
|
43
|
+
- Configuration details (endpoint, client ID).
|
|
44
|
+
- Optional custom `data` attached to the flow.
|
|
45
|
+
|
|
46
|
+
This entity must be registered with your ORM configuration so the service can save and retrieve it during the callback phase.
|
|
47
|
+
|
|
48
|
+
## Prerequisites
|
|
49
|
+
|
|
50
|
+
This module relies on the `@tstdl/base/orm` module to store the `OidcState`. Ensure your application has the ORM configured and the `OidcState` entity is included in your database schema.
|
|
51
|
+
|
|
52
|
+
## 🚀 Basic Usage
|
|
53
|
+
|
|
54
|
+
This example demonstrates the standard **Authorization Code Flow** with PKCE, commonly used for user login.
|
|
55
|
+
|
|
56
|
+
### 1. Setup & Configuration
|
|
57
|
+
|
|
58
|
+
Ensure `OidcState` is registered in your ORM configuration (e.g., in your `bootstrap.ts` or module configuration).
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { configureOrm } from '@tstdl/base/orm/server';
|
|
62
|
+
import { OidcState } from '@tstdl/base/openid-connect';
|
|
63
|
+
|
|
64
|
+
// In your bootstrap/configuration file
|
|
65
|
+
configureOrm({
|
|
66
|
+
// ... connection settings
|
|
67
|
+
entities: [
|
|
68
|
+
// ... other entities
|
|
69
|
+
OidcState, // <--- Register the OidcState entity
|
|
70
|
+
],
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 2. Initiating Authorization (Login)
|
|
75
|
+
|
|
76
|
+
When a user clicks "Login", generate the authorization URL. This creates a state record in the database.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { inject } from '@tstdl/base/injector';
|
|
80
|
+
import { OidcService } from '@tstdl/base/openid-connect';
|
|
81
|
+
import { millisecondsPerMinute } from '@tstdl/base/utils/units';
|
|
82
|
+
|
|
83
|
+
class AuthService {
|
|
84
|
+
// Inject the OidcService. You can type the generic to define custom data stored in state.
|
|
85
|
+
readonly oidcService = inject(OidcService<void>);
|
|
86
|
+
|
|
87
|
+
async getLoginUrl(): Promise<string> {
|
|
88
|
+
const result = await this.oidcService.initAuthorization({
|
|
89
|
+
endpoint: 'https://accounts.google.com', // The OIDC provider URL
|
|
90
|
+
clientId: 'my-client-id',
|
|
91
|
+
clientSecret: 'my-client-secret',
|
|
92
|
+
scope: 'openid profile email',
|
|
93
|
+
expiration: 5 * millisecondsPerMinute, // How long the login attempt is valid
|
|
94
|
+
data: undefined, // No custom data for this example
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Construct the URL to redirect the user to
|
|
98
|
+
const url = new URL(result.authorizationEndpoint);
|
|
99
|
+
url.searchParams.set('response_type', 'code');
|
|
100
|
+
url.searchParams.set('client_id', result.clientId);
|
|
101
|
+
url.searchParams.set('scope', result.scope);
|
|
102
|
+
url.searchParams.set('redirect_uri', 'https://myapp.com/callback');
|
|
103
|
+
url.searchParams.set('state', result.state);
|
|
104
|
+
url.searchParams.set('code_challenge', result.codeChallenge);
|
|
105
|
+
url.searchParams.set('code_challenge_method', result.codeChallengeMethod);
|
|
106
|
+
|
|
107
|
+
return url.toString();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 3. Handling the Callback
|
|
113
|
+
|
|
114
|
+
When the user is redirected back to your application (e.g., `https://myapp.com/callback?code=...&state=...`), validate the state and exchange the code for tokens.
|
|
115
|
+
|
|
116
|
+
```ts
|
|
117
|
+
import { inject } from '@tstdl/base/injector';
|
|
118
|
+
import { OidcService } from '@tstdl/base/openid-connect';
|
|
119
|
+
|
|
120
|
+
class CallbackController {
|
|
121
|
+
readonly oidcService = inject(OidcService<void>);
|
|
122
|
+
|
|
123
|
+
async handleCallback(code: string, state: string): Promise<void> {
|
|
124
|
+
// 1. Validate and retrieve the stored state.
|
|
125
|
+
// This throws if the state is invalid, expired, or missing.
|
|
126
|
+
// It also deletes the state from the DB to prevent replay attacks.
|
|
127
|
+
const storedState = await this.oidcService.validateState(state);
|
|
128
|
+
|
|
129
|
+
// 2. Exchange the authorization code for tokens
|
|
130
|
+
const tokenResponse = await this.oidcService.getToken({
|
|
131
|
+
grantType: 'authorization_code',
|
|
132
|
+
endpoint: storedState.endpoint,
|
|
133
|
+
clientId: storedState.clientId,
|
|
134
|
+
clientSecret: storedState.clientSecret,
|
|
135
|
+
code: code,
|
|
136
|
+
codeVerifier: storedState.codeVerifier, // Retrieved from DB
|
|
137
|
+
redirectUri: 'https://myapp.com/callback',
|
|
138
|
+
authType: 'body', // or 'basic-auth' depending on provider
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
console.log('Access Token:', tokenResponse.accessToken);
|
|
142
|
+
console.log('ID Token:', tokenResponse.idToken);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## 🔧 Advanced Topics
|
|
148
|
+
|
|
149
|
+
### Client Credentials Flow
|
|
150
|
+
|
|
151
|
+
Used for machine-to-machine communication where no user is involved.
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
const token = await oidcService.getToken({
|
|
155
|
+
grantType: 'client_credentials',
|
|
156
|
+
endpoint: 'https://auth.example.com',
|
|
157
|
+
clientId: 'service-account-id',
|
|
158
|
+
clientSecret: 'service-account-secret',
|
|
159
|
+
scope: 'api:read',
|
|
160
|
+
});
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Refreshing Tokens
|
|
164
|
+
|
|
165
|
+
If you received a `refresh_token` in the initial flow, you can use it to get a new access token.
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const newToken = await oidcService.refreshToken({
|
|
169
|
+
endpoint: 'https://accounts.google.com',
|
|
170
|
+
clientId: 'my-client-id',
|
|
171
|
+
clientSecret: 'my-client-secret',
|
|
172
|
+
refreshToken: 'existing-refresh-token',
|
|
173
|
+
});
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Fetching User Info
|
|
177
|
+
|
|
178
|
+
Retrieve the user's profile information using the `userinfo_endpoint` discovered from the configuration.
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
const userInfo = await oidcService.getUserInfo(
|
|
182
|
+
'https://accounts.google.com',
|
|
183
|
+
tokenResponse, // The object returned from getToken
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
console.log(userInfo);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
### Custom State Data
|
|
190
|
+
|
|
191
|
+
You can attach custom data to the `OidcState` when initializing authorization. This is useful for remembering where to redirect the user after login, storing a nonce, or keeping track of the original request's context.
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
type MyCustomData = { returnUrl: string };
|
|
195
|
+
|
|
196
|
+
// Define a service or inject directly with the desired type
|
|
197
|
+
const oidcService = inject(OidcService<MyCustomData>);
|
|
198
|
+
|
|
199
|
+
// Initialize
|
|
200
|
+
const result = await oidcService.initAuthorization({
|
|
201
|
+
// ... other params
|
|
202
|
+
data: { returnUrl: '/dashboard/settings' },
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ... later in callback ...
|
|
206
|
+
|
|
207
|
+
const storedState = await oidcService.validateState(state);
|
|
208
|
+
const returnUrl = storedState.data.returnUrl; // Properly typed as string
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Additional Parameters and Auth Types
|
|
212
|
+
|
|
213
|
+
Some OIDC providers require additional parameters during token exchange or use a different authentication method.
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
const tokenResponse = await oidcService.getToken({
|
|
217
|
+
grantType: 'authorization_code',
|
|
218
|
+
// ...
|
|
219
|
+
authType: 'basic-auth', // Uses HTTP Basic Auth (clientId:clientSecret)
|
|
220
|
+
}, {
|
|
221
|
+
// Additional form-data parameters (some providers require 'resource')
|
|
222
|
+
resource: 'https://api.example.com',
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## ⚠️ Error Handling
|
|
227
|
+
|
|
228
|
+
The `OidcService` utilizes standard errors from the library:
|
|
229
|
+
|
|
230
|
+
- `ForbiddenError`: Thrown by `validateState` if the state is missing, invalid, or already used (to prevent replay attacks).
|
|
231
|
+
- `NotImplementedError`: Thrown if an unsupported grant type or authentication type is requested.
|
|
232
|
+
- `Error`: Thrown if required endpoints (like `userinfo_endpoint`) are not present in the provider's configuration.
|
|
233
|
+
|
|
234
|
+
Always wrap callback logic in a `try...catch` block to handle these cases gracefully.
|
|
235
|
+
|
|
236
|
+
## 📚 API
|
|
237
|
+
|
|
238
|
+
### Services
|
|
239
|
+
|
|
240
|
+
| Class | Description |
|
|
241
|
+
| :------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
242
|
+
| `OidcService<Data>` | Main service for OIDC operations. Handles state creation, validation, token exchange, and user info retrieval. |
|
|
243
|
+
| `OidcConfigurationService` | Fetches OIDC configuration from the provider's well-known endpoint. |
|
|
244
|
+
| `CachedOidcConfigurationService` | A cached implementation of `OidcConfigurationService`. Used by default with a 5-minute cache duration (configurable via dependency injection). |
|
|
245
|
+
|
|
246
|
+
### OidcService Methods
|
|
247
|
+
|
|
248
|
+
| Method | Description |
|
|
249
|
+
| :------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------ |
|
|
250
|
+
| `initAuthorization(params)` | Discovers configuration, creates a secure state in the DB, and returns data to build the redirect URL. |
|
|
251
|
+
| `validateState(state)` | Attempts to load a state by its value, then **deletes it**. Throws if not found. Essential for security. |
|
|
252
|
+
| `getState(state)` | Just retrieves the state without deleting it. |
|
|
253
|
+
| `deleteState(state)` | Manually removes a state from the database. |
|
|
254
|
+
| `getToken(params, additionalData?)` | Exchanges a code (or credentials) for tokens. Supports `authorization_code` and `client_credentials`. |
|
|
255
|
+
| `refreshToken(params)` | Uses a refresh token to obtain a new set of tokens. |
|
|
256
|
+
| `getUserInfo(endpoint, token)` | Fetches the user's profile information using the access token. |
|
|
257
|
+
| `oidcConfigurationService.getConfiguration` | Directly fetches (and potentially caches) the OIDC provider's configuration. |
|
|
258
|
+
|
|
259
|
+
### Models
|
|
260
|
+
|
|
261
|
+
| Class | Description |
|
|
262
|
+
| :---------------- | :------------------------------------------------------------------------------------------------------------------------------------- |
|
|
263
|
+
| `OidcState<Data>` | Database entity representing the authorization state. Must be registered with ORM. Uses the `oidc` schema and `state` table by default. |
|
|
264
|
+
|
|
265
|
+
### Types & Interfaces
|
|
266
|
+
|
|
267
|
+
| Type | Description |
|
|
268
|
+
| :--------------------------- | :----------------------------------------------------------------------------------------------- |
|
|
269
|
+
| `OidcInitParameters<Data>` | Parameters for `initAuthorization` (endpoint, client details, scope, expiration, custom data). |
|
|
270
|
+
| `OidcInitResult` | Result of initialization, containing `authorizationEndpoint`, `state`, and PKCE `codeChallenge`. |
|
|
271
|
+
| `OidcToken<Raw>` | Represents the token response, including `accessToken`, `idToken`, `refreshToken`, and `raw` JSON. |
|
|
272
|
+
| `OidcGetTokenParameters` | Union type for token requests. Includes `authType` (`body` \| `basic-auth`). |
|
|
273
|
+
| `OidcRefreshTokenParameters` | Parameters for refreshing a token. |
|
|
274
|
+
| `OidcConfiguration` | Discovered provider endpoints (authorization, token, userInfo, etc.). |
|