@lastshotlabs/bunshot 0.0.13 → 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 (98) hide show
  1. package/README.md +2510 -1747
  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 +77 -2
  8. package/dist/app.js +29 -4
  9. package/dist/entrypoints/queue.d.ts +2 -2
  10. package/dist/entrypoints/queue.js +1 -1
  11. package/dist/index.d.ts +14 -5
  12. package/dist/index.js +9 -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/jwt.d.ts +1 -1
  22. package/dist/lib/jwt.js +2 -2
  23. package/dist/lib/mfaChallenge.d.ts +20 -0
  24. package/dist/lib/mfaChallenge.js +184 -0
  25. package/dist/lib/queue.d.ts +33 -0
  26. package/dist/lib/queue.js +98 -0
  27. package/dist/lib/roles.d.ts +4 -0
  28. package/dist/lib/roles.js +27 -0
  29. package/dist/lib/session.d.ts +12 -0
  30. package/dist/lib/session.js +163 -5
  31. package/dist/lib/tenant.d.ts +15 -0
  32. package/dist/lib/tenant.js +65 -0
  33. package/dist/lib/zodToMongoose.d.ts +38 -0
  34. package/dist/lib/zodToMongoose.js +84 -0
  35. package/dist/middleware/cacheResponse.js +4 -1
  36. package/dist/middleware/rateLimit.d.ts +2 -1
  37. package/dist/middleware/rateLimit.js +5 -2
  38. package/dist/middleware/requireRole.d.ts +14 -3
  39. package/dist/middleware/requireRole.js +46 -6
  40. package/dist/middleware/tenant.d.ts +5 -0
  41. package/dist/middleware/tenant.js +116 -0
  42. package/dist/models/AuthUser.d.ts +8 -0
  43. package/dist/models/AuthUser.js +8 -0
  44. package/dist/models/TenantRole.d.ts +15 -0
  45. package/dist/models/TenantRole.js +23 -0
  46. package/dist/routes/auth.d.ts +5 -3
  47. package/dist/routes/auth.js +153 -22
  48. package/dist/routes/jobs.d.ts +2 -0
  49. package/dist/routes/jobs.js +270 -0
  50. package/dist/routes/mfa.d.ts +1 -0
  51. package/dist/routes/mfa.js +409 -0
  52. package/dist/routes/oauth.js +107 -16
  53. package/dist/server.js +9 -0
  54. package/dist/services/auth.d.ts +17 -5
  55. package/dist/services/auth.js +95 -17
  56. package/dist/services/mfa.d.ts +37 -0
  57. package/dist/services/mfa.js +276 -0
  58. package/docs/sections/adding-middleware/full.md +35 -0
  59. package/docs/sections/adding-models/full.md +125 -0
  60. package/docs/sections/adding-models/overview.md +13 -0
  61. package/docs/sections/adding-routes/full.md +182 -0
  62. package/docs/sections/adding-routes/overview.md +23 -0
  63. package/docs/sections/auth-flow/full.md +456 -0
  64. package/docs/sections/auth-flow/overview.md +10 -0
  65. package/docs/sections/cli/full.md +30 -0
  66. package/docs/sections/configuration/full.md +135 -0
  67. package/docs/sections/configuration/overview.md +17 -0
  68. package/docs/sections/configuration-example/full.md +99 -0
  69. package/docs/sections/configuration-example/overview.md +30 -0
  70. package/docs/sections/documentation/full.md +171 -0
  71. package/docs/sections/environment-variables/full.md +55 -0
  72. package/docs/sections/exports/full.md +83 -0
  73. package/docs/sections/extending-context/full.md +59 -0
  74. package/docs/sections/header.md +3 -0
  75. package/docs/sections/installation/full.md +6 -0
  76. package/docs/sections/jobs/full.md +140 -0
  77. package/docs/sections/jobs/overview.md +15 -0
  78. package/docs/sections/mongodb-connections/full.md +45 -0
  79. package/docs/sections/mongodb-connections/overview.md +7 -0
  80. package/docs/sections/multi-tenancy/full.md +62 -0
  81. package/docs/sections/multi-tenancy/overview.md +15 -0
  82. package/docs/sections/oauth/full.md +119 -0
  83. package/docs/sections/oauth/overview.md +16 -0
  84. package/docs/sections/package-development/full.md +7 -0
  85. package/docs/sections/peer-dependencies/full.md +43 -0
  86. package/docs/sections/quick-start/full.md +43 -0
  87. package/docs/sections/response-caching/full.md +115 -0
  88. package/docs/sections/response-caching/overview.md +13 -0
  89. package/docs/sections/roles/full.md +136 -0
  90. package/docs/sections/roles/overview.md +12 -0
  91. package/docs/sections/running-without-redis/full.md +16 -0
  92. package/docs/sections/running-without-redis-or-mongodb/full.md +60 -0
  93. package/docs/sections/stack/full.md +10 -0
  94. package/docs/sections/websocket/full.md +100 -0
  95. package/docs/sections/websocket/overview.md +5 -0
  96. package/docs/sections/websocket-rooms/full.md +97 -0
  97. package/docs/sections/websocket-rooms/overview.md +5 -0
  98. package/package.json +19 -10
