@kb-labs/gateway-auth 2.94.0 → 2.96.0

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/dist/index.d.ts CHANGED
@@ -1,5 +1,9 @@
1
1
  import { ICache } from '@kb-labs/core-platform';
2
2
  import { TokenType, JwtPayload, RegisterRequest, RegisterResponse, TokenResponse, AuthContext } from '@kb-labs/gateway-contracts';
3
+ import { IDocumentDatabase, IKVStore } from '@kb-labs/core-platform/adapters';
4
+ import { IdentityUserPort, IdentityCredentialPort, IIdentityProvider, IdentityProviderDeps, IRedirectIdentityProvider, IdentityProviderFactory, IPolicyDecisionPoint } from '@kb-labs/core-contracts';
5
+ export { IPolicyDecisionPoint, Identity, PolicyContext, PolicyDecision, Resource } from '@kb-labs/core-contracts';
6
+ import { z } from 'zod';
3
7
 
4
8
  /**
5
9
  * JWT sign / verify / refresh logic.
@@ -15,6 +19,8 @@ interface SignAccessTokenOptions {
15
19
  namespaceId: string;
16
20
  tier: 'free' | 'pro' | 'enterprise';
17
21
  type: TokenType;
22
+ /** Permissions to embed in the JWT payload. Defaults to [] when absent. */
23
+ permissions?: string[];
18
24
  }
