@lastshotlabs/bunshot 0.0.10 → 0.0.16

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 (100) hide show
  1. package/README.md +2510 -1580
  2. package/dist/adapters/memoryAuth.d.ts +4 -0
  3. package/dist/adapters/memoryAuth.js +131 -2
  4. package/dist/adapters/mongoAuth.js +56 -0
  5. package/dist/adapters/sqliteAuth.d.ts +6 -0
  6. package/dist/adapters/sqliteAuth.js +137 -2
  7. package/dist/app.d.ts +107 -2
  8. package/dist/app.js +83 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +15 -5
  12. package/dist/index.js +10 -3
  13. package/dist/lib/appConfig.d.ts +46 -0
  14. package/dist/lib/appConfig.js +20 -0
  15. package/dist/lib/authAdapter.d.ts +30 -0
  16. package/dist/lib/constants.d.ts +2 -0
  17. package/dist/lib/constants.js +2 -0
  18. package/dist/lib/context.d.ts +2 -0
  19. package/dist/lib/createDtoMapper.d.ts +33 -0
  20. package/dist/lib/createDtoMapper.js +69 -0
  21. package/dist/lib/createRoute.d.ts +61 -0
  22. package/dist/lib/createRoute.js +147 -0
  23. package/dist/lib/jwt.d.ts +1 -1
  24. package/dist/lib/jwt.js +2 -2
  25. package/dist/lib/mfaChallenge.d.ts +20 -0
  26. package/dist/lib/mfaChallenge.js +184 -0
  27. package/dist/lib/queue.d.ts +33 -0
  28. package/dist/lib/queue.js +98 -0
  29. package/dist/lib/roles.d.ts +4 -0
  30. package/dist/lib/roles.js +27 -0
  31. package/dist/lib/session.d.ts +12 -0
  32. package/dist/lib/session.js +163 -5
  33. package/dist/lib/tenant.d.ts +15 -0
  34. package/dist/lib/tenant.js +65 -0
  35. package/dist/lib/zodToMongoose.d.ts +38 -0
  36. package/dist/lib/zodToMongoose.js +84 -0
  37. package/dist/middleware/cacheResponse.js +4 -1
  38. package/dist/middleware/rateLimit.d.ts +2 -1
  39. package/dist/middleware/rateLimit.js +5 -2
  40. package/dist/middleware/requireRole.d.ts +14 -3
  41. package/dist/middleware/requireRole.js +46 -6
  42. package/dist/middleware/tenant.d.ts +5 -0
  43. package/dist/middleware/tenant.js +116 -0
  44. package/dist/models/AuthUser.d.ts +8 -0
  45. package/dist/models/AuthUser.js +8 -0
  46. package/dist/models/TenantRole.d.ts +15 -0
  47. package/dist/models/TenantRole.js +23 -0
  48. package/dist/routes/auth.d.ts +5 -3
  49. package/dist/routes/auth.js +253 -80
  50. package/dist/routes/jobs.d.ts +2 -0
  51. package/dist/routes/jobs.js +270 -0
  52. package/dist/routes/mfa.d.ts +1 -0
  53. package/dist/routes/mfa.js +409 -0
  54. package/dist/routes/oauth.js +107 -16
  55. package/dist/server.js +9 -0
  56. package/dist/services/auth.d.ts +21 -2
  57. package/dist/services/auth.js +97 -17
  58. package/dist/services/mfa.d.ts +37 -0
  59. package/dist/services/mfa.js +276 -0
  60. package/docs/sections/adding-middleware/full.md +35 -0
  61. package/docs/sections/adding-models/full.md +125 -0
  62. package/docs/sections/adding-models/overview.md +13 -0
  63. package/docs/sections/adding-routes/full.md +182 -0
  64. package/docs/sections/adding-routes/overview.md +23 -0
  65. package/docs/sections/auth-flow/full.md +456 -0
  66. package/docs/sections/auth-flow/overview.md +10 -0
  67. package/docs/sections/cli/full.md +30 -0
  68. package/docs/sections/configuration/full.md +135 -0
  69. package/docs/sections/configuration/overview.md +17 -0
  70. package/docs/sections/configuration-example/full.md +99 -0
  71. package/docs/sections/configuration-example/overview.md +30 -0
  72. package/docs/sections/documentation/full.md +171 -0
  73. package/docs/sections/environment-variables/full.md +55 -0
  74. package/docs/sections/exports/full.md +83 -0
  75. package/docs/sections/extending-context/full.md +59 -0
  76. package/docs/sections/header.md +3 -0
  77. package/docs/sections/installation/full.md +6 -0
  78. package/docs/sections/jobs/full.md +140 -0
  79. package/docs/sections/jobs/overview.md +15 -0
  80. package/docs/sections/mongodb-connections/full.md +45 -0
  81. package/docs/sections/mongodb-connections/overview.md +7 -0
  82. package/docs/sections/multi-tenancy/full.md +62 -0
  83. package/docs/sections/multi-tenancy/overview.md +15 -0
  84. package/docs/sections/oauth/full.md +119 -0
  85. package/docs/sections/oauth/overview.md +16 -0
  86. package/docs/sections/package-development/full.md +7 -0
  87. package/docs/sections/peer-dependencies/full.md +43 -0
  88. package/docs/sections/quick-start/full.md +43 -0
  89. package/docs/sections/response-caching/full.md +115 -0
  90. package/docs/sections/response-caching/overview.md +13 -0
  91. package/docs/sections/roles/full.md +136 -0
  92. package/docs/sections/roles/overview.md +12 -0
  93. package/docs/sections/running-without-redis/full.md +16 -0
  94. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  95. package/docs/sections/stack/full.md +10 -0
  96. package/docs/sections/websocket/full.md +100 -0
  97. package/docs/sections/websocket/overview.md +5 -0
  98. package/docs/sections/websocket-rooms/full.md +97 -0
  99. package/docs/sections/websocket-rooms/overview.md +5 -0
  100. package/package.json +19 -10
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 } from "./lib/constants";
11
+ import { setAppName, setAppRoles, setDefaultRole, setPrimaryField, setEmailVerificationConfig, setPasswordResetConfig, setMaxSessions, setPersistSessionMetadata, setIncludeInactiveSessions, setTrackLastActive, setRefreshTokenConfig, setMfaConfig } from "./lib/appConfig";
12
12
  import { setEmailVerificationStore } from "./lib/emailVerification";
