@spfn/auth 0.2.0-beta.68 → 0.2.0-beta.70
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/README.md +1 -1
- package/dist/{authenticate-Cn5krz5U.d.ts → authenticate-tmmbh1ux.d.ts} +3 -23
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +3 -10
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/server.d.ts +94 -77
- package/dist/server.js +159 -114
- package/dist/server.js.map +1 -1
- package/package.json +9 -10
package/dist/server.js
CHANGED
|
@@ -4471,7 +4471,8 @@ var init_schema3 = __esm({
|
|
|
4471
4471
|
});
|
|
4472
4472
|
PasswordSchema = Type.String({
|
|
4473
4473
|
minLength: 8,
|
|
4474
|
-
|
|
4474
|
+
maxLength: 72,
|
|
4475
|
+
description: "User password (8\u201372 characters). bcrypt silently ignores bytes past 72, so longer inputs are rejected rather than truncated."
|
|
4475
4476
|
});
|
|
4476
4477
|
TargetTypeSchema = Type.Union([
|
|
4477
4478
|
Type.Literal("email"),
|
|
@@ -5590,12 +5591,13 @@ var init_users_repository = __esm({
|
|
|
5590
5591
|
|
|
5591
5592
|
// src/server/repositories/keys.repository.ts
|
|
5592
5593
|
import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
|
|
5593
|
-
import { eq as eq2, and as and2 } from "drizzle-orm";
|
|
5594
|
-
var KeysRepository, keysRepository;
|
|
5594
|
+
import { eq as eq2, and as and2, or, isNull, lt } from "drizzle-orm";
|
|
5595
|
+
var LAST_USED_THROTTLE_MS, KeysRepository, keysRepository;
|
|
5595
5596
|
var init_keys_repository = __esm({
|
|
5596
5597
|
"src/server/repositories/keys.repository.ts"() {
|
|
5597
5598
|
"use strict";
|
|
5598
5599
|
init_user_public_keys();
|
|
5600
|
+
LAST_USED_THROTTLE_MS = 6e4;
|
|
5599
5601
|
KeysRepository = class extends BaseRepository2 {
|
|
5600
5602
|
/**
|
|
5601
5603
|
* Key ID와 User ID로 공개키 조회
|
|
@@ -5699,13 +5701,24 @@ var init_keys_repository = __esm({
|
|
|
5699
5701
|
}
|
|
5700
5702
|
/**
|
|
5701
5703
|
* Primary key로 마지막 사용 시간 업데이트 (authenticate용)
|
|
5702
|
-
* Write primary
|
|
5704
|
+
* Write primary 사용.
|
|
5705
|
+
*
|
|
5706
|
+
* Throttled: only writes when lastUsedAt is stale (older than
|
|
5707
|
+
* LAST_USED_THROTTLE_MS), so a busy key isn't UPDATEd on every request. The
|
|
5708
|
+
* throttle lives in the WHERE clause — atomic, no read-then-write race. No
|
|
5709
|
+
* RETURNING (callers fire-and-forget and discard the row).
|
|
5703
5710
|
*/
|
|
5704
5711
|
async updateLastUsedById(id11) {
|
|
5705
|
-
const
|
|
5712
|
+
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
5713
|
+
await this.db.update(userPublicKeys).set({
|
|
5706
5714
|
lastUsedAt: /* @__PURE__ */ new Date()
|
|
5707
|
-
}).where(
|
|
5708
|
-
|
|
5715
|
+
}).where(and2(
|
|
5716
|
+
eq2(userPublicKeys.id, id11),
|
|
5717
|
+
or(
|
|
5718
|
+
isNull(userPublicKeys.lastUsedAt),
|
|
5719
|
+
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
5720
|
+
)
|
|
5721
|
+
));
|
|
5709
5722
|
}
|
|
5710
5723
|
};
|
|
5711
5724
|
keysRepository = new KeysRepository();
|
|
@@ -5714,7 +5727,7 @@ var init_keys_repository = __esm({
|
|
|
5714
5727
|
|
|
5715
5728
|
// src/server/repositories/verification-codes.repository.ts
|
|
5716
5729
|
import { BaseRepository as BaseRepository3 } from "@spfn/core/db";
|
|
5717
|
-
import { eq as eq3, and as and3, lt, gt, isNull } from "drizzle-orm";
|
|
5730
|
+
import { eq as eq3, and as and3, lt as lt2, gt, isNull as isNull2 } from "drizzle-orm";
|
|
5718
5731
|
var VerificationCodesRepository, verificationCodesRepository;
|
|
5719
5732
|
var init_verification_codes_repository = __esm({
|
|
5720
5733
|
"src/server/repositories/verification-codes.repository.ts"() {
|
|
@@ -5732,7 +5745,7 @@ var init_verification_codes_repository = __esm({
|
|
|
5732
5745
|
and3(
|
|
5733
5746
|
eq3(verificationCodes.target, target),
|
|
5734
5747
|
eq3(verificationCodes.purpose, purpose),
|
|
5735
|
-
|
|
5748
|
+
isNull2(verificationCodes.usedAt),
|
|
5736
5749
|
gt(verificationCodes.expiresAt, now)
|
|
5737
5750
|
)
|
|
5738
5751
|
).limit(1);
|
|
@@ -5787,7 +5800,7 @@ var init_verification_codes_repository = __esm({
|
|
|
5787
5800
|
*/
|
|
5788
5801
|
async deleteExpired() {
|
|
5789
5802
|
const now = /* @__PURE__ */ new Date();
|
|
5790
|
-
const result = await this.db.delete(verificationCodes).where(
|
|
5803
|
+
const result = await this.db.delete(verificationCodes).where(lt2(verificationCodes.expiresAt, now)).returning();
|
|
5791
5804
|
return result.length;
|
|
5792
5805
|
}
|
|
5793
5806
|
/**
|
|
@@ -5802,7 +5815,7 @@ var init_verification_codes_repository = __esm({
|
|
|
5802
5815
|
and3(
|
|
5803
5816
|
eq3(verificationCodes.target, target),
|
|
5804
5817
|
eq3(verificationCodes.purpose, purpose),
|
|
5805
|
-
|
|
5818
|
+
isNull2(verificationCodes.usedAt)
|
|
5806
5819
|
)
|
|
5807
5820
|
).returning();
|
|
5808
5821
|
return result.length;
|
|
@@ -6048,7 +6061,7 @@ var init_role_permissions_repository = __esm({
|
|
|
6048
6061
|
|
|
6049
6062
|
// src/server/repositories/user-permissions.repository.ts
|
|
6050
6063
|
import { BaseRepository as BaseRepository7 } from "@spfn/core/db";
|
|
6051
|
-
import { eq as eq7, and as and5, or, isNull as
|
|
6064
|
+
import { eq as eq7, and as and5, or as or2, isNull as isNull3, isNotNull, lt as lt3, gt as gt2 } from "drizzle-orm";
|
|
6052
6065
|
var UserPermissionsRepository, userPermissionsRepository;
|
|
6053
6066
|
var init_user_permissions_repository = __esm({
|
|
6054
6067
|
"src/server/repositories/user-permissions.repository.ts"() {
|
|
@@ -6070,8 +6083,8 @@ var init_user_permissions_repository = __esm({
|
|
|
6070
6083
|
return this.readDb.select().from(userPermissions).where(
|
|
6071
6084
|
and5(
|
|
6072
6085
|
eq7(userPermissions.userId, userId),
|
|
6073
|
-
|
|
6074
|
-
|
|
6086
|
+
or2(
|
|
6087
|
+
isNull3(userPermissions.expiresAt),
|
|
6075
6088
|
gt2(userPermissions.expiresAt, now)
|
|
6076
6089
|
)
|
|
6077
6090
|
)
|
|
@@ -6133,7 +6146,7 @@ var init_user_permissions_repository = __esm({
|
|
|
6133
6146
|
const result = await this.db.delete(userPermissions).where(
|
|
6134
6147
|
and5(
|
|
6135
6148
|
isNotNull(userPermissions.expiresAt),
|
|
6136
|
-
|
|
6149
|
+
lt3(userPermissions.expiresAt, now)
|
|
6137
6150
|
)
|
|
6138
6151
|
).returning();
|
|
6139
6152
|
return result.length;
|
|
@@ -6278,7 +6291,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6278
6291
|
});
|
|
6279
6292
|
|
|
6280
6293
|
// src/server/repositories/invitations.repository.ts
|
|
6281
|
-
import { eq as eq9, and as and6, lt as
|
|
6294
|
+
import { eq as eq9, and as and6, lt as lt4, desc, sql as sql3 } from "drizzle-orm";
|
|
6282
6295
|
import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
|
|
6283
6296
|
var InvitationsRepository, invitationsRepository;
|
|
6284
6297
|
var init_invitations_repository = __esm({
|
|
@@ -6372,7 +6385,7 @@ var init_invitations_repository = __esm({
|
|
|
6372
6385
|
}).where(
|
|
6373
6386
|
and6(
|
|
6374
6387
|
eq9(userInvitations.status, "pending"),
|
|
6375
|
-
|
|
6388
|
+
lt4(userInvitations.expiresAt, now)
|
|
6376
6389
|
)
|
|
6377
6390
|
).returning();
|
|
6378
6391
|
return result.length;
|
|
@@ -6847,7 +6860,7 @@ import { defineRouter as defineRouter5 } from "@spfn/core/route";
|
|
|
6847
6860
|
init_schema3();
|
|
6848
6861
|
|
|
6849
6862
|
// src/server/helpers/password.ts
|
|
6850
|
-
import bcrypt from "
|
|
6863
|
+
import * as bcrypt from "@node-rs/bcrypt";
|
|
6851
6864
|
import { env } from "@spfn/auth/config";
|
|
6852
6865
|
import { createPasswordParser } from "@spfn/core/env";
|
|
6853
6866
|
async function hashPassword(password) {
|
|
@@ -6856,14 +6869,14 @@ async function hashPassword(password) {
|
|
|
6856
6869
|
}
|
|
6857
6870
|
return bcrypt.hash(password, env.SPFN_AUTH_BCRYPT_SALT_ROUNDS);
|
|
6858
6871
|
}
|
|
6859
|
-
async function verifyPassword(password,
|
|
6872
|
+
async function verifyPassword(password, hash2) {
|
|
6860
6873
|
if (!password || password.length === 0) {
|
|
6861
6874
|
throw new Error("Password cannot be empty");
|
|
6862
6875
|
}
|
|
6863
|
-
if (!
|
|
6876
|
+
if (!hash2 || hash2.length === 0) {
|
|
6864
6877
|
throw new Error("Hash cannot be empty");
|
|
6865
6878
|
}
|
|
6866
|
-
return bcrypt.
|
|
6879
|
+
return bcrypt.verify(password, hash2);
|
|
6867
6880
|
}
|
|
6868
6881
|
var passwordValidator = createPasswordParser({
|
|
6869
6882
|
minLength: 8,
|
|
@@ -6902,21 +6915,37 @@ function validatePasswordStrength(password) {
|
|
|
6902
6915
|
import jwt from "jsonwebtoken";
|
|
6903
6916
|
import crypto2 from "crypto";
|
|
6904
6917
|
import { env as env2 } from "@spfn/auth/config";
|
|
6918
|
+
var PUBLIC_KEY_CACHE_MAX = 1e3;
|
|
6919
|
+
var publicKeyCache = /* @__PURE__ */ new Map();
|
|
6920
|
+
function getPublicKeyObject(publicKeyB64) {
|
|
6921
|
+
const cached = publicKeyCache.get(publicKeyB64);
|
|
6922
|
+
if (cached) {
|
|
6923
|
+
return cached;
|
|
6924
|
+
}
|
|
6925
|
+
const keyObject = crypto2.createPublicKey({
|
|
6926
|
+
key: Buffer.from(publicKeyB64, "base64"),
|
|
6927
|
+
format: "der",
|
|
6928
|
+
type: "spki"
|
|
6929
|
+
});
|
|
6930
|
+
if (publicKeyCache.size >= PUBLIC_KEY_CACHE_MAX) {
|
|
6931
|
+
const oldest = publicKeyCache.keys().next().value;
|
|
6932
|
+
if (oldest !== void 0) {
|
|
6933
|
+
publicKeyCache.delete(oldest);
|
|
6934
|
+
}
|
|
6935
|
+
}
|
|
6936
|
+
publicKeyCache.set(publicKeyB64, keyObject);
|
|
6937
|
+
return keyObject;
|
|
6938
|
+
}
|
|
6905
6939
|
function generateToken(payload) {
|
|
6906
6940
|
return jwt.sign(payload, env2.SPFN_AUTH_JWT_SECRET, {
|
|
6907
6941
|
expiresIn: env2.SPFN_AUTH_JWT_EXPIRES_IN
|
|
6908
6942
|
});
|
|
6909
6943
|
}
|
|
6910
6944
|
function verifyToken(token) {
|
|
6911
|
-
return jwt.verify(token, env2.SPFN_AUTH_JWT_SECRET);
|
|
6945
|
+
return jwt.verify(token, env2.SPFN_AUTH_JWT_SECRET, { algorithms: ["HS256"] });
|
|
6912
6946
|
}
|
|
6913
6947
|
function verifyClientToken(token, publicKeyB64, algorithm) {
|
|
6914
|
-
const
|
|
6915
|
-
const publicKeyObject = crypto2.createPublicKey({
|
|
6916
|
-
key: publicKeyDER,
|
|
6917
|
-
format: "der",
|
|
6918
|
-
type: "spki"
|
|
6919
|
-
});
|
|
6948
|
+
const publicKeyObject = getPublicKeyObject(publicKeyB64);
|
|
6920
6949
|
let decoded;
|
|
6921
6950
|
try {
|
|
6922
6951
|
decoded = jwt.verify(token, publicKeyObject, {
|
|
@@ -7002,6 +7031,7 @@ import {
|
|
|
7002
7031
|
} from "@spfn/auth/errors";
|
|
7003
7032
|
|
|
7004
7033
|
// src/server/services/verification.service.ts
|
|
7034
|
+
import crypto4 from "crypto";
|
|
7005
7035
|
import { env as env4 } from "@spfn/auth/config";
|
|
7006
7036
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
7007
7037
|
import jwt2 from "jsonwebtoken";
|
|
@@ -7027,11 +7057,12 @@ var authLogger = {
|
|
|
7027
7057
|
|
|
7028
7058
|
// src/server/services/verification.service.ts
|
|
7029
7059
|
init_repositories();
|
|
7060
|
+
var ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES = 60;
|
|
7030
7061
|
var VERIFICATION_TOKEN_EXPIRY = "15m";
|
|
7031
7062
|
var VERIFICATION_CODE_EXPIRY_MINUTES = 5;
|
|
7032
7063
|
var MAX_VERIFICATION_ATTEMPTS = 5;
|
|
7033
7064
|
function generateVerificationCode() {
|
|
7034
|
-
return
|
|
7065
|
+
return crypto4.randomInt(0, 1e6).toString().padStart(6, "0");
|
|
7035
7066
|
}
|
|
7036
7067
|
async function storeVerificationCode(target, targetType, code, purpose) {
|
|
7037
7068
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
@@ -7126,8 +7157,39 @@ async function sendVerificationSMS(phone, code, purpose) {
|
|
|
7126
7157
|
});
|
|
7127
7158
|
}
|
|
7128
7159
|
}
|
|
7160
|
+
async function accountExistsForTarget(target, targetType) {
|
|
7161
|
+
const user = targetType === "email" ? await usersRepository.findByEmail(target) : await usersRepository.findByPhone(target);
|
|
7162
|
+
return !!user;
|
|
7163
|
+
}
|
|
7164
|
+
async function sendAccountExistsNotice(target, targetType) {
|
|
7165
|
+
const result = targetType === "email" ? await sendEmail({ to: target, template: "account-exists", data: {} }) : await sendSMS({ to: target, template: "account-exists", data: {} });
|
|
7166
|
+
if (!result.success) {
|
|
7167
|
+
const log = targetType === "email" ? authLogger.email : authLogger.sms;
|
|
7168
|
+
log.error("Failed to send account-exists notice", { target, error: result.error });
|
|
7169
|
+
}
|
|
7170
|
+
}
|
|
7129
7171
|
async function sendVerificationCodeService(params) {
|
|
7130
7172
|
const { target, targetType, purpose } = params;
|
|
7173
|
+
if (purpose === "registration" && await accountExistsForTarget(target, targetType)) {
|
|
7174
|
+
const recentNotice = await verificationCodesRepository.findValidByTargetAndPurpose(target, purpose);
|
|
7175
|
+
if (!recentNotice) {
|
|
7176
|
+
const dedupeExpiresAt = new Date(Date.now() + ACCOUNT_EXISTS_NOTICE_DEDUPE_MINUTES * 6e4);
|
|
7177
|
+
await verificationCodesRepository.invalidatePreviousCodes(target, purpose);
|
|
7178
|
+
await verificationCodesRepository.create({
|
|
7179
|
+
target,
|
|
7180
|
+
targetType,
|
|
7181
|
+
code: generateVerificationCode(),
|
|
7182
|
+
purpose,
|
|
7183
|
+
expiresAt: dedupeExpiresAt,
|
|
7184
|
+
attempts: 0
|
|
7185
|
+
});
|
|
7186
|
+
await sendAccountExistsNotice(target, targetType);
|
|
7187
|
+
}
|
|
7188
|
+
return {
|
|
7189
|
+
success: true,
|
|
7190
|
+
expiresAt: new Date(Date.now() + VERIFICATION_CODE_EXPIRY_MINUTES * 6e4).toISOString()
|
|
7191
|
+
};
|
|
7192
|
+
}
|
|
7131
7193
|
const code = generateVerificationCode();
|
|
7132
7194
|
const codeRecord = await storeVerificationCode(target, targetType, code, purpose);
|
|
7133
7195
|
if (targetType === "email") {
|
|
@@ -7344,27 +7406,12 @@ var invitationAcceptedEvent = defineEvent(
|
|
|
7344
7406
|
);
|
|
7345
7407
|
|
|
7346
7408
|
// src/server/services/auth.service.ts
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
let user;
|
|
7352
|
-
if (email) {
|
|
7353
|
-
identifier = email;
|
|
7354
|
-
identifierType = "email";
|
|
7355
|
-
user = await usersRepository.findByEmail(email);
|
|
7356
|
-
} else if (phone) {
|
|
7357
|
-
identifier = phone;
|
|
7358
|
-
identifierType = "phone";
|
|
7359
|
-
user = await usersRepository.findByPhone(phone);
|
|
7360
|
-
} else {
|
|
7361
|
-
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7409
|
+
var dummyHashPromise = null;
|
|
7410
|
+
function getDummyHash() {
|
|
7411
|
+
if (!dummyHashPromise) {
|
|
7412
|
+
dummyHashPromise = hashPassword("spfn-nonexistent-account-timing-equalizer");
|
|
7362
7413
|
}
|
|
7363
|
-
return
|
|
7364
|
-
exists: !!user,
|
|
7365
|
-
identifier,
|
|
7366
|
-
identifierType
|
|
7367
|
-
};
|
|
7414
|
+
return dummyHashPromise;
|
|
7368
7415
|
}
|
|
7369
7416
|
async function registerService(params) {
|
|
7370
7417
|
const { email, phone, verificationToken, password, publicKey, keyId, fingerprint, algorithm, metadata } = params;
|
|
@@ -7431,6 +7478,7 @@ async function loginService(params) {
|
|
|
7431
7478
|
throw new ValidationError2({ message: "Either email or phone must be provided" });
|
|
7432
7479
|
}
|
|
7433
7480
|
if (!user || !user.passwordHash) {
|
|
7481
|
+
await verifyPassword(password, await getDummyHash());
|
|
7434
7482
|
throw new InvalidCredentialsError();
|
|
7435
7483
|
}
|
|
7436
7484
|
const isValid = await verifyPassword(password, user.passwordHash);
|
|
@@ -7717,6 +7765,7 @@ async function syncMappings(allMappings, rolesByName, permsByName) {
|
|
|
7717
7765
|
|
|
7718
7766
|
// src/server/services/permission.service.ts
|
|
7719
7767
|
init_repositories();
|
|
7768
|
+
import { ForbiddenError } from "@spfn/core/errors";
|
|
7720
7769
|
async function getUserPermissions(userId) {
|
|
7721
7770
|
const userIdNum = typeof userId === "string" ? Number(userId) : Number(userId);
|
|
7722
7771
|
const user = await usersRepository.findById(userIdNum);
|
|
@@ -7781,15 +7830,32 @@ async function hasAnyRole(userId, roleNames) {
|
|
|
7781
7830
|
}
|
|
7782
7831
|
return false;
|
|
7783
7832
|
}
|
|
7833
|
+
async function assertCanAssignRole(callerUserId, targetRoleId) {
|
|
7834
|
+
const callerRole = await getUserRole(callerUserId);
|
|
7835
|
+
if (callerRole === "superadmin") {
|
|
7836
|
+
return;
|
|
7837
|
+
}
|
|
7838
|
+
const targetRole = await rolesRepository.findById(targetRoleId);
|
|
7839
|
+
if (targetRole?.name === "superadmin") {
|
|
7840
|
+
throw new ForbiddenError({ message: "Only superadmin can assign superadmin role" });
|
|
7841
|
+
}
|
|
7842
|
+
if (targetRole?.name === "admin") {
|
|
7843
|
+
const canPromote = await hasPermission(callerUserId, "admin:promote");
|
|
7844
|
+
if (!canPromote) {
|
|
7845
|
+
throw new ForbiddenError({ message: "admin:promote permission required to assign admin role" });
|
|
7846
|
+
}
|
|
7847
|
+
}
|
|
7848
|
+
}
|
|
7784
7849
|
|
|
7785
7850
|
// src/server/services/index.ts
|
|
7786
7851
|
init_role_service();
|
|
7787
7852
|
|
|
7788
7853
|
// src/server/services/invitation.service.ts
|
|
7789
7854
|
init_repositories();
|
|
7790
|
-
import
|
|
7855
|
+
import crypto5 from "crypto";
|
|
7856
|
+
import { BadRequestError, NotFoundError as NotFoundError2, ConflictError } from "@spfn/core/errors";
|
|
7791
7857
|
function generateInvitationToken() {
|
|
7792
|
-
return
|
|
7858
|
+
return crypto5.randomUUID();
|
|
7793
7859
|
}
|
|
7794
7860
|
function calculateExpiresAt(days = 7) {
|
|
7795
7861
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
@@ -7800,23 +7866,23 @@ async function createInvitation(params) {
|
|
|
7800
7866
|
const { email, roleId, invitedBy, expiresInDays = 7, expiresAt: expiresAtParam, metadata } = params;
|
|
7801
7867
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
7802
7868
|
if (!emailRegex.test(email)) {
|
|
7803
|
-
throw new
|
|
7869
|
+
throw new BadRequestError({ message: "Invalid email format" });
|
|
7804
7870
|
}
|
|
7805
7871
|
const existingUser = await usersRepository.findByEmail(email);
|
|
7806
7872
|
if (existingUser) {
|
|
7807
|
-
throw new
|
|
7873
|
+
throw new ConflictError({ message: "User with this email already exists" });
|
|
7808
7874
|
}
|
|
7809
7875
|
const existingInvitation = await invitationsRepository.findPendingByEmail(email);
|
|
7810
7876
|
if (existingInvitation) {
|
|
7811
|
-
throw new
|
|
7877
|
+
throw new ConflictError({ message: "Pending invitation already exists for this email" });
|
|
7812
7878
|
}
|
|
7813
7879
|
const role = await rolesRepository.findById(roleId);
|
|
7814
7880
|
if (!role) {
|
|
7815
|
-
throw new
|
|
7881
|
+
throw new NotFoundError2({ message: `Role with id ${roleId} not found`, resource: "Role" });
|
|
7816
7882
|
}
|
|
7817
7883
|
const inviter = await usersRepository.findById(invitedBy);
|
|
7818
7884
|
if (!inviter) {
|
|
7819
|
-
throw new
|
|
7885
|
+
throw new NotFoundError2({ message: `User with id ${invitedBy} not found`, resource: "User" });
|
|
7820
7886
|
}
|
|
7821
7887
|
const token = generateInvitationToken();
|
|
7822
7888
|
const expiresAt = expiresAtParam ?? calculateExpiresAt(expiresInDays);
|
|
@@ -7870,12 +7936,12 @@ async function acceptInvitation(params) {
|
|
|
7870
7936
|
const { token, password, publicKey, keyId, fingerprint, algorithm } = params;
|
|
7871
7937
|
const validation = await validateInvitation(token);
|
|
7872
7938
|
if (!validation.valid || !validation.invitation) {
|
|
7873
|
-
throw new
|
|
7939
|
+
throw new BadRequestError({ message: validation.error || "Invalid invitation" });
|
|
7874
7940
|
}
|
|
7875
7941
|
const invitation = validation.invitation;
|
|
7876
7942
|
const role = await rolesRepository.findById(invitation.roleId);
|
|
7877
7943
|
if (!role) {
|
|
7878
|
-
throw new
|
|
7944
|
+
throw new NotFoundError2({ message: "Role not found", resource: "Role" });
|
|
7879
7945
|
}
|
|
7880
7946
|
const passwordHash = await hashPassword(password);
|
|
7881
7947
|
const newUser = await usersRepository.create({
|
|
@@ -7922,10 +7988,10 @@ async function listInvitations(params) {
|
|
|
7922
7988
|
async function cancelInvitation(id11, cancelledBy, reason) {
|
|
7923
7989
|
const invitation = await invitationsRepository.findById(id11);
|
|
7924
7990
|
if (!invitation) {
|
|
7925
|
-
throw new
|
|
7991
|
+
throw new NotFoundError2({ message: "Invitation not found", resource: "Invitation" });
|
|
7926
7992
|
}
|
|
7927
7993
|
if (invitation.status !== "pending") {
|
|
7928
|
-
throw new
|
|
7994
|
+
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
7929
7995
|
}
|
|
7930
7996
|
await invitationsRepository.cancel(id11, cancelledBy, reason, invitation.metadata);
|
|
7931
7997
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
@@ -7944,10 +8010,10 @@ async function expireOldInvitations() {
|
|
|
7944
8010
|
async function resendInvitation(id11, expiresInDays = 7) {
|
|
7945
8011
|
const invitation = await invitationsRepository.findById(id11);
|
|
7946
8012
|
if (!invitation) {
|
|
7947
|
-
throw new
|
|
8013
|
+
throw new NotFoundError2({ message: "Invitation not found", resource: "Invitation" });
|
|
7948
8014
|
}
|
|
7949
8015
|
if (!["pending", "expired"].includes(invitation.status)) {
|
|
7950
|
-
throw new
|
|
8016
|
+
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
7951
8017
|
}
|
|
7952
8018
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
7953
8019
|
const updated = await invitationsRepository.resend(id11, newExpiresAt);
|
|
@@ -8668,16 +8734,8 @@ async function persistNativeLogin(identity, params) {
|
|
|
8668
8734
|
// src/server/routes/auth/index.ts
|
|
8669
8735
|
init_esm();
|
|
8670
8736
|
import { Transactional } from "@spfn/core/db";
|
|
8737
|
+
import { rateLimit } from "@spfn/core/middleware";
|
|
8671
8738
|
import { defineRouter, route } from "@spfn/core/route";
|
|
8672
|
-
var checkAccountExists = route.post("/_auth/exists").input({
|
|
8673
|
-
body: Type.Union([
|
|
8674
|
-
Type.Object({ email: EmailSchema }),
|
|
8675
|
-
Type.Object({ phone: PhoneSchema })
|
|
8676
|
-
])
|
|
8677
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
8678
|
-
const { body } = await c.data();
|
|
8679
|
-
return await checkAccountExistsService(body);
|
|
8680
|
-
});
|
|
8681
8739
|
var sendVerificationCode = route.post("/_auth/codes").input({
|
|
8682
8740
|
body: Type.Object({
|
|
8683
8741
|
target: Type.String({
|
|
@@ -8686,7 +8744,7 @@ var sendVerificationCode = route.post("/_auth/codes").input({
|
|
|
8686
8744
|
targetType: TargetTypeSchema,
|
|
8687
8745
|
purpose: VerificationPurposeSchema
|
|
8688
8746
|
})
|
|
8689
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
8747
|
+
}).use([rateLimit({ limit: 5, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
8690
8748
|
const { body } = await c.data();
|
|
8691
8749
|
return await sendVerificationCodeService(body);
|
|
8692
8750
|
});
|
|
@@ -8704,7 +8762,7 @@ var verifyCode = route.post("/_auth/codes/verify").input({
|
|
|
8704
8762
|
}),
|
|
8705
8763
|
purpose: VerificationPurposeSchema
|
|
8706
8764
|
})
|
|
8707
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
8765
|
+
}).use([rateLimit({ limit: 10, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
8708
8766
|
const { body } = await c.data();
|
|
8709
8767
|
return await verifyCodeService(body);
|
|
8710
8768
|
});
|
|
@@ -8756,7 +8814,7 @@ var login = route.post("/_auth/login").input({
|
|
|
8756
8814
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
8757
8815
|
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" }))
|
|
8758
8816
|
})
|
|
8759
|
-
}).use([Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
8817
|
+
}).use([rateLimit({ limit: 10, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
8760
8818
|
const { body } = await c.data();
|
|
8761
8819
|
return await loginService(body);
|
|
8762
8820
|
});
|
|
@@ -8816,7 +8874,6 @@ var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
|
|
|
8816
8874
|
return await issueOneTimeTokenService(userId);
|
|
8817
8875
|
});
|
|
8818
8876
|
var authRouter = defineRouter({
|
|
8819
|
-
checkAccountExists,
|
|
8820
8877
|
sendVerificationCode,
|
|
8821
8878
|
verifyCode,
|
|
8822
8879
|
register,
|
|
@@ -8965,7 +9022,7 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
8965
9022
|
|
|
8966
9023
|
// src/server/middleware/require-permission.ts
|
|
8967
9024
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
8968
|
-
import { ForbiddenError } from "@spfn/core/errors";
|
|
9025
|
+
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
8969
9026
|
import { InsufficientPermissionsError } from "@spfn/auth/errors";
|
|
8970
9027
|
import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as authLogger3 } from "@spfn/auth/server";
|
|
8971
9028
|
var requirePermissions = defineMiddleware2(
|
|
@@ -8977,7 +9034,7 @@ var requirePermissions = defineMiddleware2(
|
|
|
8977
9034
|
permissions: permissionNames,
|
|
8978
9035
|
path: c.req.path
|
|
8979
9036
|
});
|
|
8980
|
-
throw new
|
|
9037
|
+
throw new ForbiddenError2({ message: "Authentication required" });
|
|
8981
9038
|
}
|
|
8982
9039
|
const { userId } = auth;
|
|
8983
9040
|
const allowed = await hasAllPermissions2(userId, permissionNames);
|
|
@@ -9005,7 +9062,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
9005
9062
|
permissions: permissionNames,
|
|
9006
9063
|
path: c.req.path
|
|
9007
9064
|
});
|
|
9008
|
-
throw new
|
|
9065
|
+
throw new ForbiddenError2({ message: "Authentication required" });
|
|
9009
9066
|
}
|
|
9010
9067
|
const { userId } = auth;
|
|
9011
9068
|
const allowed = await hasAnyPermission2(userId, permissionNames);
|
|
@@ -9028,7 +9085,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
9028
9085
|
// src/server/middleware/require-role.ts
|
|
9029
9086
|
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
9030
9087
|
import { getAuth as getAuth3, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
9031
|
-
import { ForbiddenError as
|
|
9088
|
+
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
9032
9089
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
9033
9090
|
var requireRole = defineMiddleware3(
|
|
9034
9091
|
"role",
|
|
@@ -9039,7 +9096,7 @@ var requireRole = defineMiddleware3(
|
|
|
9039
9096
|
roles: roleNames,
|
|
9040
9097
|
path: c.req.path
|
|
9041
9098
|
});
|
|
9042
|
-
throw new
|
|
9099
|
+
throw new ForbiddenError3({ message: "Authentication required" });
|
|
9043
9100
|
}
|
|
9044
9101
|
const { userId, role: userRole } = auth;
|
|
9045
9102
|
if (!userRole || !roleNames.includes(userRole)) {
|
|
@@ -9063,7 +9120,7 @@ var requireRole = defineMiddleware3(
|
|
|
9063
9120
|
// src/server/middleware/role-guard.ts
|
|
9064
9121
|
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
9065
9122
|
import { getAuth as getAuth4, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
9066
|
-
import { ForbiddenError as
|
|
9123
|
+
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
9067
9124
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
9068
9125
|
var roleGuard = defineMiddleware4(
|
|
9069
9126
|
"roleGuard",
|
|
@@ -9077,7 +9134,7 @@ var roleGuard = defineMiddleware4(
|
|
|
9077
9134
|
authLogger5.middleware.warn("Role guard failed: not authenticated", {
|
|
9078
9135
|
path: c.req.path
|
|
9079
9136
|
});
|
|
9080
|
-
throw new
|
|
9137
|
+
throw new ForbiddenError4({ message: "Authentication required" });
|
|
9081
9138
|
}
|
|
9082
9139
|
const { userId, role: userRole } = auth;
|
|
9083
9140
|
if (deny && deny.length > 0) {
|
|
@@ -9240,6 +9297,7 @@ var createInvitation2 = route2.post("/_auth/invitations").input({
|
|
|
9240
9297
|
}).use([authenticate, requirePermissions("user:invite")]).handler(async (c) => {
|
|
9241
9298
|
const { body } = await c.data();
|
|
9242
9299
|
const { userId } = getAuth(c);
|
|
9300
|
+
await assertCanAssignRole(userId, body.roleId);
|
|
9243
9301
|
const invitation = await createInvitation({
|
|
9244
9302
|
email: body.email,
|
|
9245
9303
|
roleId: body.roleId,
|
|
@@ -9422,6 +9480,7 @@ init_esm();
|
|
|
9422
9480
|
init_types();
|
|
9423
9481
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
9424
9482
|
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
9483
|
+
import { rateLimit as rateLimit2 } from "@spfn/core/middleware";
|
|
9425
9484
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
9426
9485
|
var providerParams = Type.Object({
|
|
9427
9486
|
provider: Type.Union(SOCIAL_PROVIDERS.map((p) => Type.Literal(p)), {
|
|
@@ -9434,7 +9493,7 @@ var oauthGoogleStart = route4.get("/_auth/oauth/google").input({
|
|
|
9434
9493
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9435
9494
|
})
|
|
9436
9495
|
})
|
|
9437
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9496
|
+
}).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9438
9497
|
const { query } = await c.data();
|
|
9439
9498
|
if (!isGoogleOAuthEnabled()) {
|
|
9440
9499
|
return c.redirect(buildOAuthErrorUrl("Google OAuth is not configured"));
|
|
@@ -9502,7 +9561,7 @@ var oauthStart = route4.post("/_auth/oauth/start").input({
|
|
|
9502
9561
|
description: "Custom metadata passed to authRegisterEvent (e.g. referral code, UTM params)"
|
|
9503
9562
|
}))
|
|
9504
9563
|
})
|
|
9505
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9564
|
+
}).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9506
9565
|
const { body } = await c.data();
|
|
9507
9566
|
const result = await oauthStartService(body);
|
|
9508
9567
|
return result;
|
|
@@ -9555,7 +9614,7 @@ var oauthProviderStart = route4.get("/_auth/oauth/:provider").input({
|
|
|
9555
9614
|
description: "Encrypted OAuth state (returnUrl, publicKey, keyId, fingerprint, algorithm)"
|
|
9556
9615
|
})
|
|
9557
9616
|
})
|
|
9558
|
-
}).skip(["auth"]).handler(async (c) => {
|
|
9617
|
+
}).use([rateLimit2({ limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
9559
9618
|
const { params, query } = await c.data();
|
|
9560
9619
|
const provider = getOAuthProvider(params.provider);
|
|
9561
9620
|
if (!provider?.isEnabled()) {
|
|
@@ -9658,9 +9717,8 @@ var oauthRouter = defineRouter4({
|
|
|
9658
9717
|
});
|
|
9659
9718
|
|
|
9660
9719
|
// src/server/routes/admin/index.ts
|
|
9661
|
-
init_repositories();
|
|
9662
9720
|
init_esm();
|
|
9663
|
-
import { ForbiddenError as
|
|
9721
|
+
import { ForbiddenError as ForbiddenError5 } from "@spfn/core/errors";
|
|
9664
9722
|
import { route as route5 } from "@spfn/core/route";
|
|
9665
9723
|
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
9666
9724
|
query: Type.Object({
|
|
@@ -9729,26 +9787,14 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
|
9729
9787
|
}).use([authenticate, requireRole("admin", "superadmin")]).handler(async (c) => {
|
|
9730
9788
|
const { params, body } = await c.data();
|
|
9731
9789
|
const auth = getAuth(c);
|
|
9732
|
-
const callerRole = await getUserRole(auth.userId);
|
|
9733
9790
|
if (params.userId === Number(auth.userId)) {
|
|
9734
|
-
throw new
|
|
9791
|
+
throw new ForbiddenError5({ message: "Cannot change your own role" });
|
|
9735
9792
|
}
|
|
9736
9793
|
const targetRole = await getUserRole(params.userId);
|
|
9737
9794
|
if (targetRole === "superadmin") {
|
|
9738
|
-
throw new
|
|
9739
|
-
}
|
|
9740
|
-
if (callerRole !== "superadmin") {
|
|
9741
|
-
const newRole = await rolesRepository.findById(body.roleId);
|
|
9742
|
-
if (newRole?.name === "superadmin") {
|
|
9743
|
-
throw new ForbiddenError4({ message: "Only superadmin can assign superadmin role" });
|
|
9744
|
-
}
|
|
9745
|
-
if (newRole?.name === "admin") {
|
|
9746
|
-
const canPromote = await hasPermission(auth.userId, "admin:promote");
|
|
9747
|
-
if (!canPromote) {
|
|
9748
|
-
throw new ForbiddenError4({ message: "admin:promote permission required to assign admin role" });
|
|
9749
|
-
}
|
|
9750
|
-
}
|
|
9795
|
+
throw new ForbiddenError5({ message: "Cannot modify superadmin role" });
|
|
9751
9796
|
}
|
|
9797
|
+
await assertCanAssignRole(auth.userId, body.roleId);
|
|
9752
9798
|
await updateUserService(params.userId, { roleId: body.roleId });
|
|
9753
9799
|
return { userId: params.userId, roleId: body.roleId };
|
|
9754
9800
|
});
|
|
@@ -9756,7 +9802,6 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
|
9756
9802
|
// src/server/routes/index.ts
|
|
9757
9803
|
var mainAuthRouter = defineRouter5({
|
|
9758
9804
|
// Auth routes
|
|
9759
|
-
checkAccountExists,
|
|
9760
9805
|
sendVerificationCode,
|
|
9761
9806
|
verifyCode,
|
|
9762
9807
|
register,
|
|
@@ -9807,11 +9852,11 @@ init_types();
|
|
|
9807
9852
|
init_schema3();
|
|
9808
9853
|
|
|
9809
9854
|
// src/server/lib/crypto.ts
|
|
9810
|
-
import
|
|
9855
|
+
import crypto6 from "crypto";
|
|
9811
9856
|
import jwt3 from "jsonwebtoken";
|
|
9812
9857
|
function generateKeyPairES256() {
|
|
9813
|
-
const keyId =
|
|
9814
|
-
const { privateKey, publicKey } =
|
|
9858
|
+
const keyId = crypto6.randomUUID();
|
|
9859
|
+
const { privateKey, publicKey } = crypto6.generateKeyPairSync("ec", {
|
|
9815
9860
|
namedCurve: "P-256",
|
|
9816
9861
|
// ES256
|
|
9817
9862
|
publicKeyEncoding: {
|
|
@@ -9825,7 +9870,7 @@ function generateKeyPairES256() {
|
|
|
9825
9870
|
});
|
|
9826
9871
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9827
9872
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9828
|
-
const fingerprint =
|
|
9873
|
+
const fingerprint = crypto6.createHash("sha256").update(publicKey).digest("hex");
|
|
9829
9874
|
return {
|
|
9830
9875
|
privateKey: privateKeyB64,
|
|
9831
9876
|
publicKey: publicKeyB64,
|
|
@@ -9835,8 +9880,8 @@ function generateKeyPairES256() {
|
|
|
9835
9880
|
};
|
|
9836
9881
|
}
|
|
9837
9882
|
function generateKeyPairRS256() {
|
|
9838
|
-
const keyId =
|
|
9839
|
-
const { privateKey, publicKey } =
|
|
9883
|
+
const keyId = crypto6.randomUUID();
|
|
9884
|
+
const { privateKey, publicKey } = crypto6.generateKeyPairSync("rsa", {
|
|
9840
9885
|
modulusLength: 2048,
|
|
9841
9886
|
publicKeyEncoding: {
|
|
9842
9887
|
type: "spki",
|
|
@@ -9849,7 +9894,7 @@ function generateKeyPairRS256() {
|
|
|
9849
9894
|
});
|
|
9850
9895
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9851
9896
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9852
|
-
const fingerprint =
|
|
9897
|
+
const fingerprint = crypto6.createHash("sha256").update(publicKey).digest("hex");
|
|
9853
9898
|
return {
|
|
9854
9899
|
privateKey: privateKeyB64,
|
|
9855
9900
|
publicKey: publicKeyB64,
|
|
@@ -9864,7 +9909,7 @@ function generateKeyPair(algorithm = "ES256") {
|
|
|
9864
9909
|
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
9865
9910
|
try {
|
|
9866
9911
|
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
9867
|
-
const privateKeyObject =
|
|
9912
|
+
const privateKeyObject = crypto6.createPrivateKey({
|
|
9868
9913
|
key: privateKeyDER,
|
|
9869
9914
|
format: "der",
|
|
9870
9915
|
type: "pkcs8"
|
|
@@ -9919,8 +9964,8 @@ async function getSessionSecretKey() {
|
|
|
9919
9964
|
}
|
|
9920
9965
|
async function getSecretFingerprint() {
|
|
9921
9966
|
const key = await getSessionSecretKey();
|
|
9922
|
-
const
|
|
9923
|
-
const hex = Array.from(new Uint8Array(
|
|
9967
|
+
const hash2 = await crypto.subtle.digest("SHA-256", key.buffer);
|
|
9968
|
+
const hex = Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
9924
9969
|
return hex.slice(0, 8);
|
|
9925
9970
|
}
|
|
9926
9971
|
async function sealSession(data, ttl = 60 * 60 * 24 * 7) {
|
|
@@ -10161,6 +10206,7 @@ export {
|
|
|
10161
10206
|
acceptInvitation,
|
|
10162
10207
|
addPermissionToRole,
|
|
10163
10208
|
appleProvider,
|
|
10209
|
+
assertCanAssignRole,
|
|
10164
10210
|
authLogger,
|
|
10165
10211
|
authLoginEvent,
|
|
10166
10212
|
authMetadata,
|
|
@@ -10172,7 +10218,6 @@ export {
|
|
|
10172
10218
|
buildOAuthErrorUrl,
|
|
10173
10219
|
cancelInvitation,
|
|
10174
10220
|
changePasswordService,
|
|
10175
|
-
checkAccountExistsService,
|
|
10176
10221
|
checkUsernameAvailableService,
|
|
10177
10222
|
configureAuth,
|
|
10178
10223
|
createAuthLifecycle,
|