@spfn/auth 0.3.0-beta.1 → 0.3.0-beta.3
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 +76 -5
- package/dist/{authenticate-DlTGaBT8.d.ts → authenticate-Ctul07Sc.d.ts} +45 -1
- package/dist/crypto.d.ts +61 -0
- package/dist/crypto.js +108 -0
- package/dist/crypto.js.map +1 -0
- package/dist/index.d.ts +46 -2
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +1 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/server.js +1 -1
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +255 -65
- package/dist/server.js +342 -102
- package/dist/server.js.map +1 -1
- package/migrations/20260807145911_mixed_invisible_woman/migration.sql +11 -0
- package/migrations/20260807145911_mixed_invisible_woman/snapshot.json +3334 -0
- package/package.json +6 -1
package/dist/server.js
CHANGED
|
@@ -5432,6 +5432,38 @@ var init_auth_metadata = __esm({
|
|
|
5432
5432
|
}
|
|
5433
5433
|
});
|
|
5434
5434
|
|
|
5435
|
+
// src/server/entities/ops-tokens.ts
|
|
5436
|
+
import { text as text12 } from "drizzle-orm/pg-core";
|
|
5437
|
+
import { id as id12, timestamps as timestamps11, utcTimestamp as utcTimestamp8 } from "@spfn/core/db";
|
|
5438
|
+
var opsTokens;
|
|
5439
|
+
var init_ops_tokens = __esm({
|
|
5440
|
+
"src/server/entities/ops-tokens.ts"() {
|
|
5441
|
+
"use strict";
|
|
5442
|
+
init_schema4();
|
|
5443
|
+
opsTokens = authSchema.table(
|
|
5444
|
+
"ops_tokens",
|
|
5445
|
+
{
|
|
5446
|
+
id: id12(),
|
|
5447
|
+
// Operator-facing label ("ci-deploy", "rayim-laptop")
|
|
5448
|
+
name: text12("name").notNull(),
|
|
5449
|
+
// SHA-256 hex of the token secret. Lookup key — the secret never lands
|
|
5450
|
+
// here, and the unique constraint doubles as the lookup index.
|
|
5451
|
+
tokenHash: text12("token_hash").notNull().unique(),
|
|
5452
|
+
// Granted scopes as permission strings ('waitlist:read', ...).
|
|
5453
|
+
// '*' grants every scope.
|
|
5454
|
+
scopes: text12("scopes").array().notNull(),
|
|
5455
|
+
// null = the token does not expire
|
|
5456
|
+
expiresAt: utcTimestamp8("expires_at"),
|
|
5457
|
+
// null = active; a timestamp revokes the token permanently
|
|
5458
|
+
revokedAt: utcTimestamp8("revoked_at"),
|
|
5459
|
+
// Last successful verification, updated fire-and-forget
|
|
5460
|
+
lastUsedAt: utcTimestamp8("last_used_at"),
|
|
5461
|
+
...timestamps11()
|
|
5462
|
+
}
|
|
5463
|
+
);
|
|
5464
|
+
}
|
|
5465
|
+
});
|
|
5466
|
+
|
|
5435
5467
|
// src/server/entities/index.ts
|
|
5436
5468
|
var init_entities = __esm({
|
|
5437
5469
|
"src/server/entities/index.ts"() {
|
|
@@ -5449,6 +5481,7 @@ var init_entities = __esm({
|
|
|
5449
5481
|
init_role_permissions();
|
|
5450
5482
|
init_user_permissions();
|
|
5451
5483
|
init_auth_metadata();
|
|
5484
|
+
init_ops_tokens();
|
|
5452
5485
|
}
|
|
5453
5486
|
});
|
|
5454
5487
|
|
|
@@ -5466,8 +5499,8 @@ var init_users_repository = __esm({
|
|
|
5466
5499
|
* ID로 사용자 조회
|
|
5467
5500
|
* Read replica 사용
|
|
5468
5501
|
*/
|
|
5469
|
-
async findById(
|
|
5470
|
-
const result = await this.readDb.select().from(users).where(eq(users.id,
|
|
5502
|
+
async findById(id13) {
|
|
5503
|
+
const result = await this.readDb.select().from(users).where(eq(users.id, id13)).limit(1);
|
|
5471
5504
|
return result[0] ?? null;
|
|
5472
5505
|
}
|
|
5473
5506
|
/**
|
|
@@ -5477,8 +5510,8 @@ var init_users_repository = __esm({
|
|
|
5477
5510
|
* 안 되는 게이트(OAuth 세션 발급 등)가 사용한다. 일반 조회는 `findById`(replica)를
|
|
5478
5511
|
* 계속 사용할 것.
|
|
5479
5512
|
*/
|
|
5480
|
-
async findByIdOnPrimary(
|
|
5481
|
-
const result = await this.db.select().from(users).where(eq(users.id,
|
|
5513
|
+
async findByIdOnPrimary(id13) {
|
|
5514
|
+
const result = await this.db.select().from(users).where(eq(users.id, id13)).limit(1);
|
|
5482
5515
|
return result[0] ?? null;
|
|
5483
5516
|
}
|
|
5484
5517
|
/**
|
|
@@ -5531,13 +5564,13 @@ var init_users_repository = __esm({
|
|
|
5531
5564
|
*
|
|
5532
5565
|
* roleId가 null인 유저는 role: null 반환
|
|
5533
5566
|
*/
|
|
5534
|
-
async findByIdWithRole(
|
|
5567
|
+
async findByIdWithRole(id13) {
|
|
5535
5568
|
const result = await this.readDb.select({
|
|
5536
5569
|
user: users,
|
|
5537
5570
|
roleName: roles.name,
|
|
5538
5571
|
roleDisplayName: roles.displayName,
|
|
5539
5572
|
rolePriority: roles.priority
|
|
5540
|
-
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id,
|
|
5573
|
+
}).from(users).leftJoin(roles, eq(users.roleId, roles.id)).where(eq(users.id, id13)).limit(1);
|
|
5541
5574
|
const row = result[0];
|
|
5542
5575
|
if (!row) {
|
|
5543
5576
|
return null;
|
|
@@ -5558,8 +5591,8 @@ var init_users_repository = __esm({
|
|
|
5558
5591
|
* 사용자 정보 업데이트
|
|
5559
5592
|
* Write primary 사용
|
|
5560
5593
|
*/
|
|
5561
|
-
async updateById(
|
|
5562
|
-
const result = await this.db.update(users).set(data).where(eq(users.id,
|
|
5594
|
+
async updateById(id13, data) {
|
|
5595
|
+
const result = await this.db.update(users).set(data).where(eq(users.id, id13)).returning();
|
|
5563
5596
|
return result[0] ?? null;
|
|
5564
5597
|
}
|
|
5565
5598
|
/**
|
|
@@ -5571,10 +5604,10 @@ var init_users_repository = __esm({
|
|
|
5571
5604
|
* status가 바뀐 상태) 시 null을 반환하며 예외를 던지지 않는다.
|
|
5572
5605
|
* Write primary 사용
|
|
5573
5606
|
*/
|
|
5574
|
-
async reactivateFromPendingDeletion(
|
|
5607
|
+
async reactivateFromPendingDeletion(id13) {
|
|
5575
5608
|
const result = await this.db.update(users).set({ status: "active" }).where(
|
|
5576
5609
|
and(
|
|
5577
|
-
eq(users.id,
|
|
5610
|
+
eq(users.id, id13),
|
|
5578
5611
|
eq(users.status, "pending_deletion")
|
|
5579
5612
|
)
|
|
5580
5613
|
).returning();
|
|
@@ -5584,32 +5617,32 @@ var init_users_repository = __esm({
|
|
|
5584
5617
|
* 비밀번호 업데이트
|
|
5585
5618
|
* Write primary 사용
|
|
5586
5619
|
*/
|
|
5587
|
-
async updatePassword(
|
|
5620
|
+
async updatePassword(id13, passwordHash, clearPasswordChangeRequired = true) {
|
|
5588
5621
|
const updateData = {
|
|
5589
5622
|
passwordHash
|
|
5590
5623
|
};
|
|
5591
5624
|
if (clearPasswordChangeRequired) {
|
|
5592
5625
|
updateData.passwordChangeRequired = false;
|
|
5593
5626
|
}
|
|
5594
|
-
const result = await this.db.update(users).set(updateData).where(eq(users.id,
|
|
5627
|
+
const result = await this.db.update(users).set(updateData).where(eq(users.id, id13)).returning();
|
|
5595
5628
|
return result[0] ?? null;
|
|
5596
5629
|
}
|
|
5597
5630
|
/**
|
|
5598
5631
|
* 마지막 로그인 시간 업데이트
|
|
5599
5632
|
* Write primary 사용
|
|
5600
5633
|
*/
|
|
5601
|
-
async updateLastLogin(
|
|
5634
|
+
async updateLastLogin(id13) {
|
|
5602
5635
|
const result = await this.db.update(users).set({
|
|
5603
5636
|
lastLoginAt: /* @__PURE__ */ new Date()
|
|
5604
|
-
}).where(eq(users.id,
|
|
5637
|
+
}).where(eq(users.id, id13)).returning();
|
|
5605
5638
|
return result[0] ?? null;
|
|
5606
5639
|
}
|
|
5607
5640
|
/**
|
|
5608
5641
|
* 사용자 삭제
|
|
5609
5642
|
* Write primary 사용
|
|
5610
5643
|
*/
|
|
5611
|
-
async deleteById(
|
|
5612
|
-
const result = await this.db.delete(users).where(eq(users.id,
|
|
5644
|
+
async deleteById(id13) {
|
|
5645
|
+
const result = await this.db.delete(users).where(eq(users.id, id13)).returning();
|
|
5613
5646
|
return result[0] ?? null;
|
|
5614
5647
|
}
|
|
5615
5648
|
/**
|
|
@@ -5957,14 +5990,14 @@ var init_keys_repository = __esm({
|
|
|
5957
5990
|
* stored, so it answers "since when has this device been on this release"
|
|
5958
5991
|
* rather than "when was it last seen", which lastUsedAt already answers.
|
|
5959
5992
|
*/
|
|
5960
|
-
async updateLastUsedById(
|
|
5993
|
+
async updateLastUsedById(id13, identity) {
|
|
5961
5994
|
const staleBefore = new Date(Date.now() - LAST_USED_THROTTLE_MS);
|
|
5962
5995
|
const lastUsedIsStale = or(
|
|
5963
5996
|
isNull(userPublicKeys.lastUsedAt),
|
|
5964
5997
|
lt(userPublicKeys.lastUsedAt, staleBefore)
|
|
5965
5998
|
);
|
|
5966
5999
|
if (!identity) {
|
|
5967
|
-
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id,
|
|
6000
|
+
await this.db.update(userPublicKeys).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(and2(eq2(userPublicKeys.id, id13), lastUsedIsStale));
|
|
5968
6001
|
return;
|
|
5969
6002
|
}
|
|
5970
6003
|
const identityChanged = sql4`(
|
|
@@ -5981,7 +6014,7 @@ var init_keys_repository = __esm({
|
|
|
5981
6014
|
clientContractVersion: identity.contractVersion,
|
|
5982
6015
|
clientSeenAt: sql4`CASE WHEN ${identityChanged} THEN ${nowParam} ELSE ${userPublicKeys.clientSeenAt} END`
|
|
5983
6016
|
}).where(and2(
|
|
5984
|
-
eq2(userPublicKeys.id,
|
|
6017
|
+
eq2(userPublicKeys.id, id13),
|
|
5985
6018
|
or(lastUsedIsStale, identityChanged)
|
|
5986
6019
|
));
|
|
5987
6020
|
}
|
|
@@ -6020,8 +6053,8 @@ var init_verification_codes_repository = __esm({
|
|
|
6020
6053
|
* ID로 인증 코드 조회
|
|
6021
6054
|
* Read replica 사용
|
|
6022
6055
|
*/
|
|
6023
|
-
async findById(
|
|
6024
|
-
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id,
|
|
6056
|
+
async findById(id13) {
|
|
6057
|
+
const result = await this.readDb.select().from(verificationCodes).where(eq3(verificationCodes.id, id13)).limit(1);
|
|
6025
6058
|
return result[0] ?? null;
|
|
6026
6059
|
}
|
|
6027
6060
|
/**
|
|
@@ -6035,22 +6068,22 @@ var init_verification_codes_repository = __esm({
|
|
|
6035
6068
|
* 인증 코드 사용 처리
|
|
6036
6069
|
* Write primary 사용
|
|
6037
6070
|
*/
|
|
6038
|
-
async markAsUsed(
|
|
6071
|
+
async markAsUsed(id13) {
|
|
6039
6072
|
const result = await this.db.update(verificationCodes).set({
|
|
6040
6073
|
usedAt: /* @__PURE__ */ new Date()
|
|
6041
|
-
}).where(eq3(verificationCodes.id,
|
|
6074
|
+
}).where(eq3(verificationCodes.id, id13)).returning();
|
|
6042
6075
|
return result[0] ?? null;
|
|
6043
6076
|
}
|
|
6044
6077
|
/**
|
|
6045
6078
|
* 시도 횟수 증가
|
|
6046
6079
|
* Write primary 사용
|
|
6047
6080
|
*/
|
|
6048
|
-
async incrementAttempts(
|
|
6049
|
-
const code = await this.findById(
|
|
6081
|
+
async incrementAttempts(id13) {
|
|
6082
|
+
const code = await this.findById(id13);
|
|
6050
6083
|
if (!code) return null;
|
|
6051
6084
|
const result = await this.db.update(verificationCodes).set({
|
|
6052
6085
|
attempts: code.attempts + 1
|
|
6053
|
-
}).where(eq3(verificationCodes.id,
|
|
6086
|
+
}).where(eq3(verificationCodes.id, id13)).returning();
|
|
6054
6087
|
return result[0] ?? null;
|
|
6055
6088
|
}
|
|
6056
6089
|
/**
|
|
@@ -6106,8 +6139,8 @@ var init_roles_repository = __esm({
|
|
|
6106
6139
|
/**
|
|
6107
6140
|
* ID로 역할 조회
|
|
6108
6141
|
*/
|
|
6109
|
-
async findById(
|
|
6110
|
-
const result = await this.readDb.select().from(roles).where(eq4(roles.id,
|
|
6142
|
+
async findById(id13) {
|
|
6143
|
+
const result = await this.readDb.select().from(roles).where(eq4(roles.id, id13)).limit(1);
|
|
6111
6144
|
return result[0] ?? null;
|
|
6112
6145
|
}
|
|
6113
6146
|
/**
|
|
@@ -6138,15 +6171,15 @@ var init_roles_repository = __esm({
|
|
|
6138
6171
|
/**
|
|
6139
6172
|
* 역할 업데이트
|
|
6140
6173
|
*/
|
|
6141
|
-
async updateById(
|
|
6142
|
-
const result = await this.db.update(roles).set(data).where(eq4(roles.id,
|
|
6174
|
+
async updateById(id13, data) {
|
|
6175
|
+
const result = await this.db.update(roles).set(data).where(eq4(roles.id, id13)).returning();
|
|
6143
6176
|
return result[0] ?? null;
|
|
6144
6177
|
}
|
|
6145
6178
|
/**
|
|
6146
6179
|
* 역할 삭제
|
|
6147
6180
|
*/
|
|
6148
|
-
async deleteById(
|
|
6149
|
-
const result = await this.db.delete(roles).where(eq4(roles.id,
|
|
6181
|
+
async deleteById(id13) {
|
|
6182
|
+
const result = await this.db.delete(roles).where(eq4(roles.id, id13)).returning();
|
|
6150
6183
|
return result[0] ?? null;
|
|
6151
6184
|
}
|
|
6152
6185
|
};
|
|
@@ -6166,8 +6199,8 @@ var init_permissions_repository = __esm({
|
|
|
6166
6199
|
/**
|
|
6167
6200
|
* ID로 권한 조회
|
|
6168
6201
|
*/
|
|
6169
|
-
async findById(
|
|
6170
|
-
const result = await this.readDb.select().from(permissions).where(eq5(permissions.id,
|
|
6202
|
+
async findById(id13) {
|
|
6203
|
+
const result = await this.readDb.select().from(permissions).where(eq5(permissions.id, id13)).limit(1);
|
|
6171
6204
|
return result[0] ?? null;
|
|
6172
6205
|
}
|
|
6173
6206
|
/**
|
|
@@ -6218,15 +6251,15 @@ var init_permissions_repository = __esm({
|
|
|
6218
6251
|
/**
|
|
6219
6252
|
* 권한 업데이트
|
|
6220
6253
|
*/
|
|
6221
|
-
async updateById(
|
|
6222
|
-
const result = await this.db.update(permissions).set(data).where(eq5(permissions.id,
|
|
6254
|
+
async updateById(id13, data) {
|
|
6255
|
+
const result = await this.db.update(permissions).set(data).where(eq5(permissions.id, id13)).returning();
|
|
6223
6256
|
return result[0] ?? null;
|
|
6224
6257
|
}
|
|
6225
6258
|
/**
|
|
6226
6259
|
* 권한 삭제
|
|
6227
6260
|
*/
|
|
6228
|
-
async deleteById(
|
|
6229
|
-
const result = await this.db.delete(permissions).where(eq5(permissions.id,
|
|
6261
|
+
async deleteById(id13) {
|
|
6262
|
+
const result = await this.db.delete(permissions).where(eq5(permissions.id, id13)).returning();
|
|
6230
6263
|
return result[0] ?? null;
|
|
6231
6264
|
}
|
|
6232
6265
|
};
|
|
@@ -6356,8 +6389,8 @@ var init_user_permissions_repository = __esm({
|
|
|
6356
6389
|
/**
|
|
6357
6390
|
* 사용자 권한 오버라이드 업데이트
|
|
6358
6391
|
*/
|
|
6359
|
-
async updateById(
|
|
6360
|
-
const result = await this.db.update(userPermissions).set(data).where(eq7(userPermissions.id,
|
|
6392
|
+
async updateById(id13, data) {
|
|
6393
|
+
const result = await this.db.update(userPermissions).set(data).where(eq7(userPermissions.id, id13)).returning();
|
|
6361
6394
|
return result[0] ?? null;
|
|
6362
6395
|
}
|
|
6363
6396
|
/**
|
|
@@ -6409,8 +6442,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6409
6442
|
/**
|
|
6410
6443
|
* ID로 프로필 조회
|
|
6411
6444
|
*/
|
|
6412
|
-
async findById(
|
|
6413
|
-
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id,
|
|
6445
|
+
async findById(id13) {
|
|
6446
|
+
const result = await this.readDb.select().from(userProfiles).where(eq8(userProfiles.id, id13)).limit(1);
|
|
6414
6447
|
return result[0] ?? null;
|
|
6415
6448
|
}
|
|
6416
6449
|
/**
|
|
@@ -6436,8 +6469,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6436
6469
|
/**
|
|
6437
6470
|
* 프로필 업데이트 (by ID)
|
|
6438
6471
|
*/
|
|
6439
|
-
async updateById(
|
|
6440
|
-
const result = await this.db.update(userProfiles).set(data).where(eq8(userProfiles.id,
|
|
6472
|
+
async updateById(id13, data) {
|
|
6473
|
+
const result = await this.db.update(userProfiles).set(data).where(eq8(userProfiles.id, id13)).returning();
|
|
6441
6474
|
return result[0] ?? null;
|
|
6442
6475
|
}
|
|
6443
6476
|
/**
|
|
@@ -6450,8 +6483,8 @@ var init_user_profiles_repository = __esm({
|
|
|
6450
6483
|
/**
|
|
6451
6484
|
* 프로필 삭제 (by ID)
|
|
6452
6485
|
*/
|
|
6453
|
-
async deleteById(
|
|
6454
|
-
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.id,
|
|
6486
|
+
async deleteById(id13) {
|
|
6487
|
+
const result = await this.db.delete(userProfiles).where(eq8(userProfiles.id, id13)).returning();
|
|
6455
6488
|
return result[0] ?? null;
|
|
6456
6489
|
}
|
|
6457
6490
|
/**
|
|
@@ -6541,8 +6574,8 @@ var init_invitations_repository = __esm({
|
|
|
6541
6574
|
/**
|
|
6542
6575
|
* ID로 초대 조회
|
|
6543
6576
|
*/
|
|
6544
|
-
async findById(
|
|
6545
|
-
const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.id,
|
|
6577
|
+
async findById(id13) {
|
|
6578
|
+
const result = await this.readDb.select().from(userInvitations).where(eq9(userInvitations.id, id13)).limit(1);
|
|
6546
6579
|
return result[0] ?? null;
|
|
6547
6580
|
}
|
|
6548
6581
|
/**
|
|
@@ -6585,7 +6618,7 @@ var init_invitations_repository = __esm({
|
|
|
6585
6618
|
/**
|
|
6586
6619
|
* 초대 상태 업데이트
|
|
6587
6620
|
*/
|
|
6588
|
-
async updateStatus(
|
|
6621
|
+
async updateStatus(id13, status, timestamp2) {
|
|
6589
6622
|
const updates = {
|
|
6590
6623
|
status
|
|
6591
6624
|
};
|
|
@@ -6596,14 +6629,14 @@ var init_invitations_repository = __esm({
|
|
|
6596
6629
|
updates.cancelledAt = timestamp2;
|
|
6597
6630
|
}
|
|
6598
6631
|
}
|
|
6599
|
-
const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id,
|
|
6632
|
+
const result = await this.db.update(userInvitations).set(updates).where(eq9(userInvitations.id, id13)).returning();
|
|
6600
6633
|
return result[0] ?? null;
|
|
6601
6634
|
}
|
|
6602
6635
|
/**
|
|
6603
6636
|
* 초대 삭제
|
|
6604
6637
|
*/
|
|
6605
|
-
async deleteById(
|
|
6606
|
-
const result = await this.db.delete(userInvitations).where(eq9(userInvitations.id,
|
|
6638
|
+
async deleteById(id13) {
|
|
6639
|
+
const result = await this.db.delete(userInvitations).where(eq9(userInvitations.id, id13)).returning();
|
|
6607
6640
|
return result[0] ?? null;
|
|
6608
6641
|
}
|
|
6609
6642
|
/**
|
|
@@ -6698,30 +6731,30 @@ var init_invitations_repository = __esm({
|
|
|
6698
6731
|
/**
|
|
6699
6732
|
* 초대 업데이트 (일반 업데이트 - 모든 필드 가능)
|
|
6700
6733
|
*/
|
|
6701
|
-
async updateById(
|
|
6702
|
-
const result = await this.db.update(userInvitations).set(data).where(eq9(userInvitations.id,
|
|
6734
|
+
async updateById(id13, data) {
|
|
6735
|
+
const result = await this.db.update(userInvitations).set(data).where(eq9(userInvitations.id, id13)).returning();
|
|
6703
6736
|
return result[0] ?? null;
|
|
6704
6737
|
}
|
|
6705
6738
|
/**
|
|
6706
6739
|
* 초대 재전송 (status와 expiresAt 동시 업데이트)
|
|
6707
6740
|
*/
|
|
6708
|
-
async resend(
|
|
6741
|
+
async resend(id13, newExpiresAt) {
|
|
6709
6742
|
const result = await this.db.update(userInvitations).set({
|
|
6710
6743
|
status: "pending",
|
|
6711
6744
|
expiresAt: newExpiresAt
|
|
6712
|
-
}).where(eq9(userInvitations.id,
|
|
6745
|
+
}).where(eq9(userInvitations.id, id13)).returning();
|
|
6713
6746
|
return result[0] ?? null;
|
|
6714
6747
|
}
|
|
6715
6748
|
/**
|
|
6716
6749
|
* 초대 취소 (status, metadata 동시 업데이트)
|
|
6717
6750
|
*/
|
|
6718
|
-
async cancel(
|
|
6751
|
+
async cancel(id13, cancelledBy, reason, currentMetadata) {
|
|
6719
6752
|
const newMetadata = currentMetadata ? { ...currentMetadata, cancelReason: reason, cancelledBy } : { cancelReason: reason, cancelledBy };
|
|
6720
6753
|
const result = await this.db.update(userInvitations).set({
|
|
6721
6754
|
status: "cancelled",
|
|
6722
6755
|
cancelledAt: /* @__PURE__ */ new Date(),
|
|
6723
6756
|
metadata: newMetadata
|
|
6724
|
-
}).where(eq9(userInvitations.id,
|
|
6757
|
+
}).where(eq9(userInvitations.id, id13)).returning();
|
|
6725
6758
|
return result[0] ?? null;
|
|
6726
6759
|
}
|
|
6727
6760
|
};
|
|
@@ -7446,11 +7479,11 @@ var init_social_accounts_repository = __esm({
|
|
|
7446
7479
|
* 토큰 정보 업데이트
|
|
7447
7480
|
* Write primary 사용
|
|
7448
7481
|
*/
|
|
7449
|
-
async updateTokens(
|
|
7482
|
+
async updateTokens(id13, data) {
|
|
7450
7483
|
const accounts = await this.db.select({
|
|
7451
7484
|
provider: userSocialAccounts.provider,
|
|
7452
7485
|
providerUserId: userSocialAccounts.providerUserId
|
|
7453
|
-
}).from(userSocialAccounts).where(eq10(userSocialAccounts.id,
|
|
7486
|
+
}).from(userSocialAccounts).where(eq10(userSocialAccounts.id, id13)).limit(1);
|
|
7454
7487
|
const account = accounts[0];
|
|
7455
7488
|
if (!account) {
|
|
7456
7489
|
return null;
|
|
@@ -7464,15 +7497,15 @@ var init_social_accounts_repository = __esm({
|
|
|
7464
7497
|
...data,
|
|
7465
7498
|
accessToken: data.accessToken ? await encryptToken(data.accessToken, context("access")) : data.accessToken,
|
|
7466
7499
|
refreshToken: data.refreshToken ? await encryptToken(data.refreshToken, context("refresh")) : data.refreshToken
|
|
7467
|
-
}).where(eq10(userSocialAccounts.id,
|
|
7500
|
+
}).where(eq10(userSocialAccounts.id, id13)).returning();
|
|
7468
7501
|
return this.decryptAccount(result[0] ?? null);
|
|
7469
7502
|
}
|
|
7470
7503
|
/**
|
|
7471
7504
|
* 소셜 계정 삭제
|
|
7472
7505
|
* Write primary 사용
|
|
7473
7506
|
*/
|
|
7474
|
-
async deleteById(
|
|
7475
|
-
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id,
|
|
7507
|
+
async deleteById(id13) {
|
|
7508
|
+
const result = await this.db.delete(userSocialAccounts).where(eq10(userSocialAccounts.id, id13)).returning();
|
|
7476
7509
|
return result[0] ?? null;
|
|
7477
7510
|
}
|
|
7478
7511
|
/**
|
|
@@ -7552,8 +7585,8 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7552
7585
|
* ID로 요청 조회
|
|
7553
7586
|
* Read replica 사용
|
|
7554
7587
|
*/
|
|
7555
|
-
async findById(
|
|
7556
|
-
const result = await this.readDb.select().from(accountDeletionRequests).where(eq12(accountDeletionRequests.id,
|
|
7588
|
+
async findById(id13) {
|
|
7589
|
+
const result = await this.readDb.select().from(accountDeletionRequests).where(eq12(accountDeletionRequests.id, id13)).limit(1);
|
|
7557
7590
|
return result[0] ?? null;
|
|
7558
7591
|
}
|
|
7559
7592
|
/**
|
|
@@ -7612,13 +7645,13 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7612
7645
|
* cancelled) 시 null을 반환하니 호출자가 그 결과를 확인해야 한다.
|
|
7613
7646
|
* Write primary 사용
|
|
7614
7647
|
*/
|
|
7615
|
-
async markCancelled(
|
|
7648
|
+
async markCancelled(id13) {
|
|
7616
7649
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7617
7650
|
status: "cancelled",
|
|
7618
7651
|
cancelledAt: /* @__PURE__ */ new Date()
|
|
7619
7652
|
}).where(
|
|
7620
7653
|
and8(
|
|
7621
|
-
eq12(accountDeletionRequests.id,
|
|
7654
|
+
eq12(accountDeletionRequests.id, id13),
|
|
7622
7655
|
eq12(accountDeletionRequests.status, "pending")
|
|
7623
7656
|
)
|
|
7624
7657
|
).returning();
|
|
@@ -7634,14 +7667,14 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7634
7667
|
* destructive DML을 실행하기 **전에** 반드시 이 결과를 확인해야 한다.
|
|
7635
7668
|
* Write primary 사용
|
|
7636
7669
|
*/
|
|
7637
|
-
async markCompleted(
|
|
7670
|
+
async markCompleted(id13, purgeStrategy) {
|
|
7638
7671
|
const result = await this.db.update(accountDeletionRequests).set({
|
|
7639
7672
|
status: "completed",
|
|
7640
7673
|
completedAt: /* @__PURE__ */ new Date(),
|
|
7641
7674
|
purgeStrategy
|
|
7642
7675
|
}).where(
|
|
7643
7676
|
and8(
|
|
7644
|
-
eq12(accountDeletionRequests.id,
|
|
7677
|
+
eq12(accountDeletionRequests.id, id13),
|
|
7645
7678
|
eq12(accountDeletionRequests.status, "pending")
|
|
7646
7679
|
)
|
|
7647
7680
|
).returning();
|
|
@@ -7652,6 +7685,52 @@ var init_account_deletion_requests_repository = __esm({
|
|
|
7652
7685
|
}
|
|
7653
7686
|
});
|
|
7654
7687
|
|
|
7688
|
+
// src/server/repositories/ops-tokens.repository.ts
|
|
7689
|
+
import { and as and9, desc as desc3, eq as eq13, isNull as isNull4 } from "drizzle-orm";
|
|
7690
|
+
import { BaseRepository as BaseRepository13 } from "@spfn/core/db";
|
|
7691
|
+
var OpsTokensRepository, opsTokensRepository;
|
|
7692
|
+
var init_ops_tokens_repository = __esm({
|
|
7693
|
+
"src/server/repositories/ops-tokens.repository.ts"() {
|
|
7694
|
+
"use strict";
|
|
7695
|
+
init_ops_tokens();
|
|
7696
|
+
OpsTokensRepository = class extends BaseRepository13 {
|
|
7697
|
+
/**
|
|
7698
|
+
* Lookup by the secret's hash — the verification path.
|
|
7699
|
+
*
|
|
7700
|
+
* Reads the primary, not the replica, unlike every other SELECT here.
|
|
7701
|
+
* Revocation writes to the primary, so a replica read would keep
|
|
7702
|
+
* authenticating a revoked token for the length of the replication lag —
|
|
7703
|
+
* and revocation is documented as taking effect immediately.
|
|
7704
|
+
*/
|
|
7705
|
+
async findByTokenHash(tokenHash) {
|
|
7706
|
+
const result = await this.db.select().from(opsTokens).where(eq13(opsTokens.tokenHash, tokenHash)).limit(1);
|
|
7707
|
+
return result[0] ?? null;
|
|
7708
|
+
}
|
|
7709
|
+
async create(data) {
|
|
7710
|
+
const result = await this.db.insert(opsTokens).values(data).returning();
|
|
7711
|
+
return result[0];
|
|
7712
|
+
}
|
|
7713
|
+
async list() {
|
|
7714
|
+
return await this.readDb.select().from(opsTokens).orderBy(desc3(opsTokens.createdAt));
|
|
7715
|
+
}
|
|
7716
|
+
/**
|
|
7717
|
+
* Revoke an active token. Returns null when the id does not exist or the
|
|
7718
|
+
* token is already revoked — the first revocation's timestamp is never
|
|
7719
|
+
* overwritten.
|
|
7720
|
+
*/
|
|
7721
|
+
async revokeById(id13) {
|
|
7722
|
+
const result = await this.db.update(opsTokens).set({ revokedAt: /* @__PURE__ */ new Date() }).where(and9(eq13(opsTokens.id, id13), isNull4(opsTokens.revokedAt))).returning();
|
|
7723
|
+
return result[0] ?? null;
|
|
7724
|
+
}
|
|
7725
|
+
/** Fire-and-forget from the verification path. */
|
|
7726
|
+
async updateLastUsedById(id13) {
|
|
7727
|
+
await this.db.update(opsTokens).set({ lastUsedAt: /* @__PURE__ */ new Date() }).where(eq13(opsTokens.id, id13));
|
|
7728
|
+
}
|
|
7729
|
+
};
|
|
7730
|
+
opsTokensRepository = new OpsTokensRepository();
|
|
7731
|
+
}
|
|
7732
|
+
});
|
|
7733
|
+
|
|
7655
7734
|
// src/server/repositories/index.ts
|
|
7656
7735
|
var init_repositories = __esm({
|
|
7657
7736
|
"src/server/repositories/index.ts"() {
|
|
@@ -7668,6 +7747,7 @@ var init_repositories = __esm({
|
|
|
7668
7747
|
init_social_accounts_repository();
|
|
7669
7748
|
init_auth_metadata_repository();
|
|
7670
7749
|
init_account_deletion_requests_repository();
|
|
7750
|
+
init_ops_tokens_repository();
|
|
7671
7751
|
}
|
|
7672
7752
|
});
|
|
7673
7753
|
|
|
@@ -7759,7 +7839,7 @@ async function removePermissionFromRole(roleId, permissionId) {
|
|
|
7759
7839
|
}
|
|
7760
7840
|
async function setRolePermissions(roleId, permissionIds) {
|
|
7761
7841
|
const roleIdNum = Number(roleId);
|
|
7762
|
-
const permissionIdNums = permissionIds.map((
|
|
7842
|
+
const permissionIdNums = permissionIds.map((id13) => Number(id13));
|
|
7763
7843
|
await rolePermissionsRepository.setPermissionsForRole(roleIdNum, permissionIdNums);
|
|
7764
7844
|
}
|
|
7765
7845
|
async function getAllRoles(includeInactive = false) {
|
|
@@ -7779,7 +7859,7 @@ async function getRolePermissions(roleId) {
|
|
|
7779
7859
|
}
|
|
7780
7860
|
const permissionIds = mappings.map((m) => m.permissionId);
|
|
7781
7861
|
const perms = await Promise.all(
|
|
7782
|
-
permissionIds.map((
|
|
7862
|
+
permissionIds.map((id13) => permissionsRepository.findById(id13))
|
|
7783
7863
|
);
|
|
7784
7864
|
return perms.filter((p) => p !== null).map((p) => p.name);
|
|
7785
7865
|
}
|
|
@@ -7992,7 +8072,7 @@ import {
|
|
|
7992
8072
|
// src/server/lib/config.ts
|
|
7993
8073
|
import { env as env4 } from "@spfn/auth/config";
|
|
7994
8074
|
function getCookieSuffix() {
|
|
7995
|
-
const port = process.env.
|
|
8075
|
+
const port = process.env.SPFN_PORT;
|
|
7996
8076
|
return port ? `_${port}` : "";
|
|
7997
8077
|
}
|
|
7998
8078
|
var COOKIE_NAMES = {
|
|
@@ -8595,8 +8675,8 @@ async function verifyReauthCredential(user, params) {
|
|
|
8595
8675
|
throw new VerificationTokenTargetMismatchError();
|
|
8596
8676
|
}
|
|
8597
8677
|
}
|
|
8598
|
-
async function sendDeletionEmail(to, subject,
|
|
8599
|
-
const result = await sendEmail2({ to, subject, text:
|
|
8678
|
+
async function sendDeletionEmail(to, subject, text13) {
|
|
8679
|
+
const result = await sendEmail2({ to, subject, text: text13 });
|
|
8600
8680
|
if (!result.success) {
|
|
8601
8681
|
authLogger.email.error("Failed to send account deletion email", { to, subject, error: result.error });
|
|
8602
8682
|
}
|
|
@@ -9144,7 +9224,7 @@ async function getUserPermissions(userId) {
|
|
|
9144
9224
|
const permIds = rolePermMappings.map((rp) => rp.permissionId);
|
|
9145
9225
|
if (permIds.length > 0) {
|
|
9146
9226
|
const rolePerms = await Promise.all(
|
|
9147
|
-
permIds.map((
|
|
9227
|
+
permIds.map((id13) => permissionsRepository.findById(id13))
|
|
9148
9228
|
);
|
|
9149
9229
|
for (const perm of rolePerms) {
|
|
9150
9230
|
if (perm && perm.isActive) {
|
|
@@ -9357,20 +9437,20 @@ async function acceptInvitation(params) {
|
|
|
9357
9437
|
async function listInvitations(params) {
|
|
9358
9438
|
return await invitationsRepository.list(params);
|
|
9359
9439
|
}
|
|
9360
|
-
async function cancelInvitation(
|
|
9361
|
-
const invitation = await invitationsRepository.findById(
|
|
9440
|
+
async function cancelInvitation(id13, cancelledBy, reason) {
|
|
9441
|
+
const invitation = await invitationsRepository.findById(id13);
|
|
9362
9442
|
if (!invitation) {
|
|
9363
9443
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9364
9444
|
}
|
|
9365
9445
|
if (invitation.status !== "pending") {
|
|
9366
9446
|
throw new ConflictError({ message: `Cannot cancel ${invitation.status} invitation` });
|
|
9367
9447
|
}
|
|
9368
|
-
await invitationsRepository.cancel(
|
|
9448
|
+
await invitationsRepository.cancel(id13, cancelledBy, reason, invitation.metadata);
|
|
9369
9449
|
console.log(`[Auth] \u26A0\uFE0F Invitation cancelled: ${invitation.email} (reason: ${reason || "none"})`);
|
|
9370
9450
|
}
|
|
9371
|
-
async function deleteInvitation(
|
|
9372
|
-
await invitationsRepository.deleteById(
|
|
9373
|
-
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${
|
|
9451
|
+
async function deleteInvitation(id13) {
|
|
9452
|
+
await invitationsRepository.deleteById(id13);
|
|
9453
|
+
console.log(`[Auth] \u{1F5D1}\uFE0F Invitation deleted: ${id13}`);
|
|
9374
9454
|
}
|
|
9375
9455
|
async function expireOldInvitations() {
|
|
9376
9456
|
const count = await invitationsRepository.updateExpiredInvitations();
|
|
@@ -9379,8 +9459,8 @@ async function expireOldInvitations() {
|
|
|
9379
9459
|
}
|
|
9380
9460
|
return count;
|
|
9381
9461
|
}
|
|
9382
|
-
async function resendInvitation(
|
|
9383
|
-
const invitation = await invitationsRepository.findById(
|
|
9462
|
+
async function resendInvitation(id13, expiresInDays = 7) {
|
|
9463
|
+
const invitation = await invitationsRepository.findById(id13);
|
|
9384
9464
|
if (!invitation) {
|
|
9385
9465
|
throw new NotFoundError3({ message: "Invitation not found", resource: "Invitation" });
|
|
9386
9466
|
}
|
|
@@ -9388,7 +9468,7 @@ async function resendInvitation(id12, expiresInDays = 7) {
|
|
|
9388
9468
|
throw new ConflictError({ message: `Cannot resend ${invitation.status} invitation` });
|
|
9389
9469
|
}
|
|
9390
9470
|
const newExpiresAt = calculateExpiresAt(expiresInDays);
|
|
9391
|
-
const updated = await invitationsRepository.resend(
|
|
9471
|
+
const updated = await invitationsRepository.resend(id13, newExpiresAt);
|
|
9392
9472
|
if (!updated) {
|
|
9393
9473
|
throw new Error("Failed to update invitation");
|
|
9394
9474
|
}
|
|
@@ -9702,8 +9782,8 @@ var registry2 = /* @__PURE__ */ new Map();
|
|
|
9702
9782
|
function registerOAuthProvider(provider) {
|
|
9703
9783
|
registry2.set(provider.id, provider);
|
|
9704
9784
|
}
|
|
9705
|
-
function getOAuthProvider(
|
|
9706
|
-
return registry2.get(
|
|
9785
|
+
function getOAuthProvider(id13) {
|
|
9786
|
+
return registry2.get(id13);
|
|
9707
9787
|
}
|
|
9708
9788
|
function getRegisteredProviders() {
|
|
9709
9789
|
return [...registry2.values()];
|
|
@@ -10798,13 +10878,61 @@ async function persistNativeLogin(identity, params) {
|
|
|
10798
10878
|
}, { context: "auth:oauth-native" });
|
|
10799
10879
|
}
|
|
10800
10880
|
|
|
10881
|
+
// src/server/services/ops-token.service.ts
|
|
10882
|
+
init_ops_tokens_repository();
|
|
10883
|
+
import { createHash as createHash4, randomBytes } from "crypto";
|
|
10884
|
+
var OPS_TOKEN_PREFIX = "spfn_ops_";
|
|
10885
|
+
function hashOpsToken(token) {
|
|
10886
|
+
return createHash4("sha256").update(token).digest("hex");
|
|
10887
|
+
}
|
|
10888
|
+
async function issueOpsTokenService(name, scopes, expiresAt) {
|
|
10889
|
+
if (scopes.length === 0) {
|
|
10890
|
+
throw new Error("An ops token needs at least one scope ('*' grants all).");
|
|
10891
|
+
}
|
|
10892
|
+
const token = OPS_TOKEN_PREFIX + randomBytes(32).toString("hex");
|
|
10893
|
+
const record = await opsTokensRepository.create({
|
|
10894
|
+
name,
|
|
10895
|
+
tokenHash: hashOpsToken(token),
|
|
10896
|
+
scopes,
|
|
10897
|
+
expiresAt
|
|
10898
|
+
});
|
|
10899
|
+
return { token, record };
|
|
10900
|
+
}
|
|
10901
|
+
async function verifyOpsTokenService(token) {
|
|
10902
|
+
if (!token.startsWith(OPS_TOKEN_PREFIX)) {
|
|
10903
|
+
return null;
|
|
10904
|
+
}
|
|
10905
|
+
const record = await opsTokensRepository.findByTokenHash(hashOpsToken(token));
|
|
10906
|
+
if (!record) {
|
|
10907
|
+
return null;
|
|
10908
|
+
}
|
|
10909
|
+
if (record.revokedAt !== null) {
|
|
10910
|
+
return null;
|
|
10911
|
+
}
|
|
10912
|
+
if (record.expiresAt !== null && /* @__PURE__ */ new Date() > record.expiresAt) {
|
|
10913
|
+
return null;
|
|
10914
|
+
}
|
|
10915
|
+
opsTokensRepository.updateLastUsedById(record.id).catch((err) => authLogger.service.error("Failed to update ops token lastUsedAt", err));
|
|
10916
|
+
return {
|
|
10917
|
+
tokenId: record.id,
|
|
10918
|
+
name: record.name,
|
|
10919
|
+
scopes: record.scopes
|
|
10920
|
+
};
|
|
10921
|
+
}
|
|
10922
|
+
async function revokeOpsTokenService(id13) {
|
|
10923
|
+
return await opsTokensRepository.revokeById(id13);
|
|
10924
|
+
}
|
|
10925
|
+
async function listOpsTokensService() {
|
|
10926
|
+
return await opsTokensRepository.list();
|
|
10927
|
+
}
|
|
10928
|
+
|
|
10801
10929
|
// src/server/routes/auth/index.ts
|
|
10802
10930
|
init_esm();
|
|
10803
10931
|
import { Transactional } from "@spfn/core/db";
|
|
10804
10932
|
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
10805
10933
|
|
|
10806
10934
|
// src/server/lib/rate-limit-keys.ts
|
|
10807
|
-
import { createHash as
|
|
10935
|
+
import { createHash as createHash5 } from "crypto";
|
|
10808
10936
|
import { getClientIp } from "@spfn/core/middleware";
|
|
10809
10937
|
async function readJsonBody(c) {
|
|
10810
10938
|
try {
|
|
@@ -10844,7 +10972,7 @@ function idTokenKey(body) {
|
|
|
10844
10972
|
if (typeof body.idToken !== "string" || !body.idToken) {
|
|
10845
10973
|
return void 0;
|
|
10846
10974
|
}
|
|
10847
|
-
return `tok:${
|
|
10975
|
+
return `tok:${createHash5("sha256").update(body.idToken).digest("hex")}`;
|
|
10848
10976
|
}
|
|
10849
10977
|
function byIpAndIdToken(options = {}) {
|
|
10850
10978
|
return async (c) => {
|
|
@@ -11086,7 +11214,7 @@ import {
|
|
|
11086
11214
|
} from "@spfn/auth/errors";
|
|
11087
11215
|
|
|
11088
11216
|
// src/server/client-proof/refusal.ts
|
|
11089
|
-
import { randomBytes } from "crypto";
|
|
11217
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
11090
11218
|
|
|
11091
11219
|
// src/server/client-proof/canonical-json.ts
|
|
11092
11220
|
var CanonicalJsonError = class extends Error {
|
|
@@ -11099,13 +11227,13 @@ var CanonicalJsonError = class extends Error {
|
|
|
11099
11227
|
var INT64_MIN = -(2n ** 63n);
|
|
11100
11228
|
var INT64_MAX = 2n ** 63n - 1n;
|
|
11101
11229
|
function parseCanonicalJson(bytes) {
|
|
11102
|
-
let
|
|
11230
|
+
let text13;
|
|
11103
11231
|
try {
|
|
11104
|
-
|
|
11232
|
+
text13 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
11105
11233
|
} catch {
|
|
11106
11234
|
throw new CanonicalJsonError("INVALID_UTF8");
|
|
11107
11235
|
}
|
|
11108
|
-
const parser = new Parser(
|
|
11236
|
+
const parser = new Parser(text13);
|
|
11109
11237
|
const value = parser.parseValue();
|
|
11110
11238
|
parser.skipWhitespace();
|
|
11111
11239
|
if (!parser.atEnd()) {
|
|
@@ -11126,8 +11254,8 @@ function isCanonicalBytes(bytes, value) {
|
|
|
11126
11254
|
return true;
|
|
11127
11255
|
}
|
|
11128
11256
|
var Parser = class {
|
|
11129
|
-
constructor(
|
|
11130
|
-
this.text =
|
|
11257
|
+
constructor(text13) {
|
|
11258
|
+
this.text = text13;
|
|
11131
11259
|
}
|
|
11132
11260
|
pos = 0;
|
|
11133
11261
|
atEnd() {
|
|
@@ -11444,7 +11572,7 @@ var HTTP_STATUS = {
|
|
|
11444
11572
|
CONTRACT_UNSUPPORTED: 409
|
|
11445
11573
|
};
|
|
11446
11574
|
function newHexId() {
|
|
11447
|
-
return
|
|
11575
|
+
return randomBytes2(16).toString("hex");
|
|
11448
11576
|
}
|
|
11449
11577
|
var ClientProofRefusal = class _ClientProofRefusal {
|
|
11450
11578
|
constructor(code, message) {
|
|
@@ -11546,11 +11674,11 @@ function contractViolation(message) {
|
|
|
11546
11674
|
}
|
|
11547
11675
|
|
|
11548
11676
|
// src/server/client-proof/contract-bundle.ts
|
|
11549
|
-
import { createHash as
|
|
11677
|
+
import { createHash as createHash7 } from "crypto";
|
|
11550
11678
|
init_types();
|
|
11551
11679
|
|
|
11552
11680
|
// src/server/client-proof/proof.ts
|
|
11553
|
-
import { createHash as
|
|
11681
|
+
import { createHash as createHash6, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
|
|
11554
11682
|
var CLIENT_PROOF_PROFILE = "clientProofV1";
|
|
11555
11683
|
var ABSENT_BODY_SHA256 = "0".repeat(64);
|
|
11556
11684
|
var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
|
|
@@ -11620,7 +11748,7 @@ function verifyClientProof(input, presentedProof, publicKey) {
|
|
|
11620
11748
|
);
|
|
11621
11749
|
}
|
|
11622
11750
|
function sha256Hex(bytes) {
|
|
11623
|
-
return
|
|
11751
|
+
return createHash6("sha256").update(bytes).digest("hex");
|
|
11624
11752
|
}
|
|
11625
11753
|
|
|
11626
11754
|
// src/server/client-proof/admission.ts
|
|
@@ -12483,6 +12611,46 @@ function extractOTTHeader(header) {
|
|
|
12483
12611
|
return header.substring(4);
|
|
12484
12612
|
}
|
|
12485
12613
|
|
|
12614
|
+
// src/server/middleware/ops-token-auth.ts
|
|
12615
|
+
import { defineMiddleware as defineMiddleware6 } from "@spfn/core/route";
|
|
12616
|
+
import { ForbiddenError as ForbiddenError5, UnauthorizedError as UnauthorizedError4 } from "@spfn/core/errors";
|
|
12617
|
+
function getOpsToken(c) {
|
|
12618
|
+
return c.get("opsToken") ?? null;
|
|
12619
|
+
}
|
|
12620
|
+
var opsTokenAuth = defineMiddleware6("opsTokenAuth", async (c, next) => {
|
|
12621
|
+
const token = extractBearer(c.req.header("Authorization"));
|
|
12622
|
+
if (!token) {
|
|
12623
|
+
throw new UnauthorizedError4({ message: "Ops token required: Authorization: Bearer <token>" });
|
|
12624
|
+
}
|
|
12625
|
+
const verified = await verifyOpsTokenService(token);
|
|
12626
|
+
if (!verified) {
|
|
12627
|
+
throw new UnauthorizedError4({ message: "Invalid ops token" });
|
|
12628
|
+
}
|
|
12629
|
+
c.set("opsToken", verified);
|
|
12630
|
+
await next();
|
|
12631
|
+
}, { skips: ["auth"] });
|
|
12632
|
+
var requireOpsScope = defineMiddleware6(
|
|
12633
|
+
"opsScope",
|
|
12634
|
+
(...scopes) => async (c, next) => {
|
|
12635
|
+
const token = getOpsToken(c);
|
|
12636
|
+
if (!token) {
|
|
12637
|
+
throw new UnauthorizedError4({ message: "Ops token required: Authorization: Bearer <token>" });
|
|
12638
|
+
}
|
|
12639
|
+
const granted = new Set(token.scopes);
|
|
12640
|
+
const missing = scopes.filter((scope) => !granted.has(scope) && !granted.has("*"));
|
|
12641
|
+
if (missing.length > 0) {
|
|
12642
|
+
throw new ForbiddenError5({ message: `Ops token lacks scope: ${missing.join(", ")}` });
|
|
12643
|
+
}
|
|
12644
|
+
await next();
|
|
12645
|
+
}
|
|
12646
|
+
);
|
|
12647
|
+
function extractBearer(header) {
|
|
12648
|
+
if (!header || !header.startsWith("Bearer ")) {
|
|
12649
|
+
return null;
|
|
12650
|
+
}
|
|
12651
|
+
return header.substring(7);
|
|
12652
|
+
}
|
|
12653
|
+
|
|
12486
12654
|
// src/server/routes/invitations/index.ts
|
|
12487
12655
|
init_types();
|
|
12488
12656
|
init_esm();
|
|
@@ -13271,7 +13439,7 @@ var oauthRouter = defineRouter4({
|
|
|
13271
13439
|
|
|
13272
13440
|
// src/server/routes/admin/index.ts
|
|
13273
13441
|
init_esm();
|
|
13274
|
-
import { ForbiddenError as
|
|
13442
|
+
import { ForbiddenError as ForbiddenError6 } from "@spfn/core/errors";
|
|
13275
13443
|
import { route as route5 } from "@spfn/core/route";
|
|
13276
13444
|
var listRoles = route5.get("/_auth/admin/roles").input({
|
|
13277
13445
|
query: Type.Object({
|
|
@@ -13341,11 +13509,11 @@ var updateUserRole = route5.patch("/_auth/admin/users/:userId/role").input({
|
|
|
13341
13509
|
const { params, body } = await c.data();
|
|
13342
13510
|
const auth = getAuth(c);
|
|
13343
13511
|
if (params.userId === Number(auth.userId)) {
|
|
13344
|
-
throw new
|
|
13512
|
+
throw new ForbiddenError6({ message: "Cannot change your own role" });
|
|
13345
13513
|
}
|
|
13346
13514
|
const targetRole = await getUserRole(params.userId);
|
|
13347
13515
|
if (targetRole === "superadmin") {
|
|
13348
|
-
throw new
|
|
13516
|
+
throw new ForbiddenError6({ message: "Cannot modify superadmin role" });
|
|
13349
13517
|
}
|
|
13350
13518
|
await assertCanAssignRole(auth.userId, body.roleId);
|
|
13351
13519
|
await updateUserService(params.userId, { roleId: body.roleId });
|
|
@@ -13400,6 +13568,64 @@ var deletionRouter = defineRouter5({
|
|
|
13400
13568
|
cancelAccountDeletion
|
|
13401
13569
|
});
|
|
13402
13570
|
|
|
13571
|
+
// src/server/routes/ops-tokens/index.ts
|
|
13572
|
+
init_esm();
|
|
13573
|
+
import { route as route7 } from "@spfn/core/route";
|
|
13574
|
+
import { BadRequestError as BadRequestError2, NotFoundError as NotFoundError4 } from "@spfn/core/errors";
|
|
13575
|
+
var MAX_EXPIRY_DAYS = 36500;
|
|
13576
|
+
function toSummary(record) {
|
|
13577
|
+
return {
|
|
13578
|
+
id: Number(record.id),
|
|
13579
|
+
name: record.name,
|
|
13580
|
+
scopes: record.scopes,
|
|
13581
|
+
expiresAt: record.expiresAt ? record.expiresAt.toISOString() : null,
|
|
13582
|
+
revokedAt: record.revokedAt ? record.revokedAt.toISOString() : null,
|
|
13583
|
+
lastUsedAt: record.lastUsedAt ? record.lastUsedAt.toISOString() : null,
|
|
13584
|
+
createdAt: record.createdAt ? record.createdAt.toISOString() : null
|
|
13585
|
+
};
|
|
13586
|
+
}
|
|
13587
|
+
var issueOpsToken = route7.post("/_auth/ops-tokens").input({
|
|
13588
|
+
body: Type.Object({
|
|
13589
|
+
name: Type.String({ minLength: 1, description: "Operator-facing label" }),
|
|
13590
|
+
scopes: Type.Array(Type.String({ minLength: 1 }), {
|
|
13591
|
+
minItems: 1,
|
|
13592
|
+
description: "Scopes the token grants ('*' grants all)"
|
|
13593
|
+
}),
|
|
13594
|
+
expiresInDays: Type.Optional(Type.Union([
|
|
13595
|
+
Type.Number({ exclusiveMinimum: 0, maximum: MAX_EXPIRY_DAYS }),
|
|
13596
|
+
Type.Null()
|
|
13597
|
+
], {
|
|
13598
|
+
description: `Days until expiry, up to ${MAX_EXPIRY_DAYS}; null issues a non-expiring token`
|
|
13599
|
+
}))
|
|
13600
|
+
})
|
|
13601
|
+
}).use([authenticate, requireRole("admin", "superadmin")]).handler(async (c) => {
|
|
13602
|
+
const { body } = await c.data();
|
|
13603
|
+
const expiresInDays = body.expiresInDays ?? null;
|
|
13604
|
+
const expiresAt = expiresInDays === null ? null : new Date(Date.now() + expiresInDays * 24 * 60 * 60 * 1e3);
|
|
13605
|
+
if (expiresAt && Number.isNaN(expiresAt.getTime())) {
|
|
13606
|
+
throw new BadRequestError2({
|
|
13607
|
+
message: `expiresInDays takes 1 to ${MAX_EXPIRY_DAYS} days, or null for no expiry.`
|
|
13608
|
+
});
|
|
13609
|
+
}
|
|
13610
|
+
const { token, record } = await issueOpsTokenService(body.name, body.scopes, expiresAt);
|
|
13611
|
+
return { token, opsToken: toSummary(record) };
|
|
13612
|
+
});
|
|
13613
|
+
var listOpsTokens = route7.get("/_auth/ops-tokens").use([authenticate, requireRole("admin", "superadmin")]).handler(async () => {
|
|
13614
|
+
return { opsTokens: (await listOpsTokensService()).map(toSummary) };
|
|
13615
|
+
});
|
|
13616
|
+
var revokeOpsToken = route7.delete("/_auth/ops-tokens/:id").input({
|
|
13617
|
+
params: Type.Object({
|
|
13618
|
+
id: Type.Number({ description: "Ops token id" })
|
|
13619
|
+
})
|
|
13620
|
+
}).use([authenticate, requireRole("admin", "superadmin")]).handler(async (c) => {
|
|
13621
|
+
const { params } = await c.data();
|
|
13622
|
+
const record = await revokeOpsTokenService(params.id);
|
|
13623
|
+
if (!record) {
|
|
13624
|
+
throw new NotFoundError4({ message: `No ops token with id ${params.id} to revoke.` });
|
|
13625
|
+
}
|
|
13626
|
+
return { opsToken: toSummary(record) };
|
|
13627
|
+
});
|
|
13628
|
+
|
|
13403
13629
|
// src/server/routes/index.ts
|
|
13404
13630
|
var mainAuthRouter = defineRouter6({
|
|
13405
13631
|
// Auth routes
|
|
@@ -13451,7 +13677,11 @@ var mainAuthRouter = defineRouter6({
|
|
|
13451
13677
|
createAdminRole,
|
|
13452
13678
|
updateAdminRole,
|
|
13453
13679
|
deleteAdminRole,
|
|
13454
|
-
updateUserRole
|
|
13680
|
+
updateUserRole,
|
|
13681
|
+
// Ops token routes (admin only)
|
|
13682
|
+
issueOpsToken,
|
|
13683
|
+
listOpsTokens,
|
|
13684
|
+
revokeOpsToken
|
|
13455
13685
|
});
|
|
13456
13686
|
|
|
13457
13687
|
// src/server.ts
|
|
@@ -13827,6 +14057,7 @@ export {
|
|
|
13827
14057
|
KEY_FINGERPRINT_PREFIX_LENGTH,
|
|
13828
14058
|
KEY_PLATFORM,
|
|
13829
14059
|
KeysRepository,
|
|
14060
|
+
OpsTokensRepository,
|
|
13830
14061
|
PURGE_STRATEGIES,
|
|
13831
14062
|
PasswordSchema,
|
|
13832
14063
|
PermissionsRepository,
|
|
@@ -13909,6 +14140,7 @@ export {
|
|
|
13909
14140
|
getLocale,
|
|
13910
14141
|
getOAuthProvider,
|
|
13911
14142
|
getOneTimeTokenManager,
|
|
14143
|
+
getOpsToken,
|
|
13912
14144
|
getOptionalAuth,
|
|
13913
14145
|
getPendingDeletionInfo,
|
|
13914
14146
|
getRegisteredProviders,
|
|
@@ -13942,10 +14174,12 @@ export {
|
|
|
13942
14174
|
isGoogleOAuthEnabled,
|
|
13943
14175
|
isOAuthProviderEnabled,
|
|
13944
14176
|
issueOneTimeTokenService,
|
|
14177
|
+
issueOpsTokenService,
|
|
13945
14178
|
kakaoProvider,
|
|
13946
14179
|
keysRepository,
|
|
13947
14180
|
listInvitations,
|
|
13948
14181
|
listKeysService,
|
|
14182
|
+
listOpsTokensService,
|
|
13949
14183
|
loginService,
|
|
13950
14184
|
logoutService,
|
|
13951
14185
|
matchOAuthCsrfCookies,
|
|
@@ -13956,6 +14190,9 @@ export {
|
|
|
13956
14190
|
oauthUnlinkNotifyService,
|
|
13957
14191
|
oauthUnlinkedEvent,
|
|
13958
14192
|
oneTimeTokenAuth,
|
|
14193
|
+
opsTokenAuth,
|
|
14194
|
+
opsTokens,
|
|
14195
|
+
opsTokensRepository,
|
|
13959
14196
|
optionalAuth,
|
|
13960
14197
|
parseDuration,
|
|
13961
14198
|
permissions,
|
|
@@ -13969,12 +14206,14 @@ export {
|
|
|
13969
14206
|
requestAccountDeletionService,
|
|
13970
14207
|
requireAnyPermission,
|
|
13971
14208
|
requireEnabledProvider,
|
|
14209
|
+
requireOpsScope,
|
|
13972
14210
|
requirePermissions,
|
|
13973
14211
|
requireRole,
|
|
13974
14212
|
resendInvitation,
|
|
13975
14213
|
resolveAuthenticatedUser,
|
|
13976
14214
|
revokeAllKeysService,
|
|
13977
14215
|
revokeKeyService,
|
|
14216
|
+
revokeOpsTokenService,
|
|
13978
14217
|
roleGuard,
|
|
13979
14218
|
rolePermissions,
|
|
13980
14219
|
rolePermissionsRepository,
|
|
@@ -14017,6 +14256,7 @@ export {
|
|
|
14017
14256
|
verifyKeyFingerprint,
|
|
14018
14257
|
verifyOAuthState,
|
|
14019
14258
|
verifyOneTimeTokenService,
|
|
14259
|
+
verifyOpsTokenService,
|
|
14020
14260
|
verifyPassword,
|
|
14021
14261
|
verifyToken
|
|
14022
14262
|
};
|