@lastshotlabs/bunshot 0.0.13 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +2816 -1747
  2. package/dist/adapters/memoryAuth.d.ts +7 -0
  3. package/dist/adapters/memoryAuth.js +177 -2
  4. package/dist/adapters/mongoAuth.js +94 -0
  5. package/dist/adapters/sqliteAuth.d.ts +9 -0
  6. package/dist/adapters/sqliteAuth.js +190 -2
  7. package/dist/app.d.ts +120 -2
  8. package/dist/app.js +104 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +24 -8
  12. package/dist/index.js +15 -5
  13. package/dist/lib/appConfig.d.ts +81 -0
  14. package/dist/lib/appConfig.js +30 -0
  15. package/dist/lib/authAdapter.d.ts +54 -0
  16. package/dist/lib/authRateLimit.d.ts +2 -0
  17. package/dist/lib/authRateLimit.js +4 -0
  18. package/dist/lib/clientIp.d.ts +14 -0
  19. package/dist/lib/clientIp.js +52 -0
  20. package/dist/lib/constants.d.ts +4 -0
  21. package/dist/lib/constants.js +4 -0
  22. package/dist/lib/context.d.ts +2 -0
  23. package/dist/lib/createDtoMapper.d.ts +33 -0
  24. package/dist/lib/createDtoMapper.js +69 -0
  25. package/dist/lib/crypto.d.ts +11 -0
  26. package/dist/lib/crypto.js +22 -0
  27. package/dist/lib/emailVerification.d.ts +4 -0
  28. package/dist/lib/emailVerification.js +20 -12
  29. package/dist/lib/jwt.d.ts +1 -1
  30. package/dist/lib/jwt.js +19 -6
  31. package/dist/lib/mfaChallenge.d.ts +42 -0
  32. package/dist/lib/mfaChallenge.js +293 -0
  33. package/dist/lib/oauth.d.ts +14 -1
  34. package/dist/lib/oauth.js +19 -1
  35. package/dist/lib/oauthCode.d.ts +15 -0
  36. package/dist/lib/oauthCode.js +90 -0
  37. package/dist/lib/queue.d.ts +33 -0
  38. package/dist/lib/queue.js +98 -0
  39. package/dist/lib/resetPassword.js +12 -16
  40. package/dist/lib/roles.d.ts +4 -0
  41. package/dist/lib/roles.js +27 -0
  42. package/dist/lib/session.d.ts +12 -0
  43. package/dist/lib/session.js +165 -5
  44. package/dist/lib/tenant.d.ts +15 -0
  45. package/dist/lib/tenant.js +65 -0
  46. package/dist/lib/ws.js +5 -1
  47. package/dist/lib/zodToMongoose.d.ts +38 -0
  48. package/dist/lib/zodToMongoose.js +84 -0
  49. package/dist/middleware/bearerAuth.js +4 -3
  50. package/dist/middleware/botProtection.js +2 -2
  51. package/dist/middleware/cacheResponse.d.ts +1 -0
  52. package/dist/middleware/cacheResponse.js +18 -3
  53. package/dist/middleware/cors.d.ts +2 -0
  54. package/dist/middleware/cors.js +22 -8
  55. package/dist/middleware/csrf.d.ts +18 -0
  56. package/dist/middleware/csrf.js +115 -0
  57. package/dist/middleware/rateLimit.d.ts +2 -1
  58. package/dist/middleware/rateLimit.js +7 -5
  59. package/dist/middleware/requireRole.d.ts +14 -3
  60. package/dist/middleware/requireRole.js +46 -6
  61. package/dist/middleware/tenant.d.ts +5 -0
  62. package/dist/middleware/tenant.js +116 -0
  63. package/dist/models/AuthUser.d.ts +17 -0
  64. package/dist/models/AuthUser.js +17 -0
  65. package/dist/models/TenantRole.d.ts +15 -0
  66. package/dist/models/TenantRole.js +23 -0
  67. package/dist/routes/auth.d.ts +5 -3
  68. package/dist/routes/auth.js +173 -30
  69. package/dist/routes/jobs.d.ts +2 -0
  70. package/dist/routes/jobs.js +270 -0
  71. package/dist/routes/mfa.d.ts +5 -0
  72. package/dist/routes/mfa.js +616 -0
  73. package/dist/routes/oauth.js +378 -23
  74. package/dist/schemas/auth.d.ts +2 -0
  75. package/dist/schemas/auth.js +22 -1
  76. package/dist/server.d.ts +6 -0
  77. package/dist/server.js +19 -3
  78. package/dist/services/auth.d.ts +18 -5
  79. package/dist/services/auth.js +112 -18
  80. package/dist/services/mfa.d.ts +84 -0
  81. package/dist/services/mfa.js +543 -0
  82. package/dist/ws/index.js +3 -2
  83. package/docs/sections/adding-middleware/full.md +35 -0
  84. package/docs/sections/adding-models/full.md +125 -0
  85. package/docs/sections/adding-models/overview.md +13 -0
  86. package/docs/sections/adding-routes/full.md +182 -0
  87. package/docs/sections/adding-routes/overview.md +23 -0
  88. package/docs/sections/auth-flow/full.md +634 -0
  89. package/docs/sections/auth-flow/overview.md +10 -0
  90. package/docs/sections/cli/full.md +30 -0
  91. package/docs/sections/configuration/full.md +155 -0
  92. package/docs/sections/configuration/overview.md +17 -0
  93. package/docs/sections/configuration-example/full.md +117 -0
  94. package/docs/sections/configuration-example/overview.md +30 -0
  95. package/docs/sections/documentation/full.md +171 -0
  96. package/docs/sections/environment-variables/full.md +55 -0
  97. package/docs/sections/exports/full.md +92 -0
  98. package/docs/sections/extending-context/full.md +59 -0
  99. package/docs/sections/header.md +3 -0
  100. package/docs/sections/installation/full.md +6 -0
  101. package/docs/sections/jobs/full.md +140 -0
  102. package/docs/sections/jobs/overview.md +15 -0
  103. package/docs/sections/mongodb-connections/full.md +45 -0
  104. package/docs/sections/mongodb-connections/overview.md +7 -0
  105. package/docs/sections/multi-tenancy/full.md +66 -0
  106. package/docs/sections/multi-tenancy/overview.md +15 -0
  107. package/docs/sections/oauth/full.md +189 -0
  108. package/docs/sections/oauth/overview.md +16 -0
  109. package/docs/sections/package-development/full.md +7 -0
  110. package/docs/sections/peer-dependencies/full.md +47 -0
  111. package/docs/sections/quick-start/full.md +43 -0
  112. package/docs/sections/response-caching/full.md +117 -0
  113. package/docs/sections/response-caching/overview.md +13 -0
  114. package/docs/sections/roles/full.md +136 -0
  115. package/docs/sections/roles/overview.md +12 -0
  116. package/docs/sections/running-without-redis/full.md +16 -0
  117. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  118. package/docs/sections/stack/full.md +10 -0
  119. package/docs/sections/websocket/full.md +101 -0
  120. package/docs/sections/websocket/overview.md +5 -0
  121. package/docs/sections/websocket-rooms/full.md +97 -0
  122. package/docs/sections/websocket-rooms/overview.md +5 -0
  123. package/package.json +30 -9