19
25
  declare function signAccessToken(opts: SignAccessTokenOptions, config: JwtConfig): Promise<{
20
26
  token: string;
@@ -25,6 +31,52 @@ declare function verifyAccessToken(token: string, config: JwtConfig): Promise<Jw
25
31
  declare function verifyRefreshToken(token: string, config: JwtConfig): Promise<{
26
32
  hostId: string;
27
33
  } | null>;
34
+ interface SignUserTokenOptions {
35
+ userId: string;
36
+ tenantId: string;
37
+ familyId: string;
38
+ /** TTL in seconds. */
39
+ ttlSec: number;
40
+ }
41
+ interface UserAccessPayload {
42
+ userId: string;
43
+ tenantId: string;
44
+ familyId: string;
45
+ jti: string;
46
+ /** Token type discriminator. Access tokens carry 'user'. */
47
+ type: 'user';
48
+ iat: number;
49
+ exp: number;
50
+ }
51
+ interface UserRefreshPayload {
52
+ userId: string;
53
+ tenantId: string;
54
+ familyId: string;
55
+ jti: string;
56
+ type: 'refresh';
57
+ iat: number;
58
+ exp: number;
59
+ }
60
+ interface UserAccessSignResult {
61
+ token: string;
62
+ expiresInSec: number;
63
+ jti: string;
64
+ }
65
+ declare function signUserAccessToken(opts: SignUserTokenOptions, config: JwtConfig): Promise<UserAccessSignResult>;
66
+ /**
67
+ * Sign a user refresh token.
68
+ *
69
+ * When the caller already owns the `jti` (typical: the sessions-store
70
+ * minted it inside `createSession`/`rotateRefresh`), pass it via the
71
+ * second argument so the JWT and the store agree on the identifier.
72
+ * Otherwise the helper generates one — used by tests that don't go
73
+ * through sessions-store.
74
+ */
75
+ declare function signUserRefreshToken(opts: SignUserTokenOptions & {
76
+ jti?: string;
77
+ }, config: JwtConfig): Promise<string>;
78
+ declare function verifyUserAccessToken(token: string, config: JwtConfig): Promise<UserAccessPayload | null>;
79
+ declare function verifyUserRefreshToken(token: string, config: JwtConfig): Promise<UserRefreshPayload | null>;
28
80
 
29
81
  /**
30
82
  * AuthService — orchestrates registration, token issuance, and refresh.
@@ -40,6 +92,11 @@ declare class AuthService {
40
92
  }>;
41
93
  issueTokens(clientId: string, clientSecret: string): Promise<TokenResponse | null>;
42
94
  refreshTokens(refreshToken: string): Promise<TokenResponse | null>;
95
+ me(hostId: string): Promise<{
96
+ hostId: string;
97
+ handle?: string;
98
+ namespaceId: string;
99
+ } | null>;
43
100
  verify(token: string): Promise<AuthContext | null>;
44
101
  }
45
102
 
@@ -50,6 +107,7 @@ declare class AuthService {
50
107
  * auth:client:{clientId} → ClientRecord (permanent)
51
108
  * auth:refresh:{tokenHash} → { hostId } (TTL 30d)
52
109
  * auth:publickey:{hostId} → string (base64url X25519 public key)
110
+ * auth:handle:{handle} → clientId (permanent, handle index)
53
111
  */
54
112
 
55
113
  interface ClientRecord {
@@ -61,22 +119,32 @@ interface ClientRecord {
61
119
  tier: 'free' | 'pro' | 'enterprise';
62
120
  name: string;
63
121
  capabilities: string[];
122
+ /** Permissions embedded in issued JWTs. If absent, defaults to ['host:connect']. */
123
+ permissions?: string[];
64
124
  publicKey?: string;
65
125
  createdAt: number;
126
+ /** Unique human-readable handle for the marketplace (e.g. "kirill"). Immutable once set. */
127
+ handle?: string;
128
+ email?: string;
66
129
  }
67
130
  declare function generateClientId(): string;
68
131
  declare function generateClientSecret(): string;
69
132
  declare function generateHostId(): string;
70
133
  declare function saveClient(cache: ICache, record: ClientRecord): Promise<void>;
134
+ declare function isHandleTaken(cache: ICache, handle: string): Promise<boolean>;
135
+ declare function getClientByHandle(cache: ICache, handle: string): Promise<ClientRecord | null>;
71
136
  declare function getClientByHostId(cache: ICache, hostId: string): Promise<ClientRecord | null>;
72
137
  declare function getClient(cache: ICache, clientId: string): Promise<ClientRecord | null>;
73
138
  declare function verifyClientSecret(cache: ICache, clientId: string, secret: string): Promise<ClientRecord | null>;
74
139
  declare function buildClientRecord(opts: {
75
140
  name: string;
76
141
  capabilities: string[];
142
+ permissions?: string[];
77
143
  publicKey?: string;
78
144
  secret: string;
79
145
  namespaceId?: string;
146
+ handle?: string;
147
+ email?: string;
80
148
  }): ClientRecord;
81
149
  declare function saveRefreshToken(cache: ICache, token: string, hostId: string, namespaceId: string): Promise<void>;
82
150
  declare function consumeRefreshToken(cache: ICache, token: string): Promise<{
@@ -86,4 +154,909 @@ declare function consumeRefreshToken(cache: ICache, token: string): Promise<{
86
154
  declare function savePublicKey(cache: ICache, hostId: string, publicKey: string): Promise<void>;
87
155
  declare function getPublicKey(cache: ICache, hostId: string): Promise<string | null>;
88
156
 
89
- export { AuthService, type ClientRecord, type JwtConfig, buildClientRecord, consumeRefreshToken, generateClientId, generateClientSecret, generateHostId, getClient, getClientByHostId, getPublicKey, saveClient, savePublicKey, saveRefreshToken, signAccessToken, signRefreshToken, verifyAccessToken, verifyClientSecret, verifyRefreshToken };
157
+ /**
158
+ * @module @kb-labs/gateway-auth/users-store
159
+ *
160
+ * Document-backed store for `User` records (ADR-0020, Phase 1.1).
161
+ *
162
+ * Schema invariants:
163
+ * - `(tenantId, email)` is a unique compound — one user per email per
164
+ * tenant. The same email can exist in different tenants.
165
+ * - `email` is **always** stored lowercased + trimmed (CD-4). Callers
166
+ * may pass any casing; the store normalises before write/read.
167
+ * - **No** password hash on this document. Credentials live in the
168
+ * separate `credentials` collection (CD-6) so a future Google/Okta
169
+ * provider can attach to the same `User` without a schema migration.
170
+ *
171
+ * Pattern follows `plugins/gateway/core/src/stores/host-store.ts`:
172
+ * one-shot `ensureSchema()` plus per-method idempotent `ensureCollection`
173
+ * registration.
174
+ */
175
+
176
+ type UserStatus = 'pending' | 'active' | 'disabled';
177
+ interface User {
178
+ userId: string;
179
+ tenantId: string;
180
+ /** Lowercase + trimmed (CD-4). */
181
+ email: string;
182
+ displayName?: string;
183
+ status: UserStatus;
184
+ createdAt: number;
185
+ updatedAt?: number;
186
+ }
187
+ /**
188
+ * Canonical email form used everywhere this store touches.
189
+ *
190
+ * Exported so other modules (the email-password provider, invites-store)
191
+ * normalise identically — having one function prevents drift where one
192
+ * caller forgets to trim and lookups silently miss.
193
+ */
194
+ declare const canonicalizeEmail: (raw: string) => string;
195
+ interface CreateUserInput {
196
+ userId: string;
197
+ tenantId: string;
198
+ email: string;
199
+ status: UserStatus;
200
+ displayName?: string;
201
+ }
202
+ declare class UsersStore {
203
+ private readonly docs;
204
+ private initialised;
205
+ constructor(docs: IDocumentDatabase);
206
+ private ensureSchema;
207
+ create(input: CreateUserInput): Promise<User>;
208
+ getById(userId: string): Promise<User | null>;
209
+ findByEmailTenant(email: string, tenantId: string): Promise<User | null>;
210
+ setStatus(userId: string, status: UserStatus): Promise<void>;
211
+ /** List all users for a given tenant. */
212
+ listByTenant(tenantId: string): Promise<User[]>;
213
+ delete(userId: string): Promise<void>;
214
+ }
215
+
216
+ /**
217
+ * @module @kb-labs/gateway-auth/credentials-store
218
+ *
219
+ * Document-backed store for per-provider credentials (ADR-0020, CD-6,
220
+ * Phase 1.2).
221
+ *
222
+ * `User` does not carry a password hash. Credentials live here, keyed by
223
+ * the compound `(userId, providerId)`. Adding a new identity provider
224
+ * (Google, Okta, LDAP, ...) means writing a row with a new `providerId`
225
+ * — no schema migration on the `users` collection, no `passwordHash?:
226
+ * null` quirks.
227
+ *
228
+ * Hash format and algorithm are the provider's concern: this store only
229
+ * persists opaque strings. The `email-password` provider chooses bcrypt
230
+ * with a configured cost.
231
+ */
232
+
233
+ interface Credential {
234
+ userId: string;
235
+ providerId: string;
236
+ /** Opaque to this store — bcrypt hash for email-password, OAuth refresh blob for future providers, etc. */
237
+ hash: string;
238
+ createdAt: number;
239
+ updatedAt?: number;
240
+ }
241
+ interface SetCredentialInput {
242
+ userId: string;
243
+ providerId: string;
244
+ hash: string;
245
+ }
246
+ declare class CredentialsStore {
247
+ private readonly docs;
248
+ private initialised;
249
+ constructor(docs: IDocumentDatabase);
250
+ private ensureSchema;
251
+ setCredential(input: SetCredentialInput): Promise<void>;
252
+ getCredential(userId: string, providerId: string): Promise<Credential | null>;
253
+ deleteCredential(userId: string, providerId: string): Promise<void>;
254
+ /**
255
+ * Cascade removal helper — call from the parent flow when a `User` is
256
+ * deleted so we never leave dangling credentials in the store.
257
+ */
258
+ deleteAllForUser(userId: string): Promise<void>;
259
+ }
260
+
261
+ /**
262
+ * @module @kb-labs/gateway-auth/memberships-store
263
+ *
264
+ * Document-backed store for `Membership` records (ADR-0020, Phase 1.3).
265
+ *
266
+ * A membership is one row per `(userId, tenantId)` with a single
267
+ * `groupId`. Group strings are hardcoded for this iteration —
268
+ * `tenant-admin` and `tenant-member` — and consumed by the stub PDP.
269
+ *
270
+ * When the real RBAC + ReBAC engine lands (ClickUp 869def338) the
271
+ * meaning of `groupId` becomes richer (multiple group memberships,
272
+ * group hierarchies, relations) but the store API stays — the engine
273
+ * sits behind the PDP and reads from here. The "one membership per
274
+ * (userId, tenantId)" invariant may need to flex later; for now we keep
275
+ * it tight so the stub PDP has unambiguous lookups.
276
+ */
277
+
278
+ /** Hardcoded group ids for the stub PDP iteration. */
279
+ type GroupId = 'tenant-admin' | 'tenant-member';
280
+ interface Membership {
281
+ userId: string;
282
+ tenantId: string;
283
+ groupId: GroupId;
284
+ createdAt: number;
285
+ updatedAt?: number;
286
+ }
287
+ interface AddMembershipInput {
288
+ userId: string;
289
+ tenantId: string;
290
+ groupId: GroupId;
291
+ }
292
+ declare class MembershipsStore {
293
+ private readonly docs;
294
+ private initialised;
295
+ constructor(docs: IDocumentDatabase);
296
+ private ensureSchema;
297
+ addMembership(input: AddMembershipInput): Promise<void>;
298
+ setGroup(userId: string, tenantId: string, groupId: GroupId): Promise<void>;
299
+ listByUser(userId: string): Promise<Membership[]>;
300
+ listByTenant(tenantId: string): Promise<Membership[]>;
301
+ removeMembership(userId: string, tenantId: string): Promise<void>;
302
+ /** Cascade hook — call when a `User` is deleted. */
303
+ removeAllForUser(userId: string): Promise<void>;
304
+ }
305
+
306
+ /**
307
+ * @module @kb-labs/gateway-auth/invites-store
308
+ *
309
+ * Document-backed store for activation invites (ADR-0020, Phase 1.4).
310
+ *
311
+ * Invites are admin-issued tokens that bootstrap a user account: the
312
+ * recipient activates by POSTing the token along with a password.
313
+ *
314
+ * Security details worth keeping straight:
315
+ *
316
+ * - Token plaintext **never** lands in the database. We store
317
+ * `tokenHash = sha256(plain)` and look up by hash. A snapshot of the
318
+ * `invites` collection is useless without the original tokens.
319
+ * - The unique compound `(tenantId, email, status='active')` is not
320
+ * easy to express across drivers, so uniqueness is enforced in code
321
+ * on top of a non-unique compound index. Concurrent races during
322
+ * `createInvite` are not a real attack vector here (admin UI is
323
+ * single-clicked) and we accept the small theoretical window.
324
+ * - TTL is best-effort on the index; **every read path** explicitly
325
+ * checks `expiresAt > now` (CD-9). Without that, a not-yet-swept row
326
+ * would be returned and the activation flow would accept it.
327
+ * - Emails are canonicalised on the way in (CD-4) so admins can paste
328
+ * "Alice@Example.COM" without breaking lookups.
329
+ */
330
+
331
+ type InviteStatus = 'active' | 'used' | 'revoked';
332
+ interface InviteRecord {
333
+ inviteId: string;
334
+ email: string;
335
+ tenantId: string;
336
+ groupId: GroupId;
337
+ status: InviteStatus;
338
+ createdBy: string;
339
+ createdAt: number;
340
+ expiresAt: number;
341
+ updatedAt?: number;
342
+ }
343
+ interface CreateInviteInput {
344
+ email: string;
345
+ tenantId: string;
346
+ groupId: GroupId;
347
+ createdBy: string;
348
+ ttlMs: number;
349
+ }
350
+ interface CreateInviteResult {
351
+ inviteId: string;
352
+ activationToken: string;
353
+ expiresAt: number;
354
+ }
355
+ declare class InvitesStore {
356
+ private readonly docs;
357
+ private readonly now;
358
+ private initialised;
359
+ constructor(docs: IDocumentDatabase, now?: () => number);
360
+ private ensureSchema;
361
+ createInvite(input: CreateInviteInput): Promise<CreateInviteResult>;
362
+ findById(inviteId: string): Promise<InviteRecord | null>;
363
+ /**
364
+ * Look up an invite by the plaintext activation token. Returns `null`
365
+ * for missing, expired, used, or revoked invites — callers do not
366
+ * need to filter further.
367
+ *
368
+ * Returns a discriminated result so callers can distinguish:
369
+ * - { kind: 'not_found' } — token hash not in DB (garbage/unknown token)
370
+ * - { kind: 'invalid' } — token exists but expired, used, or revoked
371
+ * - { kind: 'ok', invite } — valid, active invite ready for activation
372
+ */
373
+ findByToken(plainToken: string): Promise<{
374
+ kind: 'not_found';
375
+ } | {
376
+ kind: 'invalid';
377
+ } | {
378
+ kind: 'ok';
379
+ invite: InviteRecord;
380
+ }>;
381
+ /**
382
+ * Atomically mark an active invite as used.
383
+ *
384
+ * Returns `true` if THIS call flipped the invite from `active` → `used`,
385
+ * `false` if it was already used/revoked (or unknown). The filter pins
386
+ * `status: 'active'`, so a concurrent double-activation of the same token
387
+ * results in exactly one caller seeing `true` — the activation flow relies
388
+ * on this to close the TOCTOU window between `findByToken` and account
389
+ * creation (a second parallel request must NOT create a duplicate account).
390
+ */
391
+ consume(inviteId: string): Promise<boolean>;
392
+ revoke(inviteId: string): Promise<void>;
393
+ /** List all invites for a tenant (active + used + revoked). */
394
+ listByTenant(tenantId: string): Promise<InviteRecord[]>;
395
+ }
396
+
397
+ /**
398
+ * @module @kb-labs/gateway-auth/sessions-store
399
+ *
400
+ * Session families + refresh tokens (ADR-0020, Phase 1.5).
401
+ *
402
+ * This module owns three things:
403
+ *
404
+ * 1. **Session families.** One row per device. The family is the unit
405
+ * of revocation — "log out this device", "kick this user from all
406
+ * devices", "the password just changed, keep the current device".
407
+ * 2. **Refresh tokens.** One-shot rotation: every refresh request
408
+ * consumes the old jti and issues a new one in the same family.
409
+ * 3. **Reuse detection + 5-second grace window (CD-5).** Reusing an
410
+ * already-consumed refresh is either a stolen cookie or a legit
411
+ * multi-tab race. The grace window resolves the ambiguity: if the
412
+ * client presents the same already-consumed jti within 5s, we
413
+ * return the same replacement we issued first time. Beyond 5s, we
414
+ * treat it as theft and kill the entire family.
415
+ *
416
+ * Notes on storage:
417
+ *
418
+ * - Refresh tokens carry an explicit `expiresAt` and we check it
419
+ * manually on every read (CD-9) — the TTL index sweep is best-effort.
420
+ * - `rotateRefresh` runs inside a `transaction(cb)` so a concurrent
421
+ * second caller sees the consumed state and goes through the
422
+ * grace/reuse branch instead of inserting a parallel replacement.
423
+ */
424
+
425
+ interface SessionFamily {
426
+ familyId: string;
427
+ userId: string;
428
+ tenantId: string;
429
+ createdAt: number;
430
+ lastUsedAt: number;
431
+ userAgent?: string;
432
+ ipFirst?: string;
433
+ }
434
+ declare class RefreshNotFoundError extends Error {
435
+ constructor();
436
+ }
437
+ declare class RefreshExpiredError extends Error {
438
+ constructor();
439
+ }
440
+ declare class RefreshReuseDetectedError extends Error {
441
+ readonly familyId: string;
442
+ constructor(familyId: string);
443
+ }
444
+ interface SessionsStoreOptions {
445
+ /** Override `Date.now` — only used in tests. Defaults to `Date.now`. */
446
+ now?: () => number;
447
+ /** Refresh token TTL in ms. */
448
+ refreshTtlMs: number;
449
+ /** Grace window for legitimate retries (CD-5). Recommended 5_000ms. */
450
+ graceWindowMs: number;
451
+ }
452
+ interface CreateSessionInput {
453
+ userId: string;
454
+ tenantId: string;
455
+ deviceCtx: {
456
+ userAgent?: string;
457
+ ip?: string;
458
+ };
459
+ }
460
+ interface CreateSessionResult {
461
+ familyId: string;
462
+ refreshJti: string;
463
+ refreshExpiresAt: number;
464
+ }
465
+ interface RotateRefreshResult {
466
+ newJti: string;
467
+ familyId: string;
468
+ newRefreshExpiresAt: number;
469
+ }
470
+ declare class SessionsStore {
471
+ private readonly docs;
472
+ private initialised;
473
+ private readonly now;
474
+ private readonly refreshTtlMs;
475
+ private readonly graceWindowMs;
476
+ constructor(docs: IDocumentDatabase, opts: SessionsStoreOptions);
477
+ private ensureSchema;
478
+ createSession(input: CreateSessionInput): Promise<CreateSessionResult>;
479
+ /**
480
+ * Consume `oldJti` and issue a new refresh in the same family.
481
+ *
482
+ * Throws:
483
+ * - `RefreshNotFoundError` if jti is unknown or its family was revoked.
484
+ * - `RefreshExpiredError` if jti is past `expiresAt` (family stays alive).
485
+ * - `RefreshReuseDetectedError` if jti was already consumed and the
486
+ * grace window has passed — family is killed before throwing.
487
+ *
488
+ * On grace-window retry (same already-consumed jti within
489
+ * `graceWindowMs`) the previously-issued replacement is returned and
490
+ * no new refresh is created.
491
+ */
492
+ rotateRefresh(oldJti: string): Promise<RotateRefreshResult>;
493
+ revokeFamily(familyId: string): Promise<void>;
494
+ revokeAllUserSessions(userId: string): Promise<void>;
495
+ revokeAllUserSessionsExcept(userId: string, exceptFamilyId: string): Promise<void>;
496
+ listFamiliesByUser(userId: string): Promise<SessionFamily[]>;
497
+ }
498
+
499
+ /**
500
+ * @module @kb-labs/gateway-auth/password-policy
501
+ *
502
+ * Password validation rules (ADR-0020, Phase 1.6).
503
+ *
504
+ * Length: 8..256. No complexity rules — NIST 800-63B explicitly
505
+ * recommends against them.
506
+ *
507
+ * HIBP check via k-anonymity (https://haveibeenpwned.com/API/v3#PwnedPasswords):
508
+ * we send the first 5 hex chars of SHA-1(password) and grep the
509
+ * suffix list locally. The full password never leaves the process.
510
+ *
511
+ * Availability vs. UX trade-off: HIBP outages return `{ ok: true,
512
+ * warning: 'hibp_unavailable' }`. A security check that hard-fails on
513
+ * network glitches breaks more user flows than it protects — we log
514
+ * the warning and let the activation/change-password flow continue.
515
+ */
516
+ type ValidationResult = {
517
+ ok: true;
518
+ warning?: 'hibp_unavailable';
519
+ } | {
520
+ ok: false;
521
+ reason: 'too_short' | 'too_long' | 'pwned';
522
+ };
523
+ interface PasswordPolicyLogger {
524
+ warn(...args: unknown[]): void;
525
+ info(...args: unknown[]): void;
526
+ error(...args: unknown[]): void;
527
+ }
528
+ interface PasswordPolicyOptions {
529
+ minLength: number;
530
+ maxLength: number;
531
+ hibpEnabled: boolean;
532
+ /** Override for tests. Defaults to global `fetch`. */
533
+ fetch?: typeof fetch;
534
+ /** Override for tests / production logger. Defaults to console-shaped no-op. */
535
+ logger?: PasswordPolicyLogger;
536
+ }
537
+ interface PasswordPolicy {
538
+ validate(plain: string): Promise<ValidationResult>;
539
+ }
540
+ declare const createPasswordPolicy: (opts: PasswordPolicyOptions) => PasswordPolicy;
541
+
542
+ /**
543
+ * @module @kb-labs/gateway-auth/csrf
544
+ *
545
+ * Double-submit CSRF helpers (ADR-0020, Phase 1.7).
546
+ *
547
+ * The contract is intentionally minimal: a token is just an opaque
548
+ * random string and verification is a constant-time string compare.
549
+ * No server-side state, no rotation logic — the cookie + header pair
550
+ * is the entire protocol.
551
+ *
552
+ * The `Path=/` non-HttpOnly cookie is set in the route layer
553
+ * alongside `kb_access`. Studio JS reads it via `document.cookie` and
554
+ * echoes it via `X-CSRF-Token` on every mutating request.
555
+ */
556
+ /**
557
+ * Generate a fresh 32-byte token, base64url-encoded.
558
+ */
559
+ declare const issueCsrfToken: () => string;
560
+ /**
561
+ * Verify the double-submit pair.
562
+ *
563
+ * Returns `true` only when both inputs are non-empty strings of the
564
+ * same length and equal under a constant-time compare. Length
565
+ * mismatches return `false` without involving timingSafeEqual (which
566
+ * throws on length mismatch).
567
+ */
568
+ declare const verifyCsrfToken: (cookie: string | undefined, header: string | undefined) => boolean;
569
+
570
+ /**
571
+ * @module @kb-labs/gateway-auth/rate-limit
572
+ *
573
+ * Fixed-window rate limiter on top of `IKVStore` (ADR-0020, Phase 1.8).
574
+ *
575
+ * Used for per-email login limits and activation rate-limiting.
576
+ * Per-IP limits are configured at the gateway layer itself (its
577
+ * existing rate-limit plugin) and are not touched here.
578
+ *
579
+ * Fixed window, not sliding: TTL is applied **once** on the first
580
+ * increment and never extended. Subsequent increments within the
581
+ * window just bump the counter. Once the key expires, the counter
582
+ * starts over.
583
+ *
584
+ * The choice of fixed vs sliding matters: a sliding window would mean
585
+ * "5 attempts in any 60-second window", which an attacker can game by
586
+ * pacing exactly under the limit forever. Fixed window puts a hard
587
+ * ceiling per window and the counter actually resets.
588
+ */
589
+
590
+ interface RateLimitConfig {
591
+ max: number;
592
+ windowMs: number;
593
+ }
594
+ type RateLimitResult = {
595
+ allowed: true;
596
+ remaining: number;
597
+ } | {
598
+ allowed: false;
599
+ retryAfterSec: number;
600
+ };
601
+ interface RateLimiter {
602
+ /**
603
+ * Increment the counter and report whether this hit is allowed.
604
+ * Use on the event you want to count (e.g. a FAILED login attempt).
605
+ */
606
+ check(key: string, cfg: RateLimitConfig): Promise<RateLimitResult>;
607
+ /**
608
+ * Read the current counter WITHOUT incrementing and report whether the
609
+ * next `check()` would be allowed.
610
+ *
611
+ * This exists so a caller can reject early — before doing expensive work
612
+ * such as a bcrypt compare — once the window is already exhausted. Without
613
+ * a peek, the only way to know the limit is hit is to `check()` (which
614
+ * increments), forcing the expensive work to run first. peek() lets the
615
+ * login handler gate bcrypt on the existing failure count, closing a
616
+ * CPU-exhaustion DoS where an attacker spams requests past the limit and
617
+ * still pays for a bcrypt round-trip on each.
618
+ */
619
+ peek(key: string, cfg: RateLimitConfig): Promise<RateLimitResult>;
620
+ }
621
+ declare const createRateLimiter: (kv: IKVStore) => RateLimiter;
622
+
623
+ /**
624
+ * @module @kb-labs/gateway-auth/tenant-resolver
625
+ *
626
+ * Map `Host`-header → `tenantId` (ADR-0020, Phase 1.9).
627
+ *
628
+ * Pattern is configurable (`{tenant}.kblabs.ru` in cloud) so the same
629
+ * resolver can be used in dev (`{tenant}.localhost`) or for on-prem
630
+ * customers (`{tenant}.acme.example.com`).
631
+ *
632
+ * Returns `null` instead of throwing on bad/reserved hosts — the
633
+ * middleware then renders the right 4xx and avoids surfacing an
634
+ * internal error. Slug rules are strict (`[a-z0-9-]{2,40}`, no
635
+ * leading/trailing dash) so a stray "weird" subdomain doesn't become a
636
+ * valid tenant by accident.
637
+ */
638
+ declare const RESERVED_SUBDOMAINS: ReadonlySet<string>;
639
+ interface TenantResolverOptions {
640
+ /** Host pattern containing exactly one `{tenant}` placeholder. */
641
+ pattern: string;
642
+ /** Optional override of the reserved set (must remain a superset of the default in production). */
643
+ reserved?: ReadonlySet<string>;
644
+ }
645
+ interface TenantResolver {
646
+ resolve(host: string | undefined): string | null;
647
+ }
648
+ declare const createTenantResolver: (opts: TenantResolverOptions) => TenantResolver;
649
+
650
+ /**
651
+ * @module @kb-labs/gateway-auth/providers/email-password
652
+ *
653
+ * Built-in identity provider for the local email-password flow
654
+ * (ADR-0020, Phase 1.10). Implements `IIdentityProvider` exactly as a
655
+ * future Google/Okta provider would — `email-password` has no special
656
+ * status beyond being the one we ship.
657
+ *
658
+ * Two design notes worth keeping in mind:
659
+ *
660
+ * - **Constant-time response (CD-8).** Every not-ok path runs a real
661
+ * bcrypt compare against a fixed dummy hash. Without this, a missing
662
+ * user returns in microseconds while a wrong-password attempt takes
663
+ * ~hundreds of milliseconds — a textbook user-enumeration timing
664
+ * channel. The discriminated `reason` is for **logs only**; the HTTP
665
+ * layer collapses it to a single opaque "invalid_credentials".
666
+ *
667
+ * - **No `setCredential` here.** This provider only *reads* credentials.
668
+ * The activation flow writes the hash via the service layer, which
669
+ * chooses the bcrypt cost from config. Keeping write logic out of
670
+ * the provider means future Google/Okta providers don't carry a
671
+ * meaningless `setCredential` method.
672
+ */
673
+
674
+ interface EmailPasswordProviderOptions {
675
+ /**
676
+ * Narrow read port over users (DD-2). The gateway's `UsersStore`
677
+ * satisfies this structurally; the provider only ever reads.
678
+ */
679
+ users: IdentityUserPort;
680
+ /** Narrow read port over credentials (DD-2). */
681
+ credentials: IdentityCredentialPort;
682
+ /** The tenant this provider instance authenticates against. */
683
+ tenantId: string;
684
+ /** bcrypt cost used for the dummy compare timing. Should match the
685
+ * cost used by the activation flow so dummy/real timings are similar. */
686
+ bcryptCost: number;
687
+ }
688
+ declare const createEmailPasswordProvider: (opts: EmailPasswordProviderOptions) => IIdentityProvider;
689
+
690
+ /**
691
+ * @module @kb-labs/gateway-auth/providers/oidc
692
+ *
693
+ * Built-in generic OIDC redirect provider (ADR-0020, Step 5).
694
+ *
695
+ * Implements the authorization-code flow against any spec-compliant OIDC
696
+ * IdP (Google / Okta / Auth0 / Keycloak / Azure AD): `startAuthorization`
697
+ * builds the upstream authorize URL plus a per-attempt `nonce` (and optional
698
+ * PKCE `code_verifier`); `authenticate` exchanges the code at the token
699
+ * endpoint and verifies the returned ID token before returning the canonical
700
+ * identity. The gateway then does email→user→session as for any provider.
701
+ *
702
+ * Security guarantees this provider owns (not "quality of a custom" — these
703
+ * are the floor we promise):
704
+ *
705
+ * - **alg allowlist** — `jwtVerify(..., { algorithms: ['RS256','ES256'] })`.
706
+ * `none` and HS* are rejected (alg-confusion / key-confusion). Same lesson
707
+ * as pinning HS256 in jwt.ts: never let the token choose its own algorithm
708
+ * class.
709
+ * - **iss / aud** — verified against the configured issuer and clientId.
710
+ * - **exp / nonce** — exp enforced by jose; nonce must echo the per-attempt
711
+ * value we stashed in the state session (replay / token-injection guard).
712
+ * - **email_verified === true** — required unless `allowUnverifiedEmail`,
713
+ * else an IdP that federates unverified emails becomes an account-takeover
714
+ * vector.
715
+ * - **issuer https-only** — discovery / JWKS targets are admin-controlled but
716
+ * we still refuse plaintext.
717
+ * - **secret hygiene** — `clientSecret` is read from an env var
718
+ * (`clientSecretEnv`) by default; nothing secret is ever logged (no code /
719
+ * id_token / access_token / client_secret in any log line).
720
+ *
721
+ * `fetch` is injected (DD-8) so the whole flow is testable against a locally
722
+ * signed token with a fake transport — no network, no global mocks. JWKS are
723
+ * fetched via the same injected `fetch` and verified with a local JWK set,
724
+ * with a single refetch on key rotation.
725
+ */
726
+
727
+ declare const OidcConfigSchema: z.ZodObject<{
728
+ type: z.ZodLiteral<"oidc">;
729
+ id: z.ZodString;
730
+ issuer: z.ZodEffects<z.ZodString, string, string>;
731
+ clientId: z.ZodString;
732
+ /** Direct secret (discouraged — prefer clientSecretEnv). */
733
+ clientSecret: z.ZodOptional<z.ZodString>;
734
+ /** Name of the env var holding the secret (preferred). */
735
+ clientSecretEnv: z.ZodOptional<z.ZodString>;
736
+ /** OAuth scopes; `openid` is always included. Default `['openid','email']`. */
737
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
738
+ /** Enable PKCE (S256). */
739
+ pkce: z.ZodOptional<z.ZodBoolean>;
740
+ /** Accept tokens whose email_verified is false (default false). */
741
+ allowUnverifiedEmail: z.ZodOptional<z.ZodBoolean>;
742
+ }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
743
+ type: z.ZodLiteral<"oidc">;
744
+ id: z.ZodString;
745
+ issuer: z.ZodEffects<z.ZodString, string, string>;
746
+ clientId: z.ZodString;
747
+ /** Direct secret (discouraged — prefer clientSecretEnv). */
748
+ clientSecret: z.ZodOptional<z.ZodString>;
749
+ /** Name of the env var holding the secret (preferred). */
750
+ clientSecretEnv: z.ZodOptional<z.ZodString>;
751
+ /** OAuth scopes; `openid` is always included. Default `['openid','email']`. */
752
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
753
+ /** Enable PKCE (S256). */
754
+ pkce: z.ZodOptional<z.ZodBoolean>;
755
+ /** Accept tokens whose email_verified is false (default false). */
756
+ allowUnverifiedEmail: z.ZodOptional<z.ZodBoolean>;
757
+ }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
758
+ type: z.ZodLiteral<"oidc">;
759
+ id: z.ZodString;
760
+ issuer: z.ZodEffects<z.ZodString, string, string>;
761
+ clientId: z.ZodString;
762
+ /** Direct secret (discouraged — prefer clientSecretEnv). */
763
+ clientSecret: z.ZodOptional<z.ZodString>;
764
+ /** Name of the env var holding the secret (preferred). */
765
+ clientSecretEnv: z.ZodOptional<z.ZodString>;
766
+ /** OAuth scopes; `openid` is always included. Default `['openid','email']`. */
767
+ scopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
768
+ /** Enable PKCE (S256). */
769
+ pkce: z.ZodOptional<z.ZodBoolean>;
770
+ /** Accept tokens whose email_verified is false (default false). */
771
+ allowUnverifiedEmail: z.ZodOptional<z.ZodBoolean>;
772
+ }, z.ZodTypeAny, "passthrough">>;
773
+ type OidcProviderConfig = z.infer<typeof OidcConfigSchema>;
774
+ declare function createOidcProvider(rawConfig: unknown, deps: IdentityProviderDeps): IRedirectIdentityProvider;
775
+
776
+ /**
777
+ * @module @kb-labs/gateway-auth/provider-registry
778
+ *
779
+ * Tiny registry for `IIdentityProvider` instances (ADR-0020, Phase 1.11).
780
+ *
781
+ * Two invariants enforced beyond what a raw Map would give us:
782
+ * - Re-registering the same `id` throws — usually a bootstrap bug,
783
+ * never intentional.
784
+ * - `list()` returns only the public `{ id, kind }` shape so the
785
+ * `GET /auth/providers` route cannot accidentally serialise an
786
+ * `authenticate` function over the wire.
787
+ */
788
+
789
+ interface ProviderInfo {
790
+ id: string;
791
+ kind: IIdentityProvider['kind'];
792
+ }
793
+ declare class ProviderRegistry {
794
+ private readonly providers;
795
+ register(provider: IIdentityProvider): void;
796
+ get(id: string): IIdentityProvider | undefined;
797
+ has(id: string): boolean;
798
+ list(): ProviderInfo[];
799
+ }
800
+
801
+ /**
802
+ * @module @kb-labs/gateway-auth/provider-loader
803
+ *
804
+ * Config-driven loader for identity providers (ADR-0020, DD-3).
805
+ *
806
+ * This is the gateway-side analogue of the platform adapter loader
807
+ * (`core/runtime/src/loader.ts` notifier sub-channels): a type→factory
808
+ * map for the built-ins, plus dynamic `import()` of a package for any
809
+ * non-builtin `type`. Two deliberate departures from the platform
810
+ * loader:
811
+ *
812
+ * - **Fail-fast, not warn-skip.** A misconfigured *platform* adapter can
813
+ * degrade gracefully; a misconfigured *auth door* must never boot
814
+ * half-open. If a configured provider cannot be loaded, we throw.
815
+ * - **Gateway-owned (Pattern B).** Providers need gateway-domain ports
816
+ * (users/credentials) and the tenant context, which don't exist until
817
+ * gateway bootstrap — well after platform assembly. So the loader lives
818
+ * here, not in core.
819
+ *
820
+ * The built-in `email-password` provider is the zero-config default: an
821
+ * empty/absent `providers` config registers it and nothing else. It has
822
+ * no special status beyond being the one we ship — it goes through the
823
+ * exact same factory path a third-party provider would.
824
+ */
825
+
826
+ /**
827
+ * A single provider entry from `auth.providers` config. `type` is the
828
+ * discriminator (built-in name or package name); everything else is
829
+ * provider-specific and validated by the provider's own zod schema.
830
+ */
831
+ interface ProviderConfigEntry {
832
+ type: string;
833
+ [key: string]: unknown;
834
+ }
835
+ type ProvidersConfig = Record<string, ProviderConfigEntry> | undefined;
836
+ /**
837
+ * Built-in factories keyed by `type`. Anything not here is resolved as a
838
+ * package name. `oidc` is the generic OIDC redirect provider (Google / Okta /
839
+ * Auth0 / Keycloak / Azure AD); it owns its own zod config schema.
840
+ */
841
+ declare const BUILTIN_FACTORIES: Record<string, IdentityProviderFactory>;
842
+ /**
843
+ * Build a {@link ProviderRegistry} from the `auth.providers` config.
844
+ *
845
+ * @param config The `auth.providers` map (or undefined).
846
+ * @param deps Shared dependencies handed to every provider factory.
847
+ * @returns A populated registry. Throws (fail-fast) on any load error.
848
+ */
849
+ declare function loadIdentityProviders(config: ProvidersConfig, deps: IdentityProviderDeps): Promise<ProviderRegistry>;
850
+
851
+ /**
852
+ * @module @kb-labs/gateway-auth/oauth-state-store
853
+ *
854
+ * Per-attempt OAuth `state` storage (ADR-0020, DD-5).
855
+ *
856
+ * Between `GET /auth/oauth/:id/start` and the IdP callback we must hold a
857
+ * short-lived binding from the opaque `state` token to the flow context
858
+ * (which provider issued it, which tenant, where to return, and the
859
+ * per-attempt secrets like nonce / PKCE verifier). This is a thin,
860
+ * namespaced wrapper over `IKVStore` — a real abstraction, not a stub:
861
+ *
862
+ * - **Namespaced keys** (`oauth:state:{state}`) so it can share the same
863
+ * KV as the rate limiter without collision.
864
+ * - **TTL** so abandoned flows self-clean.
865
+ * - **One-shot `consume`** (get → delete, gated on the delete result) so
866
+ * a replayed callback cannot be processed twice. With a shared Redis KV
867
+ * the gating on `delete()` is what provides the atomic single-winner
868
+ * guarantee under concurrency.
869
+ *
870
+ * NOTE: in a multi-process / HA deployment this MUST be backed by a
871
+ * shared KV (Redis); an in-memory KV is per-process and a callback may
872
+ * land on a different worker than the one that issued the state. The
873
+ * bootstrap warns when a redirect provider is configured against an
874
+ * in-memory KV (see OAuth hardening, Step 4b).
875
+ */
876
+
877
+ /**
878
+ * The flow context bound to a single `state` token. `session` carries the
879
+ * provider's per-attempt secrets (OIDC nonce, PKCE verifier) verbatim and
880
+ * is never logged.
881
+ */
882
+ interface OAuthStateRecord {
883
+ /** The provider instance id that started this flow (mix-up guard). */
884
+ providerId: string;
885
+ /** The tenant the flow was started for (cross-tenant guard). */
886
+ tenantId: string;
887
+ /** Validated relative path to return to after success. */
888
+ returnTo: string;
889
+ /** Opaque per-attempt secrets the provider needs back at callback. */
890
+ session?: Record<string, unknown>;
891
+ /** Issue timestamp (ms). */
892
+ createdAt: number;
893
+ }
894
+ interface OAuthStateStoreOptions {
895
+ /** State TTL in milliseconds. Default 10 minutes. */
896
+ ttlMs?: number;
897
+ }
898
+ declare class OAuthStateStore {
899
+ private readonly kv;
900
+ private readonly ttlMs;
901
+ constructor(kv: IKVStore, opts?: OAuthStateStoreOptions);
902
+ private key;
903
+ /** Persist a state record with the configured TTL. */
904
+ put(state: string, record: OAuthStateRecord): Promise<void>;
905
+ /**
906
+ * One-shot read. Returns the record to the first caller and deletes it;
907
+ * concurrent / subsequent callers get `null`. The `delete` result gates
908
+ * the return so exactly one caller wins even under a shared KV.
909
+ */
910
+ consume(state: string): Promise<OAuthStateRecord | null>;
911
+ }
912
+
913
+ /**
914
+ * @module @kb-labs/gateway-auth/stub-pdp
915
+ *
916
+ * Stub `IPolicyDecisionPoint` (ADR-0020, Phase 1.12). Mock RBAC over
917
+ * the memberships store: tenant-admin gets every canonical permission,
918
+ * tenant-member gets none. Machine identities are temporarily allowed
919
+ * everything (matches the existing machine-token behaviour pre-RBAC).
920
+ *
921
+ * This module exists so the real handlers can call `policy.check(...)`
922
+ * and `enumeratePermissions(...)` from day one. The real RBAC + ReBAC
923
+ * engine (ClickUp 869def338) replaces this implementation; callers do
924
+ * not change.
925
+ *
926
+ * Closed-world: actions outside the canonical PERMISSIONS enum return
927
+ * `allow: false` even for tenant-admin. This is deliberate — until we
928
+ * have per-plugin permission registration, the central enum is the
929
+ * full universe of known actions.
930
+ */
931
+
932
+ interface StubPDPOptions {
933
+ memberships: MembershipsStore;
934
+ }
935
+ declare const createStubPDP: (opts: StubPDPOptions) => IPolicyDecisionPoint;
936
+
937
+ /**
938
+ * @module @kb-labs/gateway-auth/bootstrap-admin
939
+ *
940
+ * First-run admin provisioning (ADR-0020, Phase 1.14).
941
+ *
942
+ * On a fresh deployment the operator can't log in unless someone has
943
+ * already been provisioned — and we deliberately removed the public
944
+ * `/auth/register` for humans. `ensureBootstrapAdmin` reads bootstrap
945
+ * config from env and creates a single tenant-admin User on first
946
+ * start. Idempotent: subsequent runs see the existing User and do
947
+ * nothing.
948
+ *
949
+ * Conflict handling is conservative: if a User with the configured
950
+ * email already exists in the configured tenant but in a different
951
+ * status (e.g. someone manually `disabled` them and forgot the env
952
+ * was still set), we log a warning and **do not touch** the existing
953
+ * record. Silently re-activating or re-passwording is the kind of
954
+ * surprise we want to avoid on a production system.
955
+ */
956
+
957
+ interface BootstrapConfig {
958
+ adminEmail: string;
959
+ adminPassword: string;
960
+ tenantId: string;
961
+ }
962
+ interface BootstrapAdminLogger {
963
+ warn(...args: unknown[]): void;
964
+ info(...args: unknown[]): void;
965
+ error(...args: unknown[]): void;
966
+ }
967
+ interface EnsureBootstrapAdminOptions {
968
+ bootstrap: BootstrapConfig | undefined;
969
+ users: UsersStore;
970
+ credentials: CredentialsStore;
971
+ memberships: MembershipsStore;
972
+ bcryptCost: number;
973
+ logger: BootstrapAdminLogger;
974
+ }
975
+ declare const ensureBootstrapAdmin: (opts: EnsureBootstrapAdminOptions) => Promise<void>;
976
+
977
+ /**
978
+ * @module @kb-labs/gateway-auth/user-auth-service
979
+ *
980
+ * Orchestration layer for the user authentication flow (ADR-0020,
981
+ * Phase 1.15).
982
+ *
983
+ * Pulls together every store + provider + jwt + policy and exposes the
984
+ * five operations the HTTP layer calls:
985
+ *
986
+ * - `login`
987
+ * - `refresh` (CD-1 enforced here — disabled user revokes family)
988
+ * - `logout`
989
+ * - `changePassword` (revokes other families, keeps current)
990
+ * - `activate` (consumes invite, auto-logs in)
991
+ *
992
+ * All not-ok paths throw `AuthError(code)`. The HTTP layer collapses
993
+ * most codes to opaque `invalid_credentials` for the caller; `code` is
994
+ * preserved for logs and tests.
995
+ */
996
+
997
+ type AuthErrorCode = 'invalid_credentials' | 'unknown_provider' | 'invalid_refresh' | 'refresh_reuse' | 'user_disabled' | 'invalid_current_password' | 'weak_password' | 'invalid_invite' | 'unknown_invite';
998
+ declare class AuthError extends Error {
999
+ readonly code: AuthErrorCode;
1000
+ readonly reason?: string | undefined;
1001
+ readonly name = "AuthError";
1002
+ constructor(code: AuthErrorCode, reason?: string | undefined);
1003
+ }
1004
+ interface UserAuthServiceOptions {
1005
+ users: UsersStore;
1006
+ credentials: CredentialsStore;
1007
+ memberships: MembershipsStore;
1008
+ invites: InvitesStore;
1009
+ sessions: SessionsStore;
1010
+ providers: ProviderRegistry;
1011
+ passwordPolicy: PasswordPolicy;
1012
+ jwtConfig: JwtConfig;
1013
+ accessTtlSec: number;
1014
+ refreshTtlSec: number;
1015
+ bcryptCost: number;
1016
+ /** Override for tests. Defaults to Date.now. */
1017
+ now?: () => number;
1018
+ }
1019
+ interface PublicUser {
1020
+ userId: string;
1021
+ email: string;
1022
+ tenantId: string;
1023
+ }
1024
+ interface TokenInfo {
1025
+ token: string;
1026
+ expiresInSec: number;
1027
+ }
1028
+ interface SessionResult {
1029
+ user: PublicUser;
1030
+ access: TokenInfo;
1031
+ refresh: TokenInfo;
1032
+ csrf: string;
1033
+ familyId: string;
1034
+ }
1035
+ interface LoginInput {
1036
+ providerId: string;
1037
+ input: unknown;
1038
+ }
1039
+ interface DeviceCtx {
1040
+ userAgent?: string;
1041
+ ip?: string;
1042
+ }
1043
+ interface ChangePasswordInput {
1044
+ userId: string;
1045
+ currentFamilyId: string;
1046
+ currentPassword: string;
1047
+ newPassword: string;
1048
+ }
1049
+ interface ActivateInput {
1050
+ activationToken: string;
1051
+ password: string;
1052
+ deviceCtx: DeviceCtx;
1053
+ }
1054
+ declare const createUserAuthService: (opts: UserAuthServiceOptions) => {
1055
+ login: (input: LoginInput, tenantId: string, deviceCtx: DeviceCtx) => Promise<SessionResult>;
1056
+ refresh: (refreshToken: string) => Promise<SessionResult>;
1057
+ logout: (refreshToken: string) => Promise<void>;
1058
+ changePassword: (input: ChangePasswordInput) => Promise<void>;
1059
+ activate: (input: ActivateInput) => Promise<SessionResult>;
1060
+ };
1061
+
1062
+ export { type ActivateInput, type AddMembershipInput, AuthError, type AuthErrorCode, AuthService, BUILTIN_FACTORIES, type BootstrapAdminLogger, type BootstrapConfig, type ChangePasswordInput, type ClientRecord, type CreateInviteInput, type CreateInviteResult, type CreateSessionInput, type CreateSessionResult, type CreateUserInput, type Credential, CredentialsStore, type DeviceCtx, type EmailPasswordProviderOptions, type EnsureBootstrapAdminOptions, type GroupId, type InviteRecord, type InviteStatus, InvitesStore, type JwtConfig, type LoginInput, type Membership, MembershipsStore, type OAuthStateRecord, OAuthStateStore, type OAuthStateStoreOptions, type OidcProviderConfig, type PasswordPolicy, type PasswordPolicyLogger, type PasswordPolicyOptions, type ProviderConfigEntry, type ProviderInfo, ProviderRegistry, type ProvidersConfig, type PublicUser, RESERVED_SUBDOMAINS, type RateLimitConfig, type RateLimitResult, type RateLimiter, RefreshExpiredError, RefreshNotFoundError, RefreshReuseDetectedError, type RotateRefreshResult, type SessionFamily, type SessionResult, SessionsStore, type SessionsStoreOptions, type SetCredentialInput, type SignUserTokenOptions, type StubPDPOptions, type TenantResolver, type TenantResolverOptions, type TokenInfo, type User, type UserAccessPayload, type UserAuthServiceOptions, type UserRefreshPayload, type UserStatus, UsersStore, type ValidationResult, buildClientRecord, canonicalizeEmail, consumeRefreshToken, createEmailPasswordProvider, createOidcProvider, createPasswordPolicy, createRateLimiter, createStubPDP, createTenantResolver, createUserAuthService, ensureBootstrapAdmin, generateClientId, generateClientSecret, generateHostId, getClient, getClientByHandle, getClientByHostId, getPublicKey, isHandleTaken, issueCsrfToken, loadIdentityProviders, saveClient, savePublicKey, saveRefreshToken, signAccessToken, signRefreshToken, signUserAccessToken, signUserRefreshToken, verifyAccessToken, verifyClientSecret, verifyCsrfToken, verifyRefreshToken, verifyUserAccessToken, verifyUserRefreshToken };