@connectum/auth 1.0.0-rc.3

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 ADDED
@@ -0,0 +1,590 @@
1
+ # @connectum/auth
2
+
3
+ Authentication and authorization interceptors for Connectum.
4
+
5
+ **@connectum/auth** provides pluggable authentication, JWT verification, and declarative authorization for ConnectRPC services. Auth context propagates automatically via `AsyncLocalStorage` -- no manual parameter threading required.
6
+
7
+ ## Features
8
+
9
+ - **Generic auth interceptor** -- bring your own credential extractor and verifier (API keys, mTLS, custom tokens)
10
+ - **JWT auth interceptor** -- built-in JWT verification via [jose](https://github.com/panva/jose) with JWKS, HMAC, and asymmetric key support
11
+ - **Gateway auth interceptor** -- extract pre-authenticated identity from API gateway headers (Kong, Envoy, etc.) with header-based trust verification
12
+ - **Session auth interceptor** -- session-based authentication for frameworks like better-auth, lucia, etc.
13
+ - **Authorization interceptor** -- declarative RBAC rules with first-match semantics and programmatic fallback
14
+ - **AsyncLocalStorage context** -- zero-boilerplate access to auth context from any handler
15
+ - **Header propagation** -- cross-service auth context forwarding (Envoy-style `x-auth-*` headers)
16
+ - **LRU cache** -- in-memory credential verification caching with TTL expiration
17
+ - **Testing utilities** -- mock contexts, test JWTs, and context injection helpers via `@connectum/auth/testing`
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ pnpm add @connectum/auth
23
+ ```
24
+
25
+ **Peer dependencies**:
26
+
27
+ ```bash
28
+ pnpm add @connectrpc/connect
29
+ ```
30
+
31
+ ## Quick Start
32
+
33
+ ```typescript
34
+ import { createServer } from '@connectum/core';
35
+ import { createDefaultInterceptors } from '@connectum/interceptors';
36
+ import { createJwtAuthInterceptor } from '@connectum/auth';
37
+ import routes from '#gen/routes.js';
38
+
39
+ const jwtAuth = createJwtAuthInterceptor({
40
+ jwksUri: 'https://auth.example.com/.well-known/jwks.json',
41
+ issuer: 'https://auth.example.com/',
42
+ audience: 'my-api',
43
+ });
44
+
45
+ const server = createServer({
46
+ services: [routes],
47
+ port: 5000,
48
+ interceptors: [
49
+ ...createDefaultInterceptors(),
50
+ jwtAuth,
51
+ ],
52
+ });
53
+
54
+ await server.start();
55
+ ```
56
+
57
+ Access the authenticated user in any handler:
58
+
59
+ ```typescript
60
+ import { requireAuthContext } from '@connectum/auth';
61
+
62
+ const handler = {
63
+ async getProfile() {
64
+ const auth = requireAuthContext(); // throws Unauthenticated if missing
65
+ return { userId: auth.subject, roles: auth.roles };
66
+ },
67
+ };
68
+ ```
69
+
70
+ ## API Reference
71
+
72
+ ### createAuthInterceptor(options)
73
+
74
+ Generic authentication interceptor. Extracts credentials from the request, verifies them via a user-provided callback, and stores the resulting `AuthContext` in `AsyncLocalStorage`.
75
+
76
+ ```typescript
77
+ import { createAuthInterceptor } from '@connectum/auth';
78
+
79
+ const auth = createAuthInterceptor({
80
+ extractCredentials: (req) => req.header.get('x-api-key'),
81
+ verifyCredentials: async (apiKey) => {
82
+ const user = await db.findByApiKey(apiKey);
83
+ if (!user) throw new Error('Invalid API key');
84
+ return {
85
+ subject: user.id,
86
+ roles: user.roles,
87
+ scopes: [],
88
+ claims: {},
89
+ type: 'api-key',
90
+ };
91
+ },
92
+ skipMethods: ['grpc.health.v1.Health/*'],
93
+ });
94
+ ```
95
+
96
+ **Options (`AuthInterceptorOptions`)**:
97
+
98
+ | Option | Type | Default | Description |
99
+ |--------|------|---------|-------------|
100
+ | `verifyCredentials` | `(credentials: string) => AuthContext \| Promise<AuthContext>` | **required** | Verify credentials, return context. Must throw on failure. |
101
+ | `extractCredentials` | `(req: { header: Headers }) => string \| null \| Promise<string \| null>` | Bearer token from `Authorization` header | Extract credential string from request |
102
+ | `skipMethods` | `string[]` | `[]` | Methods to skip (`"Service/Method"` or `"Service/*"`) |
103
+ | `propagateHeaders` | `boolean` | `false` | Set `x-auth-*` headers for downstream services |
104
+ | `cache` | `CacheOptions` | - | LRU cache for credentials verification results. Caches AuthContext by credential string. |
105
+ | `propagatedClaims` | `string[]` | - | Filter which claim keys are propagated in `x-auth-claims` header (SEC-001). When not set, all claims are propagated. |
106
+
107
+ ### createJwtAuthInterceptor(options)
108
+
109
+ Convenience wrapper for JWT-based authentication. Handles token extraction from `Authorization: Bearer <token>`, verification via [jose](https://github.com/panva/jose), and standard claim mapping.
110
+
111
+ Key resolution priority: `jwksUri` > `secret` > `publicKey`.
112
+
113
+ A missing `sub` claim (and no `claimsMapping.subject` override) throws `ConnectError(Unauthenticated)` with message `"JWT missing subject claim"` (SEC-002).
114
+
115
+ ```typescript
116
+ import { createJwtAuthInterceptor } from '@connectum/auth';
117
+
118
+ const jwtAuth = createJwtAuthInterceptor({
119
+ jwksUri: 'https://auth.example.com/.well-known/jwks.json',
120
+ issuer: 'https://auth.example.com/',
121
+ audience: 'my-api',
122
+ claimsMapping: {
123
+ roles: 'realm_access.roles', // dot-notation for nested claims
124
+ scopes: 'scope',
125
+ },
126
+ skipMethods: ['grpc.health.v1.Health/*'],
127
+ });
128
+ ```
129
+
130
+ **Options (`JwtAuthInterceptorOptions`)**:
131
+
132
+ | Option | Type | Default | Description |
133
+ |--------|------|---------|-------------|
134
+ | `jwksUri` | `string` | - | JWKS endpoint URL for remote key set |
135
+ | `secret` | `string` | - | HMAC symmetric secret (HS256/HS384/HS512) |
136
+ | `publicKey` | `CryptoKey` | - | Asymmetric public key |
137
+ | `issuer` | `string \| string[]` | - | Expected issuer(s) |
138
+ | `audience` | `string \| string[]` | - | Expected audience(s) |
139
+ | `algorithms` | `string[]` | - | Allowed algorithms |
140
+ | `maxTokenAge` | `number \| string` | - | Maximum token age. Number (seconds) or string (e.g., `"2h"`, `"7d"`). Passed to jose `jwtVerify`. |
141
+ | `claimsMapping` | `{ subject?, name?, roles?, scopes? }` | `{}` | Map JWT claims to AuthContext (supports dot-notation) |
142
+ | `skipMethods` | `string[]` | `[]` | Methods to skip |
143
+ | `propagateHeaders` | `boolean` | `false` | Propagate auth context as headers |
144
+
145
+ At least one of `jwksUri`, `secret`, or `publicKey` is required.
146
+
147
+ ### createGatewayAuthInterceptor(options)
148
+
149
+ Authentication interceptor for services behind an API gateway (Kong, Envoy, AWS ALB, etc.) that has already performed authentication. Extracts auth context from gateway-injected headers after verifying trust.
150
+
151
+ Trust is established via a designated header (e.g., `x-gateway-secret`) rather than peer address, since ConnectRPC interceptors do not have access to peer info.
152
+
153
+ Gateway headers are **always** stripped from requests -- including skipped methods -- to prevent downstream spoofing.
154
+
155
+ ```typescript
156
+ import { createGatewayAuthInterceptor } from '@connectum/auth';
157
+
158
+ const gatewayAuth = createGatewayAuthInterceptor({
159
+ headerMapping: {
160
+ subject: 'x-user-id',
161
+ name: 'x-user-name',
162
+ roles: 'x-user-roles',
163
+ scopes: 'x-user-scopes',
164
+ },
165
+ trustSource: {
166
+ header: 'x-gateway-secret',
167
+ expectedValues: [process.env.GATEWAY_SECRET],
168
+ },
169
+ skipMethods: ['grpc.health.v1.Health/*'],
170
+ });
171
+ ```
172
+
173
+ **Options (`GatewayAuthInterceptorOptions`)**:
174
+
175
+ | Option | Type | Default | Description |
176
+ |--------|------|---------|-------------|
177
+ | `headerMapping` | `GatewayHeaderMapping` | **required** | Mapping from AuthContext fields to gateway header names |
178
+ | `trustSource` | `{ header: string; expectedValues: string[] }` | **required** | Trust verification: header name and accepted values (shared secrets or trusted IP ranges via CIDR) |
179
+ | `stripHeaders` | `string[]` | `[]` | Additional headers to strip from request after extraction |
180
+ | `skipMethods` | `string[]` | `[]` | Methods to skip authentication for (headers are still stripped) |
181
+ | `propagateHeaders` | `boolean` | `false` | Propagate auth context as `x-auth-*` headers for downstream services |
182
+ | `defaultType` | `string` | `"gateway"` | Default credential type when not provided by gateway |
183
+
184
+ **`GatewayHeaderMapping`**:
185
+
186
+ | Field | Type | Required | Description |
187
+ |-------|------|----------|-------------|
188
+ | `subject` | `string` | Yes | Header containing the authenticated subject |
189
+ | `name` | `string` | No | Header containing the display name |
190
+ | `roles` | `string` | No | Header containing JSON-encoded roles array (falls back to comma-separated parsing) |
191
+ | `scopes` | `string` | No | Header containing space-separated scopes |
192
+ | `type` | `string` | No | Header containing credential type |
193
+ | `claims` | `string` | No | Header containing JSON-encoded claims (ignored if >8192 bytes) |
194
+
195
+ Validation: `headerMapping.subject` must be non-empty, and `trustSource.expectedValues` must be non-empty. Both throw `Error` at construction time (fail-closed).
196
+
197
+ ### createSessionAuthInterceptor(options)
198
+
199
+ Session-based authentication interceptor for frameworks like [better-auth](https://www.better-auth.com/) and lucia. Implements a two-step verification flow:
200
+
201
+ 1. Extract token from request (default: `Authorization: Bearer <token>`)
202
+ 2. Verify session via user-provided callback -- receives both the token **and** full request headers for cookie-based auth support
203
+ 3. Map raw session data to `AuthContext` via user-provided mapper
204
+
205
+ ```typescript
206
+ import { createSessionAuthInterceptor } from '@connectum/auth';
207
+ import { betterAuth } from 'better-auth';
208
+
209
+ const auth = betterAuth({ /* DB adapter config */ });
210
+
211
+ const sessionAuth = createSessionAuthInterceptor({
212
+ verifySession: async (token, headers) => {
213
+ const session = await auth.api.getSession({ headers });
214
+ if (!session) throw new Error('Invalid session');
215
+ return session;
216
+ },
217
+ mapSession: (session) => ({
218
+ subject: session.user.id,
219
+ name: session.user.name,
220
+ roles: session.user.roles ?? [],
221
+ scopes: [],
222
+ claims: session.user,
223
+ type: 'session',
224
+ }),
225
+ cache: { ttl: 60_000 },
226
+ });
227
+ ```
228
+
229
+ **Options (`SessionAuthInterceptorOptions`)**:
230
+
231
+ | Option | Type | Default | Description |
232
+ |--------|------|---------|-------------|
233
+ | `verifySession` | `(token: string, headers: Headers) => unknown \| Promise<unknown>` | **required** | Verify session token and return raw session data. Receives full request headers for cookie support. Must throw on failure. |
234
+ | `mapSession` | `(session: unknown) => AuthContext \| Promise<AuthContext>` | **required** | Map raw session data to `AuthContext`. |
235
+ | `extractToken` | `(req: { header: Headers }) => string \| null \| Promise<string \| null>` | Bearer token from `Authorization` header | Custom token extraction |
236
+ | `cache` | `CacheOptions` | - | LRU cache for session verification results |
237
+ | `skipMethods` | `string[]` | `[]` | Methods to skip authentication for |
238
+ | `propagateHeaders` | `boolean` | `false` | Propagate auth context as `x-auth-*` headers for downstream services |
239
+ | `propagatedClaims` | `string[]` | - | Filter which claim keys are propagated in `x-auth-claims` header. When not set, all claims are propagated. |
240
+
241
+ ### createAuthzInterceptor(options)
242
+
243
+ Declarative rules-based authorization. Evaluates rules in order; first matching rule wins. Must run **after** an authentication interceptor.
244
+
245
+ ```typescript
246
+ import { createAuthzInterceptor } from '@connectum/auth';
247
+
248
+ const authz = createAuthzInterceptor({
249
+ defaultPolicy: 'deny',
250
+ rules: [
251
+ {
252
+ name: 'health-public',
253
+ methods: ['grpc.health.v1.Health/*'],
254
+ effect: 'allow',
255
+ },
256
+ {
257
+ name: 'admin-only',
258
+ methods: ['admin.v1.AdminService/*'],
259
+ effect: 'allow',
260
+ requires: { roles: ['admin'] },
261
+ },
262
+ {
263
+ name: 'users-read',
264
+ methods: ['user.v1.UserService/GetUser', 'user.v1.UserService/ListUsers'],
265
+ effect: 'allow',
266
+ requires: { scopes: ['read'] },
267
+ },
268
+ ],
269
+ });
270
+ ```
271
+
272
+ **Options (`AuthzInterceptorOptions`)**:
273
+
274
+ | Option | Type | Default | Description |
275
+ |--------|------|---------|-------------|
276
+ | `defaultPolicy` | `'allow' \| 'deny'` | `'deny'` | Policy when no rule matches |
277
+ | `rules` | `AuthzRule[]` | `[]` | Declarative rules (first match wins) |
278
+ | `authorize` | `(context, req) => boolean \| Promise<boolean>` | - | Programmatic fallback after rules |
279
+ | `skipMethods` | `string[]` | `[]` | Methods to skip authorization |
280
+
281
+ **AuthzRule**:
282
+
283
+ | Field | Type | Description |
284
+ |-------|------|-------------|
285
+ | `name` | `string` | Rule name (used in error messages) |
286
+ | `methods` | `string[]` | Method patterns: `"*"`, `"Service/*"`, `"Service/Method"` |
287
+ | `effect` | `'allow' \| 'deny'` | Effect when rule matches |
288
+ | `requires` | `{ roles?: string[], scopes?: string[] }` | Required roles (any-of) and/or scopes (all-of) |
289
+
290
+ ### getAuthContext() / requireAuthContext()
291
+
292
+ Access the authenticated user context set by an auth interceptor.
293
+
294
+ ```typescript
295
+ import { getAuthContext, requireAuthContext } from '@connectum/auth';
296
+
297
+ // Returns AuthContext | undefined
298
+ const auth = getAuthContext();
299
+
300
+ // Returns AuthContext, throws ConnectError(Unauthenticated) if missing
301
+ const auth = requireAuthContext();
302
+ ```
303
+
304
+ **AuthContext**:
305
+
306
+ | Field | Type | Description |
307
+ |-------|------|-------------|
308
+ | `subject` | `string` | User/service identifier |
309
+ | `name` | `string?` | Display name |
310
+ | `roles` | `readonly string[]` | Assigned roles |
311
+ | `scopes` | `readonly string[]` | Granted scopes |
312
+ | `claims` | `Record<string, unknown>` | Raw credential claims |
313
+ | `type` | `string` | Credential type (`"jwt"`, `"api-key"`, etc.) |
314
+ | `expiresAt` | `Date?` | Credential expiration |
315
+
316
+ ### LruCache
317
+
318
+ Minimal in-memory LRU cache with TTL expiration. Uses `Map` insertion order for LRU eviction. Used by `createAuthInterceptor` and `createSessionAuthInterceptor` for caching verification results.
319
+
320
+ ```typescript
321
+ import { LruCache } from '@connectum/auth';
322
+
323
+ const cache = new LruCache<{ userId: string }>({
324
+ ttl: 60_000, // 60 seconds
325
+ maxSize: 500, // default: 1000
326
+ });
327
+
328
+ cache.set('key', { userId: 'user-1' });
329
+ const value = cache.get('key'); // { userId: 'user-1' } or undefined (expired/missing)
330
+ cache.clear();
331
+ cache.size; // 0
332
+ ```
333
+
334
+ **Constructor**: `new LruCache<T>(options: { ttl: number; maxSize?: number })`
335
+
336
+ | Option | Type | Default | Description |
337
+ |--------|------|---------|-------------|
338
+ | `ttl` | `number` | **required** | Cache entry time-to-live in milliseconds. Must be positive (throws `RangeError`). |
339
+ | `maxSize` | `number` | `1000` | Maximum number of cached entries |
340
+
341
+ **Methods**:
342
+
343
+ | Method | Signature | Description |
344
+ |--------|-----------|-------------|
345
+ | `get` | `(key: string) => T \| undefined` | Get cached value. Returns `undefined` if missing or expired. Moves entry to most-recently-used. |
346
+ | `set` | `(key: string, value: T) => void` | Set a value. Evicts LRU entry if at capacity. |
347
+ | `clear` | `() => void` | Remove all entries |
348
+ | `size` | `number` (getter) | Current number of entries |
349
+
350
+ **`CacheOptions`** (used by `AuthInterceptorOptions.cache` and `SessionAuthInterceptorOptions.cache`):
351
+
352
+ | Field | Type | Default | Description |
353
+ |-------|------|---------|-------------|
354
+ | `ttl` | `number` | **required** | Cache entry time-to-live in milliseconds |
355
+ | `maxSize` | `number` | - | Maximum number of cached entries |
356
+
357
+ ### parseAuthHeaders(headers) / setAuthHeaders(headers, context, propagatedClaims?)
358
+
359
+ Serialize and deserialize `AuthContext` to/from HTTP headers for cross-service propagation.
360
+
361
+ ```typescript
362
+ import { parseAuthHeaders, setAuthHeaders } from '@connectum/auth';
363
+
364
+ // Read context from upstream headers (trusted environments only)
365
+ const context = parseAuthHeaders(req.header);
366
+
367
+ // Write context to outgoing headers
368
+ setAuthHeaders(outgoingHeaders, authContext);
369
+
370
+ // Write context with filtered claims (only propagate listed keys)
371
+ setAuthHeaders(outgoingHeaders, authContext, ['email', 'tenant_id']);
372
+ ```
373
+
374
+ `setAuthHeaders` silently drops roles, scopes, or claims values that exceed 8192 bytes to prevent header size abuse.
375
+
376
+ ### AUTH_HEADERS
377
+
378
+ Standard header names for auth context propagation:
379
+
380
+ | Constant | Value | Content |
381
+ |----------|-------|---------|
382
+ | `AUTH_HEADERS.SUBJECT` | `x-auth-subject` | Subject identifier |
383
+ | `AUTH_HEADERS.NAME` | `x-auth-name` | Display name |
384
+ | `AUTH_HEADERS.ROLES` | `x-auth-roles` | JSON-encoded roles array |
385
+ | `AUTH_HEADERS.SCOPES` | `x-auth-scopes` | Space-separated scopes |
386
+ | `AUTH_HEADERS.CLAIMS` | `x-auth-claims` | JSON-encoded claims object |
387
+ | `AUTH_HEADERS.TYPE` | `x-auth-type` | Credential type |
388
+
389
+ ### AuthzEffect
390
+
391
+ Authorization rule effect constants:
392
+
393
+ ```typescript
394
+ import { AuthzEffect } from '@connectum/auth';
395
+
396
+ AuthzEffect.ALLOW // 'allow'
397
+ AuthzEffect.DENY // 'deny'
398
+ ```
399
+
400
+ ## Interceptor Chain Order
401
+
402
+ Auth interceptors should be placed **after** the default interceptor chain (error handler, timeout, bulkhead, etc.) and **before** business logic:
403
+
404
+ ```text
405
+ errorHandler -> timeout -> bulkhead -> circuitBreaker -> retry -> validation -> auth -> authz -> handler
406
+ ```
407
+
408
+ ```typescript
409
+ import { createServer } from '@connectum/core';
410
+ import { createDefaultInterceptors } from '@connectum/interceptors';
411
+ import { createJwtAuthInterceptor, createAuthzInterceptor } from '@connectum/auth';
412
+
413
+ const server = createServer({
414
+ services: [routes],
415
+ interceptors: [
416
+ ...createDefaultInterceptors(),
417
+ createJwtAuthInterceptor({ secret: process.env.JWT_SECRET }),
418
+ createAuthzInterceptor({ defaultPolicy: 'deny', rules: [...] }),
419
+ ],
420
+ });
421
+ ```
422
+
423
+ ## Testing
424
+
425
+ The `@connectum/auth/testing` sub-export provides utilities for testing authenticated handlers and services.
426
+
427
+ ```bash
428
+ # Imported separately from the main package
429
+ import { ... } from '@connectum/auth/testing';
430
+ ```
431
+
432
+ ### createMockAuthContext(overrides?)
433
+
434
+ Create an `AuthContext` with sensible defaults. Overrides are shallow-merged.
435
+
436
+ ```typescript
437
+ import { createMockAuthContext } from '@connectum/auth/testing';
438
+
439
+ const ctx = createMockAuthContext();
440
+ // { subject: 'test-user', name: 'Test User', roles: ['user'], scopes: ['read'], claims: {}, type: 'test' }
441
+
442
+ const admin = createMockAuthContext({ subject: 'admin-1', roles: ['admin'] });
443
+ ```
444
+
445
+ ### createTestJwt(payload, options?)
446
+
447
+ Create a signed HS256 JWT for integration tests. Uses a deterministic test secret.
448
+
449
+ ```typescript
450
+ import { createTestJwt, TEST_JWT_SECRET } from '@connectum/auth/testing';
451
+ import { createJwtAuthInterceptor } from '@connectum/auth';
452
+
453
+ const token = await createTestJwt(
454
+ { sub: 'user-123', roles: ['admin'], scope: 'read write' },
455
+ { expiresIn: '1h', issuer: 'test' },
456
+ );
457
+
458
+ // Wire up the interceptor with the test secret
459
+ const auth = createJwtAuthInterceptor({ secret: TEST_JWT_SECRET, issuer: 'test' });
460
+ ```
461
+
462
+ **Options:**
463
+
464
+ | Option | Type | Default | Description |
465
+ |--------|------|---------|-------------|
466
+ | `expiresIn` | `string` | `'1h'` | Expiration (jose duration format) |
467
+ | `issuer` | `string` | - | Token issuer |
468
+ | `audience` | `string` | - | Token audience |
469
+
470
+ ### withAuthContext(context, fn)
471
+
472
+ Run a function with a pre-set `AuthContext` in `AsyncLocalStorage`. Use this to test handlers that call `getAuthContext()` or `requireAuthContext()`.
473
+
474
+ ```typescript
475
+ import { withAuthContext, createMockAuthContext } from '@connectum/auth/testing';
476
+ import { requireAuthContext } from '@connectum/auth';
477
+
478
+ await withAuthContext(createMockAuthContext({ subject: 'user-1' }), async () => {
479
+ const auth = requireAuthContext();
480
+ assert.strictEqual(auth.subject, 'user-1');
481
+ });
482
+ ```
483
+
484
+ ### TEST_JWT_SECRET
485
+
486
+ Deterministic HMAC secret for test JWTs: `"connectum-test-secret-do-not-use-in-production"`.
487
+
488
+ ## Integration with better-auth
489
+
490
+ [better-auth](https://www.better-auth.com/) is a modern authentication framework for TypeScript. It supports programmatic session verification and works directly with `createSessionAuthInterceptor`.
491
+
492
+ ```typescript
493
+ import { betterAuth } from "better-auth";
494
+ import { createSessionAuthInterceptor } from '@connectum/auth';
495
+
496
+ const auth = betterAuth({ /* DB adapter config */ });
497
+
498
+ const betterAuthInterceptor = createSessionAuthInterceptor({
499
+ verifySession: async (token, headers) => {
500
+ const session = await auth.api.getSession({ headers });
501
+ if (!session) throw new Error("Invalid session");
502
+ return session;
503
+ },
504
+ mapSession: (session) => ({
505
+ subject: session.user.id,
506
+ name: session.user.name,
507
+ roles: session.user.roles ?? [],
508
+ scopes: [],
509
+ claims: session.user,
510
+ type: "better-auth",
511
+ }),
512
+ cache: { ttl: 60_000 },
513
+ });
514
+ ```
515
+
516
+ ## Security Considerations
517
+
518
+ - **Header stripping**: `createAuthInterceptor` and `createSessionAuthInterceptor` strip all `x-auth-*` headers from incoming requests to prevent external spoofing. `createGatewayAuthInterceptor` strips all mapped gateway headers unconditionally -- including on skipped methods.
519
+ - **Header size limits**: `setAuthHeaders` silently drops roles, scopes, or claims values exceeding 8192 bytes. Gateway interceptor also ignores claims headers exceeding 8192 bytes.
520
+ - **Fail-closed trust**: `createGatewayAuthInterceptor` requires a non-empty `expectedValues` list and a non-empty `subject` mapping at construction time. Missing or mismatched trust header results in `Unauthenticated`.
521
+ - **JWT subject enforcement** (SEC-002): `createJwtAuthInterceptor` throws `Unauthenticated` when the JWT has no `sub` claim and no `claimsMapping.subject` override.
522
+ - **Claims filtering** (SEC-001): Use `propagatedClaims` to limit which claim keys are included in propagated `x-auth-claims` headers, preventing accidental leakage of sensitive token data.
523
+ - **HMAC key validation**: `createJwtAuthInterceptor` enforces minimum HMAC key sizes per RFC 7518 (32/48/64 bytes for HS256/HS384/HS512).
524
+
525
+ ## Exports Summary
526
+
527
+ ### Main export (`@connectum/auth`)
528
+
529
+ **Interceptor factories**:
530
+ - `createAuthInterceptor` -- generic pluggable authentication
531
+ - `createJwtAuthInterceptor` -- JWT convenience with jose
532
+ - `createGatewayAuthInterceptor` -- gateway-injected headers
533
+ - `createSessionAuthInterceptor` -- session-based auth
534
+ - `createAuthzInterceptor` -- declarative rules-based authorization
535
+
536
+ **Context management**:
537
+ - `getAuthContext` -- get current AuthContext (or undefined)
538
+ - `requireAuthContext` -- get current AuthContext (or throw)
539
+ - `authContextStorage` -- raw AsyncLocalStorage instance
540
+
541
+ **Header utilities**:
542
+ - `parseAuthHeaders` -- deserialize AuthContext from headers
543
+ - `setAuthHeaders` -- serialize AuthContext to headers
544
+ - `AUTH_HEADERS` -- standard header name constants
545
+
546
+ **Cache**:
547
+ - `LruCache` -- in-memory LRU cache with TTL
548
+
549
+ **Authorization**:
550
+ - `AuthzEffect` -- rule effect constants (`ALLOW`, `DENY`)
551
+ - `AuthzDeniedError` -- authorization denied error class
552
+ - `matchesMethodPattern` -- method pattern matching utility
553
+
554
+ **Types** (TypeScript only):
555
+ - `AuthContext`
556
+ - `AuthInterceptorOptions`
557
+ - `JwtAuthInterceptorOptions`
558
+ - `GatewayAuthInterceptorOptions`
559
+ - `GatewayHeaderMapping`
560
+ - `SessionAuthInterceptorOptions`
561
+ - `AuthzInterceptorOptions`
562
+ - `AuthzRule`
563
+ - `CacheOptions`
564
+ - `InterceptorFactory`
565
+ - `AuthzDeniedDetails`
566
+
567
+ ### Testing export (`@connectum/auth/testing`)
568
+
569
+ - `createMockAuthContext` -- create AuthContext with defaults
570
+ - `createTestJwt` -- create signed HS256 test JWT
571
+ - `withAuthContext` -- run code with injected AuthContext
572
+ - `TEST_JWT_SECRET` -- deterministic test secret
573
+
574
+ ## Dependencies
575
+
576
+ - `@connectrpc/connect` -- ConnectRPC core (peer dependency)
577
+ - `jose` -- JWT/JWK/JWS verification
578
+
579
+ ## Requirements
580
+
581
+ - **Node.js**: >=18.0.0
582
+ - **TypeScript**: >=5.7.2 (for type checking)
583
+
584
+ ## License
585
+
586
+ Apache-2.0
587
+
588
+ ---
589
+
590
+ **Part of [@connectum](../../README.md)** -- Universal framework for production-ready gRPC/ConnectRPC microservices