package/dist/app.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { MiddlewareHandler } from "hono";
3
3
  import type { AppEnv } from "./lib/context";
4
- import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./lib/appConfig";
4
+ import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, PasswordPolicyConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig } from "./lib/appConfig";
5
5
  import type { AuthAdapter } from "./lib/authAdapter";
6
6
  import type { OAuthProviderConfig } from "./lib/oauth";
7
7
  type StoreType = "redis" | "mongo" | "sqlite" | "memory";
@@ -60,6 +60,9 @@ export interface OAuthConfig {
60
60
  providers?: OAuthProviderConfig;
61
61
  /** Where to redirect after a successful OAuth login. Defaults to "/" */
62
62
  postRedirect?: string;
63
+ /** Allowlist of redirect URLs. If set, the postRedirect URL is validated against this list.
64
+ * Relative paths (e.g., "/") are always allowed. Only absolute URLs are validated. */
65
+ allowedRedirectUrls?: string[];
63
66
  }
64
67
  export interface AuthRateLimitConfig {
65
68
  /** Max login failures per window before the account is locked. Default: 10 per 15 min. */
@@ -92,6 +95,21 @@ export interface AuthRateLimitConfig {
92
95
  windowMs?: number;
93
96
  max?: number;
94
97
  };
98
+ /** Max account deletion attempts per user per window. Default: 3 per hour. */
99
+ deleteAccount?: {
100
+ windowMs?: number;
101
+ max?: number;
102
+ };
103
+ /** Max MFA verification attempts per IP per window. Default: 10 per 15 min. */
104
+ mfaVerify?: {
105
+ windowMs?: number;
106
+ max?: number;
107
+ };
108
+ /** Max MFA email OTP resend attempts per IP per window. Default: 5 per minute. */
109
+ mfaResend?: {
110
+ windowMs?: number;
111
+ max?: number;
112
+ };
95
113
  /**
96
114
  * Store backend for auth rate limit counters.
97
115
  * Defaults to "redis" when Redis is enabled, otherwise "memory".
@@ -132,10 +150,40 @@ export interface AuthConfig {
132
150
  * Mounts POST /auth/forgot-password and POST /auth/reset-password.
133
151
  */
134
152
  passwordReset?: PasswordResetConfig;
153
+ /** Password strength policy for registration and reset-password.
154
+ * Login is intentionally lenient (min 1) so users under older policies can still sign in.
155
+ * Defaults: minLength=8, requireLetter=true, requireDigit=true, requireSpecial=false. */
156
+ passwordPolicy?: PasswordPolicyConfig;
135
157
  /** Rate limit configuration for built-in auth endpoints. */
136
158
  rateLimit?: AuthRateLimitConfig;
137
159
  /** Session concurrency and metadata persistence policy. */
138
160
  sessionPolicy?: AuthSessionPolicyConfig;
161
+ /** Account deletion configuration. Enables DELETE /auth/me when the adapter supports deleteUser. */
162
+ accountDeletion?: AccountDeletionConfig;
163
+ /**
164
+ * Refresh token configuration. When set, login/register return short-lived access tokens
165
+ * (default 15 min) alongside long-lived refresh tokens (default 30 days). Mounts POST /auth/refresh.
166
+ * When not configured, the existing 7-day JWT behavior is unchanged.
167
+ */
168
+ refreshTokens?: RefreshTokenConfig;
169
+ /**
170
+ * MFA/TOTP configuration. When set, enables MFA setup/verify/disable routes under /auth/mfa/*.
171
+ * Login returns { mfaRequired: true, mfaToken } when MFA is enabled for the user.
172
+ * OAuth logins skip MFA (the OAuth provider is treated as the second factor).
173
+ */
174
+ mfa?: MfaConfig;
175
+ }
176
+ export interface AccountDeletionConfig {
177
+ /** Called before deletion. Throw to abort (e.g., active subscription check). */
178
+ onBeforeDelete?: (userId: string) => Promise<void>;
179
+ /** Called after auth data is deleted. Runs at execution time — query current state, not a snapshot. */
180
+ onAfterDelete?: (userId: string) => Promise<void>;
181
+ /** When true, deletion is queued as a BullMQ job instead of running synchronously. Requires Redis + BullMQ. */
182
+ queued?: boolean;
183
+ /** Grace period in seconds before queued deletion executes. Default: 0 (immediate). */
184
+ gracePeriod?: number;
185
+ /** Called when deletion is scheduled (queued + gracePeriod > 0). Use to send a confirmation/cancel email. */
186
+ onDeletionScheduled?: (userId: string, email: string, cancelToken: string) => Promise<void>;
139
187
  }
