@luckystack/login 0.15.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/README.md +6 -0
- package/dist/{chunk-ML3OQ6R4.js → chunk-CYGFR56I.js} +24 -3
- package/dist/chunk-CYGFR56I.js.map +1 -0
- package/dist/{chunk-S7TST3X6.js → chunk-MZNCQ26U.js} +10 -4
- package/dist/chunk-MZNCQ26U.js.map +1 -0
- package/dist/hookPayloads.d.ts +5 -0
- package/dist/index.js +142 -34
- package/dist/index.js.map +1 -1
- package/dist/logout-VTVJTFSZ.js +7 -0
- package/dist/oauthProviders.d.ts +12 -2
- package/dist/register.js +1 -1
- package/dist/userAdapter.d.ts +22 -0
- package/docs/oauth-providers.md +3 -1
- package/docs/user-adapter.md +2 -0
- package/package.json +3 -3
- package/dist/chunk-ML3OQ6R4.js.map +0 -1
- package/dist/chunk-S7TST3X6.js.map +0 -1
- package/dist/logout-PO7TJ442.js +0 -7
- /package/dist/{logout-PO7TJ442.js.map → logout-VTVJTFSZ.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
registerOAuthProviderFactory,
|
|
15
15
|
registerOAuthProviders,
|
|
16
16
|
registerUserAdapter
|
|
17
|
-
} from "./chunk-
|
|
17
|
+
} from "./chunk-CYGFR56I.js";
|
|
18
18
|
import {
|
|
19
19
|
activeUsersKeyFor,
|
|
20
20
|
deleteSession,
|
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
revokeUserSessions,
|
|
30
30
|
saveSession,
|
|
31
31
|
sessionKeyFor
|
|
32
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-MZNCQ26U.js";
|
|
33
33
|
|
|
34
34
|
// src/index.ts
|
|
35
35
|
import { registerRedactedLogKeys, registerSessionProvider } from "@luckystack/core";
|
|
@@ -515,6 +515,17 @@ var readStringField = (record, key, fallback = "") => {
|
|
|
515
515
|
if (typeof value === "boolean") return String(value);
|
|
516
516
|
return fallback;
|
|
517
517
|
};
|
|
518
|
+
var readAccountId = (record, key) => {
|
|
519
|
+
const value = record[key];
|
|
520
|
+
if (typeof value === "string" && value !== "") return value;
|
|
521
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return String(value);
|
|
522
|
+
return void 0;
|
|
523
|
+
};
|
|
524
|
+
var compareStoredAccountId = (stored, accountId) => {
|
|
525
|
+
if (stored === null || stored === void 0 || stored === "") return "unbound";
|
|
526
|
+
const comparable = typeof stored === "string" || typeof stored === "number" || typeof stored === "bigint";
|
|
527
|
+
return comparable && String(stored) === accountId ? "match" : "mismatch";
|
|
528
|
+
};
|
|
518
529
|
var generateAvatarFallbackColor = () => `#${Math.floor(Math.random() * 16777215).toString(16).padStart(6, "0")}`;
|
|
519
530
|
var resolveUploadedAvatar = async (userId) => {
|
|
520
531
|
const filePath = path.join(uploadsFolder(), `${userId}.webp`);
|
|
@@ -522,7 +533,13 @@ var resolveUploadedAvatar = async (userId) => {
|
|
|
522
533
|
return avatarStat ? `${userId}.webp` : null;
|
|
523
534
|
};
|
|
524
535
|
var sanitizeUserForSession = (user) => {
|
|
525
|
-
const {
|
|
536
|
+
const {
|
|
537
|
+
password: _password,
|
|
538
|
+
totpSecret: _totpSecret,
|
|
539
|
+
recoveryCodes: _recoveryCodes,
|
|
540
|
+
providerAccountId: _providerAccountId,
|
|
541
|
+
...safeUser
|
|
542
|
+
} = user;
|
|
526
543
|
return safeUser;
|
|
527
544
|
};
|
|
528
545
|
var emitLoginFailed = (payload) => {
|
|
@@ -841,6 +858,7 @@ var exchangeOAuthToken = async (provider, code, codeVerifier) => {
|
|
|
841
858
|
}
|
|
842
859
|
return accessToken;
|
|
843
860
|
};
|
|
861
|
+
var refuseOAuth = (refusal = "oauth.userResolveFailed", userId) => userId === void 0 ? { refusal } : { refusal, userId };
|
|
844
862
|
var fetchOAuthProfile = async (provider, accessToken) => {
|
|
845
863
|
const getUserData = async () => {
|
|
846
864
|
const response2 = await fetch(provider.userInfoURL, {
|
|
@@ -863,6 +881,17 @@ var fetchOAuthProfile = async (provider, accessToken) => {
|
|
|
863
881
|
return null;
|
|
864
882
|
}
|
|
865
883
|
const userData = asRecord(response);
|
|
884
|
+
let accountId;
|
|
885
|
+
if (provider.accountIdKey) {
|
|
886
|
+
accountId = readAccountId(userData, provider.accountIdKey);
|
|
887
|
+
if (accountId === void 0) {
|
|
888
|
+
getLogger3().warn("oauth: provider userinfo has no stable account id \u2014 refusing the login", {
|
|
889
|
+
provider: provider.name,
|
|
890
|
+
accountIdKey: provider.accountIdKey
|
|
891
|
+
});
|
|
892
|
+
return refuseOAuth("oauth.accountIdMissing");
|
|
893
|
+
}
|
|
894
|
+
}
|
|
866
895
|
const resolvedName = readStringField(userData, provider.nameKey);
|
|
867
896
|
const name = resolvedName === "" ? "didnt find a name" : resolvedName;
|
|
868
897
|
const emailValue = provider.alwaysUseGetEmail ? void 0 : userData[provider.emailKey];
|
|
@@ -900,23 +929,75 @@ var fetchOAuthProfile = async (provider, accessToken) => {
|
|
|
900
929
|
}
|
|
901
930
|
email = normalizedEmail;
|
|
902
931
|
}
|
|
903
|
-
return { email, name, avatar, emailVerified, rawUserData: userData };
|
|
932
|
+
return { email, name, avatar, emailVerified, accountId, rawUserData: userData };
|
|
904
933
|
};
|
|
905
|
-
var
|
|
906
|
-
|
|
907
|
-
if (
|
|
908
|
-
|
|
934
|
+
var warnedMissingAccountIdLookup = false;
|
|
935
|
+
var supportsAccountBinding = (adapter) => {
|
|
936
|
+
if (typeof adapter.findByProviderAccountId === "function") return true;
|
|
937
|
+
if (!warnedMissingAccountIdLookup) {
|
|
938
|
+
warnedMissingAccountIdLookup = true;
|
|
939
|
+
getLogger3().warn(
|
|
940
|
+
"[login] the registered UserAdapter does not implement findByProviderAccountId \u2014 OAuth accounts are not bound to the provider id: existing accounts are still matched by verified email only, and an email change at the provider starts a new account. Implement findByProviderAccountId and store providerAccountId to enable binding."
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
return false;
|
|
944
|
+
};
|
|
945
|
+
var signInExistingOAuthUser = async (userAdapter, existing) => {
|
|
946
|
+
const uploadedAvatar = await resolveUploadedAvatar(existing.id);
|
|
947
|
+
if (uploadedAvatar) {
|
|
948
|
+
existing.avatar = uploadedAvatar;
|
|
949
|
+
}
|
|
950
|
+
const previousLogin = existing.lastLogin ?? null;
|
|
951
|
+
const nowLogin = /* @__PURE__ */ new Date();
|
|
952
|
+
await tryCatch2(() => userAdapter.update(existing.id, { lastLogin: nowLogin }));
|
|
953
|
+
return {
|
|
954
|
+
user: {
|
|
955
|
+
...sanitizeUserForSession(existing),
|
|
956
|
+
token: "",
|
|
957
|
+
lastLogin: nowLogin,
|
|
958
|
+
previousLogin
|
|
959
|
+
},
|
|
960
|
+
isNewUser: false
|
|
961
|
+
};
|
|
962
|
+
};
|
|
963
|
+
var passesOAuthPreLogin = async (provider, providerEmail, existing) => {
|
|
964
|
+
const storedEmail = typeof existing?.email === "string" && existing.email !== "" ? existing.email : void 0;
|
|
965
|
+
const preLoginResult = await dispatchHook("preLogin", {
|
|
966
|
+
email: storedEmail ?? providerEmail,
|
|
967
|
+
provider: provider.name,
|
|
968
|
+
...existing ? { userId: existing.id } : {},
|
|
969
|
+
providerEmail
|
|
970
|
+
});
|
|
909
971
|
if (preLoginResult.stopped) {
|
|
910
972
|
getLogger3().warn(`oauth login aborted by preLogin hook`, { errorCode: preLoginResult.signal.errorCode });
|
|
911
|
-
return
|
|
973
|
+
return false;
|
|
912
974
|
}
|
|
975
|
+
return true;
|
|
976
|
+
};
|
|
977
|
+
var findOrCreateOAuthUser = async (provider, profile) => {
|
|
978
|
+
const { email, name, avatar, emailVerified, accountId } = profile;
|
|
979
|
+
if (!email) return refuseOAuth();
|
|
913
980
|
const userAdapter = getUserAdapter();
|
|
981
|
+
const bindingId = accountId !== void 0 && supportsAccountBinding(userAdapter) ? accountId : void 0;
|
|
982
|
+
if (bindingId !== void 0) {
|
|
983
|
+
const [boundError, boundUser] = await tryCatch2(
|
|
984
|
+
async () => await userAdapter.findByProviderAccountId?.({ provider: provider.name, providerAccountId: bindingId }) ?? null
|
|
985
|
+
);
|
|
986
|
+
if (boundError) {
|
|
987
|
+
getLogger3().error("oauth: findByProviderAccountId failed", boundError);
|
|
988
|
+
return refuseOAuth();
|
|
989
|
+
}
|
|
990
|
+
if (boundUser?.id) {
|
|
991
|
+
if (!await passesOAuthPreLogin(provider, email, boundUser)) return refuseOAuth(void 0, boundUser.id);
|
|
992
|
+
return signInExistingOAuthUser(userAdapter, boundUser);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
914
995
|
const [findError, findResponse] = await tryCatch2(
|
|
915
996
|
() => resolveUserByEmail(userAdapter, { email, provider: provider.name })
|
|
916
997
|
);
|
|
917
998
|
if (findError) {
|
|
918
999
|
getLogger3().error("oauth: findByEmail failed", findError);
|
|
919
|
-
return
|
|
1000
|
+
return refuseOAuth();
|
|
920
1001
|
}
|
|
921
1002
|
if (findResponse?.id) {
|
|
922
1003
|
const existingProvider = findResponse.provider ?? null;
|
|
@@ -926,40 +1007,56 @@ var findOrCreateOAuthUser = async (provider, profile) => {
|
|
|
926
1007
|
"oauth: refusing to link an unverified provider email to an existing account created by a different provider (account-takeover guard)",
|
|
927
1008
|
{ provider: provider.name, existingProvider }
|
|
928
1009
|
);
|
|
929
|
-
return
|
|
1010
|
+
return refuseOAuth("oauth.emailUnverified", findResponse.id);
|
|
1011
|
+
}
|
|
1012
|
+
let bindTo;
|
|
1013
|
+
if (accountId !== void 0 && (!isCrossProviderLink || existingProvider === null)) {
|
|
1014
|
+
const stored = compareStoredAccountId(findResponse.providerAccountId, accountId);
|
|
1015
|
+
if (stored === "mismatch") {
|
|
1016
|
+
getLogger3().warn(
|
|
1017
|
+
"oauth: refusing login \u2014 the account with this email is bound to a different provider account (account-takeover guard)",
|
|
1018
|
+
{ provider: provider.name, userId: findResponse.id }
|
|
1019
|
+
);
|
|
1020
|
+
return refuseOAuth("oauth.accountBoundElsewhere", findResponse.id);
|
|
1021
|
+
}
|
|
1022
|
+
if (stored === "unbound" && !isCrossProviderLink) {
|
|
1023
|
+
if (!emailVerified) {
|
|
1024
|
+
getLogger3().warn(
|
|
1025
|
+
"oauth: refusing login \u2014 the account with this email is not bound to a provider id yet and the provider did not verify the email (account-takeover guard)",
|
|
1026
|
+
{ provider: provider.name, userId: findResponse.id }
|
|
1027
|
+
);
|
|
1028
|
+
return refuseOAuth("oauth.emailUnverified", findResponse.id);
|
|
1029
|
+
}
|
|
1030
|
+
bindTo = bindingId;
|
|
1031
|
+
}
|
|
930
1032
|
}
|
|
931
|
-
|
|
932
|
-
if (
|
|
933
|
-
|
|
1033
|
+
if (!await passesOAuthPreLogin(provider, email, findResponse)) return refuseOAuth(void 0, findResponse.id);
|
|
1034
|
+
if (bindTo !== void 0) {
|
|
1035
|
+
const providerAccountId = bindTo;
|
|
1036
|
+
const [bindError] = await tryCatch2(() => userAdapter.update(findResponse.id, { providerAccountId }));
|
|
1037
|
+
if (bindError) {
|
|
1038
|
+
getLogger3().error("oauth: binding the account to the provider id failed", bindError);
|
|
1039
|
+
return refuseOAuth(void 0, findResponse.id);
|
|
1040
|
+
}
|
|
934
1041
|
}
|
|
935
|
-
|
|
936
|
-
const nowLogin = /* @__PURE__ */ new Date();
|
|
937
|
-
await tryCatch2(() => userAdapter.update(findResponse.id, { lastLogin: nowLogin }));
|
|
938
|
-
return {
|
|
939
|
-
user: {
|
|
940
|
-
...sanitizeUserForSession(findResponse),
|
|
941
|
-
token: "",
|
|
942
|
-
lastLogin: nowLogin,
|
|
943
|
-
previousLogin
|
|
944
|
-
},
|
|
945
|
-
isNewUser: false
|
|
946
|
-
};
|
|
1042
|
+
return signInExistingOAuthUser(userAdapter, findResponse);
|
|
947
1043
|
}
|
|
1044
|
+
if (!await passesOAuthPreLogin(provider, email)) return refuseOAuth();
|
|
948
1045
|
if (!emailVerified) {
|
|
949
1046
|
getLogger3().warn(
|
|
950
1047
|
"oauth: refusing first-login account creation for an unverified provider email (account-squatting guard)",
|
|
951
1048
|
{ provider: provider.name }
|
|
952
1049
|
);
|
|
953
|
-
return
|
|
1050
|
+
return refuseOAuth();
|
|
954
1051
|
}
|
|
955
1052
|
if (!getProjectConfig4().auth.allowRegistration) {
|
|
956
1053
|
getLogger3().warn("oauth: registration disabled \u2014 refusing first-login account creation");
|
|
957
|
-
return
|
|
1054
|
+
return refuseOAuth();
|
|
958
1055
|
}
|
|
959
1056
|
const preRegisterResult = await dispatchHook("preRegister", { email, provider: provider.name, name });
|
|
960
1057
|
if (preRegisterResult.stopped) {
|
|
961
1058
|
getLogger3().warn(`oauth register aborted by preRegister hook`, { errorCode: preRegisterResult.signal.errorCode });
|
|
962
|
-
return
|
|
1059
|
+
return refuseOAuth();
|
|
963
1060
|
}
|
|
964
1061
|
const createNewUser = async () => userAdapter.create({
|
|
965
1062
|
email,
|
|
@@ -967,14 +1064,15 @@ var findOrCreateOAuthUser = async (provider, profile) => {
|
|
|
967
1064
|
name,
|
|
968
1065
|
avatar,
|
|
969
1066
|
avatarFallback: generateAvatarFallbackColor(),
|
|
970
|
-
language: getProjectConfig4().defaultLanguage
|
|
1067
|
+
language: getProjectConfig4().defaultLanguage,
|
|
1068
|
+
...bindingId === void 0 ? {} : { providerAccountId: bindingId }
|
|
971
1069
|
});
|
|
972
1070
|
const [createError, createResponse] = await tryCatch2(createNewUser);
|
|
973
1071
|
if (createError) {
|
|
974
1072
|
getLogger3().error("oauth: user create failed", createError);
|
|
975
|
-
return
|
|
1073
|
+
return refuseOAuth();
|
|
976
1074
|
}
|
|
977
|
-
if (!createResponse) return
|
|
1075
|
+
if (!createResponse) return refuseOAuth();
|
|
978
1076
|
return {
|
|
979
1077
|
user: { ...sanitizeUserForSession(createResponse), token: "" },
|
|
980
1078
|
isNewUser: true
|
|
@@ -1076,9 +1174,19 @@ var loginCallback = async (pathname, req, _res, options = {}) => {
|
|
|
1076
1174
|
emitLoginFailed({ provider: providerName, reason: "oauth.profileFetchFailed", stage: "oauth" });
|
|
1077
1175
|
return false;
|
|
1078
1176
|
}
|
|
1177
|
+
if ("refusal" in profile) {
|
|
1178
|
+
emitLoginFailed({ provider: providerName, reason: profile.refusal, stage: "oauth" });
|
|
1179
|
+
return false;
|
|
1180
|
+
}
|
|
1079
1181
|
const resolved = await findOrCreateOAuthUser(provider, profile);
|
|
1080
|
-
if (
|
|
1081
|
-
emitLoginFailed({
|
|
1182
|
+
if ("refusal" in resolved) {
|
|
1183
|
+
emitLoginFailed({
|
|
1184
|
+
provider: providerName,
|
|
1185
|
+
email: profile.email,
|
|
1186
|
+
...resolved.userId === void 0 ? {} : { userId: resolved.userId },
|
|
1187
|
+
reason: resolved.refusal,
|
|
1188
|
+
stage: "oauth"
|
|
1189
|
+
});
|
|
1082
1190
|
return false;
|
|
1083
1191
|
}
|
|
1084
1192
|
const newToken = randomBytes(32).toString("hex");
|