@lastshotlabs/bunshot 0.0.25 → 0.0.27

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 (108) hide show
  1. package/dist/adapters/localStorage.js +20 -5
  2. package/dist/adapters/memoryAuth.d.ts +6 -0
  3. package/dist/adapters/memoryAuth.js +117 -2
  4. package/dist/adapters/mongoAuth.js +97 -1
  5. package/dist/adapters/sqliteAuth.d.ts +23 -0
  6. package/dist/adapters/sqliteAuth.js +153 -2
  7. package/dist/app.d.ts +105 -2
  8. package/dist/app.js +112 -9
  9. package/dist/index.d.ts +23 -4
  10. package/dist/index.js +13 -2
  11. package/dist/lib/HttpError.d.ts +2 -1
  12. package/dist/lib/HttpError.js +3 -1
  13. package/dist/lib/appConfig.d.ts +113 -0
  14. package/dist/lib/appConfig.js +38 -0
  15. package/dist/lib/auditLog.d.ts +6 -0
  16. package/dist/lib/auditLog.js +17 -0
  17. package/dist/lib/authAdapter.d.ts +71 -1
  18. package/dist/lib/authRateLimit.js +36 -0
  19. package/dist/lib/breachedPassword.d.ts +13 -0
  20. package/dist/lib/breachedPassword.js +48 -0
  21. package/dist/lib/captcha.d.ts +25 -0
  22. package/dist/lib/captcha.js +37 -0
  23. package/dist/lib/context.d.ts +5 -0
  24. package/dist/lib/credentialStuffing.d.ts +31 -0
  25. package/dist/lib/credentialStuffing.js +77 -0
  26. package/dist/lib/emailVerification.d.ts +6 -0
  27. package/dist/lib/emailVerification.js +46 -3
  28. package/dist/lib/jwks.d.ts +25 -0
  29. package/dist/lib/jwks.js +51 -0
  30. package/dist/lib/jwt.d.ts +15 -2
  31. package/dist/lib/jwt.js +92 -5
  32. package/dist/lib/logger.d.ts +2 -0
  33. package/dist/lib/logger.js +6 -0
  34. package/dist/lib/m2m.d.ts +29 -0
  35. package/dist/lib/m2m.js +48 -0
  36. package/dist/lib/mfaChallenge.d.ts +14 -1
  37. package/dist/lib/mfaChallenge.js +111 -6
  38. package/dist/lib/mongo.js +1 -1
  39. package/dist/lib/oauthCode.js +23 -18
  40. package/dist/lib/resetPassword.js +3 -1
  41. package/dist/lib/saml.d.ts +25 -0
  42. package/dist/lib/saml.js +64 -0
  43. package/dist/lib/scim.d.ts +44 -0
  44. package/dist/lib/scim.js +54 -0
  45. package/dist/lib/securityEvents.d.ts +28 -0
  46. package/dist/lib/securityEvents.js +26 -0
  47. package/dist/lib/session.d.ts +10 -0
  48. package/dist/lib/session.js +67 -5
  49. package/dist/lib/signing.js +5 -2
  50. package/dist/lib/suspension.d.ts +13 -0
  51. package/dist/lib/suspension.js +23 -0
  52. package/dist/lib/upload.d.ts +4 -0
  53. package/dist/lib/upload.js +26 -1
  54. package/dist/lib/uploadRegistry.d.ts +18 -0
  55. package/dist/lib/uploadRegistry.js +83 -0
  56. package/dist/lib/ws.js +7 -0
  57. package/dist/middleware/bearerAuth.js +1 -1
  58. package/dist/middleware/captcha.d.ts +10 -0
  59. package/dist/middleware/captcha.js +36 -0
  60. package/dist/middleware/csrf.js +8 -4
  61. package/dist/middleware/errorHandler.js +4 -1
  62. package/dist/middleware/identify.js +40 -13
  63. package/dist/middleware/requestSigning.js +6 -5
  64. package/dist/middleware/requireMfaSetup.js +2 -1
  65. package/dist/middleware/requireScope.d.ts +10 -0
  66. package/dist/middleware/requireScope.js +25 -0
  67. package/dist/middleware/requireStepUp.d.ts +18 -0
  68. package/dist/middleware/requireStepUp.js +29 -0
  69. package/dist/middleware/scimAuth.d.ts +8 -0
  70. package/dist/middleware/scimAuth.js +29 -0
  71. package/dist/middleware/webhookAuth.d.ts +1 -1
  72. package/dist/middleware/webhookAuth.js +6 -5
  73. package/dist/models/AuthUser.d.ts +7 -0
  74. package/dist/models/AuthUser.js +7 -0
  75. package/dist/models/M2MClient.d.ts +18 -0
  76. package/dist/models/M2MClient.js +18 -0
  77. package/dist/routes/auth.d.ts +3 -2
  78. package/dist/routes/auth.js +155 -16
  79. package/dist/routes/jobs.js +21 -3
  80. package/dist/routes/m2m.d.ts +2 -0
  81. package/dist/routes/m2m.js +72 -0
  82. package/dist/routes/metrics.d.ts +1 -0
  83. package/dist/routes/metrics.js +3 -0
  84. package/dist/routes/mfa.js +9 -1
  85. package/dist/routes/oauth.js +6 -0
  86. package/dist/routes/oidc.d.ts +2 -0
  87. package/dist/routes/oidc.js +29 -0
  88. package/dist/routes/passkey.d.ts +1 -0
  89. package/dist/routes/passkey.js +157 -0
  90. package/dist/routes/saml.d.ts +2 -0
  91. package/dist/routes/saml.js +86 -0
  92. package/dist/routes/scim.d.ts +2 -0
  93. package/dist/routes/scim.js +255 -0
  94. package/dist/routes/uploads.d.ts +13 -1
  95. package/dist/routes/uploads.js +98 -6
  96. package/dist/services/auth.d.ts +2 -0
  97. package/dist/services/auth.js +101 -22
  98. package/dist/services/mfa.js +2 -2
  99. package/dist/ws/index.js +2 -1
  100. package/docs/sections/auth-flow/full.md +790 -779
  101. package/docs/sections/auth-security-examples/full.md +23 -0
  102. package/docs/sections/metrics/full.md +6 -2
  103. package/docs/sections/passkey-login/full.md +90 -0
  104. package/docs/sections/passkey-login/overview.md +1 -0
  105. package/docs/sections/uploads/full.md +11 -2
  106. package/docs/sections/webhook-auth/full.md +1 -1
  107. package/docs/sections/websocket/full.md +12 -0
  108. package/package.json +3 -2