140
188
  export interface AuthSessionPolicyConfig {
141
189
  /** Max simultaneous active sessions per user. Oldest is evicted when exceeded. Default: 6. */
@@ -156,7 +204,7 @@ export interface AuthSessionPolicyConfig {
156
204
  */
157
205
  trackLastActive?: boolean;
158
206
  }
159
- export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig };
207
+ export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig };
160
208
  export interface BotProtectionConfig {
161
209
  /**
162
210
  * List of IPv4 CIDRs (e.g. "198.51.100.0/24"), IPv4 addresses, or IPv6 addresses to block outright.
@@ -172,9 +220,23 @@ export interface BotProtectionConfig {
172
220
  */
173
221
  fingerprintRateLimit?: boolean;
174
222
  }
223
+ export interface CsrfConfig {
224
+ /** Enable CSRF protection for cookie-authenticated state-changing requests. */
225
+ enabled: boolean;
226
+ /** Paths exempt from CSRF checks (in addition to built-in OAuth callback exemptions). Uses prefix matching when path ends with "*". */
227
+ exemptPaths?: string[];
228
+ /** Also validate Origin header against CORS origins. Default: true. */
229
+ checkOrigin?: boolean;
230
+ }
175
231
  export interface SecurityConfig {
176
232
  /** CORS origins. Defaults to "*" */
177
233
  cors?: string | string[];
234
+ /** Additional security headers to set via Hono's secureHeaders middleware.
235
+ * Pass a Content-Security-Policy, Permissions-Policy, etc. */
236
+ headers?: {
237
+ contentSecurityPolicy?: string;
238
+ permissionsPolicy?: string;
239
+ };
178
240
  /** Global rate limit. Defaults to 100 req / 60s */
179
241
  rateLimit?: {
180
242
  windowMs: number;
@@ -193,6 +255,18 @@ export interface SecurityConfig {
193
255
  * Runs before IP rate limiting so blocked IPs are rejected immediately.
194
256
  */
195
257
  botProtection?: BotProtectionConfig;
258
+ /**
259
+ * Trusted proxy configuration for IP extraction.
260
+ * - `false` (default): use socket-level IP only, ignore X-Forwarded-For entirely.
261
+ * - A number N: trust N proxy hops — take the Nth-from-right IP in the X-Forwarded-For chain.
262
+ */
263
+ trustProxy?: false | number;
264
+ /**
265
+ * CSRF protection for cookie-based auth. Opt-in.
266
+ * Uses signed double-submit cookie pattern with HMAC-SHA256.
267
+ * Only validates when the auth cookie is present on state-changing requests.
268
+ */
269
+ csrf?: CsrfConfig;
196
270
  }
197
271
  export interface ModelSchemasConfig {
198
272
  /**
@@ -217,6 +291,46 @@ export interface ModelSchemasConfig {
217
291
  */
218
292
  registration?: "auto" | "explicit";
219
293
  }
294
+ export interface JobsConfig {
295
+ /** Enable the job status endpoint. Default: false. */
296
+ statusEndpoint?: boolean;
297
+ /**
298
+ * Auth protection for job endpoints.
299
+ * - `"userAuth"` — requires authenticated user session (cookie/token).
300
+ * - `"none"` — no auth (not recommended for production).
301
+ * - `MiddlewareHandler[]` — custom middleware stack (e.g., `[userAuth, requireRole("admin")]`).
302
+ *
303
+ * Default: `"none"`. You must explicitly configure auth.
304
+ */
305
+ auth?: "userAuth" | "none" | import("hono").MiddlewareHandler<AppEnv>[];
306
+ /** Required roles for accessing job endpoints. Only works when auth includes userAuth. */
307
+ roles?: string[];
308
+ /** Whitelist of queue names exposed. Default: [] (nothing exposed). */
309
+ allowedQueues?: string[];
310
+ /** When using userAuth, restrict job visibility to the user who created it. Default: false. */
311
+ scopeToUser?: boolean;
312
+ }
313
+ export interface TenantConfig {
314
+ [key: string]: unknown;
315
+ }
316
+ export interface TenancyConfig {
317
+ /** How tenant is identified. */
318
+ resolution: "header" | "subdomain" | "path";
319
+ /** Header name when resolution is "header". Default: "x-tenant-id". */
320
+ headerName?: string;
321
+ /** Path segment index when resolution is "path". Default: 0. */
322
+ pathSegment?: number;
323
+ /** Callback to validate/load tenant. Return null to reject. */
324
+ onResolve?: (tenantId: string) => Promise<TenantConfig | null>;
325
+ /** TTL in ms for caching onResolve results (LRU cache). Default: 60_000. Set 0 to disable. */
326
+ cacheTtlMs?: number;
327
+ /** Max entries in tenant resolution cache. Default: 500. */
328
+ cacheMaxSize?: number;
329
+ /** Paths that skip tenant resolution. Uses startsWith matching. Default: ["/health", "/docs", "/openapi.json"]. */
330
+ exemptPaths?: string[];
331
+ /** HTTP status when onResolve returns null. Default: 403. */
332
+ rejectionStatus?: 403 | 404;
333
+ }
220
334
  export interface CreateAppConfig {
221
335
  /** Absolute path to the service's routes directory (use import.meta.dir + "/routes") */
222
336
  routesDir: string;
@@ -237,5 +351,9 @@ export interface CreateAppConfig {
237
351
  middleware?: MiddlewareHandler<AppEnv>[];
238
352
  /** Database connection and store routing configuration */
239
353
  db?: DbConfig;
354
+ /** Job status endpoint configuration. Requires BullMQ + Redis. */
355
+ jobs?: JobsConfig;
356
+ /** Multi-tenancy configuration. When set, tenant middleware resolves tenant on each request. */
357
+ tenancy?: TenancyConfig;
240
358
  }
241
359
  export declare const createApp: (config: CreateAppConfig) => Promise<OpenAPIHono<AppEnv>>;
package/dist/app.js CHANGED
@@ -7,8 +7,8 @@ import { HttpError } from "./lib/HttpError";
7
7
  import { rateLimit } from "./middleware/rateLimit";
8
8
  import { bearerAuth } from "./middleware/bearerAuth";
9
9
  import { identify } from "./middleware/identify";
10
- import { HEADER_USER_TOKEN } from "./lib/constants";
11
- import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive } from "./lib/appConfig";
10
+ import { HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN, HEADER_CSRF_TOKEN } from "./lib/constants";
11
+ import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setPasswordPolicy, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig, setCsrfEnabled } from "./lib/appConfig";
12
12
  import { setEmailVerificationStore } from "./lib/emailVerification";
13
13
  import { setPasswordResetStore } from "./lib/resetPassword";
14
14
  import { setAuthRateLimitStore } from "./lib/authRateLimit";
@@ -16,6 +16,7 @@ import { setAuthAdapter } from "./lib/authAdapter";
16
16
  import { mongoAuthAdapter } from "./adapters/mongoAuth";
17
17
  import { memoryAuthAdapter } from "./adapters/memoryAuth";
18
18
  import { initOAuthProviders, getConfiguredOAuthProviders, setOAuthStateStore } from "./lib/oauth";
19
+ import { setOAuthCodeStore } from "./lib/oauthCode";
19
20
  import { createOAuthRouter } from "./routes/oauth";
20
21
  import { connectMongo, connectAuthMongo, connectAppMongo } from "./lib/mongo";
21
22
  import { connectRedis } from "./lib/redis";
@@ -26,7 +27,13 @@ export const createApp = async (config) => {
26
27
  const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
27
28
  const appName = appConfig.name ?? "Bun Core API";
28
29
  const openApiVersion = appConfig.version ?? "1.0.0";
30
+ // Trust-proxy for IP extraction
31
+ const { setTrustProxy } = await import("./lib/clientIp");
32
+ setTrustProxy(securityConfig.trustProxy ?? false);
29
33
  const corsOrigins = securityConfig.cors ?? "*";
34
+ if (corsOrigins === "*" && process.env.NODE_ENV === "production") {
35
+ console.warn("[security] CORS is set to wildcard (*) in production. Configure security.cors with specific origins to restrict cross-origin access.");
36
+ }
30
37
  const rlConfig = securityConfig.rateLimit ?? { windowMs: 60_000, max: 100 };
31
38
  const botCfg = securityConfig.botProtection ?? {};
32
39
  const enableBearerAuth = securityConfig.bearerAuth !== false;
@@ -37,6 +44,29 @@ export const createApp = async (config) => {
37
44
  const explicitAuthAdapter = authConfig.adapter;
38
45
  const oauthProviders = authConfig.oauth?.providers;
39
46
  const postOAuthRedirect = authConfig.oauth?.postRedirect ?? "/";
47
+ const allowedRedirectUrls = authConfig.oauth?.allowedRedirectUrls;
48
+ // Validate postRedirect against allowlist at startup (not per-request)
49
+ if (allowedRedirectUrls && postOAuthRedirect !== "/") {
50
+ try {
51
+ const redirectUrl = new URL(postOAuthRedirect);
52
+ const allowed = allowedRedirectUrls.some((u) => {
53
+ try {
54
+ return new URL(u).origin === redirectUrl.origin;
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ });
60
+ if (!allowed) {
61
+ throw new Error(`createApp: oauth.postRedirect "${postOAuthRedirect}" is not in the allowedRedirectUrls list. Add its origin to oauth.allowedRedirectUrls.`);
62
+ }
63
+ }
64
+ catch (e) {
65
+ if (e instanceof Error && e.message.startsWith("createApp:"))
66
+ throw e;
67
+ // Relative path — always allowed
68
+ }
69
+ }
40
70
  const roles = authConfig.roles ?? [];
41
71
  const defaultRole = authConfig.defaultRole;
42
72
  const primaryField = authConfig.primaryField ?? "email";
@@ -63,6 +93,7 @@ export const createApp = async (config) => {
63
93
  }
64
94
  setSessionStore(sessions);
65
95
  setOAuthStateStore(oauthState);
96
+ setOAuthCodeStore(oauthState);
66
97
  setCacheStore(cache);
67
98
  if (mongo === "single")
68
99
  await connectMongo();
@@ -104,12 +135,15 @@ export const createApp = async (config) => {
104
135
  setEmailVerificationConfig(emailVerification ?? null);
105
136
  setEmailVerificationStore(sessions);
106
137
  setPasswordResetConfig(passwordReset ?? null);
138
+ setPasswordPolicy(authConfig.passwordPolicy ?? {});
107
139
  setPasswordResetStore(sessions);
108
140
  setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
109
141
  setMaxSessions(sessionPolicy.maxSessions ?? 6);
110
142
  setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
111
143
  setIncludeInactiveSessions(sessionPolicy.includeInactiveSessions ?? false);
112
144
  setTrackLastActive(sessionPolicy.trackLastActive ?? false);
145
+ setRefreshTokenConfig(authConfig.refreshTokens ?? null);
146
+ setMfaConfig(authConfig.mfa ?? null);
113
147
  if (oauthProviders)
114
148
  initOAuthProviders(oauthProviders);
115
149
  const configuredOAuth = getConfiguredOAuthProviders();
@@ -124,8 +158,26 @@ export const createApp = async (config) => {
124
158
  const bearerAuthBypass = [...DEFAULT_BYPASS, ...oauthBypass, ...extraBypass];
125
159
  const app = new OpenAPIHono();
126
160
  app.use(logger());
161
+ const headerOpts = {};
162
+ if (securityConfig.headers?.contentSecurityPolicy) {
163
+ headerOpts["Content-Security-Policy"] = securityConfig.headers.contentSecurityPolicy;
164
+ }
165
+ if (securityConfig.headers?.permissionsPolicy) {
166
+ headerOpts["Permissions-Policy"] = securityConfig.headers.permissionsPolicy;
167
+ }
127
168
  app.use(secureHeaders());
128
- app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
169
+ if (Object.keys(headerOpts).length > 0) {
170
+ app.use(async (c, next) => {
171
+ await next();
172
+ for (const [k, v] of Object.entries(headerOpts)) {
173
+ c.res.headers.set(k, v);
174
+ }
175
+ });
176
+ }
177
+ const corsAllowHeaders = ["Content-Type", "Authorization", HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN];
178
+ if (securityConfig.csrf?.enabled)
179
+ corsAllowHeaders.push(HEADER_CSRF_TOKEN);
180
+ app.use(cors({ origin: corsOrigins, allowHeaders: corsAllowHeaders, exposeHeaders: ["x-cache"], credentials: true }));
129
181
  if ((botCfg.blockList?.length ?? 0) > 0) {
130
182
  const { botProtection } = await import("./middleware/botProtection");
131
183
  app.use(botProtection({ blockList: botCfg.blockList }));
@@ -141,6 +193,36 @@ export const createApp = async (config) => {
141
193
  });
142
194
  }
143
195
  app.use(identify);
196
+ // CSRF protection (after identify so we can check for auth cookie presence)
197
+ if (securityConfig.csrf?.enabled) {
198
+ setCsrfEnabled(true);
199
+ const { csrfProtection } = await import("./middleware/csrf");
200
+ const csrfExemptPaths = [
201
+ ...oauthBypass.filter(p => p.includes("/callback")),
202
+ ...(securityConfig.csrf.exemptPaths ?? []),
203
+ ];
204
+ app.use(csrfProtection({
205
+ exemptPaths: csrfExemptPaths,
206
+ checkOrigin: securityConfig.csrf.checkOrigin ?? true,
207
+ allowedOrigins: corsOrigins,
208
+ }));
209
+ }
210
+ // Tenant resolution middleware (after identify, before user middleware + routes)
211
+ if (config.tenancy) {
212
+ if (!config.tenancy.onResolve) {
213
+ if (process.env.NODE_ENV === "production") {
214
+ throw new Error("[security] Tenancy is configured without an onResolve callback. " +
215
+ "In production, onResolve is required to validate tenant IDs and prevent cross-tenant access. " +
216
+ "Provide tenancy.onResolve or remove the tenancy config.");
217
+ }
218
+ else {
219
+ console.warn("[security] Tenancy is configured without an onResolve callback — " +
220
+ "tenant IDs will be trusted without validation. This is unsafe in production.");
221
+ }
222
+ }
223
+ const { createTenantMiddleware } = await import("./middleware/tenant");
224
+ app.use(createTenantMiddleware(config.tenancy));
225
+ }
144
226
  for (const mw of middleware)
145
227
  app.use(mw);
146
228
  setAppName(appName);
@@ -188,17 +270,35 @@ export const createApp = async (config) => {
188
270
  continue; // mounted separately below via createAuthRouter
189
271
  if (file === "oauth.ts")
190
272
  continue; // mounted separately below
273
+ if (file === "mfa.ts")
274
+ continue; // mounted separately below when mfa is configured
275
+ if (file === "jobs.ts")
276
+ continue; // mounted separately below when jobs.statusEndpoint is true
191
277
  const mod = await import(`${coreRoutesDir}/${file}`);
192
278
  if (mod.router)
193
279
  app.route("/", mod.router);
194
280
  }
195
281
  if (enableAuthRoutes) {
196
282
  const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
197
- app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit }));
283
+ app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens }));
198
284
  }
