@lastshotlabs/bunshot 0.0.21 → 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 (185) hide show
  1. package/README.md +3035 -1249
  2. package/dist/adapters/localStorage.d.ts +6 -0
  3. package/dist/adapters/localStorage.js +59 -0
  4. package/dist/adapters/memoryAuth.d.ts +13 -0
  5. package/dist/adapters/memoryAuth.js +261 -2
  6. package/dist/adapters/memoryStorage.d.ts +3 -0
  7. package/dist/adapters/memoryStorage.js +44 -0
  8. package/dist/adapters/mongoAuth.js +217 -1
  9. package/dist/adapters/s3Storage.d.ts +14 -0
  10. package/dist/adapters/s3Storage.js +126 -0
  11. package/dist/adapters/sqliteAuth.d.ts +30 -0
  12. package/dist/adapters/sqliteAuth.js +352 -2
  13. package/dist/app.d.ts +203 -3
  14. package/dist/app.js +352 -48
  15. package/dist/cli.js +118 -38
  16. package/dist/index.d.ts +69 -8
  17. package/dist/index.js +46 -5
  18. package/dist/lib/HttpError.d.ts +7 -1
  19. package/dist/lib/HttpError.js +10 -1
  20. package/dist/lib/appConfig.d.ts +157 -0
  21. package/dist/lib/appConfig.js +54 -0
  22. package/dist/lib/auditLog.d.ts +58 -0
  23. package/dist/lib/auditLog.js +218 -0
  24. package/dist/lib/authAdapter.d.ts +140 -1
  25. package/dist/lib/authRateLimit.js +36 -0
  26. package/dist/lib/breachedPassword.d.ts +13 -0
  27. package/dist/lib/breachedPassword.js +48 -0
  28. package/dist/lib/captcha.d.ts +25 -0
  29. package/dist/lib/captcha.js +37 -0
  30. package/dist/lib/constants.d.ts +4 -0
  31. package/dist/lib/constants.js +4 -0
  32. package/dist/lib/context.d.ts +24 -1
  33. package/dist/lib/context.js +17 -3
  34. package/dist/lib/createRoute.d.ts +28 -2
  35. package/dist/lib/createRoute.js +54 -3
  36. package/dist/lib/credentialStuffing.d.ts +31 -0
  37. package/dist/lib/credentialStuffing.js +77 -0
  38. package/dist/lib/deletionCancelToken.d.ts +12 -0
  39. package/dist/lib/deletionCancelToken.js +88 -0
  40. package/dist/lib/emailVerification.d.ts +6 -0
  41. package/dist/lib/emailVerification.js +46 -3
  42. package/dist/lib/groups.d.ts +113 -0
  43. package/dist/lib/groups.js +133 -0
  44. package/dist/lib/idempotency.d.ts +22 -0
  45. package/dist/lib/idempotency.js +182 -0
  46. package/dist/lib/jwks.d.ts +25 -0
  47. package/dist/lib/jwks.js +51 -0
  48. package/dist/lib/jwt.d.ts +15 -2
  49. package/dist/lib/jwt.js +92 -5
  50. package/dist/lib/logger.d.ts +2 -0
  51. package/dist/lib/logger.js +6 -0
  52. package/dist/lib/m2m.d.ts +29 -0
  53. package/dist/lib/m2m.js +48 -0
  54. package/dist/lib/metrics.d.ts +14 -0
  55. package/dist/lib/metrics.js +158 -0
  56. package/dist/lib/mfaChallenge.d.ts +14 -1
  57. package/dist/lib/mfaChallenge.js +111 -6
  58. package/dist/lib/mongo.js +1 -1
  59. package/dist/lib/oauthCode.js +23 -18
  60. package/dist/lib/pagination.d.ts +119 -0
  61. package/dist/lib/pagination.js +166 -0
  62. package/dist/lib/resetPassword.js +3 -1
  63. package/dist/lib/saml.d.ts +25 -0
  64. package/dist/lib/saml.js +64 -0
  65. package/dist/lib/scim.d.ts +44 -0
  66. package/dist/lib/scim.js +54 -0
  67. package/dist/lib/securityEvents.d.ts +28 -0
  68. package/dist/lib/securityEvents.js +26 -0
  69. package/dist/lib/session.d.ts +14 -0
  70. package/dist/lib/session.js +121 -5
  71. package/dist/lib/signing.d.ts +52 -0
  72. package/dist/lib/signing.js +183 -0
  73. package/dist/lib/storageAdapter.d.ts +30 -0
  74. package/dist/lib/storageAdapter.js +1 -0
  75. package/dist/lib/stripUnreferencedSchemas.d.ts +11 -0
  76. package/dist/lib/stripUnreferencedSchemas.js +79 -0
  77. package/dist/lib/suspension.d.ts +13 -0
  78. package/dist/lib/suspension.js +23 -0
  79. package/dist/lib/tenant.js +2 -2
  80. package/dist/lib/upload.d.ts +39 -0
  81. package/dist/lib/upload.js +112 -0
  82. package/dist/lib/uploadRegistry.d.ts +18 -0
  83. package/dist/lib/uploadRegistry.js +83 -0
  84. package/dist/lib/validate.js +2 -2
  85. package/dist/lib/ws.d.ts +1 -0
  86. package/dist/lib/ws.js +28 -0
  87. package/dist/lib/wsHeartbeat.d.ts +12 -0
  88. package/dist/lib/wsHeartbeat.js +57 -0
  89. package/dist/lib/wsMessages.d.ts +40 -0
  90. package/dist/lib/wsMessages.js +330 -0
  91. package/dist/lib/wsPresence.d.ts +25 -0
  92. package/dist/lib/wsPresence.js +99 -0
  93. package/dist/middleware/auditLog.d.ts +22 -0
  94. package/dist/middleware/auditLog.js +39 -0
  95. package/dist/middleware/bearerAuth.js +1 -1
  96. package/dist/middleware/cacheResponse.js +5 -1
  97. package/dist/middleware/captcha.d.ts +10 -0
  98. package/dist/middleware/captcha.js +36 -0
  99. package/dist/middleware/csrf.js +18 -4
  100. package/dist/middleware/errorHandler.js +4 -1
  101. package/dist/middleware/identify.js +89 -14
  102. package/dist/middleware/metrics.d.ts +9 -0
  103. package/dist/middleware/metrics.js +26 -0
  104. package/dist/middleware/requestId.d.ts +3 -0
  105. package/dist/middleware/requestId.js +7 -0
  106. package/dist/middleware/requestLogger.d.ts +38 -0
  107. package/dist/middleware/requestLogger.js +68 -0
  108. package/dist/middleware/requestSigning.d.ts +20 -0
  109. package/dist/middleware/requestSigning.js +100 -0
  110. package/dist/middleware/requireMfaSetup.d.ts +16 -0
  111. package/dist/middleware/requireMfaSetup.js +37 -0
  112. package/dist/middleware/requireRole.d.ts +9 -3
  113. package/dist/middleware/requireRole.js +23 -36
  114. package/dist/middleware/requireScope.d.ts +10 -0
  115. package/dist/middleware/requireScope.js +25 -0
  116. package/dist/middleware/requireStepUp.d.ts +18 -0
  117. package/dist/middleware/requireStepUp.js +29 -0
  118. package/dist/middleware/scimAuth.d.ts +8 -0
  119. package/dist/middleware/scimAuth.js +29 -0
  120. package/dist/middleware/upload.d.ts +5 -0
  121. package/dist/middleware/upload.js +27 -0
  122. package/dist/middleware/webhookAuth.d.ts +30 -0
  123. package/dist/middleware/webhookAuth.js +58 -0
  124. package/dist/models/AuditLog.d.ts +30 -0
  125. package/dist/models/AuditLog.js +39 -0
  126. package/dist/models/AuthUser.d.ts +7 -0
  127. package/dist/models/AuthUser.js +7 -0
  128. package/dist/models/Group.d.ts +21 -0
  129. package/dist/models/Group.js +28 -0
  130. package/dist/models/GroupMembership.d.ts +21 -0
  131. package/dist/models/GroupMembership.js +25 -0
  132. package/dist/models/M2MClient.d.ts +18 -0
  133. package/dist/models/M2MClient.js +18 -0
  134. package/dist/routes/auth.d.ts +3 -2
  135. package/dist/routes/auth.js +238 -21
  136. package/dist/routes/groups.d.ts +21 -0
  137. package/dist/routes/groups.js +346 -0
  138. package/dist/routes/jobs.js +66 -46
  139. package/dist/routes/m2m.d.ts +2 -0
  140. package/dist/routes/m2m.js +72 -0
  141. package/dist/routes/metrics.d.ts +8 -0
  142. package/dist/routes/metrics.js +55 -0
  143. package/dist/routes/mfa.js +13 -1
  144. package/dist/routes/oauth.js +6 -0
  145. package/dist/routes/oidc.d.ts +2 -0
  146. package/dist/routes/oidc.js +29 -0
  147. package/dist/routes/passkey.d.ts +1 -0
  148. package/dist/routes/passkey.js +157 -0
  149. package/dist/routes/saml.d.ts +2 -0
  150. package/dist/routes/saml.js +86 -0
  151. package/dist/routes/scim.d.ts +2 -0
  152. package/dist/routes/scim.js +255 -0
  153. package/dist/routes/uploads.d.ts +14 -0
  154. package/dist/routes/uploads.js +227 -0
  155. package/dist/server.d.ts +26 -0
  156. package/dist/server.js +46 -3
  157. package/dist/services/auth.d.ts +2 -0
  158. package/dist/services/auth.js +101 -22
  159. package/dist/services/mfa.js +2 -2
  160. package/dist/ws/index.js +5 -1
  161. package/docs/sections/auth-flow/full.md +203 -47
  162. package/docs/sections/auth-flow/overview.md +2 -2
  163. package/docs/sections/auth-security-examples/full.md +388 -0
  164. package/docs/sections/authentication/full.md +130 -0
  165. package/docs/sections/authentication/overview.md +5 -0
  166. package/docs/sections/cli/full.md +13 -1
  167. package/docs/sections/configuration/full.md +17 -0
  168. package/docs/sections/configuration/overview.md +1 -0
  169. package/docs/sections/exports/full.md +34 -3
  170. package/docs/sections/logging/full.md +83 -0
  171. package/docs/sections/metrics/full.md +131 -0
  172. package/docs/sections/oauth/full.md +189 -189
  173. package/docs/sections/oauth/overview.md +1 -1
  174. package/docs/sections/pagination/full.md +93 -0
  175. package/docs/sections/passkey-login/full.md +90 -0
  176. package/docs/sections/passkey-login/overview.md +1 -0
  177. package/docs/sections/roles/full.md +224 -135
  178. package/docs/sections/roles/overview.md +3 -1
  179. package/docs/sections/signing/full.md +203 -0
  180. package/docs/sections/uploads/full.md +208 -0
  181. package/docs/sections/versioning/full.md +85 -0
  182. package/docs/sections/webhook-auth/full.md +100 -0
  183. package/docs/sections/websocket/full.md +95 -0
  184. package/docs/sections/websocket-rooms/full.md +6 -1
  185. package/package.json +18 -5
