@luckystack/login 0.6.7 → 0.7.1
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 +51 -51
- package/CLAUDE.md +194 -194
- package/README.md +239 -239
- package/dist/{chunk-HJHCX2JO.js → chunk-L5MHCPNN.js} +1 -1
- package/dist/chunk-L5MHCPNN.js.map +1 -0
- package/dist/{chunk-5WBFVSMW.js → chunk-WFPYOCFC.js} +3 -3
- package/dist/chunk-WFPYOCFC.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/logout-OBO6AF4R.js +7 -0
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- 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 +440 -440
- package/docs/password-reset.md +343 -343
- package/docs/redirect-validation.md +390 -390
- package/docs/session-management.md +285 -285
- package/docs/user-adapter.md +353 -353
- package/package.json +3 -3
- package/dist/chunk-5WBFVSMW.js.map +0 -1
- package/dist/chunk-HJHCX2JO.js.map +0 -1
- package/dist/logout-RX77TPUH.js +0 -7
- /package/dist/{logout-RX77TPUH.js.map → logout-OBO6AF4R.js.map} +0 -0
package/README.md
CHANGED
|
@@ -1,239 +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
|
-
## 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).
|
|
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).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/oauthProviders.ts","../src/userAdapter.ts"],"sourcesContent":["//? OAuth provider registry. Replaces the previously hardcoded provider list\n//? in `loginConfig.ts`. Consumers register providers from their\n//? `luckystack/login/oauthProviders.ts` overlay file at boot.\n//?\n//? Two-layer surface:\n//? 1. `registerOAuthProviders([...])` — accept an array of provider definitions.\n//? 2. Built-in helpers (`googleProvider`, `githubProvider`, ...) so the common\n//? case is one function call per provider with only the secrets + callback.\n//?\n//? Custom providers (Okta, Apple, X, etc.) plug in as raw `OAuthProvider`\n//? objects alongside the helpers.\n\nimport { tryCatch } from '@luckystack/core';\n\nexport type OAuthUserData = Record<string, unknown>;\n\nexport const asOAuthUserData = (value: unknown): OAuthUserData => {\n if (value && typeof value === 'object') {\n return value as OAuthUserData;\n }\n return {};\n};\n\nexport interface CredentialsProvider {\n name: 'credentials';\n}\n\nexport interface FullOAuthProvider {\n name: string;\n clientID: string;\n clientSecret: string;\n callbackURL: string;\n authorizationURL: string;\n tokenExchangeURL: string;\n tokenExchangeMethod: 'json' | 'form';\n userInfoURL: string;\n /**\n * OAuth scopes requested at the authorization step. Consumers can extend\n * a built-in provider's defaults to request additional permissions:\n *\n * ```ts\n * const customGoogle = googleProvider({...});\n * customGoogle.scope.push('https://www.googleapis.com/auth/calendar.readonly');\n * ```\n *\n * Or pass `extraScopes` to the built-in factory (see `OAuthHelperInput`).\n */\n scope: string[];\n nameKey: string;\n emailKey: string;\n /**\n * Optional key on the userInfo response holding a provider \"email verified\"\n * boolean. When set AND the field is present-and-falsy, the framework rejects\n * the profile rather than trusting an unverified address — defense-in-depth\n * against account-linking takeover under the `'unified'` strategy (SEC-21).\n * A missing field (provider doesn't report it) is treated as \"trusted\",\n * preserving behaviour for providers that don't expose the flag.\n */\n emailVerifiedKey?: string;\n /**\n * Set `true` for a provider whose userinfo ONLY returns an email once the\n * address is confirmed with the provider (e.g. Facebook Graph `/me` omits\n * unconfirmed emails). Such a provider exposes no separate verified flag, so\n * the mere PRESENCE of the email is the verified signal — this opts it in so\n * both first-login CREATION and cross-provider LINK treat it as verified.\n * Mutually exclusive in practice with `emailVerifiedKey`.\n */\n emailImpliesVerified?: boolean;\n avatarKey?: string;\n avatarCodeKey: string;\n getEmail?: (accessToken: string) => Promise<string | false | undefined>;\n getAvatar?: (params: { userData: OAuthUserData; avatarId?: string; accessToken: string }) => string | undefined | Promise<string | undefined>;\n /**\n * Optional per-provider hook to attach extra runtime-only data to the\n * session (NOT to the User row). Return key/value pairs that get merged\n * into `BaseSessionLayout` before `saveSession`. To get strict typing\n * downstream, extend `BaseSessionLayout` via module augmentation:\n *\n * ```ts\n * declare module '@luckystack/core' {\n * interface BaseSessionLayout {\n * googleCalendarToken?: string;\n * }\n * }\n * ```\n *\n * Data lives on the session in Redis and disappears on logout — no Prisma\n * schema change required. Don't store secrets you wouldn't want surfaced\n * to a user inspecting their own session.\n */\n extraSessionFields?: (params: {\n userData: OAuthUserData;\n accessToken: string;\n }) => Promise<Record<string, unknown>> | Record<string, unknown>;\n /**\n * Extra query params appended to the authorization-redirect URL (CFG-21).\n * Lets a provider request e.g. `access_type=offline`, `login_hint=...`, or\n * override the framework's default `prompt=select_account`. The server's\n * authorize route merges these over the built-in params (a key present here\n * wins). Reserved OAuth params (`client_id`, `redirect_uri`, `scope`,\n * `response_type`, `state`, `code_challenge`, `code_challenge_method`) should\n * NOT be set here — they are framework-owned. Default unset.\n */\n extraAuthorizationParams?: Record<string, string>;\n /**\n * Opt this provider into PKCE (RFC 7636, S256) (F11). When `true`, the\n * framework generates a `code_verifier` at flow start, stores it server-side\n * with the OAuth state, sends `code_challenge`/`code_challenge_method=S256` on\n * the authorize redirect, and replays the verifier at token exchange. Required\n * by OAuth 2.1 / PKCE-mandating providers (X/Twitter, some Okta/Auth0\n * policies). Default `false` (no PKCE — existing flows are byte-identical).\n */\n usePkce?: boolean;\n}\n\nexport type OAuthProvider = CredentialsProvider | FullOAuthProvider;\n\nexport const isFullOAuthProvider = (provider: OAuthProvider): provider is FullOAuthProvider =>\n provider.name !== 'credentials';\n\nexport interface OAuthEndpointOverrides {\n /** Override the provider's authorization URL (e.g. GitHub Enterprise host, Microsoft custom tenant). */\n authorizationURL?: string;\n /** Override the token exchange URL. */\n tokenExchangeURL?: string;\n /** Override the userInfo URL. */\n userInfoURL?: string;\n}\n\ninterface OAuthHelperInput {\n clientId: string | undefined;\n clientSecret: string | undefined;\n callbackUrl: string;\n /** Optional URL overrides for self-hosted / custom-tenant deployments. */\n endpoints?: OAuthEndpointOverrides;\n /**\n * Additional OAuth scopes to request alongside the built-in defaults. Use\n * for \"give me access to the user's calendar / drive / repos\" flows.\n * Merged with the helper's default scopes; duplicates are deduplicated.\n */\n extraScopes?: string[];\n /**\n * Per-provider hook for attaching runtime-only fields to the session.\n * See `FullOAuthProvider.extraSessionFields` for usage + module-augmentation\n * pattern.\n */\n extraSessionFields?: FullOAuthProvider['extraSessionFields'];\n /**\n * Extra query params for the authorize redirect (CFG-21). See\n * `FullOAuthProvider.extraAuthorizationParams`.\n */\n extraAuthorizationParams?: Record<string, string>;\n /** Opt this provider into PKCE (S256) (F11). See `FullOAuthProvider.usePkce`. */\n usePkce?: boolean;\n}\n\nconst mergeScopes = (defaults: string[], extra: string[] | undefined): string[] => {\n if (!extra || extra.length === 0) return defaults;\n return [...new Set([...defaults, ...extra])];\n};\n\nconst requireString = (value: string | undefined, label: string): string => {\n if (!value || value.length === 0) {\n throw new Error(`OAuth provider configuration error: ${label} is empty.`);\n }\n return value;\n};\n\nexport const credentialsProvider = (): CredentialsProvider => ({ name: 'credentials' });\n\nexport const googleProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\n name: 'google',\n clientID: requireString(input.clientId, 'google clientId'),\n clientSecret: requireString(input.clientSecret, 'google clientSecret'),\n callbackURL: input.callbackUrl,\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://accounts.google.com/o/oauth2/v2/auth',\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://oauth2.googleapis.com/token',\n tokenExchangeMethod: 'json',\n //? OIDC userinfo (v3) — the legacy `oauth2/v1/userinfo` is deprecated and\n //? reports verification as `verified_email`; v3 reports the OIDC-standard\n //? `email_verified`. Use v3 so `emailVerifiedKey` below lines up (SEC-21/QUA-24).\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://www.googleapis.com/oauth2/v3/userinfo',\n scope: mergeScopes([\n 'https://www.googleapis.com/auth/userinfo.profile',\n 'https://www.googleapis.com/auth/userinfo.email',\n ], input.extraScopes),\n nameKey: 'name',\n emailKey: 'email',\n //? Google's OIDC userinfo returns `email_verified`; reject an unverified\n //? address so a Google account with an unverified email can't link to a\n //? victim's existing account under the `'unified'` strategy (SEC-21).\n emailVerifiedKey: 'email_verified',\n avatarKey: 'picture',\n avatarCodeKey: '',\n extraSessionFields: input.extraSessionFields,\n extraAuthorizationParams: input.extraAuthorizationParams,\n usePkce: input.usePkce,\n});\n\nexport const githubProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\n name: 'github',\n clientID: requireString(input.clientId, 'github clientId'),\n clientSecret: requireString(input.clientSecret, 'github clientSecret'),\n callbackURL: input.callbackUrl,\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://github.com/login/oauth/authorize',\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://github.com/login/oauth/access_token',\n tokenExchangeMethod: 'json',\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://api.github.com/user',\n scope: mergeScopes(['read:user', 'user:email'], input.extraScopes),\n nameKey: 'login',\n emailKey: 'email',\n avatarKey: 'avatar_url',\n avatarCodeKey: '',\n extraSessionFields: input.extraSessionFields,\n extraAuthorizationParams: input.extraAuthorizationParams,\n usePkce: input.usePkce,\n getEmail: async (accessToken) => {\n const fetchEmails = async () => {\n const response = await fetch('https://api.github.com/user/emails', {\n method: 'GET',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n Authorization: `Bearer ${accessToken}`,\n },\n });\n if (!response.ok) return false;\n const emails: unknown = await response.json();\n if (!Array.isArray(emails)) return false;\n return emails as { primary?: boolean; verified?: boolean; email?: string }[];\n };\n\n const [error, emails] = await tryCatch(fetchEmails);\n if (error || !emails) return false;\n\n //? Defense-in-depth: only ever select a `verified === true` address. GitHub\n //? forbids an unverified email from being `primary`, so in the normal case\n //? this is a no-op — but the `emails[0]` fallback (when no primary exists)\n //? could otherwise pick an UNVERIFIED address an attacker added to their\n //? GitHub account, which under the `'unified'` account strategy would link\n //? to a victim's existing account by that email (SEC-21). Filtering on\n //? `verified` first closes that fallback path.\n const verified = emails.filter((entry) => entry.verified === true);\n let mainEmail: string | undefined;\n for (const entry of verified) {\n if (entry.primary && typeof entry.email === 'string') mainEmail = entry.email;\n }\n mainEmail ??= verified[0]?.email;\n return mainEmail;\n },\n});\n\nexport const discordProvider = (input: OAuthHelperInput): FullOAuthProvider => ({\n name: 'discord',\n clientID: requireString(input.clientId, 'discord clientId'),\n clientSecret: requireString(input.clientSecret, 'discord clientSecret'),\n callbackURL: input.callbackUrl,\n authorizationURL: input.endpoints?.authorizationURL ?? 'https://discord.com/oauth2/authorize',\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? 'https://discord.com/api/oauth2/token',\n tokenExchangeMethod: 'form',\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://discord.com/api/users/@me',\n scope: mergeScopes(['identify', 'email'], input.extraScopes),\n nameKey: 'username',\n emailKey: 'email',\n //? Discord's /users/@me returns a `verified` boolean for the account email;\n //? reject the address when it is explicitly unverified (SEC-21).\n emailVerifiedKey: 'verified',\n avatarCodeKey: 'avatar',\n extraSessionFields: input.extraSessionFields,\n extraAuthorizationParams: input.extraAuthorizationParams,\n usePkce: input.usePkce,\n getAvatar: ({ userData, avatarId }) => {\n if (!avatarId) return;\n const userId = typeof userData.id === 'string' ? userData.id : '';\n if (!userId) return;\n const format = avatarId.startsWith('a_') ? 'gif' : 'png';\n //? Encode the provider-supplied id + avatar hash before interpolation so a\n //? malformed value can't inject `../` path segments into the CDN URL (SEC-23).\n return `https://cdn.discordapp.com/avatars/${encodeURIComponent(userId)}/${encodeURIComponent(avatarId)}.${format}`;\n },\n});\n\nexport interface FacebookProviderInput extends OAuthHelperInput {\n /** Graph API version segment used in the URLs. Default `'v18.0'` (current as of 2026 — bump as Meta deprecates older versions). */\n apiVersion?: string;\n}\n\nexport const facebookProvider = (input: FacebookProviderInput): FullOAuthProvider => {\n const v = input.apiVersion ?? 'v18.0';\n return {\n name: 'facebook',\n clientID: requireString(input.clientId, 'facebook clientId'),\n clientSecret: requireString(input.clientSecret, 'facebook clientSecret'),\n callbackURL: input.callbackUrl,\n authorizationURL: input.endpoints?.authorizationURL ?? `https://www.facebook.com/${v}/dialog/oauth`,\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://graph.facebook.com/${v}/oauth/access_token`,\n tokenExchangeMethod: 'form',\n userInfoURL: input.endpoints?.userInfoURL ?? 'https://graph.facebook.com/me?fields=id,name,email,picture.type(large)',\n scope: mergeScopes(['public_profile', 'email'], input.extraScopes),\n nameKey: 'name',\n emailKey: 'email',\n //? Facebook's Graph `/me` only returns `email` for an address the user has\n //? CONFIRMED with Facebook (unconfirmed emails are omitted), so there is no\n //? separate verified flag to gate on — the presence of `email` IS the verified\n //? signal. `emailImpliesVerified` encodes that so a Facebook sign-in counts as\n //? verified for BOTH first-login creation and cross-provider linking (SEC-21 /\n //? M2 creation guard). If a consumer opts into receiving unverified emails via\n //? a future Graph field, set `emailVerifiedKey` on the returned provider instead.\n emailImpliesVerified: true,\n avatarCodeKey: '',\n extraSessionFields: input.extraSessionFields,\n extraAuthorizationParams: input.extraAuthorizationParams,\n usePkce: input.usePkce,\n getAvatar: ({ userData }) => {\n const picture = asOAuthUserData(userData.picture);\n const pictureData = asOAuthUserData(picture.data);\n const url = pictureData.url;\n return typeof url === 'string' ? url : undefined;\n },\n };\n};\n\nexport interface MicrosoftProviderInput extends OAuthHelperInput {\n /** Tenant id segment in the authorize/token URLs. Default `'common'`. */\n tenant?: string;\n /** OAuth API version segment. Default `'v2.0'`. */\n apiVersion?: string;\n /** Graph API version segment for /me. Default `'v1.0'`. */\n graphApiVersion?: string;\n /**\n * Allow falling back to `userPrincipalName` as the account email when Graph\n * `/me.mail` is empty (SEC-11). A UPN is NOT guaranteed routable or verified;\n * under the `'unified'` account strategy a UPN equal to a victim's email would\n * link accounts, and it commonly mis-keys an account on a non-deliverable\n * address (reset emails never arrive). Default `false` — only enable when the\n * tenant guarantees UPN == primary SMTP. When `false` and `.mail` is empty the\n * provider reports \"no email\" and the login is rejected rather than linked.\n */\n allowUpnFallback?: boolean;\n}\n\n//? NOTE: Microsoft flow is implemented but has not been end-to-end verified\n//? against an Azure AD tenant as of 2026-05-14. The token-exchange URL, the\n//? Graph /me shape, and the photo data-URL pipeline are based on Microsoft\n//? docs and pattern-matching against Google/GitHub. First consumer to wire a\n//? real Azure tenant should report back so this note can be dropped.\nexport const microsoftProvider = (input: MicrosoftProviderInput): FullOAuthProvider => {\n const tenant = input.tenant ?? 'common';\n const oauthVersion = input.apiVersion ?? 'v2.0';\n const graphVersion = input.graphApiVersion ?? 'v1.0';\n const allowUpnFallback = input.allowUpnFallback ?? false;\n return {\n name: 'microsoft',\n clientID: requireString(input.clientId, 'microsoft clientId'),\n clientSecret: requireString(input.clientSecret, 'microsoft clientSecret'),\n callbackURL: input.callbackUrl,\n authorizationURL: input.endpoints?.authorizationURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/authorize`,\n tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/token`,\n tokenExchangeMethod: 'form',\n userInfoURL: input.endpoints?.userInfoURL ?? `https://graph.microsoft.com/${graphVersion}/me`,\n scope: mergeScopes(['openid', 'profile', 'email', 'User.Read'], input.extraScopes),\n nameKey: 'displayName',\n emailKey: 'mail',\n avatarCodeKey: 'id',\n extraSessionFields: input.extraSessionFields,\n extraAuthorizationParams: input.extraAuthorizationParams,\n usePkce: input.usePkce,\n //? Graph's /photo/$value requires bearer auth, so we can't store the URL —\n //? a browser <img> would 401. Fetch the bytes and inline as a data URL.\n getAvatar: async ({ avatarId, accessToken }) => {\n if (!avatarId) return;\n // 200 KB base64 cap: a data-URI is inlined into the Redis session record.\n // An unbounded photo (multi-MB) would bloat every session read and could\n // exhaust memory or hit Redis value-size limits on large tenants.\n const MAX_PHOTO_BYTES = 200 * 1024;\n const fetchPhoto = async () => {\n //? `encodeURIComponent` the provider-supplied id before interpolating it\n //? into the path: a malicious / malformed id could otherwise inject `../`\n //? segments and redirect the bearer-authenticated fetch to another Graph\n //? resource (SEC-23). The host is fixed, so this is not full SSRF, but the\n //? result is inlined into the session, so still worth closing.\n const response = await fetch(\n `https://graph.microsoft.com/${graphVersion}/users/${encodeURIComponent(avatarId)}/photo/$value`,\n { method: 'GET', headers: { Authorization: `Bearer ${accessToken}` } },\n );\n if (!response.ok) return;\n const rawContentType = response.headers.get('content-type') ?? 'image/jpeg';\n //? SEC: allowlist content-type to safe image MIME types before embedding in\n //? a data-URI that is stored in Redis and later served to the browser.\n const SAFE_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/gif', 'image/webp']);\n const contentType = SAFE_IMAGE_TYPES.has(rawContentType) ? rawContentType : 'image/jpeg';\n const buffer = Buffer.from(await response.arrayBuffer());\n if (buffer.byteLength > MAX_PHOTO_BYTES) return;\n return `data:${contentType};base64,${buffer.toString('base64')}`;\n };\n const [error, dataUrl] = await tryCatch(fetchPhoto);\n if (error) return;\n return dataUrl ?? undefined;\n },\n getEmail: async (accessToken) => {\n const fetchProfile = async () => {\n const response = await fetch(`https://graph.microsoft.com/${graphVersion}/me`, {\n method: 'GET',\n headers: {\n Authorization: `Bearer ${accessToken}`,\n 'Content-Type': 'application/json',\n },\n });\n if (!response.ok) return false;\n const data: unknown = await response.json();\n if (!data || typeof data !== 'object') return false;\n return data as Record<string, unknown>;\n };\n\n const [error, profile] = await tryCatch(fetchProfile);\n if (error || !profile) return false;\n\n const record = asOAuthUserData(profile);\n const mail = typeof record.mail === 'string' ? record.mail : '';\n if (mail) return mail;\n //? Only fall back to `userPrincipalName` when the consumer explicitly opted\n //? in (SEC-11). A UPN is not guaranteed routable/verified; using it as the\n //? account email under `'unified'` enables account-linking takeover and\n //? mis-keys accounts on non-deliverable addresses.\n if (allowUpnFallback) {\n const userPrincipalName = typeof record.userPrincipalName === 'string' ? record.userPrincipalName : '';\n return userPrincipalName || false;\n }\n return false;\n },\n };\n};\n\nlet registeredProviders: OAuthProvider[] = [{ name: 'credentials' }];\n//? LOGIN-01: optional factory that builds the provider list lazily on first\n//? `getOAuthProviders()` call. This lets `register.ts` (the package auto-wire\n//? side-effect) defer all config/env reads to request time rather than baking\n//? them at module-load time. Once resolved the factory is discarded and the\n//? static list is used on every subsequent call (same performance as before).\nlet providerFactory: (() => OAuthProvider[]) | null = null;\n\n//? Always returns the active list. The default value contains only the\n//? `credentials` entry so calls into `loginWithCredentials` keep working in\n//? environments that never register OAuth providers (tests, CLI, etc.).\nexport const getOAuthProviders = (): OAuthProvider[] => {\n if (providerFactory) {\n registeredProviders = providerFactory();\n providerFactory = null;\n }\n return registeredProviders;\n};\n\nexport const registerOAuthProviders = (providers: OAuthProvider[]): OAuthProvider[] => {\n providerFactory = null;\n registeredProviders = providers;\n return registeredProviders;\n};\n\n/**\n * Register a factory function that builds the provider list on first use.\n * Prefer this over `registerOAuthProviders` from the package `register` side-\n * effect so config/env reads are deferred to request time and survive any\n * `registerProjectConfig` call that happens after package import.\n * A subsequent `registerOAuthProviders` call replaces the factory.\n */\nexport const registerOAuthProviderFactory = (factory: () => OAuthProvider[]): void => {\n providerFactory = factory;\n};\n","//? User adapter registry. Decouples auth flows from a specific Prisma User\n//? schema so consumers with different column names, additional fields, or a\n//? non-Prisma data layer can plug their own implementation.\n//?\n//? `defaultPrismaUserAdapter()` implements the framework's recommended schema\n//? (id, email, password?, provider, name, avatar, avatarFallback, admin,\n//? language) and is used automatically when no adapter has been registered.\n\nimport { prisma, type BaseSessionLayout } from '@luckystack/core';\n\nexport interface UserAdapterCreateInput {\n email: string;\n provider: string;\n name?: string | null;\n password?: string | null;\n avatar?: string | null;\n avatarFallback?: string | null;\n language?: string | null;\n}\n\nexport interface UserRecord extends BaseSessionLayout {\n password?: string | null;\n provider?: string | null;\n /** Most recent successful login. Mirrors the optional `lastLogin` column on User. */\n lastLogin?: Date | null;\n /**\n * 2FA (ADR 0024): whether this user enrolled a second factor. All three 2FA\n * fields are OPTIONAL so pre-0.6 schemas/adapters keep working unchanged —\n * an adapter that never returns them simply has no 2FA-capable users.\n */\n twoFactorEnabled?: boolean | null;\n /**\n * 2FA: the TOTP shared secret (base32; `gcm:`-prefixed when encrypted at\n * rest via TOTP_ENCRYPTION_KEY). NEVER reaches the client — stripped by\n * `sanitizeUserForSession` before any session persist/broadcast.\n */\n totpSecret?: string | null;\n /** 2FA: sha256 hashes of the UNUSED one-time recovery codes (also stripped). */\n recoveryCodes?: string[] | null;\n}\n\nexport interface UserAdapter {\n findByEmail: (params: { email: string; provider: string }) => Promise<UserRecord | null>;\n /**\n * Look up a user by email IRRESPECTIVE of provider. Required only when\n * `auth.providerAccountStrategy === 'unified'` — the framework resolves\n * accounts by email alone so the same address maps to one User across\n * providers. Optional so existing custom adapters keep compiling; when a\n * project sets `'unified'` but its adapter omits this method, the framework\n * logs a one-time warning and falls back to provider-scoped lookup.\n */\n findByEmailAnyProvider?: (params: { email: string }) => Promise<UserRecord | null>;\n findById: (id: string) => Promise<UserRecord | null>;\n create: (input: UserAdapterCreateInput) => Promise<UserRecord>;\n update: (id: string, patch: Partial<UserRecord>) => Promise<UserRecord>;\n /**\n * Hard-delete a user. Account-deletion flows (`deleteAccount_v1`) call this\n * instead of reaching past the adapter to `prisma.user.delete`, so a custom\n * adapter / non-Prisma store / soft-delete strategy stays in control. To\n * implement a soft delete, ignore the hard delete here and instead expose it\n * through `update({ deletedAt: new Date() })` from your route.\n *\n * Optional so existing custom adapters keep compiling; the default Prisma\n * adapter implements it. Consumers calling account deletion against a custom\n * adapter that omits `delete` get a clear \"adapter does not support delete\"\n * failure rather than a silent Prisma bypass.\n */\n delete?: (id: string) => Promise<void>;\n}\n\nlet registeredAdapter: UserAdapter | null = null;\n\nexport const registerUserAdapter = (adapter: UserAdapter): UserAdapter => {\n registeredAdapter = adapter;\n return registeredAdapter;\n};\n\nexport const isUserAdapterRegistered = (): boolean => registeredAdapter !== null;\n\n//? Default adapter: backed by the Prisma client exposed from `@luckystack/core`.\n//?\n//? The single `as unknown as PrismaUserDelegate` below is intentional and\n//? load-bearing — it is the abstraction boundary between the framework's\n//? loose `UserAdapterCreateInput` (string-typed fields) and the consumer's\n//? strict Prisma model (which may declare enums for `provider`, `language`,\n//? etc.). The framework cannot know those enum types statically, so it\n//? type-erases once at this seam and assumes the runtime `prisma.user`\n//? satisfies the documented `PrismaUserDelegate` shape. Consumers whose User\n//? schema diverges (different column names, mandatory extra fields) should\n//? register their own `UserAdapter` via `registerUserAdapter(...)` rather\n//? than relying on this default.\ninterface PrismaUserDelegate {\n findFirst: (args: { where: { email: string; provider?: string }; orderBy?: { createdAt?: 'asc' | 'desc' } }) => Promise<UserRecord | null>;\n findUnique: (args: { where: { id: string } }) => Promise<UserRecord | null>;\n create: (args: { data: Record<string, unknown> }) => Promise<UserRecord>;\n update: (args: { where: { id: string }; data: Record<string, unknown> }) => Promise<UserRecord>;\n delete: (args: { where: { id: string } }) => Promise<UserRecord>;\n}\n\n//? Bridge between Prisma's generated client surface and the adapter contract.\n//? Structural cast required because Prisma's auto-generated delegate types\n//? don't structurally match our trimmed `PrismaUserDelegate` shape; this is a\n//? known framework boundary (see /docs/ARCHITECTURE_AUTH.md).\nconst getPrismaUser = (): PrismaUserDelegate =>\n // eslint-disable-next-line no-restricted-syntax -- structural Prisma boundary\n prisma.user as unknown as PrismaUserDelegate;\n\nexport const defaultPrismaUserAdapter = (): UserAdapter => {\n return {\n findByEmail: async ({ email, provider }) =>\n getPrismaUser().findFirst({ where: { email, provider } }),\n //? LOGIN-04: add a deterministic tiebreak so duplicate-email rows (possible\n //? under the 'unified' strategy before `email @unique` is enforced) always\n //? resolve to the OLDEST account. Without orderBy the row returned is\n //? engine-defined — two concurrent logins for the same email can resolve to\n //? different User rows AND the cross-provider link guard flips non-deterministically.\n findByEmailAnyProvider: async ({ email }) =>\n getPrismaUser().findFirst({ where: { email }, orderBy: { createdAt: 'asc' } }),\n findById: async (id) =>\n getPrismaUser().findUnique({ where: { id } }),\n create: async (input) =>\n getPrismaUser().create({\n data: {\n email: input.email,\n provider: input.provider,\n name: input.name,\n password: input.password ?? null,\n avatar: input.avatar ?? '',\n avatarFallback: input.avatarFallback,\n language: input.language,\n },\n }),\n update: async (id, patch) =>\n getPrismaUser().update({ where: { id }, data: patch }),\n delete: async (id) => {\n await getPrismaUser().delete({ where: { id } });\n },\n };\n};\n\nlet cachedDefaultAdapter: UserAdapter | null = null;\n\nexport const getUserAdapter = (): UserAdapter => {\n if (registeredAdapter) return registeredAdapter;\n cachedDefaultAdapter ??= defaultPrismaUserAdapter();\n return cachedDefaultAdapter;\n};\n"],"mappings":"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","names":["emails"]}
|
|
@@ -179,7 +179,7 @@ var saveSession = async (token, data, newUser, options) => {
|
|
|
179
179
|
tokensToKick = previousTokens.slice(0, excess);
|
|
180
180
|
}
|
|
181
181
|
if (tokensToKick.length > 0) {
|
|
182
|
-
const { logout: logout2 } = await import("./logout-
|
|
182
|
+
const { logout: logout2 } = await import("./logout-OBO6AF4R.js");
|
|
183
183
|
await Promise.all(tokensToKick.map(async (previousToken) => {
|
|
184
184
|
const sockets = io.sockets.adapter.rooms.get(previousToken);
|
|
185
185
|
if (sockets) {
|
|
@@ -282,7 +282,7 @@ var deleteSession = async (token, options) => {
|
|
|
282
282
|
const { getIoInstance } = await import("@luckystack/core");
|
|
283
283
|
const ioInstance = getIoInstance();
|
|
284
284
|
if (ioInstance && !options?.skipSocketLogout) {
|
|
285
|
-
const { logout: logout2 } = await import("./logout-
|
|
285
|
+
const { logout: logout2 } = await import("./logout-OBO6AF4R.js");
|
|
286
286
|
const sockets = ioInstance.sockets.adapter.rooms.get(token);
|
|
287
287
|
if (sockets) {
|
|
288
288
|
await Promise.all([...sockets].map(async (socketId) => {
|
|
@@ -398,4 +398,4 @@ export {
|
|
|
398
398
|
sessionKeyFor,
|
|
399
399
|
activeUsersKeyFor
|
|
400
400
|
};
|
|
401
|
-
//# sourceMappingURL=chunk-
|
|
401
|
+
//# sourceMappingURL=chunk-WFPYOCFC.js.map
|