@luckystack/login 0.1.8 → 0.2.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/CLAUDE.md +194 -194
- package/README.md +239 -212
- package/dist/chunk-22NFCMM7.js +299 -0
- package/dist/chunk-22NFCMM7.js.map +1 -0
- package/dist/chunk-NXXBZT3I.js +401 -0
- package/dist/chunk-NXXBZT3I.js.map +1 -0
- package/dist/index.d.ts +281 -90
- package/dist/index.js +529 -792
- package/dist/index.js.map +1 -1
- package/dist/logout-QQRCD2AX.js +7 -0
- package/dist/logout-QQRCD2AX.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +69 -0
- package/dist/register.js.map +1 -0
- package/docs/credentials-auth.md +219 -219
- package/docs/hooks.md +444 -444
- package/docs/logout-flow.md +235 -235
- package/docs/oauth-providers.md +431 -431
- package/docs/password-reset.md +343 -343
- package/docs/redirect-validation.md +390 -390
- package/docs/session-management.md +286 -286
- package/docs/user-adapter.md +353 -353
- package/package.json +9 -4
package/dist/index.d.ts
CHANGED
|
@@ -102,12 +102,34 @@ interface PreEmailChangePayload {
|
|
|
102
102
|
interface PostEmailChangeRequestedPayload {
|
|
103
103
|
userId: string;
|
|
104
104
|
newEmail: string;
|
|
105
|
+
sent?: boolean;
|
|
105
106
|
}
|
|
106
107
|
interface PostEmailChangedPayload {
|
|
107
108
|
userId: string;
|
|
108
109
|
oldEmail: string;
|
|
109
110
|
newEmail: string;
|
|
110
111
|
}
|
|
112
|
+
interface LoginFailedPayload {
|
|
113
|
+
/** Email the attempt was for, when available. */
|
|
114
|
+
email?: string;
|
|
115
|
+
/** Resolved userId, when the attempt matched a real account. */
|
|
116
|
+
userId?: string;
|
|
117
|
+
/** Auth provider (e.g. `credentials`, `google`). */
|
|
118
|
+
provider: string;
|
|
119
|
+
/** i18n reason key describing why the attempt failed. */
|
|
120
|
+
reason: string;
|
|
121
|
+
/** Which flow the failure occurred in. */
|
|
122
|
+
stage: 'login' | 'register' | 'oauth';
|
|
123
|
+
requesterIp?: string;
|
|
124
|
+
}
|
|
125
|
+
interface PreAccountDeletePayload {
|
|
126
|
+
userId: string;
|
|
127
|
+
email?: string;
|
|
128
|
+
}
|
|
129
|
+
interface PostAccountDeletePayload {
|
|
130
|
+
userId: string;
|
|
131
|
+
email?: string;
|
|
132
|
+
}
|
|
111
133
|
declare module '@luckystack/core' {
|
|
112
134
|
interface HookPayloads {
|
|
113
135
|
preLogin: PreLoginPayload;
|
|
@@ -128,6 +150,9 @@ declare module '@luckystack/core' {
|
|
|
128
150
|
preEmailChange: PreEmailChangePayload;
|
|
129
151
|
postEmailChangeRequested: PostEmailChangeRequestedPayload;
|
|
130
152
|
postEmailChanged: PostEmailChangedPayload;
|
|
153
|
+
loginFailed: LoginFailedPayload;
|
|
154
|
+
preAccountDelete: PreAccountDeletePayload;
|
|
155
|
+
postAccountDelete: PostAccountDeletePayload;
|
|
131
156
|
}
|
|
132
157
|
}
|
|
133
158
|
|
|
@@ -137,23 +162,48 @@ declare module '@luckystack/core' {
|
|
|
137
162
|
* @param token - The session token (unique identifier)
|
|
138
163
|
* @param data - The session data to store
|
|
139
164
|
* @param newUser - If true, this is a new login (triggers single-session enforcement)
|
|
165
|
+
* @param options.supersedeToken - A token belonging to the SAME browser that is
|
|
166
|
+
* being replaced by this login (e.g. re-login while already signed in). It is
|
|
167
|
+
* excluded from single-session enforcement so the user's current device is not
|
|
168
|
+
* kicked by its own new session. The caller is responsible for deleting it
|
|
169
|
+
* afterwards (use `deleteSession(supersedeToken, { skipSocketLogout: true })`).
|
|
140
170
|
*/
|
|
141
|
-
declare const saveSession: (token: string, data: BaseSessionLayout, newUser?: boolean
|
|
171
|
+
declare const saveSession: (token: string, data: BaseSessionLayout, newUser?: boolean, options?: {
|
|
172
|
+
supersedeToken?: string;
|
|
173
|
+
}) => Promise<{
|
|
174
|
+
ok: true;
|
|
175
|
+
} | {
|
|
176
|
+
ok: false;
|
|
177
|
+
errorCode: string;
|
|
178
|
+
}>;
|
|
142
179
|
/**
|
|
143
180
|
* Retrieve a user session through the active session adapter.
|
|
144
181
|
*
|
|
145
182
|
* @param token - The session token
|
|
183
|
+
* @param options.refresh - When false, skip the sliding-expiration TTL
|
|
184
|
+
* extension. Defaults to true. Pass false for read-only probes (e.g. a
|
|
185
|
+
* background audit, an admin lookup) where a stolen token should NOT earn
|
|
186
|
+
* extra lifetime just because the server touched it (LOGIN-F9).
|
|
146
187
|
* @returns The session data or null if not found
|
|
147
188
|
*/
|
|
148
|
-
declare const getSession: (token: string | null
|
|
189
|
+
declare const getSession: (token: string | null, options?: {
|
|
190
|
+
refresh?: boolean;
|
|
191
|
+
}) => Promise<BaseSessionLayout | null>;
|
|
149
192
|
/**
|
|
150
193
|
* Delete a user session through the active adapter and notify connected
|
|
151
194
|
* clients.
|
|
152
195
|
*
|
|
153
196
|
* @param token - The session token to delete
|
|
197
|
+
* @param options.skipSocketLogout - When true, the session data + active-token
|
|
198
|
+
* tracking are cleaned up and the pre/post delete hooks still fire, but NO
|
|
199
|
+
* `logout` is emitted to sockets in the token's room. Use this when the SAME
|
|
200
|
+
* browser is replacing its session (re-login): emitting a logout would bounce
|
|
201
|
+
* that browser to the login page and null its brand-new session.
|
|
154
202
|
* @returns true if successful
|
|
155
203
|
*/
|
|
156
|
-
declare const deleteSession: (token: string
|
|
204
|
+
declare const deleteSession: (token: string, options?: {
|
|
205
|
+
skipSocketLogout?: boolean;
|
|
206
|
+
}) => Promise<boolean>;
|
|
157
207
|
/**
|
|
158
208
|
* Get all active sessions (admin utility). Returns empty when the active
|
|
159
209
|
* adapter doesn't implement enumeration (signed-JWT-stateless, log-only).
|
|
@@ -202,10 +252,25 @@ interface SessionAdapter {
|
|
|
202
252
|
trackActive(userId: string, token: string, ttlSeconds: number): Promise<void>;
|
|
203
253
|
untrackActive(userId: string, token: string): Promise<void>;
|
|
204
254
|
listActive(userId: string): Promise<string[]>;
|
|
255
|
+
/**
|
|
256
|
+
* Refresh the TTL on the active-tokens-per-user record WITHOUT adding a
|
|
257
|
+
* token (sliding-window expiration parity with the session key). Called on
|
|
258
|
+
* an authenticated read so a session kept alive purely by reads does not
|
|
259
|
+
* outlive its activeUsers entry — which would otherwise let a stolen token
|
|
260
|
+
* survive a sign-out-everywhere / password reset. Optional: backends whose
|
|
261
|
+
* active-set entries do not expire independently (or that have no TTL
|
|
262
|
+
* concept) can omit it. Must not create a record on miss.
|
|
263
|
+
*/
|
|
264
|
+
touchActive?(userId: string, ttlSeconds: number): Promise<void>;
|
|
205
265
|
/**
|
|
206
266
|
* Admin walk: yield every active session record. Optional because
|
|
207
267
|
* non-scannable backends (signed-JWT-stateless, log-only) cannot
|
|
208
268
|
* enumerate. Callers fall back to a per-user listActive when omitted.
|
|
269
|
+
*
|
|
270
|
+
* **Dev/admin-only (LOGIN-F14).** Implementations scan the full keyspace and
|
|
271
|
+
* load every session into memory in one call — O(all sessions) in both Redis
|
|
272
|
+
* round-trips and memory. Do NOT call on the hot request path or from any
|
|
273
|
+
* user-facing API. Use `listActive(userId)` for per-user lookups.
|
|
209
274
|
*/
|
|
210
275
|
listAll?(): Promise<{
|
|
211
276
|
token: string;
|
|
@@ -230,97 +295,74 @@ declare const registerSessionAdapter: (adapter: SessionAdapter) => void;
|
|
|
230
295
|
/** Read the currently-active session adapter (defaults to Redis). */
|
|
231
296
|
declare const getSessionAdapter: () => SessionAdapter;
|
|
232
297
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
password?: string | null;
|
|
244
|
-
provider?: string | null;
|
|
245
|
-
/** Most recent successful login. Mirrors the optional `lastLogin` column on User. */
|
|
246
|
-
lastLogin?: Date | null;
|
|
247
|
-
}
|
|
248
|
-
interface UserAdapter {
|
|
249
|
-
findByEmail: (params: {
|
|
250
|
-
email: string;
|
|
251
|
-
provider: string;
|
|
252
|
-
}) => Promise<UserRecord | null>;
|
|
253
|
-
findById: (id: string) => Promise<UserRecord | null>;
|
|
254
|
-
create: (input: UserAdapterCreateInput) => Promise<UserRecord>;
|
|
255
|
-
update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;
|
|
256
|
-
}
|
|
257
|
-
declare const registerUserAdapter: (adapter: UserAdapter) => UserAdapter;
|
|
258
|
-
declare const isUserAdapterRegistered: () => boolean;
|
|
259
|
-
declare const defaultPrismaUserAdapter: () => UserAdapter;
|
|
260
|
-
declare const getUserAdapter: () => UserAdapter;
|
|
298
|
+
type SessionSanitizer = (session: BaseSessionLayout) => BaseSessionLayout;
|
|
299
|
+
/**
|
|
300
|
+
* Register a redactor applied to the session record before it is persisted to
|
|
301
|
+
* the store and broadcast to connected clients. Call once at boot. Last write
|
|
302
|
+
* wins. Pass a function that returns a shallow copy with sensitive fields
|
|
303
|
+
* stripped, e.g. `({ totpSecret, stripeCustomerId, ...safe }) => safe`.
|
|
304
|
+
*/
|
|
305
|
+
declare const registerSessionSanitizer: (sanitizer: SessionSanitizer) => void;
|
|
306
|
+
/** Read the active sanitizer (or `null` when none is registered). */
|
|
307
|
+
declare const getSessionSanitizer: () => SessionSanitizer | null;
|
|
261
308
|
|
|
262
|
-
interface
|
|
309
|
+
interface LoginOrRegisterParams {
|
|
263
310
|
email?: string;
|
|
264
311
|
password?: string;
|
|
265
312
|
name?: string;
|
|
266
313
|
confirmPassword?: string;
|
|
267
314
|
}
|
|
268
|
-
|
|
315
|
+
interface CredentialsLoginSuccess {
|
|
316
|
+
status: true;
|
|
317
|
+
reason: string;
|
|
318
|
+
newToken: string;
|
|
319
|
+
session: BaseSessionLayout;
|
|
320
|
+
}
|
|
321
|
+
interface CredentialsLoginFailure {
|
|
322
|
+
status: false;
|
|
323
|
+
reason: string;
|
|
324
|
+
}
|
|
325
|
+
type CredentialsLoginResult = CredentialsLoginSuccess | CredentialsLoginFailure;
|
|
326
|
+
declare const OAUTH_STATE_COOKIE_NAME = "ls-oauth-state";
|
|
327
|
+
interface CreateOAuthStateResult {
|
|
328
|
+
/** Opaque `state` param put on the authorize URL + echoed back on callback. */
|
|
329
|
+
state: string;
|
|
330
|
+
/**
|
|
331
|
+
* Random nonce the server must set as the value of the {@link OAUTH_STATE_COOKIE_NAME}
|
|
332
|
+
* cookie (HttpOnly, SameSite=Lax, short TTL) at the authorize redirect. The
|
|
333
|
+
* callback requires this back to accept the flow (browser-binding, F1).
|
|
334
|
+
*/
|
|
335
|
+
stateCookie: string;
|
|
336
|
+
/**
|
|
337
|
+
* PKCE `code_challenge` (S256) when the provider opted into PKCE — the server
|
|
338
|
+
* appends `code_challenge=<value>&code_challenge_method=S256` to the authorize
|
|
339
|
+
* URL. `undefined` for non-PKCE providers (default).
|
|
340
|
+
*/
|
|
341
|
+
codeChallenge?: string;
|
|
342
|
+
}
|
|
343
|
+
declare const createOAuthState: (providerName: string, options?: {
|
|
344
|
+
usePkce?: boolean;
|
|
345
|
+
returnUrl?: string;
|
|
346
|
+
}) => Promise<CreateOAuthStateResult | null>;
|
|
269
347
|
declare const registerWithCredentials: ({ email, password, name, confirmPassword, }: {
|
|
270
348
|
email: string;
|
|
271
349
|
password: string;
|
|
272
350
|
name: string;
|
|
273
351
|
confirmPassword: string;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
session?: undefined;
|
|
278
|
-
} | {
|
|
279
|
-
status: boolean;
|
|
280
|
-
reason: string;
|
|
281
|
-
session: Omit<UserRecord, "password">;
|
|
282
|
-
}>;
|
|
352
|
+
}, options?: {
|
|
353
|
+
supersedeToken?: string;
|
|
354
|
+
}) => Promise<CredentialsLoginResult>;
|
|
283
355
|
declare const loginWithCredentialsCore: ({ email, password }: {
|
|
284
356
|
email: string;
|
|
285
357
|
password: string;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
newToken?: undefined;
|
|
295
|
-
session?: undefined;
|
|
296
|
-
} | {
|
|
297
|
-
status: boolean;
|
|
298
|
-
reason: string;
|
|
299
|
-
newToken: string;
|
|
300
|
-
session: BaseSessionLayout;
|
|
301
|
-
}>;
|
|
302
|
-
declare const loginWithCredentials: (params: paramsType) => Promise<{
|
|
303
|
-
status: false;
|
|
304
|
-
reason: string;
|
|
305
|
-
} | {
|
|
306
|
-
status: boolean;
|
|
307
|
-
reason: string;
|
|
308
|
-
session?: undefined;
|
|
309
|
-
} | {
|
|
310
|
-
status: boolean;
|
|
311
|
-
reason: string;
|
|
312
|
-
session: Omit<UserRecord, "password">;
|
|
313
|
-
} | {
|
|
314
|
-
status: boolean;
|
|
315
|
-
reason: Error;
|
|
316
|
-
newToken?: undefined;
|
|
317
|
-
session?: undefined;
|
|
318
|
-
} | {
|
|
319
|
-
status: boolean;
|
|
320
|
-
reason: string;
|
|
321
|
-
newToken: string;
|
|
322
|
-
session: BaseSessionLayout;
|
|
323
|
-
}>;
|
|
358
|
+
}, options?: {
|
|
359
|
+
supersedeToken?: string;
|
|
360
|
+
requesterIp?: string;
|
|
361
|
+
}) => Promise<CredentialsLoginResult>;
|
|
362
|
+
declare const loginWithCredentials: (params: LoginOrRegisterParams, options?: {
|
|
363
|
+
supersedeToken?: string;
|
|
364
|
+
requesterIp?: string;
|
|
365
|
+
}) => Promise<CredentialsLoginResult>;
|
|
324
366
|
interface OAuthCallbackResult {
|
|
325
367
|
token: string;
|
|
326
368
|
redirectUrl: string;
|
|
@@ -330,6 +372,7 @@ interface OAuthCallbackResult {
|
|
|
330
372
|
}
|
|
331
373
|
declare const loginCallback: (pathname: string, req: IncomingMessage, _res: ServerResponse, options?: {
|
|
332
374
|
defaultRedirectUrl?: string;
|
|
375
|
+
supersedeToken?: string;
|
|
333
376
|
}) => Promise<OAuthCallbackResult | false>;
|
|
334
377
|
|
|
335
378
|
declare const logout: ({ token, socket, userId, skipSessionDelete }: {
|
|
@@ -340,15 +383,38 @@ declare const logout: ({ token, socket, userId, skipSessionDelete }: {
|
|
|
340
383
|
}) => Promise<void>;
|
|
341
384
|
|
|
342
385
|
/**
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
386
|
+
* Whether `accountKey` is currently locked out (failed-attempt counter already
|
|
387
|
+
* at/over the cap). Non-incrementing — safe to call on every login attempt.
|
|
388
|
+
* Always `false` when the feature is disabled.
|
|
389
|
+
* Pass `requesterIp` to check the IP+account composite bucket (DD-LOGIN-F5).
|
|
390
|
+
*/
|
|
391
|
+
declare const isAccountLocked: (accountKey: string, requesterIp?: string) => Promise<boolean>;
|
|
392
|
+
/**
|
|
393
|
+
* Increment the failed-attempt counter for `accountKey`. Call on every failed
|
|
394
|
+
* credentials login for a known/looked-up account. No-op when disabled.
|
|
395
|
+
* Pass `requesterIp` to increment the IP+account composite bucket (DD-LOGIN-F5).
|
|
396
|
+
*/
|
|
397
|
+
declare const recordAuthFailure: (accountKey: string, requesterIp?: string) => Promise<void>;
|
|
398
|
+
/**
|
|
399
|
+
* Reset the failed-attempt counter for `accountKey` (call after a SUCCESSFUL
|
|
400
|
+
* login so a user who eventually gets their password right isn't penalised by
|
|
401
|
+
* earlier typos). No-op when disabled.
|
|
402
|
+
* Pass `requesterIp` to clear the IP+account composite bucket (DD-LOGIN-F5).
|
|
403
|
+
*/
|
|
404
|
+
declare const clearAuthFailures: (accountKey: string, requesterIp?: string) => Promise<void>;
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Create a one-time password-reset token bound to a user id. Any previously
|
|
408
|
+
* issued (but not yet consumed) token for the same user is invalidated first
|
|
409
|
+
* (LOGIN-F16). Stored in Redis with the configured TTL — only `sha256(token)`
|
|
410
|
+
* is persisted (hash-at-rest). Returns the raw token string — caller emails it
|
|
411
|
+
* to the user (typically embedded in a URL).
|
|
346
412
|
*/
|
|
347
413
|
declare const createPasswordResetToken: (userId: string) => Promise<string>;
|
|
348
414
|
/**
|
|
349
415
|
* Validate and consume a password-reset token. Returns the bound userId on
|
|
350
|
-
* success and removes the token (one-time use). Returns null
|
|
351
|
-
* is missing, expired, or
|
|
416
|
+
* success and removes the token (one-time use, atomic GET+DEL). Returns null
|
|
417
|
+
* when the token is missing, expired, malformed, or already consumed.
|
|
352
418
|
*/
|
|
353
419
|
declare const consumePasswordResetToken: (token: string) => Promise<string | null>;
|
|
354
420
|
/**
|
|
@@ -358,6 +424,20 @@ declare const consumePasswordResetToken: (token: string) => Promise<string | nul
|
|
|
358
424
|
* (`projectConfig.auth.passwordPolicy`) before hashing — throws a `PasswordPolicyError`
|
|
359
425
|
* when the plaintext fails policy. Catch and surface the error code to the
|
|
360
426
|
* client; framework-mode reset/change flows already do this.
|
|
427
|
+
*
|
|
428
|
+
* ADR — DD-LOGIN-F17: this primitive intentionally does NOT revoke sessions.
|
|
429
|
+
* Session revocation is the responsibility of each calling route because the
|
|
430
|
+
* right behaviour differs per use-case:
|
|
431
|
+
* - `reset-password/confirmReset_v1` revokes ALL sessions (`revokeUserSessions(userId, null)`)
|
|
432
|
+
* because a forgot-password reset means the old credential is compromised.
|
|
433
|
+
* - `settings/changePassword_v1` revokes every OTHER session but keeps the
|
|
434
|
+
* current one active (`revokeUserSessions(userId, user.token)`) so the
|
|
435
|
+
* user is not kicked from the device they changed the password on.
|
|
436
|
+
* - Custom consumers may want a third behaviour (e.g. re-use the existing
|
|
437
|
+
* token after an admin password-force) — keeping the primitive pure lets
|
|
438
|
+
* them compose it freely.
|
|
439
|
+
* If you call this function directly, YOU must call `revokeUserSessions`
|
|
440
|
+
* afterwards unless you have a deliberate reason not to.
|
|
361
441
|
*/
|
|
362
442
|
declare const updatePasswordHash: (userId: string, plaintext: string) => Promise<void>;
|
|
363
443
|
/**
|
|
@@ -408,15 +488,19 @@ interface EmailChangePayload {
|
|
|
408
488
|
}
|
|
409
489
|
/**
|
|
410
490
|
* Create a one-time email-change token bound to a user id + the new email
|
|
411
|
-
* address.
|
|
412
|
-
*
|
|
491
|
+
* address. Any previously issued (but not yet consumed) token for the same
|
|
492
|
+
* user is invalidated first (LOGIN-F16) so at most one confirmation link per
|
|
493
|
+
* user is redeemable. Stored in Redis with the configured TTL — only
|
|
494
|
+
* `sha256(token)` is persisted (hash-at-rest). Returns the raw token string —
|
|
495
|
+
* caller emails the token (embedded in a URL) to the NEW address so the
|
|
413
496
|
* confirmation click proves ownership of the new mailbox.
|
|
414
497
|
*/
|
|
415
498
|
declare const createEmailChangeToken: (userId: string, newEmail: string) => Promise<string>;
|
|
416
499
|
/**
|
|
417
500
|
* Validate and consume an email-change token. Returns the bound
|
|
418
|
-
* `{ userId, newEmail }` on success and removes the token (one-time use
|
|
419
|
-
* Returns null when the token is missing, expired, or
|
|
501
|
+
* `{ userId, newEmail }` on success and removes the token (one-time use, atomic
|
|
502
|
+
* GET+DEL). Returns null when the token is missing, expired, malformed, or
|
|
503
|
+
* already consumed.
|
|
420
504
|
*/
|
|
421
505
|
declare const consumeEmailChangeToken: (token: string) => Promise<EmailChangePayload | null>;
|
|
422
506
|
|
|
@@ -445,6 +529,7 @@ declare const sendEmailChangeConfirmation: ({ userId, newEmail, userName, brand
|
|
|
445
529
|
}>;
|
|
446
530
|
|
|
447
531
|
type OAuthUserData = Record<string, unknown>;
|
|
532
|
+
declare const asOAuthUserData: (value: unknown) => OAuthUserData;
|
|
448
533
|
interface CredentialsProvider {
|
|
449
534
|
name: 'credentials';
|
|
450
535
|
}
|
|
@@ -471,6 +556,15 @@ interface FullOAuthProvider {
|
|
|
471
556
|
scope: string[];
|
|
472
557
|
nameKey: string;
|
|
473
558
|
emailKey: string;
|
|
559
|
+
/**
|
|
560
|
+
* Optional key on the userInfo response holding a provider "email verified"
|
|
561
|
+
* boolean. When set AND the field is present-and-falsy, the framework rejects
|
|
562
|
+
* the profile rather than trusting an unverified address — defense-in-depth
|
|
563
|
+
* against account-linking takeover under the `'unified'` strategy (SEC-21).
|
|
564
|
+
* A missing field (provider doesn't report it) is treated as "trusted",
|
|
565
|
+
* preserving behaviour for providers that don't expose the flag.
|
|
566
|
+
*/
|
|
567
|
+
emailVerifiedKey?: string;
|
|
474
568
|
avatarKey?: string;
|
|
475
569
|
avatarCodeKey: string;
|
|
476
570
|
getEmail?: (accessToken: string) => Promise<string | false | undefined>;
|
|
@@ -501,6 +595,25 @@ interface FullOAuthProvider {
|
|
|
501
595
|
userData: OAuthUserData;
|
|
502
596
|
accessToken: string;
|
|
503
597
|
}) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
598
|
+
/**
|
|
599
|
+
* Extra query params appended to the authorization-redirect URL (CFG-21).
|
|
600
|
+
* Lets a provider request e.g. `access_type=offline`, `login_hint=...`, or
|
|
601
|
+
* override the framework's default `prompt=select_account`. The server's
|
|
602
|
+
* authorize route merges these over the built-in params (a key present here
|
|
603
|
+
* wins). Reserved OAuth params (`client_id`, `redirect_uri`, `scope`,
|
|
604
|
+
* `response_type`, `state`, `code_challenge`, `code_challenge_method`) should
|
|
605
|
+
* NOT be set here — they are framework-owned. Default unset.
|
|
606
|
+
*/
|
|
607
|
+
extraAuthorizationParams?: Record<string, string>;
|
|
608
|
+
/**
|
|
609
|
+
* Opt this provider into PKCE (RFC 7636, S256) (F11). When `true`, the
|
|
610
|
+
* framework generates a `code_verifier` at flow start, stores it server-side
|
|
611
|
+
* with the OAuth state, sends `code_challenge`/`code_challenge_method=S256` on
|
|
612
|
+
* the authorize redirect, and replays the verifier at token exchange. Required
|
|
613
|
+
* by OAuth 2.1 / PKCE-mandating providers (X/Twitter, some Okta/Auth0
|
|
614
|
+
* policies). Default `false` (no PKCE — existing flows are byte-identical).
|
|
615
|
+
*/
|
|
616
|
+
usePkce?: boolean;
|
|
504
617
|
}
|
|
505
618
|
type OAuthProvider = CredentialsProvider | FullOAuthProvider;
|
|
506
619
|
declare const isFullOAuthProvider: (provider: OAuthProvider) => provider is FullOAuthProvider;
|
|
@@ -530,6 +643,13 @@ interface OAuthHelperInput {
|
|
|
530
643
|
* pattern.
|
|
531
644
|
*/
|
|
532
645
|
extraSessionFields?: FullOAuthProvider['extraSessionFields'];
|
|
646
|
+
/**
|
|
647
|
+
* Extra query params for the authorize redirect (CFG-21). See
|
|
648
|
+
* `FullOAuthProvider.extraAuthorizationParams`.
|
|
649
|
+
*/
|
|
650
|
+
extraAuthorizationParams?: Record<string, string>;
|
|
651
|
+
/** Opt this provider into PKCE (S256) (F11). See `FullOAuthProvider.usePkce`. */
|
|
652
|
+
usePkce?: boolean;
|
|
533
653
|
}
|
|
534
654
|
declare const credentialsProvider: () => CredentialsProvider;
|
|
535
655
|
declare const googleProvider: (input: OAuthHelperInput) => FullOAuthProvider;
|
|
@@ -547,10 +667,81 @@ interface MicrosoftProviderInput extends OAuthHelperInput {
|
|
|
547
667
|
apiVersion?: string;
|
|
548
668
|
/** Graph API version segment for /me. Default `'v1.0'`. */
|
|
549
669
|
graphApiVersion?: string;
|
|
670
|
+
/**
|
|
671
|
+
* Allow falling back to `userPrincipalName` as the account email when Graph
|
|
672
|
+
* `/me.mail` is empty (SEC-11). A UPN is NOT guaranteed routable or verified;
|
|
673
|
+
* under the `'unified'` account strategy a UPN equal to a victim's email would
|
|
674
|
+
* link accounts, and it commonly mis-keys an account on a non-deliverable
|
|
675
|
+
* address (reset emails never arrive). Default `false` — only enable when the
|
|
676
|
+
* tenant guarantees UPN == primary SMTP. When `false` and `.mail` is empty the
|
|
677
|
+
* provider reports "no email" and the login is rejected rather than linked.
|
|
678
|
+
*/
|
|
679
|
+
allowUpnFallback?: boolean;
|
|
550
680
|
}
|
|
551
681
|
declare const microsoftProvider: (input: MicrosoftProviderInput) => FullOAuthProvider;
|
|
552
682
|
declare const getOAuthProviders: () => OAuthProvider[];
|
|
553
683
|
declare const registerOAuthProviders: (providers: OAuthProvider[]) => OAuthProvider[];
|
|
684
|
+
/**
|
|
685
|
+
* Register a factory function that builds the provider list on first use.
|
|
686
|
+
* Prefer this over `registerOAuthProviders` from the package `register` side-
|
|
687
|
+
* effect so config/env reads are deferred to request time and survive any
|
|
688
|
+
* `registerProjectConfig` call that happens after package import.
|
|
689
|
+
* A subsequent `registerOAuthProviders` call replaces the factory.
|
|
690
|
+
*/
|
|
691
|
+
declare const registerOAuthProviderFactory: (factory: () => OAuthProvider[]) => void;
|
|
692
|
+
|
|
693
|
+
interface UserAdapterCreateInput {
|
|
694
|
+
email: string;
|
|
695
|
+
provider: string;
|
|
696
|
+
name?: string | null;
|
|
697
|
+
password?: string | null;
|
|
698
|
+
avatar?: string | null;
|
|
699
|
+
avatarFallback?: string | null;
|
|
700
|
+
language?: string | null;
|
|
701
|
+
}
|
|
702
|
+
interface UserRecord extends BaseSessionLayout {
|
|
703
|
+
password?: string | null;
|
|
704
|
+
provider?: string | null;
|
|
705
|
+
/** Most recent successful login. Mirrors the optional `lastLogin` column on User. */
|
|
706
|
+
lastLogin?: Date | null;
|
|
707
|
+
}
|
|
708
|
+
interface UserAdapter {
|
|
709
|
+
findByEmail: (params: {
|
|
710
|
+
email: string;
|
|
711
|
+
provider: string;
|
|
712
|
+
}) => Promise<UserRecord | null>;
|
|
713
|
+
/**
|
|
714
|
+
* Look up a user by email IRRESPECTIVE of provider. Required only when
|
|
715
|
+
* `auth.providerAccountStrategy === 'unified'` — the framework resolves
|
|
716
|
+
* accounts by email alone so the same address maps to one User across
|
|
717
|
+
* providers. Optional so existing custom adapters keep compiling; when a
|
|
718
|
+
* project sets `'unified'` but its adapter omits this method, the framework
|
|
719
|
+
* logs a one-time warning and falls back to provider-scoped lookup.
|
|
720
|
+
*/
|
|
721
|
+
findByEmailAnyProvider?: (params: {
|
|
722
|
+
email: string;
|
|
723
|
+
}) => Promise<UserRecord | null>;
|
|
724
|
+
findById: (id: string) => Promise<UserRecord | null>;
|
|
725
|
+
create: (input: UserAdapterCreateInput) => Promise<UserRecord>;
|
|
726
|
+
update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;
|
|
727
|
+
/**
|
|
728
|
+
* Hard-delete a user. Account-deletion flows (`deleteAccount_v1`) call this
|
|
729
|
+
* instead of reaching past the adapter to `prisma.user.delete`, so a custom
|
|
730
|
+
* adapter / non-Prisma store / soft-delete strategy stays in control. To
|
|
731
|
+
* implement a soft delete, ignore the hard delete here and instead expose it
|
|
732
|
+
* through `update({ deletedAt: new Date() })` from your route.
|
|
733
|
+
*
|
|
734
|
+
* Optional so existing custom adapters keep compiling; the default Prisma
|
|
735
|
+
* adapter implements it. Consumers calling account deletion against a custom
|
|
736
|
+
* adapter that omits `delete` get a clear "adapter does not support delete"
|
|
737
|
+
* failure rather than a silent Prisma bypass.
|
|
738
|
+
*/
|
|
739
|
+
delete?: (id: string) => Promise<void>;
|
|
740
|
+
}
|
|
741
|
+
declare const registerUserAdapter: (adapter: UserAdapter) => UserAdapter;
|
|
742
|
+
declare const isUserAdapterRegistered: () => boolean;
|
|
743
|
+
declare const defaultPrismaUserAdapter: () => UserAdapter;
|
|
744
|
+
declare const getUserAdapter: () => UserAdapter;
|
|
554
745
|
|
|
555
746
|
interface PostLoginRedirectInput {
|
|
556
747
|
userId: string;
|
|
@@ -562,4 +753,4 @@ type PostLoginRedirectResolver = (input: PostLoginRedirectInput) => string | Pro
|
|
|
562
753
|
declare const registerPostLoginRedirect: (resolver: PostLoginRedirectResolver) => PostLoginRedirectResolver;
|
|
563
754
|
declare const getPostLoginRedirect: () => PostLoginRedirectResolver | null;
|
|
564
755
|
|
|
565
|
-
export { type CredentialsProvider, type EmailChangePayload, type FullOAuthProvider, type OAuthProvider, PasswordPolicyError, type PostEmailChangeRequestedPayload, type PostEmailChangedPayload, type PostLoginPayload, type PostLoginRedirectInput, type PostLoginRedirectResolver, type PostLogoutPayload, type PostRegisterPayload, type PostSessionCreatePayload, type PostSessionDeletePayload, type PreEmailChangePayload, type PreLoginPayload, type PreLogoutPayload, type PreRegisterPayload, type PreSessionCreatePayload, type PreSessionDeletePayload, type SessionAdapter, type UserAdapter, type UserAdapterCreateInput, type UserRecord, activeUsersKeyFor, consumeEmailChangeToken, consumePasswordResetToken, createEmailChangeToken, createOAuthState, createPasswordResetToken, credentialsProvider, defaultPrismaUserAdapter, deleteSession, discordProvider, facebookProvider, getAllSessions, getOAuthProviders, getPostLoginRedirect, getSession, getSessionAdapter, getUserAdapter, githubProvider, googleProvider, isFullOAuthProvider, isUserAdapterRegistered, loginCallback, loginWithCredentials, loginWithCredentialsCore, logout, microsoftProvider, redisSessionAdapter, registerOAuthProviders, registerPostLoginRedirect, registerSessionAdapter, registerUserAdapter, registerWithCredentials, revokeUserSessions, saveSession, sendEmailChangeConfirmation, sendPasswordResetEmail, sessionKeyFor, updatePasswordHash, validatePassword, verifyPassword };
|
|
756
|
+
export { type CreateOAuthStateResult, type CredentialsLoginFailure, type CredentialsLoginResult, type CredentialsLoginSuccess, type CredentialsProvider, type EmailChangePayload, type FullOAuthProvider, type LoginFailedPayload, OAUTH_STATE_COOKIE_NAME, type OAuthCallbackResult, type OAuthProvider, type OAuthUserData, PasswordPolicyError, type PostAccountDeletePayload, type PostEmailChangeRequestedPayload, type PostEmailChangedPayload, type PostLoginPayload, type PostLoginRedirectInput, type PostLoginRedirectResolver, type PostLogoutPayload, type PostRegisterPayload, type PostSessionCreatePayload, type PostSessionDeletePayload, type PreAccountDeletePayload, type PreEmailChangePayload, type PreLoginPayload, type PreLogoutPayload, type PreRegisterPayload, type PreSessionCreatePayload, type PreSessionDeletePayload, type SessionAdapter, type SessionSanitizer, type UserAdapter, type UserAdapterCreateInput, type UserRecord, activeUsersKeyFor, asOAuthUserData, clearAuthFailures, consumeEmailChangeToken, consumePasswordResetToken, createEmailChangeToken, createOAuthState, createPasswordResetToken, credentialsProvider, defaultPrismaUserAdapter, deleteSession, discordProvider, facebookProvider, getAllSessions, getOAuthProviders, getPostLoginRedirect, getSession, getSessionAdapter, getSessionSanitizer, getUserAdapter, githubProvider, googleProvider, isAccountLocked, isFullOAuthProvider, isUserAdapterRegistered, loginCallback, loginWithCredentials, loginWithCredentialsCore, logout, microsoftProvider, recordAuthFailure, redisSessionAdapter, registerOAuthProviderFactory, registerOAuthProviders, registerPostLoginRedirect, registerSessionAdapter, registerSessionSanitizer, registerUserAdapter, registerWithCredentials, revokeUserSessions, saveSession, sendEmailChangeConfirmation, sendPasswordResetEmail, sessionKeyFor, updatePasswordHash, validatePassword, verifyPassword };
|