package/dist/app.d.ts CHANGED
@@ -2,10 +2,12 @@ import { OpenAPIHono } from "@hono/zod-openapi";
2
2
  import type { MiddlewareHandler } from "hono";
3
3
  import type { AppEnv, ValidationErrorFormatter } from "./lib/context";
4
4
  import type { RequestLogEntry, LogLevel } from "./middleware/requestLogger";
5
- import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, PasswordPolicyConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig } from "./lib/appConfig";
5
+ import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, PasswordPolicyConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig, JwtConfig, BreachedPasswordConfig, StepUpConfig, M2MConfig, OidcConfig, SamlConfig, ScimConfig } from "./lib/appConfig";
6
+ import type { CaptchaConfig } from "./lib/captcha";
6
7
  import type { AuthAdapter } from "./lib/authAdapter";
7
8
  import type { OAuthProviderConfig } from "./lib/oauth";
8
9
  type StoreType = "redis" | "mongo" | "sqlite" | "memory";
10
+ export type { BreachedPasswordConfig } from "./lib/appConfig";
9
11
  export interface DbConfig {
10
12
  /**
11
13
  * Absolute path to the SQLite database file.
@@ -117,6 +119,22 @@ export interface AuthRateLimitConfig {
117
119
  * Use "redis" for multi-instance deployments so limits are shared across servers.
118
120
  */
119
121
  store?: "memory" | "redis";
122
+ /** Credential stuffing detection. Tracks distinct accounts per IP and IPs per account. */
123
+ credentialStuffing?: {
124
+ maxAccountsPerIp?: {
125
+ count: number;
126
+ windowMs: number;
127
+ };
128
+ maxIpsPerAccount?: {
129
+ count: number;
130
+ windowMs: number;
131
+ };
132
+ onDetected?: (signal: {
133
+ type: "ip" | "account";
134
+ key: string;
135
+ count: number;
136
+ }) => void;
137
+ };
120
138
  }