13
13
  import { setPasswordResetStore } from "./lib/resetPassword";
14
14
  import { setAuthRateLimitStore } from "./lib/authRateLimit";
@@ -21,6 +21,7 @@ import { connectMongo, connectAuthMongo, connectAppMongo } from "./lib/mongo";
21
21
  import { connectRedis } from "./lib/redis";
22
22
  import { setSessionStore } from "./lib/session";
23
23
  import { setCacheStore } from "./middleware/cacheResponse";
24
+ import { maybeAutoRegister } from "./lib/createRoute";
24
25
  export const createApp = async (config) => {
25
26
  const { routesDir, app: appConfig = {}, auth: authConfig = {}, security: securityConfig = {}, middleware = [], db = {}, } = config;
26
27
  const appName = appConfig.name ?? "Bun Core API";
@@ -109,6 +110,8 @@ export const createApp = async (config) => {
109
110
  setPersistSessionMetadata(sessionPolicy.persistSessionMetadata ?? true);
110
111
  setIncludeInactiveSessions(sessionPolicy.includeInactiveSessions ?? false);
111
112
  setTrackLastActive(sessionPolicy.trackLastActive ?? false);
113
+ setRefreshTokenConfig(authConfig.refreshTokens ?? null);
114
+ setMfaConfig(authConfig.mfa ?? null);
112
115
  if (oauthProviders)
113
116
  initOAuthProviders(oauthProviders);
114
117
  const configuredOAuth = getConfiguredOAuthProviders();
@@ -124,7 +127,7 @@ export const createApp = async (config) => {
124
127
  const app = new OpenAPIHono();
125
128
  app.use(logger());
126
129
  app.use(secureHeaders());
127
- app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
130
+ app.use(cors({ origin: corsOrigins, allowHeaders: ["Content-Type", "Authorization", HEADER_USER_TOKEN, HEADER_REFRESH_TOKEN], exposeHeaders: ["x-cache"], credentials: true }));
128
131
  if ((botCfg.blockList?.length ?? 0) > 0) {
129
132
  const { botProtection } = await import("./middleware/botProtection");
130
133
  app.use(botProtection({ blockList: botCfg.blockList }));
@@ -140,9 +143,50 @@ export const createApp = async (config) => {
140
143
  });
141
144
  }
142
145
  app.use(identify);
146
+ // Tenant resolution middleware (after identify, before user middleware + routes)
147
+ if (config.tenancy) {
148
+ const { createTenantMiddleware } = await import("./middleware/tenant");
149
+ app.use(createTenantMiddleware(config.tenancy));
150
+ }
143
151
  for (const mw of middleware)
144
152
  app.use(mw);
145
153
  setAppName(appName);
154
+ // Schema pre-loading — import shared schema files before routes so registerSchema /
155
+ // registerSchemas calls run first, guaranteeing $ref instead of inline shapes.
156
+ const msConfig = config.modelSchemas;
157
+ if (msConfig) {
158
+ const { paths, registration = "auto" } = typeof msConfig === "string" || Array.isArray(msConfig)
159
+ ? { paths: msConfig, registration: "auto" }
160
+ : msConfig;
161
+ const pathArray = paths ? (Array.isArray(paths) ? paths : [paths]) : [];
162
+ for (const entry of pathArray) {
163
+ // Normalize to forward slashes so splitting works on both Windows and Unix.
164
+ const normalized = entry.replaceAll("\\", "/");
165
+ // Split glob patterns: everything before the first wildcard segment is the cwd.
166
+ let cwd;
167
+ let pattern;
168
+ if (!normalized.includes("*")) {
169
+ cwd = normalized;
170
+ pattern = "**/*.ts";
171
+ }
172
+ else {
173
+ const parts = normalized.split("/");
174
+ const starIdx = parts.findIndex((p) => p.includes("*"));
175
+ cwd = parts.slice(0, starIdx).join("/");
176
+ pattern = parts.slice(starIdx).join("/");
177
+ }
178
+ const schemaGlob = new Bun.Glob(pattern);
179
+ for await (const file of schemaGlob.scan({ cwd })) {
180
+ const mod = await import(`${cwd}/${file}`);
181
+ if (registration === "auto") {
182
+ for (const [exportName, value] of Object.entries(mod)) {
183
+ maybeAutoRegister(exportName, value);
184
+ }
185
+ }
186
+ // "explicit": file imported; any registerSchema/registerSchemas calls inside already ran
187
+ }
188
+ }
189
+ }
146
190
  // Core routes (auth, etc.)
147
191
  const coreRoutesDir = import.meta.dir + "/routes";
148
192
  const coreGlob = new Bun.Glob("*.ts");
@@ -151,17 +195,35 @@ export const createApp = async (config) => {
151
195
  continue; // mounted separately below via createAuthRouter
152
196
  if (file === "oauth.ts")
153
197
  continue; // mounted separately below
198
+ if (file === "mfa.ts")
199
+ continue; // mounted separately below when mfa is configured
200
+ if (file === "jobs.ts")
201
+ continue; // mounted separately below when jobs.statusEndpoint is true
154
202
  const mod = await import(`${coreRoutesDir}/${file}`);
155
203
  if (mod.router)
156
204
  app.route("/", mod.router);
157
205
  }
158
206
  if (enableAuthRoutes) {
159
207
  const { createAuthRouter } = await import(`${coreRoutesDir}/auth`);
160
- app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit }));
208
+ app.route("/", createAuthRouter({ primaryField, emailVerification, passwordReset, rateLimit: authRateLimit, accountDeletion: authConfig.accountDeletion, refreshTokens: authConfig.refreshTokens }));
161
209
  }