199
285
  if (configuredOAuth.length > 0) {
200
286
  app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
201
287
  }
288
+ if (authConfig.mfa && enableAuthRoutes) {
289
+ const { setMfaChallengeStore, setMfaChallengeSqliteDb } = await import("./lib/mfaChallenge");
290
+ setMfaChallengeStore(sessions);
291
+ if (sessions === "sqlite") {
292
+ const { getDb } = await import("./adapters/sqliteAuth");
293
+ setMfaChallengeSqliteDb(getDb());
294
+ }
295
+ const { createMfaRouter } = await import(`${coreRoutesDir}/mfa`);
296
+ app.route("/", createMfaRouter({ rateLimit: authRateLimit }));
297
+ }
298
+ if (config.jobs?.statusEndpoint) {
299
+ const { createJobsRouter } = await import(`${coreRoutesDir}/jobs`);
300
+ app.route("/", createJobsRouter(config.jobs));
301
+ }
202
302
  // Service routes — collect all, sort by optional exported `priority`, then mount
203
303
  const serviceGlob = new Bun.Glob("**/*.ts");
204
304
  const serviceFiles = [];
@@ -1,2 +1,2 @@
1
- export { createQueue, createWorker } from "../lib/queue";
2
- export type { Job } from "../lib/queue";
1
+ export { createQueue, createWorker, createCronWorker, cleanupStaleSchedulers, getRegisteredCronNames, createDLQHandler } from "../lib/queue";
2
+ export type { Job, CronSchedule, DLQOptions } from "../lib/queue";
@@ -1 +1 @@
1
- export { createQueue, createWorker } from "../lib/queue";
1
+ export { createQueue, createWorker, createCronWorker, cleanupStaleSchedulers, getRegisteredCronNames, createDLQHandler } from "../lib/queue";
package/dist/index.d.ts CHANGED
@@ -1,22 +1,33 @@
1
1
  export { createApp } from "./app";
