@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.
Files changed (50) hide show
  1. package/README.md +347 -27
  2. package/dist/{authenticate-DTA7W4v2.d.ts → authenticate-ZssfqJ7d.d.ts} +201 -5
  3. package/dist/client-proof.d.ts +185 -46
  4. package/dist/client-proof.js +253 -34
  5. package/dist/client-proof.js.map +1 -1
  6. package/dist/config.d.ts +36 -0
  7. package/dist/config.js +18 -0
  8. package/dist/config.js.map +1 -1
  9. package/dist/errors.d.ts +93 -2
  10. package/dist/errors.js +57 -0
  11. package/dist/errors.js.map +1 -1
  12. package/dist/index.d.ts +33 -3
  13. package/dist/index.js +60 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/nextjs/server.d.ts +2 -2
  16. package/dist/server.d.ts +148 -12
  17. package/dist/server.js +1410 -192
  18. package/dist/server.js.map +1 -1
  19. package/dist/{session-CGxgH3C9.d.ts → session-CFK4BT25.d.ts} +1 -1
  20. package/dist/{types-1BMx0OX1.d.ts → types-CD95yudz.d.ts} +15 -1
  21. package/migrations/20251125021229_premium_famine/snapshot.json +2641 -0
  22. package/migrations/20260225130050_smooth_the_fury/snapshot.json +2686 -0
  23. package/migrations/20260308141417_deep_iceman/snapshot.json +2686 -0
  24. package/migrations/20260308151309_perfect_deathbird/snapshot.json +2731 -0
  25. package/migrations/20260308201135_concerned_rawhide_kid/snapshot.json +2786 -0
  26. package/migrations/20260629103209_lethal_lifeguard/snapshot.json +2786 -0
  27. package/migrations/20260709073531_easy_hardball/snapshot.json +3119 -0
  28. package/migrations/20260714081434_glossy_major_mapleleaf/snapshot.json +3112 -0
  29. package/migrations/20260804105939_amazing_bushwacker/migration.sql +3 -0
  30. package/migrations/20260804105939_amazing_bushwacker/snapshot.json +3112 -0
  31. package/migrations/20260804110033_fat_piledriver/migration.sql +2 -0
  32. package/migrations/20260804110033_fat_piledriver/snapshot.json +3138 -0
  33. package/package.json +6 -9
  34. package/migrations/meta/0000_snapshot.json +0 -1632
  35. package/migrations/meta/0001_snapshot.json +0 -1660
  36. package/migrations/meta/0002_snapshot.json +0 -1660
  37. package/migrations/meta/0003_snapshot.json +0 -1689
  38. package/migrations/meta/0004_snapshot.json +0 -1721
  39. package/migrations/meta/0005_snapshot.json +0 -1721
  40. package/migrations/meta/0006_snapshot.json +0 -1921
  41. package/migrations/meta/0007_snapshot.json +0 -1916
  42. package/migrations/meta/_journal.json +0 -62
  43. /package/migrations/{0000_premium_famine.sql → 20251125021229_premium_famine/migration.sql} +0 -0
  44. /package/migrations/{0001_smooth_the_fury.sql → 20260225130050_smooth_the_fury/migration.sql} +0 -0
  45. /package/migrations/{0002_deep_iceman.sql → 20260308141417_deep_iceman/migration.sql} +0 -0
  46. /package/migrations/{0003_perfect_deathbird.sql → 20260308151309_perfect_deathbird/migration.sql} +0 -0
  47. /package/migrations/{0004_concerned_rawhide_kid.sql → 20260308201135_concerned_rawhide_kid/migration.sql} +0 -0
  48. /package/migrations/{0005_lethal_lifeguard.sql → 20260629103209_lethal_lifeguard/migration.sql} +0 -0
  49. /package/migrations/{0006_easy_hardball.sql → 20260709073531_easy_hardball/migration.sql} +0 -0
  50. /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 BigInt(options) {
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 BigInt() : trim === "string" ? yield String2() : 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) ? BigInt() : IsNumber(value) ? Literal(value) : IsBoolean(value) ? Literal(value) : IsString(value) ? Literal(value) : Object2({});
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: () => 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(desc(userInvitations.createdAt)).limit(limit).offset(offset);
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() + 90);
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.findActiveByKeyId(keyId);
8289
+ const { userId, keyId, publicKey, fingerprint, algorithm = "ES256", deviceName, platform } = params;
8290
+ const existing = await keysRepository.findByKeyId(keyId);
8172
8291
  if (existing) {
8173
- return;
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() + 3e4).toISOString();
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 ValidationError9 } from "@spfn/core/errors";
9400
- import { AccountDisabledError as AccountDisabledError2, AccountPendingDeletionError as AccountPendingDeletionError2 } from "@spfn/auth/errors";
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 { ValidationError as ValidationError4 } from "@spfn/core/errors";
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 ValidationError4({
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 ValidationError5 } from "@spfn/core/errors";
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 ValidationError5({
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 ValidationError5({
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 ValidationError6 } from "@spfn/core/errors";
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 ValidationError6({
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 configured = env3.SPFN_AUTH_GITHUB_SCOPES;
9754
- return configured ? configured.split(",").map((scope) => scope.trim()).filter(Boolean) : ["read:user", "user:email"];
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 ValidationError7 } from "@spfn/core/errors";
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 ValidationError7({
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 configured = env3.SPFN_AUTH_KAKAO_SCOPES;
9885
- return configured ? configured.split(",").map((scope) => scope.trim()).filter(Boolean) : ["account_email"];
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
- const response = await fetch(KAKAO_USERINFO_URL, {
9938
- headers: { Authorization: `Bearer ${accessToken}` }
9939
- });
9940
- if (!response.ok) {
9941
- throw new Error(`Kakao user-info request failed with status ${response.status}`);
9942
- }
9943
- const body = await response.json();
9944
- if (typeof body.id !== "number" && typeof body.id !== "string") {
9945
- throw new Error("Kakao user-info response is missing the provider user ID");
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 account = body.kakao_account;
9948
- const email = typeof account?.email === "string" ? account.email : null;
9949
- return {
9950
- providerUserId: String(body.id),
9951
- email,
9952
- emailVerified: email !== null && account?.is_email_valid === true && account.is_email_verified === true,
9953
- name: typeof account?.profile?.nickname === "string" ? account.profile.nickname : void 0,
9954
- avatar: typeof account?.profile?.profile_image_url === "string" ? account.profile.profile_image_url : void 0
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 ValidationError8 } from "@spfn/core/errors";
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 ValidationError8({
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
- const response = await fetch(NAVER_USERINFO_URL, {
10087
- headers: { Authorization: `Bearer ${accessToken}` }
10088
- });
10089
- if (!response.ok) {
10090
- throw new Error(`Naver user-info request failed with status ${response.status}`);
10091
- }
10092
- const body = await response.json();
10093
- const profile = body.response;
10094
- if (body.resultcode !== "00" || typeof profile?.id !== "string") {
10095
- throw new Error("Naver user-info response is invalid");
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
- return {
10098
- providerUserId: profile.id,
10099
- email: typeof profile.email === "string" ? profile.email : null,
10100
- // 네이버 프로필 이메일은 네이버 계정 이메일이거나 인증 절차를 거친 연락처
10101
- // 이메일이다 — 존재하면 검증된 것으로 취급한다(카카오와 같은 신뢰 수준).
10102
- emailVerified: typeof profile.email === "string",
10103
- name: typeof profile.name === "string" ? profile.name : typeof profile.nickname === "string" ? profile.nickname : void 0,
10104
- avatar: typeof profile.profile_image === "string" ? profile.profile_image : void 0
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 ValidationError9({
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 ValidationError9({
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 ValidationError9({
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 ValidationError9({
10516
+ throw new ValidationError8({
10203
10517
  message: "OAuth state validation failed"
10204
10518
  });
10205
10519
  }
10206
10520
  if (stateData.provider !== provider) {
10207
- throw new ValidationError9({
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 ValidationError9({ message: "User not found" });
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 ValidationError9({
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 ValidationError9({
10698
+ throw new ValidationError8({
10387
10699
  message: "No Google account linked. User must sign in with Google first."
10388
10700
  });
10389
10701
  }
10390
- const isExpired = !account.tokenExpiresAt || account.tokenExpiresAt.getTime() < Date.now() + TOKEN_EXPIRY_BUFFER_MS;
10391
- if (!isExpired && account.accessToken) {
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 ValidationError9({
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 ValidationError10({
10750
+ throw new NativeSignInUnsupportedError5({
10435
10751
  message: `Provider '${params.provider}' does not support native id_token sign-in.`
10436
10752
  });
10437
10753
  }
10438
- const identity = await oauthProvider.verifyNativeIdToken(params.idToken, { nonce: params.nonce });
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 changePassword = route.put("/_auth/password").input({
10998
+ var listKeys = route.post("/_auth/keys/list").input({
10645
10999
  body: Type.Object({
10646
- currentPassword: Type.Optional(Type.String({
10647
- minLength: 1,
10648
- description: "Current password for verification (required when changing existing password)"
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
- }).use([rateLimitPolicy("auth-password-change", { limit: 10, windowMs: 6e4 })]).handler(async (c) => {
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 getAuthSessionService(userId);
11007
+ return { keys: await listKeysService({ userId: Number(userId), includeRevoked: body.includeRevoked }) };
10666
11008
  });
10667
- var issueOneTimeToken = route.post("/_auth/tokens").handler(async (c) => {
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 authLogger2, keysRepository as keysRepository2, usersRepository as usersRepository2, userProfilesRepository as userProfilesRepository2, getPendingDeletionInfo as getPendingDeletionInfo2 } from "@spfn/auth/server";
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
- authLogger2.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.", {
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 UnauthorizedError({ message: "Authentication header missing or invalid: Bearer {token}" });
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 UnauthorizedError({ message: "Invalid token: missing keyId" });
11908
+ throw new UnauthorizedError2({ message: "Invalid token: missing keyId" });
10710
11909
  }
10711
11910
  const keyId = decoded.keyId;
10712
- const keyRecord = await keysRepository2.findActiveByKeyId(keyId);
11911
+ const keyRecord = await keysRepository3.findActiveByKeyId(keyId);
10713
11912
  if (!keyRecord) {
10714
- throw new UnauthorizedError({ message: "Invalid or revoked key" });
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 UnauthorizedError({ message: "Authentication failed" });
11934
+ throw new UnauthorizedError2({ message: "Authentication failed" });
10736
11935
  }
10737
- const [result, locale] = await Promise.all([
10738
- usersRepository2.findByIdWithRole(keyRecord.userId),
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: role?.name ?? null,
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
- authLogger2.middleware.info("API access", {
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 keysRepository2.findActiveByKeyId(keyId);
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
- usersRepository2.findByIdWithRole(keyRecord.userId),
10803
- userProfilesRepository2.findLocaleByUserId(keyRecord.userId)
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
- keysRepository2.updateLastUsedById(keyRecord.id).catch((err) => authLogger2.middleware.error("Failed to update lastUsedAt", err));
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 authLogger3 } from "@spfn/auth/server";
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
- authLogger3.middleware.warn("Permission check failed: not authenticated", {
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
- authLogger3.middleware.warn("Permission check failed", {
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
- authLogger3.middleware.debug("Permission check passed", {
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
- authLogger3.middleware.warn("Any permission check failed: not authenticated", {
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
- authLogger3.middleware.warn("Any permission check failed", {
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
- authLogger3.middleware.debug("Any permission check passed", {
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 authLogger4 } from "@spfn/auth/server";
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
- authLogger4.middleware.warn("Role check failed: not authenticated", {
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
- authLogger4.middleware.warn("Role check failed", {
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
- authLogger4.middleware.debug("Role check passed", {
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 authLogger5 } from "@spfn/auth/server";
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
- authLogger5.middleware.warn("Role guard failed: not authenticated", {
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
- authLogger5.middleware.warn("Role guard denied", {
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
- authLogger5.middleware.warn("Role guard failed: role not allowed", {
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
- authLogger5.middleware.debug("Role guard passed", {
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 UnauthorizedError2 } from "@spfn/core/errors";
10975
- import { usersRepository as usersRepository3, userProfilesRepository as userProfilesRepository3 } from "@spfn/auth/server";
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 UnauthorizedError2({ message: "One-time token required: ?token=xxx or Authorization: OTT xxx" });
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 UnauthorizedError2({ message: "Invalid or expired one-time token" });
12176
+ throw new UnauthorizedError3({ message: "Invalid or expired one-time token" });
10984
12177
  }
10985
12178
  const [result, locale] = await Promise.all([
10986
- usersRepository3.findByIdWithRole(Number(userId)),
10987
- userProfilesRepository3.findLocaleByUserId(Number(userId))
12179
+ usersRepository4.findByIdWithRole(Number(userId)),
12180
+ userProfilesRepository4.findLocaleByUserId(Number(userId))
10988
12181
  ]);
10989
12182
  if (!result) {
10990
- throw new UnauthorizedError2({ message: "User not found" });
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 UnauthorizedError2({ message: "Account is not active" });
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 ValidationError11 } from "@spfn/core/errors";
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 ValidationError11({ message: "Google OAuth is not configured" });
12787
+ throw new ValidationError9({ message: "Google OAuth is not configured" });
11593
12788
  }
11594
12789
  if (!body.state) {
11595
- throw new ValidationError11({
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 ValidationError11({
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({ description: "Raw nonce used when requesting the id_token" }),
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", { limit: 20, windowMs: 6e4 })]).skip(["auth"]).handler(async (c) => {
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,