162
210
  if (configuredOAuth.length > 0) {
163
211
  app.route("/", createOAuthRouter(configuredOAuth, postOAuthRedirect));
164
212
  }
213
+ if (authConfig.mfa && enableAuthRoutes) {
214
+ const { setMfaChallengeStore, setMfaChallengeSqliteDb } = await import("./lib/mfaChallenge");
215
+ setMfaChallengeStore(sessions);
216
+ if (sessions === "sqlite") {
217
+ const { getDb } = await import("./adapters/sqliteAuth");
218
+ setMfaChallengeSqliteDb(getDb());
219
+ }
220
+ const { createMfaRouter } = await import(`${coreRoutesDir}/mfa`);
221
+ app.route("/", createMfaRouter());
222
+ }
223
+ if (config.jobs?.statusEndpoint) {
224
+ const { createJobsRouter } = await import(`${coreRoutesDir}/jobs`);
225
+ app.route("/", createJobsRouter(config.jobs));
226
+ }
165
227
  // Service routes — collect all, sort by optional exported `priority`, then mount
166
228
  const serviceGlob = new Bun.Glob("**/*.ts");
167
229
  const serviceFiles = [];
@@ -186,6 +248,23 @@ export const createApp = async (config) => {
186
248
  return c.json({ error: "Internal Server Error" }, 500);
187
249
  });
188
250
  app.notFound((c) => c.json({ error: "Not Found" }, 404));