@@ -84,6 +84,10 @@ export interface MfaWebAuthnConfig {
84
84
  timeout?: number;
85
85
  /** Reject authentication when sign count goes backward (cloned key detection). Default: false (accept + warn). */
86
86
  strictSignCount?: boolean;
87
+ /** Allow passwordless (first-factor) passkey login. When true, mounts POST /auth/passkey/login-options and POST /auth/passkey/login. Default: false. */
88
+ allowPasswordlessLogin?: boolean;
89
+ /** When true (default), a verified passkey login satisfies MFA — no subsequent TOTP/OTP prompt even if the user has MFA enabled. Set false to require MFA after passkey login. */
90
+ passkeyMfaBypass?: boolean;
87
91
  }
88
92
  export interface MfaConfig {
89
93
  /** Issuer name shown in authenticator apps. Defaults to app name. */
@@ -102,6 +106,8 @@ export interface MfaConfig {
102
106
  emailOtp?: MfaEmailOtpConfig;
103
107
  /** WebAuthn/FIDO2 configuration. When set, enables security key MFA routes. */
104
108
  webauthn?: MfaWebAuthnConfig;
109
+ /** When true, authenticated users must complete MFA setup before accessing non-auth endpoints. Default: false. */
110
+ required?: boolean;
105
111
  }
106
112
  export declare const setMfaConfig: (config: MfaConfig | null) => void;
107
113
  export declare const getMfaConfig: () => MfaConfig | null;
@@ -114,5 +120,156 @@ export declare const getMfaChallengeTtl: () => number;
114
120
  export declare const getMfaEmailOtpConfig: () => MfaEmailOtpConfig | null;
115
121
  export declare const getMfaEmailOtpCodeLength: () => number;
116
122
  export declare const getMfaWebAuthnConfig: () => MfaWebAuthnConfig | null;
123
+ export declare const getMfaRequired: () => boolean;
124
+ export declare const getMfaWebAuthnAllowPasswordlessLogin: () => boolean;
125
+ export declare const getMfaWebAuthnPasskeyMfaBypass: () => boolean;
117
126
  export declare const setCsrfEnabled: (v: boolean) => void;
118
127
  export declare const getCsrfEnabled: () => boolean;
128
+ export interface SigningConfig {
129
+ /**
130
+ * HMAC secret. Defaults to JWT_SECRET_DEV/JWT_SECRET_PROD env var if omitted.
131
+ * Pass string[] to support key rotation — first element signs, all elements verify.
132
+ */
133
+ secret?: string | string[];
134
+ /** Sign/verify cookie values set via exported helpers. Default: false. */
135
+ cookies?: boolean;
136
+ /** Sign pagination cursor tokens to prevent client tampering. Default: false. */
137
+ cursors?: boolean;
138
+ /** HMAC-based stateless presigned URLs (no DB lookup). Default: false. */
139
+ presignedUrls?: boolean | {
140
+ defaultExpiry?: number;
141
+ };
142
+ /** Require clients to HMAC-sign requests (method+path+timestamp+body). Default: false. */
143
+ requestSigning?: boolean | {
144
+ tolerance?: number;
145
+ header?: string;
146
+ timestampHeader?: string;
147
+ };
148
+ /** Hash idempotency keys before storage. Default: false. */
149
+ idempotencyKeys?: boolean;
150
+ /** Bind sessions to client IP+UA fingerprint. Default: false. */
151
+ sessionBinding?: boolean | {
152
+ fields?: Array<"ip" | "ua" | "accept-language">;
153
+ /**
154
+ * What to do when fingerprint doesn't match.
155
+ * - "unauthenticate": treat as logged-out (default — graceful but masks attacks)
156
+ * - "reject": return 401 (strict — recommended for security-conscious apps)
157
+ * - "log-only": allow through but log the mismatch (useful during rollout)
158
+ */
159
+ onMismatch?: "unauthenticate" | "reject" | "log-only";
160
+ };
161
+ }
162
+ export declare const setSigningConfig: (config: SigningConfig | null) => void;
163
+ export declare const getSigningConfig: () => SigningConfig | null;
164
+ /**
165
+ * Returns the active signing secret: signing.secret → JWT_SECRET_PROD/DEV env var.
166
+ * Returns null when neither is configured — callers must handle this gracefully.
167
+ */
168
+ export declare const getSigningSecret: () => string | string[] | null;
169
+ export interface JwtConfig {
170
+ /** JWT issuer claim (`iss`). When set, added to all tokens and validated on verify. */
171
+ issuer?: string;
172
+ /** JWT audience claim (`aud`). When set, added to all tokens and validated on verify. */
173
+ audience?: string | string[];
174
+ /** JWT signing algorithm. Default: "HS256". Use "RS256" for OIDC. Requires OidcConfig when set to "RS256". */
175
+ algorithm?: "HS256" | "RS256";
176
+ }
177
+ export declare const setJwtConfig: (config: JwtConfig | null) => void;
178
+ export declare const getJwtConfig: () => JwtConfig | null;
179
+ export declare const getJwtIssuer: () => string | undefined;
180
+ export declare const getJwtAudience: () => string | string[] | undefined;
181
+ export interface BreachedPasswordConfig {
182
+ /** Block registration/reset when password is breached. Default: true. */
183
+ block?: boolean;
184
+ /** Minimum breach count to consider breached. Default: 1. */
185
+ minBreachCount?: number;
186
+ /** Request timeout in ms. Default: 3000. */
187
+ timeout?: number;
188
+ /** What to do when the HIBP API is unavailable. Default: "allow". */
189
+ onApiFailure?: "allow" | "block";
190
+ }
191
+ export declare const setBreachedPasswordConfig: (config: BreachedPasswordConfig | null) => void;
192
+ export declare const getBreachedPasswordConfig: () => BreachedPasswordConfig | null;
193
+ export interface StepUpConfig {
194
+ /** Max age in seconds since last MFA verification. Default: 300 (5 min). */
195
+ maxAge?: number;
196
+ }
197
+ export declare const setStepUpConfig: (config: StepUpConfig | null) => void;
198
+ export declare const getStepUpConfig: () => StepUpConfig | null;
199
+ export declare const setCheckSuspensionOnIdentify: (v: boolean) => void;
200
+ export declare const getCheckSuspensionOnIdentify: () => boolean;
201
+ export declare const setCaptchaConfig: (config: import("./captcha").CaptchaConfig | null) => void;
202
+ export declare const getCaptchaConfig: () => import("./captcha").CaptchaConfig | null;
203
+ export interface M2MConfig {
204
+ enabled?: boolean;
205
+ /** Access token expiry in seconds. Default: 3600 (1 hour). */
206
+ tokenExpiry?: number;
207
+ /** Allowed scopes for M2M clients. */
208
+ scopes?: string[];
209
+ }
210
+ export declare const setM2MConfig: (config: M2MConfig | null) => void;
211
+ export declare const getM2MConfig: () => M2MConfig | null;
212
+ export declare const getM2MTokenExpiry: () => number;
213
+ export interface SamlConfig {
214
+ /** Service Provider entity ID (e.g. "https://yourapp.com/auth/saml"). */
215
+ entityId: string;
216
+ /** Assertion Consumer Service URL. */
217
+ acsUrl: string;
218
+ /** IdP metadata — XML string or URL. */
219
+ idpMetadata: string;
220
+ /** SP signing private key PEM. Optional. */
221
+ signingKey?: string;
222
+ /** SP signing certificate PEM. Optional. */
223
+ signingCert?: string;
224
+ /** Map IdP attribute names to profile fields. */
225
+ attributeMapping?: {
226
+ email?: string;
227
+ firstName?: string;
228
+ lastName?: string;
229
+ groups?: string;
230
+ };
231
+ /** Custom user lookup/creation. When provided, takes precedence over findOrCreateByProvider. */
232
+ onLogin?: (profile: SamlProfile) => Promise<{
233
+ userId: string;
234
+ }>;
235
+ /** Where to redirect after successful SAML login. Default: "/". */
236
+ postLoginRedirect?: string;
237
+ }
238
+ import type { SamlProfile } from "./saml";
239
+ export declare const setSamlConfig: (config: SamlConfig | null) => void;
240
+ export declare const getSamlConfig: () => SamlConfig | null;
241
+ export interface OidcConfig {
242
+ enabled?: boolean;
243
+ /** JWT issuer — included in all tokens and OIDC discovery doc. Required. */
244
+ issuer: string;
245
+ /** RSA signing key. If not provided, a key pair is auto-generated on startup. */
246
+ signingKey?: {
247
+ privateKey: string;
248
+ publicKey: string;
249
+ kid?: string;
250
+ };
251
+ /** Previous signing keys for rotation (verification only). */
252
+ previousKeys?: Array<{
253
+ publicKey: string;
254
+ kid?: string;
255
+ }>;
256
+ /** Scopes advertised in the discovery document. Default: ["openid"]. */
257
+ scopes?: string[];
258
+ /** Token endpoint URL. Defaults to `${issuer}/oauth/token`. */
259
+ tokenEndpoint?: string;
260
+ }
261
+ export declare const setOidcConfig: (config: OidcConfig | null) => void;
262
+ export declare const getOidcConfig: () => OidcConfig | null;
263
+ export interface ScimConfig {
264
+ enabled?: boolean;
265
+ /** Bearer token(s) for SCIM endpoint authentication. Required. */
266
+ bearerTokens: string | string[];
267
+ /** Username mapping strategy. Default: "email". */
268
+ userMapping?: {
269
+ userName?: "email" | "username";
270
+ };
271
+ /** What to do when a user is deleted via SCIM. Default: "suspend". */
272
+ onDeprovision?: "suspend" | "delete" | ((userId: string) => Promise<void>);
273
+ }
274
+ export declare const setScimConfig: (config: ScimConfig | null) => void;
275
+ export declare const getScimConfig: () => ScimConfig | null;
@@ -59,9 +59,63 @@ export const getMfaChallengeTtl = () => _mfaConfig?.challengeTtlSeconds ?? 300;
59
59
  export const getMfaEmailOtpConfig = () => _mfaConfig?.emailOtp ?? null;
60
60
  export const getMfaEmailOtpCodeLength = () => _mfaConfig?.emailOtp?.codeLength ?? 6;
61
61
  export const getMfaWebAuthnConfig = () => _mfaConfig?.webauthn ?? null;
62
+ export const getMfaRequired = () => _mfaConfig?.required ?? false;
63
+ export const getMfaWebAuthnAllowPasswordlessLogin = () => _mfaConfig?.webauthn?.allowPasswordlessLogin ?? false;
64
+ export const getMfaWebAuthnPasskeyMfaBypass = () => _mfaConfig?.webauthn?.passkeyMfaBypass ?? true;
62
65
  // ---------------------------------------------------------------------------
63
66
  // CSRF config
64
67
  // ---------------------------------------------------------------------------
65
68
  let _csrfEnabled = false;
66
69
  export const setCsrfEnabled = (v) => { _csrfEnabled = v; };
67
70
  export const getCsrfEnabled = () => _csrfEnabled;
71
+ let _signingConfig = null;
72
+ export const setSigningConfig = (config) => { _signingConfig = config; };
73
+ export const getSigningConfig = () => _signingConfig;
74
+ /**
75
+ * Returns the active signing secret: signing.secret → JWT_SECRET_PROD/DEV env var.
76
+ * Returns null when neither is configured — callers must handle this gracefully.
77
+ */
78
+ export const getSigningSecret = () => {
79
+ if (_signingConfig?.secret)
80
+ return _signingConfig.secret;
81
+ const isProd = process.env.NODE_ENV === "production";
82
+ const envKey = isProd ? "JWT_SECRET_PROD" : "JWT_SECRET_DEV";
83
+ const rawSecret = process.env[envKey];
84
+ return rawSecret ?? null;
85
+ };
86
+ let _jwtConfig = null;
87
+ export const setJwtConfig = (config) => { _jwtConfig = config; };
88
+ export const getJwtConfig = () => _jwtConfig;
89
+ export const getJwtIssuer = () => _jwtConfig?.issuer;
90
+ export const getJwtAudience = () => _jwtConfig?.audience;
91
+ let _breachedPasswordConfig = null;
92
+ export const setBreachedPasswordConfig = (config) => { _breachedPasswordConfig = config; };
93
+ export const getBreachedPasswordConfig = () => _breachedPasswordConfig;
94
+ let _stepUpConfig = null;
95
+ export const setStepUpConfig = (config) => { _stepUpConfig = config; };
96
+ export const getStepUpConfig = () => _stepUpConfig;
97
+ // ---------------------------------------------------------------------------
98
+ // Suspension config
99
+ // ---------------------------------------------------------------------------
100
+ let _checkSuspensionOnIdentify = false;
101
+ export const setCheckSuspensionOnIdentify = (v) => { _checkSuspensionOnIdentify = v; };
102
+ export const getCheckSuspensionOnIdentify = () => _checkSuspensionOnIdentify;
103
+ // ---------------------------------------------------------------------------
104
+ // CAPTCHA config
105
+ // ---------------------------------------------------------------------------
106
+ let _captchaConfig = null;
107
+ export const setCaptchaConfig = (config) => { _captchaConfig = config; };
108
+ export const getCaptchaConfig = () => _captchaConfig;
109
+ let _m2mConfig = null;
110
+ export const setM2MConfig = (config) => { _m2mConfig = config; };
111
+ export const getM2MConfig = () => _m2mConfig;
112
+ export const getM2MTokenExpiry = () => _m2mConfig?.tokenExpiry ?? 3600;
113
+ let _samlConfig = null;
114
+ export const setSamlConfig = (config) => { _samlConfig = config; };
115
+ export const getSamlConfig = () => _samlConfig;
116
+ let _oidcConfig = null;
117
+ export const setOidcConfig = (config) => { _oidcConfig = config; };
118
+ export const getOidcConfig = () => _oidcConfig;
119
+ let _scimConfig = null;
120
+ export const setScimConfig = (config) => { _scimConfig = config; };
121
+ export const getScimConfig = () => _scimConfig;
@@ -0,0 +1,58 @@
1
+ import type { Database } from "bun:sqlite";
2
+ export interface AuditLogEntry {
3
+ id: string;
4
+ userId: string | null;
5
+ sessionId: string | null;
6
+ tenantId: string | null;
7
+ method: string;
8
+ path: string;
9
+ status: number;
10
+ ip: string | null;
11
+ userAgent: string | null;
12
+ action?: string;
13
+ resource?: string;
14
+ resourceId?: string;
15
+ meta?: Record<string, unknown>;
16
+ requestId?: string;
17
+ /** ISO 8601 string across all backends. */
18
+ createdAt: string;
19
+ /** MongoDB TTL only — silently ignored by SQLite and memory stores. */
20
+ expiresAt?: Date;
21
+ }
22
+ export type AuditLogStore = "mongo" | "sqlite" | "memory";
23
+ export interface AuditLogOptions {
24
+ store: AuditLogStore;
25
+ /** Required when `store === "sqlite"`. */
26
+ db?: Database;
27
+ }
28
+ export interface AuditLogQuery {
29
+ userId?: string;
30
+ tenantId?: string;
31
+ after?: Date | string;
32
+ before?: Date | string;
33
+ /** Default 50, max 200. */
34
+ limit?: number;
35
+ /** Default 0. */
36
+ offset?: number;
37
+ }
38
+ export declare function clearAuditLogMemoryStore(): void;
39
+ /**
40
+ * Persist an audit log entry to the configured store.
41
+ * Errors are caught internally — this function never throws, to ensure
42
+ * storage failures never fail the HTTP request.
43
+ */
44
+ export declare function logAuditEntry(entry: AuditLogEntry, options: AuditLogOptions): Promise<void>;
45
+ /**
46
+ * Blocking variant of logAuditEntry for critical events (e.g. account deletion,
47
+ * password changes). Awaits the write and logs errors, but never rethrows —
48
+ * a logging failure must not break the HTTP response.
49
+ */
50
+ export declare function logAuditEntryBlocking(entry: AuditLogEntry, options: AuditLogOptions): Promise<void>;
51
+ /**
52
+ * Query audit log entries from the configured store.
53
+ * Returns `{ items, total }` where `total` is the filtered count before pagination.
54
+ */
55
+ export declare function getAuditLogs(query: AuditLogQuery, options: AuditLogOptions): Promise<{
56
+ items: AuditLogEntry[];
57
+ total: number;
58
+ }>;
@@ -0,0 +1,218 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Memory store
3
+ // ---------------------------------------------------------------------------
4
+ let _auditLogs = [];
5
+ export function clearAuditLogMemoryStore() {
6
+ _auditLogs = [];
7
+ }
8
+ // ---------------------------------------------------------------------------
9
+ // SQLite helpers
10
+ // ---------------------------------------------------------------------------
11
+ function ensureSqliteTable(db) {
12
+ // No module-level flag — CREATE IF NOT EXISTS is idempotent and cheap.
13
+ // A flag would break when multiple Database instances are used (e.g. in tests).
14
+ db.run(`
15
+ CREATE TABLE IF NOT EXISTS audit_logs (
16
+ id TEXT PRIMARY KEY,
17
+ userId TEXT,
18
+ sessionId TEXT,
19
+ tenantId TEXT,
20
+ method TEXT NOT NULL,
21
+ path TEXT NOT NULL,
22
+ status INTEGER NOT NULL,
23
+ ip TEXT,
24
+ userAgent TEXT,
25
+ action TEXT,
26
+ resource TEXT,
27
+ resourceId TEXT,
28
+ meta TEXT,
29
+ createdAt TEXT NOT NULL
30
+ )
31
+ `);
32
+ db.run("CREATE INDEX IF NOT EXISTS idx_al_user ON audit_logs(userId, createdAt)");
33
+ db.run("CREATE INDEX IF NOT EXISTS idx_al_tenant ON audit_logs(tenantId, createdAt)");
34
+ db.run("CREATE INDEX IF NOT EXISTS idx_al_path ON audit_logs(path)");
35
+ }
36
+ // ---------------------------------------------------------------------------
37
+ // logAuditEntry
38
+ // ---------------------------------------------------------------------------
39
+ /**
40
+ * Persist an audit log entry to the configured store.
41
+ * Errors are caught internally — this function never throws, to ensure
42
+ * storage failures never fail the HTTP request.
43
+ */
44
+ export async function logAuditEntry(entry, options) {
45
+ try {
46
+ if (options.store === "memory") {
47
+ _auditLogs.push(entry);
48
+ return;
49
+ }
50
+ if (options.store === "sqlite") {
51
+ const db = options.db;
52
+ if (!db)
53
+ throw new Error("AuditLog: store is 'sqlite' but no db instance was provided");
54
+ ensureSqliteTable(db);
55
+ db.run(`INSERT INTO audit_logs
56
+ (id, userId, sessionId, tenantId, method, path, status,
57
+ ip, userAgent, action, resource, resourceId, meta, createdAt)
58
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [
59
+ entry.id,
60
+ entry.userId ?? null,
61
+ entry.sessionId ?? null,
62
+ entry.tenantId ?? null,
63
+ entry.method,
64
+ entry.path,
65
+ entry.status,
66
+ entry.ip ?? null,
67
+ entry.userAgent ?? null,
68
+ entry.action ?? null,
69
+ entry.resource ?? null,
70
+ entry.resourceId ?? null,
71
+ entry.meta !== undefined ? JSON.stringify(entry.meta) : null,
72
+ entry.createdAt,
73
+ ]);
74
+ return;
75
+ }
76
+ if (options.store === "mongo") {
77
+ // Lazy import to avoid bundling mongoose when not used
78
+ const { AuditLog } = await import("../models/AuditLog");
79
+ await AuditLog.create({
80
+ ...entry,
81
+ createdAt: new Date(entry.createdAt),
82
+ });
83
+ return;
84
+ }
85
+ }
86
+ catch (err) {
87
+ console.error("[auditLog] failed to write entry:", err);
88
+ }
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // logAuditEntryBlocking
92
+ // ---------------------------------------------------------------------------
93
+ /**
94
+ * Blocking variant of logAuditEntry for critical events (e.g. account deletion,
95
+ * password changes). Awaits the write and logs errors, but never rethrows —
96
+ * a logging failure must not break the HTTP response.
97
+ */
98
+ export async function logAuditEntryBlocking(entry, options) {
99
+ try {
100
+ await logAuditEntry(entry, options);
101
+ }
102
+ catch (err) {
103
+ console.error("[auditLog] CRITICAL: blocking audit write failed:", err);
104
+ // Don't rethrow — we never want a logging failure to break the response
105
+ }
106
+ }
107
+ // ---------------------------------------------------------------------------
108
+ // getAuditLogs
109
+ // ---------------------------------------------------------------------------
110
+ /**
111
+ * Query audit log entries from the configured store.
112
+ * Returns `{ items, total }` where `total` is the filtered count before pagination.
113
+ */
114
+ export async function getAuditLogs(query, options) {
115
+ const limit = Math.min(query.limit ?? 50, 200);
116
+ const offset = query.offset ?? 0;
117
+ const after = query.after ? new Date(query.after).toISOString() : undefined;
118
+ const before = query.before ? new Date(query.before).toISOString() : undefined;
119
+ // --- Memory ---
120
+ if (options.store === "memory") {
121
+ let filtered = _auditLogs.slice();
122
+ if (query.userId !== undefined)
123
+ filtered = filtered.filter(e => e.userId === query.userId);
124
+ if (query.tenantId !== undefined)
125
+ filtered = filtered.filter(e => e.tenantId === query.tenantId);
126
+ if (after)
127
+ filtered = filtered.filter(e => e.createdAt >= after);
128
+ if (before)
129
+ filtered = filtered.filter(e => e.createdAt < before);
130
+ return { items: filtered.slice(offset, offset + limit), total: filtered.length };
131
+ }
132
+ // --- SQLite ---
133
+ if (options.store === "sqlite") {
134
+ const db = options.db;
135
+ if (!db)
136
+ throw new Error("AuditLog: store is 'sqlite' but no db instance was provided");
137
+ ensureSqliteTable(db);
138
+ const conditions = [];
139
+ const params = [];
140
+ if (query.userId !== undefined) {
141
+ conditions.push("userId = ?");
142
+ params.push(query.userId);
143
+ }
144
+ if (query.tenantId !== undefined) {
145
+ conditions.push("tenantId = ?");
146
+ params.push(query.tenantId);
147
+ }
148
+ if (after) {
149
+ conditions.push("createdAt >= ?");
150
+ params.push(after);
151
+ }
152
+ if (before) {
153
+ conditions.push("createdAt < ?");
154
+ params.push(before);
155
+ }
156
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
157
+ const { count } = db.query(`SELECT COUNT(*) as count FROM audit_logs ${where}`).get(...params) ?? { count: 0 };
158
+ const rows = db.query(`SELECT * FROM audit_logs ${where} ORDER BY createdAt DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
159
+ const items = rows.map(row => ({
160
+ id: row.id,
161
+ userId: row.userId ?? null,
162
+ sessionId: row.sessionId ?? null,
163
+ tenantId: row.tenantId ?? null,
164
+ method: row.method,
165
+ path: row.path,
166
+ status: row.status,
167
+ ip: row.ip ?? null,
168
+ userAgent: row.userAgent ?? null,
169
+ action: row.action ?? undefined,
170
+ resource: row.resource ?? undefined,
171
+ resourceId: row.resourceId ?? undefined,
172
+ meta: row.meta ? JSON.parse(row.meta) : undefined,
173
+ createdAt: row.createdAt,
174
+ }));
175
+ return { items, total: count };
176
+ }
177
+ // --- MongoDB ---
178
+ if (options.store === "mongo") {
179
+ const { AuditLog } = await import("../models/AuditLog");
180
+ const filter = {};
181
+ if (query.userId !== undefined)
182
+ filter.userId = query.userId;
183
+ if (query.tenantId !== undefined)
184
+ filter.tenantId = query.tenantId;
185
+ if (after || before) {
186
+ filter.createdAt = {
187
+ ...(after ? { $gte: new Date(after) } : {}),
188
+ ...(before ? { $lt: new Date(before) } : {}),
189
+ };
190
+ }
191
+ const [total, docs] = await Promise.all([
192
+ AuditLog.countDocuments(filter),
193
+ AuditLog.find(filter)
194
+ .sort({ createdAt: -1 })
195
+ .skip(offset)
196
+ .limit(limit)
197
+ .lean(),
198
+ ]);
199
+ const items = docs.map(doc => ({
200
+ id: doc.id,
201
+ userId: doc.userId ?? null,
202
+ sessionId: doc.sessionId ?? null,
203
+ tenantId: doc.tenantId ?? null,
204
+ method: doc.method,
205
+ path: doc.path,
206
+ status: doc.status,
207
+ ip: doc.ip ?? null,
208
+ userAgent: doc.userAgent ?? null,
209
+ action: doc.action,
210
+ resource: doc.resource,
211
+ resourceId: doc.resourceId,
212
+ meta: doc.meta,
213
+ createdAt: doc.createdAt.toISOString(),
214
+ }));
215
+ return { items, total };
216
+ }
217
+ return { items: [], total: 0 };
218
+ }
@@ -1,8 +1,23 @@
1
- export interface OAuthProfile {
1
+ import type { GroupRecord, GroupMembershipRecord, PaginationOpts, PaginatedResult } from "./groups";
2
+ export type { GroupRecord, GroupMembershipRecord, PaginationOpts, PaginatedResult };
3
+ export interface M2MClientRecord {
4
+ id: string;
5
+ clientId: string;
6
+ name: string;
7
+ scopes: string[];
8
+ active: boolean;
9
+ }
10
+ export interface IdentityProfile {
2
11
  email?: string;
3
12
  name?: string;
13
+ firstName?: string;
14
+ lastName?: string;
15
+ displayName?: string;
4
16
  avatarUrl?: string;
17
+ externalId?: string;
5
18
  }
19
+ /** @deprecated Use IdentityProfile */
20
+ export type OAuthProfile = IdentityProfile;
6
21
  export interface WebAuthnCredential {
7
22
  /** Base64url-encoded credential ID. */
8
23
  credentialId: string;
@@ -47,6 +62,12 @@ export interface AuthAdapter {
47
62
  email?: string;
48
63
  providerIds?: string[];
49
64
  emailVerified?: boolean;
65
+ displayName?: string;
66
+ firstName?: string;
67
+ lastName?: string;
68
+ externalId?: string;
69
+ suspended?: boolean;
70
+ suspendedReason?: string;
50
71
  } | null>;
51
72
  /** Optional. Unlink a provider identity from a user (used by DELETE /auth/:provider/link). */
52
73
  unlinkProvider?(userId: string, provider: string): Promise<void>;
@@ -102,6 +123,124 @@ export interface AuthAdapter {
102
123
  updateWebAuthnCredentialSignCount?(userId: string, credentialId: string, signCount: number): Promise<void>;
103
124
  /** Optional. Find the user who owns a WebAuthn credential. Returns userId or null. Used for cross-user uniqueness checks. */
104
125
  findUserByWebAuthnCredentialId?(credentialId: string): Promise<string | null>;
126
+ /** Suspend or unsuspend a user. */
127
+ setSuspended?(userId: string, suspended: boolean, reason?: string): Promise<void>;
128
+ /** Get suspension status. Returns false if adapter doesn't track it. */
129
+ getSuspended?(userId: string): Promise<{
130
+ suspended: boolean;
131
+ suspendedReason?: string;
132
+ } | null>;
133
+ /** Update profile fields. */
134
+ updateProfile?(userId: string, fields: Partial<Pick<IdentityProfile, "displayName" | "firstName" | "lastName" | "externalId">>): Promise<void>;
135
+ /** List users matching a normalized query. */
136
+ listUsers?(query: UserQuery): Promise<{
137
+ users: UserRecord[];
138
+ totalResults: number;
139
+ }>;
140
+ /**
141
+ * Create a new group. Returns the new group's id.
142
+ * The name must be a slug (/^[a-z0-9_-]+$/) and unique within its scope.
143
+ * tenantId: null = app-wide group, string = tenant-scoped group.
144
+ */
145
+ createGroup?(group: Omit<GroupRecord, "id" | "createdAt" | "updatedAt">): Promise<{
146
+ id: string;
147
+ }>;
148
+ /**
149
+ * Delete a group and cascade-delete all its memberships.
150
+ * Cascade behavior is adapter-specific (MongoDB: manual deleteMany, SQLite: ON DELETE CASCADE).
151
+ */
152
+ deleteGroup?(groupId: string): Promise<void>;
153
+ /** Get a group by ID. Returns null if not found. */
154
+ getGroup?(groupId: string): Promise<GroupRecord | null>;
155
+ /**
156
+ * List groups scoped to a tenant (tenantId string) or app-wide (tenantId null).
157
+ * Results are paginated (default limit 50, max 200).
158
+ */
159
+ listGroups?(tenantId: string | null, opts?: PaginationOpts): Promise<PaginatedResult<GroupRecord>>;
160
+ /**
161
+ * Update mutable group fields: name, displayName, description, roles.
162
+ * tenantId is intentionally excluded — it is immutable after creation.
163
+ */
164
+ updateGroup?(groupId: string, updates: Partial<Pick<GroupRecord, "roles" | "name" | "displayName" | "description">>): Promise<void>;
165
+ /**
166
+ * Add a user to a group with optional per-membership roles.
167
+ *
168
+ * CONTRACT: throws if the user is already a member (unique constraint violation).
169
+ * All adapters must surface this as a thrown error, not a silent no-op.
170
+ * Use updateGroupMembership to change roles on an existing membership.
171
+ */
172
+ addGroupMember?(groupId: string, userId: string, roles?: string[]): Promise<void>;
173
+ /**
174
+ * Update the per-membership roles for an existing group member.
175
+ * Replaces the member's roles[] in place (not additive).
176
+ * No updatedAt is tracked — intentional, see GroupMembershipRecord.
177
+ */
178
+ updateGroupMembership?(groupId: string, userId: string, roles: string[]): Promise<void>;
179
+ /** Remove a user from a group. No-op if the user is not a member. */
180
+ removeGroupMember?(groupId: string, userId: string): Promise<void>;
181
+ /** List members of a group with their per-membership roles. Paginated. */
182
+ getGroupMembers?(groupId: string, opts?: PaginationOpts): Promise<PaginatedResult<{
183
+ userId: string;
184
+ roles: string[];
185
+ }>>;
186
+ /**
187
+ * List all groups a user belongs to in the given scope, with their per-membership roles.
188
+ * tenantId = null → app-wide groups; tenantId = string → tenant-scoped groups.
189
+ */
190
+ getUserGroups?(userId: string, tenantId: string | null): Promise<Array<{
191
+ group: GroupRecord;
192
+ membershipRoles: string[];
193
+ }>>;
194
+ /**
195
+ * Return all roles a user effectively has in the given scope, combining:
196
+ * 1. Direct roles (app-wide or tenant-scoped)
197
+ * 2. Group baseline roles (from all groups the user belongs to in that scope)
198
+ * 3. Per-membership roles (user-specific extras within each group)
199
+ *
200
+ * SCOPE CONTRACT (matches requireRole behavior):
201
+ * - tenantId = null → app-wide direct roles + app-wide group roles only
202
+ * - tenantId = string → tenant-scoped direct roles + tenant-scoped group roles only
203
+ *
204
+ * Tenant-scoped group roles NEVER satisfy app-wide role checks and vice versa.
205
+ */
206
+ getEffectiveRoles?(userId: string, tenantId: string | null): Promise<string[]>;
207
+ /** Optional. Look up an active M2M client by clientId (includes clientSecretHash for verification). */
208
+ getM2MClient?(clientId: string): Promise<(M2MClientRecord & {
209
+ clientSecretHash: string;
210
+ }) | null>;
211
+ /** Optional. Create a new M2M client. Returns the new client's id. */
212
+ createM2MClient?(client: {
213
+ clientId: string;
214
+ clientSecretHash: string;
215
+ name: string;
216
+ scopes: string[];
217
+ }): Promise<{
218
+ id: string;
219
+ }>;
220
+ /** Optional. Delete an M2M client by clientId. */
221
+ deleteM2MClient?(clientId: string): Promise<void>;
222
+ /** Optional. List all M2M clients (without secrets). */
223
+ listM2MClients?(): Promise<M2MClientRecord[]>;
224
+ }
225
+ export interface UserQuery {
226
+ email?: string;
227
+ externalId?: string;
228
+ suspended?: boolean;
229
+ startIndex?: number;
230
+ count?: number;
231
+ }
232
+ export interface UserRecord {
233
+ id: string;
234
+ email?: string;
235
+ displayName?: string;
236
+ firstName?: string;
237
+ lastName?: string;
238
+ externalId?: string;
239
+ suspended: boolean;
240
+ suspendedAt?: Date;
241
+ suspendedReason?: string;
242
+ emailVerified?: boolean;
243
+ providerIds?: string[];
105
244
  }
106
245
  export declare const setAuthAdapter: (adapter: AuthAdapter) => void;
107
246
  export declare const getAuthAdapter: () => AuthAdapter;