@cairs-npm/express-auth-2fa 0.0.0-stage → 0.1.0
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.
- package/README.md +84 -2
- package/dist/chunk-PQPLAWOZ.js +44 -0
- package/dist/chunk-PQPLAWOZ.js.map +1 -0
- package/dist/contract/index.cjs +25 -0
- package/dist/contract/index.cjs.map +1 -0
- package/dist/contract/index.d.cts +1 -0
- package/dist/contract/index.d.ts +1 -0
- package/dist/contract/index.js +3 -0
- package/dist/contract/index.js.map +1 -0
- package/dist/hash-GKCJDZLH.js +21 -0
- package/dist/hash-GKCJDZLH.js.map +1 -0
- package/dist/index.cjs +1759 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +577 -0
- package/dist/index.d.ts +577 -0
- package/dist/index.js +1682 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -4
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import * as mongodb from 'mongodb';
|
|
2
|
+
import { ObjectId, Db, MongoClient } from 'mongodb';
|
|
3
|
+
import { MethodType, ErrorCode, LoginResponse, Method } from '@cairs-npm/auth-2fa-contract';
|
|
4
|
+
export { ApiError, Method, Session, Tokens, User } from '@cairs-npm/auth-2fa-contract';
|
|
5
|
+
import { Request, Response, NextFunction, Router } from 'express';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
interface CredentialDoc {
|
|
9
|
+
_id?: ObjectId;
|
|
10
|
+
userId: string;
|
|
11
|
+
passwordHash: string;
|
|
12
|
+
twoStepEnabled: boolean;
|
|
13
|
+
failedLogins: {
|
|
14
|
+
count: number;
|
|
15
|
+
windowStart: Date;
|
|
16
|
+
};
|
|
17
|
+
lockedUntil: Date | null;
|
|
18
|
+
updatedAt: Date;
|
|
19
|
+
}
|
|
20
|
+
type MethodStatus = "pending" | "active";
|
|
21
|
+
interface MethodDoc {
|
|
22
|
+
_id?: ObjectId;
|
|
23
|
+
userId: string;
|
|
24
|
+
type: MethodType;
|
|
25
|
+
label: string;
|
|
26
|
+
isDefault: boolean;
|
|
27
|
+
status: MethodStatus;
|
|
28
|
+
/** AES-256-GCM ciphertext (TOTP secret). */
|
|
29
|
+
secretEnc?: string;
|
|
30
|
+
/** AES-256-GCM ciphertext (E.164 phone). */
|
|
31
|
+
phoneE164Enc?: string;
|
|
32
|
+
/** Last 4 digits for masking. */
|
|
33
|
+
phoneLast4?: string;
|
|
34
|
+
/** TOTP replay guard: last accepted time-step. */
|
|
35
|
+
lastUsedStep?: number;
|
|
36
|
+
/** SMS confirm-code hash while pending. */
|
|
37
|
+
pendingCodeHash?: string;
|
|
38
|
+
/** Expiry for pending methods (TTL). */
|
|
39
|
+
pendingExpiresAt?: Date;
|
|
40
|
+
/** Passkey: WebAuthn credential id (base64url). */
|
|
41
|
+
credentialId?: string;
|
|
42
|
+
/** Passkey: COSE public key (base64url). */
|
|
43
|
+
publicKey?: string;
|
|
44
|
+
/** Passkey: signature counter (clone detection). */
|
|
45
|
+
counter?: number;
|
|
46
|
+
/** Passkey: authenticator transports hint (usb, nfc, ble, internal, hybrid). */
|
|
47
|
+
transports?: string[];
|
|
48
|
+
createdAt: Date;
|
|
49
|
+
lastUsedAt: Date | null;
|
|
50
|
+
}
|
|
51
|
+
type ChallengePurpose = "login" | "step_up" | "enroll";
|
|
52
|
+
interface ChallengeDoc {
|
|
53
|
+
_id: string;
|
|
54
|
+
userId: string;
|
|
55
|
+
purpose: ChallengePurpose;
|
|
56
|
+
methodId: string | null;
|
|
57
|
+
smsCodeHash: string | null;
|
|
58
|
+
smsSentAt: Date | null;
|
|
59
|
+
smsSendCount: number;
|
|
60
|
+
attempts: number;
|
|
61
|
+
/** WebAuthn challenge issued for this login / passkey enrollment (base64url). */
|
|
62
|
+
webauthnChallenge?: string | null;
|
|
63
|
+
expiresAt: Date;
|
|
64
|
+
createdAt: Date;
|
|
65
|
+
}
|
|
66
|
+
interface SessionDoc {
|
|
67
|
+
_id: string;
|
|
68
|
+
userId: string;
|
|
69
|
+
refreshTokenHash: string;
|
|
70
|
+
/** Token family for reuse detection (BR-11). */
|
|
71
|
+
familyId: string;
|
|
72
|
+
/** sid of the session this one rotated from. */
|
|
73
|
+
rotatedFrom: string | null;
|
|
74
|
+
/** Previous refresh-token hashes; reuse of any revokes the family (BR-11). */
|
|
75
|
+
usedHashes?: string[];
|
|
76
|
+
userAgent?: string;
|
|
77
|
+
ip?: string;
|
|
78
|
+
createdAt: Date;
|
|
79
|
+
expiresAt: Date;
|
|
80
|
+
revokedAt: Date | null;
|
|
81
|
+
}
|
|
82
|
+
interface TrustedDeviceDoc {
|
|
83
|
+
_id?: ObjectId;
|
|
84
|
+
userId: string;
|
|
85
|
+
tokenHash: string;
|
|
86
|
+
label: string;
|
|
87
|
+
createdAt: Date;
|
|
88
|
+
expiresAt: Date;
|
|
89
|
+
}
|
|
90
|
+
interface EventDoc {
|
|
91
|
+
_id?: ObjectId;
|
|
92
|
+
userId: string;
|
|
93
|
+
type: string;
|
|
94
|
+
ip?: string;
|
|
95
|
+
userAgent?: string;
|
|
96
|
+
at: Date;
|
|
97
|
+
meta?: Record<string, unknown>;
|
|
98
|
+
}
|
|
99
|
+
interface RateLimitDoc {
|
|
100
|
+
_id: string;
|
|
101
|
+
count: number;
|
|
102
|
+
expiresAt: Date;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface AuthCollections {
|
|
106
|
+
credentials: mongodb.Collection<CredentialDoc>;
|
|
107
|
+
methods: mongodb.Collection<MethodDoc>;
|
|
108
|
+
challenges: mongodb.Collection<ChallengeDoc>;
|
|
109
|
+
sessions: mongodb.Collection<SessionDoc>;
|
|
110
|
+
trustedDevices: mongodb.Collection<TrustedDeviceDoc>;
|
|
111
|
+
events: mongodb.Collection<EventDoc>;
|
|
112
|
+
rateLimits: mongodb.Collection<RateLimitDoc>;
|
|
113
|
+
}
|
|
114
|
+
declare function getCollections(db: Db, prefix?: string): AuthCollections;
|
|
115
|
+
|
|
116
|
+
/** App-owned user record shape returned by userAdapter. */
|
|
117
|
+
interface AdapterUser {
|
|
118
|
+
id: string;
|
|
119
|
+
username?: string;
|
|
120
|
+
email?: string;
|
|
121
|
+
[key: string]: unknown;
|
|
122
|
+
}
|
|
123
|
+
interface UserAdapter {
|
|
124
|
+
findByLogin: (login: string) => Promise<AdapterUser | null>;
|
|
125
|
+
findById: (id: string) => Promise<AdapterUser | null>;
|
|
126
|
+
}
|
|
127
|
+
interface SmsProvider {
|
|
128
|
+
send: (phoneE164: string, message: string) => Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
interface EmailProvider {
|
|
131
|
+
/** `message` is a ready-made plain-text body; `code` lets apps render their own template. */
|
|
132
|
+
send: (input: {
|
|
133
|
+
to: string;
|
|
134
|
+
code: string;
|
|
135
|
+
message: string;
|
|
136
|
+
}) => Promise<void>;
|
|
137
|
+
}
|
|
138
|
+
interface SecurityEvent {
|
|
139
|
+
userId: string;
|
|
140
|
+
type: string;
|
|
141
|
+
ip?: string;
|
|
142
|
+
userAgent?: string;
|
|
143
|
+
at: Date;
|
|
144
|
+
meta?: Record<string, unknown>;
|
|
145
|
+
}
|
|
146
|
+
interface Hooks {
|
|
147
|
+
onSecurityEvent?: (event: SecurityEvent) => void | Promise<void>;
|
|
148
|
+
}
|
|
149
|
+
type Clock = () => Date;
|
|
150
|
+
interface Logger {
|
|
151
|
+
info: (obj: object, msg?: string) => void;
|
|
152
|
+
warn: (obj: object, msg?: string) => void;
|
|
153
|
+
error: (obj: object, msg?: string) => void;
|
|
154
|
+
}
|
|
155
|
+
declare const configSchema: z.ZodObject<{
|
|
156
|
+
db: z.ZodType<Db, z.ZodTypeDef, Db>;
|
|
157
|
+
issuer: z.ZodString;
|
|
158
|
+
/** PEM PKCS8 private key (Ed25519 or RSA). Required, min 32 bytes. */
|
|
159
|
+
jwtPrivateKey: z.ZodString;
|
|
160
|
+
/** 32-byte AES key, raw string or base64. Required. */
|
|
161
|
+
encryptionKey: z.ZodString;
|
|
162
|
+
keyId: z.ZodDefault<z.ZodString>;
|
|
163
|
+
userAdapter: z.ZodType<UserAdapter, z.ZodTypeDef, UserAdapter>;
|
|
164
|
+
smsProvider: z.ZodType<SmsProvider, z.ZodTypeDef, SmsProvider>;
|
|
165
|
+
/** Enables the always-present EMAIL method (default for new users). */
|
|
166
|
+
emailProvider: z.ZodOptional<z.ZodType<EmailProvider, z.ZodTypeDef, EmailProvider>>;
|
|
167
|
+
/** MongoClient for transactions; defaults to the client behind `db`. */
|
|
168
|
+
client: z.ZodOptional<z.ZodType<MongoClient, z.ZodTypeDef, MongoClient>>;
|
|
169
|
+
logger: z.ZodOptional<z.ZodType<Logger, z.ZodTypeDef, Logger>>;
|
|
170
|
+
hooks: z.ZodOptional<z.ZodType<Hooks, z.ZodTypeDef, Hooks>>;
|
|
171
|
+
clock: z.ZodOptional<z.ZodType<Clock, z.ZodTypeDef, Clock>>;
|
|
172
|
+
collectionPrefix: z.ZodDefault<z.ZodString>;
|
|
173
|
+
/**
|
|
174
|
+
* Passkeys (WebAuthn) as a second step. `rpID` is the site's domain (e.g. "childmatch.com",
|
|
175
|
+
* "localhost"); `origins` are the exact browser origins allowed (e.g. "https://app.childmatch.com").
|
|
176
|
+
* Omit to turn passkeys off.
|
|
177
|
+
*/
|
|
178
|
+
passkeys: z.ZodOptional<z.ZodObject<{
|
|
179
|
+
rpID: z.ZodString;
|
|
180
|
+
rpName: z.ZodString;
|
|
181
|
+
origins: z.ZodArray<z.ZodString, "many">;
|
|
182
|
+
}, "strip", z.ZodTypeAny, {
|
|
183
|
+
rpID: string;
|
|
184
|
+
rpName: string;
|
|
185
|
+
origins: string[];
|
|
186
|
+
}, {
|
|
187
|
+
rpID: string;
|
|
188
|
+
rpName: string;
|
|
189
|
+
origins: string[];
|
|
190
|
+
}>>;
|
|
191
|
+
/** 2-Step Verification state for users who have not chosen one yet. */
|
|
192
|
+
twoStepDefault: z.ZodDefault<z.ZodBoolean>;
|
|
193
|
+
limits: z.ZodDefault<z.ZodObject<{
|
|
194
|
+
/** Failed passwords before lockout (BR-2). */
|
|
195
|
+
loginMaxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
196
|
+
/** Window for failed-login counting, minutes (BR-2). */
|
|
197
|
+
loginWindowMin: z.ZodDefault<z.ZodNumber>;
|
|
198
|
+
/** Lockout duration, minutes (BR-2, BR-9). */
|
|
199
|
+
lockMinutes: z.ZodDefault<z.ZodNumber>;
|
|
200
|
+
/** Min seconds between SMS sends on one challenge (BR-6). */
|
|
201
|
+
smsResendSec: z.ZodDefault<z.ZodNumber>;
|
|
202
|
+
/** Max SMS sends per challenge (BR-6). */
|
|
203
|
+
smsMaxPerChallenge: z.ZodDefault<z.ZodNumber>;
|
|
204
|
+
/** Max SMS sends per user per hour (BR-6). */
|
|
205
|
+
smsMaxPerUserHour: z.ZodDefault<z.ZodNumber>;
|
|
206
|
+
/** Wrong codes per challenge before lockout (BR-9). */
|
|
207
|
+
mfaMaxAttempts: z.ZodDefault<z.ZodNumber>;
|
|
208
|
+
/** Max verification methods per user (BR-16). */
|
|
209
|
+
maxMethods: z.ZodDefault<z.ZodNumber>;
|
|
210
|
+
/** Per-IP login requests per minute (NFR-B4). */
|
|
211
|
+
loginIpPerMin: z.ZodDefault<z.ZodNumber>;
|
|
212
|
+
/** Per-IP MFA verify requests per minute (NFR-B4). */
|
|
213
|
+
verifyIpPerMin: z.ZodDefault<z.ZodNumber>;
|
|
214
|
+
/** Per-IP step-up requests per minute (NFR-B4). */
|
|
215
|
+
stepUpIpPerMin: z.ZodDefault<z.ZodNumber>;
|
|
216
|
+
}, "strip", z.ZodTypeAny, {
|
|
217
|
+
loginMaxAttempts: number;
|
|
218
|
+
loginWindowMin: number;
|
|
219
|
+
lockMinutes: number;
|
|
220
|
+
smsResendSec: number;
|
|
221
|
+
smsMaxPerChallenge: number;
|
|
222
|
+
smsMaxPerUserHour: number;
|
|
223
|
+
mfaMaxAttempts: number;
|
|
224
|
+
maxMethods: number;
|
|
225
|
+
loginIpPerMin: number;
|
|
226
|
+
verifyIpPerMin: number;
|
|
227
|
+
stepUpIpPerMin: number;
|
|
228
|
+
}, {
|
|
229
|
+
loginMaxAttempts?: number | undefined;
|
|
230
|
+
loginWindowMin?: number | undefined;
|
|
231
|
+
lockMinutes?: number | undefined;
|
|
232
|
+
smsResendSec?: number | undefined;
|
|
233
|
+
smsMaxPerChallenge?: number | undefined;
|
|
234
|
+
smsMaxPerUserHour?: number | undefined;
|
|
235
|
+
mfaMaxAttempts?: number | undefined;
|
|
236
|
+
maxMethods?: number | undefined;
|
|
237
|
+
loginIpPerMin?: number | undefined;
|
|
238
|
+
verifyIpPerMin?: number | undefined;
|
|
239
|
+
stepUpIpPerMin?: number | undefined;
|
|
240
|
+
}>>;
|
|
241
|
+
/**
|
|
242
|
+
* Set false to turn off every RATE_LIMITED check: the per-IP limits on login / code
|
|
243
|
+
* verify / send / step-up and the code-send cooldown and caps. Account lockouts after
|
|
244
|
+
* failed passwords or codes (LOCKED) still apply.
|
|
245
|
+
*/
|
|
246
|
+
rateLimits: z.ZodDefault<z.ZodBoolean>;
|
|
247
|
+
argon2: z.ZodDefault<z.ZodObject<{
|
|
248
|
+
memoryKiB: z.ZodDefault<z.ZodNumber>;
|
|
249
|
+
iterations: z.ZodDefault<z.ZodNumber>;
|
|
250
|
+
parallelism: z.ZodDefault<z.ZodNumber>;
|
|
251
|
+
}, "strip", z.ZodTypeAny, {
|
|
252
|
+
memoryKiB: number;
|
|
253
|
+
iterations: number;
|
|
254
|
+
parallelism: number;
|
|
255
|
+
}, {
|
|
256
|
+
memoryKiB?: number | undefined;
|
|
257
|
+
iterations?: number | undefined;
|
|
258
|
+
parallelism?: number | undefined;
|
|
259
|
+
}>>;
|
|
260
|
+
cookies: z.ZodDefault<z.ZodObject<{
|
|
261
|
+
secure: z.ZodDefault<z.ZodBoolean>;
|
|
262
|
+
domain: z.ZodOptional<z.ZodString>;
|
|
263
|
+
sameSite: z.ZodDefault<z.ZodEnum<["lax", "strict", "none"]>>;
|
|
264
|
+
path: z.ZodDefault<z.ZodString>;
|
|
265
|
+
}, "strip", z.ZodTypeAny, {
|
|
266
|
+
path: string;
|
|
267
|
+
secure: boolean;
|
|
268
|
+
sameSite: "lax" | "strict" | "none";
|
|
269
|
+
domain?: string | undefined;
|
|
270
|
+
}, {
|
|
271
|
+
path?: string | undefined;
|
|
272
|
+
secure?: boolean | undefined;
|
|
273
|
+
domain?: string | undefined;
|
|
274
|
+
sameSite?: "lax" | "strict" | "none" | undefined;
|
|
275
|
+
}>>;
|
|
276
|
+
ttl: z.ZodDefault<z.ZodObject<{
|
|
277
|
+
accessTokenSec: z.ZodDefault<z.ZodNumber>;
|
|
278
|
+
refreshTokenDays: z.ZodDefault<z.ZodNumber>;
|
|
279
|
+
trustedDeviceDays: z.ZodDefault<z.ZodNumber>;
|
|
280
|
+
challengeSec: z.ZodDefault<z.ZodNumber>;
|
|
281
|
+
stepUpSec: z.ZodDefault<z.ZodNumber>;
|
|
282
|
+
pendingMethodMin: z.ZodDefault<z.ZodNumber>;
|
|
283
|
+
auditDays: z.ZodDefault<z.ZodNumber>;
|
|
284
|
+
}, "strip", z.ZodTypeAny, {
|
|
285
|
+
accessTokenSec: number;
|
|
286
|
+
refreshTokenDays: number;
|
|
287
|
+
trustedDeviceDays: number;
|
|
288
|
+
challengeSec: number;
|
|
289
|
+
stepUpSec: number;
|
|
290
|
+
pendingMethodMin: number;
|
|
291
|
+
auditDays: number;
|
|
292
|
+
}, {
|
|
293
|
+
accessTokenSec?: number | undefined;
|
|
294
|
+
refreshTokenDays?: number | undefined;
|
|
295
|
+
trustedDeviceDays?: number | undefined;
|
|
296
|
+
challengeSec?: number | undefined;
|
|
297
|
+
stepUpSec?: number | undefined;
|
|
298
|
+
pendingMethodMin?: number | undefined;
|
|
299
|
+
auditDays?: number | undefined;
|
|
300
|
+
}>>;
|
|
301
|
+
/** Serve GET /auth/openapi.json (A.7 docs). */
|
|
302
|
+
exposeDocs: z.ZodDefault<z.ZodBoolean>;
|
|
303
|
+
}, "strip", z.ZodTypeAny, {
|
|
304
|
+
db: Db;
|
|
305
|
+
issuer: string;
|
|
306
|
+
jwtPrivateKey: string;
|
|
307
|
+
encryptionKey: string;
|
|
308
|
+
keyId: string;
|
|
309
|
+
userAdapter: UserAdapter;
|
|
310
|
+
smsProvider: SmsProvider;
|
|
311
|
+
collectionPrefix: string;
|
|
312
|
+
twoStepDefault: boolean;
|
|
313
|
+
limits: {
|
|
314
|
+
loginMaxAttempts: number;
|
|
315
|
+
loginWindowMin: number;
|
|
316
|
+
lockMinutes: number;
|
|
317
|
+
smsResendSec: number;
|
|
318
|
+
smsMaxPerChallenge: number;
|
|
319
|
+
smsMaxPerUserHour: number;
|
|
320
|
+
mfaMaxAttempts: number;
|
|
321
|
+
maxMethods: number;
|
|
322
|
+
loginIpPerMin: number;
|
|
323
|
+
verifyIpPerMin: number;
|
|
324
|
+
stepUpIpPerMin: number;
|
|
325
|
+
};
|
|
326
|
+
rateLimits: boolean;
|
|
327
|
+
argon2: {
|
|
328
|
+
memoryKiB: number;
|
|
329
|
+
iterations: number;
|
|
330
|
+
parallelism: number;
|
|
331
|
+
};
|
|
332
|
+
cookies: {
|
|
333
|
+
path: string;
|
|
334
|
+
secure: boolean;
|
|
335
|
+
sameSite: "lax" | "strict" | "none";
|
|
336
|
+
domain?: string | undefined;
|
|
337
|
+
};
|
|
338
|
+
ttl: {
|
|
339
|
+
accessTokenSec: number;
|
|
340
|
+
refreshTokenDays: number;
|
|
341
|
+
trustedDeviceDays: number;
|
|
342
|
+
challengeSec: number;
|
|
343
|
+
stepUpSec: number;
|
|
344
|
+
pendingMethodMin: number;
|
|
345
|
+
auditDays: number;
|
|
346
|
+
};
|
|
347
|
+
exposeDocs: boolean;
|
|
348
|
+
emailProvider?: EmailProvider | undefined;
|
|
349
|
+
client?: MongoClient | undefined;
|
|
350
|
+
logger?: Logger | undefined;
|
|
351
|
+
hooks?: Hooks | undefined;
|
|
352
|
+
clock?: Clock | undefined;
|
|
353
|
+
passkeys?: {
|
|
354
|
+
rpID: string;
|
|
355
|
+
rpName: string;
|
|
356
|
+
origins: string[];
|
|
357
|
+
} | undefined;
|
|
358
|
+
}, {
|
|
359
|
+
db: Db;
|
|
360
|
+
issuer: string;
|
|
361
|
+
jwtPrivateKey: string;
|
|
362
|
+
encryptionKey: string;
|
|
363
|
+
userAdapter: UserAdapter;
|
|
364
|
+
smsProvider: SmsProvider;
|
|
365
|
+
keyId?: string | undefined;
|
|
366
|
+
emailProvider?: EmailProvider | undefined;
|
|
367
|
+
client?: MongoClient | undefined;
|
|
368
|
+
logger?: Logger | undefined;
|
|
369
|
+
hooks?: Hooks | undefined;
|
|
370
|
+
clock?: Clock | undefined;
|
|
371
|
+
collectionPrefix?: string | undefined;
|
|
372
|
+
passkeys?: {
|
|
373
|
+
rpID: string;
|
|
374
|
+
rpName: string;
|
|
375
|
+
origins: string[];
|
|
376
|
+
} | undefined;
|
|
377
|
+
twoStepDefault?: boolean | undefined;
|
|
378
|
+
limits?: {
|
|
379
|
+
loginMaxAttempts?: number | undefined;
|
|
380
|
+
loginWindowMin?: number | undefined;
|
|
381
|
+
lockMinutes?: number | undefined;
|
|
382
|
+
smsResendSec?: number | undefined;
|
|
383
|
+
smsMaxPerChallenge?: number | undefined;
|
|
384
|
+
smsMaxPerUserHour?: number | undefined;
|
|
385
|
+
mfaMaxAttempts?: number | undefined;
|
|
386
|
+
maxMethods?: number | undefined;
|
|
387
|
+
loginIpPerMin?: number | undefined;
|
|
388
|
+
verifyIpPerMin?: number | undefined;
|
|
389
|
+
stepUpIpPerMin?: number | undefined;
|
|
390
|
+
} | undefined;
|
|
391
|
+
rateLimits?: boolean | undefined;
|
|
392
|
+
argon2?: {
|
|
393
|
+
memoryKiB?: number | undefined;
|
|
394
|
+
iterations?: number | undefined;
|
|
395
|
+
parallelism?: number | undefined;
|
|
396
|
+
} | undefined;
|
|
397
|
+
cookies?: {
|
|
398
|
+
path?: string | undefined;
|
|
399
|
+
secure?: boolean | undefined;
|
|
400
|
+
domain?: string | undefined;
|
|
401
|
+
sameSite?: "lax" | "strict" | "none" | undefined;
|
|
402
|
+
} | undefined;
|
|
403
|
+
ttl?: {
|
|
404
|
+
accessTokenSec?: number | undefined;
|
|
405
|
+
refreshTokenDays?: number | undefined;
|
|
406
|
+
trustedDeviceDays?: number | undefined;
|
|
407
|
+
challengeSec?: number | undefined;
|
|
408
|
+
stepUpSec?: number | undefined;
|
|
409
|
+
pendingMethodMin?: number | undefined;
|
|
410
|
+
auditDays?: number | undefined;
|
|
411
|
+
} | undefined;
|
|
412
|
+
exposeDocs?: boolean | undefined;
|
|
413
|
+
}>;
|
|
414
|
+
type AuthConfigInput = z.input<typeof configSchema>;
|
|
415
|
+
type AuthConfig = z.output<typeof configSchema> & {
|
|
416
|
+
clock: Clock;
|
|
417
|
+
};
|
|
418
|
+
|
|
419
|
+
/** Dev/test provider — logs the message instead of sending a real SMS. */
|
|
420
|
+
declare function consoleSms(logger?: Logger): SmsProvider;
|
|
421
|
+
|
|
422
|
+
interface TwilioSmsOptions {
|
|
423
|
+
/** Account SID (AC…). */
|
|
424
|
+
sid: string;
|
|
425
|
+
/** Auth token for the account. */
|
|
426
|
+
token: string;
|
|
427
|
+
/** Sender number in E.164 (or a Messaging Service SID, MG…). */
|
|
428
|
+
from: string;
|
|
429
|
+
/** Twilio REST base URL; defaults to https://api.twilio.com/2010-04-01. */
|
|
430
|
+
baseUrl?: string;
|
|
431
|
+
/** Optional pre-built client (for tests / custom transports). */
|
|
432
|
+
client?: {
|
|
433
|
+
messages: {
|
|
434
|
+
create: (args: {
|
|
435
|
+
to: string;
|
|
436
|
+
from: string;
|
|
437
|
+
body: string;
|
|
438
|
+
}) => Promise<unknown>;
|
|
439
|
+
};
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Twilio SMS provider over Twilio's REST API (Messages resource), so apps need no
|
|
444
|
+
* extra dependency — pass the account settings they already have.
|
|
445
|
+
*/
|
|
446
|
+
declare function twilioSms(opts: TwilioSmsOptions): SmsProvider;
|
|
447
|
+
|
|
448
|
+
/** Dev/test provider — logs the message instead of sending a real email. */
|
|
449
|
+
declare function consoleEmail(logger?: Logger): EmailProvider;
|
|
450
|
+
|
|
451
|
+
declare class ApiErrorException extends Error {
|
|
452
|
+
readonly status: number;
|
|
453
|
+
readonly code: ErrorCode;
|
|
454
|
+
readonly extra?: {
|
|
455
|
+
attemptsLeft?: number;
|
|
456
|
+
retryAt?: string;
|
|
457
|
+
} | undefined;
|
|
458
|
+
constructor(status: number, code: ErrorCode, message: string, extra?: {
|
|
459
|
+
attemptsLeft?: number;
|
|
460
|
+
retryAt?: string;
|
|
461
|
+
} | undefined);
|
|
462
|
+
}
|
|
463
|
+
declare function apiError(status: number, code: ErrorCode, message: string, extra?: {
|
|
464
|
+
attemptsLeft?: number;
|
|
465
|
+
retryAt?: Date | string;
|
|
466
|
+
}): ApiErrorException;
|
|
467
|
+
declare function errorHandler(err: unknown, _req: Request, res: Response, _next: NextFunction): void;
|
|
468
|
+
|
|
469
|
+
interface TokenBundle {
|
|
470
|
+
accessToken: string;
|
|
471
|
+
expiresInSec: number;
|
|
472
|
+
refreshToken: string;
|
|
473
|
+
sessionId: string;
|
|
474
|
+
}
|
|
475
|
+
declare class TokenService {
|
|
476
|
+
private readonly config;
|
|
477
|
+
private readonly cols;
|
|
478
|
+
private alg;
|
|
479
|
+
private privateKeyPromise;
|
|
480
|
+
private publicKeyPromise;
|
|
481
|
+
constructor(config: AuthConfig, cols: AuthCollections);
|
|
482
|
+
signAccessToken(userId: string, sid: string, amr: string[]): Promise<string>;
|
|
483
|
+
verifyAccessToken(token: string): Promise<{
|
|
484
|
+
sub: string;
|
|
485
|
+
sid: string;
|
|
486
|
+
amr: string[];
|
|
487
|
+
}>;
|
|
488
|
+
/** Create a new session + refresh token; returns tokens for the client. */
|
|
489
|
+
issueTokens(userId: string, amr: string[], meta: {
|
|
490
|
+
userAgent?: string;
|
|
491
|
+
ip?: string;
|
|
492
|
+
}, familyId?: string): Promise<TokenBundle>;
|
|
493
|
+
/**
|
|
494
|
+
* Rotate a refresh token (BR-11). Reuse of a rotated token revokes the whole
|
|
495
|
+
* family. Old hashes are retained in `usedHashes` so reuse is detectable.
|
|
496
|
+
*/
|
|
497
|
+
rotateRefreshToken(rawToken: string, meta: {
|
|
498
|
+
userAgent?: string;
|
|
499
|
+
ip?: string;
|
|
500
|
+
}): Promise<TokenBundle>;
|
|
501
|
+
revokeSession(sid: string): Promise<void>;
|
|
502
|
+
/** 5-minute step-up JWT bound to sub+sid (BR-13). */
|
|
503
|
+
signStepUpToken(userId: string, sid: string): Promise<{
|
|
504
|
+
token: string;
|
|
505
|
+
expiresInSec: number;
|
|
506
|
+
}>;
|
|
507
|
+
verifyStepUpToken(token: string, userId: string, sid: string): Promise<void>;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
declare class AuditService {
|
|
511
|
+
private readonly config;
|
|
512
|
+
private readonly cols;
|
|
513
|
+
constructor(config: AuthConfig, cols: AuthCollections);
|
|
514
|
+
record(userId: string, type: string, meta?: {
|
|
515
|
+
ip?: string;
|
|
516
|
+
userAgent?: string;
|
|
517
|
+
meta?: Record<string, unknown>;
|
|
518
|
+
}): Promise<void>;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
declare class AuthService {
|
|
522
|
+
private readonly config;
|
|
523
|
+
private readonly cols;
|
|
524
|
+
private readonly tokens;
|
|
525
|
+
private readonly audit;
|
|
526
|
+
constructor(config: AuthConfig, cols: AuthCollections, tokens: TokenService, audit: AuditService);
|
|
527
|
+
private getCredentials;
|
|
528
|
+
private ensureCredentials;
|
|
529
|
+
/** Set or update the password for a user (used by apps/tests; registration is app-owned). */
|
|
530
|
+
setPassword(userId: string, password: string): Promise<void>;
|
|
531
|
+
login(req: Request, res: Response, input: {
|
|
532
|
+
username: string;
|
|
533
|
+
password: string;
|
|
534
|
+
rememberMe: boolean;
|
|
535
|
+
}): Promise<LoginResponse>;
|
|
536
|
+
private registerFailedLogin;
|
|
537
|
+
private resetFailedLogins;
|
|
538
|
+
private checkTrustedDevice;
|
|
539
|
+
/** Issue tokens + set cookies (refresh cookie, CSRF cookie). */
|
|
540
|
+
authenticate(userId: string, req: Request, res: Response, amr: string[], opts?: {
|
|
541
|
+
rememberDevice?: boolean;
|
|
542
|
+
rememberMe?: boolean;
|
|
543
|
+
deviceLabel?: string;
|
|
544
|
+
}): Promise<{
|
|
545
|
+
status: "AUTHENTICATED";
|
|
546
|
+
user: {
|
|
547
|
+
id: string;
|
|
548
|
+
username?: string;
|
|
549
|
+
email?: string;
|
|
550
|
+
};
|
|
551
|
+
tokens: {
|
|
552
|
+
accessToken: string;
|
|
553
|
+
expiresInSec: number;
|
|
554
|
+
};
|
|
555
|
+
}>;
|
|
556
|
+
trustDevice(userId: string, res: Response, label: string): Promise<void>;
|
|
557
|
+
refresh(req: Request, res: Response): Promise<{
|
|
558
|
+
accessToken: string;
|
|
559
|
+
expiresInSec: number;
|
|
560
|
+
}>;
|
|
561
|
+
logout(req: Request, res: Response, sid: string): Promise<void>;
|
|
562
|
+
listActiveMethods(userId: string): Promise<Method[]>;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Create the auth collections with validators, indexes and TTLs (A.4).
|
|
567
|
+
* Idempotent — safe on every boot.
|
|
568
|
+
*/
|
|
569
|
+
declare function migrate(db: Db, prefix?: string, auditDays?: number): Promise<AuthCollections>;
|
|
570
|
+
/**
|
|
571
|
+
* Mount API (A.3):
|
|
572
|
+
* app.use(express.json());
|
|
573
|
+
* app.use(createAuthRouter(config));
|
|
574
|
+
*/
|
|
575
|
+
declare function createAuthRouter(input: AuthConfigInput): Router;
|
|
576
|
+
|
|
577
|
+
export { type AdapterUser, ApiErrorException, type AuthConfig, type AuthConfigInput, AuthService, type Clock, type EmailProvider, type Hooks, type Logger, type SecurityEvent, type SmsProvider, type UserAdapter, apiError, consoleEmail, consoleSms, createAuthRouter, errorHandler, getCollections, migrate, twilioSms };
|