@rpcbase/auth 0.89.0 → 0.91.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.
@@ -0,0 +1,578 @@
1
+ import crypto from "crypto";
2
+ import { models } from "@rpcbase/db";
3
+ import { hashPasswordForStorage } from "@rpcbase/server";
4
+ const STORE_KEY = /* @__PURE__ */ Symbol.for("@rpcbase/auth/oauthProviders");
5
+ const getStore = () => {
6
+ const anyGlobal = globalThis;
7
+ const existing = anyGlobal[STORE_KEY];
8
+ if (existing && typeof existing === "object" && existing.providers && typeof existing.providers === "object") {
9
+ return existing;
10
+ }
11
+ const created = { providers: {} };
12
+ anyGlobal[STORE_KEY] = created;
13
+ return created;
14
+ };
15
+ const normalizeProviders = (providers) => {
16
+ const result = {};
17
+ const isSafeRedirectPath2 = (candidate) => {
18
+ if (!candidate.startsWith("/")) return false;
19
+ if (candidate.startsWith("//")) return false;
20
+ return true;
21
+ };
22
+ for (const [providerIdRaw, value] of Object.entries(providers)) {
23
+ const providerId = providerIdRaw.trim();
24
+ if (!providerId) continue;
25
+ const issuer = typeof value?.issuer === "string" ? value.issuer.trim() : "";
26
+ const clientId = typeof value?.clientId === "string" ? value.clientId.trim() : "";
27
+ if (!issuer) throw new Error(`oauth provider "${providerId}" missing issuer`);
28
+ if (!clientId) throw new Error(`oauth provider "${providerId}" missing clientId`);
29
+ const clientSecret = typeof value?.clientSecret === "string" && value.clientSecret.trim() ? value.clientSecret : void 0;
30
+ const scope = typeof value?.scope === "string" ? value.scope.trim() : "";
31
+ if (!scope) throw new Error(`oauth provider "${providerId}" missing scope`);
32
+ const callbackPath = typeof value?.callbackPath === "string" ? value.callbackPath.trim() : "";
33
+ if (!callbackPath) throw new Error(`oauth provider "${providerId}" missing callbackPath`);
34
+ if (!isSafeRedirectPath2(callbackPath)) throw new Error(`oauth provider "${providerId}" has invalid callbackPath`);
35
+ result[providerId] = {
36
+ issuer,
37
+ clientId,
38
+ clientSecret,
39
+ scope,
40
+ callbackPath
41
+ };
42
+ }
43
+ return result;
44
+ };
45
+ const configureOAuthProviders = (providers) => {
46
+ const store = getStore();
47
+ const normalized = normalizeProviders(providers);
48
+ store.providers = { ...store.providers, ...normalized };
49
+ };
50
+ const setOAuthProviders = (providers) => {
51
+ const store = getStore();
52
+ store.providers = normalizeProviders(providers);
53
+ };
54
+ const getOAuthProviderConfig = (providerId) => {
55
+ const store = getStore();
56
+ return store.providers[providerId] ?? null;
57
+ };
58
+ const decodeBase64Url = (value) => {
59
+ const padded = value.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((value.length + 3) % 4);
60
+ return Buffer.from(padded, "base64").toString("utf8");
61
+ };
62
+ const decodeJwtPayload = (token) => {
63
+ const parts = token.split(".");
64
+ if (parts.length < 2) return null;
65
+ try {
66
+ const json = decodeBase64Url(parts[1]);
67
+ const parsed = JSON.parse(json);
68
+ if (!parsed || typeof parsed !== "object") return null;
69
+ return parsed;
70
+ } catch {
71
+ return null;
72
+ }
73
+ };
74
+ const cache = /* @__PURE__ */ new Map();
75
+ const normalizeIssuer = (raw) => raw.trim().replace(/\/+$/, "");
76
+ const getOidcWellKnown = async (issuerRaw) => {
77
+ const issuer = normalizeIssuer(issuerRaw);
78
+ if (!issuer) {
79
+ throw new Error("OIDC issuer is required");
80
+ }
81
+ const existing = cache.get(issuer);
82
+ if (existing) {
83
+ return existing;
84
+ }
85
+ const loader = (async () => {
86
+ const url = new URL("/.well-known/openid-configuration", issuer);
87
+ const response = await fetch(url, { headers: { Accept: "application/json" } });
88
+ if (!response.ok) {
89
+ const body = await response.text().catch(() => "");
90
+ throw new Error(`Failed to fetch OIDC discovery document: ${response.status} ${body}`);
91
+ }
92
+ const json = await response.json().catch(() => null);
93
+ if (!json || typeof json !== "object") {
94
+ throw new Error("Invalid OIDC discovery document");
95
+ }
96
+ const authorizationEndpoint = typeof json.authorization_endpoint === "string" ? json.authorization_endpoint : "";
97
+ const tokenEndpoint = typeof json.token_endpoint === "string" ? json.token_endpoint : "";
98
+ const issuerValue = typeof json.issuer === "string" ? json.issuer : issuer;
99
+ const userinfoEndpoint = typeof json.userinfo_endpoint === "string" ? json.userinfo_endpoint : void 0;
100
+ const jwksUri = typeof json.jwks_uri === "string" ? json.jwks_uri : void 0;
101
+ if (!authorizationEndpoint || !tokenEndpoint) {
102
+ throw new Error("OIDC discovery document missing required endpoints");
103
+ }
104
+ return {
105
+ issuer: issuerValue,
106
+ authorization_endpoint: authorizationEndpoint,
107
+ token_endpoint: tokenEndpoint,
108
+ userinfo_endpoint: userinfoEndpoint,
109
+ jwks_uri: jwksUri
110
+ };
111
+ })();
112
+ cache.set(issuer, loader);
113
+ try {
114
+ return await loader;
115
+ } catch (err) {
116
+ cache.delete(issuer);
117
+ throw err;
118
+ }
119
+ };
120
+ const base64UrlEncode$1 = (input) => input.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
121
+ const generatePkcePair = () => {
122
+ const verifier = base64UrlEncode$1(crypto.randomBytes(32));
123
+ const challenge = base64UrlEncode$1(crypto.createHash("sha256").update(verifier).digest());
124
+ return { verifier, challenge };
125
+ };
126
+ const RESERVED_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
127
+ const isSafeOAuthProviderId = (value) => {
128
+ const providerId = value.trim();
129
+ if (!providerId) return false;
130
+ if (providerId.length > 128) return false;
131
+ if (RESERVED_KEYS.has(providerId)) return false;
132
+ return /^[a-zA-Z0-9_-]+$/.test(providerId);
133
+ };
134
+ const getQueryString = (value) => {
135
+ if (typeof value === "string") return value;
136
+ if (Array.isArray(value) && typeof value[0] === "string") return value[0];
137
+ return null;
138
+ };
139
+ const isSafeRedirectPath = (candidate) => {
140
+ if (!candidate.startsWith("/")) return false;
141
+ if (candidate.startsWith("//")) return false;
142
+ return true;
143
+ };
144
+ const getRequestOrigin = (ctx) => {
145
+ const protoHeader = ctx.req.headers["x-forwarded-proto"];
146
+ const hostHeader = ctx.req.headers["x-forwarded-host"];
147
+ const protocol = typeof protoHeader === "string" ? protoHeader.split(",")[0].trim() : ctx.req.protocol;
148
+ const host = typeof hostHeader === "string" ? hostHeader.split(",")[0].trim() : ctx.req.get("host");
149
+ return protocol && host ? `${protocol}://${host}` : "";
150
+ };
151
+ const resolveCallbackPath = (callbackPath) => {
152
+ const trimmed = callbackPath.trim();
153
+ return trimmed && isSafeRedirectPath(trimmed) ? trimmed : null;
154
+ };
155
+ const readTextBody = async (req) => await new Promise((resolve, reject) => {
156
+ let body = "";
157
+ if (typeof req.setEncoding === "function") {
158
+ req.setEncoding("utf8");
159
+ }
160
+ req.on("data", (chunk) => {
161
+ body += chunk;
162
+ if (body.length > 32768) {
163
+ reject(new Error("request_body_too_large"));
164
+ }
165
+ });
166
+ req.on("end", () => resolve(body));
167
+ req.on("error", reject);
168
+ });
169
+ const getFormPostParams = async (ctx) => {
170
+ if (ctx.req.method !== "POST") return null;
171
+ const contentType = typeof ctx.req.headers["content-type"] === "string" ? ctx.req.headers["content-type"] : "";
172
+ if (!contentType.includes("application/x-www-form-urlencoded")) return null;
173
+ const body = await readTextBody(ctx.req);
174
+ const params = new URLSearchParams(body);
175
+ const result = {};
176
+ for (const [key, value] of params.entries()) {
177
+ result[key] = value;
178
+ }
179
+ return result;
180
+ };
181
+ const resolveProviderId = (ctx, providerIdOverride) => (providerIdOverride ?? String(ctx.req.params?.provider ?? "")).trim();
182
+ const RESERVED_AUTH_PARAM_KEYS = /* @__PURE__ */ new Set(["__proto__", "prototype", "constructor"]);
183
+ const normalizeAuthorizationParams = (value) => {
184
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
185
+ const result = {};
186
+ for (const [keyRaw, valueRaw] of Object.entries(value)) {
187
+ const key = keyRaw.trim();
188
+ if (!key) continue;
189
+ if (RESERVED_AUTH_PARAM_KEYS.has(key)) continue;
190
+ const val = typeof valueRaw === "string" ? valueRaw.trim() : "";
191
+ if (!val) continue;
192
+ result[key] = val;
193
+ }
194
+ return Object.keys(result).length ? result : void 0;
195
+ };
196
+ const getOAuthStartRedirectUrl = async ({
197
+ ctx,
198
+ providerId: providerIdOverride,
199
+ returnTo,
200
+ authorizationParams
201
+ }) => {
202
+ const providerId = resolveProviderId(ctx, providerIdOverride);
203
+ if (!providerId) {
204
+ return { success: false, error: "missing_provider", statusCode: 400 };
205
+ }
206
+ if (!isSafeOAuthProviderId(providerId)) {
207
+ return { success: false, error: "invalid_provider", statusCode: 400 };
208
+ }
209
+ const provider = getOAuthProviderConfig(providerId);
210
+ if (!provider) {
211
+ return { success: false, error: "unknown_provider", statusCode: 404 };
212
+ }
213
+ if (!ctx.req.session) {
214
+ return { success: false, error: "session_unavailable", statusCode: 500 };
215
+ }
216
+ const origin = getRequestOrigin(ctx);
217
+ if (!origin) {
218
+ return { success: false, error: "origin_unavailable", statusCode: 500 };
219
+ }
220
+ const callbackPath = resolveCallbackPath(provider.callbackPath);
221
+ if (!callbackPath) {
222
+ return { success: false, error: "invalid_callback_path", statusCode: 500 };
223
+ }
224
+ const state = crypto.randomBytes(16).toString("hex");
225
+ const { verifier, challenge } = generatePkcePair();
226
+ const sessionAny = ctx.req.session;
227
+ const rbOauthRaw = sessionAny.rbOauth;
228
+ if (!rbOauthRaw || typeof rbOauthRaw !== "object" || Array.isArray(rbOauthRaw)) {
229
+ sessionAny.rbOauth = /* @__PURE__ */ Object.create(null);
230
+ } else if (Object.getPrototypeOf(rbOauthRaw) !== null) {
231
+ sessionAny.rbOauth = Object.assign(/* @__PURE__ */ Object.create(null), rbOauthRaw);
232
+ }
233
+ const safeReturnTo = typeof returnTo === "string" && isSafeRedirectPath(returnTo) ? returnTo : void 0;
234
+ sessionAny.rbOauth[providerId] = {
235
+ state,
236
+ codeVerifier: verifier,
237
+ createdAt: Date.now(),
238
+ returnTo: safeReturnTo
239
+ };
240
+ const oidc = await getOidcWellKnown(provider.issuer);
241
+ const redirectUri = `${origin}${callbackPath}`;
242
+ const url = new URL(oidc.authorization_endpoint);
243
+ url.searchParams.set("client_id", provider.clientId);
244
+ url.searchParams.set("redirect_uri", redirectUri);
245
+ url.searchParams.set("response_type", "code");
246
+ url.searchParams.set("scope", provider.scope);
247
+ url.searchParams.set("state", state);
248
+ url.searchParams.set("code_challenge", challenge);
249
+ url.searchParams.set("code_challenge_method", "S256");
250
+ const extraAuthorizationParams = normalizeAuthorizationParams(authorizationParams);
251
+ for (const [key, value] of Object.entries(extraAuthorizationParams ?? {})) {
252
+ if (url.searchParams.has(key)) continue;
253
+ url.searchParams.set(key, value);
254
+ }
255
+ return { success: true, redirectUrl: url.toString() };
256
+ };
257
+ const OAUTH_SUCCESS_REDIRECT_PATH = "/onboarding";
258
+ const AUTH_ERROR_REDIRECT_BASE = "/auth/sign-in";
259
+ const processOAuthCallback = async ({
260
+ ctx,
261
+ providerId: providerIdOverride,
262
+ createUserOnFirstSignIn,
263
+ missingUserRedirectPath,
264
+ successRedirectPath
265
+ }) => {
266
+ const providerId = resolveProviderId(ctx, providerIdOverride);
267
+ if (!providerId) {
268
+ return { success: false, type: "error", error: "missing_provider", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_provider` };
269
+ }
270
+ if (!isSafeOAuthProviderId(providerId)) {
271
+ return { success: false, type: "error", error: "invalid_provider", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_provider` };
272
+ }
273
+ const provider = getOAuthProviderConfig(providerId);
274
+ if (!provider) {
275
+ return { success: false, type: "error", error: "unknown_provider", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=unknown_provider` };
276
+ }
277
+ if (!ctx.req.session) {
278
+ return { success: false, type: "error", error: "session_unavailable", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=session_unavailable` };
279
+ }
280
+ const origin = getRequestOrigin(ctx);
281
+ if (!origin) {
282
+ return { success: false, type: "error", error: "origin_unavailable", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=origin_unavailable` };
283
+ }
284
+ const callbackPath = resolveCallbackPath(provider.callbackPath);
285
+ if (!callbackPath) {
286
+ return { success: false, type: "error", error: "invalid_callback_path", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_callback_path` };
287
+ }
288
+ const bodyParams = await getFormPostParams(ctx);
289
+ const code = (getQueryString(bodyParams?.code) ?? getQueryString(ctx.req.query?.code))?.trim() ?? "";
290
+ const state = (getQueryString(bodyParams?.state) ?? getQueryString(ctx.req.query?.state))?.trim() ?? "";
291
+ const oauthUserRaw = (getQueryString(bodyParams?.user) ?? getQueryString(ctx.req.query?.user))?.trim() ?? "";
292
+ const sessionAny = ctx.req.session;
293
+ const sessionState = sessionAny.rbOauth?.[providerId];
294
+ const returnTo = typeof sessionState?.returnTo === "string" ? sessionState.returnTo : void 0;
295
+ if (!code || !state) {
296
+ return { success: false, type: "error", error: "missing_code_or_state", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_code_or_state` };
297
+ }
298
+ if (!sessionState || typeof sessionState.state !== "string" || sessionState.state !== state) {
299
+ return { success: false, type: "error", error: "invalid_state", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=invalid_state` };
300
+ }
301
+ const codeVerifier = typeof sessionState.codeVerifier === "string" ? sessionState.codeVerifier : "";
302
+ if (!codeVerifier) {
303
+ return { success: false, type: "error", error: "missing_code_verifier", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_code_verifier` };
304
+ }
305
+ delete sessionAny.rbOauth?.[providerId];
306
+ if (sessionAny.rbOauth && Object.keys(sessionAny.rbOauth).length === 0) {
307
+ delete sessionAny.rbOauth;
308
+ }
309
+ const oidc = await getOidcWellKnown(provider.issuer);
310
+ const redirectUri = `${origin}${callbackPath}`;
311
+ const tokenBody = new URLSearchParams();
312
+ tokenBody.set("grant_type", "authorization_code");
313
+ tokenBody.set("code", code);
314
+ tokenBody.set("redirect_uri", redirectUri);
315
+ tokenBody.set("client_id", provider.clientId);
316
+ tokenBody.set("code_verifier", codeVerifier);
317
+ if (provider.clientSecret) {
318
+ tokenBody.set("client_secret", provider.clientSecret);
319
+ }
320
+ const tokenResponse = await fetch(oidc.token_endpoint, {
321
+ method: "POST",
322
+ headers: {
323
+ "Content-Type": "application/x-www-form-urlencoded",
324
+ Accept: "application/json"
325
+ },
326
+ body: tokenBody.toString()
327
+ });
328
+ const tokenJson = await tokenResponse.json().catch(() => null);
329
+ if (!tokenResponse.ok || !tokenJson || typeof tokenJson !== "object") {
330
+ const body = tokenJson ? JSON.stringify(tokenJson) : "";
331
+ console.warn("oauth::token_exchange failed", tokenResponse.status, body);
332
+ return { success: false, type: "error", error: "token_exchange_failed", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=token_exchange_failed` };
333
+ }
334
+ const accessToken = typeof tokenJson.access_token === "string" ? tokenJson.access_token : "";
335
+ const refreshToken = typeof tokenJson.refresh_token === "string" ? tokenJson.refresh_token : void 0;
336
+ const idToken = typeof tokenJson.id_token === "string" ? tokenJson.id_token : void 0;
337
+ const scope = typeof tokenJson.scope === "string" ? tokenJson.scope : void 0;
338
+ const tokenType = typeof tokenJson.token_type === "string" ? tokenJson.token_type : void 0;
339
+ const expiresIn = typeof tokenJson.expires_in === "number" ? tokenJson.expires_in : typeof tokenJson.expires_in === "string" ? Number(tokenJson.expires_in) : void 0;
340
+ const expiresInSeconds = typeof expiresIn === "number" && Number.isFinite(expiresIn) ? expiresIn : null;
341
+ const expiresAt = expiresInSeconds !== null ? new Date(Date.now() + expiresInSeconds * 1e3) : void 0;
342
+ let userInfo = null;
343
+ if (oidc.userinfo_endpoint && accessToken) {
344
+ const userInfoResponse = await fetch(oidc.userinfo_endpoint, {
345
+ headers: {
346
+ Authorization: `Bearer ${accessToken}`,
347
+ Accept: "application/json"
348
+ }
349
+ });
350
+ if (userInfoResponse.ok) {
351
+ userInfo = await userInfoResponse.json().catch(() => null);
352
+ }
353
+ }
354
+ const idTokenPayload = idToken ? decodeJwtPayload(idToken) : null;
355
+ const accessTokenPayload = decodeJwtPayload(accessToken);
356
+ const subject = typeof userInfo?.sub === "string" ? userInfo.sub : typeof idTokenPayload?.sub === "string" ? idTokenPayload.sub : typeof accessTokenPayload?.sub === "string" ? accessTokenPayload.sub : "";
357
+ if (!subject) {
358
+ return { success: false, type: "error", error: "missing_subject", redirectPath: `${AUTH_ERROR_REDIRECT_BASE}?error=missing_subject` };
359
+ }
360
+ const email = typeof userInfo?.email === "string" ? userInfo.email : typeof idTokenPayload?.email === "string" ? idTokenPayload.email : void 0;
361
+ const name = typeof userInfo?.name === "string" ? userInfo.name : typeof idTokenPayload?.name === "string" ? idTokenPayload.name : void 0;
362
+ let oauthUser = null;
363
+ if (oauthUserRaw) {
364
+ try {
365
+ oauthUser = JSON.parse(oauthUserRaw);
366
+ } catch {
367
+ oauthUser = null;
368
+ }
369
+ }
370
+ const oauthUserEmail = typeof oauthUser?.email === "string" ? String(oauthUser.email).trim() : "";
371
+ const oauthUserNameFirst = typeof oauthUser?.name?.firstName === "string" ? String(oauthUser.name.firstName).trim() : "";
372
+ const oauthUserNameLast = typeof oauthUser?.name?.lastName === "string" ? String(oauthUser.name.lastName).trim() : "";
373
+ const oauthUserName = [oauthUserNameFirst, oauthUserNameLast].filter(Boolean).join(" ").trim();
374
+ const resolvedEmail = email ?? (oauthUserEmail || void 0);
375
+ const resolvedName = name ?? (oauthUserName || void 0);
376
+ const [User, Tenant] = await Promise.all([
377
+ models.getGlobal("RBUser", ctx),
378
+ models.getGlobal("RBTenant", ctx)
379
+ ]);
380
+ const subjectQueryKey = `oauthProviders.${providerId}.subject`;
381
+ let user = await User.findOne({ [subjectQueryKey]: subject });
382
+ if (!user && resolvedEmail) {
383
+ user = await User.findOne({ email: resolvedEmail });
384
+ }
385
+ const shouldCreateUser = createUserOnFirstSignIn ?? true;
386
+ if (!user && !shouldCreateUser) {
387
+ const redirectPath2 = missingUserRedirectPath;
388
+ const result = {
389
+ success: false,
390
+ type: "missing_user",
391
+ providerId,
392
+ subject,
393
+ email: resolvedEmail,
394
+ name: resolvedName
395
+ };
396
+ if (redirectPath2 && isSafeRedirectPath(redirectPath2)) {
397
+ const url = new URL(redirectPath2, origin);
398
+ url.searchParams.set("oauth_provider", providerId);
399
+ if (resolvedEmail) url.searchParams.set("email", resolvedEmail);
400
+ if (resolvedName) url.searchParams.set("name", resolvedName);
401
+ result.redirectPath = `${url.pathname}${url.search}`;
402
+ }
403
+ return result;
404
+ }
405
+ const now = /* @__PURE__ */ new Date();
406
+ let providerCreatedAt;
407
+ const oauthProvidersValue = user ? user.oauthProviders : void 0;
408
+ if (oauthProvidersValue instanceof Map) {
409
+ const existing = oauthProvidersValue.get(providerId);
410
+ if (existing?.createdAt instanceof Date) providerCreatedAt = existing.createdAt;
411
+ } else if (oauthProvidersValue && typeof oauthProvidersValue === "object") {
412
+ const existing = oauthProvidersValue[providerId];
413
+ if (existing?.createdAt instanceof Date) providerCreatedAt = existing.createdAt;
414
+ }
415
+ const oauthProviderPayload = {
416
+ subject,
417
+ email: resolvedEmail,
418
+ name: resolvedName,
419
+ accessToken,
420
+ refreshToken,
421
+ idToken,
422
+ scope,
423
+ tokenType,
424
+ expiresAt,
425
+ rawUserInfo: userInfo ?? void 0,
426
+ createdAt: providerCreatedAt ?? now,
427
+ updatedAt: now
428
+ };
429
+ if (!user) {
430
+ const tenantId2 = crypto.randomUUID();
431
+ const password = await hashPasswordForStorage(crypto.randomBytes(32).toString("hex"));
432
+ user = new User({
433
+ email: resolvedEmail,
434
+ name: resolvedName,
435
+ password,
436
+ tenants: [tenantId2],
437
+ tenantRoles: {
438
+ [tenantId2]: ["owner"]
439
+ },
440
+ oauthProviders: {
441
+ [providerId]: oauthProviderPayload
442
+ }
443
+ });
444
+ await user.save();
445
+ try {
446
+ await Tenant.create({
447
+ tenantId: tenantId2,
448
+ name: resolvedEmail || subject
449
+ });
450
+ } catch (err) {
451
+ console.warn("oauth::failed_to_create_tenant", err);
452
+ }
453
+ } else {
454
+ const setFields = {
455
+ [`oauthProviders.${providerId}`]: oauthProviderPayload
456
+ };
457
+ if (!user.email && resolvedEmail) {
458
+ setFields.email = resolvedEmail;
459
+ }
460
+ if (!user.name && resolvedName) {
461
+ setFields.name = resolvedName;
462
+ }
463
+ await User.updateOne({ _id: user._id }, { $set: setFields });
464
+ }
465
+ const tenantId = user.tenants?.[0]?.toString?.() || "00000000";
466
+ const signedInTenants = (user.tenants || []).map((t) => t.toString?.() || String(t)) || [tenantId];
467
+ const tenantRolesRaw = user.tenantRoles;
468
+ const tenantRoles = tenantRolesRaw instanceof Map ? Object.fromEntries(tenantRolesRaw.entries()) : tenantRolesRaw && typeof tenantRolesRaw === "object" ? tenantRolesRaw : void 0;
469
+ ctx.req.session.user = {
470
+ id: user._id.toString(),
471
+ currentTenantId: tenantId,
472
+ signedInTenants: signedInTenants.length ? signedInTenants : [tenantId],
473
+ isEntryGateAuthorized: true,
474
+ tenantRoles
475
+ };
476
+ const redirectPath = returnTo && isSafeRedirectPath(returnTo) ? returnTo : successRedirectPath && isSafeRedirectPath(successRedirectPath) ? successRedirectPath : OAUTH_SUCCESS_REDIRECT_PATH;
477
+ return {
478
+ success: true,
479
+ type: "signed_in",
480
+ redirectPath,
481
+ userId: user._id.toString(),
482
+ tenantId,
483
+ signedInTenants: signedInTenants.length ? signedInTenants : [tenantId]
484
+ };
485
+ };
486
+ const createOAuthStartHandler = ({
487
+ providerId,
488
+ getAuthorizationParams
489
+ } = {}) => async (_payload, ctx) => {
490
+ const resolvedProviderId = resolveProviderId(ctx, providerId);
491
+ const providerAuthorizationParams = resolvedProviderId && getAuthorizationParams ? getAuthorizationParams(resolvedProviderId) : void 0;
492
+ const returnTo = getQueryString(ctx.req.query?.returnTo)?.trim();
493
+ const result = await getOAuthStartRedirectUrl({
494
+ ctx,
495
+ providerId: resolvedProviderId,
496
+ returnTo,
497
+ authorizationParams: providerAuthorizationParams
498
+ });
499
+ if (!result.success) {
500
+ ctx.res.status(result.statusCode);
501
+ return { success: false, error: result.error };
502
+ }
503
+ ctx.res.redirect(result.redirectUrl);
504
+ return { success: true };
505
+ };
506
+ const createOAuthCallbackHandler = ({
507
+ providerId,
508
+ createUserOnFirstSignIn,
509
+ missingUserRedirectPath,
510
+ successRedirectPath
511
+ } = {}) => async (_payload, ctx) => {
512
+ const result = await processOAuthCallback({
513
+ ctx,
514
+ providerId,
515
+ createUserOnFirstSignIn,
516
+ missingUserRedirectPath,
517
+ successRedirectPath
518
+ });
519
+ if (result.type === "error") {
520
+ ctx.res.redirect(result.redirectPath);
521
+ return { success: false, error: result.error };
522
+ }
523
+ if (result.type === "missing_user") {
524
+ if (result.redirectPath) {
525
+ ctx.res.redirect(result.redirectPath);
526
+ } else {
527
+ ctx.res.redirect("/auth/sign-up");
528
+ }
529
+ return { success: false, error: "user_not_found" };
530
+ }
531
+ ctx.res.redirect(result.redirectPath);
532
+ return { success: true };
533
+ };
534
+ const base64UrlEncode = (input) => Buffer.from(input).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
535
+ const createAppleClientSecret = ({
536
+ teamId,
537
+ clientId,
538
+ keyId,
539
+ privateKeyPem,
540
+ now = /* @__PURE__ */ new Date(),
541
+ expiresInSeconds = 10 * 60
542
+ }) => {
543
+ const teamIdValue = teamId.trim();
544
+ const clientIdValue = clientId.trim();
545
+ const keyIdValue = keyId.trim();
546
+ const privateKeyPemValue = privateKeyPem.trim();
547
+ if (!teamIdValue) throw new Error("teamId is required");
548
+ if (!clientIdValue) throw new Error("clientId is required");
549
+ if (!keyIdValue) throw new Error("keyId is required");
550
+ if (!privateKeyPemValue) throw new Error("privateKeyPem is required");
551
+ const nowSeconds = Math.floor(now.getTime() / 1e3);
552
+ const expiresIn = Number.isFinite(expiresInSeconds) ? expiresInSeconds : 10 * 60;
553
+ const exp = nowSeconds + Math.max(60, Math.min(expiresIn, 60 * 60 * 24 * 180));
554
+ const header = { alg: "ES256", kid: keyIdValue, typ: "JWT" };
555
+ const payload = {
556
+ iss: teamIdValue,
557
+ iat: nowSeconds,
558
+ exp,
559
+ aud: "https://appleid.apple.com",
560
+ sub: clientIdValue
561
+ };
562
+ const signingInput = `${base64UrlEncode(JSON.stringify(header))}.${base64UrlEncode(JSON.stringify(payload))}`;
563
+ const signature = crypto.sign("sha256", Buffer.from(signingInput), {
564
+ key: privateKeyPemValue,
565
+ dsaEncoding: "ieee-p1363"
566
+ });
567
+ return `${signingInput}.${base64UrlEncode(signature)}`;
568
+ };
569
+ export {
570
+ configureOAuthProviders,
571
+ createAppleClientSecret,
572
+ createOAuthCallbackHandler,
573
+ createOAuthStartHandler,
574
+ getOAuthStartRedirectUrl,
575
+ processOAuthCallback,
576
+ setOAuthProviders
577
+ };
578
+ //# sourceMappingURL=index.js.map