@@ -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,21 +1,27 @@
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, 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
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";
12
16
  export type { AppEnv, AppVariables } from "./lib/context";
13
17
  export { signToken, verifyToken } from "./lib/jwt";
14
18
  export { log } from "./lib/logger";
15
19
  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";
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";
18
22
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
23
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore } from "./lib/mfaChallenge";
24
+ export type { MfaChallengeData } from "./lib/mfaChallenge";
19
25
  export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
20
26
  export type { LimitOpts } from "./lib/authRateLimit";
21
27
  export { validate } from "./lib/validate";
@@ -32,9 +38,12 @@ export { cacheResponse, bustCache, bustCachePattern, setCacheStore } from "./mid
32
38
  export { buildFingerprint } from "./lib/fingerprint";
33
39
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
34
40
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
35
- export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
41
+ export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
36
42
  export type { AuthAdapter, OAuthProfile } from "./lib/authAdapter";
37
43
  export type { OAuthProviderConfig } from "./lib/oauth";
38
44
  export { websocket, createWsUpgradeHandler } from "./ws/index";
39
45
  export type { SocketData } from "./ws/index";
40
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,14 +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
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 { createSession, getSession, deleteSession, getUserSessions, getActiveSessionCount, evictOldestSession, updateSessionLastActive, setSessionStore, deleteUserSessions, setRefreshToken, getSessionByRefreshToken, rotateRefreshToken } from "./lib/session";
17
19
  export { createVerificationToken, getVerificationToken, deleteVerificationToken } from "./lib/emailVerification";
20
+ export { createMfaChallenge, consumeMfaChallenge, replaceMfaChallengeOtp, setMfaChallengeStore } from "./lib/mfaChallenge";
18
21
  export { bustAuthLimit, trackAttempt, isLimited } from "./lib/authRateLimit";
19
22
  export { validate } from "./lib/validate";
20
23
  // Middleware
@@ -31,7 +34,10 @@ export { buildFingerprint } from "./lib/fingerprint";
31
34
  // Models
32
35
  export { sqliteAuthAdapter, setSqliteDb, startSqliteCleanup } from "./adapters/sqliteAuth";
33
36
  export { memoryAuthAdapter, clearMemoryStore } from "./adapters/memoryAuth";
34
- export { setUserRoles, addUserRole, removeUserRole } from "./lib/roles";
37
+ export { setUserRoles, addUserRole, removeUserRole, getTenantRoles, setTenantRoles, addTenantRole, removeTenantRole } from "./lib/roles";
35
38
  // WebSocket
36
39
  export { websocket, createWsUpgradeHandler } from "./ws/index";
37
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
+ }
package/dist/lib/jwt.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const signToken: (userId: string, sessionId: string) => Promise<string>;
1
+ export declare const signToken: (userId: string, sessionId: string, expirySeconds?: number) => Promise<string>;
2
2
  export declare const verifyToken: (token: string) => Promise<import("jose").JWTPayload>;
package/dist/lib/jwt.js CHANGED
@@ -1,9 +1,9 @@
1
1
  import { SignJWT, jwtVerify } from "jose";
2
2
  const isProd = process.env.NODE_ENV === "production";
3
3
  const secret = new TextEncoder().encode(isProd ? process.env.JWT_SECRET_PROD : process.env.JWT_SECRET_DEV);
4
- export const signToken = async (userId, sessionId) => new SignJWT({ sub: userId, sid: sessionId })
4
+ export const signToken = async (userId, sessionId, expirySeconds) => new SignJWT({ sub: userId, sid: sessionId })
5
5
  .setProtectedHeader({ alg: "HS256" })
6
- .setExpirationTime("7d")
6
+ .setExpirationTime(expirySeconds ? `${expirySeconds}s` : "7d")
7
7
  .sign(secret);