2
2
  export { createServer } from "./server";
3
- export type { CreateAppConfig, ModelSchemasConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, OAuthConfig, SecurityConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./app";
3
+ export type { CreateAppConfig, ModelSchemasConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, AccountDeletionConfig, OAuthConfig, SecurityConfig, CsrfConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, JobsConfig, TenancyConfig, TenantConfig } from "./app";
4
+ export type { PasswordPolicyConfig } from "./lib/appConfig";
4
5
  export type { CreateServerConfig, WsConfig } from "./server";
5
6
  export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
6
7
  export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
7
8
  export { getAppRoles } from "./lib/appConfig";
8
9
  export { HttpError } from "./lib/HttpError";
9
- export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
10
+ export { COOKIE_TOKEN, HEADER_USER_TOKEN, COOKIE_REFRESH_TOKEN, HEADER_REFRESH_TOKEN, COOKIE_CSRF_TOKEN, HEADER_CSRF_TOKEN } from "./lib/constants";
10
11
  export { createRouter } from "./lib/context";
11
12
  export { createRoute, withSecurity, registerSchema, registerSchemas } from "./lib/createRoute";
13
+ export { zodToMongoose } from "./lib/zodToMongoose";
14
+ export type { ZodToMongooseConfig, ZodToMongooseRefConfig } from "./lib/zodToMongoose";
15
+ export { createDtoMapper } from "./lib/createDtoMapper";
16
+ export type { DtoMapperConfig } from "./lib/createDtoMapper";
12
17
  export type { AppEnv, AppVariables } from "./lib/context";
13
18
  export { signToken, verifyToken } from "./lib/jwt";
14
19
  export { log } from "./lib/logger";
15
20
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
16
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
17
- export type { SessionMetadata, SessionInfo } from "./lib/session";
21
+ export { timingSafeEqual, sha256 } from "./lib/crypto";
22
+ export { getClientIp, setTrustProxy } from "./lib/clientIp";
23
+ export { storeOAuthCode, consumeOAuthCode, setOAuthCodeStore } from "./lib/oauthCode";
24
+ export type { OAuthCodePayload } from "./lib/oauthCode";
25
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "./lib/session";
26
+ export type { SessionMetadata, SessionInfo, RefreshResult } from "./lib/session";
18
27
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
19
- export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
28
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, createWebAuthnRegistrationChallenge, consumeWebAuthnRegistrationChallenge, clearMemoryMfaChallenges } from "./lib/mfaChallenge";
29
+ export type { MfaChallengeData, MfaChallengeOptions, MfaChallengePurpose } from "./lib/mfaChallenge";
30
+ export { bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore } from "./lib/authRateLimit";
20
31
  export type { LimitOpts } from "./lib/authRateLimit";
