@jskit-ai/auth-provider-supabase-core 0.1.4

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,868 @@
1
+ import { createClient } from "@supabase/supabase-js";
2
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
3
+ import {
4
+ AUTH_METHOD_PASSWORD_ID,
5
+ AUTH_METHOD_PASSWORD_PROVIDER,
6
+ buildOAuthMethodId
7
+ } from "@jskit-ai/auth-core/shared/authMethods";
8
+ import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
9
+ import { validators } from "@jskit-ai/auth-core/server/validators";
10
+ import {
11
+ isTransientAuthMessage,
12
+ isTransientSupabaseError,
13
+ mapAuthError,
14
+ validationError,
15
+ isUserNotFoundLikeAuthError,
16
+ mapRecoveryError,
17
+ mapPasswordUpdateError,
18
+ mapOtpVerifyError,
19
+ mapProfileUpdateError,
20
+ mapCurrentPasswordError
21
+ } from "./authErrorMappers.js";
22
+ import { displayNameFromEmail, resolveDisplayName, resolveDisplayNameFromClaims } from "./authProfileNames.js";
23
+ import {
24
+ normalizeOAuthProviderInput as normalizeOAuthProviderInputFromCatalog,
25
+ validatePasswordRecoveryPayload,
26
+ parseOAuthCompletePayload as parseOAuthCompletePayloadFromCatalog,
27
+ parseOtpLoginVerifyPayload,
28
+ mapOAuthCallbackError
29
+ } from "./authInputParsers.js";
30
+ import {
31
+ parseHttpUrl,
32
+ buildPasswordResetRedirectUrl,
33
+ buildOtpLoginRedirectUrl,
34
+ normalizeOAuthIntent,
35
+ normalizeReturnToPath,
36
+ buildOAuthRedirectUrl,
37
+ buildOAuthLoginRedirectUrl,
38
+ buildOAuthLinkRedirectUrl
39
+ } from "./authRedirectUrls.js";
40
+ import {
41
+ normalizeIdentityProviderId,
42
+ collectProviderIdsFromSupabaseUser,
43
+ buildAuthMethodsStatusFromProviderIds,
44
+ buildAuthMethodsStatusFromSupabaseUser,
45
+ buildSecurityStatusFromAuthMethodsStatus,
46
+ findAuthMethodById,
47
+ findLinkedIdentityByProvider
48
+ } from "./authMethodStatus.js";
49
+ import { safeRequestCookies, cookieOptions } from "./authCookies.js";
50
+ import { loadJose, isExpiredJwtError, classifyJwtVerifyError } from "./authJwt.js";
51
+ import { buildDisabledPasswordSecret } from "./authSecrets.js";
52
+ import { createAccountFlows } from "./accountFlows.js";
53
+ import { createOauthFlows } from "./oauthFlows.js";
54
+ import { createPasswordSecurityFlows } from "./passwordSecurityFlows.js";
55
+ import { USER_PROFILE_EMAIL_CONFLICT_CODE } from "./standaloneProfileSyncService.js";
56
+ import {
57
+ buildOAuthProviderCatalogResponse,
58
+ resolveOAuthProviderQueryParams,
59
+ resolveSupabaseOAuthProviderCatalog
60
+ } from "./oauthProviderCatalog.js";
61
+
62
+ const ACCESS_TOKEN_COOKIE = "sb_access_token";
63
+ const REFRESH_TOKEN_COOKIE = "sb_refresh_token";
64
+ const DEFAULT_AUDIENCE = "authenticated";
65
+ const DEFAULT_AUTH_PROVIDER_ID = "supabase";
66
+ const AUTH_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/;
67
+ const PERSISTENT_SESSION_COOKIE_MAX_AGE_SECONDS = 60 * 60 * 24 * 400;
68
+
69
+ function normalizeAuthProviderId(value, { fallback = DEFAULT_AUTH_PROVIDER_ID } = {}) {
70
+ const normalized = String(value || "")
71
+ .trim()
72
+ .toLowerCase();
73
+ if (AUTH_PROVIDER_ID_PATTERN.test(normalized)) {
74
+ return normalized;
75
+ }
76
+
77
+ const fallbackNormalized = String(fallback || "")
78
+ .trim()
79
+ .toLowerCase();
80
+ if (AUTH_PROVIDER_ID_PATTERN.test(fallbackNormalized)) {
81
+ return fallbackNormalized;
82
+ }
83
+
84
+ return DEFAULT_AUTH_PROVIDER_ID;
85
+ }
86
+
87
+ function normalizeProviderUserId(value) {
88
+ return String(value || "").trim();
89
+ }
90
+
91
+ function createService(options) {
92
+ const authProvider = options.authProvider && typeof options.authProvider === "object" ? options.authProvider : null;
93
+ if (!authProvider) {
94
+ throw new Error("authProvider is required.");
95
+ }
96
+
97
+ const authProviderId = normalizeAuthProviderId(authProvider.id);
98
+ if (authProviderId !== DEFAULT_AUTH_PROVIDER_ID) {
99
+ throw new Error(`Unsupported auth provider "${authProviderId}".`);
100
+ }
101
+
102
+ const supabaseUrl = String(authProvider.supabaseUrl || "").trim();
103
+ const supabasePublishableKey = String(authProvider.supabasePublishableKey || "").trim();
104
+ const userSettingsRepository = options.userSettingsRepository || null;
105
+ const userProfileSyncService = options.userProfileSyncService;
106
+ if (
107
+ !userProfileSyncService ||
108
+ typeof userProfileSyncService.syncIdentityProfile !== "function" ||
109
+ typeof userProfileSyncService.findByIdentity !== "function"
110
+ ) {
111
+ throw new Error("userProfileSyncService with syncIdentityProfile() and findByIdentity() is required.");
112
+ }
113
+ const isProduction = options.nodeEnv === "production";
114
+ const jwtAudience = String(authProvider.jwtAudience || DEFAULT_AUDIENCE).trim();
115
+ const settingsProfileAuthInfo = Object.freeze({
116
+ emailManagedBy: normalizeAuthProviderId(authProvider.emailManagedBy || authProviderId, { fallback: authProviderId }),
117
+ emailChangeFlow: normalizeAuthProviderId(authProvider.emailChangeFlow || authProviderId, { fallback: authProviderId })
118
+ });
119
+ const passwordResetRedirectUrl = buildPasswordResetRedirectUrl({
120
+ appPublicUrl: options.appPublicUrl
121
+ });
122
+ const otpLoginRedirectUrl = buildOtpLoginRedirectUrl({
123
+ appPublicUrl: options.appPublicUrl
124
+ });
125
+ const appPublicUrl = String(options.appPublicUrl || "");
126
+ const authAllowedReturnToOrigins = Array.isArray(options.authAllowedReturnToOrigins)
127
+ ? options.authAllowedReturnToOrigins
128
+ : [];
129
+ const authOAuthCatalog = resolveSupabaseOAuthProviderCatalog({
130
+ oauthProviderCatalog: authProvider.oauthProviderCatalog || options.authOAuthProviderCatalog,
131
+ oauthProviders: authProvider.oauthProviders ?? options.authOAuthProviders,
132
+ oauthDefaultProvider: authProvider.oauthDefaultProvider ?? options.authOAuthDefaultProvider,
133
+ oauthProviderLabels: authProvider.oauthProviderLabels || options.authOAuthProviderLabels,
134
+ oauthProviderQueryParams: authProvider.oauthProviderQueryParams || options.authOAuthProviderQueryParams
135
+ });
136
+ const authOAuthProviders = authOAuthCatalog.providers;
137
+ const authOAuthProviderIds = authOAuthCatalog.providerIds;
138
+ const authOAuthDefaultProvider = authOAuthCatalog.defaultProvider;
139
+ const authOAuthCatalogResponse = Object.freeze(buildOAuthProviderCatalogResponse(authOAuthCatalog));
140
+
141
+ function normalizeOAuthProviderInput(value) {
142
+ return normalizeOAuthProviderInputFromCatalog(value, {
143
+ providerIds: authOAuthProviderIds,
144
+ defaultProvider: authOAuthDefaultProvider
145
+ });
146
+ }
147
+
148
+ function parseOAuthCompletePayload(payload) {
149
+ return parseOAuthCompletePayloadFromCatalog(payload, {
150
+ providerIds: authOAuthProviderIds,
151
+ defaultProvider: authOAuthDefaultProvider
152
+ });
153
+ }
154
+
155
+ function buildOAuthLoginRedirectUrlWithCatalog(payload) {
156
+ return buildOAuthLoginRedirectUrl({
157
+ ...payload,
158
+ providerIds: authOAuthProviderIds
159
+ });
160
+ }
161
+
162
+ function buildOAuthLinkRedirectUrlWithCatalog(payload) {
163
+ return buildOAuthLinkRedirectUrl({
164
+ ...payload,
165
+ providerIds: authOAuthProviderIds
166
+ });
167
+ }
168
+
169
+ function resolveOAuthProviderQueryParamsForProvider(providerId) {
170
+ return resolveOAuthProviderQueryParams(providerId, {
171
+ providerQueryParamsById: authOAuthCatalog.providerQueryParamsById
172
+ });
173
+ }
174
+
175
+ function normalizeAuthReturnToTarget(value, { fallback = "/" } = {}) {
176
+ return normalizeReturnToPath(value, {
177
+ fallback,
178
+ allowedOrigins: authAllowedReturnToOrigins
179
+ });
180
+ }
181
+
182
+ const issuerUrl = supabaseUrl ? new URL("/auth/v1", supabaseUrl).toString().replace(/\/$/, "") : "";
183
+ const jwksUrl = issuerUrl ? `${issuerUrl}/.well-known/jwks.json` : "";
184
+
185
+ let jwksResolver = null;
186
+
187
+ function ensureConfigured() {
188
+ if (!supabaseUrl || !supabasePublishableKey) {
189
+ throw new AppError(500, "Auth provider is not configured.");
190
+ }
191
+ }
192
+
193
+ function createSupabaseClient() {
194
+ ensureConfigured();
195
+ return createClient(supabaseUrl, supabasePublishableKey, {
196
+ auth: {
197
+ autoRefreshToken: false,
198
+ persistSession: false
199
+ }
200
+ });
201
+ }
202
+
203
+ function getSupabaseClient() {
204
+ return createSupabaseClient();
205
+ }
206
+
207
+ function createStatelessSupabaseClient() {
208
+ return createSupabaseClient();
209
+ }
210
+
211
+ async function getJwksResolver() {
212
+ if (jwksResolver) {
213
+ return jwksResolver;
214
+ }
215
+
216
+ const jose = await loadJose();
217
+ jwksResolver = jose.createRemoteJWKSet(new URL(jwksUrl));
218
+ return jwksResolver;
219
+ }
220
+
221
+ async function verifyAccessToken(accessToken) {
222
+ const jose = await loadJose();
223
+ const jwks = await getJwksResolver();
224
+
225
+ try {
226
+ const { payload } = await jose.jwtVerify(accessToken, jwks, {
227
+ issuer: issuerUrl,
228
+ audience: jwtAudience,
229
+ clockTolerance: 5
230
+ });
231
+
232
+ return {
233
+ status: "valid",
234
+ payload
235
+ };
236
+ } catch (error) {
237
+ return {
238
+ status: classifyJwtVerifyError(error)
239
+ };
240
+ }
241
+ }
242
+
243
+ async function verifyAccessTokenViaSupabase(accessToken) {
244
+ const supabase = getSupabaseClient();
245
+
246
+ try {
247
+ const response = await supabase.auth.getUser(accessToken);
248
+ if (response.error) {
249
+ if (isTransientSupabaseError(response.error)) {
250
+ return { status: "transient" };
251
+ }
252
+
253
+ return { status: "invalid" };
254
+ }
255
+
256
+ if (!response.data?.user) {
257
+ return { status: "invalid" };
258
+ }
259
+
260
+ const profile = await syncProfileFromSupabaseUser(response.data.user, response.data.user.email);
261
+ return {
262
+ status: "valid",
263
+ profile
264
+ };
265
+ } catch (error) {
266
+ if (isTransientSupabaseError(error)) {
267
+ return { status: "transient" };
268
+ }
269
+
270
+ return { status: "invalid" };
271
+ }
272
+ }
273
+
274
+ async function setSessionFromRequestCookies(request, options = {}) {
275
+ const cookies = safeRequestCookies(request);
276
+ const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
277
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
278
+
279
+ if (!accessToken || !refreshToken) {
280
+ throw new AppError(401, "Authentication required.");
281
+ }
282
+
283
+ const supabase = options.supabaseClient || getSupabaseClient();
284
+ let sessionResponse;
285
+ try {
286
+ sessionResponse = await supabase.auth.setSession({
287
+ access_token: accessToken,
288
+ refresh_token: refreshToken
289
+ });
290
+ } catch (error) {
291
+ throw mapRecoveryError(error);
292
+ }
293
+
294
+ const session = sessionResponse.data?.session || null;
295
+ const user = sessionResponse.data?.user || null;
296
+
297
+ if (sessionResponse.error || !session || !user) {
298
+ throw mapRecoveryError(sessionResponse.error);
299
+ }
300
+
301
+ return {
302
+ ...sessionResponse,
303
+ data: {
304
+ ...sessionResponse.data,
305
+ session,
306
+ user
307
+ }
308
+ };
309
+ }
310
+
311
+ async function resolveCurrentSupabaseUser(request, options = {}) {
312
+ const supabase = options.supabaseClient || getSupabaseClient();
313
+ const cookies = safeRequestCookies(request);
314
+ const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
315
+ let user = null;
316
+ let session = null;
317
+
318
+ async function getUserByAccessToken(token) {
319
+ if (!token) {
320
+ return null;
321
+ }
322
+
323
+ try {
324
+ const userResponse = await supabase.auth.getUser(token);
325
+ if (!userResponse.error && userResponse.data?.user) {
326
+ return userResponse.data.user;
327
+ }
328
+
329
+ if (userResponse.error && isTransientSupabaseError(userResponse.error)) {
330
+ throw mapAuthError(userResponse.error, 503);
331
+ }
332
+ } catch (error) {
333
+ if (isTransientSupabaseError(error)) {
334
+ throw mapAuthError(error, 503);
335
+ }
336
+ }
337
+
338
+ return null;
339
+ }
340
+
341
+ if (accessToken) {
342
+ user = await getUserByAccessToken(accessToken);
343
+ }
344
+
345
+ if (!user) {
346
+ const sessionResponse = await setSessionFromRequestCookies(request, {
347
+ supabaseClient: supabase
348
+ });
349
+ user = sessionResponse.data.user || null;
350
+ session = sessionResponse.data.session || null;
351
+ }
352
+
353
+ const currentAccessToken = String(session?.access_token || accessToken || "").trim();
354
+ const explicitUser = await getUserByAccessToken(currentAccessToken);
355
+ if (explicitUser) {
356
+ user = explicitUser;
357
+ }
358
+
359
+ if (!user) {
360
+ throw new AppError(401, "Authentication required.");
361
+ }
362
+
363
+ return {
364
+ session,
365
+ user
366
+ };
367
+ }
368
+
369
+ function writeSessionCookies(reply, session) {
370
+ const accessToken = String(session?.access_token || "");
371
+ const refreshToken = String(session?.refresh_token || "");
372
+ if (!accessToken || !refreshToken) {
373
+ return;
374
+ }
375
+
376
+ const sessionAccessMaxAge = Number.isFinite(Number(session?.expires_in)) ? Number(session.expires_in) : 3600;
377
+ const persistentCookieMaxAge = Math.max(Math.floor(sessionAccessMaxAge), PERSISTENT_SESSION_COOKIE_MAX_AGE_SECONDS);
378
+
379
+ reply.setCookie(ACCESS_TOKEN_COOKIE, accessToken, cookieOptions(isProduction, persistentCookieMaxAge));
380
+ reply.setCookie(REFRESH_TOKEN_COOKIE, refreshToken, cookieOptions(isProduction, persistentCookieMaxAge));
381
+ }
382
+
383
+ function clearSessionCookies(reply) {
384
+ const clearOptions = cookieOptions(isProduction, 0);
385
+ reply.clearCookie(ACCESS_TOKEN_COOKIE, clearOptions);
386
+ reply.clearCookie(REFRESH_TOKEN_COOKIE, clearOptions);
387
+ }
388
+
389
+ function requireSynchronizedProfile(profile) {
390
+ if (profile && Number.isFinite(Number(profile.id)) && String(profile.displayName || "").trim()) {
391
+ return profile;
392
+ }
393
+
394
+ throw new AppError(500, "Authentication profile synchronization failed. Please retry.");
395
+ }
396
+
397
+ function buildNormalizedIdentityKey(identityLike) {
398
+ const source = identityLike && typeof identityLike === "object" ? identityLike : {};
399
+ const authProvider = normalizeAuthProviderId(source.authProvider || authProviderId, {
400
+ fallback: authProviderId
401
+ });
402
+ const authProviderUserId = normalizeProviderUserId(source.authProviderUserId);
403
+
404
+ if (!authProviderUserId) {
405
+ throw new TypeError("Profile identity is missing required fields.");
406
+ }
407
+
408
+ return {
409
+ authProvider,
410
+ authProviderUserId
411
+ };
412
+ }
413
+
414
+ function buildNormalizedIdentityProfile(profileLike) {
415
+ const source = profileLike && typeof profileLike === "object" ? profileLike : {};
416
+ const identity = buildNormalizedIdentityKey(source);
417
+ const email = normalizeEmail(source.email || "");
418
+ const displayName = String(source.displayName || "").trim();
419
+
420
+ if (!email || !displayName) {
421
+ throw new TypeError("Profile identity is missing required fields.");
422
+ }
423
+
424
+ return {
425
+ authProvider: identity.authProvider,
426
+ authProviderUserId: identity.authProviderUserId,
427
+ email,
428
+ displayName
429
+ };
430
+ }
431
+
432
+ async function findProfileByIdentity(identityProfile, options = {}) {
433
+ const normalized = buildNormalizedIdentityKey(identityProfile);
434
+ return userProfileSyncService.findByIdentity(
435
+ {
436
+ authProvider: normalized.authProvider,
437
+ authProviderUserId: normalized.authProviderUserId
438
+ },
439
+ options
440
+ );
441
+ }
442
+
443
+ function getSettingsProfileAuthInfo() {
444
+ return {
445
+ emailManagedBy: settingsProfileAuthInfo.emailManagedBy,
446
+ emailChangeFlow: settingsProfileAuthInfo.emailChangeFlow
447
+ };
448
+ }
449
+
450
+ async function syncProfileMirror(nextProfile) {
451
+ try {
452
+ const normalized = buildNormalizedIdentityProfile(nextProfile);
453
+ const synchronizedProfile = await userProfileSyncService.syncIdentityProfile(normalized);
454
+ return requireSynchronizedProfile(synchronizedProfile);
455
+ } catch (error) {
456
+ if (String(error?.code || "") === USER_PROFILE_EMAIL_CONFLICT_CODE) {
457
+ throw new AppError(
458
+ 409,
459
+ "This email is already registered with another sign-in method. Sign in with that method, then link this provider in Settings > Security."
460
+ );
461
+ }
462
+
463
+ throw error;
464
+ }
465
+ }
466
+
467
+ async function syncProfileFromSupabaseUser(supabaseUser, fallbackEmail) {
468
+ const supabaseUserId = normalizeProviderUserId(supabaseUser?.id);
469
+ const email = normalizeEmail(supabaseUser?.email || fallbackEmail);
470
+
471
+ if (!supabaseUserId || !email) {
472
+ throw new AppError(500, "Auth provider user payload is missing required fields.");
473
+ }
474
+
475
+ return syncProfileMirror({
476
+ authProvider: authProviderId,
477
+ authProviderUserId: supabaseUserId,
478
+ email,
479
+ displayName: resolveDisplayName(supabaseUser, email)
480
+ });
481
+ }
482
+
483
+ async function syncProfileFromJwtClaims(claims) {
484
+ const supabaseUserId = normalizeProviderUserId(claims?.sub);
485
+ if (!supabaseUserId) {
486
+ throw new AppError(401, "Token is missing subject claim.");
487
+ }
488
+
489
+ const existing = await findProfileByIdentity({
490
+ authProvider: authProviderId,
491
+ authProviderUserId: supabaseUserId
492
+ });
493
+ const emailFromToken = normalizeEmail(claims?.email || "");
494
+
495
+ if (!emailFromToken) {
496
+ if (existing) {
497
+ return existing;
498
+ }
499
+ throw new AppError(401, "Token is missing email claim.");
500
+ }
501
+
502
+ return syncProfileMirror({
503
+ authProvider: authProviderId,
504
+ authProviderUserId: supabaseUserId,
505
+ email: emailFromToken,
506
+ displayName: resolveDisplayNameFromClaims(claims, emailFromToken)
507
+ });
508
+ }
509
+
510
+ async function resolvePasswordSignInPolicyForUserId(userId) {
511
+ if (!userSettingsRepository || typeof userSettingsRepository.ensureForUserId !== "function") {
512
+ return {
513
+ passwordSignInEnabled: true,
514
+ passwordSetupRequired: false
515
+ };
516
+ }
517
+
518
+ const settings = await userSettingsRepository.ensureForUserId(userId);
519
+ return {
520
+ passwordSignInEnabled: settings?.passwordSignInEnabled !== false,
521
+ passwordSetupRequired: settings?.passwordSetupRequired === true
522
+ };
523
+ }
524
+
525
+ async function setPasswordSignInEnabledForUserId(userId, enabled, options = {}) {
526
+ if (!userSettingsRepository || typeof userSettingsRepository.updatePasswordSignInEnabled !== "function") {
527
+ throw new AppError(500, "Password sign-in settings repository is not configured.");
528
+ }
529
+
530
+ const updated = await userSettingsRepository.updatePasswordSignInEnabled(userId, enabled, options);
531
+ return {
532
+ passwordSignInEnabled: updated.passwordSignInEnabled !== false,
533
+ passwordSetupRequired: updated.passwordSetupRequired === true
534
+ };
535
+ }
536
+
537
+ async function setPasswordSetupRequiredForUserId(userId, required) {
538
+ if (!userSettingsRepository || typeof userSettingsRepository.updatePasswordSetupRequired !== "function") {
539
+ return;
540
+ }
541
+
542
+ await userSettingsRepository.updatePasswordSetupRequired(userId, required);
543
+ }
544
+
545
+ async function resolveCurrentAuthContext(request, options = {}) {
546
+ const current = await resolveCurrentSupabaseUser(request, options);
547
+ const profile = await syncProfileFromSupabaseUser(current.user, current.user?.email || "");
548
+ const passwordSignInPolicy = await resolvePasswordSignInPolicyForUserId(profile.id);
549
+ const authMethodsStatus = buildAuthMethodsStatusFromSupabaseUser(current.user, {
550
+ ...passwordSignInPolicy,
551
+ oauthProviders: authOAuthProviders
552
+ });
553
+
554
+ return {
555
+ ...current,
556
+ profile,
557
+ passwordSignInEnabled: passwordSignInPolicy.passwordSignInEnabled,
558
+ passwordSetupRequired: passwordSignInPolicy.passwordSetupRequired,
559
+ authMethodsStatus
560
+ };
561
+ }
562
+
563
+ const { register, login, requestOtpLogin, verifyOtpLogin, updateDisplayName } = createAccountFlows({
564
+ ensureConfigured,
565
+ validators,
566
+ validationError,
567
+ getSupabaseClient,
568
+ displayNameFromEmail,
569
+ mapAuthError,
570
+ syncProfileFromSupabaseUser,
571
+ resolvePasswordSignInPolicyForUserId,
572
+ otpLoginRedirectUrl,
573
+ buildOtpLoginRedirectUrl,
574
+ appPublicUrl,
575
+ isTransientSupabaseError,
576
+ isUserNotFoundLikeAuthError,
577
+ parseOtpLoginVerifyPayload,
578
+ mapOtpVerifyError,
579
+ setSessionFromRequestCookies,
580
+ mapProfileUpdateError,
581
+ normalizeReturnToPath: normalizeAuthReturnToTarget
582
+ });
583
+
584
+ const { oauthStart, startProviderLink, oauthComplete, unlinkProvider } = createOauthFlows({
585
+ ensureConfigured,
586
+ normalizeOAuthProviderInput,
587
+ normalizeReturnToPath: normalizeAuthReturnToTarget,
588
+ buildOAuthLoginRedirectUrl: buildOAuthLoginRedirectUrlWithCatalog,
589
+ appPublicUrl,
590
+ authOAuthDefaultProvider,
591
+ resolveOAuthProviderQueryParams: resolveOAuthProviderQueryParamsForProvider,
592
+ getSupabaseClient,
593
+ mapAuthError,
594
+ setSessionFromRequestCookies,
595
+ buildOAuthLinkRedirectUrl: buildOAuthLinkRedirectUrlWithCatalog,
596
+ parseOAuthCompletePayload,
597
+ validationError,
598
+ mapOAuthCallbackError,
599
+ mapRecoveryError,
600
+ syncProfileFromSupabaseUser,
601
+ resolveCurrentAuthContext,
602
+ buildOAuthMethodId,
603
+ findAuthMethodById,
604
+ findLinkedIdentityByProvider,
605
+ buildSecurityStatusFromAuthMethodsStatus
606
+ });
607
+
608
+ const {
609
+ requestPasswordReset,
610
+ completePasswordRecovery,
611
+ resetPassword,
612
+ changePassword,
613
+ setPasswordSignInEnabled,
614
+ signOutOtherSessions,
615
+ getSecurityStatus
616
+ } = createPasswordSecurityFlows({
617
+ ensureConfigured,
618
+ validators,
619
+ validationError,
620
+ getSupabaseClient,
621
+ passwordResetRedirectUrl,
622
+ mapAuthError,
623
+ validatePasswordRecoveryPayload,
624
+ mapRecoveryError,
625
+ syncProfileFromSupabaseUser,
626
+ setSessionFromRequestCookies,
627
+ resolvePasswordSignInPolicyForUserId,
628
+ mapPasswordUpdateError,
629
+ setPasswordSetupRequiredForUserId,
630
+ normalizeEmail,
631
+ createStatelessSupabaseClient,
632
+ mapCurrentPasswordError,
633
+ resolveCurrentAuthContext,
634
+ findAuthMethodById,
635
+ authMethodPasswordId: AUTH_METHOD_PASSWORD_ID,
636
+ buildDisabledPasswordSecret,
637
+ setPasswordSignInEnabledForUserId,
638
+ buildAuthMethodsStatusFromSupabaseUser: (user, statusOptions = {}) =>
639
+ buildAuthMethodsStatusFromSupabaseUser(user, {
640
+ ...statusOptions,
641
+ oauthProviders: authOAuthProviders
642
+ }),
643
+ buildSecurityStatusFromAuthMethodsStatus,
644
+ authMethodPasswordProvider: AUTH_METHOD_PASSWORD_PROVIDER,
645
+ buildAuthMethodsStatusFromProviderIds: (providerIds, statusOptions = {}) =>
646
+ buildAuthMethodsStatusFromProviderIds(providerIds, {
647
+ ...statusOptions,
648
+ oauthProviders: authOAuthProviders
649
+ })
650
+ });
651
+
652
+ async function authenticateRequest(request) {
653
+ ensureConfigured();
654
+
655
+ const cookies = safeRequestCookies(request);
656
+ const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "");
657
+ const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "");
658
+
659
+ if (!accessToken && !refreshToken) {
660
+ return {
661
+ authenticated: false,
662
+ clearSession: false,
663
+ session: null,
664
+ transientFailure: false
665
+ };
666
+ }
667
+
668
+ if (accessToken) {
669
+ const verification = await verifyAccessToken(accessToken);
670
+
671
+ if (verification.status === "valid") {
672
+ const profile = await syncProfileFromJwtClaims(verification.payload);
673
+ return {
674
+ authenticated: true,
675
+ profile,
676
+ clearSession: false,
677
+ session: null,
678
+ transientFailure: false
679
+ };
680
+ }
681
+
682
+ if (verification.status === "transient") {
683
+ return {
684
+ authenticated: false,
685
+ clearSession: false,
686
+ session: null,
687
+ transientFailure: true
688
+ };
689
+ }
690
+
691
+ if (verification.status === "invalid") {
692
+ const supabaseVerification = await verifyAccessTokenViaSupabase(accessToken);
693
+ if (supabaseVerification.status === "valid") {
694
+ return {
695
+ authenticated: true,
696
+ profile: supabaseVerification.profile,
697
+ clearSession: false,
698
+ session: null,
699
+ transientFailure: false
700
+ };
701
+ }
702
+
703
+ if (supabaseVerification.status === "transient") {
704
+ return {
705
+ authenticated: false,
706
+ clearSession: false,
707
+ session: null,
708
+ transientFailure: true
709
+ };
710
+ }
711
+ }
712
+ }
713
+
714
+ if (!refreshToken) {
715
+ return {
716
+ authenticated: false,
717
+ clearSession: true,
718
+ session: null,
719
+ transientFailure: false
720
+ };
721
+ }
722
+
723
+ const supabase = getSupabaseClient();
724
+ let refreshResponse;
725
+ try {
726
+ refreshResponse = await supabase.auth.refreshSession({ refresh_token: refreshToken });
727
+ /* c8 ignore next 17 -- defensive: refreshSession usually resolves with { error } for auth/transport issues. */
728
+ } catch (error) {
729
+ if (isTransientSupabaseError(error)) {
730
+ return {
731
+ authenticated: false,
732
+ clearSession: false,
733
+ session: null,
734
+ transientFailure: true
735
+ };
736
+ }
737
+
738
+ return {
739
+ authenticated: false,
740
+ clearSession: true,
741
+ session: null,
742
+ transientFailure: false
743
+ };
744
+ }
745
+
746
+ if (refreshResponse.error) {
747
+ if (isTransientSupabaseError(refreshResponse.error)) {
748
+ return {
749
+ authenticated: false,
750
+ clearSession: false,
751
+ session: null,
752
+ transientFailure: true
753
+ };
754
+ }
755
+
756
+ return {
757
+ authenticated: false,
758
+ clearSession: true,
759
+ session: null,
760
+ transientFailure: false
761
+ };
762
+ }
763
+
764
+ if (!refreshResponse.data?.session || !refreshResponse.data?.user) {
765
+ return {
766
+ authenticated: false,
767
+ clearSession: true,
768
+ session: null,
769
+ transientFailure: false
770
+ };
771
+ }
772
+
773
+ const profile = await syncProfileFromSupabaseUser(refreshResponse.data.user, refreshResponse.data.user.email);
774
+
775
+ return {
776
+ authenticated: true,
777
+ profile,
778
+ clearSession: false,
779
+ session: refreshResponse.data.session,
780
+ transientFailure: false
781
+ };
782
+ }
783
+
784
+ function hasAccessTokenCookie(request) {
785
+ const cookies = safeRequestCookies(request);
786
+ return Boolean(cookies[ACCESS_TOKEN_COOKIE]);
787
+ }
788
+
789
+ function hasSessionCookie(request) {
790
+ const cookies = safeRequestCookies(request);
791
+ return Boolean(cookies[ACCESS_TOKEN_COOKIE] || cookies[REFRESH_TOKEN_COOKIE]);
792
+ }
793
+
794
+ function getOAuthProviderCatalog() {
795
+ return authOAuthCatalogResponse;
796
+ }
797
+
798
+ return {
799
+ register,
800
+ login,
801
+ requestOtpLogin,
802
+ verifyOtpLogin,
803
+ oauthStart,
804
+ oauthComplete,
805
+ startProviderLink,
806
+ requestPasswordReset,
807
+ completePasswordRecovery,
808
+ resetPassword,
809
+ updateDisplayName,
810
+ changePassword,
811
+ setPasswordSignInEnabled,
812
+ unlinkProvider,
813
+ signOutOtherSessions,
814
+ getSecurityStatus,
815
+ getSettingsProfileAuthInfo,
816
+ getOAuthProviderCatalog,
817
+ authenticateRequest,
818
+ hasAccessTokenCookie,
819
+ hasSessionCookie,
820
+ writeSessionCookies,
821
+ clearSessionCookies
822
+ };
823
+ }
824
+
825
+ const __testables = {
826
+ validatePasswordRecoveryPayload,
827
+ displayNameFromEmail,
828
+ resolveDisplayName,
829
+ resolveDisplayNameFromClaims,
830
+ isTransientAuthMessage,
831
+ isTransientSupabaseError,
832
+ mapAuthError,
833
+ validationError,
834
+ isUserNotFoundLikeAuthError,
835
+ mapRecoveryError,
836
+ mapPasswordUpdateError,
837
+ mapOtpVerifyError,
838
+ mapProfileUpdateError,
839
+ mapCurrentPasswordError,
840
+ parseHttpUrl,
841
+ buildPasswordResetRedirectUrl,
842
+ buildOtpLoginRedirectUrl,
843
+ normalizeOAuthIntent,
844
+ normalizeReturnToPath,
845
+ buildOAuthRedirectUrl,
846
+ buildOAuthLoginRedirectUrl,
847
+ buildOAuthLinkRedirectUrl,
848
+ normalizeOAuthProviderInput: normalizeOAuthProviderInputFromCatalog,
849
+ parseOAuthCompletePayload: parseOAuthCompletePayloadFromCatalog,
850
+ parseOtpLoginVerifyPayload,
851
+ mapOAuthCallbackError,
852
+ resolveSupabaseOAuthProviderCatalog,
853
+ resolveOAuthProviderQueryParams,
854
+ buildOAuthProviderCatalogResponse,
855
+ normalizeIdentityProviderId,
856
+ collectProviderIdsFromSupabaseUser,
857
+ buildAuthMethodsStatusFromProviderIds,
858
+ buildAuthMethodsStatusFromSupabaseUser,
859
+ buildSecurityStatusFromAuthMethodsStatus,
860
+ findAuthMethodById,
861
+ findLinkedIdentityByProvider,
862
+ safeRequestCookies,
863
+ cookieOptions,
864
+ isExpiredJwtError,
865
+ classifyJwtVerifyError
866
+ };
867
+
868
+ export { createService, __testables };