@luckystack/login 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/CHANGELOG.md +14 -0
- package/CLAUDE.md +194 -0
- package/LICENSE +21 -0
- package/README.md +212 -0
- package/dist/index.d.ts +565 -0
- package/dist/index.js +1497 -0
- package/dist/index.js.map +1 -0
- package/docs/credentials-auth.md +219 -0
- package/docs/hooks.md +444 -0
- package/docs/logout-flow.md +235 -0
- package/docs/oauth-providers.md +431 -0
- package/docs/password-reset.md +343 -0
- package/docs/redirect-validation.md +390 -0
- package/docs/session-management.md +286 -0
- package/docs/user-adapter.md +353 -0
- package/package.json +70 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
import { BaseSessionLayout } from '@luckystack/core';
|
|
2
|
+
export { AuthProps, BaseSessionLayout, SessionLocation } from '@luckystack/core';
|
|
3
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
|
+
import { Socket } from 'socket.io';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Module augmentation: extends `@luckystack/core`'s `HookPayloads` interface
|
|
8
|
+
* with auth and session lifecycle hooks owned by this package.
|
|
9
|
+
*
|
|
10
|
+
* This file is picked up by the TypeScript compiler because it is in the
|
|
11
|
+
* tsconfig `include` path. A side-effect `import './hookPayloads';` in
|
|
12
|
+
* `index.ts` makes the dependency explicit for readers even though TS would
|
|
13
|
+
* merge the declaration regardless.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
interface PreLoginPayload {
|
|
17
|
+
email: string;
|
|
18
|
+
provider: string;
|
|
19
|
+
}
|
|
20
|
+
interface PostLoginPayload {
|
|
21
|
+
userId: string;
|
|
22
|
+
provider: string;
|
|
23
|
+
isNewUser: boolean;
|
|
24
|
+
token: string;
|
|
25
|
+
}
|
|
26
|
+
interface PreRegisterPayload {
|
|
27
|
+
email: string;
|
|
28
|
+
provider: string;
|
|
29
|
+
name?: string;
|
|
30
|
+
}
|
|
31
|
+
interface PostRegisterPayload {
|
|
32
|
+
userId: string;
|
|
33
|
+
provider: string;
|
|
34
|
+
}
|
|
35
|
+
interface PreLogoutPayload {
|
|
36
|
+
userId: string | null;
|
|
37
|
+
token: string | null;
|
|
38
|
+
}
|
|
39
|
+
interface PostLogoutPayload {
|
|
40
|
+
userId: string | null;
|
|
41
|
+
token: string | null;
|
|
42
|
+
}
|
|
43
|
+
interface PreSessionCreatePayload {
|
|
44
|
+
token: string;
|
|
45
|
+
user: BaseSessionLayout;
|
|
46
|
+
persistent: boolean;
|
|
47
|
+
}
|
|
48
|
+
interface PostSessionCreatePayload {
|
|
49
|
+
token: string;
|
|
50
|
+
user: BaseSessionLayout;
|
|
51
|
+
persistent: boolean;
|
|
52
|
+
}
|
|
53
|
+
interface PreSessionDeletePayload {
|
|
54
|
+
token: string;
|
|
55
|
+
userId: string | null;
|
|
56
|
+
}
|
|
57
|
+
interface PostSessionDeletePayload {
|
|
58
|
+
token: string;
|
|
59
|
+
userId: string | null;
|
|
60
|
+
}
|
|
61
|
+
interface PasswordResetRequestedPayload {
|
|
62
|
+
/** Email the user typed in the reset form. May or may not match a real user. */
|
|
63
|
+
email: string;
|
|
64
|
+
/** True when a matching user was found and an email is being sent. */
|
|
65
|
+
matched: boolean;
|
|
66
|
+
/** The userId, when matched. */
|
|
67
|
+
userId?: string;
|
|
68
|
+
/** The reset token (for audit; do not log). */
|
|
69
|
+
token?: string;
|
|
70
|
+
/** Token TTL in seconds. */
|
|
71
|
+
ttlSeconds?: number;
|
|
72
|
+
}
|
|
73
|
+
interface PasswordResetCompletedPayload {
|
|
74
|
+
/** User whose password was reset via the forgot-password flow. */
|
|
75
|
+
userId: string;
|
|
76
|
+
/** Whether the framework auto-revoked all other active sessions. */
|
|
77
|
+
revokedOtherSessions: boolean;
|
|
78
|
+
}
|
|
79
|
+
interface PasswordChangedPayload {
|
|
80
|
+
/** User whose password was changed via the in-session change flow. */
|
|
81
|
+
userId: string;
|
|
82
|
+
/** True if the user verified their current password successfully. */
|
|
83
|
+
verifiedCurrent: boolean;
|
|
84
|
+
/** Whether sessions other than the current one were revoked. */
|
|
85
|
+
revokedOtherSessions: boolean;
|
|
86
|
+
}
|
|
87
|
+
interface PrePasswordResetCompletedPayload {
|
|
88
|
+
/** User whose password is about to be reset via the forgot-password flow. */
|
|
89
|
+
userId: string;
|
|
90
|
+
}
|
|
91
|
+
interface PrePasswordChangedPayload {
|
|
92
|
+
/** User whose password is about to be changed via the in-session flow. */
|
|
93
|
+
userId: string;
|
|
94
|
+
/** True if the user verified their current password successfully. */
|
|
95
|
+
verifiedCurrent: boolean;
|
|
96
|
+
}
|
|
97
|
+
interface PreEmailChangePayload {
|
|
98
|
+
userId: string;
|
|
99
|
+
currentEmail: string;
|
|
100
|
+
newEmail: string;
|
|
101
|
+
}
|
|
102
|
+
interface PostEmailChangeRequestedPayload {
|
|
103
|
+
userId: string;
|
|
104
|
+
newEmail: string;
|
|
105
|
+
}
|
|
106
|
+
interface PostEmailChangedPayload {
|
|
107
|
+
userId: string;
|
|
108
|
+
oldEmail: string;
|
|
109
|
+
newEmail: string;
|
|
110
|
+
}
|
|
111
|
+
declare module '@luckystack/core' {
|
|
112
|
+
interface HookPayloads {
|
|
113
|
+
preLogin: PreLoginPayload;
|
|
114
|
+
postLogin: PostLoginPayload;
|
|
115
|
+
preRegister: PreRegisterPayload;
|
|
116
|
+
postRegister: PostRegisterPayload;
|
|
117
|
+
preLogout: PreLogoutPayload;
|
|
118
|
+
postLogout: PostLogoutPayload;
|
|
119
|
+
preSessionCreate: PreSessionCreatePayload;
|
|
120
|
+
postSessionCreate: PostSessionCreatePayload;
|
|
121
|
+
preSessionDelete: PreSessionDeletePayload;
|
|
122
|
+
postSessionDelete: PostSessionDeletePayload;
|
|
123
|
+
passwordResetRequested: PasswordResetRequestedPayload;
|
|
124
|
+
prePasswordResetCompleted: PrePasswordResetCompletedPayload;
|
|
125
|
+
passwordResetCompleted: PasswordResetCompletedPayload;
|
|
126
|
+
prePasswordChanged: PrePasswordChangedPayload;
|
|
127
|
+
passwordChanged: PasswordChangedPayload;
|
|
128
|
+
preEmailChange: PreEmailChangePayload;
|
|
129
|
+
postEmailChangeRequested: PostEmailChangeRequestedPayload;
|
|
130
|
+
postEmailChanged: PostEmailChangedPayload;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Save or update a user session through the active session adapter.
|
|
136
|
+
*
|
|
137
|
+
* @param token - The session token (unique identifier)
|
|
138
|
+
* @param data - The session data to store
|
|
139
|
+
* @param newUser - If true, this is a new login (triggers single-session enforcement)
|
|
140
|
+
*/
|
|
141
|
+
declare const saveSession: (token: string, data: BaseSessionLayout, newUser?: boolean) => Promise<void>;
|
|
142
|
+
/**
|
|
143
|
+
* Retrieve a user session through the active session adapter.
|
|
144
|
+
*
|
|
145
|
+
* @param token - The session token
|
|
146
|
+
* @returns The session data or null if not found
|
|
147
|
+
*/
|
|
148
|
+
declare const getSession: (token: string | null) => Promise<BaseSessionLayout | null>;
|
|
149
|
+
/**
|
|
150
|
+
* Delete a user session through the active adapter and notify connected
|
|
151
|
+
* clients.
|
|
152
|
+
*
|
|
153
|
+
* @param token - The session token to delete
|
|
154
|
+
* @returns true if successful
|
|
155
|
+
*/
|
|
156
|
+
declare const deleteSession: (token: string) => Promise<boolean>;
|
|
157
|
+
/**
|
|
158
|
+
* Get all active sessions (admin utility). Returns empty when the active
|
|
159
|
+
* adapter doesn't implement enumeration (signed-JWT-stateless, log-only).
|
|
160
|
+
*
|
|
161
|
+
* @returns Array of all session data
|
|
162
|
+
*/
|
|
163
|
+
declare const getAllSessions: () => Promise<BaseSessionLayout[]>;
|
|
164
|
+
/**
|
|
165
|
+
* Revoke every active session for a user, optionally keeping one alive.
|
|
166
|
+
*
|
|
167
|
+
* @param userId - The user whose sessions to revoke
|
|
168
|
+
* @param exceptToken - If provided, this session is left untouched. Pass the
|
|
169
|
+
* caller's own token when revoking after a password change so the user
|
|
170
|
+
* isn't logged out of the device they just changed it from. Pass null/omit
|
|
171
|
+
* to revoke everything (account deletion, "sign out everywhere", etc.).
|
|
172
|
+
*
|
|
173
|
+
* Each revocation goes through `deleteSession`, so connected sockets are
|
|
174
|
+
* told to log out via the same flow as a normal logout. Returns the number
|
|
175
|
+
* of sessions actually revoked.
|
|
176
|
+
*/
|
|
177
|
+
declare const revokeUserSessions: (userId: string, exceptToken?: string | null) => Promise<number>;
|
|
178
|
+
declare const sessionKeyFor: (token: string) => string;
|
|
179
|
+
declare const activeUsersKeyFor: (userId: string) => string;
|
|
180
|
+
|
|
181
|
+
interface SessionAdapter {
|
|
182
|
+
/** Human-readable identifier (used in logs/diagnostics). */
|
|
183
|
+
name: string;
|
|
184
|
+
/** Read the raw stored session string for `token`. Null when missing. */
|
|
185
|
+
getRaw(token: string): Promise<string | null>;
|
|
186
|
+
/**
|
|
187
|
+
* Write the raw session string for `token` with a TTL. Implementations
|
|
188
|
+
* MUST honour the TTL — sessions without one would never expire.
|
|
189
|
+
*/
|
|
190
|
+
setRaw(token: string, value: string, ttlSeconds: number): Promise<void>;
|
|
191
|
+
/** Delete the session record for `token`. Idempotent. */
|
|
192
|
+
delete(token: string): Promise<void>;
|
|
193
|
+
/**
|
|
194
|
+
* Refresh the TTL on an existing session record (sliding-window
|
|
195
|
+
* expiration). Returns true when the underlying record existed and the
|
|
196
|
+
* TTL was applied, false otherwise. Implementations must not create a
|
|
197
|
+
* record on miss.
|
|
198
|
+
*/
|
|
199
|
+
expire(token: string, ttlSeconds: number): Promise<boolean>;
|
|
200
|
+
/** Remaining TTL in seconds (or null when the record is missing). */
|
|
201
|
+
ttl(token: string): Promise<number | null>;
|
|
202
|
+
trackActive(userId: string, token: string, ttlSeconds: number): Promise<void>;
|
|
203
|
+
untrackActive(userId: string, token: string): Promise<void>;
|
|
204
|
+
listActive(userId: string): Promise<string[]>;
|
|
205
|
+
/**
|
|
206
|
+
* Admin walk: yield every active session record. Optional because
|
|
207
|
+
* non-scannable backends (signed-JWT-stateless, log-only) cannot
|
|
208
|
+
* enumerate. Callers fall back to a per-user listActive when omitted.
|
|
209
|
+
*/
|
|
210
|
+
listAll?(): Promise<{
|
|
211
|
+
token: string;
|
|
212
|
+
raw: string;
|
|
213
|
+
}[]>;
|
|
214
|
+
}
|
|
215
|
+
declare const redisSessionAdapter: SessionAdapter;
|
|
216
|
+
/**
|
|
217
|
+
* Replace the active session storage backend. Call once at boot from
|
|
218
|
+
* `luckystack/server/index.ts`, BEFORE the first login request. Common
|
|
219
|
+
* use cases:
|
|
220
|
+
*
|
|
221
|
+
* - DynamoDB / Cosmos DB for serverless / edge deployments
|
|
222
|
+
* - Postgres `sessions` table when you already operate Postgres
|
|
223
|
+
* - signed-JWT-stateless (the adapter writes nothing, reads from the
|
|
224
|
+
* JWT, and stores activeUsers per token in a small Redis hash)
|
|
225
|
+
* - in-memory mock for integration tests
|
|
226
|
+
*
|
|
227
|
+
* Last-write-wins: subsequent calls overwrite the active adapter.
|
|
228
|
+
*/
|
|
229
|
+
declare const registerSessionAdapter: (adapter: SessionAdapter) => void;
|
|
230
|
+
/** Read the currently-active session adapter (defaults to Redis). */
|
|
231
|
+
declare const getSessionAdapter: () => SessionAdapter;
|
|
232
|
+
|
|
233
|
+
interface UserAdapterCreateInput {
|
|
234
|
+
email: string;
|
|
235
|
+
provider: string;
|
|
236
|
+
name?: string | null;
|
|
237
|
+
password?: string | null;
|
|
238
|
+
avatar?: string | null;
|
|
239
|
+
avatarFallback?: string | null;
|
|
240
|
+
language?: string | null;
|
|
241
|
+
}
|
|
242
|
+
interface UserRecord extends BaseSessionLayout {
|
|
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;
|
|
261
|
+
|
|
262
|
+
interface paramsType {
|
|
263
|
+
email?: string;
|
|
264
|
+
password?: string;
|
|
265
|
+
name?: string;
|
|
266
|
+
confirmPassword?: string;
|
|
267
|
+
}
|
|
268
|
+
declare const createOAuthState: (providerName: string) => Promise<string | null>;
|
|
269
|
+
declare const registerWithCredentials: ({ email, password, name, confirmPassword, }: {
|
|
270
|
+
email: string;
|
|
271
|
+
password: string;
|
|
272
|
+
name: string;
|
|
273
|
+
confirmPassword: string;
|
|
274
|
+
}) => Promise<{
|
|
275
|
+
status: boolean;
|
|
276
|
+
reason: string;
|
|
277
|
+
session?: undefined;
|
|
278
|
+
} | {
|
|
279
|
+
status: boolean;
|
|
280
|
+
reason: string;
|
|
281
|
+
session: Omit<UserRecord, "password">;
|
|
282
|
+
}>;
|
|
283
|
+
declare const loginWithCredentialsCore: ({ email, password }: {
|
|
284
|
+
email: string;
|
|
285
|
+
password: string;
|
|
286
|
+
}) => Promise<{
|
|
287
|
+
status: boolean;
|
|
288
|
+
reason: string;
|
|
289
|
+
newToken?: undefined;
|
|
290
|
+
session?: undefined;
|
|
291
|
+
} | {
|
|
292
|
+
status: boolean;
|
|
293
|
+
reason: Error;
|
|
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
|
+
}>;
|
|
324
|
+
interface OAuthCallbackResult {
|
|
325
|
+
token: string;
|
|
326
|
+
redirectUrl: string;
|
|
327
|
+
userId: string;
|
|
328
|
+
provider: string;
|
|
329
|
+
isNewUser: boolean;
|
|
330
|
+
}
|
|
331
|
+
declare const loginCallback: (pathname: string, req: IncomingMessage, _res: ServerResponse, options?: {
|
|
332
|
+
defaultRedirectUrl?: string;
|
|
333
|
+
}) => Promise<OAuthCallbackResult | false>;
|
|
334
|
+
|
|
335
|
+
declare const logout: ({ token, socket, userId, skipSessionDelete }: {
|
|
336
|
+
token: string | null;
|
|
337
|
+
socket: Socket | undefined;
|
|
338
|
+
userId: string | null;
|
|
339
|
+
skipSessionDelete?: boolean;
|
|
340
|
+
}) => Promise<void>;
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Create a one-time password-reset token bound to a user id. Stored in Redis
|
|
344
|
+
* with the configured TTL. Returns the token string — caller emails it to
|
|
345
|
+
* the user (typically embedded in a URL).
|
|
346
|
+
*/
|
|
347
|
+
declare const createPasswordResetToken: (userId: string) => Promise<string>;
|
|
348
|
+
/**
|
|
349
|
+
* Validate and consume a password-reset token. Returns the bound userId on
|
|
350
|
+
* success and removes the token (one-time use). Returns null when the token
|
|
351
|
+
* is missing, expired, or malformed.
|
|
352
|
+
*/
|
|
353
|
+
declare const consumePasswordResetToken: (token: string) => Promise<string | null>;
|
|
354
|
+
/**
|
|
355
|
+
* Bcrypt-hash a plaintext password and write it to the user record via the
|
|
356
|
+
* registered user adapter. Used by the reset flow and by the settings
|
|
357
|
+
* password-change flow. Validates against the active password policy
|
|
358
|
+
* (`projectConfig.auth.passwordPolicy`) before hashing — throws a `PasswordPolicyError`
|
|
359
|
+
* when the plaintext fails policy. Catch and surface the error code to the
|
|
360
|
+
* client; framework-mode reset/change flows already do this.
|
|
361
|
+
*/
|
|
362
|
+
declare const updatePasswordHash: (userId: string, plaintext: string) => Promise<void>;
|
|
363
|
+
/**
|
|
364
|
+
* Thrown by `updatePasswordHash` when the supplied plaintext violates the
|
|
365
|
+
* active password policy. `errorCode` matches the i18n reason key the rest
|
|
366
|
+
* of the login flow uses (e.g. `login.passwordRequiresUppercase`).
|
|
367
|
+
*/
|
|
368
|
+
declare class PasswordPolicyError extends Error {
|
|
369
|
+
readonly errorCode: string;
|
|
370
|
+
constructor(errorCode: string);
|
|
371
|
+
}
|
|
372
|
+
/** Verify a plaintext password against a stored bcrypt hash. */
|
|
373
|
+
declare const verifyPassword: (plaintext: string, hash: string) => Promise<boolean>;
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Validate `password` against the active project's password policy.
|
|
377
|
+
* Returns null on success, a string reason-key on the first failure.
|
|
378
|
+
*
|
|
379
|
+
* The policy lives in `projectConfig.auth.passwordPolicy` and is read at
|
|
380
|
+
* call time, so consumers can override defaults via `registerProjectConfig`.
|
|
381
|
+
*/
|
|
382
|
+
declare const validatePassword: (password: string) => string | null;
|
|
383
|
+
|
|
384
|
+
interface SendResetEmailArgs {
|
|
385
|
+
email: string;
|
|
386
|
+
/**
|
|
387
|
+
* App-facing display name used in the email greeting + subject. If omitted,
|
|
388
|
+
* falls back to `projectConfig.auth.passwordResetBrand`, then to `'LuckyStack'`
|
|
389
|
+
* as the absolute fallback. Real consumers should set this on either the
|
|
390
|
+
* call site OR via `projectConfig.auth.passwordResetBrand`.
|
|
391
|
+
*/
|
|
392
|
+
brand?: string;
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* Look up the user by credentials-provider email, generate a reset token,
|
|
396
|
+
* and email them a reset link. Always resolves successfully regardless of
|
|
397
|
+
* whether the email matched a user (anti-enumeration). Returns the result
|
|
398
|
+
* of the email send for diagnostics.
|
|
399
|
+
*/
|
|
400
|
+
declare const sendPasswordResetEmail: ({ email, brand }: SendResetEmailArgs) => Promise<{
|
|
401
|
+
ok: boolean;
|
|
402
|
+
reason?: string;
|
|
403
|
+
}>;
|
|
404
|
+
|
|
405
|
+
interface EmailChangePayload {
|
|
406
|
+
userId: string;
|
|
407
|
+
newEmail: string;
|
|
408
|
+
}
|
|
409
|
+
/**
|
|
410
|
+
* Create a one-time email-change token bound to a user id + the new email
|
|
411
|
+
* address. Stored in Redis with the configured TTL. Returns the token string
|
|
412
|
+
* — caller emails the token (embedded in a URL) to the NEW address so the
|
|
413
|
+
* confirmation click proves ownership of the new mailbox.
|
|
414
|
+
*/
|
|
415
|
+
declare const createEmailChangeToken: (userId: string, newEmail: string) => Promise<string>;
|
|
416
|
+
/**
|
|
417
|
+
* 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 malformed.
|
|
420
|
+
*/
|
|
421
|
+
declare const consumeEmailChangeToken: (token: string) => Promise<EmailChangePayload | null>;
|
|
422
|
+
|
|
423
|
+
interface SendEmailChangeArgs {
|
|
424
|
+
userId: string;
|
|
425
|
+
newEmail: string;
|
|
426
|
+
/** Optional display name used in the email greeting (falls back to "there"). */
|
|
427
|
+
userName?: string | null;
|
|
428
|
+
/**
|
|
429
|
+
* App-facing display name used in the email greeting + subject. Falls back
|
|
430
|
+
* to `projectConfig.auth.passwordResetBrand` (the shared brand label), then
|
|
431
|
+
* to `'LuckyStack'` as the absolute fallback.
|
|
432
|
+
*/
|
|
433
|
+
brand?: string;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Mint a one-shot email-change token and send a confirmation email to the
|
|
437
|
+
* NEW address with the tokenized URL. Returns the send result for diagnostics
|
|
438
|
+
* + the token itself (callers can audit-log it through the
|
|
439
|
+
* `postEmailChangeRequested` hook).
|
|
440
|
+
*/
|
|
441
|
+
declare const sendEmailChangeConfirmation: ({ userId, newEmail, userName, brand }: SendEmailChangeArgs) => Promise<{
|
|
442
|
+
ok: boolean;
|
|
443
|
+
reason?: string;
|
|
444
|
+
token: string;
|
|
445
|
+
}>;
|
|
446
|
+
|
|
447
|
+
type OAuthUserData = Record<string, unknown>;
|
|
448
|
+
interface CredentialsProvider {
|
|
449
|
+
name: 'credentials';
|
|
450
|
+
}
|
|
451
|
+
interface FullOAuthProvider {
|
|
452
|
+
name: string;
|
|
453
|
+
clientID: string;
|
|
454
|
+
clientSecret: string;
|
|
455
|
+
callbackURL: string;
|
|
456
|
+
authorizationURL: string;
|
|
457
|
+
tokenExchangeURL: string;
|
|
458
|
+
tokenExchangeMethod: 'json' | 'form';
|
|
459
|
+
userInfoURL: string;
|
|
460
|
+
/**
|
|
461
|
+
* OAuth scopes requested at the authorization step. Consumers can extend
|
|
462
|
+
* a built-in provider's defaults to request additional permissions:
|
|
463
|
+
*
|
|
464
|
+
* ```ts
|
|
465
|
+
* const customGoogle = googleProvider({...});
|
|
466
|
+
* customGoogle.scope.push('https://www.googleapis.com/auth/calendar.readonly');
|
|
467
|
+
* ```
|
|
468
|
+
*
|
|
469
|
+
* Or pass `extraScopes` to the built-in factory (see `OAuthHelperInput`).
|
|
470
|
+
*/
|
|
471
|
+
scope: string[];
|
|
472
|
+
nameKey: string;
|
|
473
|
+
emailKey: string;
|
|
474
|
+
avatarKey?: string;
|
|
475
|
+
avatarCodeKey: string;
|
|
476
|
+
getEmail?: (accessToken: string) => Promise<string | false | undefined>;
|
|
477
|
+
getAvatar?: (params: {
|
|
478
|
+
userData: OAuthUserData;
|
|
479
|
+
avatarId?: string;
|
|
480
|
+
accessToken: string;
|
|
481
|
+
}) => string | undefined | Promise<string | undefined>;
|
|
482
|
+
/**
|
|
483
|
+
* Optional per-provider hook to attach extra runtime-only data to the
|
|
484
|
+
* session (NOT to the User row). Return key/value pairs that get merged
|
|
485
|
+
* into `BaseSessionLayout` before `saveSession`. To get strict typing
|
|
486
|
+
* downstream, extend `BaseSessionLayout` via module augmentation:
|
|
487
|
+
*
|
|
488
|
+
* ```ts
|
|
489
|
+
* declare module '@luckystack/core' {
|
|
490
|
+
* interface BaseSessionLayout {
|
|
491
|
+
* googleCalendarToken?: string;
|
|
492
|
+
* }
|
|
493
|
+
* }
|
|
494
|
+
* ```
|
|
495
|
+
*
|
|
496
|
+
* Data lives on the session in Redis and disappears on logout — no Prisma
|
|
497
|
+
* schema change required. Don't store secrets you wouldn't want surfaced
|
|
498
|
+
* to a user inspecting their own session.
|
|
499
|
+
*/
|
|
500
|
+
extraSessionFields?: (params: {
|
|
501
|
+
userData: OAuthUserData;
|
|
502
|
+
accessToken: string;
|
|
503
|
+
}) => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
504
|
+
}
|
|
505
|
+
type OAuthProvider = CredentialsProvider | FullOAuthProvider;
|
|
506
|
+
declare const isFullOAuthProvider: (provider: OAuthProvider) => provider is FullOAuthProvider;
|
|
507
|
+
interface OAuthEndpointOverrides {
|
|
508
|
+
/** Override the provider's authorization URL (e.g. GitHub Enterprise host, Microsoft custom tenant). */
|
|
509
|
+
authorizationURL?: string;
|
|
510
|
+
/** Override the token exchange URL. */
|
|
511
|
+
tokenExchangeURL?: string;
|
|
512
|
+
/** Override the userInfo URL. */
|
|
513
|
+
userInfoURL?: string;
|
|
514
|
+
}
|
|
515
|
+
interface OAuthHelperInput {
|
|
516
|
+
clientId: string | undefined;
|
|
517
|
+
clientSecret: string | undefined;
|
|
518
|
+
callbackUrl: string;
|
|
519
|
+
/** Optional URL overrides for self-hosted / custom-tenant deployments. */
|
|
520
|
+
endpoints?: OAuthEndpointOverrides;
|
|
521
|
+
/**
|
|
522
|
+
* Additional OAuth scopes to request alongside the built-in defaults. Use
|
|
523
|
+
* for "give me access to the user's calendar / drive / repos" flows.
|
|
524
|
+
* Merged with the helper's default scopes; duplicates are deduplicated.
|
|
525
|
+
*/
|
|
526
|
+
extraScopes?: string[];
|
|
527
|
+
/**
|
|
528
|
+
* Per-provider hook for attaching runtime-only fields to the session.
|
|
529
|
+
* See `FullOAuthProvider.extraSessionFields` for usage + module-augmentation
|
|
530
|
+
* pattern.
|
|
531
|
+
*/
|
|
532
|
+
extraSessionFields?: FullOAuthProvider['extraSessionFields'];
|
|
533
|
+
}
|
|
534
|
+
declare const credentialsProvider: () => CredentialsProvider;
|
|
535
|
+
declare const googleProvider: (input: OAuthHelperInput) => FullOAuthProvider;
|
|
536
|
+
declare const githubProvider: (input: OAuthHelperInput) => FullOAuthProvider;
|
|
537
|
+
declare const discordProvider: (input: OAuthHelperInput) => FullOAuthProvider;
|
|
538
|
+
interface FacebookProviderInput extends OAuthHelperInput {
|
|
539
|
+
/** Graph API version segment used in the URLs. Default `'v18.0'` (current as of 2026 — bump as Meta deprecates older versions). */
|
|
540
|
+
apiVersion?: string;
|
|
541
|
+
}
|
|
542
|
+
declare const facebookProvider: (input: FacebookProviderInput) => FullOAuthProvider;
|
|
543
|
+
interface MicrosoftProviderInput extends OAuthHelperInput {
|
|
544
|
+
/** Tenant id segment in the authorize/token URLs. Default `'common'`. */
|
|
545
|
+
tenant?: string;
|
|
546
|
+
/** OAuth API version segment. Default `'v2.0'`. */
|
|
547
|
+
apiVersion?: string;
|
|
548
|
+
/** Graph API version segment for /me. Default `'v1.0'`. */
|
|
549
|
+
graphApiVersion?: string;
|
|
550
|
+
}
|
|
551
|
+
declare const microsoftProvider: (input: MicrosoftProviderInput) => FullOAuthProvider;
|
|
552
|
+
declare const getOAuthProviders: () => OAuthProvider[];
|
|
553
|
+
declare const registerOAuthProviders: (providers: OAuthProvider[]) => OAuthProvider[];
|
|
554
|
+
|
|
555
|
+
interface PostLoginRedirectInput {
|
|
556
|
+
userId: string;
|
|
557
|
+
provider: string;
|
|
558
|
+
isNewUser: boolean;
|
|
559
|
+
defaultUrl: string;
|
|
560
|
+
}
|
|
561
|
+
type PostLoginRedirectResolver = (input: PostLoginRedirectInput) => string | Promise<string>;
|
|
562
|
+
declare const registerPostLoginRedirect: (resolver: PostLoginRedirectResolver) => PostLoginRedirectResolver;
|
|
563
|
+
declare const getPostLoginRedirect: () => PostLoginRedirectResolver | null;
|
|
564
|
+
|
|
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 };
|