21
32
  export { validate } from "./lib/validate";
22
33
  export { bearerAuth } from "./middleware/bearerAuth";
@@ -28,13 +39,18 @@ export type { RateLimitOptions } from "./middleware/rateLimit";
28
39
  export { userAuth } from "./middleware/userAuth";
29
40
  export { requireRole } from "./middleware/requireRole";
30
41
  export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
31
- export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./middleware/cacheResponse";
42
+ export { csrfProtection, refreshCsrfToken, clearCsrfToken } from "./middleware/csrf";
43
+ export type { CsrfMiddlewareOptions } from "./middleware/csrf";
44
+ export { cacheResponse, bustCache, bustCachePattern, setCacheStore, getCacheModel } from "./middleware/cacheResponse";
32
45
  export { buildFingerprint } from "./lib/fingerprint";
33
46
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
34
47
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
35
- export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
36
- export type { AuthAdapter, OAuthProfile } from "./lib/authAdapter";
48
+ export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
49
+ export type { AuthAdapter, OAuthProfile, WebAuthnCredential } from "./lib/authAdapter";
37
50
  export type { OAuthProviderConfig } from "./lib/oauth";
38
51
  export { websocket, createWsUpgradeHandler } from "./ws/index";
39
52
  export type { SocketData } from "./ws/index";
40
53
  export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers } from "./lib/ws";
54
+ export { createTenant, deleteTenant, getTenant, listTenants } from "./lib/tenant";
55
+ export type { TenantInfo, CreateTenantOptions } from "./lib/tenant";
56
+ export { invalidateTenantCache } from "./middleware/tenant";
package/dist/index.js CHANGED
@@ -7,15 +7,21 @@ export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
7
7
  // Lib utilities
8
8
  export { getAppRoles } from "./lib/appConfig";
9
9
  export { HttpError } from "./lib/HttpError";
10
- export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
10
+ export { COOKIE_TOKEN, HEADER_USER_TOKEN, COOKIE_REFRESH_TOKEN, HEADER_REFRESH_TOKEN, COOKIE_CSRF_TOKEN, HEADER_CSRF_TOKEN } from "./lib/constants";
11
11
  export { createRouter } from "./lib/context";
12
12
  export { createRoute, withSecurity, registerSchema, registerSchemas } from "./lib/createRoute";
13
+ export { zodToMongoose } from "./lib/zodToMongoose";
14
+ export { createDtoMapper } from "./lib/createDtoMapper";
13
15
  export { signToken, verifyToken } from "./lib/jwt";
14
16
  export { log } from "./lib/logger";
15
17
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
16
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
18
+ export { timingSafeEqual, sha256 } from "./lib/crypto";
19
+ export { getClientIp, setTrustProxy } from "./lib/clientIp";
20
+ export { storeOAuthCode, consumeOAuthCode, setOAuthCodeStore } from "./lib/oauthCode";
21
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "./lib/session";
17
22
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
18
- export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
23
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, createWebAuthnRegistrationChallenge, consumeWebAuthnRegistrationChallenge, clearMemoryMfaChallenges } from "./lib/mfaChallenge";
24
+ export { bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore } from "./lib/authRateLimit";
19
25
  export { validate } from "./lib/validate";