251
+ app.openAPIRegistry.registerComponent("securitySchemes", "cookieAuth", {
252
+ type: "apiKey",
253
+ in: "cookie",
254
+ name: "token",
255
+ description: "Session cookie set automatically on login/register.",
256
+ });
257
+ app.openAPIRegistry.registerComponent("securitySchemes", "userToken", {
258
+ type: "apiKey",
259
+ in: "header",
260
+ name: "x-user-token",
261
+ description: "JWT session token passed as the x-user-token request header (alternative to the session cookie).",
262
+ });
263
+ app.openAPIRegistry.registerComponent("securitySchemes", "bearerAuth", {
264
+ type: "http",
265
+ scheme: "bearer",
266
+ description: "API key passed as Authorization: Bearer <token>. Required on all endpoints unless bearer auth is disabled in CreateAppConfig or the path is in the bypass list.",
267
+ });
189
268
  app.doc("/openapi.json", { openapi: "3.0.0", info: { title: appName, version: openApiVersion } });
190
269
  app.get("/docs", Scalar({ url: "/openapi.json" }));
191
270
  app.get("/sw.js", (c) => c.body("", 200, { "Content-Type": "application/javascript" }));
@@ -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,20 +1,27 @@
1
1
  export { createApp } from "./app";
2
2
  export { createServer } from "./server";
3
- export type { CreateAppConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, OAuthConfig, SecurityConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "./app";
3
+ export type { CreateAppConfig, ModelSchemasConfig, DbConfig, AppMeta, AuthConfig, AuthRateLimitConfig, AccountDeletionConfig, OAuthConfig, SecurityConfig, BotProtectionConfig, PrimaryField, EmailVerificationConfig, PasswordResetConfig, RefreshTokenConfig, MfaConfig, MfaEmailOtpConfig, JobsConfig, TenancyConfig, TenantConfig } from "./app";
4
4
  export type { CreateServerConfig, WsConfig } from "./server";
5
5
  export { appConnection, authConnection, mongoose, connectMongo, connectAuthMongo, connectAppMongo, disconnectMongo } from "./lib/mongo";
6
6
  export { connectRedis, disconnectRedis, getRedis } from "./lib/redis";
7
7
  export { getAppRoles } from "./lib/appConfig";
8
8
  export { HttpError } from "./lib/HttpError";
9
- export { COOKIE_TOKEN, HEADER_USER_TOKEN } from "./lib/constants";
9
+ export { COOKIE_TOKEN, HEADER_USER_TOKEN, COOKIE_REFRESH_TOKEN, HEADER_REFRESH_TOKEN } from "./lib/constants";
10
10
  export { createRouter } from "./lib/context";
11
+ export { createRoute, withSecurity, registerSchema, registerSchemas } from "./lib/createRoute";
12
+ export { zodToMongoose } from "./lib/zodToMongoose";
13
+ export type { ZodToMongooseConfig, ZodToMongooseRefConfig } from "./lib/zodToMongoose";
14
+ export { createDtoMapper } from "./lib/createDtoMapper";
15
+ export type { DtoMapperConfig } from "./lib/createDtoMapper";
11
16
  export type { AppEnv, AppVariables } from "./lib/context";
12
17
  export { signToken, verifyToken } from "./lib/jwt";
13
18
  export { log } from "./lib/logger";
14
19
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
15
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
16
- export type { SessionMetadata, SessionInfo } from "./lib/session";
20
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "./lib/session";
21
+ export type { SessionMetadata, SessionInfo, RefreshResult } from "./lib/session";
17
22
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
23
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore } from "./lib/mfaChallenge";
24
+ export type { MfaChallengeData } from "./lib/mfaChallenge";
18
25
  export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
19
26
  export type { LimitOpts } from "./lib/authRateLimit";
20
27
  export { validate } from "./lib/validate";
@@ -31,9 +38,12 @@ export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./mid
31
38
  export { buildFingerprint } from "./lib/fingerprint";
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
  export type { AuthAdapter, OAuthProfile } from "./lib/authAdapter";
36
43
  export type { OAuthProviderConfig } from "./lib/oauth";
37
44
  export { websocket, createWsUpgradeHandler } from "./ws/index";
38
45
  export type { SocketData } from "./ws/index";
39
46
  export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers } from "./lib/ws";
47
+ export { createTenant, deleteTenant, getTenant, listTenants } from "./lib/tenant";
48
+ export type { TenantInfo, CreateTenantOptions } from "./lib/tenant";
49
+ export { invalidateTenantCache } from "./middleware/tenant";
package/dist/index.js CHANGED
@@ -7,13 +7,17 @@ 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 } from "./lib/constants";
11
11
  export { createRouter } from "./lib/context";
