@jskit-ai/auth-provider-supabase-core 0.1.99 → 0.1.100
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/package.descriptor.mjs +6 -6
- package/package.json +3 -3
- package/src/server/lib/accountFlows.js +6 -2
- package/src/server/lib/actions/auth.contributor.js +3 -279
- package/src/server/lib/authMethodStatus.js +3 -23
- package/src/server/lib/index.js +1 -1
- package/src/server/lib/passwordSecurityFlows.js +14 -2
- package/src/server/lib/service.js +159 -29
- package/src/server/providers/AuthSupabaseServiceProvider.js +92 -126
- package/test/auth.provider.supabase.test.js +202 -51
- package/test/providerRuntime.test.js +146 -8
- package/src/server/lib/authSessionEventsService.js +0 -20
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createClient } from "@supabase/supabase-js";
|
|
2
2
|
import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
|
|
3
|
+
import { normalizeAuthCapabilities } from "@jskit-ai/auth-core/shared/authCapabilities";
|
|
4
|
+
import { normalizeAuthActor, normalizeAuthResult } from "@jskit-ai/auth-core/server/authActor";
|
|
3
5
|
import {
|
|
4
6
|
AUTH_METHOD_PASSWORD_ID,
|
|
5
7
|
AUTH_METHOD_PASSWORD_PROVIDER,
|
|
@@ -68,6 +70,8 @@ import {
|
|
|
68
70
|
|
|
69
71
|
const ACCESS_TOKEN_COOKIE = "sb_access_token";
|
|
70
72
|
const REFRESH_TOKEN_COOKIE = "sb_refresh_token";
|
|
73
|
+
const RECOVERY_ACCESS_TOKEN_COOKIE = "sb_recovery_access_token";
|
|
74
|
+
const RECOVERY_REFRESH_TOKEN_COOKIE = "sb_recovery_refresh_token";
|
|
71
75
|
const DEFAULT_AUDIENCE = "authenticated";
|
|
72
76
|
const DEFAULT_AUTH_PROVIDER_ID = "supabase";
|
|
73
77
|
const AUTH_PROVIDER_ID_PATTERN = /^[a-z][a-z0-9_-]{1,63}$/;
|
|
@@ -108,9 +112,14 @@ function createService(options) {
|
|
|
108
112
|
|
|
109
113
|
const supabaseUrl = String(authProvider.supabaseUrl || "").trim();
|
|
110
114
|
const supabasePublishableKey = String(authProvider.supabasePublishableKey || "").trim();
|
|
115
|
+
const supabaseConfigured = Boolean(supabaseUrl && supabasePublishableKey);
|
|
111
116
|
const userSettingsRepository = options.userSettingsRepository || null;
|
|
117
|
+
const passwordMethodToggleSupported =
|
|
118
|
+
typeof userSettingsRepository?.ensureForUserId === "function" &&
|
|
119
|
+
typeof userSettingsRepository?.updatePasswordSignInEnabled === "function";
|
|
112
120
|
const userProfilesRepository = options.userProfilesRepository || null;
|
|
113
121
|
const userProfileSyncService = options.userProfileSyncService;
|
|
122
|
+
const profileProjectionEnabled = options.profileProjectionEnabled === true;
|
|
114
123
|
if (
|
|
115
124
|
!userProfileSyncService ||
|
|
116
125
|
typeof userProfileSyncService.syncIdentityProfile !== "function" ||
|
|
@@ -156,6 +165,56 @@ function createService(options) {
|
|
|
156
165
|
const authOAuthProviderIds = authOAuthCatalog.providerIds;
|
|
157
166
|
const authOAuthDefaultProvider = authOAuthCatalog.defaultProvider;
|
|
158
167
|
const authOAuthCatalogResponse = Object.freeze(buildOAuthProviderCatalogResponse(authOAuthCatalog));
|
|
168
|
+
const capabilities = normalizeAuthCapabilities({
|
|
169
|
+
provider: {
|
|
170
|
+
id: authProviderId,
|
|
171
|
+
label: "Supabase"
|
|
172
|
+
},
|
|
173
|
+
features: {
|
|
174
|
+
password: {
|
|
175
|
+
login: supabaseConfigured,
|
|
176
|
+
register: supabaseConfigured,
|
|
177
|
+
change: supabaseConfigured,
|
|
178
|
+
methodToggle: supabaseConfigured && passwordMethodToggleSupported
|
|
179
|
+
},
|
|
180
|
+
passwordRecovery: {
|
|
181
|
+
request: supabaseConfigured,
|
|
182
|
+
complete: supabaseConfigured,
|
|
183
|
+
delivery: supabaseConfigured ? "smtp" : "disabled"
|
|
184
|
+
},
|
|
185
|
+
otp: {
|
|
186
|
+
login: supabaseConfigured
|
|
187
|
+
},
|
|
188
|
+
oauthLogin: {
|
|
189
|
+
enabled: supabaseConfigured && authOAuthProviders.length > 0,
|
|
190
|
+
providers: authOAuthCatalogResponse.providers,
|
|
191
|
+
defaultProvider: authOAuthCatalogResponse.defaultProvider
|
|
192
|
+
},
|
|
193
|
+
emailConfirmation: supabaseConfigured,
|
|
194
|
+
profileUpdate: supabaseConfigured,
|
|
195
|
+
providerLinking: {
|
|
196
|
+
start: supabaseConfigured && authOAuthProviders.length > 0,
|
|
197
|
+
unlink: supabaseConfigured && authOAuthProviders.length > 0
|
|
198
|
+
},
|
|
199
|
+
securityStatus: supabaseConfigured || devAuthConfig.enabled === true,
|
|
200
|
+
signOutOtherSessions: supabaseConfigured,
|
|
201
|
+
appProfileProjection: profileProjectionEnabled,
|
|
202
|
+
devLoginAs: devAuthConfig.enabled === true
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
const securityStatusActions = Object.freeze({
|
|
206
|
+
changePassword: true,
|
|
207
|
+
setPasswordEnabled: capabilities.features.password.methodToggle,
|
|
208
|
+
linkProvider: capabilities.features.providerLinking.start,
|
|
209
|
+
unlinkProvider: capabilities.features.providerLinking.unlink,
|
|
210
|
+
signOutOtherSessions: capabilities.features.signOutOtherSessions
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
function buildProviderSecurityStatus(authMethodsStatus) {
|
|
214
|
+
return buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus, {
|
|
215
|
+
actions: securityStatusActions
|
|
216
|
+
});
|
|
217
|
+
}
|
|
159
218
|
|
|
160
219
|
function normalizeOAuthProviderInput(value) {
|
|
161
220
|
return normalizeOAuthProviderInputFromCatalog(value, {
|
|
@@ -283,7 +342,22 @@ function createService(options) {
|
|
|
283
342
|
const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
|
|
284
343
|
const refreshToken = String(cookies[REFRESH_TOKEN_COOKIE] || "").trim();
|
|
285
344
|
|
|
286
|
-
|
|
345
|
+
return setSupabaseSessionFromTokens({ accessToken, refreshToken }, options);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function setRecoverySessionFromRequestCookies(request, options = {}) {
|
|
349
|
+
const cookies = safeRequestCookies(request);
|
|
350
|
+
const accessToken = String(cookies[RECOVERY_ACCESS_TOKEN_COOKIE] || "").trim();
|
|
351
|
+
const refreshToken = String(cookies[RECOVERY_REFRESH_TOKEN_COOKIE] || "").trim();
|
|
352
|
+
|
|
353
|
+
return setSupabaseSessionFromTokens({ accessToken, refreshToken }, options);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async function setSupabaseSessionFromTokens({ accessToken, refreshToken }, options = {}) {
|
|
357
|
+
const normalizedAccessToken = String(accessToken || "").trim();
|
|
358
|
+
const normalizedRefreshToken = String(refreshToken || "").trim();
|
|
359
|
+
|
|
360
|
+
if (!normalizedAccessToken || !normalizedRefreshToken) {
|
|
287
361
|
throw new AppError(401, "Authentication required.");
|
|
288
362
|
}
|
|
289
363
|
|
|
@@ -291,8 +365,8 @@ function createService(options) {
|
|
|
291
365
|
let sessionResponse;
|
|
292
366
|
try {
|
|
293
367
|
sessionResponse = await supabase.auth.setSession({
|
|
294
|
-
access_token:
|
|
295
|
-
refresh_token:
|
|
368
|
+
access_token: normalizedAccessToken,
|
|
369
|
+
refresh_token: normalizedRefreshToken
|
|
296
370
|
});
|
|
297
371
|
} catch (error) {
|
|
298
372
|
throw mapRecoveryError(error);
|
|
@@ -398,12 +472,12 @@ function createService(options) {
|
|
|
398
472
|
return devAuthResult;
|
|
399
473
|
}
|
|
400
474
|
|
|
401
|
-
return {
|
|
475
|
+
return withActor({
|
|
402
476
|
...devAuthResult,
|
|
403
477
|
profile: requireAuthenticatedProfile(devAuthResult.profile, {
|
|
404
478
|
context: "dev auth profile"
|
|
405
479
|
})
|
|
406
|
-
};
|
|
480
|
+
});
|
|
407
481
|
}
|
|
408
482
|
|
|
409
483
|
function writeSessionCookies(reply, session) {
|
|
@@ -414,16 +488,23 @@ function createService(options) {
|
|
|
414
488
|
}
|
|
415
489
|
|
|
416
490
|
const sessionAccessMaxAge = Number.isFinite(Number(session?.expires_in)) ? Number(session.expires_in) : 3600;
|
|
417
|
-
const
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
491
|
+
const purpose = String(session?.purpose || "normal").trim();
|
|
492
|
+
const accessCookieName = purpose === "recovery" ? RECOVERY_ACCESS_TOKEN_COOKIE : ACCESS_TOKEN_COOKIE;
|
|
493
|
+
const refreshCookieName = purpose === "recovery" ? RECOVERY_REFRESH_TOKEN_COOKIE : REFRESH_TOKEN_COOKIE;
|
|
494
|
+
const cookieMaxAge = purpose === "recovery"
|
|
495
|
+
? Math.floor(sessionAccessMaxAge)
|
|
496
|
+
: Math.max(Math.floor(sessionAccessMaxAge), PERSISTENT_SESSION_COOKIE_MAX_AGE_SECONDS);
|
|
497
|
+
|
|
498
|
+
reply.setCookie(accessCookieName, accessToken, cookieOptions(isProduction, cookieMaxAge));
|
|
499
|
+
reply.setCookie(refreshCookieName, refreshToken, cookieOptions(isProduction, cookieMaxAge));
|
|
421
500
|
}
|
|
422
501
|
|
|
423
502
|
function clearSessionCookies(reply) {
|
|
424
503
|
for (const clearOptions of cookieClearOptions(isProduction)) {
|
|
425
504
|
reply.clearCookie(ACCESS_TOKEN_COOKIE, clearOptions);
|
|
426
505
|
reply.clearCookie(REFRESH_TOKEN_COOKIE, clearOptions);
|
|
506
|
+
reply.clearCookie(RECOVERY_ACCESS_TOKEN_COOKIE, clearOptions);
|
|
507
|
+
reply.clearCookie(RECOVERY_REFRESH_TOKEN_COOKIE, clearOptions);
|
|
427
508
|
}
|
|
428
509
|
}
|
|
429
510
|
|
|
@@ -565,6 +646,44 @@ function createService(options) {
|
|
|
565
646
|
});
|
|
566
647
|
}
|
|
567
648
|
|
|
649
|
+
function buildActorFromProfile(profile) {
|
|
650
|
+
return normalizeAuthActor(
|
|
651
|
+
{
|
|
652
|
+
provider: profile?.authProvider || authProviderId,
|
|
653
|
+
providerUserId: profile?.authProviderUserSid || profile?.id,
|
|
654
|
+
email: profile?.email,
|
|
655
|
+
displayName: profile?.displayName,
|
|
656
|
+
appUserId: profileProjectionEnabled ? profile?.id : null,
|
|
657
|
+
profileSource: profile?.profileSource || (profileProjectionEnabled ? "users" : "auth-provider")
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
provider: authProviderId,
|
|
661
|
+
profileSource: profileProjectionEnabled ? "users" : "auth-provider"
|
|
662
|
+
}
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function withActor(result) {
|
|
667
|
+
if (!result || typeof result !== "object") {
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
if (result.authenticated !== true && !result.profile) {
|
|
671
|
+
return result;
|
|
672
|
+
}
|
|
673
|
+
const actor = buildActorFromProfile(result.profile);
|
|
674
|
+
if (!actor) {
|
|
675
|
+
return result;
|
|
676
|
+
}
|
|
677
|
+
return normalizeAuthResult({
|
|
678
|
+
...result,
|
|
679
|
+
actor
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function withActorResult(operation) {
|
|
684
|
+
return async (...args) => withActor(await operation(...args));
|
|
685
|
+
}
|
|
686
|
+
|
|
568
687
|
async function syncProfileFromJwtClaims(claims) {
|
|
569
688
|
const supabaseUserId = normalizeProviderUserId(claims?.sub);
|
|
570
689
|
if (!supabaseUserId) {
|
|
@@ -661,7 +780,7 @@ function createService(options) {
|
|
|
661
780
|
oauthProviders: authOAuthProviders
|
|
662
781
|
});
|
|
663
782
|
|
|
664
|
-
return
|
|
783
|
+
return buildProviderSecurityStatus(authMethodsStatus);
|
|
665
784
|
}
|
|
666
785
|
|
|
667
786
|
const { register, resendRegisterConfirmation, login, requestOtpLogin, verifyOtpLogin, updateDisplayName } = createAccountFlows({
|
|
@@ -703,7 +822,7 @@ function createService(options) {
|
|
|
703
822
|
buildOAuthMethodId,
|
|
704
823
|
findAuthMethodById,
|
|
705
824
|
findLinkedIdentityByProvider,
|
|
706
|
-
buildSecurityStatusFromAuthMethodsStatus
|
|
825
|
+
buildSecurityStatusFromAuthMethodsStatus: buildProviderSecurityStatus
|
|
707
826
|
});
|
|
708
827
|
|
|
709
828
|
const {
|
|
@@ -723,6 +842,7 @@ function createService(options) {
|
|
|
723
842
|
mapRecoveryError,
|
|
724
843
|
syncProfileFromSupabaseUser,
|
|
725
844
|
setSessionFromRequestCookies,
|
|
845
|
+
setRecoverySessionFromRequestCookies,
|
|
726
846
|
resolvePasswordSignInPolicyForUserId,
|
|
727
847
|
mapPasswordUpdateError,
|
|
728
848
|
setPasswordSetupRequiredForUserId,
|
|
@@ -739,7 +859,7 @@ function createService(options) {
|
|
|
739
859
|
...statusOptions,
|
|
740
860
|
oauthProviders: authOAuthProviders
|
|
741
861
|
}),
|
|
742
|
-
buildSecurityStatusFromAuthMethodsStatus,
|
|
862
|
+
buildSecurityStatusFromAuthMethodsStatus: buildProviderSecurityStatus,
|
|
743
863
|
authMethodPasswordProvider: AUTH_METHOD_PASSWORD_PROVIDER,
|
|
744
864
|
buildAuthMethodsStatusFromProviderIds: (providerIds, statusOptions = {}) =>
|
|
745
865
|
buildAuthMethodsStatusFromProviderIds(providerIds, {
|
|
@@ -773,13 +893,13 @@ function createService(options) {
|
|
|
773
893
|
|
|
774
894
|
if (verification.status === "valid") {
|
|
775
895
|
const profile = await syncProfileFromJwtClaims(verification.payload);
|
|
776
|
-
return {
|
|
896
|
+
return withActor({
|
|
777
897
|
authenticated: true,
|
|
778
898
|
profile,
|
|
779
899
|
clearSession: false,
|
|
780
900
|
session: null,
|
|
781
901
|
transientFailure: false
|
|
782
|
-
};
|
|
902
|
+
});
|
|
783
903
|
}
|
|
784
904
|
|
|
785
905
|
if (verification.status === "transient") {
|
|
@@ -794,13 +914,13 @@ function createService(options) {
|
|
|
794
914
|
if (verification.status === "invalid") {
|
|
795
915
|
const supabaseVerification = await verifyAccessTokenViaSupabase(accessToken);
|
|
796
916
|
if (supabaseVerification.status === "valid") {
|
|
797
|
-
return {
|
|
917
|
+
return withActor({
|
|
798
918
|
authenticated: true,
|
|
799
919
|
profile: supabaseVerification.profile,
|
|
800
920
|
clearSession: false,
|
|
801
921
|
session: null,
|
|
802
922
|
transientFailure: false
|
|
803
|
-
};
|
|
923
|
+
});
|
|
804
924
|
}
|
|
805
925
|
|
|
806
926
|
if (supabaseVerification.status === "transient") {
|
|
@@ -875,29 +995,38 @@ function createService(options) {
|
|
|
875
995
|
|
|
876
996
|
const profile = await syncProfileFromSupabaseUser(refreshResponse.data.user, refreshResponse.data.user.email);
|
|
877
997
|
|
|
878
|
-
return {
|
|
998
|
+
return withActor({
|
|
879
999
|
authenticated: true,
|
|
880
1000
|
profile,
|
|
881
1001
|
clearSession: false,
|
|
882
1002
|
session: refreshResponse.data.session,
|
|
883
1003
|
transientFailure: false
|
|
884
|
-
};
|
|
1004
|
+
});
|
|
885
1005
|
}
|
|
886
1006
|
|
|
887
1007
|
function hasAccessTokenCookie(request) {
|
|
888
1008
|
const cookies = safeRequestCookies(request);
|
|
889
|
-
return Boolean(cookies[ACCESS_TOKEN_COOKIE]);
|
|
1009
|
+
return Boolean(cookies[ACCESS_TOKEN_COOKIE] || cookies[RECOVERY_ACCESS_TOKEN_COOKIE]);
|
|
890
1010
|
}
|
|
891
1011
|
|
|
892
1012
|
function hasSessionCookie(request) {
|
|
893
1013
|
const cookies = safeRequestCookies(request);
|
|
894
|
-
return Boolean(
|
|
1014
|
+
return Boolean(
|
|
1015
|
+
cookies[ACCESS_TOKEN_COOKIE] ||
|
|
1016
|
+
cookies[REFRESH_TOKEN_COOKIE] ||
|
|
1017
|
+
cookies[RECOVERY_ACCESS_TOKEN_COOKIE] ||
|
|
1018
|
+
cookies[RECOVERY_REFRESH_TOKEN_COOKIE]
|
|
1019
|
+
);
|
|
895
1020
|
}
|
|
896
1021
|
|
|
897
1022
|
function getOAuthProviderCatalog() {
|
|
898
1023
|
return authOAuthCatalogResponse;
|
|
899
1024
|
}
|
|
900
1025
|
|
|
1026
|
+
function getCapabilities() {
|
|
1027
|
+
return capabilities;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
901
1030
|
function isDevAuthBootstrapEnabled() {
|
|
902
1031
|
return devAuthConfig.enabled === true;
|
|
903
1032
|
}
|
|
@@ -911,26 +1040,27 @@ function createService(options) {
|
|
|
911
1040
|
const profile = requireAuthenticatedProfile(rawProfile, {
|
|
912
1041
|
context: "dev auth profile"
|
|
913
1042
|
});
|
|
914
|
-
return {
|
|
1043
|
+
return withActor({
|
|
915
1044
|
profile,
|
|
916
1045
|
session: await createDevAuthSession(profile, devAuthConfig)
|
|
917
|
-
};
|
|
1046
|
+
});
|
|
918
1047
|
}
|
|
919
1048
|
|
|
920
1049
|
return {
|
|
921
|
-
|
|
1050
|
+
getCapabilities,
|
|
1051
|
+
register: withActorResult(register),
|
|
922
1052
|
resendRegisterConfirmation,
|
|
923
|
-
login,
|
|
1053
|
+
login: withActorResult(login),
|
|
924
1054
|
requestOtpLogin,
|
|
925
|
-
verifyOtpLogin,
|
|
1055
|
+
verifyOtpLogin: withActorResult(verifyOtpLogin),
|
|
926
1056
|
oauthStart,
|
|
927
|
-
oauthComplete,
|
|
1057
|
+
oauthComplete: withActorResult(oauthComplete),
|
|
928
1058
|
startProviderLink,
|
|
929
1059
|
requestPasswordReset,
|
|
930
|
-
completePasswordRecovery,
|
|
1060
|
+
completePasswordRecovery: withActorResult(completePasswordRecovery),
|
|
931
1061
|
resetPassword,
|
|
932
|
-
updateDisplayName,
|
|
933
|
-
changePassword,
|
|
1062
|
+
updateDisplayName: withActorResult(updateDisplayName),
|
|
1063
|
+
changePassword: withActorResult(changePassword),
|
|
934
1064
|
setPasswordSignInEnabled,
|
|
935
1065
|
unlinkProvider,
|
|
936
1066
|
signOutOtherSessions,
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { resolveAllowedOriginsFromSurfaceDefinitions } from "@jskit-ai/kernel/shared/support/returnToPath";
|
|
2
2
|
import { withActionDefaults } from "@jskit-ai/kernel/shared/actions";
|
|
3
|
-
import { normalizeRecordId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
4
3
|
import { resolveAuthServiceDecorators } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
4
|
+
import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
|
|
5
5
|
import { createService } from "../lib/service.js";
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
import { buildAuthActions } from "../lib/actions/auth.contributor.js";
|
|
6
|
+
import { devLoginAsAction } from "../lib/actions/auth.contributor.js";
|
|
7
|
+
const PROFILE_MODE_PROVIDER = "provider";
|
|
9
8
|
const PROFILE_MODE_STANDALONE = "standalone";
|
|
10
9
|
const PROFILE_MODE_USERS = "users";
|
|
11
|
-
const SUPPORTED_PROFILE_MODES = Object.freeze([PROFILE_MODE_STANDALONE, PROFILE_MODE_USERS]);
|
|
10
|
+
const SUPPORTED_PROFILE_MODES = Object.freeze([PROFILE_MODE_PROVIDER, PROFILE_MODE_STANDALONE, PROFILE_MODE_USERS]);
|
|
12
11
|
const INTERNAL_JSON_REST_API = "internal.json-rest-api";
|
|
13
12
|
|
|
14
13
|
function splitCsv(value) {
|
|
@@ -93,11 +92,11 @@ function resolveAuthProviderConfig(env, appConfig = {}) {
|
|
|
93
92
|
|
|
94
93
|
function resolveAuthProfileMode(appConfig = {}) {
|
|
95
94
|
const auth = normalizeRecord(normalizeRecord(appConfig).auth);
|
|
96
|
-
const mode = String(auth.profileMode ||
|
|
95
|
+
const mode = String(auth.profileMode || PROFILE_MODE_PROVIDER)
|
|
97
96
|
.trim()
|
|
98
97
|
.toLowerCase();
|
|
99
98
|
if (SUPPORTED_PROFILE_MODES.includes(mode)) {
|
|
100
|
-
return mode;
|
|
99
|
+
return mode === PROFILE_MODE_STANDALONE ? PROFILE_MODE_PROVIDER : mode;
|
|
101
100
|
}
|
|
102
101
|
throw new Error(
|
|
103
102
|
`Unsupported config.auth.profileMode "${mode}". Supported values: ${SUPPORTED_PROFILE_MODES.join(", ")}.`
|
|
@@ -116,44 +115,32 @@ function isDevAuthBypassRequested(env) {
|
|
|
116
115
|
return parseBoolean(env?.AUTH_DEV_BYPASS_ENABLED, false);
|
|
117
116
|
}
|
|
118
117
|
|
|
119
|
-
function
|
|
120
|
-
const settingsByUserId = new Map();
|
|
121
|
-
|
|
122
|
-
function ensure(userId) {
|
|
123
|
-
const normalizedUserId = normalizeRecordId(userId, { fallback: null });
|
|
124
|
-
if (!normalizedUserId) {
|
|
125
|
-
throw new TypeError("User settings require a valid user id.");
|
|
126
|
-
}
|
|
127
|
-
if (!settingsByUserId.has(normalizedUserId)) {
|
|
128
|
-
settingsByUserId.set(normalizedUserId, {
|
|
129
|
-
userId: normalizedUserId,
|
|
130
|
-
passwordSignInEnabled: true,
|
|
131
|
-
passwordSetupRequired: false
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
return settingsByUserId.get(normalizedUserId);
|
|
135
|
-
}
|
|
136
|
-
|
|
118
|
+
function createProviderIdentityProfileSyncService({ authProviderId = "supabase" } = {}) {
|
|
137
119
|
return Object.freeze({
|
|
138
|
-
async
|
|
139
|
-
return
|
|
120
|
+
async findByIdentity() {
|
|
121
|
+
return null;
|
|
140
122
|
},
|
|
141
|
-
async
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
123
|
+
async syncIdentityProfile(profileLike) {
|
|
124
|
+
const source = normalizeRecord(profileLike);
|
|
125
|
+
const authProvider = String(source.authProvider || authProviderId).trim() || authProviderId;
|
|
126
|
+
const authProviderUserSid = String(source.authProviderUserSid || "").trim();
|
|
127
|
+
const email = normalizeEmail(source.email || "");
|
|
128
|
+
const displayName = String(source.displayName || email.split("@")[0] || "User").trim();
|
|
129
|
+
if (!authProviderUserSid || !email) {
|
|
130
|
+
throw new TypeError("Provider identity profile requires authProviderUserSid and email.");
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
id: authProviderUserSid,
|
|
134
|
+
authProvider,
|
|
135
|
+
authProviderUserSid,
|
|
136
|
+
email,
|
|
137
|
+
displayName,
|
|
138
|
+
profileSource: "auth-provider"
|
|
139
|
+
};
|
|
150
140
|
}
|
|
151
141
|
});
|
|
152
142
|
}
|
|
153
143
|
|
|
154
|
-
const fallbackUserSettingsRepository = createInMemoryUserSettingsRepository();
|
|
155
|
-
const fallbackStandaloneProfileSyncService = createStandaloneProfileSyncService();
|
|
156
|
-
|
|
157
144
|
function resolveCommonDependencies(scope) {
|
|
158
145
|
const dependencies = {};
|
|
159
146
|
if (scope.has("jskit.env")) {
|
|
@@ -176,6 +163,15 @@ function resolveRuntimeEnv(scope) {
|
|
|
176
163
|
};
|
|
177
164
|
}
|
|
178
165
|
|
|
166
|
+
function assertSelectedAuthProvider(env) {
|
|
167
|
+
const selectedProvider = String(env?.AUTH_PROVIDER || "").trim().toLowerCase();
|
|
168
|
+
if (selectedProvider && selectedProvider !== "supabase") {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`AUTH_PROVIDER is "${selectedProvider}", but @jskit-ai/auth-provider-supabase-core is installed as the selected auth provider.`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
179
175
|
function resolveOptionalRepositories(scope) {
|
|
180
176
|
const repositories = {};
|
|
181
177
|
if (scope.has("internal.repository.user-settings")) {
|
|
@@ -231,95 +227,69 @@ class AuthSupabaseServiceProvider {
|
|
|
231
227
|
throw new Error("AuthSupabaseServiceProvider requires application singleton()/has()/actions()/service().");
|
|
232
228
|
}
|
|
233
229
|
|
|
234
|
-
|
|
235
|
-
app.singleton("authService", (scope) => {
|
|
236
|
-
const env = resolveRuntimeEnv(scope);
|
|
237
|
-
const appConfig = scope.has("appConfig") ? scope.make("appConfig") : {};
|
|
238
|
-
const authProvider = resolveAuthProviderConfig(env, appConfig);
|
|
239
|
-
const repositories = resolveOptionalRepositories(scope);
|
|
240
|
-
const userSettingsRepository = repositories.userSettingsRepository || fallbackUserSettingsRepository;
|
|
241
|
-
const devAuthBypassEnabled = parseBoolean(env.AUTH_DEV_BYPASS_ENABLED, false);
|
|
242
|
-
if (!authProvider.supabaseUrl || !authProvider.supabasePublishableKey) {
|
|
243
|
-
if (!devAuthBypassEnabled) {
|
|
244
|
-
return null;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
const authProfileMode = resolveAuthProfileMode(appConfig);
|
|
248
|
-
let userProfileSyncService = fallbackStandaloneProfileSyncService;
|
|
249
|
-
if (authProfileMode === PROFILE_MODE_USERS) {
|
|
250
|
-
if (!scope.has("users.profile.sync.service")) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
"AuthSupabaseServiceProvider requires users.profile.sync.service when config.auth.profileMode is \"users\"."
|
|
253
|
-
);
|
|
254
|
-
}
|
|
255
|
-
userProfileSyncService = scope.make("users.profile.sync.service");
|
|
256
|
-
}
|
|
230
|
+
assertSelectedAuthProvider(resolveRuntimeEnv(app));
|
|
257
231
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim(),
|
|
261
|
-
authAllowedReturnToOrigins: resolveAllowedReturnToOrigins({
|
|
262
|
-
appConfig,
|
|
263
|
-
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim()
|
|
264
|
-
}),
|
|
265
|
-
nodeEnv: String(env.NODE_ENV || "development").trim() || "development",
|
|
266
|
-
userSettingsRepository,
|
|
267
|
-
userProfileSyncService,
|
|
268
|
-
userProfilesRepository: repositories.userProfilesRepository || null,
|
|
269
|
-
devAuthBypassEnabled,
|
|
270
|
-
devAuthBypassSecret: String(env.AUTH_DEV_BYPASS_SECRET || "").trim(),
|
|
271
|
-
devAuthAccessTtlSeconds: env.AUTH_DEV_ACCESS_TTL_SECONDS,
|
|
272
|
-
devAuthRefreshTtlSeconds: env.AUTH_DEV_REFRESH_TTL_SECONDS
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
return applyAuthServiceDecorators(scope, authService);
|
|
276
|
-
});
|
|
232
|
+
if (app.has("authService")) {
|
|
233
|
+
throw new Error("AuthSupabaseServiceProvider cannot register authService because another auth provider already registered it.");
|
|
277
234
|
}
|
|
278
235
|
|
|
279
|
-
app.
|
|
280
|
-
|
|
281
|
-
()
|
|
282
|
-
{
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
type: "entity.changed",
|
|
298
|
-
source: "users",
|
|
299
|
-
entity: "bootstrap",
|
|
300
|
-
operation: "updated",
|
|
301
|
-
entityId: ({ result }) => result?.id,
|
|
302
|
-
realtime: {
|
|
303
|
-
event: "users.bootstrap.changed",
|
|
304
|
-
audience: "actor_user"
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
]
|
|
236
|
+
app.singleton("authService", (scope) => {
|
|
237
|
+
const env = resolveRuntimeEnv(scope);
|
|
238
|
+
assertSelectedAuthProvider(env);
|
|
239
|
+
const appConfig = scope.has("appConfig") ? scope.make("appConfig") : {};
|
|
240
|
+
const authProvider = resolveAuthProviderConfig(env, appConfig);
|
|
241
|
+
const repositories = resolveOptionalRepositories(scope);
|
|
242
|
+
const userSettingsRepository = repositories.userSettingsRepository || null;
|
|
243
|
+
const devAuthBypassEnabled = parseBoolean(env.AUTH_DEV_BYPASS_ENABLED, false);
|
|
244
|
+
const authProfileMode = resolveAuthProfileMode(appConfig);
|
|
245
|
+
let userProfileSyncService = createProviderIdentityProfileSyncService({
|
|
246
|
+
authProviderId: authProvider.id
|
|
247
|
+
});
|
|
248
|
+
let profileProjectionEnabled = false;
|
|
249
|
+
if (authProfileMode === PROFILE_MODE_USERS) {
|
|
250
|
+
if (!scope.has("users.profile.sync.service")) {
|
|
251
|
+
throw new Error(
|
|
252
|
+
"AuthSupabaseServiceProvider requires users.profile.sync.service when config.auth.profileMode is \"users\"."
|
|
253
|
+
);
|
|
308
254
|
}
|
|
255
|
+
userProfileSyncService = scope.make("users.profile.sync.service");
|
|
256
|
+
profileProjectionEnabled = true;
|
|
257
|
+
} else if (scope.has("auth.profile.projector")) {
|
|
258
|
+
userProfileSyncService = scope.make("auth.profile.projector");
|
|
259
|
+
profileProjectionEnabled = true;
|
|
309
260
|
}
|
|
310
|
-
);
|
|
311
261
|
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
262
|
+
const authService = createService({
|
|
263
|
+
authProvider,
|
|
264
|
+
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim(),
|
|
265
|
+
authAllowedReturnToOrigins: resolveAllowedReturnToOrigins({
|
|
266
|
+
appConfig,
|
|
267
|
+
appPublicUrl: String(env.APP_PUBLIC_URL || "").trim()
|
|
268
|
+
}),
|
|
269
|
+
nodeEnv: String(env.NODE_ENV || "development").trim() || "development",
|
|
270
|
+
userSettingsRepository,
|
|
271
|
+
userProfileSyncService,
|
|
272
|
+
profileProjectionEnabled,
|
|
273
|
+
userProfilesRepository: repositories.userProfilesRepository || null,
|
|
274
|
+
devAuthBypassEnabled,
|
|
275
|
+
devAuthBypassSecret: String(env.AUTH_DEV_BYPASS_SECRET || "").trim(),
|
|
276
|
+
devAuthAccessTtlSeconds: env.AUTH_DEV_ACCESS_TTL_SECONDS,
|
|
277
|
+
devAuthRefreshTtlSeconds: env.AUTH_DEV_REFRESH_TTL_SECONDS
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
return applyAuthServiceDecorators(scope, authService);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
if (isDevAuthBypassEnabledForRegistration(resolveRuntimeEnv(app))) {
|
|
284
|
+
app.actions(
|
|
285
|
+
withActionDefaults([devLoginAsAction], {
|
|
286
|
+
domain: "auth",
|
|
287
|
+
dependencies: {
|
|
288
|
+
authService: "authService"
|
|
289
|
+
}
|
|
290
|
+
})
|
|
291
|
+
);
|
|
292
|
+
}
|
|
323
293
|
}
|
|
324
294
|
|
|
325
295
|
boot(app) {
|
|
@@ -327,10 +297,6 @@ class AuthSupabaseServiceProvider {
|
|
|
327
297
|
throw new Error("AuthSupabaseServiceProvider requires application make().");
|
|
328
298
|
}
|
|
329
299
|
|
|
330
|
-
if (!isDevAuthBypassRequested(resolveRuntimeEnv(app))) {
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
|
|
334
300
|
try {
|
|
335
301
|
app.make("authService");
|
|
336
302
|
} catch (error) {
|