20
26
  // Middleware
21
27
  export { bearerAuth } from "./middleware/bearerAuth";
@@ -25,13 +31,17 @@ export { rateLimit } from "./middleware/rateLimit";
25
31
  export { userAuth } from "./middleware/userAuth";
26
32
  export { requireRole } from "./middleware/requireRole";
27
33
  export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
28
- export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./middleware/cacheResponse";
34
+ export { csrfProtection, refreshCsrfToken, clearCsrfToken } from "./middleware/csrf";
35
+ export { cacheResponse, bustCache, bustCachePattern, setCacheStore, getCacheModel } from "./middleware/cacheResponse";
29
36
  // Lib utilities (bot protection)
30
37
  export { buildFingerprint } from "./lib/fingerprint";
31
38
  // Models
32
39
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
33
40
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
34
- export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
41
+ export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
35
42
  // WebSocket
36
43
  export { websocket, createWsUpgradeHandler } from "./ws/index";
37
44
  export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers } from "./lib/ws";
45
+ // Tenancy
46
+ export { createTenant, deleteTenant, getTenant, listTenants } from "./lib/tenant";
47
+ export { invalidateTenantCache } from "./middleware/tenant";
@@ -13,6 +13,16 @@ export interface PasswordResetConfig {
13
13
  /** Called with the user's email and the reset token. Use to send the reset email. */
14
14
  onSend: (email: string, token: string) => Promise<void>;
15
15
  }
16
+ export interface PasswordPolicyConfig {
17
+ /** Minimum password length. Defaults to 8. */
18
+ minLength?: number;
19
+ /** Require at least one letter (a–z or A–Z). Defaults to true. */
20
+ requireLetter?: boolean;
21
+ /** Require at least one digit (0–9). Defaults to true. */
22
+ requireDigit?: boolean;
23
+ /** Require at least one special character. Defaults to false. */
24
+ requireSpecial?: boolean;
25
+ }
16
26
  export declare const setAppName: (name: string) => void;
17
27
  export declare const getAppName: () => string;
18
28
  export declare const setAppRoles: (roles: string[]) => void;
@@ -26,6 +36,8 @@ export declare const getEmailVerificationConfig: () => EmailVerificationConfig |
26
36
  export declare const getTokenExpiry: () => number;
27
37
  export declare const setPasswordResetConfig: (config: PasswordResetConfig | null) => void;
28
38
  export declare const getPasswordResetConfig: () => PasswordResetConfig | null;
39
+ export declare const setPasswordPolicy: (config: PasswordPolicyConfig) => void;
40
+ export declare const getPasswordPolicy: () => PasswordPolicyConfig;
29
41
  export declare const getResetTokenExpiry: () => number;
30
42
  export declare const setMaxSessions: (n: number) => void;
31
43
  export declare const getMaxSessions: () => number;
@@ -35,3 +47,72 @@ export declare const setIncludeInactiveSessions: (v: boolean) => void;
35
47
  export declare const getIncludeInactiveSessions: () => boolean;
36
48
  export declare const setTrackLastActive: (v: boolean) => void;
37
49
  export declare const getTrackLastActive: () => boolean;
50
+ export interface RefreshTokenConfig {
51
+ /** Access token expiry in seconds. Default: 900 (15 min). */
52
+ accessTokenExpiry?: number;
53
+ /** Refresh token expiry in seconds. Default: 2_592_000 (30 days). */
54
+ refreshTokenExpiry?: number;
55
+ /** Grace window in seconds where the old refresh token still works after rotation.
56
+ * Prevents lockout when the client's network drops mid-refresh. Default: 30. */
57
+ rotationGraceSeconds?: number;
58
+ }
59
+ export declare const setRefreshTokenConfig: (config: RefreshTokenConfig | null) => void;
60
+ export declare const getRefreshTokenConfig: () => RefreshTokenConfig | null;
61
+ export declare const getAccessTokenExpiry: () => number;
62
+ export declare const getRefreshTokenExpiry: () => number;
63
+ export declare const getRotationGraceSeconds: () => number;
64
+ export interface MfaEmailOtpConfig {
65
+ /** Called with the user's email and the OTP code. Use to send the email. */
66
+ onSend: (email: string, code: string) => Promise<void>;
67
+ /** OTP code length. Default: 6. */
68
+ codeLength?: number;
69
+ }
70
+ export interface MfaWebAuthnConfig {
71
+ /** Relying Party ID — typically the domain (e.g. "example.com"). Required. */
72
+ rpId: string;
73
+ /** Relying Party name shown in browser prompts. Defaults to app name. */
74
+ rpName?: string;
75
+ /** Expected origin(s) — full origin URL(s) like "https://example.com". Required. */
76
+ origin: string | string[];
77
+ /** Supported attestation conveyance preference. Default: "none". */
78
+ attestationType?: "none" | "direct" | "enterprise";
79
+ /** Authenticator attachment preference. Default: undefined (allows both platform + cross-platform). */
80
+ authenticatorAttachment?: "platform" | "cross-platform";
81
+ /** User verification requirement. Default: "preferred". */
82
+ userVerification?: "required" | "preferred" | "discouraged";
83
+ /** Timeout for ceremonies in milliseconds. Default: 60000 (60s). */
84
+ timeout?: number;
85
+ /** Reject authentication when sign count goes backward (cloned key detection). Default: false (accept + warn). */
86
+ strictSignCount?: boolean;
87
+ }
88
+ export interface MfaConfig {
89
+ /** Issuer name shown in authenticator apps. Defaults to app name. */
90
+ issuer?: string;
91
+ /** TOTP algorithm. Default: "SHA1" (most compatible). */
92
+ algorithm?: "SHA1" | "SHA256" | "SHA512";
93
+ /** TOTP digits. Default: 6. */
94
+ digits?: number;
95
+ /** TOTP period in seconds. Default: 30. */
96
+ period?: number;
97
+ /** Number of recovery codes to generate. Default: 10. */
98
+ recoveryCodes?: number;
99
+ /** MFA challenge window in seconds. Default: 300 (5 min). */
100
+ challengeTtlSeconds?: number;
101
+ /** Email OTP configuration. When set, enables email-based MFA as an option. */
102
+ emailOtp?: MfaEmailOtpConfig;
103
+ /** WebAuthn/FIDO2 configuration. When set, enables security key MFA routes. */
104
+ webauthn?: MfaWebAuthnConfig;
105
+ }
106
+ export declare const setMfaConfig: (config: MfaConfig | null) => void;
107
+ export declare const getMfaConfig: () => MfaConfig | null;
108
+ export declare const getMfaIssuer: () => string;
109
+ export declare const getMfaAlgorithm: () => string;
110
+ export declare const getMfaDigits: () => number;
111
+ export declare const getMfaPeriod: () => number;
112
+ export declare const getMfaRecoveryCodeCount: () => number;
113
+ export declare const getMfaChallengeTtl: () => number;
114
+ export declare const getMfaEmailOtpConfig: () => MfaEmailOtpConfig | null;
115
+ export declare const getMfaEmailOtpCodeLength: () => number;
116
+ export declare const getMfaWebAuthnConfig: () => MfaWebAuthnConfig | null;
117
+ export declare const setCsrfEnabled: (v: boolean) => void;
118
+ export declare const getCsrfEnabled: () => boolean;
@@ -4,6 +4,7 @@ let defaultRole = null;
4
4
  let _primaryField = "email";