12
+ export { createRoute, withSecurity, registerSchema, registerSchemas } from "./lib/createRoute";
13
+ export { zodToMongoose } from "./lib/zodToMongoose";
14
+ export { createDtoMapper } from "./lib/createDtoMapper";
12
15
  export { signToken, verifyToken } from "./lib/jwt";
13
16
  export { log } from "./lib/logger";
14
17
  export { createResetToken, consumeResetToken, setPasswordResetStore } from "./lib/resetPassword";
15
- export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore } from "./lib/session";
18
+ export { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "./lib/session";
16
19
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
20
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore } from "./lib/mfaChallenge";
17
21
  export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
18
22
  export { validate } from "./lib/validate";
19
23
  // Middleware
@@ -30,7 +34,10 @@ export { buildFingerprint } from "./lib/fingerprint";
30
34
  // Models
31
35
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
32
36
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
33
- export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
37
+ export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
34
38
  // WebSocket
35
39
  export { websocket, createWsUpgradeHandler } from "./ws/index";
36
40
  export { publish, subscribe, unsubscribe, getSubscriptions, handleRoomActions, getRooms, getRoomSubscribers } from "./lib/ws";
41
+ // Tenancy
42
+ export { createTenant, deleteTenant, getTenant, listTenants } from "./lib/tenant";
43
+ export { invalidateTenantCache } from "./middleware/tenant";
@@ -35,3 +35,49 @@ export declare const setIncludeInactiveSessions: (v: boolean) => void;
35
35
  export declare const getIncludeInactiveSessions: () => boolean;
36
36
  export declare const setTrackLastActive: (v: boolean) => void;
37
37
  export declare const getTrackLastActive: () => boolean;