121
139
  export interface AuthConfig {
122
140
  /** Set false to skip mounting /auth/* routes. Defaults to true */
@@ -173,6 +191,49 @@ export interface AuthConfig {
173
191
  * OAuth logins skip MFA (the OAuth provider is treated as the second factor).
174
192
  */
175
193
  mfa?: MfaConfig;
194
+ /**
195
+ * JWT claims configuration. When set, `iss`, `aud`, and `iat` are included in all tokens.
196
+ * Tokens with a non-matching issuer or audience will fail verification.
197
+ *
198
+ * - **`iss`** (issuer) and **`aud`** (audience) are validated on every token verification when
199
+ * configured. A token issued for a different issuer or intended for a different audience is
200
+ * rejected outright.
201
+ * - **`iat`** (issued-at) is always included in tokens once this config is set. Use it to detect
202
+ * token reuse or implement absolute expiry windows independent of `exp`.
203
+ *
204
+ * Recommended for fintech and multi-service deployments where tokens from one service should
205
+ * never be accepted by another.
206
+ * Use `algorithm: "RS256"` to enable OIDC mode.
207
+ */
208
+ jwt?: JwtConfig;
209
+ /**
210
+ * When true, suspension status is checked on every authenticated request (via identify middleware).
211
+ * This adds one adapter call per request. Default: false.
212
+ * Suspension is always enforced at login time regardless of this setting.
213
+ */
214
+ checkSuspensionOnIdentify?: boolean;
215
+ /**
216
+ * Breached password detection using the HaveIBeenPwned k-Anonymity API.
217
+ * Checks passwords at registration and reset. No full hash leaves the server.
218
+ */
219
+ breachedPasswordCheck?: BreachedPasswordConfig;
220
+ /**
221
+ * Step-up MFA configuration. When set, the requireStepUp() middleware and
222
+ * POST /auth/step-up endpoint are available. Requires auth.mfa to be configured.
223
+ */
224
+ stepUp?: StepUpConfig;
225
+ /** M2M client credentials configuration. Enables POST /oauth/token with client_credentials grant. */
226
+ m2m?: M2MConfig;
227
+ /**
228
+ * OIDC discovery and RS256 JWT signing configuration.
229
+ * When set, mounts /.well-known/openid-configuration and /.well-known/jwks.json.
230
+ * Auto-generates an RSA-2048 key pair on startup if signingKey is not provided.
231
+ */
232
+ oidc?: OidcConfig;
233
+ /** SAML 2.0 SSO configuration. Enables /auth/saml/* routes. Requires samlify peer dependency. */
234
+ saml?: SamlConfig;
235
+ /** SCIM 2.0 user provisioning. Enables /scim/v2/* endpoints with its own bearer token. */
236
+ scim?: ScimConfig;
176
237
  }
177
238
  export interface AccountDeletionConfig {
178
239
  /** Called before deletion. Throw to abort (e.g., active subscription check). */
@@ -205,7 +266,8 @@ export interface AuthSessionPolicyConfig {
205
266
  */
206
267
  trackLastActive?: boolean;
207
268
  }
208
- export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig };
269
+ export type { PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, SigningConfig, JwtConfig, StepUpConfig, OidcConfig, SamlConfig, ScimConfig };
270
+ export type { CaptchaConfig, CaptchaProvider } from "./lib/captcha";
209
271
  export interface BotProtectionConfig {
210
272
  /**
211
273
  * List of IPv4 CIDRs (e.g. "198.51.100.0/24"), IPv4 addresses, or IPv6 addresses to block outright.
@@ -273,6 +335,11 @@ export interface SecurityConfig {
273
335
  * idempotency key hashing, and session binding. All features are opt-in.
274
336
  */
275
337
  signing?: SigningConfig;
338
+ /**
339
+ * Global CAPTCHA configuration. When set, use requireCaptcha() middleware on specific routes,
340
+ * or enable adaptive mode to auto-require CAPTCHA after rate limit thresholds.
341
+ */
342
+ captcha?: CaptchaConfig;
276
343
  }
277
344
  export interface ModelSchemasConfig {
278
345
  /**
@@ -315,6 +382,12 @@ export interface JobsConfig {
315
382
  allowedQueues?: string[];
316
383
  /** When using userAuth, restrict job visibility to the user who created it. Default: false. */
317
384
  scopeToUser?: boolean;
385
+ /**
386
+ * Explicitly acknowledge that jobs endpoint is public in production.
387
+ * Set to true only when auth is "none" and you understand the risk.
388
+ * Without this, createApp throws in production when auth is "none".
389
+ */
390
+ unsafePublic?: boolean;
318
391
  }
319
392
  export interface TenantConfig {
320
393
  [key: string]: unknown;
@@ -368,6 +441,12 @@ export interface MetricsConfig {
368
441
  normalizePath?: (path: string) => string;
369
442
  /** BullMQ queue names to report depth gauges for. */
370
443
  queues?: string[];
444
+ /**
445
+ * Explicitly acknowledge that metrics endpoint is public in production.
446
+ * Set to true only when auth is "none" and you understand the risk.
447
+ * Without this, createApp throws in production when auth is "none".
448
+ */
449
+ unsafePublic?: boolean;
371
450
  }
372
451
  export interface ValidationConfig {
373
452
  /** Custom formatter for Zod validation errors. Receives issues + requestId, returns the JSON body. */
@@ -407,6 +486,24 @@ export interface UploadConfig {
407
486
  }) => string;
408
487
  tenantScopedKeys?: boolean;
409
488
  presignedUrls?: boolean | PresignedUrlConfig;
489
+ /**
490
+ * Authorization callback for upload read/delete operations.
491
+ * Called when registry ownership check fails or key is not in registry.
492
+ */
493
+ authorization?: {
494
+ authorize?: (input: {
495
+ action: "read" | "delete";
496
+ key: string;
497
+ userId?: string;
498
+ tenantId?: string;
499
+ }) => boolean | Promise<boolean>;
500
+ };
501
+ /**
502
+ * Allow operations on keys not in the upload registry.
503
+ * When false (default), operations on unknown keys return 404.
504
+ * When true, requires an authorize callback — denies if absent.
505
+ */
506
+ allowExternalKeys?: boolean;
410
507
  }
411
508
  export interface CreateAppConfig {
412
509
  /** Absolute path to the service's routes directory (use import.meta.dir + "/routes") */
@@ -452,5 +549,11 @@ export interface CreateAppConfig {
452
549
  * `/{version}/docs`. Root `/docs` becomes a version selector.
453
550
  */
454
551
  versioning?: VersioningConfig;
552
+ /**
553
+ * Security event streaming (SIEM integration). When set, auth and security events
554
+ * are emitted to the provided onEvent callback. Non-blocking — errors are swallowed.
555
+ * Use include/exclude to filter event types.
556
+ */
557
+ securityEvents?: import("./lib/securityEvents").SecurityEventConfig;
455
558
  }
456
559
  export declare const createApp: (config: CreateAppConfig) => Promise<OpenAPIHono<AppEnv>>;
package/dist/app.js CHANGED
@@ -10,7 +10,7 @@ import { defaultValidationErrorFormatter } from "./lib/context";
10
10
  import { HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN, HEADER_CSRF_TOKEN, HEADER_REQUEST_ID } from "./lib/constants";
11
11
  import { requestId } from "./middleware/requestId";
12
12
  import { requestLogger } from "./middleware/requestLogger";
13
- import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setPasswordPolicy, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig, setCsrfEnabled, setSigningConfig } from "./lib/appConfig";
13
+ import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setPasswordPolicy, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig, setCsrfEnabled, setSigningConfig, setJwtConfig, setCheckSuspensionOnIdentify, setBreachedPasswordConfig, setCaptchaConfig, setStepUpConfig, setM2MConfig, setOidcConfig, setSamlConfig, setScimConfig } from "./lib/appConfig";
14
14
  import { setEmailVerificationStore } from "./lib/emailVerification";
