@luckystack/login 0.1.8 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +194 -194
- package/README.md +239 -212
- package/dist/chunk-22NFCMM7.js +299 -0
- package/dist/chunk-22NFCMM7.js.map +1 -0
- package/dist/chunk-NXXBZT3I.js +401 -0
- package/dist/chunk-NXXBZT3I.js.map +1 -0
- package/dist/index.d.ts +281 -90
- package/dist/index.js +529 -792
- package/dist/index.js.map +1 -1
- package/dist/logout-QQRCD2AX.js +7 -0
- package/dist/logout-QQRCD2AX.js.map +1 -0
- package/dist/register.d.ts +2 -0
- package/dist/register.js +69 -0
- package/dist/register.js.map +1 -0
- package/docs/credentials-auth.md +219 -219
- package/docs/hooks.md +444 -444
- package/docs/logout-flow.md +235 -235
- package/docs/oauth-providers.md +431 -431
- package/docs/password-reset.md +343 -343
- package/docs/redirect-validation.md +390 -390
- package/docs/session-management.md +286 -286
- package/docs/user-adapter.md +353 -353
- package/package.json +9 -4
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
// src/oauthProviders.ts
|
|
2
|
+
import { tryCatch } from "@luckystack/core";
|
|
3
|
+
var asOAuthUserData = (value) => {
|
|
4
|
+
if (value && typeof value === "object") {
|
|
5
|
+
return value;
|
|
6
|
+
}
|
|
7
|
+
return {};
|
|
8
|
+
};
|
|
9
|
+
var isFullOAuthProvider = (provider) => provider.name !== "credentials";
|
|
10
|
+
var mergeScopes = (defaults, extra) => {
|
|
11
|
+
if (!extra || extra.length === 0) return defaults;
|
|
12
|
+
return [.../* @__PURE__ */ new Set([...defaults, ...extra])];
|
|
13
|
+
};
|
|
14
|
+
var requireString = (value, label) => {
|
|
15
|
+
if (!value || value.length === 0) {
|
|
16
|
+
throw new Error(`OAuth provider configuration error: ${label} is empty.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
};
|
|
20
|
+
var credentialsProvider = () => ({ name: "credentials" });
|
|
21
|
+
var googleProvider = (input) => ({
|
|
22
|
+
name: "google",
|
|
23
|
+
clientID: requireString(input.clientId, "google clientId"),
|
|
24
|
+
clientSecret: requireString(input.clientSecret, "google clientSecret"),
|
|
25
|
+
callbackURL: input.callbackUrl,
|
|
26
|
+
authorizationURL: input.endpoints?.authorizationURL ?? "https://accounts.google.com/o/oauth2/v2/auth",
|
|
27
|
+
tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? "https://oauth2.googleapis.com/token",
|
|
28
|
+
tokenExchangeMethod: "json",
|
|
29
|
+
//? OIDC userinfo (v3) — the legacy `oauth2/v1/userinfo` is deprecated and
|
|
30
|
+
//? reports verification as `verified_email`; v3 reports the OIDC-standard
|
|
31
|
+
//? `email_verified`. Use v3 so `emailVerifiedKey` below lines up (SEC-21/QUA-24).
|
|
32
|
+
userInfoURL: input.endpoints?.userInfoURL ?? "https://www.googleapis.com/oauth2/v3/userinfo",
|
|
33
|
+
scope: mergeScopes([
|
|
34
|
+
"https://www.googleapis.com/auth/userinfo.profile",
|
|
35
|
+
"https://www.googleapis.com/auth/userinfo.email"
|
|
36
|
+
], input.extraScopes),
|
|
37
|
+
nameKey: "name",
|
|
38
|
+
emailKey: "email",
|
|
39
|
+
//? Google's OIDC userinfo returns `email_verified`; reject an unverified
|
|
40
|
+
//? address so a Google account with an unverified email can't link to a
|
|
41
|
+
//? victim's existing account under the `'unified'` strategy (SEC-21).
|
|
42
|
+
emailVerifiedKey: "email_verified",
|
|
43
|
+
avatarKey: "picture",
|
|
44
|
+
avatarCodeKey: "",
|
|
45
|
+
extraSessionFields: input.extraSessionFields,
|
|
46
|
+
extraAuthorizationParams: input.extraAuthorizationParams,
|
|
47
|
+
usePkce: input.usePkce
|
|
48
|
+
});
|
|
49
|
+
var githubProvider = (input) => ({
|
|
50
|
+
name: "github",
|
|
51
|
+
clientID: requireString(input.clientId, "github clientId"),
|
|
52
|
+
clientSecret: requireString(input.clientSecret, "github clientSecret"),
|
|
53
|
+
callbackURL: input.callbackUrl,
|
|
54
|
+
authorizationURL: input.endpoints?.authorizationURL ?? "https://github.com/login/oauth/authorize",
|
|
55
|
+
tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? "https://github.com/login/oauth/access_token",
|
|
56
|
+
tokenExchangeMethod: "json",
|
|
57
|
+
userInfoURL: input.endpoints?.userInfoURL ?? "https://api.github.com/user",
|
|
58
|
+
scope: mergeScopes(["read:user", "user:email"], input.extraScopes),
|
|
59
|
+
nameKey: "login",
|
|
60
|
+
emailKey: "email",
|
|
61
|
+
avatarKey: "avatar_url",
|
|
62
|
+
avatarCodeKey: "",
|
|
63
|
+
extraSessionFields: input.extraSessionFields,
|
|
64
|
+
extraAuthorizationParams: input.extraAuthorizationParams,
|
|
65
|
+
usePkce: input.usePkce,
|
|
66
|
+
getEmail: async (accessToken) => {
|
|
67
|
+
const fetchEmails = async () => {
|
|
68
|
+
const response = await fetch("https://api.github.com/user/emails", {
|
|
69
|
+
method: "GET",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
Accept: "application/json",
|
|
73
|
+
Authorization: `Bearer ${accessToken}`
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) return false;
|
|
77
|
+
const emails2 = await response.json();
|
|
78
|
+
if (!Array.isArray(emails2)) return false;
|
|
79
|
+
return emails2;
|
|
80
|
+
};
|
|
81
|
+
const [error, emails] = await tryCatch(fetchEmails);
|
|
82
|
+
if (error || !emails) return false;
|
|
83
|
+
const verified = emails.filter((entry) => entry.verified === true);
|
|
84
|
+
let mainEmail;
|
|
85
|
+
for (const entry of verified) {
|
|
86
|
+
if (entry.primary && typeof entry.email === "string") mainEmail = entry.email;
|
|
87
|
+
}
|
|
88
|
+
mainEmail ??= verified[0]?.email;
|
|
89
|
+
return mainEmail;
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
var discordProvider = (input) => ({
|
|
93
|
+
name: "discord",
|
|
94
|
+
clientID: requireString(input.clientId, "discord clientId"),
|
|
95
|
+
clientSecret: requireString(input.clientSecret, "discord clientSecret"),
|
|
96
|
+
callbackURL: input.callbackUrl,
|
|
97
|
+
authorizationURL: input.endpoints?.authorizationURL ?? "https://discord.com/oauth2/authorize",
|
|
98
|
+
tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? "https://discord.com/api/oauth2/token",
|
|
99
|
+
tokenExchangeMethod: "form",
|
|
100
|
+
userInfoURL: input.endpoints?.userInfoURL ?? "https://discord.com/api/users/@me",
|
|
101
|
+
scope: mergeScopes(["identify", "email"], input.extraScopes),
|
|
102
|
+
nameKey: "username",
|
|
103
|
+
emailKey: "email",
|
|
104
|
+
//? Discord's /users/@me returns a `verified` boolean for the account email;
|
|
105
|
+
//? reject the address when it is explicitly unverified (SEC-21).
|
|
106
|
+
emailVerifiedKey: "verified",
|
|
107
|
+
avatarCodeKey: "avatar",
|
|
108
|
+
extraSessionFields: input.extraSessionFields,
|
|
109
|
+
extraAuthorizationParams: input.extraAuthorizationParams,
|
|
110
|
+
usePkce: input.usePkce,
|
|
111
|
+
getAvatar: ({ userData, avatarId }) => {
|
|
112
|
+
if (!avatarId) return;
|
|
113
|
+
const userId = typeof userData.id === "string" ? userData.id : "";
|
|
114
|
+
if (!userId) return;
|
|
115
|
+
const format = avatarId.startsWith("a_") ? "gif" : "png";
|
|
116
|
+
return `https://cdn.discordapp.com/avatars/${encodeURIComponent(userId)}/${encodeURIComponent(avatarId)}.${format}`;
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
var facebookProvider = (input) => {
|
|
120
|
+
const v = input.apiVersion ?? "v18.0";
|
|
121
|
+
return {
|
|
122
|
+
name: "facebook",
|
|
123
|
+
clientID: requireString(input.clientId, "facebook clientId"),
|
|
124
|
+
clientSecret: requireString(input.clientSecret, "facebook clientSecret"),
|
|
125
|
+
callbackURL: input.callbackUrl,
|
|
126
|
+
authorizationURL: input.endpoints?.authorizationURL ?? `https://www.facebook.com/${v}/dialog/oauth`,
|
|
127
|
+
tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://graph.facebook.com/${v}/oauth/access_token`,
|
|
128
|
+
tokenExchangeMethod: "form",
|
|
129
|
+
userInfoURL: input.endpoints?.userInfoURL ?? "https://graph.facebook.com/me?fields=id,name,email,picture.type(large)",
|
|
130
|
+
scope: mergeScopes(["public_profile", "email"], input.extraScopes),
|
|
131
|
+
nameKey: "name",
|
|
132
|
+
emailKey: "email",
|
|
133
|
+
//? Facebook's Graph `/me` only returns `email` for an address the user has
|
|
134
|
+
//? CONFIRMED with Facebook (unconfirmed emails are omitted), so there is no
|
|
135
|
+
//? separate verified flag to gate on — the presence of `email` IS the verified
|
|
136
|
+
//? signal. If a consumer opts into receiving unverified emails via a future
|
|
137
|
+
//? Graph field, set `emailVerifiedKey` on the returned provider (SEC-21).
|
|
138
|
+
avatarCodeKey: "",
|
|
139
|
+
extraSessionFields: input.extraSessionFields,
|
|
140
|
+
extraAuthorizationParams: input.extraAuthorizationParams,
|
|
141
|
+
usePkce: input.usePkce,
|
|
142
|
+
getAvatar: ({ userData }) => {
|
|
143
|
+
const picture = asOAuthUserData(userData.picture);
|
|
144
|
+
const pictureData = asOAuthUserData(picture.data);
|
|
145
|
+
const url = pictureData.url;
|
|
146
|
+
return typeof url === "string" ? url : void 0;
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
var microsoftProvider = (input) => {
|
|
151
|
+
const tenant = input.tenant ?? "common";
|
|
152
|
+
const oauthVersion = input.apiVersion ?? "v2.0";
|
|
153
|
+
const graphVersion = input.graphApiVersion ?? "v1.0";
|
|
154
|
+
const allowUpnFallback = input.allowUpnFallback ?? false;
|
|
155
|
+
return {
|
|
156
|
+
name: "microsoft",
|
|
157
|
+
clientID: requireString(input.clientId, "microsoft clientId"),
|
|
158
|
+
clientSecret: requireString(input.clientSecret, "microsoft clientSecret"),
|
|
159
|
+
callbackURL: input.callbackUrl,
|
|
160
|
+
authorizationURL: input.endpoints?.authorizationURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/authorize`,
|
|
161
|
+
tokenExchangeURL: input.endpoints?.tokenExchangeURL ?? `https://login.microsoftonline.com/${tenant}/oauth2/${oauthVersion}/token`,
|
|
162
|
+
tokenExchangeMethod: "form",
|
|
163
|
+
userInfoURL: input.endpoints?.userInfoURL ?? `https://graph.microsoft.com/${graphVersion}/me`,
|
|
164
|
+
scope: mergeScopes(["openid", "profile", "email", "User.Read"], input.extraScopes),
|
|
165
|
+
nameKey: "displayName",
|
|
166
|
+
emailKey: "mail",
|
|
167
|
+
avatarCodeKey: "id",
|
|
168
|
+
extraSessionFields: input.extraSessionFields,
|
|
169
|
+
extraAuthorizationParams: input.extraAuthorizationParams,
|
|
170
|
+
usePkce: input.usePkce,
|
|
171
|
+
//? Graph's /photo/$value requires bearer auth, so we can't store the URL —
|
|
172
|
+
//? a browser <img> would 401. Fetch the bytes and inline as a data URL.
|
|
173
|
+
getAvatar: async ({ avatarId, accessToken }) => {
|
|
174
|
+
if (!avatarId) return;
|
|
175
|
+
const MAX_PHOTO_BYTES = 200 * 1024;
|
|
176
|
+
const fetchPhoto = async () => {
|
|
177
|
+
const response = await fetch(
|
|
178
|
+
`https://graph.microsoft.com/${graphVersion}/users/${encodeURIComponent(avatarId)}/photo/$value`,
|
|
179
|
+
{ method: "GET", headers: { Authorization: `Bearer ${accessToken}` } }
|
|
180
|
+
);
|
|
181
|
+
if (!response.ok) return;
|
|
182
|
+
const contentType = response.headers.get("content-type") ?? "image/jpeg";
|
|
183
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
184
|
+
if (buffer.byteLength > MAX_PHOTO_BYTES) return;
|
|
185
|
+
return `data:${contentType};base64,${buffer.toString("base64")}`;
|
|
186
|
+
};
|
|
187
|
+
const [error, dataUrl] = await tryCatch(fetchPhoto);
|
|
188
|
+
if (error) return;
|
|
189
|
+
return dataUrl ?? void 0;
|
|
190
|
+
},
|
|
191
|
+
getEmail: async (accessToken) => {
|
|
192
|
+
const fetchProfile = async () => {
|
|
193
|
+
const response = await fetch(`https://graph.microsoft.com/${graphVersion}/me`, {
|
|
194
|
+
method: "GET",
|
|
195
|
+
headers: {
|
|
196
|
+
Authorization: `Bearer ${accessToken}`,
|
|
197
|
+
"Content-Type": "application/json"
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (!response.ok) return false;
|
|
201
|
+
const data = await response.json();
|
|
202
|
+
if (!data || typeof data !== "object") return false;
|
|
203
|
+
return data;
|
|
204
|
+
};
|
|
205
|
+
const [error, profile] = await tryCatch(fetchProfile);
|
|
206
|
+
if (error || !profile) return false;
|
|
207
|
+
const record = asOAuthUserData(profile);
|
|
208
|
+
const mail = typeof record.mail === "string" ? record.mail : "";
|
|
209
|
+
if (mail) return mail;
|
|
210
|
+
if (allowUpnFallback) {
|
|
211
|
+
const userPrincipalName = typeof record.userPrincipalName === "string" ? record.userPrincipalName : "";
|
|
212
|
+
return userPrincipalName || false;
|
|
213
|
+
}
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
};
|
|
218
|
+
var registeredProviders = [{ name: "credentials" }];
|
|
219
|
+
var providerFactory = null;
|
|
220
|
+
var getOAuthProviders = () => {
|
|
221
|
+
if (providerFactory) {
|
|
222
|
+
registeredProviders = providerFactory();
|
|
223
|
+
providerFactory = null;
|
|
224
|
+
}
|
|
225
|
+
return registeredProviders;
|
|
226
|
+
};
|
|
227
|
+
var registerOAuthProviders = (providers) => {
|
|
228
|
+
providerFactory = null;
|
|
229
|
+
registeredProviders = providers;
|
|
230
|
+
return registeredProviders;
|
|
231
|
+
};
|
|
232
|
+
var registerOAuthProviderFactory = (factory) => {
|
|
233
|
+
providerFactory = factory;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/userAdapter.ts
|
|
237
|
+
import { prisma } from "@luckystack/core";
|
|
238
|
+
var registeredAdapter = null;
|
|
239
|
+
var registerUserAdapter = (adapter) => {
|
|
240
|
+
registeredAdapter = adapter;
|
|
241
|
+
return registeredAdapter;
|
|
242
|
+
};
|
|
243
|
+
var isUserAdapterRegistered = () => registeredAdapter !== null;
|
|
244
|
+
var getPrismaUser = () => (
|
|
245
|
+
// eslint-disable-next-line no-restricted-syntax -- structural Prisma boundary
|
|
246
|
+
prisma.user
|
|
247
|
+
);
|
|
248
|
+
var defaultPrismaUserAdapter = () => {
|
|
249
|
+
return {
|
|
250
|
+
findByEmail: async ({ email, provider }) => getPrismaUser().findFirst({ where: { email, provider } }),
|
|
251
|
+
//? LOGIN-04: add a deterministic tiebreak so duplicate-email rows (possible
|
|
252
|
+
//? under the 'unified' strategy before `email @unique` is enforced) always
|
|
253
|
+
//? resolve to the OLDEST account. Without orderBy the row returned is
|
|
254
|
+
//? engine-defined — two concurrent logins for the same email can resolve to
|
|
255
|
+
//? different User rows AND the cross-provider link guard flips non-deterministically.
|
|
256
|
+
findByEmailAnyProvider: async ({ email }) => getPrismaUser().findFirst({ where: { email }, orderBy: { createdAt: "asc" } }),
|
|
257
|
+
findById: async (id) => getPrismaUser().findUnique({ where: { id } }),
|
|
258
|
+
create: async (input) => getPrismaUser().create({
|
|
259
|
+
data: {
|
|
260
|
+
email: input.email,
|
|
261
|
+
provider: input.provider,
|
|
262
|
+
name: input.name,
|
|
263
|
+
password: input.password ?? null,
|
|
264
|
+
avatar: input.avatar ?? "",
|
|
265
|
+
avatarFallback: input.avatarFallback,
|
|
266
|
+
language: input.language
|
|
267
|
+
}
|
|
268
|
+
}),
|
|
269
|
+
update: async (id, patch) => getPrismaUser().update({ where: { id }, data: patch }),
|
|
270
|
+
delete: async (id) => {
|
|
271
|
+
await getPrismaUser().delete({ where: { id } });
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
};
|
|
275
|
+
var cachedDefaultAdapter = null;
|
|
276
|
+
var getUserAdapter = () => {
|
|
277
|
+
if (registeredAdapter) return registeredAdapter;
|
|
278
|
+
cachedDefaultAdapter ??= defaultPrismaUserAdapter();
|
|
279
|
+
return cachedDefaultAdapter;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
export {
|
|
283
|
+
asOAuthUserData,
|
|
284
|
+
isFullOAuthProvider,
|
|
285
|
+
credentialsProvider,
|
|
286
|
+
googleProvider,
|
|
287
|
+
githubProvider,
|
|
288
|
+
discordProvider,
|
|
289
|
+
facebookProvider,
|
|
290
|
+
microsoftProvider,
|
|
291
|
+
getOAuthProviders,
|
|
292
|
+
registerOAuthProviders,
|
|
293
|
+
registerOAuthProviderFactory,
|
|
294
|
+
registerUserAdapter,
|
|
295
|
+
isUserAdapterRegistered,
|
|
296
|
+
defaultPrismaUserAdapter,
|
|
297
|
+
getUserAdapter
|
|
298
|
+
};
|
|
299
|
+
//# sourceMappingURL=chunk-22NFCMM7.js.map
|
|
@@ -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 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. If a consumer opts into receiving unverified emails via a future\n //? Graph field, set `emailVerifiedKey` on the returned provider (SEC-21).\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 contentType = response.headers.get('content-type') ?? '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\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"]}
|