@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,194 @@
1
+ import { AppError, createValidationError } from "@jskit-ai/kernel/server/runtime/errors";
2
+
3
+ const TRANSIENT_AUTH_MESSAGE_PARTS = [
4
+ "network",
5
+ "fetch",
6
+ "timeout",
7
+ "timed out",
8
+ "econn",
9
+ "enotfound",
10
+ "socket",
11
+ "temporar"
12
+ ];
13
+
14
+ const validationError = createValidationError;
15
+
16
+ function isTransientAuthMessage(message) {
17
+ const normalized = String(message || "").toLowerCase();
18
+ return TRANSIENT_AUTH_MESSAGE_PARTS.some((part) => normalized.includes(part));
19
+ }
20
+
21
+ function isTransientSupabaseError(error) {
22
+ if (!error) {
23
+ return false;
24
+ }
25
+
26
+ const status = Number(error.status || error.statusCode);
27
+ if (Number.isFinite(status) && status >= 500) {
28
+ return true;
29
+ }
30
+
31
+ return isTransientAuthMessage(error.message);
32
+ }
33
+
34
+ function sanitizeAuthMessage(message, fallback = "Authentication request could not be processed.") {
35
+ const normalized = String(message || "")
36
+ .replace(/\s+/g, " ")
37
+ .trim();
38
+ if (!normalized) {
39
+ return fallback;
40
+ }
41
+
42
+ return normalized.slice(0, 320);
43
+ }
44
+
45
+ function mapAuthError(error, fallbackStatus) {
46
+ if (isTransientSupabaseError(error)) {
47
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
48
+ }
49
+
50
+ const message = sanitizeAuthMessage(error?.message, "Authentication failed.");
51
+ const lower = message.toLowerCase();
52
+
53
+ if (lower.includes("already registered") || lower.includes("already been registered")) {
54
+ return new AppError(409, "Email is already registered.");
55
+ }
56
+
57
+ if (lower.includes("email not confirmed") || lower.includes("confirm your email")) {
58
+ return new AppError(403, "Account exists but email confirmation is required before login.");
59
+ }
60
+
61
+ if (lower.includes("invalid login credentials") || lower.includes("invalid credentials")) {
62
+ return new AppError(401, "Invalid email or password.");
63
+ }
64
+
65
+ if (
66
+ lower.includes("already linked") ||
67
+ lower.includes("identity is already linked") ||
68
+ (lower.includes("identity") && lower.includes("already exists"))
69
+ ) {
70
+ return new AppError(409, "This sign-in method is already linked.");
71
+ }
72
+
73
+ if (lower.includes("manual linking is disabled")) {
74
+ return new AppError(
75
+ 409,
76
+ "Provider linking is disabled in Supabase. Enable Manual Linking in Supabase Auth settings to link or unlink providers."
77
+ );
78
+ }
79
+
80
+ if (
81
+ lower.includes("last identity") ||
82
+ lower.includes("only identity") ||
83
+ (lower.includes("at least one") && lower.includes("identity"))
84
+ ) {
85
+ return new AppError(409, "At least one linked sign-in method must remain available.");
86
+ }
87
+
88
+ if (lower.includes("identity") && lower.includes("not found")) {
89
+ return new AppError(409, "This sign-in method is not currently linked.");
90
+ }
91
+
92
+ const status = Number.isInteger(Number(fallbackStatus)) ? Number(fallbackStatus) : 400;
93
+ if (status >= 500) {
94
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
95
+ }
96
+ if (status === 401) {
97
+ return new AppError(401, "Invalid email or password.");
98
+ }
99
+ if (status >= 400 && status < 500) {
100
+ return new AppError(status, "Authentication request could not be processed.");
101
+ }
102
+
103
+ return new AppError(status, "Authentication request could not be processed.");
104
+ }
105
+
106
+ function isUserNotFoundLikeAuthError(error) {
107
+ const message = String(error?.message || "").toLowerCase();
108
+ return (
109
+ message.includes("user not found") ||
110
+ message.includes("no user") ||
111
+ message.includes("email not found") ||
112
+ message.includes("signup is disabled") ||
113
+ message.includes("signups not allowed")
114
+ );
115
+ }
116
+
117
+ function mapRecoveryError(error) {
118
+ if (isTransientSupabaseError(error)) {
119
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
120
+ }
121
+
122
+ const status = Number(error?.status || error?.statusCode);
123
+ if (status === 429) {
124
+ return new AppError(429, "Too many recovery attempts. Please wait and try again.");
125
+ }
126
+
127
+ return new AppError(401, "Recovery link is invalid or has expired.");
128
+ }
129
+
130
+ function mapPasswordUpdateError(error) {
131
+ if (isTransientSupabaseError(error)) {
132
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
133
+ }
134
+
135
+ const message = String(error?.message || "").toLowerCase();
136
+ if (message.includes("same") && message.includes("password")) {
137
+ return createValidationError({
138
+ password: "New password must be different from the current password."
139
+ });
140
+ }
141
+
142
+ return createValidationError({
143
+ password: "Unable to update password with the provided value."
144
+ });
145
+ }
146
+
147
+ function mapOtpVerifyError(error) {
148
+ if (isTransientSupabaseError(error)) {
149
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
150
+ }
151
+
152
+ return new AppError(401, "One-time code is invalid or expired.");
153
+ }
154
+
155
+ function mapProfileUpdateError(error) {
156
+ if (isTransientSupabaseError(error)) {
157
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
158
+ }
159
+
160
+ return validationError({
161
+ displayName: "Unable to update profile details."
162
+ });
163
+ }
164
+
165
+ function mapCurrentPasswordError(error) {
166
+ if (isTransientSupabaseError(error)) {
167
+ return new AppError(503, "Authentication service temporarily unavailable. Please retry.");
168
+ }
169
+
170
+ const status = Number(error?.status || error?.statusCode);
171
+ if (status === 400 || status === 401 || status === 403) {
172
+ return validationError({
173
+ currentPassword: "Current password is incorrect."
174
+ });
175
+ }
176
+
177
+ return validationError({
178
+ currentPassword: "Unable to verify current password."
179
+ });
180
+ }
181
+
182
+ export {
183
+ isTransientAuthMessage,
184
+ isTransientSupabaseError,
185
+ sanitizeAuthMessage,
186
+ mapAuthError,
187
+ validationError,
188
+ isUserNotFoundLikeAuthError,
189
+ mapRecoveryError,
190
+ mapPasswordUpdateError,
191
+ mapOtpVerifyError,
192
+ mapProfileUpdateError,
193
+ mapCurrentPasswordError
194
+ };
@@ -0,0 +1,217 @@
1
+ import { AppError } from "@jskit-ai/kernel/server/runtime/errors";
2
+ import {
3
+ AUTH_ACCESS_TOKEN_MAX_LENGTH,
4
+ AUTH_RECOVERY_TOKEN_MAX_LENGTH,
5
+ AUTH_REFRESH_TOKEN_MAX_LENGTH
6
+ } from "@jskit-ai/auth-core/shared/authConstraints";
7
+ import { normalizeOAuthProviderList } from "@jskit-ai/auth-core/shared/oauthProviders";
8
+ import { validators } from "@jskit-ai/auth-core/server/validators";
9
+ import { normalizeOAuthProviderFromCatalog } from "./oauthProviderCatalog.js";
10
+ import { validationError } from "./authErrorMappers.js";
11
+
12
+ const OTP_VERIFY_TYPE = "email";
13
+ const SESSION_PAIR_REQUIRED_ERRORS = Object.freeze({
14
+ accessToken: "Access token is required when a refresh token is provided.",
15
+ refreshToken: "Refresh token is required when an access token is provided."
16
+ });
17
+
18
+ function applySessionPairValidation(accessToken, refreshToken, fieldErrors) {
19
+ if ((accessToken && !refreshToken) || (!accessToken && refreshToken)) {
20
+ if (!accessToken) {
21
+ fieldErrors.accessToken = SESSION_PAIR_REQUIRED_ERRORS.accessToken;
22
+ }
23
+ if (!refreshToken) {
24
+ fieldErrors.refreshToken = SESSION_PAIR_REQUIRED_ERRORS.refreshToken;
25
+ }
26
+ }
27
+ }
28
+
29
+ function resolveConfiguredOAuthProviders(options = {}) {
30
+ return normalizeOAuthProviderList(options.providerIds, { fallback: [] });
31
+ }
32
+
33
+ function normalizeOAuthProviderInput(value, options = {}) {
34
+ const providerIds = resolveConfiguredOAuthProviders(options);
35
+ if (providerIds.length < 1) {
36
+ throw validationError({
37
+ provider: "OAuth sign-in is not enabled."
38
+ });
39
+ }
40
+
41
+ const provider = normalizeOAuthProviderFromCatalog(value, {
42
+ providerIds,
43
+ fallback: options.defaultProvider
44
+ });
45
+ if (provider) {
46
+ return provider;
47
+ }
48
+
49
+ throw validationError({
50
+ provider: `OAuth provider must be one of: ${providerIds.join(", ")}.`
51
+ });
52
+ }
53
+
54
+ function validatePasswordRecoveryPayload(payload) {
55
+ const code = String(payload?.code || "").trim();
56
+ const tokenHash = String(payload?.tokenHash || "").trim();
57
+ const type = String(payload?.type || "recovery")
58
+ .trim()
59
+ .toLowerCase();
60
+ const accessToken = String(payload?.accessToken || "").trim();
61
+ const refreshToken = String(payload?.refreshToken || "").trim();
62
+
63
+ const fieldErrors = {};
64
+
65
+ if (type !== "recovery") {
66
+ fieldErrors.type = "Only recovery password reset links are supported.";
67
+ }
68
+
69
+ if (code.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
70
+ fieldErrors.code = "Recovery code is too long.";
71
+ }
72
+
73
+ if (tokenHash.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
74
+ fieldErrors.tokenHash = "Recovery token is too long.";
75
+ }
76
+
77
+ if (accessToken.length > AUTH_ACCESS_TOKEN_MAX_LENGTH) {
78
+ fieldErrors.accessToken = "Access token is too long.";
79
+ }
80
+
81
+ if (refreshToken.length > AUTH_REFRESH_TOKEN_MAX_LENGTH) {
82
+ fieldErrors.refreshToken = "Refresh token is too long.";
83
+ }
84
+
85
+ applySessionPairValidation(accessToken, refreshToken, fieldErrors);
86
+
87
+ const hasCode = Boolean(code);
88
+ const hasTokenHash = Boolean(tokenHash);
89
+ const hasSessionPair = Boolean(accessToken && refreshToken);
90
+
91
+ if (!hasCode && !hasTokenHash && !hasSessionPair) {
92
+ fieldErrors.recovery = "Recovery token is required.";
93
+ }
94
+
95
+ return {
96
+ code,
97
+ tokenHash,
98
+ type,
99
+ accessToken,
100
+ refreshToken,
101
+ hasCode,
102
+ hasTokenHash,
103
+ hasSessionPair,
104
+ fieldErrors
105
+ };
106
+ }
107
+
108
+ function parseOAuthCompletePayload(payload = {}, options = {}) {
109
+ const provider = normalizeOAuthProviderInput(payload.provider || options.defaultProvider, options);
110
+ const code = String(payload.code || "").trim();
111
+ const accessToken = String(payload.accessToken || "").trim();
112
+ const refreshToken = String(payload.refreshToken || "").trim();
113
+ const errorCode = String(payload.error || payload.error_code || "").trim();
114
+ const errorDescription = String(payload.errorDescription || payload.error_description || "").trim();
115
+ const fieldErrors = {};
116
+
117
+ if (code.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
118
+ fieldErrors.code = "OAuth code is too long.";
119
+ }
120
+
121
+ if (errorCode.length > 128) {
122
+ fieldErrors.error = "OAuth error code is too long.";
123
+ }
124
+
125
+ if (errorDescription.length > 1024) {
126
+ fieldErrors.errorDescription = "OAuth error description is too long.";
127
+ }
128
+
129
+ if (accessToken.length > AUTH_ACCESS_TOKEN_MAX_LENGTH) {
130
+ fieldErrors.accessToken = "Access token is too long.";
131
+ }
132
+
133
+ if (refreshToken.length > AUTH_REFRESH_TOKEN_MAX_LENGTH) {
134
+ fieldErrors.refreshToken = "Refresh token is too long.";
135
+ }
136
+
137
+ applySessionPairValidation(accessToken, refreshToken, fieldErrors);
138
+
139
+ const hasSessionPair = Boolean(accessToken && refreshToken);
140
+
141
+ if (!code && !errorCode && !hasSessionPair) {
142
+ fieldErrors.code = "OAuth code is required when access/refresh tokens are not provided.";
143
+ }
144
+
145
+ return {
146
+ provider,
147
+ code,
148
+ accessToken,
149
+ refreshToken,
150
+ hasSessionPair,
151
+ errorCode,
152
+ errorDescription,
153
+ fieldErrors
154
+ };
155
+ }
156
+
157
+ function parseOtpLoginVerifyPayload(payload = {}) {
158
+ const parsedEmail = validators.forgotPasswordInput(payload);
159
+ const token = String(payload?.token || "").trim();
160
+ const tokenHash = String(payload?.tokenHash || "").trim();
161
+ const type = String(payload?.type || OTP_VERIFY_TYPE)
162
+ .trim()
163
+ .toLowerCase();
164
+ const fieldErrors = {
165
+ ...parsedEmail.fieldErrors
166
+ };
167
+
168
+ if (type !== OTP_VERIFY_TYPE) {
169
+ fieldErrors.type = "Only email OTP verification is supported.";
170
+ }
171
+
172
+ if (!token && !tokenHash) {
173
+ fieldErrors.token = "One-time code is required.";
174
+ }
175
+
176
+ if (token && token.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
177
+ fieldErrors.token = "One-time code is too long.";
178
+ }
179
+
180
+ if (tokenHash && tokenHash.length > AUTH_RECOVERY_TOKEN_MAX_LENGTH) {
181
+ fieldErrors.tokenHash = "One-time token hash is too long.";
182
+ }
183
+
184
+ if (token && parsedEmail.fieldErrors.email) {
185
+ fieldErrors.email = parsedEmail.fieldErrors.email;
186
+ } else if (tokenHash) {
187
+ delete fieldErrors.email;
188
+ }
189
+
190
+ return {
191
+ email: parsedEmail.email,
192
+ token,
193
+ tokenHash,
194
+ type,
195
+ fieldErrors
196
+ };
197
+ }
198
+
199
+ function mapOAuthCallbackError(errorCode) {
200
+ const normalizedCode = String(errorCode || "")
201
+ .trim()
202
+ .toLowerCase();
203
+
204
+ if (normalizedCode === "access_denied") {
205
+ return new AppError(401, "OAuth sign-in was cancelled.");
206
+ }
207
+
208
+ return new AppError(401, "OAuth sign-in failed.");
209
+ }
210
+
211
+ export {
212
+ normalizeOAuthProviderInput,
213
+ validatePasswordRecoveryPayload,
214
+ parseOAuthCompletePayload,
215
+ parseOtpLoginVerifyPayload,
216
+ mapOAuthCallbackError
217
+ };
@@ -0,0 +1,49 @@
1
+ import { isTransientAuthMessage } from "./authErrorMappers.js";
2
+
3
+ const INVALID_JWT_ERROR_CODES = new Set([
4
+ "ERR_JWS_SIGNATURE_VERIFICATION_FAILED",
5
+ "ERR_JWT_INVALID",
6
+ "ERR_JWT_CLAIM_VALIDATION_FAILED",
7
+ "ERR_JWS_INVALID",
8
+ "ERR_JOSE_ALG_NOT_ALLOWED",
9
+ "ERR_JWKS_NO_MATCHING_KEY"
10
+ ]);
11
+
12
+ const TRANSIENT_JWT_ERROR_CODES = new Set(["ERR_JWKS_TIMEOUT", "ERR_JOSE_GENERIC", "ERR_JWKS_INVALID"]);
13
+
14
+ let joseImportPromise = null;
15
+ function loadJose() {
16
+ if (!joseImportPromise) {
17
+ joseImportPromise = import("jose");
18
+ }
19
+ return joseImportPromise;
20
+ }
21
+
22
+ function isExpiredJwtError(error) {
23
+ const code = String(error?.code || "");
24
+ const name = String(error?.name || "");
25
+ return code === "ERR_JWT_EXPIRED" || name === "JWTExpired";
26
+ }
27
+
28
+ function classifyJwtVerifyError(error) {
29
+ if (isExpiredJwtError(error)) {
30
+ return "expired";
31
+ }
32
+
33
+ const code = String(error?.code || "");
34
+ if (INVALID_JWT_ERROR_CODES.has(code)) {
35
+ return "invalid";
36
+ }
37
+
38
+ if (TRANSIENT_JWT_ERROR_CODES.has(code)) {
39
+ return "transient";
40
+ }
41
+
42
+ if (isTransientAuthMessage(error?.message)) {
43
+ return "transient";
44
+ }
45
+
46
+ return "invalid";
47
+ }
48
+
49
+ export { loadJose, isExpiredJwtError, classifyJwtVerifyError };
@@ -0,0 +1,233 @@
1
+ import {
2
+ AUTH_METHOD_EMAIL_OTP_ID,
3
+ AUTH_METHOD_EMAIL_OTP_PROVIDER,
4
+ AUTH_METHOD_KIND_OAUTH,
5
+ AUTH_METHOD_KIND_OTP,
6
+ AUTH_METHOD_KIND_PASSWORD,
7
+ AUTH_METHOD_MINIMUM_ENABLED,
8
+ AUTH_METHOD_PASSWORD_ID,
9
+ AUTH_METHOD_PASSWORD_PROVIDER,
10
+ buildAuthMethodDefinitions,
11
+ buildOAuthMethodId
12
+ } from "@jskit-ai/auth-core/shared/authMethods";
13
+
14
+ function normalizeIdentityProviderId(value) {
15
+ return String(value || "")
16
+ .trim()
17
+ .toLowerCase();
18
+ }
19
+
20
+ function normalizeProviderIdList(providerIds) {
21
+ const normalized = [];
22
+ for (const providerId of Array.isArray(providerIds) ? providerIds : []) {
23
+ const provider = normalizeIdentityProviderId(providerId);
24
+ if (!provider) {
25
+ continue;
26
+ }
27
+ normalized.push(provider);
28
+ }
29
+ return normalized;
30
+ }
31
+
32
+ function countEnabledMethods(methods) {
33
+ let count = 0;
34
+ for (const method of methods) {
35
+ if (method?.enabled === true) {
36
+ count += 1;
37
+ }
38
+ }
39
+ return count;
40
+ }
41
+
42
+ function countConfiguredIdentityMethods(methods) {
43
+ let count = 0;
44
+ for (const method of methods) {
45
+ if (method.kind === AUTH_METHOD_KIND_OAUTH && method.configured) {
46
+ count += 1;
47
+ continue;
48
+ }
49
+ if (method.kind === AUTH_METHOD_KIND_PASSWORD && method.configured) {
50
+ count += 1;
51
+ }
52
+ }
53
+ return count;
54
+ }
55
+
56
+ function collectProviderIdsFromSupabaseUser(user) {
57
+ const providerIds = new Set();
58
+
59
+ const appProvider = normalizeIdentityProviderId(user?.app_metadata?.provider);
60
+ if (appProvider) {
61
+ providerIds.add(appProvider);
62
+ }
63
+
64
+ const appProviders = Array.isArray(user?.app_metadata?.providers) ? user.app_metadata.providers : [];
65
+ for (const provider of appProviders) {
66
+ const normalized = normalizeIdentityProviderId(provider);
67
+ if (normalized) {
68
+ providerIds.add(normalized);
69
+ }
70
+ }
71
+
72
+ const identities = Array.isArray(user?.identities) ? user.identities : [];
73
+ for (const identity of identities) {
74
+ const normalized = normalizeIdentityProviderId(identity?.provider);
75
+ if (normalized) {
76
+ providerIds.add(normalized);
77
+ }
78
+ }
79
+
80
+ return [...providerIds];
81
+ }
82
+
83
+ function buildAuthMethodsStatusFromProviderIds(providerIds, options = {}) {
84
+ const normalizedProviders = normalizeProviderIdList(providerIds);
85
+ const uniqueProviders = new Set(normalizedProviders);
86
+ const passwordSignInEnabled = options.passwordSignInEnabled !== false;
87
+ const passwordSetupRequired = options.passwordSetupRequired === true;
88
+ const oauthProviders = Array.isArray(options.oauthProviders) ? options.oauthProviders : [];
89
+ const methodDefinitions = buildAuthMethodDefinitions({ oauthProviders });
90
+ const methods = [];
91
+
92
+ for (const definition of methodDefinitions) {
93
+ if (definition.kind === AUTH_METHOD_KIND_PASSWORD) {
94
+ const configured = uniqueProviders.has(AUTH_METHOD_PASSWORD_PROVIDER);
95
+ const enabled = configured && passwordSignInEnabled;
96
+ methods.push({
97
+ id: AUTH_METHOD_PASSWORD_ID,
98
+ kind: AUTH_METHOD_KIND_PASSWORD,
99
+ provider: AUTH_METHOD_PASSWORD_PROVIDER,
100
+ label: definition.label,
101
+ configured,
102
+ enabled,
103
+ canEnable: configured && !enabled,
104
+ canDisable: false,
105
+ supportsSecretUpdate: true,
106
+ requiresCurrentPassword: enabled && !passwordSetupRequired
107
+ });
108
+ continue;
109
+ }
110
+
111
+ if (definition.kind === AUTH_METHOD_KIND_OTP) {
112
+ methods.push({
113
+ id: AUTH_METHOD_EMAIL_OTP_ID,
114
+ kind: AUTH_METHOD_KIND_OTP,
115
+ provider: AUTH_METHOD_EMAIL_OTP_PROVIDER,
116
+ label: definition.label,
117
+ configured: true,
118
+ enabled: true,
119
+ canEnable: false,
120
+ canDisable: false,
121
+ supportsSecretUpdate: false,
122
+ requiresCurrentPassword: false
123
+ });
124
+ continue;
125
+ }
126
+
127
+ if (definition.kind === AUTH_METHOD_KIND_OAUTH) {
128
+ const provider = normalizeIdentityProviderId(definition.provider);
129
+ const configured = uniqueProviders.has(provider);
130
+ methods.push({
131
+ id: buildOAuthMethodId(provider),
132
+ kind: AUTH_METHOD_KIND_OAUTH,
133
+ provider,
134
+ label: definition.label,
135
+ configured,
136
+ enabled: configured,
137
+ canEnable: !configured,
138
+ canDisable: false,
139
+ supportsSecretUpdate: false,
140
+ requiresCurrentPassword: false
141
+ });
142
+ }
143
+ }
144
+
145
+ const enabledMethodsCount = countEnabledMethods(methods);
146
+ const minimumEnabledMethods = AUTH_METHOD_MINIMUM_ENABLED;
147
+ const canDisableAny = enabledMethodsCount > minimumEnabledMethods;
148
+ const configuredIdentityMethodCount = countConfiguredIdentityMethods(methods);
149
+
150
+ for (const method of methods) {
151
+ if (method.kind === AUTH_METHOD_KIND_OAUTH) {
152
+ method.canDisable = method.enabled && configuredIdentityMethodCount > 1;
153
+ continue;
154
+ }
155
+
156
+ method.canDisable = method.enabled && canDisableAny;
157
+ }
158
+
159
+ return {
160
+ methods,
161
+ enabledMethodsCount,
162
+ minimumEnabledMethods,
163
+ canDisableAny
164
+ };
165
+ }
166
+
167
+ function buildAuthMethodsStatusFromSupabaseUser(user, options = {}) {
168
+ return buildAuthMethodsStatusFromProviderIds(collectProviderIdsFromSupabaseUser(user), options);
169
+ }
170
+
171
+ function buildSecurityStatusFromAuthMethodsStatus(authMethodsStatus) {
172
+ const minimumEnabledMethods = Number(authMethodsStatus?.minimumEnabledMethods || AUTH_METHOD_MINIMUM_ENABLED);
173
+ let enabledMethodsCount = 0;
174
+
175
+ const storedCount = Number(authMethodsStatus?.enabledMethodsCount);
176
+ if (Number.isFinite(storedCount)) {
177
+ enabledMethodsCount = storedCount;
178
+ } else if (Array.isArray(authMethodsStatus?.methods)) {
179
+ enabledMethodsCount = countEnabledMethods(authMethodsStatus.methods);
180
+ }
181
+
182
+ return {
183
+ mfa: {
184
+ status: "not_enabled",
185
+ enrolled: false,
186
+ methods: []
187
+ },
188
+ authPolicy: {
189
+ minimumEnabledMethods,
190
+ enabledMethodsCount
191
+ },
192
+ authMethods: Array.isArray(authMethodsStatus?.methods) ? authMethodsStatus.methods : []
193
+ };
194
+ }
195
+
196
+ function findAuthMethodById(authMethodsStatus, methodId) {
197
+ const normalizedMethodId = String(methodId || "")
198
+ .trim()
199
+ .toLowerCase();
200
+ if (!normalizedMethodId || !Array.isArray(authMethodsStatus?.methods)) {
201
+ return null;
202
+ }
203
+
204
+ return authMethodsStatus.methods.find(
205
+ (method) =>
206
+ String(method?.id || "")
207
+ .trim()
208
+ .toLowerCase() === normalizedMethodId
209
+ );
210
+ }
211
+
212
+ function findLinkedIdentityByProvider(user, provider) {
213
+ const normalizedProvider = normalizeIdentityProviderId(provider);
214
+ const identities = Array.isArray(user?.identities) ? user.identities : [];
215
+
216
+ for (const identity of identities) {
217
+ if (normalizeIdentityProviderId(identity?.provider) === normalizedProvider) {
218
+ return identity;
219
+ }
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ export {
226
+ normalizeIdentityProviderId,
227
+ collectProviderIdsFromSupabaseUser,
228
+ buildAuthMethodsStatusFromProviderIds,
229
+ buildAuthMethodsStatusFromSupabaseUser,
230
+ buildSecurityStatusFromAuthMethodsStatus,
231
+ findAuthMethodById,
232
+ findLinkedIdentityByProvider
233
+ };