15
15
  import { setPasswordResetStore } from "./lib/resetPassword";
16
16
  import { setAuthRateLimitStore } from "./lib/authRateLimit";
@@ -25,10 +25,17 @@ import { setSessionStore } from "./lib/session";
25
25
  import { setCacheStore } from "./middleware/cacheResponse";
26
26
  import { maybeAutoRegister } from "./lib/createRoute";
27
27
  import { setStorageAdapter, setUploadConfig } from "./lib/upload";
28
+ import { validateJwtSecrets } from "./lib/jwt";
28
29
  export const createApp = async (config) => {
29
30
  const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
31
+ if (config.securityEvents) {
32
+ const { setSecurityEventConfig } = await import("./lib/securityEvents");
33
+ setSecurityEventConfig(config.securityEvents);
34
+ }
30
35
  const appName = appConfig.name ?? "Bun Core API";
31
36
  const openApiVersion = appConfig.version ?? "1.0.0";
37
+ // Validate JWT secrets eagerly so misconfiguration is caught at startup
38
+ validateJwtSecrets();
32
39
  // Trust-proxy for IP extraction
33
40
  const { setTrustProxy } = await import("./lib/clientIp");
34
41
  setTrustProxy(securityConfig.trustProxy ?? false);
@@ -36,6 +43,13 @@ export const createApp = async (config) => {
36
43
  if (corsOrigins === "*" && process.env.NODE_ENV === "production") {
37
44
  console.warn("[security] CORS is set to wildcard (*) in production. Configure security.cors with specific origins to restrict cross-origin access.");
38
45
  }
46
+ if (securityConfig.csrf?.enabled && corsOrigins === "*") {
47
+ if (process.env.NODE_ENV === "production") {
48
+ throw new Error("[security] CSRF protection with wildcard CORS (*) is unsafe. " +
49
+ "Set security.cors to specific origins when using CSRF.");
50
+ }
51
+ console.warn("[security] CSRF is enabled with wildcard CORS. This will be rejected in production.");
52
+ }
39
53
  const rlConfig = securityConfig.rateLimit ?? { windowMs: 60_000, max: 100 };
40
54
  const botCfg = securityConfig.botProtection ?? {};
41
55
  const enableBearerAuth = securityConfig.bearerAuth !== false;
@@ -142,12 +156,45 @@ export const createApp = async (config) => {
142
156
  const { setDeletionCancelTokenStore } = await import("./lib/deletionCancelToken");
143
157
  setDeletionCancelTokenStore(sessions);
144
158
  setAuthRateLimitStore(authRateLimit?.store ?? (enableRedis ? "redis" : "memory"));
159
+ if (authRateLimit?.credentialStuffing) {
160
+ const { setCredentialStuffingConfig } = await import("./lib/credentialStuffing");
161
+ setCredentialStuffingConfig(authRateLimit.credentialStuffing);
162
+ }
145
163
  setMaxSessions(sessionPolicy.maxSessions ?? 6);
146
164
  setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
147
165
  setIncludeInactiveSessions(sessionPolicy.includeInactiveSessions ?? false);
148
166
  setTrackLastActive(sessionPolicy.trackLastActive ?? false);
149
167
  setRefreshTokenConfig(authConfig.refreshTokens ?? null);
150
168
  setMfaConfig(authConfig.mfa ?? null);
169
+ if (authConfig.jwt)
170
+ setJwtConfig(authConfig.jwt);
171
+ if (authConfig.checkSuspensionOnIdentify)
172
+ setCheckSuspensionOnIdentify(true);
173
+ if (authConfig.breachedPasswordCheck)
174
+ setBreachedPasswordConfig(authConfig.breachedPasswordCheck);
175
+ if (authConfig.stepUp)
176
+ setStepUpConfig(authConfig.stepUp);
177
+ // JWT config
178
+ if (authConfig.jwt)
179
+ setJwtConfig(authConfig.jwt);
180
+ // OIDC: load keys, set RS256, mount discovery routes
181
+ if (authConfig.oidc) {
182
+ setOidcConfig(authConfig.oidc);
183
+ // Override JWT config with OIDC issuer and RS256
184
+ setJwtConfig({ ...(authConfig.jwt ?? {}), issuer: authConfig.oidc.issuer, algorithm: "RS256" });
185
+ const { loadJwksKey, generateAndLoadKeyPair, loadPreviousKey } = await import("./lib/jwks");
186
+ const { _setAlgorithm } = await import("./lib/jwt");
187
+ if (authConfig.oidc.signingKey) {
188
+ await loadJwksKey(authConfig.oidc.signingKey);
189
+ }
190
+ else {
191
+ await generateAndLoadKeyPair();
192
+ }
193
+ for (const prev of authConfig.oidc.previousKeys ?? []) {
194
+ await loadPreviousKey(prev);
195
+ }
196
+ _setAlgorithm("RS256");
197
+ }
151
198
  if (oauthProviders)
152
199
  initOAuthProviders(oauthProviders);
153
200
  const configuredOAuth = getConfiguredOAuthProviders();
@@ -183,7 +230,7 @@ export const createApp = async (config) => {
183
230
  `/auth/${p}/callback`,
184
231
  `/auth/${p}/link`,
185
232
  ]);
186
- const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/", "/metrics"];
233
+ const DEFAULT_BYPASS = ["/docs", "/openapi.json", "/sw.js", "/health", "/", "/metrics", "/oauth/token", "/.well-known/openid-configuration", "/.well-known/jwks.json", "/auth/saml/*", "/scim/v2/*"];
187
234
  // Add per-version docs/spec paths when versioning is configured
188
235
  const versionBypass = config.versioning
189
236
  ? config.versioning.versions.flatMap((v) => [`/${v}/docs`, `/${v}/openapi.json`])
@@ -199,10 +246,12 @@ export const createApp = async (config) => {
199
246
  });
200
247
  // Metrics collection middleware (before requestLogger so it captures all requests)
201
248
  if (config.metrics?.enabled) {
202
- if (!config.metrics.auth || config.metrics.auth === "none") {
249
+ const metricsAuth = config.metrics.auth ?? "none";
250
+ if (metricsAuth === "none" && !config.metrics.unsafePublic) {
203
251
  if (process.env.NODE_ENV === "production") {
204
- console.warn("[security] /metrics endpoint is enabled without auth. Configure metrics.auth to restrict access in production.");
252
+ throw new Error("[security] metrics.auth is required in production. Set metrics.auth or explicitly set unsafePublic: true with auth: \"none\".");
205
253
  }
254
+ console.warn("[security] /metrics is enabled without auth. Configure metrics.auth for production.");
206
255
  }
207
256
  const { metricsCollector } = await import("./middleware/metrics");
208
257
  app.use(metricsCollector({
@@ -247,7 +296,8 @@ export const createApp = async (config) => {
247
296
  if (enableBearerAuth) {
248
297
  app.use(async (c, next) => {
249
298
  const path = c.req.path;
250
- if (bearerAuthBypass.includes(path)) {
299
+ const bypassed = bearerAuthBypass.some((entry) => entry.endsWith("*") ? path.startsWith(entry.slice(0, -1)) : path === entry);
300
+ if (bypassed) {
251
301
  return next();
252
302
  }
253
303
  return bearerAuth(c, next);
@@ -258,6 +308,10 @@ export const createApp = async (config) => {
258
308
  if (securityConfig.signing) {
259
309
  setSigningConfig(securityConfig.signing);
260
310
  }
311
+ // CAPTCHA config — store globally so requireCaptcha() can read it without explicit param
312
+ if (securityConfig.captcha) {
313
+ setCaptchaConfig(securityConfig.captcha);
314
+ }
261
315
  // CSRF protection (after identify so we can check for auth cookie presence)
262
316
  if (securityConfig.csrf?.enabled) {
263
317
  setCsrfEnabled(true);
@@ -343,13 +397,15 @@ export const createApp = async (config) => {
343
397
  continue; // mounted separately below when mfa is configured
344
398
  if (file === "jobs.ts")
345
399
  continue; // mounted separately below when jobs.statusEndpoint is true
400
+ if (file === "oidc.ts")
401
+ continue; // mounted separately below when oidc is configured
346
402
  const mod = await import(`${coreRoutesDir}/${file}`);
347
403
  if (mod.router)
348
404
  app.route("/", mod.router);
349
405
  }
350
406
  if (enableAuthRoutes) {
351
407
  const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
352
- app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens }));
408
+ app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens, stepUp: authConfig.stepUp }));
353
409
  }
354
410
  if (configuredOAuth.length > 0) {
355
411
  const { createOAuthRouter } = await import(`${coreRoutesDir}/oauth`);
@@ -365,6 +421,26 @@ export const createApp = async (config) => {
365
421
  const { createMfaRouter } = await import(`${coreRoutesDir}/mfa`);
366
422
  app.route("/", createMfaRouter({ rateLimit: authRateLimit }));
367
423
  }
424
+ if (authConfig.mfa?.webauthn?.allowPasswordlessLogin && enableAuthRoutes) {
425
+ const { assertWebAuthnDependency } = await import("./services/mfa");
426
+ await assertWebAuthnDependency();
427
+ const { createPasskeyRouter } = await import(`${coreRoutesDir}/passkey`);
428
+ app.route("/", createPasskeyRouter());
429
+ }
430
+ if (authConfig.m2m?.enabled !== false && authConfig.m2m) {
431
+ setM2MConfig(authConfig.m2m);
432
+ const { createM2MRouter } = await import(`${coreRoutesDir}/m2m`);
433
+ app.route("/", createM2MRouter());
434
+ }
435
+ if (config.jobs?.statusEndpoint) {
436
+ const jobsAuth = config.jobs.auth ?? "none";
437
+ if (jobsAuth === "none" && !config.jobs.unsafePublic) {
438
+ if (process.env.NODE_ENV === "production") {
439
+ throw new Error("[security] jobs.auth is required in production. Set jobs.auth or explicitly set unsafePublic: true with auth: \"none\".");
440
+ }
441
+ console.warn("[security] /jobs is enabled without auth. Configure jobs.auth for production.");
442
+ }
443
+ }
368
444
  if (config.jobs?.statusEndpoint) {
369
445
  const { createJobsRouter } = await import(`${coreRoutesDir}/jobs`);
370
446
  app.route("/", createJobsRouter(config.jobs));
@@ -374,20 +450,44 @@ export const createApp = async (config) => {
374
450
  app.route("/", createMetricsRouter({
375
451
  auth: config.metrics.auth,
376
452
  queues: config.metrics.queues,
453
+ unsafePublic: config.metrics.unsafePublic,
377
454
  }));
378
455
  }
379
456
  if (config.groups?.managementRoutes) {
380
457
  const { createGroupsRouter } = await import(`${coreRoutesDir}/groups`);
381
458
  app.route("/", createGroupsRouter(config.groups));
382
459
  }
460
+ if (authConfig.oidc) {
461
+ const { createOidcRouter } = await import(`${coreRoutesDir}/oidc`);
462
+ app.route("/", createOidcRouter());
463
+ }
464
+ if (authConfig.saml) {
465
+ setSamlConfig(authConfig.saml);
466
+ const { createSamlRouter } = await import(`${coreRoutesDir}/saml`);
467
+ app.route("/", createSamlRouter());
468
+ }
469
+ if (authConfig.scim) {
470
+ const { setScimTokens } = await import("./middleware/scimAuth");
471
+ setScimConfig(authConfig.scim);
472
+ setScimTokens(authConfig.scim.bearerTokens);
473
+ const { createScimRouter } = await import(`${coreRoutesDir}/scim`);
474
+ app.route("/", createScimRouter());
475
+ }
383
476
  if (config.upload) {
384
- const { storage, presignedUrls, ...uploadOpts } = config.upload;
477
+ const { storage, presignedUrls, authorization, allowExternalKeys, ...uploadOpts } = config.upload;
385
478
  setStorageAdapter(storage);
386
479
  setUploadConfig(uploadOpts);
480
+ // Wire upload registry store to match session store backend
481
+ const { setUploadRegistryStore } = await import("./lib/uploadRegistry");
482
+ setUploadRegistryStore(sessions);
387
483
  if (presignedUrls) {
388
484
  const { createUploadsRouter } = await import(`${coreRoutesDir}/uploads`);
389
485
  const presignConfig = presignedUrls === true ? {} : presignedUrls;
390
- app.route("/", createUploadsRouter(presignConfig));
486
+ app.route("/", createUploadsRouter({
487
+ ...presignConfig,
488
+ authorization,
489
+ allowExternalKeys,
490
+ }));
391
491
  }
392
492
  }
393
493
  // Helper to register standard security schemes on an OpenAPI registry
@@ -537,7 +637,10 @@ export const createApp = async (config) => {
537
637
  }
538
638
  }
539
639
  if (err instanceof HttpError) {
540
- return c.json({ error: err.message, requestId: reqId }, err.status);
640
+ const body = { error: err.message, requestId: reqId };
641
+ if (err.code !== undefined)
642
+ body.code = err.code;
643
+ return c.json(body, err.status);
541
644
  }
542
645
  console.error(err);
543
646
  return c.json({ error: "Internal Server Error", requestId: reqId }, 500);
package/dist/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  export { createApp } from "./app";
2
2
  export { createServer } from "./server";
3
- export type { CreateAppConfig, ModelSchemasConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, AccountDeletionConfig, OAuthConfig, SecurityConfig, CsrfConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, JobsConfig, TenancyConfig, TenantConfig, LoggingConfig, MetricsConfig, ValidationConfig, VersioningConfig, SigningConfig } from "./app";
3
+ export type { SecurityEvent, SecurityEventType, SecurityEventConfig } from "./lib/securityEvents";
4
+ export { emitSecurityEvent, setSecurityEventConfig, getSecurityEventConfig } from "./lib/securityEvents";
5
+ export type { CreateAppConfig, ModelSchemasConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, AccountDeletionConfig, OAuthConfig, SecurityConfig, CsrfConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, MfaWebAuthnConfig, JobsConfig, TenancyConfig, TenantConfig, LoggingConfig, MetricsConfig, ValidationConfig, VersioningConfig, SigningConfig, JwtConfig, BreachedPasswordConfig, StepUpConfig, OidcConfig, SamlConfig, ScimConfig } from "./app";
6
+ export { setScimTokens } from "./middleware/scimAuth";
4
7
  export type { PasswordPolicyConfig } from "./lib/appConfig";
8
+ export type { SamlProfile } from "./lib/saml";
5
9
  export type { CreateServerConfig, WsConfig } from "./server";
6
10
  export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
7
11
  export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
@@ -18,6 +22,7 @@ export type { DtoMapperConfig } from "./lib/createDtoMapper";
18
22
  export type { AppEnv, AppVariables, ValidationErrorFormatter, DefaultValidationErrorBody, ValidationErrorDetail } from "./lib/context";
19
23
  export { defaultValidationErrorFormatter } from "./lib/context";
20
24
  export { signToken, verifyToken } from "./lib/jwt";
25
+ export { getJwks, loadJwksKey, generateAndLoadKeyPair, isJwksLoaded } from "./lib/jwks";
21
26
  export { log } from "./lib/logger";
22
27
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
23
28
  export { createDeletionCancelToken, consumeDeletionCancelToken, setDeletionCancelTokenStore } from "./lib/deletionCancelToken";
@@ -28,13 +33,15 @@ export type { IdempotencyOptions } from "./lib/idempotency";
28
33
  export { getClientIp, setTrustProxy } from "./lib/clientIp";
29
34
  export { storeOAuthCode, consumeOAuthCode, setOAuthCodeStore } from "./lib/oauthCode";
30
35
  export type { OAuthCodePayload } from "./lib/oauthCode";
31
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken, getSessionFingerprint, setSessionFingerprint } from "./lib/session";
36
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken, getSessionFingerprint, setSessionFingerprint, setMfaVerifiedAt, getMfaVerifiedAt } from "./lib/session";
32
37
  export type { SessionMetadata, SessionInfo, RefreshResult } from "./lib/session";
33
38
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
34
39
  export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, createWebAuthnRegistrationChallenge, consumeWebAuthnRegistrationChallenge, clearMemoryMfaChallenges } from "./lib/mfaChallenge";
35
40
  export type { MfaChallengeData, MfaChallengeOptions, MfaChallengePurpose } from "./lib/mfaChallenge";
36
41
  export { bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore } from "./lib/authRateLimit";
37
42
  export type { LimitOpts } from "./lib/authRateLimit";
43
+ export { trackFailedLogin, isStuffingBlocked, setCredentialStuffingConfig, clearCredentialStuffingStore } from "./lib/credentialStuffing";
44
+ export type { CredentialStuffingConfig } from "./lib/credentialStuffing";
38
45
  export { validate } from "./lib/validate";
39
46
  export { bearerAuth } from "./middleware/bearerAuth";
40
47
  export { botProtection } from "./middleware/botProtection";
@@ -46,6 +53,8 @@ export { userAuth } from "./middleware/userAuth";
46
53
  export { requireRole } from "./middleware/requireRole";
47
54
  export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
48
55
  export { requireMfaSetup } from "./middleware/requireMfaSetup";
56
+ export { requireStepUp } from "./middleware/requireStepUp";
57
+ export type { StepUpOptions } from "./middleware/requireStepUp";
49
58
  export { csrfProtection, refreshCsrfToken, clearCsrfToken } from "./middleware/csrf";
50
59
  export type { CsrfMiddlewareOptions } from "./middleware/csrf";
51
60
  export { cacheResponse, bustCache, bustCachePattern, setCacheStore, getCacheModel } from "./middleware/cacheResponse";
@@ -60,6 +69,13 @@ export { requestLogger } from "./middleware/requestLogger";
60
69
  export type { RequestLogEntry, RequestLoggerOptions, LogLevel } from "./middleware/requestLogger";
61
70
  export { metricsCollector } from "./middleware/metrics";
62
71
  export type { MetricsMiddlewareOptions } from "./middleware/metrics";
72
+ export { requireCaptcha } from "./middleware/captcha";
73
+ export type { CaptchaConfig, CaptchaProvider } from "./lib/captcha";
74
+ export { verifyCaptcha } from "./lib/captcha";
75
+ export { requireScope } from "./middleware/requireScope";
76
+ export { createM2MClient, deleteM2MClient, listM2MClients, getM2MClient } from "./lib/m2m";
77
+ export type { M2MClientRecord } from "./lib/authAdapter";
78
+ export type { M2MConfig } from "./lib/appConfig";
63
79
  export { buildFingerprint } from "./lib/fingerprint";
64
80
  export { logAuditEntry, getAuditLogs, clearAuditLogMemoryStore } from "./lib/auditLog";
65
81
  export { resetMetrics, incrementCounter, observeHistogram, registerGaugeCallback, serializeMetrics, closeMetricsQueues } from "./lib/metrics";
@@ -67,7 +83,8 @@ export type { AuditLogEntry, AuditLogOptions, AuditLogQuery } from "./lib/auditL
67
83
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
68
84
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
69
85
  export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
70
- export type { AuthAdapter, OAuthProfile, WebAuthnCredential } from "./lib/authAdapter";
86
+ export { setSuspended, getSuspended } from "./lib/suspension";
87
+ export type { AuthAdapter, OAuthProfile, IdentityProfile, UserRecord, UserQuery, WebAuthnCredential } from "./lib/authAdapter";
71
88
  export type { OAuthProviderConfig } from "./lib/oauth";
72
89
  export { websocket, createWsUpgradeHandler } from "./ws/index";
73
90
  export type { SocketData } from "./ws/index";
@@ -87,8 +104,10 @@ export { offsetParams, parseOffsetParams, paginatedResponse, cursorParams, parse
87
104
  export type { OffsetParamDefaults, ParsedOffsetParams, CursorParamDefaults, ParsedCursorParams, CursorResult, } from "./lib/pagination";
88
105
  export { handleUpload } from "./middleware/upload";
89
106
  export type { UploadMiddlewareOptions } from "./middleware/upload";
90
- export { parseUpload, setStorageAdapter, getStorageAdapter, setUploadConfig, getUploadConfig } from "./lib/upload";
107
+ export { parseUpload, setStorageAdapter, getStorageAdapter, setUploadConfig, getUploadConfig, generateUploadKeyFromFilename } from "./lib/upload";
91
108
  export type { UploadOpts } from "./lib/upload";
109
+ export { setUploadRegistryStore, clearUploadRegistry, registerUpload, getUploadRecord, deleteUploadRecord } from "./lib/uploadRegistry";
110
+ export type { UploadRecord } from "./lib/uploadRegistry";
92
111
  export type { StorageAdapter, UploadResult } from "./lib/storageAdapter";
93
112
  export type { UploadConfig, PresignedUrlConfig } from "./app";
94
113
  export { memoryStorage, clearMemoryUploadStore } from "./adapters/memoryStorage";
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
1
  // App factory
2
2
  export { createApp } from "./app";
3
3
  export { createServer } from "./server";
4
+ export { emitSecurityEvent, setSecurityEventConfig, getSecurityEventConfig } from "./lib/securityEvents";
5
+ export { setScimTokens } from "./middleware/scimAuth";
4
6
  // Database
5
7
  export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
6
8
  export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
@@ -15,6 +17,7 @@ export { zodToMongoose } from "./lib/zodToMongoose";
15
17
  export { createDtoMapper } from "./lib/createDtoMapper";
16
18
  export { defaultValidationErrorFormatter } from "./lib/context";
17
19
  export { signToken, verifyToken } from "./lib/jwt";
20
+ export { getJwks, loadJwksKey, generateAndLoadKeyPair, isJwksLoaded } from "./lib/jwks";
18
21
  export { log } from "./lib/logger";
19
22
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
20
23
  export { createDeletionCancelToken, consumeDeletionCancelToken, setDeletionCancelTokenStore } from "./lib/deletionCancelToken";
@@ -23,10 +26,11 @@ export { hmacSign, hmacVerify, signCookieValue, verifyCookieValue, signCursor, v
23
26
  export { idempotent, setIdempotencyStore, clearIdempotencyMemoryStore } from "./lib/idempotency";
24
27
  export { getClientIp, setTrustProxy } from "./lib/clientIp";
25
28
  export { storeOAuthCode, consumeOAuthCode, setOAuthCodeStore } from "./lib/oauthCode";
26
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken, getSessionFingerprint, setSessionFingerprint } from "./lib/session";
29
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken, getSessionFingerprint, setSessionFingerprint, setMfaVerifiedAt, getMfaVerifiedAt } from "./lib/session";
27
30
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
28
31
  export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore, createWebAuthnRegistrationChallenge, consumeWebAuthnRegistrationChallenge, clearMemoryMfaChallenges } from "./lib/mfaChallenge";
29
32
  export { bustAuthLimit, trackAttempt, isLimited, clearMemoryRateLimitStore } from "./lib/authRateLimit";
33
+ export { trackFailedLogin, isStuffingBlocked, setCredentialStuffingConfig, clearCredentialStuffingStore } from "./lib/credentialStuffing";
30
34
  export { validate } from "./lib/validate";
31
35
  // Middleware
32
36
  export { bearerAuth } from "./middleware/bearerAuth";
@@ -37,6 +41,7 @@ export { userAuth } from "./middleware/userAuth";
37
41
  export { requireRole } from "./middleware/requireRole";
38
42
  export { requireVerifiedEmail } from "./middleware/requireVerifiedEmail";
39
43
  export { requireMfaSetup } from "./middleware/requireMfaSetup";
44
+ export { requireStepUp } from "./middleware/requireStepUp";
40
45
  export { csrfProtection, refreshCsrfToken, clearCsrfToken } from "./middleware/csrf";
41
46
  export { cacheResponse, bustCache, bustCachePattern, setCacheStore, getCacheModel } from "./middleware/cacheResponse";
42
47
  export { webhookAuth } from "./middleware/webhookAuth";
@@ -45,6 +50,10 @@ export { auditLog } from "./middleware/auditLog";
45
50
  export { requestId } from "./middleware/requestId";
46
51
  export { requestLogger } from "./middleware/requestLogger";
47
52
  export { metricsCollector } from "./middleware/metrics";
53
+ export { requireCaptcha } from "./middleware/captcha";
54
+ export { verifyCaptcha } from "./lib/captcha";
55
+ export { requireScope } from "./middleware/requireScope";
56
+ export { createM2MClient, deleteM2MClient, listM2MClients, getM2MClient } from "./lib/m2m";
48
57
  // Lib utilities (bot protection)
49
58
  export { buildFingerprint } from "./lib/fingerprint";
50
59
  export { logAuditEntry, getAuditLogs, clearAuditLogMemoryStore } from "./lib/auditLog";
@@ -53,6 +62,7 @@ export { resetMetrics, incrementCounter, observeHistogram, registerGaugeCallback
53
62
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
54
63
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
55
64
  export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
65
+ export { setSuspended, getSuspended } from "./lib/suspension";
56
66
  // WebSocket
57
67
  export { websocket, createWsUpgradeHandler } from "./ws/index";
58
68
  export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers, setPresenceEnabled } from "./lib/ws";
@@ -71,7 +81,8 @@ export { createGroup, deleteGroup, getGroup, listGroups, updateGroup, addGroupMe
71
81
  export { offsetParams, parseOffsetParams, paginatedResponse, cursorParams, parseCursorParams, cursorResponse, maybeSignCursor, } from "./lib/pagination";
72
82
  // Upload
73
83
  export { handleUpload } from "./middleware/upload";
74
- export { parseUpload, setStorageAdapter, getStorageAdapter, setUploadConfig, getUploadConfig } from "./lib/upload";
84
+ export { parseUpload, setStorageAdapter, getStorageAdapter, setUploadConfig, getUploadConfig, generateUploadKeyFromFilename } from "./lib/upload";
85
+ export { setUploadRegistryStore, clearUploadRegistry, registerUpload, getUploadRecord, deleteUploadRecord } from "./lib/uploadRegistry";
75
86
  export { memoryStorage, clearMemoryUploadStore } from "./adapters/memoryStorage";
76
87
  export { localStorage } from "./adapters/localStorage";
77
88
  export { s3Storage } from "./adapters/s3Storage";
@@ -1,6 +1,7 @@
1
1
  export declare class HttpError extends Error {
2
2
  status: number;
3
- constructor(status: number, message: string);
3
+ code?: string | undefined;
4
+ constructor(status: number, message: string, code?: string | undefined);
4
5
  }
5
6
  import type { ZodIssue } from "zod";
6
7
  export declare class ValidationError extends HttpError {
@@ -1,8 +1,10 @@
1
1
  export class HttpError extends Error {
2
2
  status;
3
- constructor(status, message) {
3
+ code;
4
+ constructor(status, message, code) {
4
5
  super(message);
5
6
  this.status = status;
7
+ this.code = code;
6
8
  }
7
9
  }
8
10
  export class ValidationError extends HttpError {