8
8
  export const verifyToken = async (token) => {
9
9
  const { payload } = await jwtVerify(token, secret);
@@ -0,0 +1,20 @@
1
+ export interface MfaChallengeData {
2
+ userId: string;
3
+ emailOtpHash?: string;
4
+ }
5
+ /** Must be called when store is "sqlite" to inject the db instance. */
6
+ export declare const setMfaChallengeSqliteDb: (db: any) => void;
7
+ type MfaChallengeStore = "redis" | "mongo" | "sqlite" | "memory";
8
+ export declare const setMfaChallengeStore: (store: MfaChallengeStore) => void;
9
+ export declare const createMfaChallenge: (userId: string, emailOtpHash?: string) => Promise<string>;
10
+ export declare const consumeMfaChallenge: (token: string) => Promise<MfaChallengeData | null>;
11
+ /**
12
+ * Replace the email OTP hash on an existing challenge without consuming it.
13
+ * Used for the resend flow. Increments resendCount and caps the challenge lifetime.
14
+ * Returns { userId, resendCount } on success, null if challenge not found/expired/max resends reached.
15
+ */
16
+ export declare const replaceMfaChallengeOtp: (token: string, newEmailOtpHash: string) => Promise<{
17
+ userId: string;
18
+ resendCount: number;
19
+ } | null>;
20
+ export {};
@@ -0,0 +1,184 @@
1
+ import { getRedis } from "./redis";
2
+ import { appConnection, mongoose } from "./mongo";
3
+ import { getAppName, getMfaChallengeTtl } from "./appConfig";
4
+ const MAX_RESENDS = 3;
5
+ function getMfaChallengeModel() {
6
+ if (appConnection.models["MfaChallenge"])
7
+ return appConnection.models["MfaChallenge"];
8
+ const { Schema } = mongoose;
9
+ const schema = new Schema({
10
+ token: { type: String, required: true, unique: true },
11
+ userId: { type: String, required: true },
12
+ emailOtpHash: { type: String },
13
+ createdAt: { type: Date, required: true },
14
+ resendCount: { type: Number, required: true, default: 0 },
15
+ expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
16
+ }, { collection: "mfa_challenges" });
17
+ return appConnection.model("MfaChallenge", schema);
18
+ }
19
+ // ---------------------------------------------------------------------------
20
+ // In-memory store
21
+ // ---------------------------------------------------------------------------
22
+ const _memoryChallenges = new Map();
23
+ // ---------------------------------------------------------------------------
24
+ // SQLite store (reuses the existing SQLite DB instance)
25
+ // ---------------------------------------------------------------------------
26
+ let _sqliteDb = null;
27
+ let _sqliteTableCreated = false;
28
+ /** Must be called when store is "sqlite" to inject the db instance. */
29
+ export const setMfaChallengeSqliteDb = (db) => { _sqliteDb = db; };
30
+ function ensureSqliteMfaTable() {
31
+ if (_sqliteTableCreated || !_sqliteDb)
32
+ return;
33
+ _sqliteDb.run(`CREATE TABLE IF NOT EXISTS mfa_challenges (
34
+ token TEXT PRIMARY KEY,
35
+ userId TEXT NOT NULL,
36
+ emailOtpHash TEXT,
37
+ createdAt INTEGER NOT NULL,
38
+ resendCount INTEGER NOT NULL DEFAULT 0,
39
+ expiresAt INTEGER NOT NULL
40
+ )`);
41
+ // Migrate pre-existing tables that lack the new columns
42
+ try {
43
+ _sqliteDb.run("ALTER TABLE mfa_challenges ADD COLUMN emailOtpHash TEXT");
44
+ }
45
+ catch { /* already exists */ }
46
+ try {
47
+ _sqliteDb.run("ALTER TABLE mfa_challenges ADD COLUMN createdAt INTEGER NOT NULL DEFAULT 0");
48
+ }
49
+ catch { /* already exists */ }
50
+ try {
51
+ _sqliteDb.run("ALTER TABLE mfa_challenges ADD COLUMN resendCount INTEGER NOT NULL DEFAULT 0");
52
+ }
53
+ catch { /* already exists */ }
54
+ _sqliteTableCreated = true;
55
+ }
56
+ let _store = "redis";
57
+ export const setMfaChallengeStore = (store) => { _store = store; };
58
+ // ---------------------------------------------------------------------------
59
+ // Public API
60
+ // ---------------------------------------------------------------------------
61
+ export const createMfaChallenge = async (userId, emailOtpHash) => {
62
+ const token = crypto.randomUUID();
63
+ const ttl = getMfaChallengeTtl();
64
+ const now = Date.now();
65
+ if (_store === "memory") {
66
+ _memoryChallenges.set(token, { userId, emailOtpHash, createdAt: now, resendCount: 0, expiresAt: now + ttl * 1000 });
67
+ return token;
68
+ }
69
+ if (_store === "sqlite") {
70
+ ensureSqliteMfaTable();
71
+ _sqliteDb.run("INSERT INTO mfa_challenges (token, userId, emailOtpHash, createdAt, resendCount, expiresAt) VALUES (?, ?, ?, ?, 0, ?)", [token, userId, emailOtpHash ?? null, now, now + ttl * 1000]);
72
+ return token;
73
+ }
74
+ if (_store === "mongo") {
75
+ await getMfaChallengeModel().create({
76
+ token,
77
+ userId,
78
+ emailOtpHash,
79
+ createdAt: new Date(now),
80
+ resendCount: 0,
81
+ expiresAt: new Date(now + ttl * 1000),
82
+ });
83
+ return token;
84
+ }
85
+ // redis
86
+ await getRedis().set(`mfachallenge:${getAppName()}:${token}`, JSON.stringify({ userId, emailOtpHash, createdAt: now, resendCount: 0 }), "EX", ttl);
87
+ return token;
88
+ };
89
+ export const consumeMfaChallenge = async (token) => {
90
+ if (_store === "memory") {
91
+ const entry = _memoryChallenges.get(token);
92
+ if (!entry || entry.expiresAt <= Date.now()) {
93
+ _memoryChallenges.delete(token);
94
+ return null;
95
+ }
96
+ _memoryChallenges.delete(token);
97
+ return { userId: entry.userId, emailOtpHash: entry.emailOtpHash };
98
+ }
99
+ if (_store === "sqlite") {
100
+ ensureSqliteMfaTable();
101
+ const row = _sqliteDb.query("DELETE FROM mfa_challenges WHERE token = ? AND expiresAt > ? RETURNING userId, emailOtpHash").get(token, Date.now());
102
+ return row ? { userId: row.userId, emailOtpHash: row.emailOtpHash ?? undefined } : null;
103
+ }
104
+ if (_store === "mongo") {
105
+ const doc = await getMfaChallengeModel().findOneAndDelete({ token, expiresAt: { $gt: new Date() } });
106
+ return doc ? { userId: doc.userId, emailOtpHash: doc.emailOtpHash } : null;
107
+ }
108
+ // redis
109
+ const key = `mfachallenge:${getAppName()}:${token}`;
110
+ const raw = await getRedis().get(key);
111
+ if (!raw)
112
+ return null;
113
+ await getRedis().del(key);
114
+ const data = JSON.parse(raw);
115
+ return { userId: data.userId, emailOtpHash: data.emailOtpHash };
116
+ };
117
+ /**
118
+ * Replace the email OTP hash on an existing challenge without consuming it.
119
+ * Used for the resend flow. Increments resendCount and caps the challenge lifetime.
120
+ * Returns { userId, resendCount } on success, null if challenge not found/expired/max resends reached.
121
+ */
122
+ export const replaceMfaChallengeOtp = async (token, newEmailOtpHash) => {
123
+ const ttl = getMfaChallengeTtl();
124
+ if (_store === "memory") {
125
+ const entry = _memoryChallenges.get(token);
126
+ if (!entry || entry.expiresAt <= Date.now()) {
127
+ _memoryChallenges.delete(token);
128
+ return null;
129
+ }
130
+ if (entry.resendCount >= MAX_RESENDS)
131
+ return null;
132
+ entry.emailOtpHash = newEmailOtpHash;
133
+ entry.resendCount++;
134
+ // Cap lifetime: min(now + ttl, createdAt + ttl * 3)
135
+ const maxExpiry = entry.createdAt + ttl * 3 * 1000;
136
+ entry.expiresAt = Math.min(Date.now() + ttl * 1000, maxExpiry);
137
+ return { userId: entry.userId, resendCount: entry.resendCount };
138
+ }
139
+ if (_store === "sqlite") {
140
+ ensureSqliteMfaTable();
141
+ const now = Date.now();
142
+ const existing = _sqliteDb.query("SELECT createdAt, resendCount FROM mfa_challenges WHERE token = ? AND expiresAt > ?").get(token, now);
143
+ if (!existing || existing.resendCount >= MAX_RESENDS)
144
+ return null;
145
+ const newExpiry = Math.min(now + ttl * 1000, existing.createdAt + ttl * 3 * 1000);
146
+ const newCount = existing.resendCount + 1;
147
+ const row = _sqliteDb.query("UPDATE mfa_challenges SET emailOtpHash = ?, resendCount = ?, expiresAt = ? WHERE token = ? RETURNING userId").get(newEmailOtpHash, newCount, newExpiry, token);
148
+ return row ? { userId: row.userId, resendCount: newCount } : null;
149
+ }
150
+ if (_store === "mongo") {
151
+ const now = new Date();
152
+ const doc = await getMfaChallengeModel().findOneAndUpdate({ token, expiresAt: { $gt: now }, resendCount: { $lt: MAX_RESENDS } }, [
153
+ {
154
+ $set: {
155
+ emailOtpHash: newEmailOtpHash,
156
+ resendCount: { $add: ["$resendCount", 1] },
157
+ expiresAt: {
158
+ $min: [
159
+ new Date(Date.now() + ttl * 1000),
160
+ { $add: ["$createdAt", ttl * 3 * 1000] },
161
+ ],
162
+ },
163
+ },
164
+ },
165
+ ], { new: true });
166
+ return doc ? { userId: doc.userId, resendCount: doc.resendCount } : null;
167
+ }
168
+ // redis
169
+ const key = `mfachallenge:${getAppName()}:${token}`;
170
+ const raw = await getRedis().get(key);
171
+ if (!raw)
172
+ return null;
173
+ const data = JSON.parse(raw);
174
+ if (data.resendCount >= MAX_RESENDS)
175
+ return null;
176
+ data.emailOtpHash = newEmailOtpHash;
177
+ data.resendCount++;
178
+ // Cap lifetime
179
+ const maxExpiry = data.createdAt + ttl * 3 * 1000;
180
+ const newExpiry = Math.min(Date.now() + ttl * 1000, maxExpiry);
181
+ const remainingTtl = Math.max(1, Math.ceil((newExpiry - Date.now()) / 1000));
182
+ await getRedis().set(key, JSON.stringify(data), "EX", remainingTtl);
183
+ return { userId: data.userId, resendCount: data.resendCount };
184
+ };
@@ -1,4 +1,37 @@
1
1
  import type { Queue as QueueType, Worker as WorkerType, Processor, QueueOptions, WorkerOptions, Job } from "bullmq";
2
2
  export declare const createQueue: <T = unknown, R = unknown>(name: string, options?: Omit<QueueOptions, "connection">) => QueueType<T, R>;
3
3
  export declare const createWorker: <T = unknown, R = unknown>(name: string, processor: Processor<T, R>, options?: Omit<WorkerOptions, "connection">) => WorkerType<T, R>;
4
+ export declare const getRegisteredCronNames: () => ReadonlySet<string>;
5
+ export interface CronSchedule {
6
+ /** Cron expression. Mutually exclusive with `every`. */
7
+ cron?: string;
8
+ /** Interval in milliseconds. Mutually exclusive with `cron`. */
9
+ every?: number;
10
+ /** Timezone for cron expressions. */
11
+ timezone?: string;
12
+ }
13
+ export declare const createCronWorker: <T = void, R = unknown>(name: string, processor: Processor<T, R>, schedule: CronSchedule, options?: Omit<WorkerOptions, "connection">) => {
14
+ worker: WorkerType<T, R>;
15
+ queue: QueueType<T, R>;
16
+ };
17
+ /**
18
+ * Remove job schedulers that are no longer registered.
19
+ * Called automatically after worker discovery in createServer.
20
+ * Can also be called manually for workers managed outside workersDir.
21
+ */
22
+ export declare const cleanupStaleSchedulers: (activeNames: string[]) => Promise<void>;
23
+ export interface DLQOptions<T = unknown> {
24
+ /** Max jobs to keep in the DLQ. Default: 1000. */
25
+ maxSize?: number;
26
+ /** Called when a job is moved to the DLQ. */
27
+ onDeadLetter?: (job: Job<T>, error: Error) => Promise<void>;
28
+ /** Auto-retry delay in ms. No auto-retry by default. */
29
+ retryAfter?: number;
30
+ /** Preserve original job options on retry. Default: true. */
31
+ preserveJobOptions?: boolean;
32
+ }
33
+ export declare const createDLQHandler: <T = unknown>(sourceWorker: WorkerType<T>, sourceQueueName: string, options?: DLQOptions<T>) => {
34
+ dlqQueue: QueueType<T>;
35
+ retryJob: (jobId: string) => Promise<void>;
36
+ };
4
37
  export type { Job };