@luckystack/login 0.1.9 → 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/CHANGELOG.md +14 -14
- package/CLAUDE.md +194 -194
- package/LICENSE +21 -21
- 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 +256 -89
- package/dist/index.js +518 -793
- 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/oauth-providers.md +431 -431
- package/package.json +9 -4
package/README.md
CHANGED
|
@@ -1,212 +1,239 @@
|
|
|
1
|
-
# @luckystack/login
|
|
2
|
-
|
|
3
|
-
> Authentication for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Credentials + OAuth (Google, GitHub, Facebook, Discord, Microsoft), Redis-backed sessions, single-session enforcement, and lifecycle hooks (`preLogin`, `preRegister`, `preLogout`, `preSessionCreate`, `preSessionDelete` and their `post*` counterparts).
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @luckystack/login @luckystack/core @prisma/client socket.io
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
## Quickstart
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { loginWithCredentials, getSession, logout } from '@luckystack/login';
|
|
15
|
-
import { registerHook } from '@luckystack/core';
|
|
16
|
-
|
|
17
|
-
// Block login for unverified users:
|
|
18
|
-
registerHook('preLogin', async ({ email }) => {
|
|
19
|
-
const user = await prisma.user.findFirst({ where: { email } });
|
|
20
|
-
if (user && !user.verified) {
|
|
21
|
-
return { stop: true, errorCode: 'login.notVerified' };
|
|
22
|
-
}
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// Inside an /auth route handler:
|
|
26
|
-
const result = await loginWithCredentials({ email, password });
|
|
27
|
-
// → { status: true, reason, newToken, session } on success
|
|
28
|
-
// → { status: false, reason } on failure (including hook-stop with the hook's errorCode)
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Sessions are stored in Redis under `${PROJECT_NAME}-session:<token>` and are sliding (every authenticated read extends the TTL by `ProjectConfig.session.expiryDays`).
|
|
32
|
-
|
|
33
|
-
## Hooks
|
|
34
|
-
|
|
35
|
-
`pre*` hooks fire before the side-effect and may return a `HookStopSignal` to abort. `post*` hooks fire after success. Payload types live in `./hookPayloads.ts` and are merged into `@luckystack/core`'s `HookPayloads` via module augmentation.
|
|
36
|
-
|
|
37
|
-
| Hook | Aborts | Fires from |
|
|
38
|
-
| --- | --- | --- |
|
|
39
|
-
| `preLogin` / `postLogin` | yes | `loginWithCredentials`, `loginCallback` |
|
|
40
|
-
| `preRegister` / `postRegister` | yes | `loginWithCredentials` (register branch), `loginCallback` (new OAuth user) |
|
|
41
|
-
| `preLogout` / `postLogout` | yes | `logout` |
|
|
42
|
-
| `preSessionCreate` / `postSessionCreate` | yes | `saveSession({ newUser: true })` |
|
|
43
|
-
| `preSessionDelete` / `postSessionDelete` | yes | `deleteSession` |
|
|
44
|
-
|
|
45
|
-
## OAuth provider registry
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
import {
|
|
49
|
-
registerOAuthProviders,
|
|
50
|
-
googleProvider,
|
|
51
|
-
githubProvider,
|
|
52
|
-
credentialsProvider,
|
|
53
|
-
} from '@luckystack/login';
|
|
54
|
-
|
|
55
|
-
registerOAuthProviders([
|
|
56
|
-
credentialsProvider(),
|
|
57
|
-
googleProvider({
|
|
58
|
-
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
59
|
-
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
60
|
-
callbackUrl: `http://localhost:80/auth/callback/google`,
|
|
61
|
-
}),
|
|
62
|
-
githubProvider({
|
|
63
|
-
clientId: process.env.GITHUB_CLIENT_ID,
|
|
64
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
65
|
-
callbackUrl: `http://localhost:80/auth/callback/github`,
|
|
66
|
-
}),
|
|
67
|
-
]);
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
Built-in helpers: `googleProvider`, `githubProvider`, `discordProvider`, `facebookProvider`, `microsoftProvider`, `credentialsProvider`. Each non-credentials helper takes `{ clientId, clientSecret, callbackUrl }` and returns a fully-typed `FullOAuthProvider` with the provider's authorize/token/userinfo URLs and standard scopes prefilled. Pass anything that satisfies the `OAuthProvider` interface to register custom providers — see [`packages/login/src/oauthProviders.ts`](./src/oauthProviders.ts) for the canonical shape.
|
|
71
|
-
|
|
72
|
-
### Provider options
|
|
73
|
-
|
|
74
|
-
Every helper accepts the base shape plus optional overrides for self-hosted instances and provider-specific tunables.
|
|
75
|
-
|
|
76
|
-
| Option | Applies to | Default | When to override |
|
|
77
|
-
| --- | --- | --- | --- |
|
|
78
|
-
| `clientId` | all | — | Required. The OAuth app's client identifier. |
|
|
79
|
-
| `clientSecret` | all | — | Required. The OAuth app's client secret. |
|
|
80
|
-
| `callbackUrl` | all | — | Required. Must match the URL registered with the provider — your BACKEND origin + `/auth/callback/<name>` (dev `http://localhost:80/auth/callback/<name>`, prod `https://your-domain.com/auth/callback/<name>`). |
|
|
81
|
-
| `endpoints?.authorizationURL` | all | provider-default | GitHub Enterprise host, Microsoft custom-tenant authorize URL, internal auth proxy, etc. |
|
|
82
|
-
| `endpoints?.tokenExchangeURL` | all | provider-default | Same use cases as `authorizationURL`. |
|
|
83
|
-
| `endpoints?.userInfoURL` | all | provider-default | Self-hosted GitHub Enterprise / Microsoft Graph mirror, etc. |
|
|
84
|
-
| `apiVersion?` | `facebookProvider`, `microsoftProvider` | facebook: `v18.0`, microsoft: `v2.0` | Pin to a known-good Graph API version when the upstream rolls out breaking changes. |
|
|
85
|
-
| `tenant?` | `microsoftProvider` | `'common'` | Restrict logins to a specific Azure AD tenant ID. Use a UUID or `'organizations'` / `'consumers'`. |
|
|
86
|
-
| `graphApiVersion?` | `microsoftProvider` | `'v1.0'` | Pin the Microsoft Graph version used to fetch the user profile (separate from the OAuth `apiVersion`). |
|
|
87
|
-
|
|
88
|
-
Example — self-hosted GitHub Enterprise:
|
|
89
|
-
|
|
90
|
-
```ts
|
|
91
|
-
githubProvider({
|
|
92
|
-
clientId: process.env.GITHUB_CLIENT_ID,
|
|
93
|
-
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
94
|
-
callbackUrl: `http://localhost:80/auth/callback/github`,
|
|
95
|
-
endpoints: {
|
|
96
|
-
authorizationURL: 'https://github.acme.example/login/oauth/authorize',
|
|
97
|
-
tokenExchangeURL: 'https://github.acme.example/login/oauth/access_token',
|
|
98
|
-
userInfoURL: 'https://github.acme.example/api/v3/user',
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
Example — single-tenant Microsoft Entra ID:
|
|
104
|
-
|
|
105
|
-
```ts
|
|
106
|
-
microsoftProvider({
|
|
107
|
-
clientId: process.env.MICROSOFT_CLIENT_ID,
|
|
108
|
-
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
|
109
|
-
callbackUrl: `http://localhost:80/auth/callback/microsoft`,
|
|
110
|
-
tenant: process.env.MICROSOFT_TENANT_ID, // 'common' for any tenant; UUID for a single tenant.
|
|
111
|
-
apiVersion: 'v2.0', // OAuth endpoint version
|
|
112
|
-
graphApiVersion: 'v1.0', // Graph endpoint version
|
|
113
|
-
});
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
## User adapter
|
|
117
|
-
|
|
118
|
-
By default the package reads / writes the Prisma `User` model directly. To bind auth flows to a different table (multi-tenant, alternative ORM, soft-deleted users, etc.), register your own `UserAdapter`:
|
|
119
|
-
|
|
120
|
-
```ts
|
|
121
|
-
import { registerUserAdapter, defaultPrismaUserAdapter } from '@luckystack/login';
|
|
122
|
-
|
|
123
|
-
registerUserAdapter({
|
|
124
|
-
...defaultPrismaUserAdapter,
|
|
125
|
-
findByEmail: async (email) => {
|
|
126
|
-
const user = await prisma.user.findFirst({ where: { email, deletedAt: null } });
|
|
127
|
-
return user;
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
##
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
});
|
|
143
|
-
```
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
1
|
+
# @luckystack/login
|
|
2
|
+
|
|
3
|
+
> Authentication for [LuckyStack](https://github.com/ItsLucky23/LuckyStack-v2). Credentials + OAuth (Google, GitHub, Facebook, Discord, Microsoft), Redis-backed sessions, single-session enforcement, and lifecycle hooks (`preLogin`, `preRegister`, `preLogout`, `preSessionCreate`, `preSessionDelete` and their `post*` counterparts).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @luckystack/login @luckystack/core @prisma/client socket.io
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quickstart
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { loginWithCredentials, getSession, logout } from '@luckystack/login';
|
|
15
|
+
import { registerHook } from '@luckystack/core';
|
|
16
|
+
|
|
17
|
+
// Block login for unverified users:
|
|
18
|
+
registerHook('preLogin', async ({ email }) => {
|
|
19
|
+
const user = await prisma.user.findFirst({ where: { email } });
|
|
20
|
+
if (user && !user.verified) {
|
|
21
|
+
return { stop: true, errorCode: 'login.notVerified' };
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Inside an /auth route handler:
|
|
26
|
+
const result = await loginWithCredentials({ email, password });
|
|
27
|
+
// → { status: true, reason, newToken, session } on success
|
|
28
|
+
// → { status: false, reason } on failure (including hook-stop with the hook's errorCode)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Sessions are stored in Redis under `${PROJECT_NAME}-session:<token>` and are sliding (every authenticated read extends the TTL by `ProjectConfig.session.expiryDays`).
|
|
32
|
+
|
|
33
|
+
## Hooks
|
|
34
|
+
|
|
35
|
+
`pre*` hooks fire before the side-effect and may return a `HookStopSignal` to abort. `post*` hooks fire after success. Payload types live in `./hookPayloads.ts` and are merged into `@luckystack/core`'s `HookPayloads` via module augmentation.
|
|
36
|
+
|
|
37
|
+
| Hook | Aborts | Fires from |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `preLogin` / `postLogin` | yes | `loginWithCredentials`, `loginCallback` |
|
|
40
|
+
| `preRegister` / `postRegister` | yes | `loginWithCredentials` (register branch), `loginCallback` (new OAuth user) |
|
|
41
|
+
| `preLogout` / `postLogout` | yes | `logout` |
|
|
42
|
+
| `preSessionCreate` / `postSessionCreate` | yes | `saveSession({ newUser: true })` |
|
|
43
|
+
| `preSessionDelete` / `postSessionDelete` | yes | `deleteSession` |
|
|
44
|
+
|
|
45
|
+
## OAuth provider registry
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
import {
|
|
49
|
+
registerOAuthProviders,
|
|
50
|
+
googleProvider,
|
|
51
|
+
githubProvider,
|
|
52
|
+
credentialsProvider,
|
|
53
|
+
} from '@luckystack/login';
|
|
54
|
+
|
|
55
|
+
registerOAuthProviders([
|
|
56
|
+
credentialsProvider(),
|
|
57
|
+
googleProvider({
|
|
58
|
+
clientId: process.env.GOOGLE_CLIENT_ID,
|
|
59
|
+
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
|
60
|
+
callbackUrl: `http://localhost:80/auth/callback/google`,
|
|
61
|
+
}),
|
|
62
|
+
githubProvider({
|
|
63
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
64
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
65
|
+
callbackUrl: `http://localhost:80/auth/callback/github`,
|
|
66
|
+
}),
|
|
67
|
+
]);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Built-in helpers: `googleProvider`, `githubProvider`, `discordProvider`, `facebookProvider`, `microsoftProvider`, `credentialsProvider`. Each non-credentials helper takes `{ clientId, clientSecret, callbackUrl }` and returns a fully-typed `FullOAuthProvider` with the provider's authorize/token/userinfo URLs and standard scopes prefilled. Pass anything that satisfies the `OAuthProvider` interface to register custom providers — see [`packages/login/src/oauthProviders.ts`](./src/oauthProviders.ts) for the canonical shape.
|
|
71
|
+
|
|
72
|
+
### Provider options
|
|
73
|
+
|
|
74
|
+
Every helper accepts the base shape plus optional overrides for self-hosted instances and provider-specific tunables.
|
|
75
|
+
|
|
76
|
+
| Option | Applies to | Default | When to override |
|
|
77
|
+
| --- | --- | --- | --- |
|
|
78
|
+
| `clientId` | all | — | Required. The OAuth app's client identifier. |
|
|
79
|
+
| `clientSecret` | all | — | Required. The OAuth app's client secret. |
|
|
80
|
+
| `callbackUrl` | all | — | Required. Must match the URL registered with the provider — your BACKEND origin + `/auth/callback/<name>` (dev `http://localhost:80/auth/callback/<name>`, prod `https://your-domain.com/auth/callback/<name>`). |
|
|
81
|
+
| `endpoints?.authorizationURL` | all | provider-default | GitHub Enterprise host, Microsoft custom-tenant authorize URL, internal auth proxy, etc. |
|
|
82
|
+
| `endpoints?.tokenExchangeURL` | all | provider-default | Same use cases as `authorizationURL`. |
|
|
83
|
+
| `endpoints?.userInfoURL` | all | provider-default | Self-hosted GitHub Enterprise / Microsoft Graph mirror, etc. |
|
|
84
|
+
| `apiVersion?` | `facebookProvider`, `microsoftProvider` | facebook: `v18.0`, microsoft: `v2.0` | Pin to a known-good Graph API version when the upstream rolls out breaking changes. |
|
|
85
|
+
| `tenant?` | `microsoftProvider` | `'common'` | Restrict logins to a specific Azure AD tenant ID. Use a UUID or `'organizations'` / `'consumers'`. |
|
|
86
|
+
| `graphApiVersion?` | `microsoftProvider` | `'v1.0'` | Pin the Microsoft Graph version used to fetch the user profile (separate from the OAuth `apiVersion`). |
|
|
87
|
+
|
|
88
|
+
Example — self-hosted GitHub Enterprise:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
githubProvider({
|
|
92
|
+
clientId: process.env.GITHUB_CLIENT_ID,
|
|
93
|
+
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
|
94
|
+
callbackUrl: `http://localhost:80/auth/callback/github`,
|
|
95
|
+
endpoints: {
|
|
96
|
+
authorizationURL: 'https://github.acme.example/login/oauth/authorize',
|
|
97
|
+
tokenExchangeURL: 'https://github.acme.example/login/oauth/access_token',
|
|
98
|
+
userInfoURL: 'https://github.acme.example/api/v3/user',
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Example — single-tenant Microsoft Entra ID:
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
microsoftProvider({
|
|
107
|
+
clientId: process.env.MICROSOFT_CLIENT_ID,
|
|
108
|
+
clientSecret: process.env.MICROSOFT_CLIENT_SECRET,
|
|
109
|
+
callbackUrl: `http://localhost:80/auth/callback/microsoft`,
|
|
110
|
+
tenant: process.env.MICROSOFT_TENANT_ID, // 'common' for any tenant; UUID for a single tenant.
|
|
111
|
+
apiVersion: 'v2.0', // OAuth endpoint version
|
|
112
|
+
graphApiVersion: 'v1.0', // Graph endpoint version
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## User adapter
|
|
117
|
+
|
|
118
|
+
By default the package reads / writes the Prisma `User` model directly. To bind auth flows to a different table (multi-tenant, alternative ORM, soft-deleted users, etc.), register your own `UserAdapter`:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
import { registerUserAdapter, defaultPrismaUserAdapter } from '@luckystack/login';
|
|
122
|
+
|
|
123
|
+
registerUserAdapter({
|
|
124
|
+
...defaultPrismaUserAdapter,
|
|
125
|
+
findByEmail: async (email) => {
|
|
126
|
+
const user = await prisma.user.findFirst({ where: { email, deletedAt: null } });
|
|
127
|
+
return user;
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Account strategy: per-provider vs unified
|
|
133
|
+
|
|
134
|
+
`auth.providerAccountStrategy` (in `registerProjectConfig`) controls how the same email address is treated across sign-in providers:
|
|
135
|
+
|
|
136
|
+
| Strategy | Behavior | Schema |
|
|
137
|
+
|---|---|---|
|
|
138
|
+
| `'per-provider'` (default) | `sam@x.com` via Google and via GitHub are **two separate `User` rows**. Lookups are scoped to `(email, provider)`. | `@@unique([email, provider])` recommended. |
|
|
139
|
+
| `'unified'` | `sam@x.com` maps to **one `User` row**; signing in via a new provider links to the existing account (credentials login, OAuth find-or-create, and register dedupe all resolve by email alone). | `email` must be `@unique`. |
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
registerProjectConfig({ auth: { providerAccountStrategy: 'unified' } });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**Migrating an existing project to `'unified'`** (the strategy reads accounts by email irrespective of provider, so the DB must enforce one row per email):
|
|
146
|
+
|
|
147
|
+
1. **Dedupe existing rows.** If you previously ran `'per-provider'`, the same email may already exist under multiple providers. Merge or remove duplicates so each email appears once. (Pick the row to keep — usually the credentials account or the earliest — repoint related rows, delete the rest.)
|
|
148
|
+
2. **Make `email` unique** in `prisma/schema.prisma`:
|
|
149
|
+
```prisma
|
|
150
|
+
model User {
|
|
151
|
+
// ...
|
|
152
|
+
email String @unique // was: email String (+ optional @@unique([email, provider]))
|
|
153
|
+
provider String // now records the ORIGINAL signup provider only
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
Then `prisma migrate` (or `db push`). The DB constraint closes the registration race that the application-level check alone cannot.
|
|
157
|
+
3. **No code change is required** beyond the config flag — the default `UserAdapter` already implements `findByEmailAnyProvider`. A **custom** `UserAdapter` must add `findByEmailAnyProvider({ email })` (resolve by email, ignoring provider); if it doesn't, the framework logs a one-time warning and falls back to provider-scoped lookup so the misconfiguration is visible rather than silent.
|
|
158
|
+
|
|
159
|
+
## Post-login redirect
|
|
160
|
+
|
|
161
|
+
Compute the OAuth callback destination dynamically (per-user, per-tenant, per-provider):
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
import { registerPostLoginRedirect } from '@luckystack/login';
|
|
165
|
+
|
|
166
|
+
registerPostLoginRedirect(async ({ user, provider }) => {
|
|
167
|
+
if (user.organizationId) return `/org/${user.organizationId}`;
|
|
168
|
+
return '/welcome';
|
|
169
|
+
});
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Password reset primitives
|
|
173
|
+
|
|
174
|
+
Used by the `forgotPassword: 'framework'` mode and exported for `'custom'` consumers who want to drive their own flow:
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import {
|
|
178
|
+
createPasswordResetToken,
|
|
179
|
+
consumePasswordResetToken,
|
|
180
|
+
updatePasswordHash,
|
|
181
|
+
verifyPassword,
|
|
182
|
+
sendPasswordResetEmail, // requires @luckystack/email registered
|
|
183
|
+
} from '@luckystack/login';
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`sendPasswordResetEmail` is a no-op when `@luckystack/email` has not been registered, so you can keep the import unconditionally.
|
|
187
|
+
|
|
188
|
+
## Public API
|
|
189
|
+
|
|
190
|
+
| Export | Purpose |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `loginWithCredentials(params)` | Combined login/register dispatcher. Routes to `register*` or `login*` based on the body shape (presence of `confirmPassword`). Used by the HTTP `/auth/api/credentials` route. |
|
|
193
|
+
| `registerWithCredentials({ email, password, name, confirmPassword })` | Register-only entry point. Use this when you wire a custom auth surface that bypasses the dispatcher's body-shape branching. |
|
|
194
|
+
| `loginWithCredentialsCore({ email, password })` | Login-only entry point. Same idea as `registerWithCredentials`. |
|
|
195
|
+
| `loginCallback(pathname, req, res)` | OAuth state-exchange handler — wired to `/auth/callback/<provider>` by `@luckystack/server`. |
|
|
196
|
+
| `createOAuthState(providerName)` | Issue a CSRF state token (Redis, NX, TTL from project config). |
|
|
197
|
+
| `logout({ token, socket, userId })` | End a single socket's session. |
|
|
198
|
+
| `saveSession(token, user, newUser?)` | Write to Redis + broadcast to existing connections. |
|
|
199
|
+
| `getSession(token)` | Read + slide expiration. Dispatches `preSessionRefresh` / `postSessionRefresh` around the Redis EXPIRE call. |
|
|
200
|
+
| `deleteSession(token)` | Hard delete + clean up active-tokens set. |
|
|
201
|
+
| `getAllSessions()` | Admin utility — scans all sessions. |
|
|
202
|
+
| `revokeUserSessions(userId)` | Force-logout every active session for a user. |
|
|
203
|
+
| `sessionKeyFor(token)` / `activeUsersKeyFor(userId)` | Centralized Redis-key builders (`{projectName}-session:{token}` / `{projectName}-activeUsers:{userId}`). Use these when you read or scan session data from outside `@luckystack/login` so the key shape stays in lockstep. |
|
|
204
|
+
| `registerOAuthProviders(list)` / `getOAuthProviders()` / `isFullOAuthProvider(p)` | OAuth registry. |
|
|
205
|
+
| `googleProvider`, `githubProvider`, `discordProvider`, `facebookProvider`, `microsoftProvider`, `credentialsProvider` | Built-in provider factories. |
|
|
206
|
+
| `registerUserAdapter(adapter)` / `getUserAdapter()` / `isUserAdapterRegistered()` / `defaultPrismaUserAdapter` | Pluggable user store. |
|
|
207
|
+
| `registerPostLoginRedirect(resolver)` / `getPostLoginRedirect()` | Dynamic redirect resolution. |
|
|
208
|
+
| `createPasswordResetToken`, `consumePasswordResetToken`, `updatePasswordHash`, `verifyPassword`, `sendPasswordResetEmail` | Password-reset primitives. |
|
|
209
|
+
|
|
210
|
+
Types: `BaseSessionLayout`, `SessionLocation`, `AuthProps` (re-exported from `@luckystack/core`); `OAuthProvider`, `CredentialsProvider`, `FullOAuthProvider`, `UserAdapter`, `UserAdapterCreateInput`, `UserRecord`, `PostLoginRedirectResolver`, `PostLoginRedirectInput`, plus all `Pre*Payload` / `Post*Payload` types.
|
|
211
|
+
|
|
212
|
+
## Stored-XSS warning: OAuth `name` fields
|
|
213
|
+
|
|
214
|
+
`loginCallback` reads the user's display name straight from the OAuth provider's profile response and stores it on the `User.name` column unsanitized. That is fine for plain text rendering and for the framework's avatar-fallback initials, but **becomes stored XSS the moment a consumer renders the name as raw HTML** (e.g. `dangerouslySetInnerHTML`, an HTML email body, a server-rendered widget). Two recommended mitigations on the consumer side:
|
|
215
|
+
|
|
216
|
+
- Render names with React text nodes (the default; safe).
|
|
217
|
+
- Strip or escape any `<` / `>` characters before injecting names into HTML emails or non-React surfaces.
|
|
218
|
+
|
|
219
|
+
If your project has no such surfaces this is informational. The framework intentionally does not silently mutate the field because some apps need exact-match search across providers.
|
|
220
|
+
|
|
221
|
+
## Related architecture docs
|
|
222
|
+
|
|
223
|
+
- [`docs/ARCHITECTURE_AUTH.md`](../../docs/ARCHITECTURE_AUTH.md) — OAuth + credentials lifecycle, allowed-origin checks, role guards.
|
|
224
|
+
- [`docs/ARCHITECTURE_SESSION.md`](../../docs/ARCHITECTURE_SESSION.md) — Redis layout, sliding expiration, single-session enforcement.
|
|
225
|
+
- [`docs/ARCHITECTURE_EMAIL.md`](../../docs/ARCHITECTURE_EMAIL.md) — forgot-password modes (`framework` / `custom` / `disabled`).
|
|
226
|
+
|
|
227
|
+
## Dependencies
|
|
228
|
+
|
|
229
|
+
- Runtime: `@luckystack/core`, `bcryptjs`, `validator`, `dotenv`
|
|
230
|
+
- Peer (canonical ranges, standardized 2026-05-07):
|
|
231
|
+
- `@prisma/client@^6.19.0`
|
|
232
|
+
- `socket.io@^4.8.0`
|
|
233
|
+
- Optional peer: `@luckystack/email` — only required when `forgotPassword: 'framework'`. The package lazy-imports it; without it, the framework-mode flow is disabled but every other API works.
|
|
234
|
+
|
|
235
|
+
Your Prisma schema must include a `User` model with at least: `id`, `email`, `provider` (enum `PROVIDERS`), `password` (nullable), `name`, `avatar`, `avatarFallback`, `admin`, `language`. See [`prisma/schema.prisma`](../../prisma/schema.prisma) for the canonical shape, or register a `UserAdapter` to talk to a different schema.
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
MIT — see [LICENSE](../../LICENSE).
|