@spfn/auth 0.2.0-beta.64 → 0.2.0-beta.65
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/dist/server.d.ts +40 -32
- package/dist/server.js +128 -47
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
package/dist/server.d.ts
CHANGED
|
@@ -3186,10 +3186,10 @@ declare class UsersRepository extends BaseRepository {
|
|
|
3186
3186
|
* Write primary 사용
|
|
3187
3187
|
*/
|
|
3188
3188
|
create(data: NewUser): Promise<{
|
|
3189
|
+
username: string | null;
|
|
3190
|
+
status: "active" | "inactive" | "suspended";
|
|
3189
3191
|
email: string | null;
|
|
3190
3192
|
phone: string | null;
|
|
3191
|
-
status: "active" | "inactive" | "suspended";
|
|
3192
|
-
username: string | null;
|
|
3193
3193
|
id: number;
|
|
3194
3194
|
createdAt: Date;
|
|
3195
3195
|
updatedAt: Date;
|
|
@@ -3266,10 +3266,10 @@ declare class UsersRepository extends BaseRepository {
|
|
|
3266
3266
|
* Write primary 사용
|
|
3267
3267
|
*/
|
|
3268
3268
|
deleteById(id: number): Promise<{
|
|
3269
|
+
username: string | null;
|
|
3270
|
+
status: "active" | "inactive" | "suspended";
|
|
3269
3271
|
email: string | null;
|
|
3270
3272
|
phone: string | null;
|
|
3271
|
-
status: "active" | "inactive" | "suspended";
|
|
3272
|
-
username: string | null;
|
|
3273
3273
|
id: number;
|
|
3274
3274
|
createdAt: Date;
|
|
3275
3275
|
updatedAt: Date;
|
|
@@ -4345,8 +4345,8 @@ declare class InvitationsRepository extends BaseRepository {
|
|
|
4345
4345
|
* 초대 생성
|
|
4346
4346
|
*/
|
|
4347
4347
|
create(data: NewInvitation): Promise<{
|
|
4348
|
-
email: string;
|
|
4349
4348
|
status: "pending" | "accepted" | "expired" | "cancelled";
|
|
4349
|
+
email: string;
|
|
4350
4350
|
id: number;
|
|
4351
4351
|
createdAt: Date;
|
|
4352
4352
|
updatedAt: Date;
|
|
@@ -4379,8 +4379,8 @@ declare class InvitationsRepository extends BaseRepository {
|
|
|
4379
4379
|
* 초대 삭제
|
|
4380
4380
|
*/
|
|
4381
4381
|
deleteById(id: number): Promise<{
|
|
4382
|
-
email: string;
|
|
4383
4382
|
status: "pending" | "accepted" | "expired" | "cancelled";
|
|
4383
|
+
email: string;
|
|
4384
4384
|
id: number;
|
|
4385
4385
|
createdAt: Date;
|
|
4386
4386
|
updatedAt: Date;
|
|
@@ -4524,59 +4524,69 @@ declare const invitationsRepository: InvitationsRepository;
|
|
|
4524
4524
|
* Social Accounts Repository 클래스
|
|
4525
4525
|
*/
|
|
4526
4526
|
declare class SocialAccountsRepository extends BaseRepository {
|
|
4527
|
+
/**
|
|
4528
|
+
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
4529
|
+
*
|
|
4530
|
+
* 레거시 평문(마커 없음)이 감지되면 즉시 재암호화해 저장하는
|
|
4531
|
+
* self-healing 마이그레이션을 수행한다. 호출자에게는 항상 평문이 반환되어
|
|
4532
|
+
* 외부 API 계약(평문 토큰)이 유지된다.
|
|
4533
|
+
*/
|
|
4534
|
+
private decryptAccount;
|
|
4527
4535
|
/**
|
|
4528
4536
|
* provider와 providerUserId로 소셜 계정 조회
|
|
4529
4537
|
* Read replica 사용
|
|
4530
4538
|
*/
|
|
4531
4539
|
findByProviderAndProviderId(provider: SocialProvider, providerUserId: string): Promise<{
|
|
4540
|
+
accessToken: string | null;
|
|
4541
|
+
refreshToken: string | null;
|
|
4542
|
+
userId: number;
|
|
4543
|
+
id: number;
|
|
4532
4544
|
createdAt: Date;
|
|
4533
4545
|
updatedAt: Date;
|
|
4534
|
-
id: number;
|
|
4535
|
-
userId: number;
|
|
4536
4546
|
provider: "google" | "github" | "kakao" | "naver" | "superself";
|
|
4537
4547
|
providerUserId: string;
|
|
4538
4548
|
providerEmail: string | null;
|
|
4539
|
-
accessToken: string | null;
|
|
4540
|
-
refreshToken: string | null;
|
|
4541
4549
|
tokenExpiresAt: Date | null;
|
|
4542
|
-
}>;
|
|
4550
|
+
} | null>;
|
|
4543
4551
|
/**
|
|
4544
4552
|
* userId로 모든 소셜 계정 조회
|
|
4545
4553
|
* Read replica 사용
|
|
4546
4554
|
*/
|
|
4547
|
-
findByUserId(userId: number): Promise<{
|
|
4555
|
+
findByUserId(userId: number): Promise<({
|
|
4556
|
+
accessToken: string | null;
|
|
4557
|
+
refreshToken: string | null;
|
|
4558
|
+
userId: number;
|
|
4559
|
+
id: number;
|
|
4548
4560
|
createdAt: Date;
|
|
4549
4561
|
updatedAt: Date;
|
|
4550
|
-
id: number;
|
|
4551
|
-
userId: number;
|
|
4552
4562
|
provider: "google" | "github" | "kakao" | "naver" | "superself";
|
|
4553
4563
|
providerUserId: string;
|
|
4554
4564
|
providerEmail: string | null;
|
|
4555
|
-
accessToken: string | null;
|
|
4556
|
-
refreshToken: string | null;
|
|
4557
4565
|
tokenExpiresAt: Date | null;
|
|
4558
|
-
}[]>;
|
|
4566
|
+
} | null)[]>;
|
|
4559
4567
|
/**
|
|
4560
4568
|
* userId와 provider로 소셜 계정 조회
|
|
4561
4569
|
* Read replica 사용
|
|
4562
4570
|
*/
|
|
4563
4571
|
findByUserIdAndProvider(userId: number, provider: SocialProvider): Promise<{
|
|
4572
|
+
accessToken: string | null;
|
|
4573
|
+
refreshToken: string | null;
|
|
4574
|
+
userId: number;
|
|
4575
|
+
id: number;
|
|
4564
4576
|
createdAt: Date;
|
|
4565
4577
|
updatedAt: Date;
|
|
4566
|
-
id: number;
|
|
4567
|
-
userId: number;
|
|
4568
4578
|
provider: "google" | "github" | "kakao" | "naver" | "superself";
|
|
4569
4579
|
providerUserId: string;
|
|
4570
4580
|
providerEmail: string | null;
|
|
4571
|
-
accessToken: string | null;
|
|
4572
|
-
refreshToken: string | null;
|
|
4573
4581
|
tokenExpiresAt: Date | null;
|
|
4574
|
-
}>;
|
|
4582
|
+
} | null>;
|
|
4575
4583
|
/**
|
|
4576
4584
|
* 소셜 계정 생성
|
|
4577
4585
|
* Write primary 사용
|
|
4578
4586
|
*/
|
|
4579
4587
|
create(data: NewUserSocialAccount): Promise<{
|
|
4588
|
+
accessToken: string | null;
|
|
4589
|
+
refreshToken: string | null;
|
|
4580
4590
|
userId: number;
|
|
4581
4591
|
id: number;
|
|
4582
4592
|
createdAt: Date;
|
|
@@ -4584,10 +4594,8 @@ declare class SocialAccountsRepository extends BaseRepository {
|
|
|
4584
4594
|
provider: "google" | "github" | "kakao" | "naver" | "superself";
|
|
4585
4595
|
providerUserId: string;
|
|
4586
4596
|
providerEmail: string | null;
|
|
4587
|
-
accessToken: string | null;
|
|
4588
|
-
refreshToken: string | null;
|
|
4589
4597
|
tokenExpiresAt: Date | null;
|
|
4590
|
-
}>;
|
|
4598
|
+
} | null>;
|
|
4591
4599
|
/**
|
|
4592
4600
|
* 토큰 정보 업데이트
|
|
4593
4601
|
* Write primary 사용
|
|
@@ -4597,17 +4605,17 @@ declare class SocialAccountsRepository extends BaseRepository {
|
|
|
4597
4605
|
refreshToken?: string | null;
|
|
4598
4606
|
tokenExpiresAt?: Date | null;
|
|
4599
4607
|
}): Promise<{
|
|
4608
|
+
accessToken: string | null;
|
|
4609
|
+
refreshToken: string | null;
|
|
4610
|
+
userId: number;
|
|
4611
|
+
id: number;
|
|
4600
4612
|
createdAt: Date;
|
|
4601
4613
|
updatedAt: Date;
|
|
4602
|
-
id: number;
|
|
4603
|
-
userId: number;
|
|
4604
4614
|
provider: "google" | "github" | "kakao" | "naver" | "superself";
|
|
4605
4615
|
providerUserId: string;
|
|
4606
4616
|
providerEmail: string | null;
|
|
4607
|
-
accessToken: string | null;
|
|
4608
|
-
refreshToken: string | null;
|
|
4609
4617
|
tokenExpiresAt: Date | null;
|
|
4610
|
-
}>;
|
|
4618
|
+
} | null>;
|
|
4611
4619
|
/**
|
|
4612
4620
|
* 소셜 계정 삭제
|
|
4613
4621
|
* Write primary 사용
|
|
@@ -5095,10 +5103,10 @@ declare function getOptionalAuth(c: Context | {
|
|
|
5095
5103
|
declare function getUser(c: Context | {
|
|
5096
5104
|
raw: Context;
|
|
5097
5105
|
}): {
|
|
5106
|
+
username: string | null;
|
|
5107
|
+
status: "active" | "inactive" | "suspended";
|
|
5098
5108
|
email: string | null;
|
|
5099
5109
|
phone: string | null;
|
|
5100
|
-
status: "active" | "inactive" | "suspended";
|
|
5101
|
-
username: string | null;
|
|
5102
5110
|
id: number;
|
|
5103
5111
|
createdAt: Date;
|
|
5104
5112
|
updatedAt: Date;
|
package/dist/server.js
CHANGED
|
@@ -6492,6 +6492,50 @@ var init_invitations_repository = __esm({
|
|
|
6492
6492
|
}
|
|
6493
6493
|
});
|
|
6494
6494
|
|
|
6495
|
+
// src/server/lib/oauth/token-cipher.ts
|
|
6496
|
+
import crypto3 from "crypto";
|
|
6497
|
+
import { env as env3 } from "@spfn/auth/config";
|
|
6498
|
+
function getTokenKey() {
|
|
6499
|
+
return crypto3.createHash("sha256").update(`social-token:${env3.SPFN_AUTH_SESSION_SECRET}`).digest();
|
|
6500
|
+
}
|
|
6501
|
+
function isEncrypted(value) {
|
|
6502
|
+
return value.startsWith(ENC_PREFIX);
|
|
6503
|
+
}
|
|
6504
|
+
function encryptToken(plain) {
|
|
6505
|
+
if (isEncrypted(plain)) {
|
|
6506
|
+
return plain;
|
|
6507
|
+
}
|
|
6508
|
+
const iv = crypto3.randomBytes(IV_BYTES);
|
|
6509
|
+
const cipher = crypto3.createCipheriv("aes-256-gcm", getTokenKey(), iv);
|
|
6510
|
+
const ciphertext = Buffer.concat([cipher.update(plain, "utf8"), cipher.final()]);
|
|
6511
|
+
const tag = cipher.getAuthTag();
|
|
6512
|
+
return ENC_PREFIX + Buffer.concat([iv, tag, ciphertext]).toString("base64");
|
|
6513
|
+
}
|
|
6514
|
+
function decryptToken(stored) {
|
|
6515
|
+
if (!isEncrypted(stored)) {
|
|
6516
|
+
return stored;
|
|
6517
|
+
}
|
|
6518
|
+
const packed = Buffer.from(stored.slice(ENC_PREFIX.length), "base64");
|
|
6519
|
+
if (packed.length < IV_BYTES + TAG_BYTES) {
|
|
6520
|
+
throw new Error("Malformed encrypted token: payload too short");
|
|
6521
|
+
}
|
|
6522
|
+
const iv = packed.subarray(0, IV_BYTES);
|
|
6523
|
+
const tag = packed.subarray(IV_BYTES, IV_BYTES + TAG_BYTES);
|
|
6524
|
+
const ciphertext = packed.subarray(IV_BYTES + TAG_BYTES);
|
|
6525
|
+
const decipher = crypto3.createDecipheriv("aes-256-gcm", getTokenKey(), iv);
|
|
6526
|
+
decipher.setAuthTag(tag);
|
|
6527
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()]).toString("utf8");
|
|
6528
|
+
}
|
|
6529
|
+
var ENC_PREFIX, IV_BYTES, TAG_BYTES;
|
|
6530
|
+
var init_token_cipher = __esm({
|
|
6531
|
+
"src/server/lib/oauth/token-cipher.ts"() {
|
|
6532
|
+
"use strict";
|
|
6533
|
+
ENC_PREFIX = "enc:v1:";
|
|
6534
|
+
IV_BYTES = 12;
|
|
6535
|
+
TAG_BYTES = 16;
|
|
6536
|
+
}
|
|
6537
|
+
});
|
|
6538
|
+
|
|
6495
6539
|
// src/server/repositories/social-accounts.repository.ts
|
|
6496
6540
|
import { eq as eq10, and as and7 } from "drizzle-orm";
|
|
6497
6541
|
import { BaseRepository as BaseRepository10 } from "@spfn/core/db";
|
|
@@ -6500,7 +6544,38 @@ var init_social_accounts_repository = __esm({
|
|
|
6500
6544
|
"src/server/repositories/social-accounts.repository.ts"() {
|
|
6501
6545
|
"use strict";
|
|
6502
6546
|
init_entities();
|
|
6547
|
+
init_token_cipher();
|
|
6503
6548
|
SocialAccountsRepository = class extends BaseRepository10 {
|
|
6549
|
+
/**
|
|
6550
|
+
* 저장 row 의 토큰을 평문으로 복호화해 반환한다.
|
|
6551
|
+
*
|
|
6552
|
+
* 레거시 평문(마커 없음)이 감지되면 즉시 재암호화해 저장하는
|
|
6553
|
+
* self-healing 마이그레이션을 수행한다. 호출자에게는 항상 평문이 반환되어
|
|
6554
|
+
* 외부 API 계약(평문 토큰)이 유지된다.
|
|
6555
|
+
*/
|
|
6556
|
+
async decryptAccount(account) {
|
|
6557
|
+
if (!account) {
|
|
6558
|
+
return account;
|
|
6559
|
+
}
|
|
6560
|
+
const heal = {};
|
|
6561
|
+
if (account.accessToken && !isEncrypted(account.accessToken)) {
|
|
6562
|
+
heal.accessToken = encryptToken(account.accessToken);
|
|
6563
|
+
}
|
|
6564
|
+
if (account.refreshToken && !isEncrypted(account.refreshToken)) {
|
|
6565
|
+
heal.refreshToken = encryptToken(account.refreshToken);
|
|
6566
|
+
}
|
|
6567
|
+
if (heal.accessToken || heal.refreshToken) {
|
|
6568
|
+
try {
|
|
6569
|
+
await this.db.update(userSocialAccounts).set(heal).where(eq10(userSocialAccounts.id, account.id));
|
|
6570
|
+
} catch {
|
|
6571
|
+
}
|
|
6572
|
+
}
|
|
6573
|
+
return {
|
|
6574
|
+
...account,
|
|
6575
|
+
accessToken: account.accessToken ? decryptToken(account.accessToken) : account.accessToken,
|
|
6576
|
+
refreshToken: account.refreshToken ? decryptToken(account.refreshToken) : account.refreshToken
|
|
6577
|
+
};
|
|
6578
|
+
}
|
|
6504
6579
|
/**
|
|
6505
6580
|
* provider와 providerUserId로 소셜 계정 조회
|
|
6506
6581
|
* Read replica 사용
|
|
@@ -6512,14 +6587,15 @@ var init_social_accounts_repository = __esm({
|
|
|
6512
6587
|
eq10(userSocialAccounts.providerUserId, providerUserId)
|
|
6513
6588
|
)
|
|
6514
6589
|
).limit(1);
|
|
6515
|
-
return result[0] ?? null;
|
|
6590
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6516
6591
|
}
|
|
6517
6592
|
/**
|
|
6518
6593
|
* userId로 모든 소셜 계정 조회
|
|
6519
6594
|
* Read replica 사용
|
|
6520
6595
|
*/
|
|
6521
6596
|
async findByUserId(userId) {
|
|
6522
|
-
|
|
6597
|
+
const result = await this.readDb.select().from(userSocialAccounts).where(eq10(userSocialAccounts.userId, userId));
|
|
6598
|
+
return Promise.all(result.map((account) => this.decryptAccount(account)));
|
|
6523
6599
|
}
|
|
6524
6600
|
/**
|
|
6525
6601
|
* userId와 provider로 소셜 계정 조회
|
|
@@ -6532,18 +6608,21 @@ var init_social_accounts_repository = __esm({
|
|
|
6532
6608
|
eq10(userSocialAccounts.provider, provider)
|
|
6533
6609
|
)
|
|
6534
6610
|
).limit(1);
|
|
6535
|
-
return result[0] ?? null;
|
|
6611
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6536
6612
|
}
|
|
6537
6613
|
/**
|
|
6538
6614
|
* 소셜 계정 생성
|
|
6539
6615
|
* Write primary 사용
|
|
6540
6616
|
*/
|
|
6541
6617
|
async create(data) {
|
|
6542
|
-
|
|
6618
|
+
const created = await this._create(userSocialAccounts, {
|
|
6543
6619
|
...data,
|
|
6620
|
+
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6621
|
+
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
6544
6622
|
createdAt: /* @__PURE__ */ new Date(),
|
|
6545
6623
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6546
6624
|
});
|
|
6625
|
+
return this.decryptAccount(created);
|
|
6547
6626
|
}
|
|
6548
6627
|
/**
|
|
6549
6628
|
* 토큰 정보 업데이트
|
|
@@ -6552,9 +6631,11 @@ var init_social_accounts_repository = __esm({
|
|
|
6552
6631
|
async updateTokens(id11, data) {
|
|
6553
6632
|
const result = await this.db.update(userSocialAccounts).set({
|
|
6554
6633
|
...data,
|
|
6634
|
+
accessToken: data.accessToken ? encryptToken(data.accessToken) : data.accessToken,
|
|
6635
|
+
refreshToken: data.refreshToken ? encryptToken(data.refreshToken) : data.refreshToken,
|
|
6555
6636
|
updatedAt: /* @__PURE__ */ new Date()
|
|
6556
6637
|
}).where(eq10(userSocialAccounts.id, id11)).returning();
|
|
6557
|
-
return result[0] ?? null;
|
|
6638
|
+
return this.decryptAccount(result[0] ?? null);
|
|
6558
6639
|
}
|
|
6559
6640
|
/**
|
|
6560
6641
|
* 소셜 계정 삭제
|
|
@@ -6921,7 +7002,7 @@ import {
|
|
|
6921
7002
|
} from "@spfn/auth/errors";
|
|
6922
7003
|
|
|
6923
7004
|
// src/server/services/verification.service.ts
|
|
6924
|
-
import { env as
|
|
7005
|
+
import { env as env4 } from "@spfn/auth/config";
|
|
6925
7006
|
import { InvalidVerificationCodeError } from "@spfn/auth/errors";
|
|
6926
7007
|
import jwt2 from "jsonwebtoken";
|
|
6927
7008
|
import { sendEmail, sendSMS } from "@spfn/notification/server";
|
|
@@ -6989,7 +7070,7 @@ async function markCodeAsUsed(codeId) {
|
|
|
6989
7070
|
await verificationCodesRepository.markAsUsed(codeId);
|
|
6990
7071
|
}
|
|
6991
7072
|
function createVerificationToken(payload) {
|
|
6992
|
-
return jwt2.sign(payload,
|
|
7073
|
+
return jwt2.sign(payload, env4.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
6993
7074
|
expiresIn: VERIFICATION_TOKEN_EXPIRY,
|
|
6994
7075
|
issuer: "spfn-auth",
|
|
6995
7076
|
audience: "spfn-client"
|
|
@@ -6997,7 +7078,7 @@ function createVerificationToken(payload) {
|
|
|
6997
7078
|
}
|
|
6998
7079
|
function validateVerificationToken(token) {
|
|
6999
7080
|
try {
|
|
7000
|
-
const decoded = jwt2.verify(token,
|
|
7081
|
+
const decoded = jwt2.verify(token, env4.SPFN_AUTH_VERIFICATION_TOKEN_SECRET, {
|
|
7001
7082
|
issuer: "spfn-auth",
|
|
7002
7083
|
audience: "spfn-client"
|
|
7003
7084
|
});
|
|
@@ -7142,7 +7223,7 @@ async function revokeKeyService(params) {
|
|
|
7142
7223
|
init_repositories();
|
|
7143
7224
|
import { ValidationError } from "@spfn/core/errors";
|
|
7144
7225
|
import { ReservedUsernameError, UsernameAlreadyTakenError } from "@spfn/auth/errors";
|
|
7145
|
-
import { env as
|
|
7226
|
+
import { env as env5 } from "@spfn/auth/config";
|
|
7146
7227
|
async function getUserByIdService(userId) {
|
|
7147
7228
|
return await usersRepository.findById(userId);
|
|
7148
7229
|
}
|
|
@@ -7159,7 +7240,7 @@ async function updateUserService(userId, updates) {
|
|
|
7159
7240
|
await usersRepository.updateById(userId, updates);
|
|
7160
7241
|
}
|
|
7161
7242
|
function getReservedUsernames() {
|
|
7162
|
-
const raw =
|
|
7243
|
+
const raw = env5.SPFN_AUTH_RESERVED_USERNAMES ?? "";
|
|
7163
7244
|
if (!raw) {
|
|
7164
7245
|
return /* @__PURE__ */ new Set();
|
|
7165
7246
|
}
|
|
@@ -7171,8 +7252,8 @@ function isReservedUsername(username) {
|
|
|
7171
7252
|
return getReservedUsernames().has(username.toLowerCase());
|
|
7172
7253
|
}
|
|
7173
7254
|
function validateUsernameLength(username) {
|
|
7174
|
-
const min =
|
|
7175
|
-
const max =
|
|
7255
|
+
const min = env5.SPFN_AUTH_USERNAME_MIN_LENGTH ?? 3;
|
|
7256
|
+
const max = env5.SPFN_AUTH_USERNAME_MAX_LENGTH ?? 30;
|
|
7176
7257
|
if (username.length < min) {
|
|
7177
7258
|
throw new ValidationError({
|
|
7178
7259
|
message: `Username must be at least ${min} characters`,
|
|
@@ -7428,7 +7509,7 @@ init_rbac();
|
|
|
7428
7509
|
import { createHash } from "crypto";
|
|
7429
7510
|
|
|
7430
7511
|
// src/server/lib/config.ts
|
|
7431
|
-
import { env as
|
|
7512
|
+
import { env as env6 } from "@spfn/auth/config";
|
|
7432
7513
|
function getCookieSuffix() {
|
|
7433
7514
|
const port = process.env.PORT;
|
|
7434
7515
|
return port ? `_${port}` : "";
|
|
@@ -7490,7 +7571,7 @@ function getSessionTtl(override) {
|
|
|
7490
7571
|
if (globalConfig.sessionTtl !== void 0) {
|
|
7491
7572
|
return parseDuration(globalConfig.sessionTtl);
|
|
7492
7573
|
}
|
|
7493
|
-
const envTtl =
|
|
7574
|
+
const envTtl = env6.SPFN_AUTH_SESSION_TTL;
|
|
7494
7575
|
if (envTtl) {
|
|
7495
7576
|
return parseDuration(envTtl);
|
|
7496
7577
|
}
|
|
@@ -7706,9 +7787,9 @@ init_role_service();
|
|
|
7706
7787
|
|
|
7707
7788
|
// src/server/services/invitation.service.ts
|
|
7708
7789
|
init_repositories();
|
|
7709
|
-
import
|
|
7790
|
+
import crypto4 from "crypto";
|
|
7710
7791
|
function generateInvitationToken() {
|
|
7711
|
-
return
|
|
7792
|
+
return crypto4.randomUUID();
|
|
7712
7793
|
}
|
|
7713
7794
|
function calculateExpiresAt(days = 7) {
|
|
7714
7795
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
@@ -8023,25 +8104,25 @@ async function updateUserProfileService(userId, params) {
|
|
|
8023
8104
|
|
|
8024
8105
|
// src/server/services/oauth.service.ts
|
|
8025
8106
|
init_repositories();
|
|
8026
|
-
import { env as
|
|
8107
|
+
import { env as env9 } from "@spfn/auth/config";
|
|
8027
8108
|
import { ValidationError as ValidationError3 } from "@spfn/core/errors";
|
|
8028
8109
|
|
|
8029
8110
|
// src/server/lib/oauth/google.ts
|
|
8030
|
-
import { env as
|
|
8111
|
+
import { env as env7 } from "@spfn/auth/config";
|
|
8031
8112
|
var GOOGLE_AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
8032
8113
|
var GOOGLE_TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
8033
8114
|
var GOOGLE_USERINFO_URL = "https://www.googleapis.com/oauth2/v2/userinfo";
|
|
8034
8115
|
function isGoogleOAuthEnabled() {
|
|
8035
|
-
return !!(
|
|
8116
|
+
return !!(env7.SPFN_AUTH_GOOGLE_CLIENT_ID && env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET);
|
|
8036
8117
|
}
|
|
8037
8118
|
function getGoogleOAuthConfig() {
|
|
8038
|
-
const clientId =
|
|
8039
|
-
const clientSecret =
|
|
8119
|
+
const clientId = env7.SPFN_AUTH_GOOGLE_CLIENT_ID;
|
|
8120
|
+
const clientSecret = env7.SPFN_AUTH_GOOGLE_CLIENT_SECRET;
|
|
8040
8121
|
if (!clientId || !clientSecret) {
|
|
8041
8122
|
throw new Error("Google OAuth is not configured. Set SPFN_AUTH_GOOGLE_CLIENT_ID and SPFN_AUTH_GOOGLE_CLIENT_SECRET.");
|
|
8042
8123
|
}
|
|
8043
|
-
const baseUrl =
|
|
8044
|
-
const redirectUri =
|
|
8124
|
+
const baseUrl = env7.NEXT_PUBLIC_SPFN_API_URL || env7.SPFN_API_URL;
|
|
8125
|
+
const redirectUri = env7.SPFN_AUTH_GOOGLE_REDIRECT_URI || `${baseUrl}/_auth/oauth/google/callback`;
|
|
8045
8126
|
return {
|
|
8046
8127
|
clientId,
|
|
8047
8128
|
clientSecret,
|
|
@@ -8049,7 +8130,7 @@ function getGoogleOAuthConfig() {
|
|
|
8049
8130
|
};
|
|
8050
8131
|
}
|
|
8051
8132
|
function getDefaultScopes() {
|
|
8052
|
-
const envScopes =
|
|
8133
|
+
const envScopes = env7.SPFN_AUTH_GOOGLE_SCOPES;
|
|
8053
8134
|
if (envScopes) {
|
|
8054
8135
|
return envScopes.split(",").map((s) => s.trim()).filter(Boolean);
|
|
8055
8136
|
}
|
|
@@ -8127,9 +8208,9 @@ async function refreshAccessToken(refreshToken) {
|
|
|
8127
8208
|
|
|
8128
8209
|
// src/server/lib/oauth/state.ts
|
|
8129
8210
|
import * as jose from "jose";
|
|
8130
|
-
import { env as
|
|
8211
|
+
import { env as env8 } from "@spfn/auth/config";
|
|
8131
8212
|
async function getStateKey() {
|
|
8132
|
-
const secret =
|
|
8213
|
+
const secret = env8.SPFN_AUTH_SESSION_SECRET;
|
|
8133
8214
|
const encoder = new TextEncoder();
|
|
8134
8215
|
const data = encoder.encode(`oauth-state:${secret}`);
|
|
8135
8216
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -8282,8 +8363,8 @@ async function oauthCallbackService(params) {
|
|
|
8282
8363
|
algorithm: stateData.algorithm
|
|
8283
8364
|
});
|
|
8284
8365
|
await updateLastLoginService(userId);
|
|
8285
|
-
const appUrl =
|
|
8286
|
-
const callbackPath =
|
|
8366
|
+
const appUrl = env9.NEXT_PUBLIC_SPFN_APP_URL || env9.SPFN_APP_URL;
|
|
8367
|
+
const callbackPath = env9.SPFN_AUTH_OAUTH_SUCCESS_URL || "/auth/callback";
|
|
8287
8368
|
const callbackUrl = callbackPath.startsWith("http") ? callbackPath : `${appUrl}${callbackPath}`;
|
|
8288
8369
|
const redirectUrl = buildRedirectUrl(callbackUrl, {
|
|
8289
8370
|
userId: String(userId),
|
|
@@ -8368,7 +8449,7 @@ function buildRedirectUrl(baseUrl, params) {
|
|
|
8368
8449
|
return `${url.pathname}${url.search}`;
|
|
8369
8450
|
}
|
|
8370
8451
|
function buildOAuthErrorUrl(error) {
|
|
8371
|
-
const errorUrl =
|
|
8452
|
+
const errorUrl = env9.SPFN_AUTH_OAUTH_ERROR_URL || "/auth/error?error={error}";
|
|
8372
8453
|
return errorUrl.replace("{error}", encodeURIComponent(error));
|
|
8373
8454
|
}
|
|
8374
8455
|
function isOAuthProviderEnabled(provider) {
|
|
@@ -9518,11 +9599,11 @@ init_types();
|
|
|
9518
9599
|
init_schema3();
|
|
9519
9600
|
|
|
9520
9601
|
// src/server/lib/crypto.ts
|
|
9521
|
-
import
|
|
9602
|
+
import crypto5 from "crypto";
|
|
9522
9603
|
import jwt3 from "jsonwebtoken";
|
|
9523
9604
|
function generateKeyPairES256() {
|
|
9524
|
-
const keyId =
|
|
9525
|
-
const { privateKey, publicKey } =
|
|
9605
|
+
const keyId = crypto5.randomUUID();
|
|
9606
|
+
const { privateKey, publicKey } = crypto5.generateKeyPairSync("ec", {
|
|
9526
9607
|
namedCurve: "P-256",
|
|
9527
9608
|
// ES256
|
|
9528
9609
|
publicKeyEncoding: {
|
|
@@ -9536,7 +9617,7 @@ function generateKeyPairES256() {
|
|
|
9536
9617
|
});
|
|
9537
9618
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9538
9619
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9539
|
-
const fingerprint =
|
|
9620
|
+
const fingerprint = crypto5.createHash("sha256").update(publicKey).digest("hex");
|
|
9540
9621
|
return {
|
|
9541
9622
|
privateKey: privateKeyB64,
|
|
9542
9623
|
publicKey: publicKeyB64,
|
|
@@ -9546,8 +9627,8 @@ function generateKeyPairES256() {
|
|
|
9546
9627
|
};
|
|
9547
9628
|
}
|
|
9548
9629
|
function generateKeyPairRS256() {
|
|
9549
|
-
const keyId =
|
|
9550
|
-
const { privateKey, publicKey } =
|
|
9630
|
+
const keyId = crypto5.randomUUID();
|
|
9631
|
+
const { privateKey, publicKey } = crypto5.generateKeyPairSync("rsa", {
|
|
9551
9632
|
modulusLength: 2048,
|
|
9552
9633
|
publicKeyEncoding: {
|
|
9553
9634
|
type: "spki",
|
|
@@ -9560,7 +9641,7 @@ function generateKeyPairRS256() {
|
|
|
9560
9641
|
});
|
|
9561
9642
|
const privateKeyB64 = privateKey.toString("base64");
|
|
9562
9643
|
const publicKeyB64 = publicKey.toString("base64");
|
|
9563
|
-
const fingerprint =
|
|
9644
|
+
const fingerprint = crypto5.createHash("sha256").update(publicKey).digest("hex");
|
|
9564
9645
|
return {
|
|
9565
9646
|
privateKey: privateKeyB64,
|
|
9566
9647
|
publicKey: publicKeyB64,
|
|
@@ -9575,7 +9656,7 @@ function generateKeyPair(algorithm = "ES256") {
|
|
|
9575
9656
|
function generateClientToken(payload, privateKeyB64, algorithm, options) {
|
|
9576
9657
|
try {
|
|
9577
9658
|
const privateKeyDER = Buffer.from(privateKeyB64, "base64");
|
|
9578
|
-
const privateKeyObject =
|
|
9659
|
+
const privateKeyObject = crypto5.createPrivateKey({
|
|
9579
9660
|
key: privateKeyDER,
|
|
9580
9661
|
format: "der",
|
|
9581
9662
|
type: "pkcs8"
|
|
@@ -9619,10 +9700,10 @@ function shouldRotateKey(createdAt, rotationDays = 90) {
|
|
|
9619
9700
|
|
|
9620
9701
|
// src/server/lib/session.ts
|
|
9621
9702
|
import * as jose2 from "jose";
|
|
9622
|
-
import { env as
|
|
9703
|
+
import { env as env10 } from "@spfn/auth/config";
|
|
9623
9704
|
import { env as coreEnv } from "@spfn/core/config";
|
|
9624
9705
|
async function getSessionSecretKey() {
|
|
9625
|
-
const secret =
|
|
9706
|
+
const secret = env10.SPFN_AUTH_SESSION_SECRET;
|
|
9626
9707
|
const encoder = new TextEncoder();
|
|
9627
9708
|
const data = encoder.encode(secret);
|
|
9628
9709
|
const hashBuffer = await crypto.subtle.digest("SHA-256", data);
|
|
@@ -9704,14 +9785,14 @@ async function shouldRefreshSession(jwt4, thresholdHours = 24) {
|
|
|
9704
9785
|
}
|
|
9705
9786
|
|
|
9706
9787
|
// src/server/setup.ts
|
|
9707
|
-
import { env as
|
|
9788
|
+
import { env as env11 } from "@spfn/auth/config";
|
|
9708
9789
|
import { getRoleByName as getRoleByName2 } from "@spfn/auth/server";
|
|
9709
9790
|
init_repositories();
|
|
9710
9791
|
function parseAdminAccounts() {
|
|
9711
9792
|
const accounts = [];
|
|
9712
|
-
if (
|
|
9793
|
+
if (env11.SPFN_AUTH_ADMIN_ACCOUNTS) {
|
|
9713
9794
|
try {
|
|
9714
|
-
const accountsJson =
|
|
9795
|
+
const accountsJson = env11.SPFN_AUTH_ADMIN_ACCOUNTS;
|
|
9715
9796
|
const parsed = JSON.parse(accountsJson);
|
|
9716
9797
|
if (!Array.isArray(parsed)) {
|
|
9717
9798
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_ACCOUNTS must be an array");
|
|
@@ -9738,11 +9819,11 @@ function parseAdminAccounts() {
|
|
|
9738
9819
|
return accounts;
|
|
9739
9820
|
}
|
|
9740
9821
|
}
|
|
9741
|
-
const adminEmails =
|
|
9822
|
+
const adminEmails = env11.SPFN_AUTH_ADMIN_EMAILS;
|
|
9742
9823
|
if (adminEmails) {
|
|
9743
9824
|
const emails = adminEmails.split(",").map((s) => s.trim());
|
|
9744
|
-
const passwords = (
|
|
9745
|
-
const roles2 = (
|
|
9825
|
+
const passwords = (env11.SPFN_AUTH_ADMIN_PASSWORDS || "").split(",").map((s) => s.trim());
|
|
9826
|
+
const roles2 = (env11.SPFN_AUTH_ADMIN_ROLES || "").split(",").map((s) => s.trim());
|
|
9746
9827
|
if (passwords.length !== emails.length) {
|
|
9747
9828
|
authLogger.setup.error("\u274C SPFN_AUTH_ADMIN_EMAILS and SPFN_AUTH_ADMIN_PASSWORDS length mismatch");
|
|
9748
9829
|
return accounts;
|
|
@@ -9764,8 +9845,8 @@ function parseAdminAccounts() {
|
|
|
9764
9845
|
}
|
|
9765
9846
|
return accounts;
|
|
9766
9847
|
}
|
|
9767
|
-
const adminEmail =
|
|
9768
|
-
const adminPassword =
|
|
9848
|
+
const adminEmail = env11.SPFN_AUTH_ADMIN_EMAIL;
|
|
9849
|
+
const adminPassword = env11.SPFN_AUTH_ADMIN_PASSWORD;
|
|
9769
9850
|
if (adminEmail && adminPassword) {
|
|
9770
9851
|
accounts.push({
|
|
9771
9852
|
email: adminEmail,
|