38
+ export interface RefreshTokenConfig {
39
+ /** Access token expiry in seconds. Default: 900 (15 min). */
40
+ accessTokenExpiry?: number;
41
+ /** Refresh token expiry in seconds. Default: 2_592_000 (30 days). */
42
+ refreshTokenExpiry?: number;
43
+ /** Grace window in seconds where the old refresh token still works after rotation.
44
+ * Prevents lockout when the client's network drops mid-refresh. Default: 30. */
45
+ rotationGraceSeconds?: number;
46
+ }
47
+ export declare const setRefreshTokenConfig: (config: RefreshTokenConfig | null) => void;
48
+ export declare const getRefreshTokenConfig: () => RefreshTokenConfig | null;
49
+ export declare const getAccessTokenExpiry: () => number;
50
+ export declare const getRefreshTokenExpiry: () => number;
51
+ export declare const getRotationGraceSeconds: () => number;
52
+ export interface MfaEmailOtpConfig {
53
+ /** Called with the user's email and the OTP code. Use to send the email. */
54
+ onSend: (email: string, code: string) => Promise<void>;
55
+ /** OTP code length. Default: 6. */
56
+ codeLength?: number;
57
+ }
58
+ export interface MfaConfig {
59
+ /** Issuer name shown in authenticator apps. Defaults to app name. */
60
+ issuer?: string;
61
+ /** TOTP algorithm. Default: "SHA1" (most compatible). */
62
+ algorithm?: "SHA1" | "SHA256" | "SHA512";
63
+ /** TOTP digits. Default: 6. */
64
+ digits?: number;
65
+ /** TOTP period in seconds. Default: 30. */
66
+ period?: number;
67
+ /** Number of recovery codes to generate. Default: 10. */
68
+ recoveryCodes?: number;
69
+ /** MFA challenge window in seconds. Default: 300 (5 min). */
70
+ challengeTtlSeconds?: number;
71
+ /** Email OTP configuration. When set, enables email-based MFA as an option. */
72
+ emailOtp?: MfaEmailOtpConfig;
73
+ }
74
+ export declare const setMfaConfig: (config: MfaConfig | null) => void;
75
+ export declare const getMfaConfig: () => MfaConfig | null;
76
+ export declare const getMfaIssuer: () => string;
77
+ export declare const getMfaAlgorithm: () => string;
78
+ export declare const getMfaDigits: () => number;
79
+ export declare const getMfaPeriod: () => number;
80
+ export declare const getMfaRecoveryCodeCount: () => number;
81
+ export declare const getMfaChallengeTtl: () => number;
82
+ export declare const getMfaEmailOtpConfig: () => MfaEmailOtpConfig | null;
83
+ export declare const getMfaEmailOtpCodeLength: () => number;
@@ -35,3 +35,23 @@ export const setIncludeInactiveSessions = (v) => { _includeInactiveSessions = v;
35
35
  export const getIncludeInactiveSessions = () => _includeInactiveSessions;
36
36
  export const setTrackLastActive = (v) => { _trackLastActive = v; };
37
37
  export const getTrackLastActive = () => _trackLastActive;
38
+ let _refreshTokenConfig = null;
39
+ export const setRefreshTokenConfig = (config) => { _refreshTokenConfig = config; };
40
+ export const getRefreshTokenConfig = () => _refreshTokenConfig;
41
+ const DEFAULT_ACCESS_TOKEN_EXPIRY = 900; // 15 min
42
+ const DEFAULT_REFRESH_TOKEN_EXPIRY = 2_592_000; // 30 days
43
+ const DEFAULT_ROTATION_GRACE_SECONDS = 30;
44
+ export const getAccessTokenExpiry = () => _refreshTokenConfig?.accessTokenExpiry ?? DEFAULT_ACCESS_TOKEN_EXPIRY;
45
+ export const getRefreshTokenExpiry = () => _refreshTokenConfig?.refreshTokenExpiry ?? DEFAULT_REFRESH_TOKEN_EXPIRY;
46
+ export const getRotationGraceSeconds = () => _refreshTokenConfig?.rotationGraceSeconds ?? DEFAULT_ROTATION_GRACE_SECONDS;
47
+ let _mfaConfig = null;
48
+ export const setMfaConfig = (config) => { _mfaConfig = config; };
49
+ export const getMfaConfig = () => _mfaConfig;
50
+ export const getMfaIssuer = () => _mfaConfig?.issuer ?? getAppName();
51
+ export const getMfaAlgorithm = () => _mfaConfig?.algorithm ?? "SHA1";
52
+ export const getMfaDigits = () => _mfaConfig?.digits ?? 6;
53
+ export const getMfaPeriod = () => _mfaConfig?.period ?? 30;
54
+ export const getMfaRecoveryCodeCount = () => _mfaConfig?.recoveryCodes ?? 10;
55
+ export const getMfaChallengeTtl = () => _mfaConfig?.challengeTtlSeconds ?? 300;
56
+ export const getMfaEmailOtpConfig = () => _mfaConfig?.emailOtp ?? null;
57
+ export const getMfaEmailOtpCodeLength = () => _mfaConfig?.emailOtp?.codeLength ?? 6;
@@ -48,6 +48,36 @@ export interface AuthAdapter {
48
48
  setEmailVerified?(userId: string, verified: boolean): Promise<void>;
49
49
  /** Optional. Return whether a user's email address has been verified. */
50
50
  getEmailVerified?(userId: string): Promise<boolean>;
51
+ /** Optional. Permanently delete a user account. Used by DELETE /auth/me. */
52
+ deleteUser?(userId: string): Promise<void>;
53
+ /** Optional. Check whether a user has a password set (credential account vs OAuth-only). */
54
+ hasPassword?(userId: string): Promise<boolean>;
55
+ /** Optional. Store the TOTP secret for MFA setup (encrypted or plaintext, adapter decides). */
56
+ setMfaSecret?(userId: string, secret: string | null): Promise<void>;
57
+ /** Optional. Retrieve the TOTP secret for MFA verification. */
58
+ getMfaSecret?(userId: string): Promise<string | null>;
59
+ /** Optional. Check whether MFA is enabled for a user. */
60
+ isMfaEnabled?(userId: string): Promise<boolean>;
61
+ /** Optional. Enable or disable MFA for a user. */
62
+ setMfaEnabled?(userId: string, enabled: boolean): Promise<void>;
63
+ /** Optional. Store hashed recovery codes for MFA. */
64
+ setRecoveryCodes?(userId: string, codes: string[]): Promise<void>;
65
+ /** Optional. Retrieve hashed recovery codes for MFA. */
66
+ getRecoveryCodes?(userId: string): Promise<string[]>;
67
+ /** Optional. Remove a single recovery code after use. */
68
+ removeRecoveryCode?(userId: string, code: string): Promise<void>;
69
+ /** Optional. Get the MFA methods enabled for a user (e.g., ["totp"], ["emailOtp"], ["totp", "emailOtp"]). */
70
+ getMfaMethods?(userId: string): Promise<string[]>;
71
+ /** Optional. Set the MFA methods enabled for a user. */
72
+ setMfaMethods?(userId: string, methods: string[]): Promise<void>;
73
+ /** Optional. Get roles for a user within a specific tenant. */
74
+ getTenantRoles?(userId: string, tenantId: string): Promise<string[]>;
75
+ /** Optional. Set roles for a user within a specific tenant (replaces existing). */
76
+ setTenantRoles?(userId: string, tenantId: string, roles: string[]): Promise<void>;
77
+ /** Optional. Add a single role to a user within a specific tenant. */
78
+ addTenantRole?(userId: string, tenantId: string, role: string): Promise<void>;
79
+ /** Optional. Remove a single role from a user within a specific tenant. */
80
+ removeTenantRole?(userId: string, tenantId: string, role: string): Promise<void>;
51
81
  }
52
82
  export declare const setAuthAdapter: (adapter: AuthAdapter) => void;
53
83
  export declare const getAuthAdapter: () => AuthAdapter;
@@ -1,2 +1,4 @@
1
1
  export declare const COOKIE_TOKEN = "token";
2
2
  export declare const HEADER_USER_TOKEN = "x-user-token";
3
+ export declare const COOKIE_REFRESH_TOKEN = "refresh_token";
4
+ export declare const HEADER_REFRESH_TOKEN = "x-refresh-token";
@@ -1,2 +1,4 @@
1
1
  export const COOKIE_TOKEN = "token";
2
2
  export const HEADER_USER_TOKEN = "x-user-token";
3
+ export const COOKIE_REFRESH_TOKEN = "refresh_token";
4
+ export const HEADER_REFRESH_TOKEN = "x-refresh-token";
@@ -3,6 +3,8 @@ export type AppVariables = {
3
3
  authUserId: string | null;
4
4
  roles: string[] | null;
5
5
  sessionId: string | null;
6
+ tenantId: string | null;
7
+ tenantConfig: Record<string, unknown> | null;
6
8
  };
7
9
  export type AppEnv = {
8
10
  Variables: AppVariables;
@@ -0,0 +1,33 @@
1
+ type ZodSchema = any;
2
+ export type DtoMapperConfig = {
3
+ /** DB field name → API field name for ObjectId refs (e.g., { account: "accountId" }) */
4
+ refs?: Record<string, string>;
5
+ /** API field names that are Date in DB, string in DTO */
6
+ dates?: string[];
7
+ /** Subdocument array fields mapped with a sub-mapper: { items: itemMapper } */
8
+ subdocs?: Record<string, (item: any) => any>;
9
+ };
10
+ /**
11
+ * Create a toDto mapper function from a Zod schema.
12
+ *
13
+ * The Zod schema defines which fields exist in the DTO. The config declares
14
+ * how to transform DB-specific types (ObjectId refs, Dates, subdocuments).
15
+ *
16
+ * Handles automatically:
17
+ * - `_id` → `id` (toString)
18
+ * - ObjectId refs → string (toString), with field renaming via `refs`
19
+ * - Date fields → ISO string via `dates`
20
+ * - Subdocument arrays via `subdocs`
21
+ * - Nullable/optional fields → `null` coercion (from `undefined`)
22
+ * - All other fields → passthrough
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * const toDto = createDtoMapper<LedgerItemDto>(LedgerItemSchema, {
27
+ * refs: { account: "accountId" },
28
+ * dates: ["date"],
29
+ * });
30
+ * ```
31
+ */
32
+ export declare function createDtoMapper<TDto>(zodSchema: ZodSchema, config?: DtoMapperConfig): (doc: any) => TDto;
33
+ export {};
@@ -0,0 +1,69 @@
1
+ /** Check if a Zod type is nullable or optional */
2
+ function isNullable(zodType) {
3
+ const defType = zodType?._zod?.def?.type;
4
+ if (defType === "nullable")
5
+ return true;
6
+ if (defType === "optional")
7
+ return true;
8
+ if (defType === "default")
9
+ return isNullable(zodType._zod.def.innerType);
10
+ return false;
11
+ }
12
+ /**
13
+ * Create a toDto mapper function from a Zod schema.
14
+ *
15
+ * The Zod schema defines which fields exist in the DTO. The config declares
16
+ * how to transform DB-specific types (ObjectId refs, Dates, subdocuments).
17
+ *
18
+ * Handles automatically:
19
+ * - `_id` → `id` (toString)
20
+ * - ObjectId refs → string (toString), with field renaming via `refs`
21
+ * - Date fields → ISO string via `dates`
22
+ * - Subdocument arrays via `subdocs`
23
+ * - Nullable/optional fields → `null` coercion (from `undefined`)
24
+ * - All other fields → passthrough
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * const toDto = createDtoMapper<LedgerItemDto>(LedgerItemSchema, {
29
+ * refs: { account: "accountId" },
30
+ * dates: ["date"],
31
+ * });
32
+ * ```
33
+ */
34
+ export function createDtoMapper(zodSchema, config = {}) {
35
+ const apiFields = Object.keys(zodSchema.shape);
36
+ const shape = zodSchema.shape;
37
+ // Build reverse lookup: apiField → dbField for refs
38
+ const refByApiField = new Map();
39
+ if (config.refs) {
40
+ for (const [dbField, apiField] of Object.entries(config.refs)) {
41
+ refByApiField.set(apiField, dbField);
42
+ }
43
+ }
44
+ const dateSet = new Set(config.dates ?? []);
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ return (doc) => {
47
+ const dto = {};
48
+ for (const field of apiFields) {
49
+ if (field === "id") {
50
+ dto.id = doc._id.toString();
51
+ continue;
52
+ }
53
+ if (refByApiField.has(field)) {
54
+ dto[field] = doc[refByApiField.get(field)].toString();
55
+ continue;
56
+ }
57
+ if (dateSet.has(field)) {
58
+ dto[field] = doc[field].toISOString();
59
+ continue;
60
+ }
61
+ if (config.subdocs?.[field]) {
62
+ dto[field] = (doc[field] ?? []).map(config.subdocs[field]);
63
+ continue;
64
+ }
65
+ dto[field] = isNullable(shape[field]) ? (doc[field] ?? null) : doc[field];
66
+ }
67
+ return dto;
68
+ };
69
+ }
@@ -0,0 +1,61 @@
1
+ import type { RouteConfig } from "@hono/zod-openapi";
2
+ import type { ZodType } from "zod";
3
+ /**
4
+ * Registers a Zod schema as a named entry in `components/schemas`.
5
+ *
6
+ * Use this for shared schemas (e.g. shared error types, reusable response shapes)
7
+ * that aren't directly attached to a specific route. Schemas already registered
8
+ * under the same name are silently skipped.
9
+ *
10
+ * @example
11
+ * export const MySchema = registerSchema("MySchema", z.object({ id: z.string() }));
12
+ */
13
+ export declare const registerSchema: <T extends ZodType>(name: string, schema: T) => T;
14
+ /**
15
+ * Registers multiple Zod schemas at once as named entries in `components/schemas`.
16
+ * Object keys become the schema names. Returns the same object so you can
17
+ * destructure or re-export the schemas normally.
18
+ *
19
+ * Schemas already registered (e.g. via a prior `registerSchema` call) are skipped.
20
+ *
21
+ * @example
22
+ * export const { LedgerItem, Product } = registerSchemas({
23
+ * LedgerItem: z.object({ id: z.string(), amount: z.number() }),
24
+ * Product: z.object({ id: z.string(), price: z.number() }),
25
+ * });
26
+ */
27
+ export declare const registerSchemas: <T extends Record<string, ZodType>>(schemas: T) => T;
28
+ /**
29
+ * Auto-registers a module export as a named OpenAPI schema.
30
+ * Used internally by modelSchemas auto-discovery in createApp.
31
+ * Strips a trailing "Schema" suffix from the export name.
32
+ * Skips non-Zod values and already-registered schemas.
33
+ */
34
+ export declare function maybeAutoRegister(exportName: string, value: unknown): void;
35
+ /**
36
+ * Adds an OpenAPI `security` requirement to a route without affecting TypeScript
37
+ * type inference on the handler. Pass each security scheme as a separate object.
38
+ *
39
+ * Use this instead of inlining `security` in `createRoute(...)` — inlining a
40
+ * field typed as `{ [name: string]: string[] }` breaks `c.req.valid()` inference.
41
+ *
42
+ * @example
43
+ * router.openapi(
44
+ * withSecurity(createRoute({ method: "get", path: "/me", ... }), { cookieAuth: [] }, { userToken: [] }),
45
+ * async (c) => { ... }
46
+ * )
47
+ */
48
+ export declare const withSecurity: <T extends RouteConfig>(route: T, ...schemes: Array<Record<string, string[]>>) => T;
49
+ /**
50
+ * Drop-in replacement for `createRoute` from `@hono/zod-openapi`.
51
+ *
52
+ * Automatically registers unnamed request body and response schemas as named
53
+ * OpenAPI components so they appear in `components/schemas` instead of being
54
+ * inlined at every use site. Generated names follow the convention:
55
+ *
56
+ * {Method}{PathSegments}Body — request body
57
+ * {Method}{PathSegments}{StatusCode} — response body
58
+ *
59
+ * Schemas already named via `.openapi("Name")` are never overwritten.
60
+ */
61
+ export declare const createRoute: <T extends RouteConfig>(config: T) => T;