5
5
  let _emailVerificationConfig = null;
6
6
  let _passwordResetConfig = null;
7
+ let _passwordPolicy = {};
7
8
  export const setAppName = (name) => { appName = name; };
8
9
  export const getAppName = () => appName;
9
10
  export const setAppRoles = (roles) => { appRoles = roles; };
@@ -18,6 +19,8 @@ const DEFAULT_TOKEN_EXPIRY = 60 * 60 * 24; // 24 hours
18
19
  export const getTokenExpiry = () => _emailVerificationConfig?.tokenExpiry ?? DEFAULT_TOKEN_EXPIRY;
19
20
  export const setPasswordResetConfig = (config) => { _passwordResetConfig = config; };
20
21
  export const getPasswordResetConfig = () => _passwordResetConfig;
22
+ export const setPasswordPolicy = (config) => { _passwordPolicy = config; };
23
+ export const getPasswordPolicy = () => _passwordPolicy;
21
24
  const DEFAULT_RESET_TOKEN_EXPIRY = 60 * 60; // 1 hour
22
25
  export const getResetTokenExpiry = () => _passwordResetConfig?.tokenExpiry ?? DEFAULT_RESET_TOKEN_EXPIRY;
23
26
  // ---------------------------------------------------------------------------
@@ -35,3 +38,30 @@ export const setIncludeInactiveSessions = (v) => { _includeInactiveSessions = v;
35
38
  export const getIncludeInactiveSessions = () => _includeInactiveSessions;
36
39
  export const setTrackLastActive = (v) => { _trackLastActive = v; };
37
40
  export const getTrackLastActive = () => _trackLastActive;
41
+ let _refreshTokenConfig = null;
42
+ export const setRefreshTokenConfig = (config) => { _refreshTokenConfig = config; };
43
+ export const getRefreshTokenConfig = () => _refreshTokenConfig;
44
+ const DEFAULT_ACCESS_TOKEN_EXPIRY = 900; // 15 min
45
+ const DEFAULT_REFRESH_TOKEN_EXPIRY = 2_592_000; // 30 days
46
+ const DEFAULT_ROTATION_GRACE_SECONDS = 30;
47
+ export const getAccessTokenExpiry = () => _refreshTokenConfig?.accessTokenExpiry ?? DEFAULT_ACCESS_TOKEN_EXPIRY;
48
+ export const getRefreshTokenExpiry = () => _refreshTokenConfig?.refreshTokenExpiry ?? DEFAULT_REFRESH_TOKEN_EXPIRY;
49
+ export const getRotationGraceSeconds = () => _refreshTokenConfig?.rotationGraceSeconds ?? DEFAULT_ROTATION_GRACE_SECONDS;
50
+ let _mfaConfig = null;
51
+ export const setMfaConfig = (config) => { _mfaConfig = config; };
52
+ export const getMfaConfig = () => _mfaConfig;
53
+ export const getMfaIssuer = () => _mfaConfig?.issuer ?? getAppName();
54
+ export const getMfaAlgorithm = () => _mfaConfig?.algorithm ?? "SHA1";
55
+ export const getMfaDigits = () => _mfaConfig?.digits ?? 6;
56
+ export const getMfaPeriod = () => _mfaConfig?.period ?? 30;
57
+ export const getMfaRecoveryCodeCount = () => _mfaConfig?.recoveryCodes ?? 10;
58
+ export const getMfaChallengeTtl = () => _mfaConfig?.challengeTtlSeconds ?? 300;
59
+ export const getMfaEmailOtpConfig = () => _mfaConfig?.emailOtp ?? null;
60
+ export const getMfaEmailOtpCodeLength = () => _mfaConfig?.emailOtp?.codeLength ?? 6;
61
+ export const getMfaWebAuthnConfig = () => _mfaConfig?.webauthn ?? null;
62
+ // ---------------------------------------------------------------------------
63
+ // CSRF config
64
+ // ---------------------------------------------------------------------------
65
+ let _csrfEnabled = false;
66
+ export const setCsrfEnabled = (v) => { _csrfEnabled = v; };
67
+ export const getCsrfEnabled = () => _csrfEnabled;