@spfn/auth 0.2.0-beta.86 → 0.2.0-beta.88
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 +347 -27
- package/dist/{authenticate-DTA7W4v2.d.ts → authenticate-ZssfqJ7d.d.ts} +201 -5
- package/dist/client-proof.d.ts +185 -46
- package/dist/client-proof.js +253 -34
- package/dist/client-proof.js.map +1 -1
- package/dist/config.d.ts +36 -0
- package/dist/config.js +18 -0
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +93 -2
- package/dist/errors.js +57 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +33 -3
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -1
- package/dist/nextjs/server.d.ts +2 -2
- package/dist/server.d.ts +148 -12
- package/dist/server.js +1410 -192
- package/dist/server.js.map +1 -1
- package/dist/{session-CGxgH3C9.d.ts → session-CFK4BT25.d.ts} +1 -1
- package/dist/{types-1BMx0OX1.d.ts → types-CD95yudz.d.ts} +15 -1
- package/migrations/20251125021229_premium_famine/snapshot.json +2641 -0
- package/migrations/20260225130050_smooth_the_fury/snapshot.json +2686 -0
- package/migrations/20260308141417_deep_iceman/snapshot.json +2686 -0
- package/migrations/20260308151309_perfect_deathbird/snapshot.json +2731 -0
- package/migrations/20260308201135_concerned_rawhide_kid/snapshot.json +2786 -0
- package/migrations/20260629103209_lethal_lifeguard/snapshot.json +2786 -0
- package/migrations/20260709073531_easy_hardball/snapshot.json +3119 -0
- package/migrations/20260714081434_glossy_major_mapleleaf/snapshot.json +3112 -0
- package/migrations/20260804105939_amazing_bushwacker/migration.sql +3 -0
- package/migrations/20260804105939_amazing_bushwacker/snapshot.json +3112 -0
- package/migrations/20260804110033_fat_piledriver/migration.sql +2 -0
- package/migrations/20260804110033_fat_piledriver/snapshot.json +3138 -0
- package/package.json +6 -9
- package/migrations/meta/0000_snapshot.json +0 -1632
- package/migrations/meta/0001_snapshot.json +0 -1660
- package/migrations/meta/0002_snapshot.json +0 -1660
- package/migrations/meta/0003_snapshot.json +0 -1689
- package/migrations/meta/0004_snapshot.json +0 -1721
- package/migrations/meta/0005_snapshot.json +0 -1721
- package/migrations/meta/0006_snapshot.json +0 -1921
- package/migrations/meta/0007_snapshot.json +0 -1916
- package/migrations/meta/_journal.json +0 -62
- /package/migrations/{0000_premium_famine.sql → 20251125021229_premium_famine/migration.sql} +0 -0
- /package/migrations/{0001_smooth_the_fury.sql → 20260225130050_smooth_the_fury/migration.sql} +0 -0
- /package/migrations/{0002_deep_iceman.sql → 20260308141417_deep_iceman/migration.sql} +0 -0
- /package/migrations/{0003_perfect_deathbird.sql → 20260308151309_perfect_deathbird/migration.sql} +0 -0
- /package/migrations/{0004_concerned_rawhide_kid.sql → 20260308201135_concerned_rawhide_kid/migration.sql} +0 -0
- /package/migrations/{0005_lethal_lifeguard.sql → 20260629103209_lethal_lifeguard/migration.sql} +0 -0
- /package/migrations/{0006_easy_hardball.sql → 20260709073531_easy_hardball/migration.sql} +0 -0
- /package/migrations/{0007_glossy_major_mapleleaf.sql → 20260714081434_glossy_major_mapleleaf/migration.sql} +0 -0
package/dist/server.js
CHANGED
|
@@ -1379,7 +1379,7 @@ var init_boolean2 = __esm({
|
|
|
1379
1379
|
});
|
|
1380
1380
|
|
|
1381
1381
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/bigint/bigint.mjs
|
|
1382
|
-
function
|
|
1382
|
+
function BigInt2(options) {
|
|
1383
1383
|
return CreateType({ [Kind]: "BigInt", type: "bigint" }, options);
|
|
1384
1384
|
}
|
|
1385
1385
|
var init_bigint = __esm({
|
|
@@ -1441,7 +1441,7 @@ var init_string2 = __esm({
|
|
|
1441
1441
|
// ../../node_modules/.pnpm/@sinclair+typebox@0.34.41/node_modules/@sinclair/typebox/build/esm/type/template-literal/syntax.mjs
|
|
1442
1442
|
function* FromUnion(syntax) {
|
|
1443
1443
|
const trim = syntax.trim().replace(/"|'/g, "");
|
|
1444
|
-
return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield
|
|
1444
|
+
return trim === "boolean" ? yield Boolean2() : trim === "number" ? yield Number2() : trim === "bigint" ? yield BigInt2() : trim === "string" ? yield String2() : yield (() => {
|
|
1445
1445
|
const literals = trim.split("|").map((literal) => Literal(literal.trim()));
|
|
1446
1446
|
return literals.length === 0 ? Never() : literals.length === 1 ? literals[0] : UnionEvaluated(literals);
|
|
1447
1447
|
})();
|
|
@@ -2457,7 +2457,7 @@ function ConditionalReadonly(T, root) {
|
|
|
2457
2457
|
return root === true ? T : Readonly(T);
|
|
2458
2458
|
}
|
|
2459
2459
|
function FromValue(value, root) {
|
|
2460
|
-
return IsAsyncIterator(value) ? ConditionalReadonly(Any(), root) : IsIterator(value) ? ConditionalReadonly(Any(), root) : IsArray(value) ? Readonly(Tuple(FromArray3(value))) : IsUint8Array(value) ? Uint8Array2() : IsDate(value) ? Date2() : IsObject(value) ? ConditionalReadonly(Object2(FromProperties7(value)), root) : IsFunction(value) ? ConditionalReadonly(Function([], Unknown()), root) : IsUndefined(value) ? Undefined() : IsNull(value) ? Null() : IsSymbol(value) ? Symbol2() : IsBigInt(value) ?
|
|
2460
|
+
return IsAsyncIterator(value) ? ConditionalReadonly(Any(), root) : IsIterator(value) ? ConditionalReadonly(Any(), root) : IsArray(value) ? Readonly(Tuple(FromArray3(value))) : IsUint8Array(value) ? Uint8Array2() : IsDate(value) ? Date2() : IsObject(value) ? ConditionalReadonly(Object2(FromProperties7(value)), root) : IsFunction(value) ? ConditionalReadonly(Function([], Unknown()), root) : IsUndefined(value) ? Undefined() : IsNull(value) ? Null() : IsSymbol(value) ? Symbol2() : IsBigInt(value) ? BigInt2() : IsNumber(value) ? Literal(value) : IsBoolean(value) ? Literal(value) : IsString(value) ? Literal(value) : Object2({});
|
|
2461
2461
|
}
|
|
2462
2462
|
function Const(T, options) {
|
|
2463
2463
|
return CreateType(FromValue(T, true), options);
|
|
@@ -4243,7 +4243,7 @@ __export(type_exports3, {
|
|
|
4243
4243
|
Array: () => Array2,
|
|
4244
4244
|
AsyncIterator: () => AsyncIterator,
|
|
4245
4245
|
Awaited: () => Awaited,
|
|
4246
|
-
BigInt: () =>
|
|
4246
|
+
BigInt: () => BigInt2,
|
|
4247
4247
|
Boolean: () => Boolean2,
|
|
4248
4248
|
Capitalize: () => Capitalize,
|
|
4249
4249
|
Composite: () => Composite,
|
|
@@ -4454,13 +4454,31 @@ var init_esm = __esm({
|
|
|
4454
4454
|
}
|
|
4455
4455
|
});
|
|
4456
4456
|
|
|
4457
|
+
// src/server/types.ts
|
|
4458
|
+
var KEY_ALGORITHM, KEY_PLATFORM, KEY_DEVICE_NAME_MAX_LENGTH, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS, ACCOUNT_DELETION_REQUEST_STATUSES, ACCOUNT_DELETION_REQUESTED_BY, PURGE_STRATEGIES;
|
|
4459
|
+
var init_types = __esm({
|
|
4460
|
+
"src/server/types.ts"() {
|
|
4461
|
+
"use strict";
|
|
4462
|
+
KEY_ALGORITHM = ["ES256", "RS256"];
|
|
4463
|
+
KEY_PLATFORM = ["ios", "android", "web", "desktop"];
|
|
4464
|
+
KEY_DEVICE_NAME_MAX_LENGTH = 64;
|
|
4465
|
+
INVITATION_STATUSES = ["pending", "accepted", "expired", "cancelled"];
|
|
4466
|
+
USER_STATUSES = ["active", "inactive", "suspended", "pending_deletion", "deleted"];
|
|
4467
|
+
SOCIAL_PROVIDERS = ["google", "apple", "github", "kakao", "naver", "superself"];
|
|
4468
|
+
ACCOUNT_DELETION_REQUEST_STATUSES = ["pending", "cancelled", "completed"];
|
|
4469
|
+
ACCOUNT_DELETION_REQUESTED_BY = ["self", "admin"];
|
|
4470
|
+
PURGE_STRATEGIES = ["anonymize", "hard-delete"];
|
|
4471
|
+
}
|
|
4472
|
+
});
|
|
4473
|
+
|
|
4457
4474
|
// src/server/routes/schema.ts
|
|
4458
4475
|
import { EMAIL_PATTERN, PHONE_PATTERN } from "@spfn/auth";
|
|
4459
|
-
var EmailSchema, PhoneSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4476
|
+
var EmailSchema, PhoneSchema, DeviceNameSchema, PlatformSchema, PasswordSchema, TargetTypeSchema, VERIFICATION_TARGET_TYPES, VerificationPurposeSchema, VERIFICATION_PURPOSES;
|
|
4460
4477
|
var init_schema3 = __esm({
|
|
4461
4478
|
"src/server/routes/schema.ts"() {
|
|
4462
4479
|
"use strict";
|
|
4463
4480
|
init_esm();
|
|
4481
|
+
init_types();
|
|
4464
4482
|
EmailSchema = Type.String({
|
|
4465
4483
|
pattern: EMAIL_PATTERN,
|
|
4466
4484
|
description: "Email address"
|
|
@@ -4469,6 +4487,14 @@ var init_schema3 = __esm({
|
|
|
4469
4487
|
pattern: PHONE_PATTERN,
|
|
4470
4488
|
description: "Phone number in E.164 format (e.g., +821012345678)"
|
|
4471
4489
|
});
|
|
4490
|
+
DeviceNameSchema = Type.String({
|
|
4491
|
+
maxLength: KEY_DEVICE_NAME_MAX_LENGTH,
|
|
4492
|
+
description: `Device label shown in the key list (max ${KEY_DEVICE_NAME_MAX_LENGTH} chars)`
|
|
4493
|
+
});
|
|
4494
|
+
PlatformSchema = Type.Union(
|
|
4495
|
+
KEY_PLATFORM.map((platform) => Type.Literal(platform)),
|
|
4496
|
+
{ description: "Platform the key lives on" }
|
|
4497
|
+
);
|
|
4472
4498
|
PasswordSchema = Type.String({
|
|
4473
4499
|
minLength: 8,
|
|
4474
4500
|
maxLength: 72,
|
|
@@ -4495,21 +4521,6 @@ var init_schema3 = __esm({
|
|
|
4495
4521
|
}
|
|
4496
4522
|
});
|
|
4497
4523
|
|
|
4498
|
-
// src/server/types.ts
|
|
4499
|
-
var KEY_ALGORITHM, INVITATION_STATUSES, USER_STATUSES, SOCIAL_PROVIDERS, ACCOUNT_DELETION_REQUEST_STATUSES, ACCOUNT_DELETION_REQUESTED_BY, PURGE_STRATEGIES;
|
|
4500
|
-
var init_types = __esm({
|
|
4501
|
-
"src/server/types.ts"() {
|
|
4502
|
-
"use strict";
|
|
4503
|
-
KEY_ALGORITHM = ["ES256", "RS256"];
|
|
4504
|
-
INVITATION_STATUSES = ["pending", "accepted", "expired", "cancelled"];
|
|
4505
|
-
USER_STATUSES = ["active", "inactive", "suspended", "pending_deletion", "deleted"];
|
|
4506
|
-
SOCIAL_PROVIDERS = ["google", "apple", "github", "kakao", "naver", "superself"];
|
|
4507
|
-
ACCOUNT_DELETION_REQUEST_STATUSES = ["pending", "cancelled", "completed"];
|
|
4508
|
-
ACCOUNT_DELETION_REQUESTED_BY = ["self", "admin"];
|
|
4509
|
-
PURGE_STRATEGIES = ["anonymize", "hard-delete"];
|
|
4510
|
-
}
|
|
4511
|
-
});
|
|
4512
|
-
|
|
4513
4524
|
// src/server/entities/schema.ts
|
|
4514
4525
|
import { createSchema } from "@spfn/core/db";
|
|
4515
4526
|
var authSchema;
|
|
@@ -4751,6 +4762,15 @@ var init_user_public_keys = __esm({
|
|
|
4751
4762
|
// Format: hex-encoded string (64 chars)
|
|
4752
4763
|
// Used for: duplicate detection, key verification
|
|
4753
4764
|
fingerprint: text4("fingerprint").notNull(),
|
|
4765
|
+
// Device label the client supplied at registration
|
|
4766
|
+
// null: the client sent none (every key registered before this column existed)
|
|
4767
|
+
// Used for: telling one entry apart from another in the key list
|
|
4768
|
+
// Display only — nothing is authorized by it, so a client that lies gains nothing
|
|
4769
|
+
deviceName: text4("device_name"),
|
|
4770
|
+
// Platform the key lives on, as the client declared it
|
|
4771
|
+
// null: the client sent none
|
|
4772
|
+
// Used for: the same list, alongside deviceName
|
|
4773
|
+
platform: enumText2("platform", KEY_PLATFORM),
|
|
4754
4774
|
// Key status
|
|
4755
4775
|
// false: Key is deactivated (cannot be used for verification)
|
|
4756
4776
|
// Used for: soft key rotation, temporary key suspension
|
|
@@ -5677,7 +5697,7 @@ var init_users_repository = __esm({
|
|
|
5677
5697
|
|
|
5678
5698
|
// src/server/repositories/keys.repository.ts
|
|
5679
5699
|
import { BaseRepository as BaseRepository2 } from "@spfn/core/db";
|
|
5680
|
-
import { eq as eq2, and as and2, or, isNull, lt } from "drizzle-orm";
|
|
5700
|
+
import { eq as eq2, and as and2, or, isNull, lt, ne, desc } from "drizzle-orm";
|
|
5681
5701
|
var LAST_USED_THROTTLE_MS, KeysRepository, keysRepository;
|
|
5682
5702
|
var init_keys_repository = __esm({
|
|
5683
5703
|
"src/server/repositories/keys.repository.ts"() {
|
|
@@ -5717,6 +5737,35 @@ var init_keys_repository = __esm({
|
|
|
5717
5737
|
)
|
|
5718
5738
|
);
|
|
5719
5739
|
}
|
|
5740
|
+
/**
|
|
5741
|
+
* 키 목록 화면이 쓰는 공개키 조회 — 최근 등록순
|
|
5742
|
+
*
|
|
5743
|
+
* publicKey 원문은 고르지 않는다. 목록의 용도는 기기를 알아보고 지목하는 것이고,
|
|
5744
|
+
* 공개키는 그 어느 쪽에도 필요 없다. fingerprint는 호출자가 잘라서 내보낸다.
|
|
5745
|
+
*
|
|
5746
|
+
* `includeRevoked`는 이미 끊은 기기까지 보여준다 — "내가 언제 무엇을 끊었나"를
|
|
5747
|
+
* 확인하는 용도라, 폐기 시각과 사유를 함께 고른다.
|
|
5748
|
+
* Read replica 사용
|
|
5749
|
+
*/
|
|
5750
|
+
async listForUser(userId, includeRevoked = false) {
|
|
5751
|
+
return this.readDb.select({
|
|
5752
|
+
keyId: userPublicKeys.keyId,
|
|
5753
|
+
deviceName: userPublicKeys.deviceName,
|
|
5754
|
+
platform: userPublicKeys.platform,
|
|
5755
|
+
algorithm: userPublicKeys.algorithm,
|
|
5756
|
+
fingerprint: userPublicKeys.fingerprint,
|
|
5757
|
+
isActive: userPublicKeys.isActive,
|
|
5758
|
+
createdAt: userPublicKeys.createdAt,
|
|
5759
|
+
lastUsedAt: userPublicKeys.lastUsedAt,
|
|
5760
|
+
expiresAt: userPublicKeys.expiresAt,
|
|
5761
|
+
revokedAt: userPublicKeys.revokedAt
|
|
5762
|
+
}).from(userPublicKeys).where(
|
|
5763
|
+
includeRevoked ? eq2(userPublicKeys.userId, userId) : and2(
|
|
5764
|
+
eq2(userPublicKeys.userId, userId),
|
|
5765
|
+
eq2(userPublicKeys.isActive, true)
|
|
5766
|
+
)
|
|
5767
|
+
).orderBy(desc(userPublicKeys.createdAt));
|
|
5768
|
+
}
|
|
5720
5769
|
/**
|
|
5721
5770
|
* 공개키 생성
|
|
5722
5771
|
* Write primary 사용
|
|
@@ -5763,6 +5812,26 @@ var init_keys_repository = __esm({
|
|
|
5763
5812
|
)
|
|
5764
5813
|
).returning();
|
|
5765
5814
|
}
|
|
5815
|
+
/**
|
|
5816
|
+
* 지정한 키 하나만 남기고 사용자의 활성 공개키를 전부 revoke
|
|
5817
|
+
*
|
|
5818
|
+
* "다른 기기 전부 로그아웃" — 요청을 보낸 기기는 살려 둔다. 남길 키를 별도 조회로
|
|
5819
|
+
* 확인하지 않고 조건에 담아, 그 사이에 다른 요청이 키를 바꾸는 경쟁을 만들지 않는다.
|
|
5820
|
+
* Write primary 사용
|
|
5821
|
+
*/
|
|
5822
|
+
async revokeAllActiveByUserIdExcept(userId, keepKeyId, reason) {
|
|
5823
|
+
return await this.db.update(userPublicKeys).set({
|
|
5824
|
+
isActive: false,
|
|
5825
|
+
revokedAt: /* @__PURE__ */ new Date(),
|
|
5826
|
+
revokedReason: reason
|
|
5827
|
+
}).where(
|
|
5828
|
+
and2(
|
|
5829
|
+
eq2(userPublicKeys.userId, userId),
|
|
5830
|
+
eq2(userPublicKeys.isActive, true),
|
|
5831
|
+
ne(userPublicKeys.keyId, keepKeyId)
|
|
5832
|
+
)
|
|
5833
|
+
).returning();
|
|
5834
|
+
}
|
|
5766
5835
|
/**
|
|
5767
5836
|
* 공개키 삭제
|
|
5768
5837
|
* Write primary 사용
|
|
@@ -5802,6 +5871,30 @@ var init_keys_repository = __esm({
|
|
|
5802
5871
|
).returning();
|
|
5803
5872
|
return result[0] ?? null;
|
|
5804
5873
|
}
|
|
5874
|
+
/**
|
|
5875
|
+
* 만료 시각 연장 — 같은 사용자가 로그인으로 신원을 다시 증명했을 때 쓴다.
|
|
5876
|
+
* Write primary 사용
|
|
5877
|
+
*/
|
|
5878
|
+
async extendExpiry(keyId, userId, expiresAt) {
|
|
5879
|
+
const result = await this.db.update(userPublicKeys).set({ expiresAt }).where(
|
|
5880
|
+
and2(
|
|
5881
|
+
eq2(userPublicKeys.keyId, keyId),
|
|
5882
|
+
eq2(userPublicKeys.userId, userId)
|
|
5883
|
+
)
|
|
5884
|
+
).returning();
|
|
5885
|
+
return result[0] ?? null;
|
|
5886
|
+
}
|
|
5887
|
+
/**
|
|
5888
|
+
* Key ID로 공개키 조회 — 활성 여부 무관 (clientProofV1 admission의 revocation 판정용)
|
|
5889
|
+
*
|
|
5890
|
+
* 폐기(isActive=false)·만료(expiresAt 경과)를 SESSION_REVOKED로, 미등록을
|
|
5891
|
+
* PROOF_INVALID로 구분해야 하므로 활성 필터 없이 조회한다. keyId는 UNIQUE.
|
|
5892
|
+
* Read replica 사용
|
|
5893
|
+
*/
|
|
5894
|
+
async findByKeyId(keyId) {
|
|
5895
|
+
const result = await this.readDb.select().from(userPublicKeys).where(eq2(userPublicKeys.keyId, keyId)).limit(1);
|
|
5896
|
+
return result[0] ?? null;
|
|
5897
|
+
}
|
|
5805
5898
|
/**
|
|
5806
5899
|
* Key ID로 활성 공개키 조회 (authenticate용)
|
|
5807
5900
|
* Read replica 사용
|
|
@@ -6418,7 +6511,7 @@ var init_user_profiles_repository = __esm({
|
|
|
6418
6511
|
});
|
|
6419
6512
|
|
|
6420
6513
|
// src/server/repositories/invitations.repository.ts
|
|
6421
|
-
import { eq as eq9, and as and6, lt as lt4, desc, sql as sql3 } from "drizzle-orm";
|
|
6514
|
+
import { eq as eq9, and as and6, lt as lt4, desc as desc2, sql as sql3 } from "drizzle-orm";
|
|
6422
6515
|
import { BaseRepository as BaseRepository9 } from "@spfn/core/db";
|
|
6423
6516
|
var InvitationsRepository, invitationsRepository;
|
|
6424
6517
|
var init_invitations_repository = __esm({
|
|
@@ -6584,7 +6677,7 @@ var init_invitations_repository = __esm({
|
|
|
6584
6677
|
id: users.id,
|
|
6585
6678
|
email: users.email
|
|
6586
6679
|
}
|
|
6587
|
-
}).from(userInvitations).innerJoin(roles, eq9(userInvitations.roleId, roles.id)).innerJoin(users, eq9(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(
|
|
6680
|
+
}).from(userInvitations).innerJoin(roles, eq9(userInvitations.roleId, roles.id)).innerJoin(users, eq9(userInvitations.invitedBy, users.id)).where(whereClause).orderBy(desc2(userInvitations.createdAt)).limit(limit).offset(offset);
|
|
6588
6681
|
return {
|
|
6589
6682
|
invitations: results,
|
|
6590
6683
|
total,
|
|
@@ -7041,6 +7134,24 @@ var init_schema5 = __esm({
|
|
|
7041
7134
|
]
|
|
7042
7135
|
})
|
|
7043
7136
|
},
|
|
7137
|
+
SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS: {
|
|
7138
|
+
...envString({
|
|
7139
|
+
description: "Comma-separated Kakao app keys accepted as id_token audience for native sign-in (native app key). SPFN_AUTH_KAKAO_CLIENT_ID (REST API key) is also accepted automatically, so native sign-in is available when either variable is set. Requires OpenID Connect to be enabled in the Kakao developer console.",
|
|
7140
|
+
required: false,
|
|
7141
|
+
examples: [
|
|
7142
|
+
"your-kakao-native-app-key"
|
|
7143
|
+
]
|
|
7144
|
+
})
|
|
7145
|
+
},
|
|
7146
|
+
SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS: {
|
|
7147
|
+
...envString({
|
|
7148
|
+
description: "Comma-separated Naver client IDs accepted as id_token audience for native sign-in. SPFN_AUTH_NAVER_CLIENT_ID is also accepted automatically, and one Naver application has a single client ID covering web and app environments \u2014 set this only when the app uses a separate application.",
|
|
7149
|
+
required: false,
|
|
7150
|
+
examples: [
|
|
7151
|
+
"your-naver-app-client-id"
|
|
7152
|
+
]
|
|
7153
|
+
})
|
|
7154
|
+
},
|
|
7044
7155
|
SPFN_AUTH_OAUTH_SUCCESS_URL: {
|
|
7045
7156
|
...envString({
|
|
7046
7157
|
description: "OAuth callback page URL. This page should use OAuthCallback component to finalize session.",
|
|
@@ -7870,6 +7981,7 @@ function getKeyId(c) {
|
|
|
7870
7981
|
|
|
7871
7982
|
// src/server/routes/auth/index.ts
|
|
7872
7983
|
init_types();
|
|
7984
|
+
import { KeyNotFoundError } from "@spfn/auth/errors";
|
|
7873
7985
|
|
|
7874
7986
|
// src/server/services/auth.service.ts
|
|
7875
7987
|
init_repositories();
|
|
@@ -8158,19 +8270,32 @@ async function verifyCodeService(params) {
|
|
|
8158
8270
|
};
|
|
8159
8271
|
}
|
|
8160
8272
|
|
|
8273
|
+
// src/server/lib/key-policy.ts
|
|
8274
|
+
var KEY_TTL_DAYS = 90;
|
|
8275
|
+
|
|
8161
8276
|
// src/server/services/key.service.ts
|
|
8162
8277
|
init_repositories();
|
|
8163
|
-
import { InvalidKeyFingerprintError } from "@spfn/auth/errors";
|
|
8278
|
+
import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/auth/errors";
|
|
8279
|
+
var KEY_FINGERPRINT_PREFIX_LENGTH = 8;
|
|
8164
8280
|
function getKeyExpiryDate() {
|
|
8165
8281
|
const expiresAt = /* @__PURE__ */ new Date();
|
|
8166
|
-
expiresAt.setDate(expiresAt.getDate() +
|
|
8282
|
+
expiresAt.setDate(expiresAt.getDate() + KEY_TTL_DAYS);
|
|
8167
8283
|
return expiresAt;
|
|
8168
8284
|
}
|
|
8285
|
+
function isExpired(expiresAt) {
|
|
8286
|
+
return expiresAt !== null && /* @__PURE__ */ new Date() > expiresAt;
|
|
8287
|
+
}
|
|
8169
8288
|
async function registerPublicKeyService(params) {
|
|
8170
|
-
const { userId, keyId, publicKey, fingerprint, algorithm = "ES256" } = params;
|
|
8171
|
-
const existing = await keysRepository.
|
|
8289
|
+
const { userId, keyId, publicKey, fingerprint, algorithm = "ES256", deviceName, platform } = params;
|
|
8290
|
+
const existing = await keysRepository.findByKeyId(keyId);
|
|
8172
8291
|
if (existing) {
|
|
8173
|
-
|
|
8292
|
+
if (existing.userId === userId && existing.isActive) {
|
|
8293
|
+
if (isExpired(existing.expiresAt)) {
|
|
8294
|
+
await keysRepository.extendExpiry(keyId, userId, getKeyExpiryDate());
|
|
8295
|
+
}
|
|
8296
|
+
return;
|
|
8297
|
+
}
|
|
8298
|
+
throw new KeyIdAlreadyRegisteredError();
|
|
8174
8299
|
}
|
|
8175
8300
|
const isValidFingerprint = verifyKeyFingerprint(publicKey, fingerprint);
|
|
8176
8301
|
if (!isValidFingerprint) {
|
|
@@ -8182,6 +8307,8 @@ async function registerPublicKeyService(params) {
|
|
|
8182
8307
|
publicKey,
|
|
8183
8308
|
algorithm,
|
|
8184
8309
|
fingerprint,
|
|
8310
|
+
deviceName,
|
|
8311
|
+
platform,
|
|
8185
8312
|
isActive: true,
|
|
8186
8313
|
createdAt: /* @__PURE__ */ new Date(),
|
|
8187
8314
|
expiresAt: getKeyExpiryDate()
|
|
@@ -8193,6 +8320,7 @@ async function rotateKeyService(params) {
|
|
|
8193
8320
|
if (!isValidFingerprint) {
|
|
8194
8321
|
throw new InvalidKeyFingerprintError();
|
|
8195
8322
|
}
|
|
8323
|
+
const replaced = await keysRepository.findByKeyIdAndUserId(oldKeyId, userId);
|
|
8196
8324
|
await keysRepository.revokeByKeyIdAndUserId(
|
|
8197
8325
|
oldKeyId,
|
|
8198
8326
|
userId,
|
|
@@ -8204,6 +8332,8 @@ async function rotateKeyService(params) {
|
|
|
8204
8332
|
publicKey: newPublicKey,
|
|
8205
8333
|
algorithm,
|
|
8206
8334
|
fingerprint,
|
|
8335
|
+
deviceName: params.deviceName ?? replaced?.deviceName ?? void 0,
|
|
8336
|
+
platform: params.platform ?? replaced?.platform ?? void 0,
|
|
8207
8337
|
isActive: true,
|
|
8208
8338
|
createdAt: /* @__PURE__ */ new Date(),
|
|
8209
8339
|
expiresAt: getKeyExpiryDate()
|
|
@@ -8215,7 +8345,29 @@ async function rotateKeyService(params) {
|
|
|
8215
8345
|
}
|
|
8216
8346
|
async function revokeKeyService(params) {
|
|
8217
8347
|
const { userId, keyId, reason } = params;
|
|
8218
|
-
await keysRepository.revokeByKeyIdAndUserId(keyId, userId, reason);
|
|
8348
|
+
const revoked = await keysRepository.revokeByKeyIdAndUserId(keyId, userId, reason);
|
|
8349
|
+
return revoked !== null;
|
|
8350
|
+
}
|
|
8351
|
+
async function listKeysService(params) {
|
|
8352
|
+
const rows = await keysRepository.listForUser(params.userId, params.includeRevoked);
|
|
8353
|
+
return rows.map((row) => ({
|
|
8354
|
+
keyId: row.keyId,
|
|
8355
|
+
deviceName: row.deviceName ?? void 0,
|
|
8356
|
+
platform: row.platform ?? void 0,
|
|
8357
|
+
algorithm: row.algorithm,
|
|
8358
|
+
fingerprintPrefix: row.fingerprint.slice(0, KEY_FINGERPRINT_PREFIX_LENGTH),
|
|
8359
|
+
createdAt: row.createdAt.toISOString(),
|
|
8360
|
+
lastUsedAt: row.lastUsedAt?.toISOString(),
|
|
8361
|
+
expiresAt: row.expiresAt?.toISOString(),
|
|
8362
|
+
isExpired: isExpired(row.expiresAt),
|
|
8363
|
+
isActive: row.isActive,
|
|
8364
|
+
revokedAt: row.revokedAt?.toISOString()
|
|
8365
|
+
}));
|
|
8366
|
+
}
|
|
8367
|
+
async function revokeAllKeysService(params) {
|
|
8368
|
+
const { userId, currentKeyId, includeCurrent = false, reason } = params;
|
|
8369
|
+
const revoked = includeCurrent ? await keysRepository.revokeAllActiveByUserId(userId, reason) : await keysRepository.revokeAllActiveByUserIdExcept(userId, currentKeyId, reason);
|
|
8370
|
+
return { revokedCount: revoked.length, currentKeyRevoked: includeCurrent };
|
|
8219
8371
|
}
|
|
8220
8372
|
|
|
8221
8373
|
// src/server/services/user.service.ts
|
|
@@ -8741,7 +8893,9 @@ async function registerService(params) {
|
|
|
8741
8893
|
keyId,
|
|
8742
8894
|
publicKey,
|
|
8743
8895
|
fingerprint,
|
|
8744
|
-
algorithm
|
|
8896
|
+
algorithm,
|
|
8897
|
+
deviceName: params.deviceName,
|
|
8898
|
+
platform: params.platform
|
|
8745
8899
|
});
|
|
8746
8900
|
const result = {
|
|
8747
8901
|
userId: String(newUser.id),
|
|
@@ -8791,7 +8945,9 @@ async function loginService(params) {
|
|
|
8791
8945
|
keyId,
|
|
8792
8946
|
publicKey,
|
|
8793
8947
|
fingerprint,
|
|
8794
|
-
algorithm
|
|
8948
|
+
algorithm,
|
|
8949
|
+
deviceName: params.deviceName,
|
|
8950
|
+
platform: params.platform
|
|
8795
8951
|
});
|
|
8796
8952
|
await updateLastLoginService(user.id);
|
|
8797
8953
|
const result = {
|
|
@@ -9302,7 +9458,7 @@ function getOneTimeTokenManager() {
|
|
|
9302
9458
|
async function issueOneTimeTokenService(userId) {
|
|
9303
9459
|
const manager2 = getOneTimeTokenManager();
|
|
9304
9460
|
const token = await manager2.issue(userId);
|
|
9305
|
-
const expiresAt = new Date(Date.now() +
|
|
9461
|
+
const expiresAt = new Date(Date.now() + manager2.ttlMs).toISOString();
|
|
9306
9462
|
return { token, expiresAt };
|
|
9307
9463
|
}
|
|
9308
9464
|
async function verifyOneTimeTokenService(token) {
|
|
@@ -9396,8 +9552,12 @@ async function updateUserProfileService(userId, params) {
|
|
|
9396
9552
|
// src/server/services/oauth.service.ts
|
|
9397
9553
|
init_repositories();
|
|
9398
9554
|
import { env as env11 } from "@spfn/auth/config";
|
|
9399
|
-
import { ValidationError as
|
|
9400
|
-
import {
|
|
9555
|
+
import { ValidationError as ValidationError8 } from "@spfn/core/errors";
|
|
9556
|
+
import {
|
|
9557
|
+
AccountDisabledError as AccountDisabledError2,
|
|
9558
|
+
AccountPendingDeletionError as AccountPendingDeletionError2,
|
|
9559
|
+
UnverifiedEmailLinkError
|
|
9560
|
+
} from "@spfn/auth/errors";
|
|
9401
9561
|
|
|
9402
9562
|
// src/server/lib/oauth/google.ts
|
|
9403
9563
|
import { env as env7 } from "@spfn/auth/config";
|
|
@@ -9562,6 +9722,7 @@ function getRegisteredProviders() {
|
|
|
9562
9722
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
9563
9723
|
import { InvalidSocialTokenError } from "@spfn/auth/errors";
|
|
9564
9724
|
var CLOCK_TOLERANCE_SECONDS = 30;
|
|
9725
|
+
var MAX_TOKEN_AGE_SECONDS = 600;
|
|
9565
9726
|
var jwksByUri = /* @__PURE__ */ new Map();
|
|
9566
9727
|
function getRemoteJwks(jwksUri) {
|
|
9567
9728
|
const cached = jwksByUri.get(jwksUri);
|
|
@@ -9588,7 +9749,8 @@ async function verifySignature(params) {
|
|
|
9588
9749
|
issuer: params.issuer,
|
|
9589
9750
|
audience: params.audiences,
|
|
9590
9751
|
algorithms: params.algorithms,
|
|
9591
|
-
clockTolerance: CLOCK_TOLERANCE_SECONDS
|
|
9752
|
+
clockTolerance: CLOCK_TOLERANCE_SECONDS,
|
|
9753
|
+
maxTokenAge: MAX_TOKEN_AGE_SECONDS
|
|
9592
9754
|
});
|
|
9593
9755
|
return payload;
|
|
9594
9756
|
} catch (err) {
|
|
@@ -9604,7 +9766,7 @@ init_token_cipher();
|
|
|
9604
9766
|
|
|
9605
9767
|
// src/server/lib/oauth/google-provider.ts
|
|
9606
9768
|
import { env as env9 } from "@spfn/auth/config";
|
|
9607
|
-
import {
|
|
9769
|
+
import { NativeSignInUnsupportedError } from "@spfn/auth/errors";
|
|
9608
9770
|
var GOOGLE_JWKS_URI = "https://www.googleapis.com/oauth2/v3/certs";
|
|
9609
9771
|
var GOOGLE_ISSUERS = ["https://accounts.google.com", "accounts.google.com"];
|
|
9610
9772
|
function getGoogleNativeAudiences() {
|
|
@@ -9647,7 +9809,7 @@ var googleProvider = {
|
|
|
9647
9809
|
async verifyNativeIdToken(idToken, options) {
|
|
9648
9810
|
const audiences = getGoogleNativeAudiences();
|
|
9649
9811
|
if (audiences.length === 0) {
|
|
9650
|
-
throw new
|
|
9812
|
+
throw new NativeSignInUnsupportedError({
|
|
9651
9813
|
message: "Google native sign-in is not configured. Set SPFN_AUTH_GOOGLE_NATIVE_CLIENT_IDS."
|
|
9652
9814
|
});
|
|
9653
9815
|
}
|
|
@@ -9673,7 +9835,8 @@ registerOAuthProvider(googleProvider);
|
|
|
9673
9835
|
// src/server/lib/oauth/apple-provider.ts
|
|
9674
9836
|
import { createHash as createHash2 } from "crypto";
|
|
9675
9837
|
import { env as env10 } from "@spfn/auth/config";
|
|
9676
|
-
import { ValidationError as
|
|
9838
|
+
import { ValidationError as ValidationError4 } from "@spfn/core/errors";
|
|
9839
|
+
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError2 } from "@spfn/auth/errors";
|
|
9677
9840
|
var APPLE_JWKS_URI = "https://appleid.apple.com/auth/keys";
|
|
9678
9841
|
var APPLE_ISSUER = "https://appleid.apple.com";
|
|
9679
9842
|
function getAppleClientIds() {
|
|
@@ -9683,7 +9846,7 @@ function hashNonce(rawNonce) {
|
|
|
9683
9846
|
return createHash2("sha256").update(rawNonce).digest("hex");
|
|
9684
9847
|
}
|
|
9685
9848
|
function unsupportedWebFlow() {
|
|
9686
|
-
throw new
|
|
9849
|
+
throw new ValidationError4({
|
|
9687
9850
|
message: "Apple provider supports native id_token sign-in only. Use POST /_auth/oauth/apple/native."
|
|
9688
9851
|
});
|
|
9689
9852
|
}
|
|
@@ -9704,7 +9867,7 @@ var appleProvider = {
|
|
|
9704
9867
|
async verifyNativeIdToken(idToken, options) {
|
|
9705
9868
|
const audiences = getAppleClientIds();
|
|
9706
9869
|
if (audiences.length === 0) {
|
|
9707
|
-
throw new
|
|
9870
|
+
throw new NativeSignInUnsupportedError2({
|
|
9708
9871
|
message: "Apple native sign-in is not configured. Set SPFN_AUTH_APPLE_CLIENT_IDS."
|
|
9709
9872
|
});
|
|
9710
9873
|
}
|
|
@@ -9727,7 +9890,7 @@ registerOAuthProvider(appleProvider);
|
|
|
9727
9890
|
|
|
9728
9891
|
// src/server/lib/oauth/github-provider.ts
|
|
9729
9892
|
init_config();
|
|
9730
|
-
import { ValidationError as
|
|
9893
|
+
import { ValidationError as ValidationError5 } from "@spfn/core/errors";
|
|
9731
9894
|
var GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize";
|
|
9732
9895
|
var GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
9733
9896
|
var GITHUB_USERINFO_URL = "https://api.github.com/user";
|
|
@@ -9738,7 +9901,7 @@ function getGithubConfig() {
|
|
|
9738
9901
|
const clientId = env3.SPFN_AUTH_GITHUB_CLIENT_ID;
|
|
9739
9902
|
const clientSecret = env3.SPFN_AUTH_GITHUB_CLIENT_SECRET;
|
|
9740
9903
|
if (!clientId || !clientSecret) {
|
|
9741
|
-
throw new
|
|
9904
|
+
throw new ValidationError5({
|
|
9742
9905
|
message: "GitHub OAuth is not configured. Set SPFN_AUTH_GITHUB_CLIENT_ID and SPFN_AUTH_GITHUB_CLIENT_SECRET."
|
|
9743
9906
|
});
|
|
9744
9907
|
}
|
|
@@ -9750,8 +9913,8 @@ function getGithubConfig() {
|
|
|
9750
9913
|
};
|
|
9751
9914
|
}
|
|
9752
9915
|
function getGithubScopes() {
|
|
9753
|
-
const
|
|
9754
|
-
return
|
|
9916
|
+
const configured2 = env3.SPFN_AUTH_GITHUB_SCOPES;
|
|
9917
|
+
return configured2 ? configured2.split(",").map((scope) => scope.trim()).filter(Boolean) : ["read:user", "user:email"];
|
|
9755
9918
|
}
|
|
9756
9919
|
async function requestGithubTokens(params) {
|
|
9757
9920
|
const response = await fetch(GITHUB_TOKEN_URL, {
|
|
@@ -9860,16 +10023,20 @@ registerOAuthProvider(githubProvider);
|
|
|
9860
10023
|
|
|
9861
10024
|
// src/server/lib/oauth/kakao-provider.ts
|
|
9862
10025
|
init_config();
|
|
9863
|
-
import { ValidationError as
|
|
10026
|
+
import { ValidationError as ValidationError6 } from "@spfn/core/errors";
|
|
10027
|
+
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError3 } from "@spfn/auth/errors";
|
|
9864
10028
|
import { timingSafeEqual } from "crypto";
|
|
9865
10029
|
var KAKAO_AUTH_URL = "https://kauth.kakao.com/oauth/authorize";
|
|
9866
10030
|
var KAKAO_TOKEN_URL = "https://kauth.kakao.com/oauth/token";
|
|
9867
10031
|
var KAKAO_USERINFO_URL = "https://kapi.kakao.com/v2/user/me";
|
|
10032
|
+
var KAKAO_ISSUER = "https://kauth.kakao.com";
|
|
10033
|
+
var KAKAO_JWKS_URI = "https://kauth.kakao.com/.well-known/jwks.json";
|
|
10034
|
+
var USERINFO_TIMEOUT_MS = 5e3;
|
|
9868
10035
|
function getKakaoConfig() {
|
|
9869
10036
|
const clientId = env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
9870
10037
|
const clientSecret = env3.SPFN_AUTH_KAKAO_CLIENT_SECRET;
|
|
9871
10038
|
if (!clientId) {
|
|
9872
|
-
throw new
|
|
10039
|
+
throw new ValidationError6({
|
|
9873
10040
|
message: "Kakao OAuth is not configured. Set SPFN_AUTH_KAKAO_CLIENT_ID."
|
|
9874
10041
|
});
|
|
9875
10042
|
}
|
|
@@ -9881,8 +10048,15 @@ function getKakaoConfig() {
|
|
|
9881
10048
|
};
|
|
9882
10049
|
}
|
|
9883
10050
|
function getKakaoScopes() {
|
|
9884
|
-
const
|
|
9885
|
-
return
|
|
10051
|
+
const configured2 = env3.SPFN_AUTH_KAKAO_SCOPES;
|
|
10052
|
+
return configured2 ? configured2.split(",").map((scope) => scope.trim()).filter(Boolean) : ["account_email"];
|
|
10053
|
+
}
|
|
10054
|
+
function getKakaoNativeAudiences() {
|
|
10055
|
+
const ids = (env3.SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10056
|
+
if (env3.SPFN_AUTH_KAKAO_CLIENT_ID) {
|
|
10057
|
+
ids.push(env3.SPFN_AUTH_KAKAO_CLIENT_ID);
|
|
10058
|
+
}
|
|
10059
|
+
return ids;
|
|
9886
10060
|
}
|
|
9887
10061
|
async function requestKakaoTokens(params) {
|
|
9888
10062
|
const response = await fetch(KAKAO_TOKEN_URL, {
|
|
@@ -9904,8 +10078,61 @@ async function requestKakaoTokens(params) {
|
|
|
9904
10078
|
expiresIn
|
|
9905
10079
|
};
|
|
9906
10080
|
}
|
|
10081
|
+
async function fetchKakaoIdentity(accessToken) {
|
|
10082
|
+
const response = await fetch(KAKAO_USERINFO_URL, {
|
|
10083
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
10084
|
+
signal: AbortSignal.timeout(USERINFO_TIMEOUT_MS)
|
|
10085
|
+
});
|
|
10086
|
+
if (!response.ok) {
|
|
10087
|
+
throw new Error(`Kakao user-info request failed with status ${response.status}`);
|
|
10088
|
+
}
|
|
10089
|
+
const body = await response.json();
|
|
10090
|
+
if (typeof body.id !== "number" && typeof body.id !== "string") {
|
|
10091
|
+
throw new Error("Kakao user-info response is missing the provider user ID");
|
|
10092
|
+
}
|
|
10093
|
+
const account = body.kakao_account;
|
|
10094
|
+
const email = typeof account?.email === "string" ? account.email : null;
|
|
10095
|
+
return {
|
|
10096
|
+
providerUserId: String(body.id),
|
|
10097
|
+
email,
|
|
10098
|
+
emailVerified: email !== null && account?.is_email_valid === true && account.is_email_verified === true,
|
|
10099
|
+
name: typeof account?.profile?.nickname === "string" ? account.profile.nickname : void 0,
|
|
10100
|
+
avatar: typeof account?.profile?.profile_image_url === "string" ? account.profile.profile_image_url : void 0
|
|
10101
|
+
};
|
|
10102
|
+
}
|
|
10103
|
+
async function withKakaoVerifiedEmail(identity, accessToken) {
|
|
10104
|
+
const fetched = await fetchKakaoIdentity(accessToken).catch((err) => {
|
|
10105
|
+
authLogger.service.warn("Kakao user-info lookup failed; continuing without email verification", {
|
|
10106
|
+
reason: err instanceof Error ? err.message : "unknown"
|
|
10107
|
+
});
|
|
10108
|
+
return null;
|
|
10109
|
+
});
|
|
10110
|
+
if (!fetched) {
|
|
10111
|
+
return identity;
|
|
10112
|
+
}
|
|
10113
|
+
if (fetched.providerUserId !== identity.providerUserId) {
|
|
10114
|
+
authLogger.service.warn("Kakao access token belongs to another user; ignoring it", {
|
|
10115
|
+
subject: identity.providerUserId
|
|
10116
|
+
});
|
|
10117
|
+
return identity;
|
|
10118
|
+
}
|
|
10119
|
+
return {
|
|
10120
|
+
...fetched,
|
|
10121
|
+
email: fetched.email ?? identity.email,
|
|
10122
|
+
name: fetched.name ?? identity.name,
|
|
10123
|
+
avatar: fetched.avatar ?? identity.avatar
|
|
10124
|
+
};
|
|
10125
|
+
}
|
|
9907
10126
|
var kakaoProvider = {
|
|
9908
10127
|
id: "kakao",
|
|
10128
|
+
/**
|
|
10129
|
+
* 웹 authorization-code 흐름을 쓸 수 있는지만 판정한다.
|
|
10130
|
+
*
|
|
10131
|
+
* native 로그인은 이 값을 보지 않는다 — oauthNativeService는 verifyNativeIdToken의 존재만
|
|
10132
|
+
* 확인하고, 허용 audience는 그 메서드가 직접 검사한다. 네이티브 앱 키만 설정한 서비스에서
|
|
10133
|
+
* 이 값이 true가 되면 웹 흐름 게이트를 통과한 뒤 더 깊은 곳에서 REST API 키 부재로
|
|
10134
|
+
* 실패하므로, 판정을 웹 키에 그대로 둔다.
|
|
10135
|
+
*/
|
|
9909
10136
|
isEnabled() {
|
|
9910
10137
|
return !!env3.SPFN_AUTH_KAKAO_CLIENT_ID;
|
|
9911
10138
|
},
|
|
@@ -9934,25 +10161,41 @@ var kakaoProvider = {
|
|
|
9934
10161
|
return requestKakaoTokens(params);
|
|
9935
10162
|
},
|
|
9936
10163
|
async getUserInfo(accessToken) {
|
|
9937
|
-
|
|
9938
|
-
|
|
9939
|
-
|
|
9940
|
-
|
|
9941
|
-
|
|
9942
|
-
|
|
9943
|
-
|
|
9944
|
-
|
|
9945
|
-
|
|
10164
|
+
return fetchKakaoIdentity(accessToken);
|
|
10165
|
+
},
|
|
10166
|
+
/**
|
|
10167
|
+
* 네이티브/웹 SDK가 받은 카카오 id_token을 검증한다.
|
|
10168
|
+
*
|
|
10169
|
+
* 카카오는 표준 OIDC라 nonce를 raw 그대로 담는다(Apple 같은 SHA-256 해싱이 없다).
|
|
10170
|
+
* 앱에서 id_token을 받으려면 개발자 콘솔에서 OpenID Connect를 켜고 로그인 요청 scope에
|
|
10171
|
+
* openid를 넣어야 한다.
|
|
10172
|
+
*
|
|
10173
|
+
* emailVerified는 기본이 false다. id_token에는 이메일의 유효·인증 여부를 알려주는 claim이
|
|
10174
|
+
* 없어서, 웹 흐름과 같은 강도로 판정하려면 access token이 함께 와야 한다.
|
|
10175
|
+
*/
|
|
10176
|
+
async verifyNativeIdToken(idToken, options) {
|
|
10177
|
+
const audiences = getKakaoNativeAudiences();
|
|
10178
|
+
if (audiences.length === 0) {
|
|
10179
|
+
throw new NativeSignInUnsupportedError3({
|
|
10180
|
+
message: "Kakao native sign-in is not configured. Set SPFN_AUTH_KAKAO_NATIVE_CLIENT_IDS."
|
|
10181
|
+
});
|
|
9946
10182
|
}
|
|
9947
|
-
const
|
|
9948
|
-
|
|
9949
|
-
|
|
9950
|
-
|
|
9951
|
-
|
|
9952
|
-
|
|
9953
|
-
|
|
9954
|
-
|
|
10183
|
+
const payload = await verifyIdToken({
|
|
10184
|
+
idToken,
|
|
10185
|
+
jwksUri: KAKAO_JWKS_URI,
|
|
10186
|
+
issuer: KAKAO_ISSUER,
|
|
10187
|
+
audiences,
|
|
10188
|
+
algorithms: ["RS256"],
|
|
10189
|
+
expectedNonce: options.nonce
|
|
10190
|
+
});
|
|
10191
|
+
const identity = {
|
|
10192
|
+
providerUserId: payload.sub,
|
|
10193
|
+
email: typeof payload.email === "string" ? payload.email : null,
|
|
10194
|
+
emailVerified: false,
|
|
10195
|
+
name: typeof payload.nickname === "string" ? payload.nickname : void 0,
|
|
10196
|
+
avatar: typeof payload.picture === "string" ? payload.picture : void 0
|
|
9955
10197
|
};
|
|
10198
|
+
return options.accessToken ? withKakaoVerifiedEmail(identity, options.accessToken) : identity;
|
|
9956
10199
|
},
|
|
9957
10200
|
async refreshTokens(refreshToken) {
|
|
9958
10201
|
const config2 = getKakaoConfig();
|
|
@@ -9997,16 +10240,20 @@ registerOAuthProvider(kakaoProvider);
|
|
|
9997
10240
|
|
|
9998
10241
|
// src/server/lib/oauth/naver-provider.ts
|
|
9999
10242
|
init_config();
|
|
10000
|
-
import { ValidationError as
|
|
10243
|
+
import { ValidationError as ValidationError7 } from "@spfn/core/errors";
|
|
10244
|
+
import { NativeSignInUnsupportedError as NativeSignInUnsupportedError4 } from "@spfn/auth/errors";
|
|
10001
10245
|
import { createDecipheriv, createHash as createHash3, createHmac, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
10002
10246
|
var NAVER_AUTH_URL = "https://nid.naver.com/oauth2.0/authorize";
|
|
10003
10247
|
var NAVER_TOKEN_URL = "https://nid.naver.com/oauth2.0/token";
|
|
10004
10248
|
var NAVER_USERINFO_URL = "https://openapi.naver.com/v1/nid/me";
|
|
10249
|
+
var NAVER_ISSUER = "https://nid.naver.com";
|
|
10250
|
+
var NAVER_JWKS_URI = "https://nid.naver.com/oauth2/jwks";
|
|
10251
|
+
var USERINFO_TIMEOUT_MS2 = 5e3;
|
|
10005
10252
|
function getNaverConfig() {
|
|
10006
10253
|
const clientId = env3.SPFN_AUTH_NAVER_CLIENT_ID;
|
|
10007
10254
|
const clientSecret = env3.SPFN_AUTH_NAVER_CLIENT_SECRET;
|
|
10008
10255
|
if (!clientId || !clientSecret) {
|
|
10009
|
-
throw new
|
|
10256
|
+
throw new ValidationError7({
|
|
10010
10257
|
message: "Naver OAuth is not configured. Set SPFN_AUTH_NAVER_CLIENT_ID and SPFN_AUTH_NAVER_CLIENT_SECRET."
|
|
10011
10258
|
});
|
|
10012
10259
|
}
|
|
@@ -10017,6 +10264,13 @@ function getNaverConfig() {
|
|
|
10017
10264
|
redirectUri: env3.SPFN_AUTH_NAVER_REDIRECT_URI || `${baseUrl}/_auth/oauth/naver/callback`
|
|
10018
10265
|
};
|
|
10019
10266
|
}
|
|
10267
|
+
function getNaverNativeAudiences() {
|
|
10268
|
+
const ids = (env3.SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
10269
|
+
if (env3.SPFN_AUTH_NAVER_CLIENT_ID) {
|
|
10270
|
+
ids.push(env3.SPFN_AUTH_NAVER_CLIENT_ID);
|
|
10271
|
+
}
|
|
10272
|
+
return ids;
|
|
10273
|
+
}
|
|
10020
10274
|
async function requestNaverTokens(params) {
|
|
10021
10275
|
const response = await fetch(NAVER_TOKEN_URL, {
|
|
10022
10276
|
method: "POST",
|
|
@@ -10056,6 +10310,53 @@ function decryptNaverUniqueId(encryptUniqueId, key) {
|
|
|
10056
10310
|
return null;
|
|
10057
10311
|
}
|
|
10058
10312
|
}
|
|
10313
|
+
async function fetchNaverIdentity(accessToken) {
|
|
10314
|
+
const response = await fetch(NAVER_USERINFO_URL, {
|
|
10315
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
10316
|
+
signal: AbortSignal.timeout(USERINFO_TIMEOUT_MS2)
|
|
10317
|
+
});
|
|
10318
|
+
if (!response.ok) {
|
|
10319
|
+
throw new Error(`Naver user-info request failed with status ${response.status}`);
|
|
10320
|
+
}
|
|
10321
|
+
const body = await response.json();
|
|
10322
|
+
const profile = body.response;
|
|
10323
|
+
if (body.resultcode !== "00" || typeof profile?.id !== "string") {
|
|
10324
|
+
throw new Error("Naver user-info response is invalid");
|
|
10325
|
+
}
|
|
10326
|
+
return {
|
|
10327
|
+
providerUserId: profile.id,
|
|
10328
|
+
email: typeof profile.email === "string" ? profile.email : null,
|
|
10329
|
+
// 존재하면 검증된 것으로 취급한다. 응답에는 검증 플래그가 없고, 근거는 네이버가
|
|
10330
|
+
// 연락처 이메일을 바꿀 때 새 주소로 보낸 인증번호를 요구한다는 것 하나다. 그래서
|
|
10331
|
+
// 이 값은 소유가 확인된 주소이지 안정적인 식별자는 아니다 — 사용자가 바꿀 수 있고,
|
|
10332
|
+
// 하나의 주소를 최대 6개 아이디가 공유할 수 있으며, 아예 없을 수도 있다.
|
|
10333
|
+
// 계정을 가리키는 유일 키는 providerUserId뿐이다.
|
|
10334
|
+
//
|
|
10335
|
+
// 카카오와 근거가 다르다. 카카오는 응답의 is_email_valid·is_email_verified를
|
|
10336
|
+
// 직접 보고, 네이버는 변경 시점의 인증에 의존한다.
|
|
10337
|
+
emailVerified: typeof profile.email === "string",
|
|
10338
|
+
name: typeof profile.name === "string" ? profile.name : typeof profile.nickname === "string" ? profile.nickname : void 0,
|
|
10339
|
+
avatar: typeof profile.profile_image === "string" ? profile.profile_image : void 0
|
|
10340
|
+
};
|
|
10341
|
+
}
|
|
10342
|
+
async function withNaverProfile(identity, accessToken) {
|
|
10343
|
+
const fetched = await fetchNaverIdentity(accessToken).catch((err) => {
|
|
10344
|
+
authLogger.service.warn("Naver user-info lookup failed; continuing without email", {
|
|
10345
|
+
reason: err instanceof Error ? err.message : "unknown"
|
|
10346
|
+
});
|
|
10347
|
+
return null;
|
|
10348
|
+
});
|
|
10349
|
+
if (!fetched) {
|
|
10350
|
+
return identity;
|
|
10351
|
+
}
|
|
10352
|
+
if (fetched.providerUserId !== identity.providerUserId) {
|
|
10353
|
+
authLogger.service.warn("Naver access token belongs to another user; ignoring it", {
|
|
10354
|
+
subject: identity.providerUserId
|
|
10355
|
+
});
|
|
10356
|
+
return identity;
|
|
10357
|
+
}
|
|
10358
|
+
return fetched;
|
|
10359
|
+
}
|
|
10059
10360
|
var naverProvider = {
|
|
10060
10361
|
id: "naver",
|
|
10061
10362
|
isEnabled() {
|
|
@@ -10083,26 +10384,39 @@ var naverProvider = {
|
|
|
10083
10384
|
}));
|
|
10084
10385
|
},
|
|
10085
10386
|
async getUserInfo(accessToken) {
|
|
10086
|
-
|
|
10087
|
-
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10387
|
+
return fetchNaverIdentity(accessToken);
|
|
10388
|
+
},
|
|
10389
|
+
/**
|
|
10390
|
+
* 네이티브/웹 SDK가 받은 네이버 id_token을 검증한다.
|
|
10391
|
+
*
|
|
10392
|
+
* 표준 OIDC라 nonce는 raw 그대로 대조한다(Apple 같은 해싱이 없다). 다만 네이버는
|
|
10393
|
+
* nonce의 끝 문자가 A일 때 그것을 떨어뜨려 돌려주므로, 클라이언트는 nonce를 소문자
|
|
10394
|
+
* hex로 만들어야 한다 — 문자집합에 A가 없어야 이 경우를 만나지 않는다. base64url은
|
|
10395
|
+
* 16바이트 값의 끝이 A·Q·g·w 중 하나라 4번에 1번, 대문자 hex는 16번에 1번 걸린다.
|
|
10396
|
+
*
|
|
10397
|
+
* id_token에는 이메일도 프로필도 없다. 이메일은 access token이 함께 왔을 때만 얻는다.
|
|
10398
|
+
*/
|
|
10399
|
+
async verifyNativeIdToken(idToken, options) {
|
|
10400
|
+
const audiences = getNaverNativeAudiences();
|
|
10401
|
+
if (audiences.length === 0) {
|
|
10402
|
+
throw new NativeSignInUnsupportedError4({
|
|
10403
|
+
message: "Naver native sign-in is not configured. Set SPFN_AUTH_NAVER_NATIVE_CLIENT_IDS."
|
|
10404
|
+
});
|
|
10096
10405
|
}
|
|
10097
|
-
|
|
10098
|
-
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10103
|
-
|
|
10104
|
-
|
|
10406
|
+
const payload = await verifyIdToken({
|
|
10407
|
+
idToken,
|
|
10408
|
+
jwksUri: NAVER_JWKS_URI,
|
|
10409
|
+
issuer: NAVER_ISSUER,
|
|
10410
|
+
audiences,
|
|
10411
|
+
algorithms: ["RS256"],
|
|
10412
|
+
expectedNonce: options.nonce
|
|
10413
|
+
});
|
|
10414
|
+
const identity = {
|
|
10415
|
+
providerUserId: payload.sub,
|
|
10416
|
+
email: null,
|
|
10417
|
+
emailVerified: false
|
|
10105
10418
|
};
|
|
10419
|
+
return options.accessToken ? withNaverProfile(identity, options.accessToken) : identity;
|
|
10106
10420
|
},
|
|
10107
10421
|
async refreshTokens(refreshToken) {
|
|
10108
10422
|
const config2 = getNaverConfig();
|
|
@@ -10160,12 +10474,12 @@ registerOAuthProvider(naverProvider);
|
|
|
10160
10474
|
function requireEnabledProvider(provider) {
|
|
10161
10475
|
const oauthProvider = getOAuthProvider(provider);
|
|
10162
10476
|
if (!oauthProvider) {
|
|
10163
|
-
throw new
|
|
10477
|
+
throw new ValidationError8({
|
|
10164
10478
|
message: `Unsupported OAuth provider: ${provider}. No provider is registered for this id.`
|
|
10165
10479
|
});
|
|
10166
10480
|
}
|
|
10167
10481
|
if (!oauthProvider.isEnabled()) {
|
|
10168
|
-
throw new
|
|
10482
|
+
throw new ValidationError8({
|
|
10169
10483
|
message: `OAuth provider '${provider}' is registered but not configured. Check its required environment variables.`
|
|
10170
10484
|
});
|
|
10171
10485
|
}
|
|
@@ -10173,7 +10487,7 @@ function requireEnabledProvider(provider) {
|
|
|
10173
10487
|
}
|
|
10174
10488
|
function tokenExpiryDate(expiresIn) {
|
|
10175
10489
|
if (!Number.isFinite(expiresIn)) {
|
|
10176
|
-
throw new
|
|
10490
|
+
throw new ValidationError8({
|
|
10177
10491
|
message: `Invalid token expiry returned from OAuth provider: ${expiresIn}`
|
|
10178
10492
|
});
|
|
10179
10493
|
}
|
|
@@ -10199,12 +10513,12 @@ async function oauthCallbackService(params) {
|
|
|
10199
10513
|
const stateData = await verifyOAuthState(state);
|
|
10200
10514
|
const nonceCandidates = typeof expectedNonce === "string" ? [expectedNonce] : expectedNonce ?? [];
|
|
10201
10515
|
if (!stateData.nonce || !nonceCandidates.includes(stateData.nonce)) {
|
|
10202
|
-
throw new
|
|
10516
|
+
throw new ValidationError8({
|
|
10203
10517
|
message: "OAuth state validation failed"
|
|
10204
10518
|
});
|
|
10205
10519
|
}
|
|
10206
10520
|
if (stateData.provider !== provider) {
|
|
10207
|
-
throw new
|
|
10521
|
+
throw new ValidationError8({
|
|
10208
10522
|
message: "OAuth state provider mismatch"
|
|
10209
10523
|
});
|
|
10210
10524
|
}
|
|
@@ -10271,7 +10585,7 @@ async function oauthCallbackService(params) {
|
|
|
10271
10585
|
async function assertActiveForOAuthSession(userId) {
|
|
10272
10586
|
const user = await usersRepository.findByIdOnPrimary(userId);
|
|
10273
10587
|
if (!user) {
|
|
10274
|
-
throw new
|
|
10588
|
+
throw new ValidationError8({ message: "User not found" });
|
|
10275
10589
|
}
|
|
10276
10590
|
if (user.status === "active") {
|
|
10277
10591
|
return;
|
|
@@ -10312,9 +10626,7 @@ async function createOrLinkUser(provider, identity, tokens, metadata) {
|
|
|
10312
10626
|
let isNewUser = false;
|
|
10313
10627
|
if (existingUser) {
|
|
10314
10628
|
if (!identity.emailVerified) {
|
|
10315
|
-
throw new
|
|
10316
|
-
message: "Cannot link to existing account with unverified email. Please verify your email with the provider first."
|
|
10317
|
-
});
|
|
10629
|
+
throw new UnverifiedEmailLinkError();
|
|
10318
10630
|
}
|
|
10319
10631
|
userId = existingUser.id;
|
|
10320
10632
|
if (!existingUser.emailVerifiedAt) {
|
|
@@ -10383,16 +10695,16 @@ var TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1e3;
|
|
|
10383
10695
|
async function getGoogleAccessToken(userId) {
|
|
10384
10696
|
const account = await socialAccountsRepository.findByUserIdAndProvider(userId, "google");
|
|
10385
10697
|
if (!account) {
|
|
10386
|
-
throw new
|
|
10698
|
+
throw new ValidationError8({
|
|
10387
10699
|
message: "No Google account linked. User must sign in with Google first."
|
|
10388
10700
|
});
|
|
10389
10701
|
}
|
|
10390
|
-
const
|
|
10391
|
-
if (!
|
|
10702
|
+
const isExpired2 = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
|
|
10703
|
+
if (!isExpired2 && account.accessToken) {
|
|
10392
10704
|
return account.accessToken;
|
|
10393
10705
|
}
|
|
10394
10706
|
if (!account.refreshToken) {
|
|
10395
|
-
throw new
|
|
10707
|
+
throw new ValidationError8({
|
|
10396
10708
|
message: "Google refresh token not available. User must re-authenticate with Google."
|
|
10397
10709
|
});
|
|
10398
10710
|
}
|
|
@@ -10425,22 +10737,38 @@ async function oauthUnlinkNotifyService(provider, notification) {
|
|
|
10425
10737
|
}
|
|
10426
10738
|
|
|
10427
10739
|
// src/server/services/oauth-native.service.ts
|
|
10428
|
-
init_repositories();
|
|
10429
|
-
import { ValidationError as ValidationError10 } from "@spfn/core/errors";
|
|
10430
10740
|
import { runInTransaction as runInTransaction2, onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
|
|
10741
|
+
import {
|
|
10742
|
+
InvalidKeyFingerprintError as InvalidKeyFingerprintError2,
|
|
10743
|
+
NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
|
|
10744
|
+
NonceKeyBindingError
|
|
10745
|
+
} from "@spfn/auth/errors";
|
|
10746
|
+
init_repositories();
|
|
10431
10747
|
async function oauthNativeService(params) {
|
|
10432
10748
|
const oauthProvider = getOAuthProvider(params.provider);
|
|
10433
10749
|
if (!oauthProvider?.verifyNativeIdToken) {
|
|
10434
|
-
throw new
|
|
10750
|
+
throw new NativeSignInUnsupportedError5({
|
|
10435
10751
|
message: `Provider '${params.provider}' does not support native id_token sign-in.`
|
|
10436
10752
|
});
|
|
10437
10753
|
}
|
|
10438
|
-
|
|
10754
|
+
assertNonceBindsPublicKey(params);
|
|
10755
|
+
const identity = await oauthProvider.verifyNativeIdToken(params.idToken, {
|
|
10756
|
+
nonce: params.nonce,
|
|
10757
|
+
accessToken: params.accessToken
|
|
10758
|
+
});
|
|
10439
10759
|
if (params.profile?.name && !identity.name) {
|
|
10440
10760
|
identity.name = params.profile.name;
|
|
10441
10761
|
}
|
|
10442
10762
|
return persistNativeLogin(identity, params);
|
|
10443
10763
|
}
|
|
10764
|
+
function assertNonceBindsPublicKey(params) {
|
|
10765
|
+
if (params.nonce !== params.fingerprint) {
|
|
10766
|
+
throw new NonceKeyBindingError();
|
|
10767
|
+
}
|
|
10768
|
+
if (!verifyKeyFingerprint(params.publicKey, params.fingerprint)) {
|
|
10769
|
+
throw new InvalidKeyFingerprintError2();
|
|
10770
|
+
}
|
|
10771
|
+
}
|
|
10444
10772
|
async function persistNativeLogin(identity, params) {
|
|
10445
10773
|
return runInTransaction2(async () => {
|
|
10446
10774
|
const existing = await socialAccountsRepository.findByProviderAndProviderId(
|
|
@@ -10463,7 +10791,9 @@ async function persistNativeLogin(identity, params) {
|
|
|
10463
10791
|
keyId: params.keyId,
|
|
10464
10792
|
publicKey: params.publicKey,
|
|
10465
10793
|
fingerprint: params.fingerprint,
|
|
10466
|
-
algorithm: params.algorithm
|
|
10794
|
+
algorithm: params.algorithm,
|
|
10795
|
+
deviceName: params.deviceName,
|
|
10796
|
+
platform: params.platform
|
|
10467
10797
|
});
|
|
10468
10798
|
await updateLastLoginService(userId);
|
|
10469
10799
|
const eventPayload = {
|
|
@@ -10483,6 +10813,7 @@ import { Transactional } from "@spfn/core/db";
|
|
|
10483
10813
|
import { rateLimitPolicy } from "@spfn/core/middleware";
|
|
10484
10814
|
|
|
10485
10815
|
// src/server/lib/rate-limit-keys.ts
|
|
10816
|
+
import { createHash as createHash4 } from "crypto";
|
|
10486
10817
|
import { getClientIp } from "@spfn/core/middleware";
|
|
10487
10818
|
async function readJsonBody(c) {
|
|
10488
10819
|
try {
|
|
@@ -10518,6 +10849,21 @@ function byIpAndAccount(options = {}) {
|
|
|
10518
10849
|
];
|
|
10519
10850
|
};
|
|
10520
10851
|
}
|
|
10852
|
+
function idTokenKey(body) {
|
|
10853
|
+
if (typeof body.idToken !== "string" || !body.idToken) {
|
|
10854
|
+
return void 0;
|
|
10855
|
+
}
|
|
10856
|
+
return `tok:${createHash4("sha256").update(body.idToken).digest("hex")}`;
|
|
10857
|
+
}
|
|
10858
|
+
function byIpAndIdToken(options = {}) {
|
|
10859
|
+
return async (c) => {
|
|
10860
|
+
const body = await readJsonBody(c);
|
|
10861
|
+
return [
|
|
10862
|
+
{ key: `ip:${getClientIp(c)}`, limit: options.ipLimit },
|
|
10863
|
+
idTokenKey(body)
|
|
10864
|
+
];
|
|
10865
|
+
};
|
|
10866
|
+
}
|
|
10521
10867
|
function byIpAndTarget(options = {}) {
|
|
10522
10868
|
return async (c) => {
|
|
10523
10869
|
const body = await readJsonBody(c);
|
|
@@ -10582,7 +10928,9 @@ var register = route.post("/_auth/register").input({
|
|
|
10582
10928
|
publicKey: Type.String({ description: "Client public key" }),
|
|
10583
10929
|
keyId: Type.String({ description: "Key identifier" }),
|
|
10584
10930
|
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
10585
|
-
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
|
|
10931
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
10932
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
10933
|
+
platform: Type.Optional(PlatformSchema)
|
|
10586
10934
|
})
|
|
10587
10935
|
}).use([rateLimitPolicy("auth-register", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
10588
10936
|
const { body } = await c.data();
|
|
@@ -10607,7 +10955,9 @@ var login = route.post("/_auth/login").input({
|
|
|
10607
10955
|
keyId: Type.String({ description: "Key identifier" }),
|
|
10608
10956
|
fingerprint: Type.String({ description: "Key fingerprint" }),
|
|
10609
10957
|
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
10610
|
-
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" }))
|
|
10958
|
+
oldKeyId: Type.Optional(Type.String({ description: "Previous key ID for rotation" })),
|
|
10959
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
10960
|
+
platform: Type.Optional(PlatformSchema)
|
|
10611
10961
|
})
|
|
10612
10962
|
}).use([rateLimitPolicy("auth-login", { limit: 10, windowMs: 6e4, by: byIpAndAccount({ ipLimit: 100 }) }), Transactional()]).skip(["auth"]).handler(async (c) => {
|
|
10613
10963
|
const { body } = await c.data();
|
|
@@ -10627,7 +10977,9 @@ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
|
10627
10977
|
publicKey: Type.String({ description: "New public key" }),
|
|
10628
10978
|
keyId: Type.String({ description: "New key identifier" }),
|
|
10629
10979
|
fingerprint: Type.String({ description: "New key fingerprint" }),
|
|
10630
|
-
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" })
|
|
10980
|
+
algorithm: Type.Union(KEY_ALGORITHM.map((algo) => Type.Literal(algo)), { description: "Signature algorithm" }),
|
|
10981
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
10982
|
+
platform: Type.Optional(PlatformSchema)
|
|
10631
10983
|
})
|
|
10632
10984
|
}).use([Transactional()]).handler(async (c) => {
|
|
10633
10985
|
const { body } = await c.data();
|
|
@@ -10638,33 +10990,79 @@ var rotateKey = route.post("/_auth/keys/rotate").interceptor({
|
|
|
10638
10990
|
newKeyId: body.keyId,
|
|
10639
10991
|
newPublicKey: body.publicKey,
|
|
10640
10992
|
fingerprint: body.fingerprint,
|
|
10641
|
-
algorithm: body.algorithm
|
|
10993
|
+
algorithm: body.algorithm,
|
|
10994
|
+
deviceName: body.deviceName,
|
|
10995
|
+
platform: body.platform
|
|
10642
10996
|
});
|
|
10643
10997
|
});
|
|
10644
|
-
var
|
|
10998
|
+
var listKeys = route.post("/_auth/keys/list").input({
|
|
10645
10999
|
body: Type.Object({
|
|
10646
|
-
|
|
10647
|
-
|
|
10648
|
-
|
|
10649
|
-
})),
|
|
10650
|
-
newPassword: PasswordSchema
|
|
11000
|
+
includeRevoked: Type.Optional(Type.Boolean({
|
|
11001
|
+
description: "Also return keys already revoked, for reviewing what was cut off. Default false."
|
|
11002
|
+
}))
|
|
10651
11003
|
})
|
|
10652
|
-
}).
|
|
11004
|
+
}).handler(async (c) => {
|
|
10653
11005
|
const { body } = await c.data();
|
|
10654
|
-
const user = getUser(c);
|
|
10655
|
-
await changePasswordService({
|
|
10656
|
-
userId: user.id,
|
|
10657
|
-
currentPassword: body.currentPassword,
|
|
10658
|
-
newPassword: body.newPassword,
|
|
10659
|
-
passwordHash: user.passwordHash || void 0
|
|
10660
|
-
});
|
|
10661
|
-
return c.noContent();
|
|
10662
|
-
});
|
|
10663
|
-
var getAuthSession = route.get("/_auth/session").handler(async (c) => {
|
|
10664
11006
|
const { userId } = getAuth(c);
|
|
10665
|
-
return await
|
|
11007
|
+
return { keys: await listKeysService({ userId: Number(userId), includeRevoked: body.includeRevoked }) };
|
|
10666
11008
|
});
|
|
10667
|
-
var
|
|
11009
|
+
var revokeKey = route.post("/_auth/keys/revoke").input({
|
|
11010
|
+
body: Type.Object({
|
|
11011
|
+
keyId: Type.String({ description: "Key to revoke \u2014 must belong to the caller" })
|
|
11012
|
+
})
|
|
11013
|
+
}).use([Transactional()]).handler(async (c) => {
|
|
11014
|
+
const { body } = await c.data();
|
|
11015
|
+
const { userId, keyId: currentKeyId } = getAuth(c);
|
|
11016
|
+
const revoked = await revokeKeyService({
|
|
11017
|
+
userId: Number(userId),
|
|
11018
|
+
keyId: body.keyId,
|
|
11019
|
+
reason: "Revoked by user"
|
|
11020
|
+
});
|
|
11021
|
+
if (!revoked) {
|
|
11022
|
+
throw new KeyNotFoundError();
|
|
11023
|
+
}
|
|
11024
|
+
return { keyId: body.keyId, selfRevoked: body.keyId === currentKeyId };
|
|
11025
|
+
});
|
|
11026
|
+
var revokeAllKeys = route.post("/_auth/keys/revoke-all").input({
|
|
11027
|
+
body: Type.Object({
|
|
11028
|
+
includeCurrent: Type.Optional(Type.Boolean({
|
|
11029
|
+
description: "Also revoke the key this request is signed with. Default false."
|
|
11030
|
+
}))
|
|
11031
|
+
})
|
|
11032
|
+
}).use([Transactional()]).handler(async (c) => {
|
|
11033
|
+
const { body } = await c.data();
|
|
11034
|
+
const { userId, keyId } = getAuth(c);
|
|
11035
|
+
return await revokeAllKeysService({
|
|
11036
|
+
userId: Number(userId),
|
|
11037
|
+
currentKeyId: keyId,
|
|
11038
|
+
includeCurrent: body.includeCurrent,
|
|
11039
|
+
reason: "Revoked by user"
|
|
11040
|
+
});
|
|
11041
|
+
});
|
|
11042
|
+
var changePassword = route.put("/_auth/password").input({
|
|
11043
|
+
body: Type.Object({
|
|
11044
|
+
currentPassword: Type.Optional(Type.String({
|
|
11045
|
+
minLength: 1,
|
|
11046
|
+
description: "Current password for verification (required when changing existing password)"
|
|
11047
|
+
})),
|
|
11048
|
+
newPassword: PasswordSchema
|
|
11049
|
+
})
|
|
11050
|
+
}).use([rateLimitPolicy("auth-password-change", { limit: 10, windowMs: 6e4 })]).handler(async (c) => {
|
|
11051
|
+
const { body } = await c.data();
|
|
11052
|
+
const user = getUser(c);
|
|
11053
|
+
await changePasswordService({
|
|
11054
|
+
userId: user.id,
|
|
11055
|
+
currentPassword: body.currentPassword,
|
|
11056
|
+
newPassword: body.newPassword,
|
|
11057
|
+
passwordHash: user.passwordHash || void 0
|
|
11058
|
+
});
|
|
11059
|
+
return c.noContent();
|
|
11060
|
+
});
|
|
11061
|
+
var getAuthSession = route.get("/_auth/session").handler(async (c) => {
|
|
11062
|
+
const { userId } = getAuth(c);
|
|
11063
|
+
return await getAuthSessionService(userId);
|
|
11064
|
+
});
|
|
11065
|
+
var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
|
|
10668
11066
|
const { userId } = getAuth(c);
|
|
10669
11067
|
return await issueOneTimeTokenService(userId);
|
|
10670
11068
|
});
|
|
@@ -10675,6 +11073,9 @@ var authRouter = defineRouter({
|
|
|
10675
11073
|
login,
|
|
10676
11074
|
logout,
|
|
10677
11075
|
rotateKey,
|
|
11076
|
+
listKeys,
|
|
11077
|
+
revokeKey,
|
|
11078
|
+
revokeAllKeys,
|
|
10678
11079
|
changePassword,
|
|
10679
11080
|
getAuthSession,
|
|
10680
11081
|
issueOneTimeToken
|
|
@@ -10685,33 +11086,831 @@ import { EMAIL_PATTERN as EMAIL_PATTERN2, UUID_PATTERN } from "@spfn/auth";
|
|
|
10685
11086
|
|
|
10686
11087
|
// src/server/middleware/authenticate.ts
|
|
10687
11088
|
import { defineMiddleware } from "@spfn/core/route";
|
|
10688
|
-
import { UnauthorizedError } from "@spfn/core/errors";
|
|
10689
|
-
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as
|
|
11089
|
+
import { UnauthorizedError as UnauthorizedError2 } from "@spfn/core/errors";
|
|
11090
|
+
import { verifyClientToken as verifyClientToken2, decodeToken as decodeToken2, authLogger as authLogger3, keysRepository as keysRepository3, usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
|
|
10690
11091
|
import {
|
|
10691
11092
|
InvalidTokenError,
|
|
10692
11093
|
TokenExpiredError,
|
|
10693
|
-
KeyExpiredError
|
|
10694
|
-
AccountDisabledError as AccountDisabledError3,
|
|
10695
|
-
AccountPendingDeletionError as AccountPendingDeletionError3
|
|
11094
|
+
KeyExpiredError
|
|
10696
11095
|
} from "@spfn/auth/errors";
|
|
11096
|
+
|
|
11097
|
+
// src/server/middleware/auth-profiles.ts
|
|
11098
|
+
import {
|
|
11099
|
+
BadRequestError as BadRequestError2,
|
|
11100
|
+
ConflictError as ConflictError2,
|
|
11101
|
+
ServiceUnavailableError,
|
|
11102
|
+
UnauthorizedError
|
|
11103
|
+
} from "@spfn/core/errors";
|
|
11104
|
+
import {
|
|
11105
|
+
authLogger as authLogger2,
|
|
11106
|
+
keysRepository as keysRepository2,
|
|
11107
|
+
usersRepository as usersRepository2,
|
|
11108
|
+
userProfilesRepository as userProfilesRepository2,
|
|
11109
|
+
getPendingDeletionInfo as getPendingDeletionInfo2
|
|
11110
|
+
} from "@spfn/auth/server";
|
|
11111
|
+
import { AccountDisabledError as AccountDisabledError3, AccountPendingDeletionError as AccountPendingDeletionError3 } from "@spfn/auth/errors";
|
|
11112
|
+
|
|
11113
|
+
// src/server/client-proof/canonical-json.ts
|
|
11114
|
+
var CanonicalJsonError = class extends Error {
|
|
11115
|
+
constructor(code) {
|
|
11116
|
+
super(`canonical JSON: ${code}`);
|
|
11117
|
+
this.code = code;
|
|
11118
|
+
this.name = "CanonicalJsonError";
|
|
11119
|
+
}
|
|
11120
|
+
};
|
|
11121
|
+
var INT64_MIN = -(2n ** 63n);
|
|
11122
|
+
var INT64_MAX = 2n ** 63n - 1n;
|
|
11123
|
+
function parseCanonicalJson(bytes) {
|
|
11124
|
+
let text12;
|
|
11125
|
+
try {
|
|
11126
|
+
text12 = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
11127
|
+
} catch {
|
|
11128
|
+
throw new CanonicalJsonError("INVALID_UTF8");
|
|
11129
|
+
}
|
|
11130
|
+
const parser = new Parser(text12);
|
|
11131
|
+
const value = parser.parseValue();
|
|
11132
|
+
parser.skipWhitespace();
|
|
11133
|
+
if (!parser.atEnd()) {
|
|
11134
|
+
throw new CanonicalJsonError("TRAILING_CONTENT");
|
|
11135
|
+
}
|
|
11136
|
+
return value;
|
|
11137
|
+
}
|
|
11138
|
+
function isCanonicalBytes(bytes, value) {
|
|
11139
|
+
const encoded = encodeCanonicalJson(value);
|
|
11140
|
+
if (encoded.length !== bytes.length) {
|
|
11141
|
+
return false;
|
|
11142
|
+
}
|
|
11143
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
11144
|
+
if (encoded[i] !== bytes[i]) {
|
|
11145
|
+
return false;
|
|
11146
|
+
}
|
|
11147
|
+
}
|
|
11148
|
+
return true;
|
|
11149
|
+
}
|
|
11150
|
+
var Parser = class {
|
|
11151
|
+
constructor(text12) {
|
|
11152
|
+
this.text = text12;
|
|
11153
|
+
}
|
|
11154
|
+
pos = 0;
|
|
11155
|
+
atEnd() {
|
|
11156
|
+
return this.pos >= this.text.length;
|
|
11157
|
+
}
|
|
11158
|
+
skipWhitespace() {
|
|
11159
|
+
while (!this.atEnd()) {
|
|
11160
|
+
const c = this.text[this.pos];
|
|
11161
|
+
if (c === " " || c === " " || c === "\n" || c === "\r") {
|
|
11162
|
+
this.pos++;
|
|
11163
|
+
continue;
|
|
11164
|
+
}
|
|
11165
|
+
break;
|
|
11166
|
+
}
|
|
11167
|
+
}
|
|
11168
|
+
parseValue() {
|
|
11169
|
+
this.skipWhitespace();
|
|
11170
|
+
if (this.atEnd()) {
|
|
11171
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11172
|
+
}
|
|
11173
|
+
const c = this.text[this.pos];
|
|
11174
|
+
if (c === "{") {
|
|
11175
|
+
return this.parseObject();
|
|
11176
|
+
}
|
|
11177
|
+
if (c === "[") {
|
|
11178
|
+
return this.parseArray();
|
|
11179
|
+
}
|
|
11180
|
+
if (c === '"') {
|
|
11181
|
+
return this.parseString();
|
|
11182
|
+
}
|
|
11183
|
+
if (c === "-" || c >= "0" && c <= "9") {
|
|
11184
|
+
return this.parseNumber();
|
|
11185
|
+
}
|
|
11186
|
+
if (this.text.startsWith("null", this.pos)) {
|
|
11187
|
+
this.pos += 4;
|
|
11188
|
+
return null;
|
|
11189
|
+
}
|
|
11190
|
+
if (this.text.startsWith("true", this.pos)) {
|
|
11191
|
+
this.pos += 4;
|
|
11192
|
+
return true;
|
|
11193
|
+
}
|
|
11194
|
+
if (this.text.startsWith("false", this.pos)) {
|
|
11195
|
+
this.pos += 5;
|
|
11196
|
+
return false;
|
|
11197
|
+
}
|
|
11198
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11199
|
+
}
|
|
11200
|
+
parseObject() {
|
|
11201
|
+
this.pos++;
|
|
11202
|
+
const members = /* @__PURE__ */ new Map();
|
|
11203
|
+
this.skipWhitespace();
|
|
11204
|
+
if (this.atEnd()) {
|
|
11205
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11206
|
+
}
|
|
11207
|
+
if (this.text[this.pos] === "}") {
|
|
11208
|
+
this.pos++;
|
|
11209
|
+
return members;
|
|
11210
|
+
}
|
|
11211
|
+
for (; ; ) {
|
|
11212
|
+
this.skipWhitespace();
|
|
11213
|
+
if (this.atEnd()) {
|
|
11214
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11215
|
+
}
|
|
11216
|
+
if (this.text[this.pos] !== '"') {
|
|
11217
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11218
|
+
}
|
|
11219
|
+
const key = this.parseString();
|
|
11220
|
+
if (members.has(key)) {
|
|
11221
|
+
throw new CanonicalJsonError("DUPLICATE_KEY");
|
|
11222
|
+
}
|
|
11223
|
+
this.skipWhitespace();
|
|
11224
|
+
if (this.atEnd()) {
|
|
11225
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11226
|
+
}
|
|
11227
|
+
if (this.text[this.pos] !== ":") {
|
|
11228
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11229
|
+
}
|
|
11230
|
+
this.pos++;
|
|
11231
|
+
members.set(key, this.parseValue());
|
|
11232
|
+
this.skipWhitespace();
|
|
11233
|
+
if (this.atEnd()) {
|
|
11234
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11235
|
+
}
|
|
11236
|
+
const next = this.text[this.pos];
|
|
11237
|
+
if (next === ",") {
|
|
11238
|
+
this.pos++;
|
|
11239
|
+
continue;
|
|
11240
|
+
}
|
|
11241
|
+
if (next === "}") {
|
|
11242
|
+
this.pos++;
|
|
11243
|
+
return members;
|
|
11244
|
+
}
|
|
11245
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11246
|
+
}
|
|
11247
|
+
}
|
|
11248
|
+
parseArray() {
|
|
11249
|
+
this.pos++;
|
|
11250
|
+
const items = [];
|
|
11251
|
+
this.skipWhitespace();
|
|
11252
|
+
if (this.atEnd()) {
|
|
11253
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11254
|
+
}
|
|
11255
|
+
if (this.text[this.pos] === "]") {
|
|
11256
|
+
this.pos++;
|
|
11257
|
+
return items;
|
|
11258
|
+
}
|
|
11259
|
+
for (; ; ) {
|
|
11260
|
+
items.push(this.parseValue());
|
|
11261
|
+
this.skipWhitespace();
|
|
11262
|
+
if (this.atEnd()) {
|
|
11263
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11264
|
+
}
|
|
11265
|
+
const next = this.text[this.pos];
|
|
11266
|
+
if (next === ",") {
|
|
11267
|
+
this.pos++;
|
|
11268
|
+
continue;
|
|
11269
|
+
}
|
|
11270
|
+
if (next === "]") {
|
|
11271
|
+
this.pos++;
|
|
11272
|
+
return items;
|
|
11273
|
+
}
|
|
11274
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11275
|
+
}
|
|
11276
|
+
}
|
|
11277
|
+
parseString() {
|
|
11278
|
+
this.pos++;
|
|
11279
|
+
let out = "";
|
|
11280
|
+
for (; ; ) {
|
|
11281
|
+
if (this.atEnd()) {
|
|
11282
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11283
|
+
}
|
|
11284
|
+
const c = this.text[this.pos];
|
|
11285
|
+
const code = this.text.charCodeAt(this.pos);
|
|
11286
|
+
if (c === '"') {
|
|
11287
|
+
this.pos++;
|
|
11288
|
+
return out;
|
|
11289
|
+
}
|
|
11290
|
+
if (c === "\\") {
|
|
11291
|
+
out += this.parseEscape();
|
|
11292
|
+
continue;
|
|
11293
|
+
}
|
|
11294
|
+
if (code < 32) {
|
|
11295
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11296
|
+
}
|
|
11297
|
+
out += c;
|
|
11298
|
+
this.pos++;
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
parseEscape() {
|
|
11302
|
+
this.pos++;
|
|
11303
|
+
if (this.atEnd()) {
|
|
11304
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11305
|
+
}
|
|
11306
|
+
const c = this.text[this.pos];
|
|
11307
|
+
this.pos++;
|
|
11308
|
+
switch (c) {
|
|
11309
|
+
case '"':
|
|
11310
|
+
return '"';
|
|
11311
|
+
case "\\":
|
|
11312
|
+
return "\\";
|
|
11313
|
+
case "/":
|
|
11314
|
+
return "/";
|
|
11315
|
+
case "b":
|
|
11316
|
+
return "\b";
|
|
11317
|
+
case "f":
|
|
11318
|
+
return "\f";
|
|
11319
|
+
case "n":
|
|
11320
|
+
return "\n";
|
|
11321
|
+
case "r":
|
|
11322
|
+
return "\r";
|
|
11323
|
+
case "t":
|
|
11324
|
+
return " ";
|
|
11325
|
+
case "u":
|
|
11326
|
+
return this.parseUnicodeEscape();
|
|
11327
|
+
default:
|
|
11328
|
+
throw new CanonicalJsonError("INVALID_ESCAPE");
|
|
11329
|
+
}
|
|
11330
|
+
}
|
|
11331
|
+
parseUnicodeEscape() {
|
|
11332
|
+
const high = this.readHex4();
|
|
11333
|
+
if (high >= 56320 && high <= 57343) {
|
|
11334
|
+
throw new CanonicalJsonError("INVALID_ESCAPE");
|
|
11335
|
+
}
|
|
11336
|
+
if (high < 55296 || high > 56319) {
|
|
11337
|
+
return String.fromCharCode(high);
|
|
11338
|
+
}
|
|
11339
|
+
if (this.text[this.pos] !== "\\" || this.text[this.pos + 1] !== "u") {
|
|
11340
|
+
throw new CanonicalJsonError("INVALID_ESCAPE");
|
|
11341
|
+
}
|
|
11342
|
+
this.pos += 2;
|
|
11343
|
+
const low = this.readHex4();
|
|
11344
|
+
if (low < 56320 || low > 57343) {
|
|
11345
|
+
throw new CanonicalJsonError("INVALID_ESCAPE");
|
|
11346
|
+
}
|
|
11347
|
+
return String.fromCharCode(high, low);
|
|
11348
|
+
}
|
|
11349
|
+
readHex4() {
|
|
11350
|
+
if (this.pos + 4 > this.text.length) {
|
|
11351
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11352
|
+
}
|
|
11353
|
+
const hex = this.text.slice(this.pos, this.pos + 4);
|
|
11354
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) {
|
|
11355
|
+
throw new CanonicalJsonError("INVALID_ESCAPE");
|
|
11356
|
+
}
|
|
11357
|
+
this.pos += 4;
|
|
11358
|
+
return parseInt(hex, 16);
|
|
11359
|
+
}
|
|
11360
|
+
parseNumber() {
|
|
11361
|
+
const start = this.pos;
|
|
11362
|
+
if (this.text[this.pos] === "-") {
|
|
11363
|
+
this.pos++;
|
|
11364
|
+
}
|
|
11365
|
+
if (this.atEnd()) {
|
|
11366
|
+
throw new CanonicalJsonError("UNEXPECTED_END");
|
|
11367
|
+
}
|
|
11368
|
+
const first = this.text[this.pos];
|
|
11369
|
+
if (first < "0" || first > "9") {
|
|
11370
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11371
|
+
}
|
|
11372
|
+
if (first === "0") {
|
|
11373
|
+
this.pos++;
|
|
11374
|
+
} else {
|
|
11375
|
+
while (!this.atEnd() && this.text[this.pos] >= "0" && this.text[this.pos] <= "9") {
|
|
11376
|
+
this.pos++;
|
|
11377
|
+
}
|
|
11378
|
+
}
|
|
11379
|
+
if (!this.atEnd()) {
|
|
11380
|
+
const next = this.text[this.pos];
|
|
11381
|
+
if (next >= "0" && next <= "9") {
|
|
11382
|
+
throw new CanonicalJsonError("INVALID_TOKEN");
|
|
11383
|
+
}
|
|
11384
|
+
if (next === "." || next === "e" || next === "E") {
|
|
11385
|
+
throw new CanonicalJsonError("NON_INTEGER_NUMBER");
|
|
11386
|
+
}
|
|
11387
|
+
}
|
|
11388
|
+
const value = BigInt(this.text.slice(start, this.pos));
|
|
11389
|
+
if (value < INT64_MIN || value > INT64_MAX) {
|
|
11390
|
+
throw new CanonicalJsonError("INTEGER_OUT_OF_RANGE");
|
|
11391
|
+
}
|
|
11392
|
+
return value;
|
|
11393
|
+
}
|
|
11394
|
+
};
|
|
11395
|
+
function encodeCanonicalJson(value) {
|
|
11396
|
+
return new TextEncoder().encode(encodeToString(value));
|
|
11397
|
+
}
|
|
11398
|
+
function encodeToString(value) {
|
|
11399
|
+
if (value === null) {
|
|
11400
|
+
return "null";
|
|
11401
|
+
}
|
|
11402
|
+
if (typeof value === "boolean") {
|
|
11403
|
+
return value ? "true" : "false";
|
|
11404
|
+
}
|
|
11405
|
+
if (typeof value === "bigint") {
|
|
11406
|
+
return value.toString();
|
|
11407
|
+
}
|
|
11408
|
+
if (typeof value === "string") {
|
|
11409
|
+
return encodeString(value);
|
|
11410
|
+
}
|
|
11411
|
+
if (Array.isArray(value)) {
|
|
11412
|
+
return `[${value.map(encodeToString).join(",")}]`;
|
|
11413
|
+
}
|
|
11414
|
+
const keys = [...value.keys()].sort(compareByCodePoints);
|
|
11415
|
+
const members = keys.map((key) => `${encodeString(key)}:${encodeToString(value.get(key))}`);
|
|
11416
|
+
return `{${members.join(",")}}`;
|
|
11417
|
+
}
|
|
11418
|
+
function compareByCodePoints(a, b) {
|
|
11419
|
+
let i = 0;
|
|
11420
|
+
let j = 0;
|
|
11421
|
+
while (i < a.length && j < b.length) {
|
|
11422
|
+
const ca = a.codePointAt(i);
|
|
11423
|
+
const cb = b.codePointAt(j);
|
|
11424
|
+
if (ca !== cb) {
|
|
11425
|
+
return ca - cb;
|
|
11426
|
+
}
|
|
11427
|
+
i += ca > 65535 ? 2 : 1;
|
|
11428
|
+
j += cb > 65535 ? 2 : 1;
|
|
11429
|
+
}
|
|
11430
|
+
return a.length - i - (b.length - j);
|
|
11431
|
+
}
|
|
11432
|
+
function encodeString(value) {
|
|
11433
|
+
let out = '"';
|
|
11434
|
+
for (const ch of value) {
|
|
11435
|
+
const code = ch.codePointAt(0);
|
|
11436
|
+
if (ch === '"') {
|
|
11437
|
+
out += '\\"';
|
|
11438
|
+
} else if (ch === "\\") {
|
|
11439
|
+
out += "\\\\";
|
|
11440
|
+
} else if (code === 8) {
|
|
11441
|
+
out += "\\b";
|
|
11442
|
+
} else if (code === 12) {
|
|
11443
|
+
out += "\\f";
|
|
11444
|
+
} else if (code === 10) {
|
|
11445
|
+
out += "\\n";
|
|
11446
|
+
} else if (code === 13) {
|
|
11447
|
+
out += "\\r";
|
|
11448
|
+
} else if (code === 9) {
|
|
11449
|
+
out += "\\t";
|
|
11450
|
+
} else if (code < 32) {
|
|
11451
|
+
out += `\\u00${code.toString(16).padStart(2, "0")}`;
|
|
11452
|
+
} else {
|
|
11453
|
+
out += ch;
|
|
11454
|
+
}
|
|
11455
|
+
}
|
|
11456
|
+
return out + '"';
|
|
11457
|
+
}
|
|
11458
|
+
|
|
11459
|
+
// src/server/client-proof/proof.ts
|
|
11460
|
+
import { createHash as createHash5, createPrivateKey, createPublicKey, sign, verify as verify2 } from "crypto";
|
|
11461
|
+
var CLIENT_PROOF_PROFILE = "clientProofV1";
|
|
11462
|
+
var ABSENT_BODY_SHA256 = "0".repeat(64);
|
|
11463
|
+
var DEFAULT_REPLAY_WINDOW_MILLIS = 3e5;
|
|
11464
|
+
var PROOF_INPUT_FIELDS = [
|
|
11465
|
+
"profile",
|
|
11466
|
+
"method",
|
|
11467
|
+
"path",
|
|
11468
|
+
"clientId",
|
|
11469
|
+
"keyId",
|
|
11470
|
+
"nonce",
|
|
11471
|
+
"issuedAtMillis",
|
|
11472
|
+
"bodySha256"
|
|
11473
|
+
];
|
|
11474
|
+
var PROOF_INPUT_SEPARATOR = "\n";
|
|
11475
|
+
var PROOF_SIGNATURE_BYTES = 64;
|
|
11476
|
+
var PROOF_SIGNATURE_HEX_LENGTH = PROOF_SIGNATURE_BYTES * 2;
|
|
11477
|
+
var PROOF_SIGNATURE_PATTERN = /^[0-9a-f]{128}$/;
|
|
11478
|
+
var RAW_SIGNATURE_ENCODING = "ieee-p1363";
|
|
11479
|
+
var ProofInputError = class extends Error {
|
|
11480
|
+
constructor() {
|
|
11481
|
+
super("proof input field contains a C0 control character");
|
|
11482
|
+
this.name = "ProofInputError";
|
|
11483
|
+
}
|
|
11484
|
+
};
|
|
11485
|
+
function canonicalProofInput(input) {
|
|
11486
|
+
const values = {
|
|
11487
|
+
profile: CLIENT_PROOF_PROFILE,
|
|
11488
|
+
method: input.method,
|
|
11489
|
+
path: input.path,
|
|
11490
|
+
clientId: input.clientId,
|
|
11491
|
+
keyId: input.keyId,
|
|
11492
|
+
nonce: input.nonce,
|
|
11493
|
+
issuedAtMillis: input.issuedAtMillis.toString(),
|
|
11494
|
+
bodySha256: input.bodySha256
|
|
11495
|
+
};
|
|
11496
|
+
const fields = PROOF_INPUT_FIELDS.map((name) => values[name]);
|
|
11497
|
+
for (const field of fields) {
|
|
11498
|
+
for (const ch of field) {
|
|
11499
|
+
if (ch.codePointAt(0) < 32) {
|
|
11500
|
+
throw new ProofInputError();
|
|
11501
|
+
}
|
|
11502
|
+
}
|
|
11503
|
+
}
|
|
11504
|
+
return fields.join(PROOF_INPUT_SEPARATOR);
|
|
11505
|
+
}
|
|
11506
|
+
function parseClientProofPublicKey(spkiDerBase64) {
|
|
11507
|
+
const key = createPublicKey({
|
|
11508
|
+
key: Buffer.from(spkiDerBase64, "base64"),
|
|
11509
|
+
format: "der",
|
|
11510
|
+
type: "spki"
|
|
11511
|
+
});
|
|
11512
|
+
if (key.asymmetricKeyType !== "ec" || key.asymmetricKeyDetails?.namedCurve !== "prime256v1") {
|
|
11513
|
+
throw new Error("a clientProofV1 public key must be an ECDSA P-256 key");
|
|
11514
|
+
}
|
|
11515
|
+
return key;
|
|
11516
|
+
}
|
|
11517
|
+
function verifyClientProof(input, presentedProof, publicKey) {
|
|
11518
|
+
const data = Buffer.from(canonicalProofInput(input), "utf8");
|
|
11519
|
+
if (!PROOF_SIGNATURE_PATTERN.test(presentedProof)) {
|
|
11520
|
+
return false;
|
|
11521
|
+
}
|
|
11522
|
+
return verify2(
|
|
11523
|
+
"sha256",
|
|
11524
|
+
data,
|
|
11525
|
+
{ key: publicKey, dsaEncoding: RAW_SIGNATURE_ENCODING },
|
|
11526
|
+
Buffer.from(presentedProof, "hex")
|
|
11527
|
+
);
|
|
11528
|
+
}
|
|
11529
|
+
function sha256Hex(bytes) {
|
|
11530
|
+
return createHash5("sha256").update(bytes).digest("hex");
|
|
11531
|
+
}
|
|
11532
|
+
|
|
11533
|
+
// src/server/client-proof/refusal.ts
|
|
11534
|
+
import { randomBytes } from "crypto";
|
|
11535
|
+
var HTTP_STATUS = {
|
|
11536
|
+
PROOF_INVALID: 401,
|
|
11537
|
+
PROOF_REPLAYED: 401,
|
|
11538
|
+
PROOF_EXPIRED: 401,
|
|
11539
|
+
SESSION_REVOKED: 401,
|
|
11540
|
+
PROFILE_REJECTED: 400,
|
|
11541
|
+
CONTRACT_UNSUPPORTED: 409
|
|
11542
|
+
};
|
|
11543
|
+
var ClientProofRefusal = class _ClientProofRefusal {
|
|
11544
|
+
constructor(code, message) {
|
|
11545
|
+
this.code = code;
|
|
11546
|
+
this.message = message;
|
|
11547
|
+
}
|
|
11548
|
+
get httpStatus() {
|
|
11549
|
+
return HTTP_STATUS[this.code];
|
|
11550
|
+
}
|
|
11551
|
+
/** The canonical bytes of `{"error":{"code":…,"message":…,"requestId":…}}`. */
|
|
11552
|
+
envelopeBytes(requestId) {
|
|
11553
|
+
const error = /* @__PURE__ */ new Map([
|
|
11554
|
+
["code", this.code],
|
|
11555
|
+
["message", this.message],
|
|
11556
|
+
["requestId", requestId]
|
|
11557
|
+
]);
|
|
11558
|
+
return encodeCanonicalJson(/* @__PURE__ */ new Map([["error", error]]));
|
|
11559
|
+
}
|
|
11560
|
+
/** Nothing request-derived reaches a log through this. */
|
|
11561
|
+
toString() {
|
|
11562
|
+
return `ClientProofRefusal(${this.code})`;
|
|
11563
|
+
}
|
|
11564
|
+
// ---- shape: what arrived is not the contract (rule 2) -------------------
|
|
11565
|
+
static unroutable() {
|
|
11566
|
+
return contractViolation("no operation in this contract answers that method and path");
|
|
11567
|
+
}
|
|
11568
|
+
static malformedHeaders() {
|
|
11569
|
+
return contractViolation("the request does not carry the contract header fields exactly once each");
|
|
11570
|
+
}
|
|
11571
|
+
static missingContentType() {
|
|
11572
|
+
return contractViolation("a request that carries a body must declare the contract content type");
|
|
11573
|
+
}
|
|
11574
|
+
static bodyTooLarge() {
|
|
11575
|
+
return contractViolation("the request body exceeds the size this server accepts");
|
|
11576
|
+
}
|
|
11577
|
+
/**
|
|
11578
|
+
* The body parsed but its bytes are not the canonical form of what it
|
|
11579
|
+
* parsed to. Not PROOF_INVALID even though it is discovered next to the
|
|
11580
|
+
* proof: the proof over these bytes verifies perfectly well, and an
|
|
11581
|
+
* auth-family answer would tell the client to re-handshake and send the
|
|
11582
|
+
* same non-canonical bytes again.
|
|
11583
|
+
*/
|
|
11584
|
+
static bodyNotCanonical() {
|
|
11585
|
+
return contractViolation("the request body is not the canonical JSON form of the value it encodes");
|
|
11586
|
+
}
|
|
11587
|
+
static bodyNotTheDeclaredType() {
|
|
11588
|
+
return contractViolation("the request body is not the request type this operation declares");
|
|
11589
|
+
}
|
|
11590
|
+
static sessionHeaderMisplaced() {
|
|
11591
|
+
return contractViolation("the session header is present exactly on the operations that require one");
|
|
11592
|
+
}
|
|
11593
|
+
static unprocessable() {
|
|
11594
|
+
return contractViolation("the request could not be processed");
|
|
11595
|
+
}
|
|
11596
|
+
// ---- the profile allowlist ----------------------------------------------
|
|
11597
|
+
static profileRejected() {
|
|
11598
|
+
return new _ClientProofRefusal("PROFILE_REJECTED", "the named auth profile is not on this contract's allowlist");
|
|
11599
|
+
}
|
|
11600
|
+
// ---- auth: a new session might clear it (rule 1) -------------------------
|
|
11601
|
+
static sessionRevoked() {
|
|
11602
|
+
return new _ClientProofRefusal("SESSION_REVOKED", "the key or session was revoked");
|
|
11603
|
+
}
|
|
11604
|
+
static proofExpired() {
|
|
11605
|
+
return new _ClientProofRefusal("PROOF_EXPIRED", "issuedAtMillis falls outside the replay window");
|
|
11606
|
+
}
|
|
11607
|
+
static proofReplayed() {
|
|
11608
|
+
return new _ClientProofRefusal("PROOF_REPLAYED", "the nonce was already used inside the replay window");
|
|
11609
|
+
}
|
|
11610
|
+
static proofInvalid() {
|
|
11611
|
+
return new _ClientProofRefusal("PROOF_INVALID", "the client proof did not verify");
|
|
11612
|
+
}
|
|
11613
|
+
};
|
|
11614
|
+
function contractViolation(message) {
|
|
11615
|
+
return new ClientProofRefusal("CONTRACT_UNSUPPORTED", message);
|
|
11616
|
+
}
|
|
11617
|
+
|
|
11618
|
+
// src/server/client-proof/admission.ts
|
|
11619
|
+
var CLIENT_PROOF_HEADERS = {
|
|
11620
|
+
profile: "x-spfn-auth-profile",
|
|
11621
|
+
clientId: "x-spfn-client-id",
|
|
11622
|
+
keyId: "x-spfn-key-id",
|
|
11623
|
+
nonce: "x-spfn-nonce",
|
|
11624
|
+
issuedAtMillis: "x-spfn-issued-at",
|
|
11625
|
+
proof: "x-spfn-proof",
|
|
11626
|
+
session: "x-spfn-session"
|
|
11627
|
+
};
|
|
11628
|
+
var CLIENT_PROOF_CONTENT_TYPE = "application/json";
|
|
11629
|
+
var INT64_MIN2 = -(2n ** 63n);
|
|
11630
|
+
var INT64_MAX2 = 2n ** 63n - 1n;
|
|
11631
|
+
function readCredentials(headers) {
|
|
11632
|
+
const profile = headers.get(CLIENT_PROOF_HEADERS.profile);
|
|
11633
|
+
const clientId = headers.get(CLIENT_PROOF_HEADERS.clientId);
|
|
11634
|
+
const keyId = headers.get(CLIENT_PROOF_HEADERS.keyId);
|
|
11635
|
+
const nonce = headers.get(CLIENT_PROOF_HEADERS.nonce);
|
|
11636
|
+
const issuedAtRaw = headers.get(CLIENT_PROOF_HEADERS.issuedAtMillis);
|
|
11637
|
+
const proof = headers.get(CLIENT_PROOF_HEADERS.proof);
|
|
11638
|
+
if (profile === null || clientId === null || keyId === null || nonce === null || issuedAtRaw === null || proof === null) {
|
|
11639
|
+
return null;
|
|
11640
|
+
}
|
|
11641
|
+
const issuedAtMillis = parseInt64(issuedAtRaw);
|
|
11642
|
+
if (issuedAtMillis === null) {
|
|
11643
|
+
return null;
|
|
11644
|
+
}
|
|
11645
|
+
return {
|
|
11646
|
+
profile,
|
|
11647
|
+
clientId,
|
|
11648
|
+
keyId,
|
|
11649
|
+
nonce,
|
|
11650
|
+
issuedAtMillis,
|
|
11651
|
+
proof,
|
|
11652
|
+
sessionId: headers.get(CLIENT_PROOF_HEADERS.session)
|
|
11653
|
+
};
|
|
11654
|
+
}
|
|
11655
|
+
function parseInt64(raw) {
|
|
11656
|
+
if (!/^[+-]?\d{1,19}$/.test(raw)) {
|
|
11657
|
+
return null;
|
|
11658
|
+
}
|
|
11659
|
+
const value = BigInt(raw);
|
|
11660
|
+
if (value < INT64_MIN2 || value > INT64_MAX2) {
|
|
11661
|
+
return null;
|
|
11662
|
+
}
|
|
11663
|
+
return value;
|
|
11664
|
+
}
|
|
11665
|
+
function isRequestContentType(value) {
|
|
11666
|
+
if (value === null) {
|
|
11667
|
+
return false;
|
|
11668
|
+
}
|
|
11669
|
+
return value.split(";")[0].trim().toLowerCase() === CLIENT_PROOF_CONTENT_TYPE;
|
|
11670
|
+
}
|
|
11671
|
+
|
|
11672
|
+
// src/server/client-proof/replay-store.ts
|
|
11673
|
+
import { getCache } from "@spfn/core/cache";
|
|
11674
|
+
function replayLedgerKey(clientId, nonce) {
|
|
11675
|
+
return JSON.stringify([clientId, nonce]);
|
|
11676
|
+
}
|
|
11677
|
+
var MemoryReplayLedger = class {
|
|
11678
|
+
/** replayLedgerKey(...) → the millis it was spent at. */
|
|
11679
|
+
spent = /* @__PURE__ */ new Map();
|
|
11680
|
+
isSpent(clientId, nonce) {
|
|
11681
|
+
return this.spent.has(replayLedgerKey(clientId, nonce));
|
|
11682
|
+
}
|
|
11683
|
+
/** Records the pair at `atMillis`; false when it was already spent. */
|
|
11684
|
+
spend(clientId, nonce, atMillis) {
|
|
11685
|
+
const key = replayLedgerKey(clientId, nonce);
|
|
11686
|
+
if (this.spent.has(key)) {
|
|
11687
|
+
return false;
|
|
11688
|
+
}
|
|
11689
|
+
this.spent.set(key, atMillis);
|
|
11690
|
+
return true;
|
|
11691
|
+
}
|
|
11692
|
+
/** Drops entries older than the window, judged against `nowMillis`. */
|
|
11693
|
+
prune(nowMillis, windowMillis) {
|
|
11694
|
+
for (const [key, spentAtMillis] of this.spent) {
|
|
11695
|
+
if (nowMillis - spentAtMillis > windowMillis) {
|
|
11696
|
+
this.spent.delete(key);
|
|
11697
|
+
}
|
|
11698
|
+
}
|
|
11699
|
+
}
|
|
11700
|
+
get size() {
|
|
11701
|
+
return this.spent.size;
|
|
11702
|
+
}
|
|
11703
|
+
clear() {
|
|
11704
|
+
this.spent.clear();
|
|
11705
|
+
}
|
|
11706
|
+
};
|
|
11707
|
+
var MemoryReplayStore = class {
|
|
11708
|
+
constructor(windowMillis = DEFAULT_REPLAY_WINDOW_MILLIS) {
|
|
11709
|
+
this.windowMillis = windowMillis;
|
|
11710
|
+
}
|
|
11711
|
+
ledger = new MemoryReplayLedger();
|
|
11712
|
+
async isSpent(clientId, nonce) {
|
|
11713
|
+
this.ledger.prune(Date.now(), this.windowMillis);
|
|
11714
|
+
return this.ledger.isSpent(clientId, nonce);
|
|
11715
|
+
}
|
|
11716
|
+
async spend(clientId, nonce) {
|
|
11717
|
+
const now = Date.now();
|
|
11718
|
+
this.ledger.prune(now, this.windowMillis);
|
|
11719
|
+
return this.ledger.spend(clientId, nonce, now);
|
|
11720
|
+
}
|
|
11721
|
+
};
|
|
11722
|
+
var configured = null;
|
|
11723
|
+
function getClientProofReplayStore() {
|
|
11724
|
+
configured ??= new MemoryReplayStore();
|
|
11725
|
+
return configured;
|
|
11726
|
+
}
|
|
11727
|
+
|
|
11728
|
+
// src/server/middleware/auth-profiles.ts
|
|
11729
|
+
function selectAuthProfile(c) {
|
|
11730
|
+
const profile = c.req.header(CLIENT_PROOF_HEADERS.profile);
|
|
11731
|
+
if (profile === void 0) {
|
|
11732
|
+
return null;
|
|
11733
|
+
}
|
|
11734
|
+
if (c.req.header("Authorization") !== void 0) {
|
|
11735
|
+
throw new BadRequestError2({
|
|
11736
|
+
message: "an auth profile and Bearer credentials must not be mixed in one request",
|
|
11737
|
+
details: { code: "PROFILE_REJECTED" }
|
|
11738
|
+
});
|
|
11739
|
+
}
|
|
11740
|
+
const verifier = AUTH_PROFILE_VERIFIERS.get(profile);
|
|
11741
|
+
if (verifier === void 0) {
|
|
11742
|
+
throw refusalError(ClientProofRefusal.profileRejected());
|
|
11743
|
+
}
|
|
11744
|
+
return verifier;
|
|
11745
|
+
}
|
|
11746
|
+
async function resolveAuthenticatedUser(userId) {
|
|
11747
|
+
const [result, locale] = await Promise.all([
|
|
11748
|
+
usersRepository2.findByIdWithRole(userId),
|
|
11749
|
+
userProfilesRepository2.findLocaleByUserId(userId)
|
|
11750
|
+
]);
|
|
11751
|
+
if (!result) {
|
|
11752
|
+
throw new UnauthorizedError({ message: "User not found" });
|
|
11753
|
+
}
|
|
11754
|
+
const { user, role } = result;
|
|
11755
|
+
if (user.status !== "active") {
|
|
11756
|
+
if (user.status === "pending_deletion") {
|
|
11757
|
+
const pending = await getPendingDeletionInfo2(user.id);
|
|
11758
|
+
throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
11759
|
+
}
|
|
11760
|
+
throw new AccountDisabledError3({ status: user.status });
|
|
11761
|
+
}
|
|
11762
|
+
return { user, role: role?.name ?? null, locale };
|
|
11763
|
+
}
|
|
11764
|
+
function refusalError(refusal) {
|
|
11765
|
+
const data = { message: refusal.message, details: { code: refusal.code } };
|
|
11766
|
+
if (refusal.code === "PROFILE_REJECTED") {
|
|
11767
|
+
return new BadRequestError2(data);
|
|
11768
|
+
}
|
|
11769
|
+
if (refusal.code === "CONTRACT_UNSUPPORTED") {
|
|
11770
|
+
return new ConflictError2(data);
|
|
11771
|
+
}
|
|
11772
|
+
return new UnauthorizedError(data);
|
|
11773
|
+
}
|
|
11774
|
+
async function failClosed(operation) {
|
|
11775
|
+
try {
|
|
11776
|
+
return await operation();
|
|
11777
|
+
} catch (err) {
|
|
11778
|
+
authLogger2.middleware.error("client-proof replay store unavailable \u2014 refusing (fail-closed)", err);
|
|
11779
|
+
throw new ServiceUnavailableError({ message: "Authentication is temporarily unavailable" });
|
|
11780
|
+
}
|
|
11781
|
+
}
|
|
11782
|
+
async function verifyClientProofProfile(c) {
|
|
11783
|
+
const headers = c.req.raw.headers;
|
|
11784
|
+
const credentials = readCredentials(headers);
|
|
11785
|
+
if (credentials === null) {
|
|
11786
|
+
throw refusalError(ClientProofRefusal.malformedHeaders());
|
|
11787
|
+
}
|
|
11788
|
+
if (credentials.profile !== CLIENT_PROOF_PROFILE) {
|
|
11789
|
+
throw refusalError(ClientProofRefusal.profileRejected());
|
|
11790
|
+
}
|
|
11791
|
+
if (credentials.sessionId !== null) {
|
|
11792
|
+
throw refusalError(ClientProofRefusal.sessionHeaderMisplaced());
|
|
11793
|
+
}
|
|
11794
|
+
const body = new Uint8Array(await c.req.arrayBuffer());
|
|
11795
|
+
const proofInput = {
|
|
11796
|
+
method: c.req.method,
|
|
11797
|
+
path: c.req.path,
|
|
11798
|
+
clientId: credentials.clientId,
|
|
11799
|
+
keyId: credentials.keyId,
|
|
11800
|
+
nonce: credentials.nonce,
|
|
11801
|
+
issuedAtMillis: credentials.issuedAtMillis,
|
|
11802
|
+
bodySha256: admitBodySha256(headers, body)
|
|
11803
|
+
};
|
|
11804
|
+
const keyRecord = await keysRepository2.findByKeyId(credentials.keyId);
|
|
11805
|
+
if (keyRecord !== null) {
|
|
11806
|
+
if (!keyRecord.isActive) {
|
|
11807
|
+
throw refusalError(ClientProofRefusal.sessionRevoked());
|
|
11808
|
+
}
|
|
11809
|
+
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
11810
|
+
throw refusalError(ClientProofRefusal.sessionRevoked());
|
|
11811
|
+
}
|
|
11812
|
+
}
|
|
11813
|
+
const age = Date.now() - Number(credentials.issuedAtMillis);
|
|
11814
|
+
if (age < 0 || age > DEFAULT_REPLAY_WINDOW_MILLIS) {
|
|
11815
|
+
throw refusalError(ClientProofRefusal.proofExpired());
|
|
11816
|
+
}
|
|
11817
|
+
const replayStore = getClientProofReplayStore();
|
|
11818
|
+
if (await failClosed(() => replayStore.isSpent(credentials.clientId, credentials.nonce))) {
|
|
11819
|
+
throw refusalError(ClientProofRefusal.proofReplayed());
|
|
11820
|
+
}
|
|
11821
|
+
if (keyRecord === null) {
|
|
11822
|
+
throw refusalError(ClientProofRefusal.proofInvalid());
|
|
11823
|
+
}
|
|
11824
|
+
if (credentials.clientId !== String(keyRecord.userId)) {
|
|
11825
|
+
throw refusalError(ClientProofRefusal.proofInvalid());
|
|
11826
|
+
}
|
|
11827
|
+
verifyProofOrThrow(proofInput, credentials, keyRecord.publicKey);
|
|
11828
|
+
if (!await failClosed(() => replayStore.spend(credentials.clientId, credentials.nonce))) {
|
|
11829
|
+
throw refusalError(ClientProofRefusal.proofReplayed());
|
|
11830
|
+
}
|
|
11831
|
+
const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
|
|
11832
|
+
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
11833
|
+
authLogger2.middleware.info("API access", {
|
|
11834
|
+
userId: user.id,
|
|
11835
|
+
email: user.email,
|
|
11836
|
+
keyId: credentials.keyId,
|
|
11837
|
+
scheme: CLIENT_PROOF_PROFILE,
|
|
11838
|
+
method: c.req.method,
|
|
11839
|
+
path: c.req.path,
|
|
11840
|
+
ip: c.req.header("x-forwarded-for") || c.req.header("x-real-ip"),
|
|
11841
|
+
userAgent: c.req.header("user-agent")
|
|
11842
|
+
});
|
|
11843
|
+
return {
|
|
11844
|
+
user,
|
|
11845
|
+
userId: String(user.id),
|
|
11846
|
+
keyId: credentials.keyId,
|
|
11847
|
+
role,
|
|
11848
|
+
locale,
|
|
11849
|
+
scheme: "clientProofV1"
|
|
11850
|
+
};
|
|
11851
|
+
}
|
|
11852
|
+
function admitBodySha256(headers, body) {
|
|
11853
|
+
if (body.length === 0) {
|
|
11854
|
+
return ABSENT_BODY_SHA256;
|
|
11855
|
+
}
|
|
11856
|
+
if (!isRequestContentType(headers.get("content-type"))) {
|
|
11857
|
+
throw refusalError(ClientProofRefusal.missingContentType());
|
|
11858
|
+
}
|
|
11859
|
+
let value;
|
|
11860
|
+
try {
|
|
11861
|
+
value = parseCanonicalJson(body);
|
|
11862
|
+
} catch {
|
|
11863
|
+
throw refusalError(ClientProofRefusal.bodyNotCanonical());
|
|
11864
|
+
}
|
|
11865
|
+
if (!isCanonicalBytes(body, value)) {
|
|
11866
|
+
throw refusalError(ClientProofRefusal.bodyNotCanonical());
|
|
11867
|
+
}
|
|
11868
|
+
return sha256Hex(body);
|
|
11869
|
+
}
|
|
11870
|
+
function verifyProofOrThrow(proofInput, credentials, publicKeySpkiDerBase64) {
|
|
11871
|
+
let verified;
|
|
11872
|
+
try {
|
|
11873
|
+
const publicKey = parseClientProofPublicKey(publicKeySpkiDerBase64);
|
|
11874
|
+
verified = verifyClientProof(proofInput, credentials.proof, publicKey);
|
|
11875
|
+
} catch (err) {
|
|
11876
|
+
if (err instanceof ProofInputError) {
|
|
11877
|
+
throw refusalError(ClientProofRefusal.unprocessable());
|
|
11878
|
+
}
|
|
11879
|
+
throw refusalError(ClientProofRefusal.proofInvalid());
|
|
11880
|
+
}
|
|
11881
|
+
if (!verified) {
|
|
11882
|
+
throw refusalError(ClientProofRefusal.proofInvalid());
|
|
11883
|
+
}
|
|
11884
|
+
}
|
|
11885
|
+
var AUTH_PROFILE_VERIFIERS = /* @__PURE__ */ new Map([
|
|
11886
|
+
[CLIENT_PROOF_PROFILE, { verify: verifyClientProofProfile }]
|
|
11887
|
+
]);
|
|
11888
|
+
|
|
11889
|
+
// src/server/middleware/authenticate.ts
|
|
10697
11890
|
var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
11891
|
+
const profileVerifier = selectAuthProfile(c);
|
|
11892
|
+
if (profileVerifier !== null) {
|
|
11893
|
+
c.set("auth", await profileVerifier.verify(c));
|
|
11894
|
+
await next();
|
|
11895
|
+
return;
|
|
11896
|
+
}
|
|
10698
11897
|
const authHeader = c.req.header("Authorization");
|
|
10699
11898
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
10700
|
-
|
|
11899
|
+
authLogger3.middleware.error("Missing or invalid authorization header. If using Next.js API routes, ensure you have imported '@spfn/auth/nextjs/api' in your API route handler (e.g., src/app/api/actions/[[...path]]/route.ts) to enable automatic authentication header forwarding from client to backend.", {
|
|
10701
11900
|
headers: c.req.header(),
|
|
10702
11901
|
path: c.req.path
|
|
10703
11902
|
});
|
|
10704
|
-
throw new
|
|
11903
|
+
throw new UnauthorizedError2({ message: "Authentication header missing or invalid: Bearer {token}" });
|
|
10705
11904
|
}
|
|
10706
11905
|
const token = authHeader.substring(7);
|
|
10707
11906
|
const decoded = decodeToken2(token);
|
|
10708
11907
|
if (!decoded || !decoded.keyId) {
|
|
10709
|
-
throw new
|
|
11908
|
+
throw new UnauthorizedError2({ message: "Invalid token: missing keyId" });
|
|
10710
11909
|
}
|
|
10711
11910
|
const keyId = decoded.keyId;
|
|
10712
|
-
const keyRecord = await
|
|
11911
|
+
const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
|
|
10713
11912
|
if (!keyRecord) {
|
|
10714
|
-
throw new
|
|
11913
|
+
throw new UnauthorizedError2({ message: "Invalid or revoked key" });
|
|
10715
11914
|
}
|
|
10716
11915
|
if (keyRecord.expiresAt && /* @__PURE__ */ new Date() > keyRecord.expiresAt) {
|
|
10717
11916
|
throw new KeyExpiredError();
|
|
@@ -10732,34 +11931,21 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
10732
11931
|
throw new InvalidTokenError({ message: "Invalid token signature" });
|
|
10733
11932
|
}
|
|
10734
11933
|
}
|
|
10735
|
-
throw new
|
|
11934
|
+
throw new UnauthorizedError2({ message: "Authentication failed" });
|
|
10736
11935
|
}
|
|
10737
|
-
const
|
|
10738
|
-
|
|
10739
|
-
userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
|
|
10740
|
-
]);
|
|
10741
|
-
if (!result) {
|
|
10742
|
-
throw new UnauthorizedError({ message: "User not found" });
|
|
10743
|
-
}
|
|
10744
|
-
const { user, role } = result;
|
|
10745
|
-
if (user.status !== "active") {
|
|
10746
|
-
if (user.status === "pending_deletion") {
|
|
10747
|
-
const pending = await getPendingDeletionInfo2(user.id);
|
|
10748
|
-
throw new AccountPendingDeletionError3({ purgeScheduledAt: pending?.purgeScheduledAt.toISOString() });
|
|
10749
|
-
}
|
|
10750
|
-
throw new AccountDisabledError3({ status: user.status });
|
|
10751
|
-
}
|
|
10752
|
-
keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
|
|
11936
|
+
const { user, role, locale } = await resolveAuthenticatedUser(keyRecord.userId);
|
|
11937
|
+
keysRepository3.updateLastUsedById(keyRecord.id).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
10753
11938
|
c.set("auth", {
|
|
10754
11939
|
user,
|
|
10755
11940
|
userId: String(user.id),
|
|
10756
11941
|
keyId,
|
|
10757
|
-
role
|
|
10758
|
-
locale
|
|
11942
|
+
role,
|
|
11943
|
+
locale,
|
|
11944
|
+
scheme: "bearer"
|
|
10759
11945
|
});
|
|
10760
11946
|
const method = c.req.method;
|
|
10761
11947
|
const path = c.req.path;
|
|
10762
|
-
|
|
11948
|
+
authLogger3.middleware.info("API access", {
|
|
10763
11949
|
userId: user.id,
|
|
10764
11950
|
email: user.email,
|
|
10765
11951
|
keyId,
|
|
@@ -10771,6 +11957,12 @@ var authenticate = defineMiddleware("auth", async (c, next) => {
|
|
|
10771
11957
|
await next();
|
|
10772
11958
|
});
|
|
10773
11959
|
var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
11960
|
+
const profileVerifier = selectAuthProfile(c);
|
|
11961
|
+
if (profileVerifier !== null) {
|
|
11962
|
+
c.set("auth", await profileVerifier.verify(c));
|
|
11963
|
+
await next();
|
|
11964
|
+
return;
|
|
11965
|
+
}
|
|
10774
11966
|
const authHeader = c.req.header("Authorization");
|
|
10775
11967
|
if (!authHeader || !authHeader.startsWith("Bearer ")) {
|
|
10776
11968
|
await next();
|
|
@@ -10784,7 +11976,7 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
10784
11976
|
return;
|
|
10785
11977
|
}
|
|
10786
11978
|
const keyId = decoded.keyId;
|
|
10787
|
-
const keyRecord = await
|
|
11979
|
+
const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
|
|
10788
11980
|
if (!keyRecord) {
|
|
10789
11981
|
await next();
|
|
10790
11982
|
return;
|
|
@@ -10799,21 +11991,22 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
10799
11991
|
keyRecord.algorithm
|
|
10800
11992
|
);
|
|
10801
11993
|
const [result, locale] = await Promise.all([
|
|
10802
|
-
|
|
10803
|
-
|
|
11994
|
+
usersRepository3.findByIdWithRole(keyRecord.userId),
|
|
11995
|
+
userProfilesRepository3.findLocaleByUserId(keyRecord.userId)
|
|
10804
11996
|
]);
|
|
10805
11997
|
if (!result || result.user.status !== "active") {
|
|
10806
11998
|
await next();
|
|
10807
11999
|
return;
|
|
10808
12000
|
}
|
|
10809
12001
|
const { user, role } = result;
|
|
10810
|
-
|
|
12002
|
+
keysRepository3.updateLastUsedById(keyRecord.id).catch((err) => authLogger3.middleware.error("Failed to update lastUsedAt", err));
|
|
10811
12003
|
c.set("auth", {
|
|
10812
12004
|
user,
|
|
10813
12005
|
userId: String(user.id),
|
|
10814
12006
|
keyId,
|
|
10815
12007
|
role: role?.name ?? null,
|
|
10816
|
-
locale
|
|
12008
|
+
locale,
|
|
12009
|
+
scheme: "bearer"
|
|
10817
12010
|
});
|
|
10818
12011
|
} catch {
|
|
10819
12012
|
}
|
|
@@ -10824,13 +12017,13 @@ var optionalAuth = defineMiddleware("optionalAuth", async (c, next) => {
|
|
|
10824
12017
|
import { defineMiddleware as defineMiddleware2 } from "@spfn/core/route";
|
|
10825
12018
|
import { ForbiddenError as ForbiddenError2 } from "@spfn/core/errors";
|
|
10826
12019
|
import { InsufficientPermissionsError } from "@spfn/auth/errors";
|
|
10827
|
-
import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as
|
|
12020
|
+
import { getAuth as getAuth2, hasAllPermissions as hasAllPermissions2, hasAnyPermission as hasAnyPermission2, authLogger as authLogger4 } from "@spfn/auth/server";
|
|
10828
12021
|
var requirePermissions = defineMiddleware2(
|
|
10829
12022
|
"permission",
|
|
10830
12023
|
(...permissionNames) => async (c, next) => {
|
|
10831
12024
|
const auth = getAuth2(c);
|
|
10832
12025
|
if (!auth) {
|
|
10833
|
-
|
|
12026
|
+
authLogger4.middleware.warn("Permission check failed: not authenticated", {
|
|
10834
12027
|
permissions: permissionNames,
|
|
10835
12028
|
path: c.req.path
|
|
10836
12029
|
});
|
|
@@ -10839,14 +12032,14 @@ var requirePermissions = defineMiddleware2(
|
|
|
10839
12032
|
const { userId } = auth;
|
|
10840
12033
|
const allowed = await hasAllPermissions2(userId, permissionNames);
|
|
10841
12034
|
if (!allowed) {
|
|
10842
|
-
|
|
12035
|
+
authLogger4.middleware.warn("Permission check failed", {
|
|
10843
12036
|
userId,
|
|
10844
12037
|
requiredPermissions: permissionNames,
|
|
10845
12038
|
path: c.req.path
|
|
10846
12039
|
});
|
|
10847
12040
|
throw new InsufficientPermissionsError({ requiredPermissions: permissionNames });
|
|
10848
12041
|
}
|
|
10849
|
-
|
|
12042
|
+
authLogger4.middleware.debug("Permission check passed", {
|
|
10850
12043
|
userId,
|
|
10851
12044
|
permissions: permissionNames
|
|
10852
12045
|
});
|
|
@@ -10858,7 +12051,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
10858
12051
|
(...permissionNames) => async (c, next) => {
|
|
10859
12052
|
const auth = getAuth2(c);
|
|
10860
12053
|
if (!auth) {
|
|
10861
|
-
|
|
12054
|
+
authLogger4.middleware.warn("Any permission check failed: not authenticated", {
|
|
10862
12055
|
permissions: permissionNames,
|
|
10863
12056
|
path: c.req.path
|
|
10864
12057
|
});
|
|
@@ -10867,14 +12060,14 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
10867
12060
|
const { userId } = auth;
|
|
10868
12061
|
const allowed = await hasAnyPermission2(userId, permissionNames);
|
|
10869
12062
|
if (!allowed) {
|
|
10870
|
-
|
|
12063
|
+
authLogger4.middleware.warn("Any permission check failed", {
|
|
10871
12064
|
userId,
|
|
10872
12065
|
requiredAnyOf: permissionNames,
|
|
10873
12066
|
path: c.req.path
|
|
10874
12067
|
});
|
|
10875
12068
|
throw new InsufficientPermissionsError({ requiredPermissions: permissionNames });
|
|
10876
12069
|
}
|
|
10877
|
-
|
|
12070
|
+
authLogger4.middleware.debug("Any permission check passed", {
|
|
10878
12071
|
userId,
|
|
10879
12072
|
permissions: permissionNames
|
|
10880
12073
|
});
|
|
@@ -10884,7 +12077,7 @@ var requireAnyPermission = defineMiddleware2(
|
|
|
10884
12077
|
|
|
10885
12078
|
// src/server/middleware/require-role.ts
|
|
10886
12079
|
import { defineMiddleware as defineMiddleware3 } from "@spfn/core/route";
|
|
10887
|
-
import { getAuth as getAuth3, authLogger as
|
|
12080
|
+
import { getAuth as getAuth3, authLogger as authLogger5 } from "@spfn/auth/server";
|
|
10888
12081
|
import { ForbiddenError as ForbiddenError3 } from "@spfn/core/errors";
|
|
10889
12082
|
import { InsufficientRoleError } from "@spfn/auth/errors";
|
|
10890
12083
|
var requireRole = defineMiddleware3(
|
|
@@ -10892,7 +12085,7 @@ var requireRole = defineMiddleware3(
|
|
|
10892
12085
|
(...roleNames) => async (c, next) => {
|
|
10893
12086
|
const auth = getAuth3(c);
|
|
10894
12087
|
if (!auth) {
|
|
10895
|
-
|
|
12088
|
+
authLogger5.middleware.warn("Role check failed: not authenticated", {
|
|
10896
12089
|
roles: roleNames,
|
|
10897
12090
|
path: c.req.path
|
|
10898
12091
|
});
|
|
@@ -10900,7 +12093,7 @@ var requireRole = defineMiddleware3(
|
|
|
10900
12093
|
}
|
|
10901
12094
|
const { userId, role: userRole } = auth;
|
|
10902
12095
|
if (!userRole || !roleNames.includes(userRole)) {
|
|
10903
|
-
|
|
12096
|
+
authLogger5.middleware.warn("Role check failed", {
|
|
10904
12097
|
userId,
|
|
10905
12098
|
userRole,
|
|
10906
12099
|
requiredRoles: roleNames,
|
|
@@ -10908,7 +12101,7 @@ var requireRole = defineMiddleware3(
|
|
|
10908
12101
|
});
|
|
10909
12102
|
throw new InsufficientRoleError({ requiredRoles: roleNames });
|
|
10910
12103
|
}
|
|
10911
|
-
|
|
12104
|
+
authLogger5.middleware.debug("Role check passed", {
|
|
10912
12105
|
userId,
|
|
10913
12106
|
userRole,
|
|
10914
12107
|
roles: roleNames
|
|
@@ -10919,7 +12112,7 @@ var requireRole = defineMiddleware3(
|
|
|
10919
12112
|
|
|
10920
12113
|
// src/server/middleware/role-guard.ts
|
|
10921
12114
|
import { defineMiddleware as defineMiddleware4 } from "@spfn/core/route";
|
|
10922
|
-
import { getAuth as getAuth4, authLogger as
|
|
12115
|
+
import { getAuth as getAuth4, authLogger as authLogger6 } from "@spfn/auth/server";
|
|
10923
12116
|
import { ForbiddenError as ForbiddenError4 } from "@spfn/core/errors";
|
|
10924
12117
|
import { InsufficientRoleError as InsufficientRoleError2 } from "@spfn/auth/errors";
|
|
10925
12118
|
var roleGuard = defineMiddleware4(
|
|
@@ -10931,7 +12124,7 @@ var roleGuard = defineMiddleware4(
|
|
|
10931
12124
|
}
|
|
10932
12125
|
const auth = getAuth4(c);
|
|
10933
12126
|
if (!auth) {
|
|
10934
|
-
|
|
12127
|
+
authLogger6.middleware.warn("Role guard failed: not authenticated", {
|
|
10935
12128
|
path: c.req.path
|
|
10936
12129
|
});
|
|
10937
12130
|
throw new ForbiddenError4({ message: "Authentication required" });
|
|
@@ -10939,7 +12132,7 @@ var roleGuard = defineMiddleware4(
|
|
|
10939
12132
|
const { userId, role: userRole } = auth;
|
|
10940
12133
|
if (deny && deny.length > 0) {
|
|
10941
12134
|
if (userRole && deny.includes(userRole)) {
|
|
10942
|
-
|
|
12135
|
+
authLogger6.middleware.warn("Role guard denied", {
|
|
10943
12136
|
userId,
|
|
10944
12137
|
userRole,
|
|
10945
12138
|
deniedRoles: deny,
|
|
@@ -10950,7 +12143,7 @@ var roleGuard = defineMiddleware4(
|
|
|
10950
12143
|
}
|
|
10951
12144
|
if (allow && allow.length > 0) {
|
|
10952
12145
|
if (!userRole || !allow.includes(userRole)) {
|
|
10953
|
-
|
|
12146
|
+
authLogger6.middleware.warn("Role guard failed: role not allowed", {
|
|
10954
12147
|
userId,
|
|
10955
12148
|
userRole,
|
|
10956
12149
|
allowedRoles: allow,
|
|
@@ -10959,7 +12152,7 @@ var roleGuard = defineMiddleware4(
|
|
|
10959
12152
|
throw new InsufficientRoleError2({ requiredRoles: allow });
|
|
10960
12153
|
}
|
|
10961
12154
|
}
|
|
10962
|
-
|
|
12155
|
+
authLogger6.middleware.debug("Role guard passed", {
|
|
10963
12156
|
userId,
|
|
10964
12157
|
userRole,
|
|
10965
12158
|
allow,
|
|
@@ -10971,27 +12164,27 @@ var roleGuard = defineMiddleware4(
|
|
|
10971
12164
|
|
|
10972
12165
|
// src/server/middleware/one-time-token-auth.ts
|
|
10973
12166
|
import { defineMiddleware as defineMiddleware5 } from "@spfn/core/route";
|
|
10974
|
-
import { UnauthorizedError as
|
|
10975
|
-
import { usersRepository as
|
|
12167
|
+
import { UnauthorizedError as UnauthorizedError3 } from "@spfn/core/errors";
|
|
12168
|
+
import { usersRepository as usersRepository4, userProfilesRepository as userProfilesRepository4 } from "@spfn/auth/server";
|
|
10976
12169
|
var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) => {
|
|
10977
12170
|
const token = c.req.query("token") ?? extractOTTHeader(c.req.header("Authorization"));
|
|
10978
12171
|
if (!token) {
|
|
10979
|
-
throw new
|
|
12172
|
+
throw new UnauthorizedError3({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
|
|
10980
12173
|
}
|
|
10981
12174
|
const userId = await verifyOneTimeTokenService(token);
|
|
10982
12175
|
if (!userId) {
|
|
10983
|
-
throw new
|
|
12176
|
+
throw new UnauthorizedError3({ message: "Invalid or expired one-time token" });
|
|
10984
12177
|
}
|
|
10985
12178
|
const [result, locale] = await Promise.all([
|
|
10986
|
-
|
|
10987
|
-
|
|
12179
|
+
usersRepository4.findByIdWithRole(Number(userId)),
|
|
12180
|
+
userProfilesRepository4.findLocaleByUserId(Number(userId))
|
|
10988
12181
|
]);
|
|
10989
12182
|
if (!result) {
|
|
10990
|
-
throw new
|
|
12183
|
+
throw new UnauthorizedError3({ message: "User not found" });
|
|
10991
12184
|
}
|
|
10992
12185
|
const { user, role } = result;
|
|
10993
12186
|
if (user.status !== "active") {
|
|
10994
|
-
throw new
|
|
12187
|
+
throw new UnauthorizedError3({ message: "Account is not active" });
|
|
10995
12188
|
}
|
|
10996
12189
|
c.set("auth", {
|
|
10997
12190
|
user,
|
|
@@ -10999,7 +12192,8 @@ var oneTimeTokenAuth = defineMiddleware5("oneTimeTokenAuth", async (c, next) =>
|
|
|
10999
12192
|
keyId: "",
|
|
11000
12193
|
// No key involved in OTT auth
|
|
11001
12194
|
role: role?.name ?? null,
|
|
11002
|
-
locale
|
|
12195
|
+
locale,
|
|
12196
|
+
scheme: "oneTimeToken"
|
|
11003
12197
|
});
|
|
11004
12198
|
await next();
|
|
11005
12199
|
}, { skips: ["auth"] });
|
|
@@ -11468,8 +12662,9 @@ var deleteCookie = (c, name, opt) => {
|
|
|
11468
12662
|
|
|
11469
12663
|
// src/server/routes/oauth/index.ts
|
|
11470
12664
|
init_types();
|
|
12665
|
+
init_schema3();
|
|
11471
12666
|
import { Transactional as Transactional3 } from "@spfn/core/db";
|
|
11472
|
-
import { ValidationError as
|
|
12667
|
+
import { ValidationError as ValidationError9 } from "@spfn/core/errors";
|
|
11473
12668
|
import { rateLimitPolicy as rateLimitPolicy4 } from "@spfn/core/middleware";
|
|
11474
12669
|
import { defineRouter as defineRouter4, route as route4 } from "@spfn/core/route";
|
|
11475
12670
|
var providerParams = Type.Object({
|
|
@@ -11589,10 +12784,10 @@ var getGoogleOAuthUrl = route4.post("/_auth/oauth/google/url").input({
|
|
|
11589
12784
|
}).use([rateLimitPolicy4("oauth-start", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
|
|
11590
12785
|
const { body } = await c.data();
|
|
11591
12786
|
if (!isGoogleOAuthEnabled()) {
|
|
11592
|
-
throw new
|
|
12787
|
+
throw new ValidationError9({ message: "Google OAuth is not configured" });
|
|
11593
12788
|
}
|
|
11594
12789
|
if (!body.state) {
|
|
11595
|
-
throw new
|
|
12790
|
+
throw new ValidationError9({
|
|
11596
12791
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
11597
12792
|
});
|
|
11598
12793
|
}
|
|
@@ -11687,7 +12882,7 @@ var getProviderOAuthUrl = route4.post("/_auth/oauth/:provider/url").input({
|
|
|
11687
12882
|
const { params, body } = await c.data();
|
|
11688
12883
|
const provider = requireEnabledProvider(params.provider);
|
|
11689
12884
|
if (!body.state) {
|
|
11690
|
-
throw new
|
|
12885
|
+
throw new ValidationError9({
|
|
11691
12886
|
message: "OAuth state is required. Ensure the OAuth interceptor is configured."
|
|
11692
12887
|
});
|
|
11693
12888
|
}
|
|
@@ -11697,13 +12892,20 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
|
|
|
11697
12892
|
params: providerParams,
|
|
11698
12893
|
body: Type.Object({
|
|
11699
12894
|
idToken: Type.String({ description: "id_token from native/web social SDK" }),
|
|
11700
|
-
nonce: Type.String({
|
|
12895
|
+
nonce: Type.String({
|
|
12896
|
+
description: "Raw nonce used when requesting the id_token. Must equal `fingerprint` \u2014 the nonce is what binds the id_token to this public key, so a stolen id_token cannot be paired with a different key."
|
|
12897
|
+
}),
|
|
12898
|
+
accessToken: Type.Optional(Type.String({
|
|
12899
|
+
description: "Provider access token from the same sign-in. Optional \u2014 sent only when the provider needs it to resolve claims the id_token omits (Kakao email verification)."
|
|
12900
|
+
})),
|
|
11701
12901
|
publicKey: Type.String({ description: "Client public key (Base64 DER)" }),
|
|
11702
12902
|
keyId: Type.String({ description: "Key identifier (UUID)" }),
|
|
11703
12903
|
fingerprint: Type.String({ description: "Key fingerprint (SHA-256 hex)" }),
|
|
11704
12904
|
algorithm: Type.Union(KEY_ALGORITHM.map((a) => Type.Literal(a)), {
|
|
11705
12905
|
description: "Key algorithm (ES256 or RS256)"
|
|
11706
12906
|
}),
|
|
12907
|
+
deviceName: Type.Optional(DeviceNameSchema),
|
|
12908
|
+
platform: Type.Optional(PlatformSchema),
|
|
11707
12909
|
profile: Type.Optional(Type.Object({
|
|
11708
12910
|
name: Type.Optional(Type.String())
|
|
11709
12911
|
}, {
|
|
@@ -11713,7 +12915,11 @@ var oauthNative = route4.post("/_auth/oauth/:provider/native").input({
|
|
|
11713
12915
|
description: "Custom metadata passed to auth events (e.g. referral code, UTM params)"
|
|
11714
12916
|
}))
|
|
11715
12917
|
})
|
|
11716
|
-
}).use([rateLimitPolicy4("oauth-native", {
|
|
12918
|
+
}).use([rateLimitPolicy4("oauth-native", {
|
|
12919
|
+
limit: 5,
|
|
12920
|
+
windowMs: 6e4,
|
|
12921
|
+
by: byIpAndIdToken({ ipLimit: 20 })
|
|
12922
|
+
})]).skip(["auth"]).handler(async (c) => {
|
|
11717
12923
|
const { params, body } = await c.data();
|
|
11718
12924
|
return await oauthNativeService({ provider: params.provider, ...body });
|
|
11719
12925
|
});
|
|
@@ -11922,6 +13128,9 @@ var mainAuthRouter = defineRouter6({
|
|
|
11922
13128
|
login,
|
|
11923
13129
|
logout,
|
|
11924
13130
|
rotateKey,
|
|
13131
|
+
listKeys,
|
|
13132
|
+
revokeKey,
|
|
13133
|
+
revokeAllKeys,
|
|
11925
13134
|
changePassword,
|
|
11926
13135
|
getAuthSession,
|
|
11927
13136
|
// One-Time Token routes
|
|
@@ -12327,16 +13536,21 @@ export {
|
|
|
12327
13536
|
DEFAULT_DELETION_PURGE_CRON,
|
|
12328
13537
|
DEFAULT_DELETION_PURGE_STRATEGY,
|
|
12329
13538
|
DEFAULT_DELETION_SEND_NOTIFICATIONS,
|
|
13539
|
+
DeviceNameSchema,
|
|
12330
13540
|
EmailSchema,
|
|
12331
13541
|
EnvironmentKeyringTokenCipher,
|
|
12332
13542
|
INVITATION_STATUSES,
|
|
12333
13543
|
InvitationsRepository,
|
|
12334
13544
|
KEY_ALGORITHM,
|
|
13545
|
+
KEY_DEVICE_NAME_MAX_LENGTH,
|
|
13546
|
+
KEY_FINGERPRINT_PREFIX_LENGTH,
|
|
13547
|
+
KEY_PLATFORM,
|
|
12335
13548
|
KeysRepository,
|
|
12336
13549
|
PURGE_STRATEGIES,
|
|
12337
13550
|
PasswordSchema,
|
|
12338
13551
|
PermissionsRepository,
|
|
12339
13552
|
PhoneSchema,
|
|
13553
|
+
PlatformSchema,
|
|
12340
13554
|
RolePermissionsRepository,
|
|
12341
13555
|
RolesRepository,
|
|
12342
13556
|
SOCIAL_PROVIDERS,
|
|
@@ -12450,6 +13664,7 @@ export {
|
|
|
12450
13664
|
kakaoProvider,
|
|
12451
13665
|
keysRepository,
|
|
12452
13666
|
listInvitations,
|
|
13667
|
+
listKeysService,
|
|
12453
13668
|
loginService,
|
|
12454
13669
|
logoutService,
|
|
12455
13670
|
matchOAuthCsrfCookies,
|
|
@@ -12476,6 +13691,8 @@ export {
|
|
|
12476
13691
|
requirePermissions,
|
|
12477
13692
|
requireRole,
|
|
12478
13693
|
resendInvitation,
|
|
13694
|
+
resolveAuthenticatedUser,
|
|
13695
|
+
revokeAllKeysService,
|
|
12479
13696
|
revokeKeyService,
|
|
12480
13697
|
roleGuard,
|
|
12481
13698
|
rolePermissions,
|
|
@@ -12485,6 +13702,7 @@ export {
|
|
|
12485
13702
|
rotateKeyService,
|
|
12486
13703
|
runBeforeRegister,
|
|
12487
13704
|
sealSession,
|
|
13705
|
+
selectAuthProfile,
|
|
12488
13706
|
sendVerificationCodeService,
|
|
12489
13707
|
setRolePermissions,
|
|
12490
13708
|
shouldRefreshSession,
|