@fonderie/auth 3.0.0 → 4.0.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/brain/signatures.md +21 -0
- package/dist/index.cjs +111 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -4
- package/dist/index.d.ts +25 -4
- package/dist/index.js +109 -2
- package/dist/index.js.map +1 -1
- package/dist/middlewares/index.d.cts +1 -1
- package/dist/middlewares/index.d.ts +1 -1
- package/dist/{session-BoJjRB7t.d.cts → session-DxsmQjXX.d.cts} +1 -0
- package/dist/{session-BoJjRB7t.d.ts → session-DxsmQjXX.d.ts} +1 -0
- package/package.json +5 -5
package/brain/signatures.md
CHANGED
|
@@ -49,6 +49,7 @@ interface IMfaChallenge {
|
|
|
49
49
|
|
|
50
50
|
new AuthModule(store: IStoreAdapter, config: IAuthConfig, bus?: EventBus | undefined): AuthModule
|
|
51
51
|
.name: "@fonderie/auth"
|
|
52
|
+
.checkReadiness(): IReadinessProblem[]
|
|
52
53
|
.install(app: IFonderieApp): void
|
|
53
54
|
|
|
54
55
|
interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
|
|
@@ -61,6 +62,7 @@ interface IAuthConfig extends IAuthSecrets, IAuthRuntimeConfig {
|
|
|
61
62
|
meta: Record<string, unknown>;
|
|
62
63
|
}) => Partial<IAuthRuntimeConfig>;
|
|
63
64
|
routes?: Partial<Record<AuthRouteId, AuthRouteOverride>>;
|
|
65
|
+
legacyVerify?: (plain: string, hash: string) => boolean | Promise<boolean>;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
interface IAuthSecrets {
|
|
@@ -127,6 +129,25 @@ function normalizeEmail(email: string): string
|
|
|
127
129
|
|
|
128
130
|
function normalizeEmailSafe(email: string): string | null
|
|
129
131
|
|
|
132
|
+
function importUser(store: IStoreAdapter, user: IImportUser): Promise<{ id: string; }>
|
|
133
|
+
|
|
134
|
+
interface IImportUser {
|
|
135
|
+
id?: string;
|
|
136
|
+
email: string;
|
|
137
|
+
passwordHash?: string | null;
|
|
138
|
+
firstName?: string | null;
|
|
139
|
+
lastName?: string | null;
|
|
140
|
+
phone?: string | null;
|
|
141
|
+
profileImageUrl?: string | null;
|
|
142
|
+
locale?: string;
|
|
143
|
+
timezone?: string;
|
|
144
|
+
emailVerifiedAt?: Date | null;
|
|
145
|
+
mfaEnabled?: boolean;
|
|
146
|
+
createdAt?: Date;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function validateAuthConfig(config: IAuthConfig): void
|
|
150
|
+
|
|
130
151
|
function buildAuthIpLimiter(route: AuthLimitedRoute, store: IStoreAdapter, config: false | IAuthRateLimitConfig | undefined): Middleware | null
|
|
131
152
|
|
|
132
153
|
function buildAuthAccountLimiter(route: AuthLimitedRoute, store: IStoreAdapter, config: false | IAuthRateLimitConfig | undefined): Middleware | null
|
package/dist/index.cjs
CHANGED
|
@@ -34,7 +34,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
34
34
|
var password_exports = {};
|
|
35
35
|
__export(password_exports, {
|
|
36
36
|
hashPassword: () => hashPassword,
|
|
37
|
-
verifyPassword: () => verifyPassword
|
|
37
|
+
verifyPassword: () => verifyPassword,
|
|
38
|
+
verifyPasswordForLogin: () => verifyPasswordForLogin
|
|
38
39
|
});
|
|
39
40
|
async function hashPassword(plain) {
|
|
40
41
|
return import_bcryptjs.default.hash(plain, ROUNDS);
|
|
@@ -42,6 +43,19 @@ async function hashPassword(plain) {
|
|
|
42
43
|
async function verifyPassword(plain, hash) {
|
|
43
44
|
return import_bcryptjs.default.compare(plain, hash);
|
|
44
45
|
}
|
|
46
|
+
async function verifyPasswordForLogin(plain, hash, legacyVerify) {
|
|
47
|
+
if (await verifyPassword(plain, hash)) return { valid: true, needsRehash: false };
|
|
48
|
+
if (legacyVerify) {
|
|
49
|
+
let legacyOk = false;
|
|
50
|
+
try {
|
|
51
|
+
legacyOk = await legacyVerify(plain, hash);
|
|
52
|
+
} catch {
|
|
53
|
+
legacyOk = false;
|
|
54
|
+
}
|
|
55
|
+
if (legacyOk) return { valid: true, needsRehash: true };
|
|
56
|
+
}
|
|
57
|
+
return { valid: false, needsRehash: false };
|
|
58
|
+
}
|
|
45
59
|
var import_bcryptjs, ROUNDS;
|
|
46
60
|
var init_password = __esm({
|
|
47
61
|
"src/services/password.ts"() {
|
|
@@ -59,12 +73,14 @@ __export(index_exports, {
|
|
|
59
73
|
MESSAGE_KEYS: () => MESSAGE_KEYS,
|
|
60
74
|
buildAuthAccountLimiter: () => buildAuthAccountLimiter,
|
|
61
75
|
buildAuthIpLimiter: () => buildAuthIpLimiter,
|
|
76
|
+
importUser: () => importUser,
|
|
62
77
|
normalizeEmail: () => normalizeEmail,
|
|
63
78
|
normalizeEmailSafe: () => normalizeEmailSafe,
|
|
64
79
|
requireAuth: () => import_middlewares3.requireAuth,
|
|
65
80
|
schemas: () => schemas_exports,
|
|
66
81
|
toUserDTO: () => toUserDTO,
|
|
67
82
|
validate: () => import_middlewares.validate,
|
|
83
|
+
validateAuthConfig: () => validateAuthConfig,
|
|
68
84
|
withSession: () => withSession
|
|
69
85
|
});
|
|
70
86
|
module.exports = __toCommonJS(index_exports);
|
|
@@ -1240,10 +1256,17 @@ function authController(store, config, bus) {
|
|
|
1240
1256
|
if (!user || !user.passwordHash) {
|
|
1241
1257
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1242
1258
|
}
|
|
1243
|
-
const valid = await
|
|
1259
|
+
const { valid, needsRehash } = await verifyPasswordForLogin(
|
|
1260
|
+
password2,
|
|
1261
|
+
user.passwordHash,
|
|
1262
|
+
config.legacyVerify
|
|
1263
|
+
);
|
|
1244
1264
|
if (!valid) {
|
|
1245
1265
|
return (0, import_core4.setApiResponse)(import_core4.HTTP.UNAUTHORIZED, "INVALID_CREDENTIALS", "Invalid credentials");
|
|
1246
1266
|
}
|
|
1267
|
+
if (needsRehash) {
|
|
1268
|
+
await users.updatePassword(user.id, await hashPassword(password2));
|
|
1269
|
+
}
|
|
1247
1270
|
if (user.suspended) {
|
|
1248
1271
|
return (0, import_core4.setApiResponse)(
|
|
1249
1272
|
import_core4.HTTP.FORBIDDEN,
|
|
@@ -1998,6 +2021,53 @@ function buildAuthRoutes(store, config, bus) {
|
|
|
1998
2021
|
return routes;
|
|
1999
2022
|
}
|
|
2000
2023
|
|
|
2024
|
+
// src/services/config-guard.ts
|
|
2025
|
+
var MODULE = "@fonderie/auth";
|
|
2026
|
+
var PLACEHOLDER_SECRET = /dev-secret|test-secret|changeme|change-me|your[-_]secret|placeholder|example|insecure|min-32-chars/i;
|
|
2027
|
+
var MIN_SECRET_LENGTH = 32;
|
|
2028
|
+
function collectAuthConfigProblems(config) {
|
|
2029
|
+
const problems = [];
|
|
2030
|
+
const secret = config.jwtSecret ?? "";
|
|
2031
|
+
if (secret.length < MIN_SECRET_LENGTH) {
|
|
2032
|
+
problems.push({
|
|
2033
|
+
module: MODULE,
|
|
2034
|
+
severity: "error",
|
|
2035
|
+
message: `jwtSecret must be at least ${MIN_SECRET_LENGTH} characters (got ${secret.length})`
|
|
2036
|
+
});
|
|
2037
|
+
} else if (PLACEHOLDER_SECRET.test(secret)) {
|
|
2038
|
+
problems.push({
|
|
2039
|
+
module: MODULE,
|
|
2040
|
+
severity: "error",
|
|
2041
|
+
message: "jwtSecret looks like a placeholder or dev-default value"
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
if (config.secureCookies === false) {
|
|
2045
|
+
problems.push({
|
|
2046
|
+
module: MODULE,
|
|
2047
|
+
severity: "warning",
|
|
2048
|
+
message: "secureCookies is false \u2014 auth cookies may be sent over non-HTTPS connections in production"
|
|
2049
|
+
});
|
|
2050
|
+
}
|
|
2051
|
+
return problems;
|
|
2052
|
+
}
|
|
2053
|
+
function validateAuthConfig(config) {
|
|
2054
|
+
const isProduction = process.env["NODE_ENV"] === "production";
|
|
2055
|
+
const problems = collectAuthConfigProblems(config);
|
|
2056
|
+
const errors = problems.filter((p) => p.severity === "error");
|
|
2057
|
+
if (isProduction && errors.length > 0) {
|
|
2058
|
+
throw new Error(
|
|
2059
|
+
`[auth] insecure config \u2014 ${errors.map((e) => e.message).join("; ")}. Set a long, random jwtSecret (e.g. \`openssl rand -base64 32\`). Refusing to boot in production.`
|
|
2060
|
+
);
|
|
2061
|
+
}
|
|
2062
|
+
if (isProduction) {
|
|
2063
|
+
for (const p of problems) console.warn(`[auth] ${p.message}`);
|
|
2064
|
+
} else {
|
|
2065
|
+
for (const p of errors) {
|
|
2066
|
+
console.warn(`[auth] ${p.message} (insecure \u2014 would refuse to boot in production)`);
|
|
2067
|
+
}
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
|
|
2001
2071
|
// src/middlewares/session.ts
|
|
2002
2072
|
function withSession(store, config) {
|
|
2003
2073
|
const users = new UserModel(store);
|
|
@@ -2045,11 +2115,16 @@ var AuthModule = class {
|
|
|
2045
2115
|
this.store = store;
|
|
2046
2116
|
this.config = config;
|
|
2047
2117
|
this.bus = bus;
|
|
2118
|
+
validateAuthConfig(config);
|
|
2048
2119
|
}
|
|
2049
2120
|
store;
|
|
2050
2121
|
config;
|
|
2051
2122
|
bus;
|
|
2052
2123
|
name = "@fonderie/auth";
|
|
2124
|
+
// Report config problems for app.checkProductionReadiness() (data, not throw).
|
|
2125
|
+
checkReadiness() {
|
|
2126
|
+
return collectAuthConfigProblems(this.config);
|
|
2127
|
+
}
|
|
2053
2128
|
install(app) {
|
|
2054
2129
|
app.use(withSession(this.store, this.config));
|
|
2055
2130
|
const routes = buildAuthRoutes(this.store, this.config, this.bus);
|
|
@@ -2061,6 +2136,38 @@ var AuthModule = class {
|
|
|
2061
2136
|
|
|
2062
2137
|
// src/middlewares/require-auth.ts
|
|
2063
2138
|
var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
2139
|
+
|
|
2140
|
+
// src/migrate.ts
|
|
2141
|
+
async function importUser(store, user) {
|
|
2142
|
+
const cols = [];
|
|
2143
|
+
const vals = [];
|
|
2144
|
+
const placeholders = [];
|
|
2145
|
+
const add = (col, val) => {
|
|
2146
|
+
cols.push(col);
|
|
2147
|
+
vals.push(val);
|
|
2148
|
+
placeholders.push(`$${vals.length}`);
|
|
2149
|
+
};
|
|
2150
|
+
if (user.id !== void 0) add("id", user.id);
|
|
2151
|
+
add("email", user.email.toLowerCase().trim());
|
|
2152
|
+
if (user.passwordHash !== void 0) add("password_hash", user.passwordHash);
|
|
2153
|
+
if (user.firstName !== void 0) add("first_name", user.firstName);
|
|
2154
|
+
if (user.lastName !== void 0) add("last_name", user.lastName);
|
|
2155
|
+
if (user.phone !== void 0) add("phone", user.phone);
|
|
2156
|
+
if (user.profileImageUrl !== void 0) add("profile_image_url", user.profileImageUrl);
|
|
2157
|
+
if (user.locale !== void 0) add("locale", user.locale);
|
|
2158
|
+
if (user.timezone !== void 0) add("timezone", user.timezone);
|
|
2159
|
+
if (user.emailVerifiedAt !== void 0) add("email_verified_at", user.emailVerifiedAt);
|
|
2160
|
+
if (user.mfaEnabled !== void 0) add("mfa_enabled", user.mfaEnabled);
|
|
2161
|
+
if (user.createdAt !== void 0) add("created_at", user.createdAt);
|
|
2162
|
+
const [row] = await store.query(
|
|
2163
|
+
`INSERT INTO fonderie_users (${cols.join(", ")})
|
|
2164
|
+
VALUES (${placeholders.join(", ")})
|
|
2165
|
+
RETURNING id`,
|
|
2166
|
+
vals
|
|
2167
|
+
);
|
|
2168
|
+
if (!row) throw new Error("[auth] importUser: insert returned no row");
|
|
2169
|
+
return row;
|
|
2170
|
+
}
|
|
2064
2171
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2065
2172
|
0 && (module.exports = {
|
|
2066
2173
|
AUTH_CONFIG_KEYS,
|
|
@@ -2068,12 +2175,14 @@ var import_middlewares3 = require("@fonderie/core/middlewares");
|
|
|
2068
2175
|
MESSAGE_KEYS,
|
|
2069
2176
|
buildAuthAccountLimiter,
|
|
2070
2177
|
buildAuthIpLimiter,
|
|
2178
|
+
importUser,
|
|
2071
2179
|
normalizeEmail,
|
|
2072
2180
|
normalizeEmailSafe,
|
|
2073
2181
|
requireAuth,
|
|
2074
2182
|
schemas,
|
|
2075
2183
|
toUserDTO,
|
|
2076
2184
|
validate,
|
|
2185
|
+
validateAuthConfig,
|
|
2077
2186
|
withSession
|
|
2078
2187
|
});
|
|
2079
2188
|
//# sourceMappingURL=index.cjs.map
|