@spfn/auth 0.2.1 → 0.3.0-beta.1

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 (55) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1032 -2397
  3. package/dist/{authenticate-eucncHxN.d.ts → authenticate-DlTGaBT8.d.ts} +545 -82
  4. package/dist/client-proof.d.ts +606 -0
  5. package/dist/client-proof.js +1842 -0
  6. package/dist/client-proof.js.map +1 -0
  7. package/dist/config.d.ts +319 -3
  8. package/dist/config.js +155 -5
  9. package/dist/config.js.map +1 -1
  10. package/dist/errors.d.ts +180 -3
  11. package/dist/errors.js +116 -1
  12. package/dist/errors.js.map +1 -1
  13. package/dist/index.d.ts +122 -18
  14. package/dist/index.js +129 -8
  15. package/dist/index.js.map +1 -1
  16. package/dist/nextjs/api.js +404 -96
  17. package/dist/nextjs/api.js.map +1 -1
  18. package/dist/nextjs/server.d.ts +5 -4
  19. package/dist/nextjs/server.js +165 -26
  20. package/dist/nextjs/server.js.map +1 -1
  21. package/dist/server.d.ts +2130 -1016
  22. package/dist/server.js +4916 -739
  23. package/dist/server.js.map +1 -1
  24. package/dist/session-DTHahDQ9.d.ts +53 -0
  25. package/dist/types-DYyhze28.d.ts +98 -0
  26. package/dist/wire-version-CtzMKvBB.d.ts +134 -0
  27. package/migrations/20251125021229_premium_famine/snapshot.json +2641 -0
  28. package/migrations/20260225130050_smooth_the_fury/snapshot.json +2686 -0
  29. package/migrations/20260308141417_deep_iceman/snapshot.json +2686 -0
  30. package/migrations/20260308151309_perfect_deathbird/snapshot.json +2731 -0
  31. package/migrations/20260308201135_concerned_rawhide_kid/snapshot.json +2786 -0
  32. package/migrations/20260629103209_lethal_lifeguard/migration.sql +32 -0
  33. package/migrations/20260629103209_lethal_lifeguard/snapshot.json +2786 -0
  34. package/migrations/20260709073531_easy_hardball/migration.sql +24 -0
  35. package/migrations/20260709073531_easy_hardball/snapshot.json +3119 -0
  36. package/migrations/20260714081434_glossy_major_mapleleaf/migration.sql +1 -0
  37. package/migrations/20260714081434_glossy_major_mapleleaf/snapshot.json +3112 -0
  38. package/migrations/20260804105939_amazing_bushwacker/migration.sql +3 -0
  39. package/migrations/20260804105939_amazing_bushwacker/snapshot.json +3112 -0
  40. package/migrations/20260804110033_fat_piledriver/migration.sql +2 -0
  41. package/migrations/20260804110033_fat_piledriver/snapshot.json +3138 -0
  42. package/migrations/20260805143152_vengeful_ravenous/migration.sql +4 -0
  43. package/migrations/20260805143152_vengeful_ravenous/snapshot.json +3190 -0
  44. package/package.json +54 -40
  45. package/migrations/meta/0000_snapshot.json +0 -1632
  46. package/migrations/meta/0001_snapshot.json +0 -1660
  47. package/migrations/meta/0002_snapshot.json +0 -1660
  48. package/migrations/meta/0003_snapshot.json +0 -1689
  49. package/migrations/meta/0004_snapshot.json +0 -1721
  50. package/migrations/meta/_journal.json +0 -41
  51. /package/migrations/{0000_premium_famine.sql → 20251125021229_premium_famine/migration.sql} +0 -0
  52. /package/migrations/{0001_smooth_the_fury.sql → 20260225130050_smooth_the_fury/migration.sql} +0 -0
  53. /package/migrations/{0002_deep_iceman.sql → 20260308141417_deep_iceman/migration.sql} +0 -0
  54. /package/migrations/{0003_perfect_deathbird.sql → 20260308151309_perfect_deathbird/migration.sql} +0 -0
  55. /package/migrations/{0004_concerned_rawhide_kid.sql → 20260308201135_concerned_rawhide_kid/migration.sql} +0 -0
@@ -1,6 +1,8 @@
1
1
  import * as _spfn_core_route from '@spfn/core/route';
2
+ import { K as KeyAlgorithmType, h as KeyPlatformType, j as SocialProvider } from './types-DYyhze28.js';
2
3
  import * as _sinclair_typebox from '@sinclair/typebox';
3
4
  import { Static } from '@sinclair/typebox';
5
+ import { Context } from 'hono';
4
6
  import { User } from '@spfn/auth/server';
5
7
 
6
8
  /**
@@ -40,6 +42,7 @@ interface AuthSession {
40
42
  email: string | null;
41
43
  emailVerified: boolean;
42
44
  phoneVerified: boolean;
45
+ hasPassword: boolean;
43
46
  role: Role;
44
47
  permissions: Permission[];
45
48
  }
@@ -83,65 +86,12 @@ interface UserProfile {
83
86
  profile: ProfileInfo | null;
84
87
  }
85
88
 
86
- /**
87
- * @spfn/auth - Shared Types
88
- *
89
- * Common types and constants used across the auth package
90
- */
91
- /**
92
- * Supported JWT signature algorithms
93
- *
94
- * - ES256: ECDSA with P-256 and SHA-256 (recommended, smaller keys)
95
- * - RS256: RSA with SHA-256 (fallback, larger keys)
96
- */
97
- declare const KEY_ALGORITHM: readonly ["ES256", "RS256"];
98
- /**
99
- * Key algorithm type derived from the const array
100
- */
101
- type KeyAlgorithmType = typeof KEY_ALGORITHM[number];
102
- /**
103
- * Invitation status enum values
104
- * Single source of truth for all invitation statuses
105
- */
106
- declare const INVITATION_STATUSES: readonly ["pending", "accepted", "expired", "cancelled"];
107
- /**
108
- * Invitation status type derived from the const array
109
- */
110
- type InvitationStatus = typeof INVITATION_STATUSES[number];
111
- /**
112
- * User status enum values
113
- * Single source of truth for all user statuses
114
- */
115
- declare const USER_STATUSES: readonly ["active", "inactive", "suspended"];
116
- /**
117
- * User status type derived from the const array
118
- */
119
- type UserStatus = typeof USER_STATUSES[number];
120
- /**
121
- * Social provider enum values
122
- * Single source of truth for supported OAuth providers
123
- */
124
- declare const SOCIAL_PROVIDERS: readonly ["google", "github", "kakao", "naver"];
125
- /**
126
- * Social provider type derived from the const array
127
- */
128
- type SocialProvider = typeof SOCIAL_PROVIDERS[number];
129
-
130
89
  /**
131
90
  * @spfn/auth - Auth Service
132
91
  *
133
92
  * Core authentication logic: registration, login, logout, password management
134
93
  */
135
94
 
136
- interface CheckAccountExistsParams {
137
- email?: string;
138
- phone?: string;
139
- }
140
- interface CheckAccountExistsResult {
141
- exists: boolean;
142
- identifier: string;
143
- identifierType: 'email' | 'phone';
144
- }
145
95
  interface RegisterParams {
146
96
  email?: string;
147
97
  phone?: string;
@@ -151,6 +101,8 @@ interface RegisterParams {
151
101
  keyId: string;
152
102
  fingerprint: string;
153
103
  algorithm?: KeyAlgorithmType;
104
+ deviceName?: string;
105
+ platform?: KeyPlatformType;
154
106
  metadata?: Record<string, unknown>;
155
107
  }
156
108
  interface RegisterResult {
@@ -168,6 +120,8 @@ interface LoginParams {
168
120
  fingerprint: string;
169
121
  oldKeyId?: string;
170
122
  algorithm?: KeyAlgorithmType;
123
+ deviceName?: string;
124
+ platform?: KeyPlatformType;
171
125
  }
172
126
  interface LoginResult {
173
127
  userId: string;
@@ -182,14 +136,10 @@ interface LogoutParams {
182
136
  }
183
137
  interface ChangePasswordParams {
184
138
  userId: number;
185
- currentPassword: string;
139
+ currentPassword?: string;
186
140
  newPassword: string;
187
141
  passwordHash?: string;
188
142
  }
189
- /**
190
- * Check if an account exists by email or phone
191
- */
192
- declare function checkAccountExistsService(params: CheckAccountExistsParams): Promise<CheckAccountExistsResult>;
193
143
  /**
194
144
  * Register a new user account
195
145
  */
@@ -209,13 +159,23 @@ declare function changePasswordService(params: ChangePasswordParams): Promise<vo
209
159
 
210
160
  declare const EmailSchema: _sinclair_typebox.TString;
211
161
  declare const PhoneSchema: _sinclair_typebox.TString;
162
+ /**
163
+ * Optional device labels a client may send when registering a key.
164
+ *
165
+ * Display only: the key list uses them to tell one device from another, and
166
+ * nothing is authorized or refused by either value, so a client that lies about
167
+ * them gains nothing. Both are omitted by every key registered before they
168
+ * existed, hence optional rather than defaulted.
169
+ */
170
+ declare const DeviceNameSchema: _sinclair_typebox.TString;
171
+ declare const PlatformSchema: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>;
212
172
  declare const PasswordSchema: _sinclair_typebox.TString;
213
173
  declare const TargetTypeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
214
174
  type VerificationTargetType = Static<typeof TargetTypeSchema>;
215
175
  declare const VERIFICATION_TARGET_TYPES: readonly ["email", "phone"];
216
- declare const VerificationPurposeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">]>;
176
+ declare const VerificationPurposeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
217
177
  type VerificationPurpose = Static<typeof VerificationPurposeSchema>;
218
- declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change"];
178
+ declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
219
179
 
220
180
  /**
221
181
  * @spfn/auth - Verification Service
@@ -266,6 +226,9 @@ interface RegisterPublicKeyParams {
266
226
  publicKey: string;
267
227
  fingerprint: string;
268
228
  algorithm?: KeyAlgorithmType;
229
+ /** Device label for the key list. Display only — nothing is authorized by it. */
230
+ deviceName?: string;
231
+ platform?: KeyPlatformType;
269
232
  }
270
233
  interface RotateKeyParams {
271
234
  userId: number;
@@ -274,6 +237,9 @@ interface RotateKeyParams {
274
237
  newPublicKey: string;
275
238
  fingerprint: string;
276
239
  algorithm?: KeyAlgorithmType;
240
+ /** Omitted: the replaced key's label carries over, so rotation keeps its name. */
241
+ deviceName?: string;
242
+ platform?: KeyPlatformType;
277
243
  }
278
244
  interface RotateKeyResult {
279
245
  success: boolean;
@@ -284,8 +250,61 @@ interface RevokeKeyParams {
284
250
  keyId: string;
285
251
  reason: string;
286
252
  }
253
+ interface RevokeAllKeysParams {
254
+ userId: number;
255
+ /** The key the request itself is signed with — spared unless includeCurrent. */
256
+ currentKeyId: string;
257
+ /** true signs the caller out too. Default false: "my other devices". */
258
+ includeCurrent?: boolean;
259
+ reason: string;
260
+ }
261
+ interface RevokeAllKeysResult {
262
+ revokedCount: number;
263
+ currentKeyRevoked: boolean;
264
+ }
265
+ /** One registered device as the account surface shows it. */
266
+ interface KeySummary {
267
+ keyId: string;
268
+ deviceName?: string;
269
+ platform?: string;
270
+ algorithm: KeyAlgorithmType;
271
+ /** First bytes of the fingerprint — enough to tell two entries apart. */
272
+ fingerprintPrefix: string;
273
+ /**
274
+ * Milliseconds since the Unix epoch, not an ISO string.
275
+ *
276
+ * One representation of a moment across the whole surface: a generated Swift
277
+ * or Kotlin client reads an integer with no date formatter, and
278
+ * `ISO8601DateFormatter` rejecting fractional seconds by default stops being
279
+ * a way for the two SDKs to disagree about the same value.
280
+ */
281
+ createdAtMillis: number;
282
+ lastUsedAtMillis?: number;
283
+ expiresAtMillis?: number;
284
+ /** The TTL has run out. The key still reads as active; authenticate refuses it. */
285
+ isExpired: boolean;
286
+ /** False once revoked. Only ever false when the caller asked for revoked keys. */
287
+ isActive: boolean;
288
+ /** When it was revoked, for the "what did I cut off, and when" reading. */
289
+ revokedAtMillis?: number;
290
+ }
291
+ interface ListKeysParams {
292
+ userId: number;
293
+ /** Also return keys already revoked. Default false: only what can still sign. */
294
+ includeRevoked?: boolean;
295
+ }
296
+ /** How much of the fingerprint the list returns. */
297
+ declare const KEY_FINGERPRINT_PREFIX_LENGTH = 8;
287
298
  /**
288
299
  * Register a new public key for a user
300
+ *
301
+ * `keyId` is UNIQUE across all users, so the lookup must ignore `isActive` —
302
+ * filtering on it misses a revoked row and the insert then fails on the unique
303
+ * index, rolling the whole login transaction back into a 500. Reuse is refused
304
+ * with a domain error instead, telling the client to generate a fresh keyId.
305
+ *
306
+ * @throws KeyIdAlreadyRegisteredError keyId가 이미 쓰인 값일 때 (자기 폐기 키 재사용 · 남의 키)
307
+ * @throws InvalidKeyFingerprintError fingerprint가 publicKey와 맞지 않을 때
289
308
  */
290
309
  declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<void>;
291
310
  /**
@@ -293,9 +312,36 @@ declare function registerPublicKeyService(params: RegisterPublicKeyParams): Prom
293
312
  */
294
313
  declare function rotateKeyService(params: RotateKeyParams): Promise<RotateKeyResult>;
295
314
  /**
296
- * Revoke a user's public key
315
+ * Revoke a user's public key.
316
+ *
317
+ * Returns false when the key does not belong to this user, so a caller acting
318
+ * on a key id from outside (the device list) can answer "not found" instead of
319
+ * reporting a revocation that never happened. The repository already scopes the
320
+ * update by userId, so someone else's key is never touched either way.
321
+ */
322
+ declare function revokeKeyService(params: RevokeKeyParams): Promise<boolean>;
323
+ /**
324
+ * List the caller's active keys — one entry per device that can sign for them.
325
+ *
326
+ * `isExpired` is computed rather than stored: an expired key keeps `isActive`
327
+ * true (nothing flips it), and `authenticate` refuses it at request time. A list
328
+ * that showed it as simply "active" would be telling the user something the
329
+ * server does not act on.
330
+ *
331
+ * The fingerprint is truncated. Its full value is what a native sign-in must
332
+ * send as its nonce (issue #63), and an account page has no use for it beyond
333
+ * telling two entries apart.
334
+ */
335
+ declare function listKeysService(params: ListKeysParams): Promise<KeySummary[]>;
336
+ /**
337
+ * Revoke every active key the user has, optionally sparing the current one.
338
+ *
339
+ * The caller's own key is spared by default, so "sign out my other devices"
340
+ * does not also end the session making the request. Passing
341
+ * `includeCurrent: true` is the full sign-out, which until now was reachable
342
+ * only as a side effect of changing a password.
297
343
  */
298
- declare function revokeKeyService(params: RevokeKeyParams): Promise<void>;
344
+ declare function revokeAllKeysService(params: RevokeAllKeysParams): Promise<RevokeAllKeysResult>;
299
345
 
300
346
  /**
301
347
  * @spfn/auth - RBAC Type Definitions
@@ -406,6 +452,161 @@ declare function issueOneTimeTokenService(userId: string): Promise<IssueOneTimeT
406
452
  */
407
453
  declare function verifyOneTimeTokenService(token: string): Promise<string | null>;
408
454
 
455
+ /**
456
+ * OAuth Provider 추상화
457
+ *
458
+ * Provider별로 하드코딩된 분기를 제거하기 위한 공통 인터페이스와 registry.
459
+ * - 내장 provider(google)는 패키지 로드 시점에 자기 등록(dogfood)
460
+ * - 외부 패키지(@superself/auth 등)는 registerOAuthProvider()로 런타임 등록
461
+ *
462
+ * @spfn/auth는 토큰 issuer가 아니라 소비(client) 측이므로, 이 추상화는
463
+ * web 흐름("authorize URL 생성 → code 교환 → 사용자 정보 정규화")과
464
+ * native 흐름(네이티브/웹 SDK가 받은 id_token 직접 검증)을 다룬다.
465
+ */
466
+
467
+ /**
468
+ * Provider 사용자 정보를 공통 형태로 정규화한 신원
469
+ *
470
+ * provider별 응답 형태(snake_case 등)를 service에 노출하지 않기 위한 경계.
471
+ */
472
+ interface NormalizedIdentity {
473
+ providerUserId: string;
474
+ email: string | null;
475
+ emailVerified: boolean;
476
+ name?: string;
477
+ avatar?: string;
478
+ }
479
+ /**
480
+ * 정규화된 OAuth 토큰 응답
481
+ *
482
+ * @property expiresIn - access token 만료까지 남은 초(seconds)
483
+ */
484
+ interface OAuthTokens {
485
+ accessToken: string;
486
+ refreshToken?: string;
487
+ expiresIn: number;
488
+ }
489
+ /**
490
+ * 네이티브 id_token 검증 옵션
491
+ */
492
+ interface NativeVerifyOptions {
493
+ /** 클라이언트가 생성한 raw nonce. provider별 규약(raw 또는 SHA-256 해시)으로 대조된다. */
494
+ nonce: string;
495
+ /**
496
+ * 같은 로그인에서 SDK가 함께 받은 provider access token (선택).
497
+ *
498
+ * id_token만으로는 알 수 없는 claim을 provider API로 보강하려는 provider가 쓴다
499
+ * (카카오: id_token에 email_verified가 없어 /v2/user/me의 이메일 유효·인증 플래그를 본다).
500
+ *
501
+ * ⚠️ 클라이언트가 보낸 검증되지 않은 값이다. 다른 사용자의 토큰일 수 있으므로,
502
+ * 이 값으로 조회한 신원은 반드시 id_token의 sub와 대조한 뒤에만 신뢰해야 한다.
503
+ */
504
+ accessToken?: string;
505
+ }
506
+ interface OAuthCodeExchangeOptions {
507
+ /** Provider가 callback에 돌려준 원본 state. 일부 provider는 token 교환에도 요구한다. */
508
+ state: string;
509
+ }
510
+ /**
511
+ * Provider가 서비스로 보내는 연동 해제 알림의 원재료
512
+ *
513
+ * provider마다 전달 방식(query/form/JSON, 헤더 인증)이 달라 route가 정규화해 넘긴다.
514
+ * fields는 query string과 body(form/JSON)를 병합한 문자열 맵이다.
515
+ */
516
+ interface UnlinkNotifyRequest {
517
+ /** Authorization 헤더 원문 (없으면 null) */
518
+ authorization: string | null;
519
+ fields: Record<string, string>;
520
+ }
521
+ /**
522
+ * 검증에 성공한 연동 해제 알림
523
+ */
524
+ interface UnlinkNotification {
525
+ providerUserId: string;
526
+ /** provider가 전달한 해제 경로 (kakao referrer_type 등) */
527
+ reason?: string;
528
+ }
529
+ /**
530
+ * 연동 해제 알림 검증 실패
531
+ *
532
+ * API 에러 응답 체계를 타지 않는다 — route가 잡아 status만 반환한다.
533
+ * (provider 웹훅은 사람이 아닌 provider 서버가 호출자라서 에러 본문이 무의미하다)
534
+ */
535
+ declare class UnlinkNotifyRejection extends Error {
536
+ readonly status: 400 | 401 | 403;
537
+ constructor(status: 400 | 401 | 403, message: string);
538
+ }
539
+ /**
540
+ * OAuth provider 구현 인터페이스
541
+ *
542
+ * google, superself 등 모든 provider가 이 형태를 만족해야 registry에 등록된다.
543
+ */
544
+ interface OAuthProvider {
545
+ id: SocialProvider;
546
+ /**
547
+ * provider가 사용 가능한 상태인지(필수 env 등) 확인
548
+ */
549
+ isEnabled(): boolean;
550
+ /**
551
+ * provider 로그인 페이지로 보낼 authorization URL 생성
552
+ *
553
+ * @param state - CSRF 방지용 암호화 state
554
+ * @param scopes - 요청할 scope (미지정 시 provider 기본값)
555
+ */
556
+ getAuthUrl(state: string, scopes?: string[]): string;
557
+ /**
558
+ * authorization code를 토큰으로 교환
559
+ */
560
+ exchangeCodeForTokens(code: string, options: OAuthCodeExchangeOptions): Promise<OAuthTokens>;
561
+ /**
562
+ * access token으로 사용자 정보를 조회하고 공통 형태로 정규화
563
+ */
564
+ getUserInfo(accessToken: string): Promise<NormalizedIdentity>;
565
+ /**
566
+ * refresh token으로 access token 갱신 (provider가 지원하는 경우)
567
+ *
568
+ * 저장된 provider 토큰을 이후 API 호출에 재사용할 때 사용한다.
569
+ * 미구현 provider는 갱신 불가로 간주한다.
570
+ */
571
+ refreshTokens?(refreshToken: string): Promise<OAuthTokens>;
572
+ /**
573
+ * 네이티브/웹 SDK가 받은 id_token을 직접 검증하고 신원을 정규화한다.
574
+ *
575
+ * authorization code 교환 없이 provider JWKS로 서명을 검증하므로 client secret이
576
+ * 필요 없다. native sign-in을 지원하는 provider만 구현한다(Apple은 web SDK 부재로
577
+ * Android·웹도 이 경로를 쓴다).
578
+ */
579
+ verifyNativeIdToken?(idToken: string, options: NativeVerifyOptions): Promise<NormalizedIdentity>;
580
+ /**
581
+ * Provider발 연동 해제 알림(웹훅)을 검증하고 대상 사용자를 식별한다.
582
+ *
583
+ * 인증 없는 공개 엔드포인트로 들어오므로 provider별 규격(카카오: 어드민 키 헤더,
584
+ * 네이버: HMAC 서명 + AES 복호화)의 검증을 통과해야만 처리된다.
585
+ * 검증 실패는 UnlinkNotifyRejection을 던진다.
586
+ */
587
+ verifyUnlinkNotification?(request: UnlinkNotifyRequest): Promise<UnlinkNotification>;
588
+ /**
589
+ * 연동 해제 알림 처리 성공 시 응답할 HTTP status (미지정 시 200)
590
+ *
591
+ * 네이버는 204 No Content를 요구한다.
592
+ */
593
+ unlinkNotifyAckStatus?: 200 | 204;
594
+ }
595
+ /**
596
+ * OAuth provider 등록 (public)
597
+ *
598
+ * 동일 id로 다시 등록하면 덮어쓴다(외부 패키지의 override 허용).
599
+ */
600
+ declare function registerOAuthProvider(provider: OAuthProvider): void;
601
+ /**
602
+ * 등록된 provider 조회. 미등록이면 undefined.
603
+ */
604
+ declare function getOAuthProvider(id: SocialProvider): OAuthProvider | undefined;
605
+ /**
606
+ * 등록된 모든 provider 목록
607
+ */
608
+ declare function getRegisteredProviders(): OAuthProvider[];
609
+
409
610
  /**
410
611
  * @spfn/auth - OAuth Service
411
612
  *
@@ -423,6 +624,8 @@ interface OAuthStartParams {
423
624
  fingerprint: string;
424
625
  algorithm: KeyAlgorithmType;
425
626
  metadata?: Record<string, unknown>;
627
+ /** CSRF nonce bound into the state; the route sets the matching oauth_csrf cookie. */
628
+ nonce?: string;
426
629
  }
427
630
  interface OAuthStartResult {
428
631
  authUrl: string;
@@ -431,6 +634,16 @@ interface OAuthCallbackParams {
431
634
  provider: SocialProvider;
432
635
  code: string;
433
636
  state: string;
637
+ /**
638
+ * Value(s) of the oauth_csrf cookie from the callback request. One of them
639
+ * must equal the nonce bound into the (encrypted) state — otherwise the flow
640
+ * wasn't initiated by this browser (login CSRF). An array arises because the
641
+ * cookie name carries the PORT suffix of the process that set it (the Next.js
642
+ * web process), which differs from the API process in a split deployment, so
643
+ * the callback collects every spfn_oauth_csrf* candidate. Pass `undefined` or
644
+ * an empty array when absent; verification then fails closed.
645
+ */
646
+ expectedNonce: string | string[] | undefined;
434
647
  }
435
648
  interface OAuthCallbackResult {
436
649
  redirectUrl: string;
@@ -438,6 +651,13 @@ interface OAuthCallbackResult {
438
651
  keyId: string;
439
652
  isNewUser: boolean;
440
653
  }
654
+ /**
655
+ * registry에서 provider를 찾아 사용 가능한지 검증 후 반환
656
+ *
657
+ * 미등록과 비활성을 구분해 디버깅 신호를 남긴다.
658
+ * 라우트 레이어에서도 재사용한다(중복 조회/non-null 단언 제거).
659
+ */
660
+ declare function requireEnabledProvider(provider: SocialProvider): OAuthProvider;
441
661
  /**
442
662
  * OAuth 로그인 시작 - Provider 로그인 페이지로 리다이렉트할 URL 생성
443
663
  *
@@ -456,11 +676,11 @@ declare function oauthCallbackService(params: OAuthCallbackParams): Promise<OAut
456
676
  */
457
677
  declare function buildOAuthErrorUrl(error: string): string;
458
678
  /**
459
- * OAuth provider가 활성화되어 있는지 확인
679
+ * OAuth provider가 등록되어 있고 활성화되어 있는지 확인
460
680
  */
461
681
  declare function isOAuthProviderEnabled(provider: SocialProvider): boolean;
462
682
  /**
463
- * 활성화된 모든 OAuth provider 목록
683
+ * 활성화된 모든 OAuth provider 목록 (registry 기반)
464
684
  */
465
685
  declare function getEnabledOAuthProviders(): SocialProvider[];
466
686
  /**
@@ -473,6 +693,72 @@ declare function getEnabledOAuthProviders(): SocialProvider[];
473
693
  * @returns 유효한 Google access token
474
694
  */
475
695
  declare function getGoogleAccessToken(userId: number): Promise<string>;
696
+ interface UnlinkNotifyResult {
697
+ /** provider 규격이 요구하는 성공 응답 status */
698
+ ackStatus: 200 | 204;
699
+ /** 대상 소셜 계정을 찾아 삭제했는지 (미존재·이미 삭제면 false) */
700
+ handled: boolean;
701
+ }
702
+ /**
703
+ * Provider발 연동 해제 알림(unlink-notify) 처리
704
+ *
705
+ * 검증(verifyUnlinkNotification)을 통과한 요청만 여기 도달한다.
706
+ * 소셜 계정 연결 row를 삭제해 저장 토큰(access/refresh)까지 함께 파기하고,
707
+ * 후속 정책(계정 탈퇴 연계 등)은 auth.oauth.unlinked 이벤트 구독에 맡긴다.
708
+ *
709
+ * 대상 계정이 없어도 성공으로 응답한다 — provider 재전송·이미 해제된 계정에
710
+ * 대한 알림은 정상 시나리오다.
711
+ */
712
+ declare function oauthUnlinkNotifyService(provider: SocialProvider, notification: UnlinkNotification): Promise<UnlinkNotifyResult>;
713
+
714
+ /**
715
+ * @spfn/auth - Native Social Login Service
716
+ *
717
+ * 네이티브/웹 SDK가 받은 id_token을 JWKS로 검증하고, 검증된 신원에 클라이언트가 만든
718
+ * 공개키를 등록한다. 토큰은 발급하지 않는다 — 클라이언트가 등록한 키로 client token을
719
+ * 직접 서명해 Bearer로 사용한다(client-signs / server-verifies 모델).
720
+ *
721
+ * 흐름은 두 단계로 분리한다:
722
+ * 1) id_token 검증 — 외부 JWKS 네트워크 조회. DB 트랜잭션 밖에서 수행한다.
723
+ * 2) persist — 사용자 link/create + 공개키 등록을 한 트랜잭션으로. 이벤트는 커밋 후 발행.
724
+ */
725
+
726
+ interface OAuthNativeParams {
727
+ provider: SocialProvider;
728
+ idToken: string;
729
+ nonce: string;
730
+ publicKey: string;
731
+ keyId: string;
732
+ fingerprint: string;
733
+ algorithm: KeyAlgorithmType;
734
+ /** 키 목록에 보일 기기 라벨 (선택). 표시용이라 권한 판정에 쓰이지 않는다. */
735
+ deviceName?: string;
736
+ platform?: KeyPlatformType;
737
+ /**
738
+ * SDK가 id_token과 함께 받은 provider access token (선택).
739
+ *
740
+ * provider가 id_token만으로 확인할 수 없는 claim을 보강할 때만 쓴다. 없으면 provider는
741
+ * id_token이 담은 정보만으로 신원을 정규화한다.
742
+ */
743
+ accessToken?: string;
744
+ /** Apple은 첫 로그인에만 이름을 별도로 주므로 클라이언트가 전달할 수 있다. */
745
+ profile?: {
746
+ name?: string;
747
+ };
748
+ metadata?: Record<string, unknown>;
749
+ }
750
+ interface OAuthNativeResult {
751
+ userId: string;
752
+ keyId: string;
753
+ isNewUser: boolean;
754
+ }
755
+ /**
756
+ * native id_token 로그인 처리
757
+ *
758
+ * @throws NativeSignInUnsupportedError provider가 native sign-in을 지원하지 않을 때
759
+ * @throws InvalidSocialTokenError id_token 검증 실패 시
760
+ */
761
+ declare function oauthNativeService(params: OAuthNativeParams): Promise<OAuthNativeResult>;
476
762
 
477
763
  /**
478
764
  * @spfn/auth - Main Router
@@ -484,25 +770,19 @@ declare function getGoogleAccessToken(userId: number): Promise<string>;
484
770
  * Exports all authentication-related routes
485
771
  *
486
772
  * Routes:
487
- * - Auth: /_auth/exists, /_auth/codes, /_auth/login, /_auth/logout, etc.
773
+ * - Auth: /_auth/codes, /_auth/login, /_auth/logout, etc.
488
774
  * - OAuth: /_auth/oauth/google, /_auth/oauth/google/callback, etc.
489
775
  * - Invitations: /_auth/invitations/*
490
776
  * - Users: /_auth/users/*
777
+ * - Deletion: /_auth/deletion/request, /_auth/deletion/cancel
491
778
  * - Admin: /_auth/admin/* (superadmin only)
492
779
  */
493
780
  declare const mainAuthRouter: _spfn_core_route.Router<{
494
- checkAccountExists: _spfn_core_route.RouteDef<{
495
- body: _sinclair_typebox.TUnion<[_sinclair_typebox.TObject<{
496
- email: _sinclair_typebox.TString;
497
- }>, _sinclair_typebox.TObject<{
498
- phone: _sinclair_typebox.TString;
499
- }>]>;
500
- }, {}, CheckAccountExistsResult>;
501
781
  sendVerificationCode: _spfn_core_route.RouteDef<{
502
782
  body: _sinclair_typebox.TObject<{
503
783
  target: _sinclair_typebox.TString;
504
784
  targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
505
- purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">]>;
785
+ purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
506
786
  }>;
507
787
  }, {}, SendVerificationCodeResult>;
508
788
  verifyCode: _spfn_core_route.RouteDef<{
@@ -510,7 +790,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
510
790
  target: _sinclair_typebox.TString;
511
791
  targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
512
792
  code: _sinclair_typebox.TString;
513
- purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">]>;
793
+ purpose: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"registration">, _sinclair_typebox.TLiteral<"login">, _sinclair_typebox.TLiteral<"password_reset">, _sinclair_typebox.TLiteral<"email_change">, _sinclair_typebox.TLiteral<"phone_change">, _sinclair_typebox.TLiteral<"account_deletion">]>;
514
794
  }>;
515
795
  }, {}, {
516
796
  valid: boolean;
@@ -530,6 +810,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
530
810
  keyId: _sinclair_typebox.TString;
531
811
  fingerprint: _sinclair_typebox.TString;
532
812
  algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
813
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
814
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
533
815
  }>;
534
816
  }, RegisterResult>;
535
817
  login: _spfn_core_route.RouteDef<{
@@ -545,6 +827,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
545
827
  fingerprint: _sinclair_typebox.TString;
546
828
  algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
547
829
  oldKeyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
830
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
831
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
548
832
  }>;
549
833
  }, LoginResult>;
550
834
  logout: _spfn_core_route.RouteDef<{}, {}, void>;
@@ -554,11 +838,33 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
554
838
  keyId: _sinclair_typebox.TString;
555
839
  fingerprint: _sinclair_typebox.TString;
556
840
  algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
841
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
842
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
557
843
  }>;
558
844
  }, RotateKeyResult>;
845
+ listKeys: _spfn_core_route.RouteDef<{
846
+ body: _sinclair_typebox.TObject<{
847
+ includeRevoked: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
848
+ }>;
849
+ }, {}, {
850
+ keys: KeySummary[];
851
+ }>;
852
+ revokeKey: _spfn_core_route.RouteDef<{
853
+ body: _sinclair_typebox.TObject<{
854
+ keyId: _sinclair_typebox.TString;
855
+ }>;
856
+ }, {}, {
857
+ keyId: string;
858
+ selfRevoked: boolean;
859
+ }>;
860
+ revokeAllKeys: _spfn_core_route.RouteDef<{
861
+ body: _sinclair_typebox.TObject<{
862
+ includeCurrent: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
863
+ }>;
864
+ }, {}, RevokeAllKeysResult>;
559
865
  changePassword: _spfn_core_route.RouteDef<{
560
866
  body: _sinclair_typebox.TObject<{
561
- currentPassword: _sinclair_typebox.TString;
867
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
562
868
  newPassword: _sinclair_typebox.TString;
563
869
  }>;
564
870
  }, {}, void>;
@@ -580,8 +886,27 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
580
886
  email: string | null;
581
887
  emailVerified: boolean;
582
888
  phoneVerified: boolean;
889
+ hasPassword: boolean;
583
890
  }>;
584
891
  issueOneTimeToken: _spfn_core_route.RouteDef<{}, {}, IssueOneTimeTokenResult>;
892
+ requestAccountDeletion: _spfn_core_route.RouteDef<{
893
+ body: _sinclair_typebox.TObject<{
894
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
895
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
896
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
897
+ immediate: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
898
+ }>;
899
+ }, {}, {
900
+ purgeScheduledAt: string;
901
+ }>;
902
+ cancelAccountDeletion: _spfn_core_route.RouteDef<{
903
+ body: _sinclair_typebox.TObject<{
904
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
905
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
906
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
907
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
908
+ }>;
909
+ }, {}, void>;
585
910
  oauthGoogleStart: _spfn_core_route.RouteDef<{
586
911
  query: _sinclair_typebox.TObject<{
587
912
  state: _sinclair_typebox.TString;
@@ -597,7 +922,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
597
922
  }, {}, Response>;
598
923
  oauthStart: _spfn_core_route.RouteDef<{
599
924
  body: _sinclair_typebox.TObject<{
600
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "github" | "kakao" | "naver">[]>;
925
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
601
926
  returnUrl: _sinclair_typebox.TString;
602
927
  publicKey: _sinclair_typebox.TString;
603
928
  keyId: _sinclair_typebox.TString;
@@ -607,11 +932,12 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
607
932
  }>;
608
933
  }, {}, OAuthStartResult>;
609
934
  oauthProviders: _spfn_core_route.RouteDef<{}, {}, {
610
- providers: ("google" | "github" | "kakao" | "naver")[];
935
+ providers: ("google" | "apple" | "github" | "kakao" | "naver" | "superself")[];
611
936
  }>;
612
937
  getGoogleOAuthUrl: _spfn_core_route.RouteDef<{
613
938
  body: _sinclair_typebox.TObject<{
614
939
  returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
940
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
615
941
  state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
616
942
  }>;
617
943
  }, {}, {
@@ -629,6 +955,67 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
629
955
  keyId: string;
630
956
  returnUrl: string;
631
957
  }>;
958
+ oauthProviderStart: _spfn_core_route.RouteDef<{
959
+ params: _sinclair_typebox.TObject<{
960
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
961
+ }>;
962
+ query: _sinclair_typebox.TObject<{
963
+ state: _sinclair_typebox.TString;
964
+ }>;
965
+ }, {}, Response>;
966
+ oauthProviderCallback: _spfn_core_route.RouteDef<{
967
+ params: _sinclair_typebox.TObject<{
968
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
969
+ }>;
970
+ query: _sinclair_typebox.TObject<{
971
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
972
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
973
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
974
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
975
+ }>;
976
+ }, {}, Response>;
977
+ getProviderOAuthUrl: _spfn_core_route.RouteDef<{
978
+ params: _sinclair_typebox.TObject<{
979
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
980
+ }>;
981
+ body: _sinclair_typebox.TObject<{
982
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
983
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
984
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
985
+ }>;
986
+ }, {}, {
987
+ authUrl: string;
988
+ }>;
989
+ oauthNative: _spfn_core_route.RouteDef<{
990
+ params: _sinclair_typebox.TObject<{
991
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
992
+ }>;
993
+ body: _sinclair_typebox.TObject<{
994
+ idToken: _sinclair_typebox.TString;
995
+ nonce: _sinclair_typebox.TString;
996
+ accessToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
997
+ publicKey: _sinclair_typebox.TString;
998
+ keyId: _sinclair_typebox.TString;
999
+ fingerprint: _sinclair_typebox.TString;
1000
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1001
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1002
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1003
+ profile: _sinclair_typebox.TOptional<_sinclair_typebox.TObject<{
1004
+ name: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1005
+ }>>;
1006
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
1007
+ }>;
1008
+ }, {}, OAuthNativeResult>;
1009
+ oauthUnlinkNotify: _spfn_core_route.RouteDef<{
1010
+ params: _sinclair_typebox.TObject<{
1011
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
1012
+ }>;
1013
+ }, {}, void | Response>;
1014
+ oauthUnlinkNotifyGet: _spfn_core_route.RouteDef<{
1015
+ params: _sinclair_typebox.TObject<{
1016
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
1017
+ }>;
1018
+ }, {}, void | Response>;
632
1019
  getInvitation: _spfn_core_route.RouteDef<{
633
1020
  params: _sinclair_typebox.TObject<{
634
1021
  token: _sinclair_typebox.TString;
@@ -761,6 +1148,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
761
1148
  username: _sinclair_typebox.TUnion<[_sinclair_typebox.TString, _sinclair_typebox.TNull]>;
762
1149
  }>;
763
1150
  }, {}, {
1151
+ deletedAt: Date | null;
1152
+ deletedBy: string | null;
764
1153
  createdAt: Date;
765
1154
  updatedAt: Date;
766
1155
  id: number;
@@ -771,7 +1160,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
771
1160
  passwordHash: string | null;
772
1161
  passwordChangeRequired: boolean;
773
1162
  roleId: number;
774
- status: "active" | "inactive" | "suspended";
1163
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
775
1164
  emailVerifiedAt: Date | null;
776
1165
  phoneVerifiedAt: Date | null;
777
1166
  lastLoginAt: Date | null;
@@ -790,8 +1179,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
790
1179
  }, {}, {
791
1180
  roles: {
792
1181
  description: string | null;
793
- id: number;
794
1182
  name: string;
1183
+ id: number;
795
1184
  displayName: string;
796
1185
  isBuiltin: boolean;
797
1186
  isSystem: boolean;
@@ -812,8 +1201,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
812
1201
  }, {}, {
813
1202
  role: {
814
1203
  description: string | null;
815
- id: number;
816
1204
  name: string;
1205
+ id: number;
817
1206
  displayName: string;
818
1207
  isBuiltin: boolean;
819
1208
  isSystem: boolean;
@@ -836,8 +1225,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
836
1225
  }, {}, {
837
1226
  role: {
838
1227
  description: string | null;
839
- id: number;
840
1228
  name: string;
1229
+ id: number;
841
1230
  displayName: string;
842
1231
  isBuiltin: boolean;
843
1232
  isSystem: boolean;
@@ -865,13 +1254,87 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
865
1254
  }>;
866
1255
  }>;
867
1256
 
1257
+ /**
1258
+ * The auth-profile registry the authenticate middleware dispatches on.
1259
+ *
1260
+ * A request that names `x-spfn-auth-profile` is answered by the verifier
1261
+ * registered for that profile — an O(1) map lookup, never a per-profile if
1262
+ * chain in the middleware body. A request that names no profile falls through
1263
+ * to the existing Bearer path untouched.
1264
+ *
1265
+ * Every verifier converges on the same `AuthContext` the Bearer path sets, so
1266
+ * downstream permission/tenant code consumes one principal shape and never
1267
+ * branches on how it was authenticated.
1268
+ *
1269
+ * The clientProofV1 verifier reuses the phase-1 admission pieces (header
1270
+ * shape, canonical body, proof-input assembly, ECDSA verification) with two
1271
+ * production substitutions: the key directory is `user_public_keys` via
1272
+ * `keysRepository`, and the replay ledger is the pluggable store from
1273
+ * `client-proof/replay-store` (memory default, Redis opt-in). The admission
1274
+ * order is the contract's — revocation → session → expiry → replay → proof —
1275
+ * and the non-disclosure rule holds: an unregistered keyId shares
1276
+ * PROOF_INVALID with a failed signature, while a revoked or expired key
1277
+ * answers SESSION_REVOKED before the proof is ever examined.
1278
+ *
1279
+ * @module server/middleware/auth-profiles
1280
+ */
1281
+
1282
+ /** What a verified request leaves in the context — one shape for every scheme. */
868
1283
  interface AuthContext {
869
1284
  user: User;
870
1285
  userId: string;
871
1286
  keyId: string;
872
1287
  role: string | null;
873
1288
  locale: string;
1289
+ /** How the principal was authenticated. Informational — downstream code never branches on it. */
1290
+ scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken';
874
1291
  }
1292
+ /** A profile's verifier: admits the request and returns the principal, or throws. */
1293
+ interface AuthProfileVerifier {
1294
+ verify(c: Context): Promise<AuthContext>;
1295
+ }
1296
+ /**
1297
+ * Routes a request to its profile verifier.
1298
+ *
1299
+ * - no profile header → null: the caller continues on the Bearer path;
1300
+ * - profile header + Authorization header → rejected (mixing prohibited);
1301
+ * - unknown profile value → rejected (unknownProfilePolicy: reject).
1302
+ *
1303
+ * Shared by authenticate and optionalAuth so "presented but invalid" refuses
1304
+ * identically on both — only the "presented nothing" outcome differs.
1305
+ */
1306
+ declare function selectAuthProfile(c: Context): AuthProfileVerifier | null;
1307
+ /**
1308
+ * Loads the user for an authenticated key and applies the account-status
1309
+ * rules. One implementation for every scheme: the Bearer path and the profile
1310
+ * verifiers call this, so a status added here gates both identically.
1311
+ */
1312
+ declare function resolveAuthenticatedUser(userId: number): Promise<{
1313
+ user: User;
1314
+ role: string | null;
1315
+ locale: string;
1316
+ }>;
1317
+ /** What the profile path produced for one request. */
1318
+ type AuthProfileOutcome = {
1319
+ kind: 'none';
1320
+ } | {
1321
+ kind: 'authenticated';
1322
+ auth: AuthContext;
1323
+ } | {
1324
+ kind: 'refused';
1325
+ response: Response;
1326
+ };
1327
+ /**
1328
+ * The profile path from dispatch to answer — what `authenticate` and
1329
+ * `optionalAuth` both run before their own Bearer code.
1330
+ *
1331
+ * `none` means the request named no profile and the caller continues on the
1332
+ * Bearer path. A refusal comes back as a built response rather than a throw:
1333
+ * the answer a proven call gets is the contract's own envelope, and an error
1334
+ * handed to the generic error handler is classified by its class name instead.
1335
+ */
1336
+ declare function runAuthProfile(c: Context): Promise<AuthProfileOutcome>;
1337
+
875
1338
  declare module 'hono' {
876
1339
  interface ContextVariableMap {
877
1340
  auth: AuthContext;
@@ -937,4 +1400,4 @@ declare const authenticate: _spfn_core_route.NamedMiddleware<"auth">;
937
1400
  */
938
1401
  declare const optionalAuth: _spfn_core_route.NamedMiddleware<"optionalAuth">;
939
1402
 
940
- export { oauthCallbackService as $, type AuthSession as A, type LogoutParams as B, type CheckAccountExistsResult as C, type ChangePasswordParams as D, sendVerificationCodeService as E, verifyCodeService as F, type SendVerificationCodeParams as G, type VerifyCodeParams as H, type IssueOneTimeTokenResult as I, type VerifyCodeResult as J, KEY_ALGORITHM as K, type LoginResult as L, registerPublicKeyService as M, rotateKeyService as N, type OAuthStartResult as O, type PermissionConfig as P, revokeKeyService as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RegisterPublicKeyParams as T, type UserProfile as U, type VerificationTargetType as V, type RotateKeyParams as W, type RevokeKeyParams as X, issueOneTimeTokenService as Y, verifyOneTimeTokenService as Z, oauthStartService as _, type RegisterResult as a, buildOAuthErrorUrl as a0, isOAuthProviderEnabled as a1, getEnabledOAuthProviders as a2, getGoogleAccessToken as a3, type OAuthStartParams as a4, type OAuthCallbackParams as a5, type OAuthCallbackResult as a6, authenticate as a7, optionalAuth as a8, EmailSchema as a9, PhoneSchema as aa, PasswordSchema as ab, TargetTypeSchema as ac, VerificationPurposeSchema as ad, type RotateKeyResult as b, type ProfileInfo as c, INVITATION_STATUSES as d, USER_STATUSES as e, SOCIAL_PROVIDERS as f, type VerificationPurpose as g, VERIFICATION_TARGET_TYPES as h, VERIFICATION_PURPOSES as i, PERMISSION_CATEGORIES as j, type PermissionCategory as k, type AuthInitOptions as l, mainAuthRouter as m, type KeyAlgorithmType as n, type InvitationStatus as o, type UserStatus as p, type SocialProvider as q, type AuthContext as r, checkAccountExistsService as s, registerService as t, loginService as u, logoutService as v, changePasswordService as w, type CheckAccountExistsParams as x, type RegisterParams as y, type LoginParams as z };
1403
+ export { type UnlinkNotifyResult as $, type AuthInitOptions as A, PasswordSchema as B, type ChangePasswordParams as C, DeviceNameSchema as D, EmailSchema as E, PhoneSchema as F, PlatformSchema as G, type RegisterParams as H, type IssueOneTimeTokenResult as I, type RegisterPublicKeyParams as J, type KeySummary as K, type LoginResult as L, type RevokeAllKeysParams as M, type NativeVerifyOptions as N, type OAuthStartResult as O, type PermissionConfig as P, type RevokeKeyParams as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type RotateKeyParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type SendVerificationCodeParams as W, TargetTypeSchema as X, type UnlinkNotification as Y, UnlinkNotifyRejection as Z, type UnlinkNotifyRequest as _, type RegisterResult as a, VerificationPurposeSchema as a0, type VerifyCodeParams as a1, type VerifyCodeResult as a2, authenticate as a3, buildOAuthErrorUrl as a4, changePasswordService as a5, getEnabledOAuthProviders as a6, getGoogleAccessToken as a7, getOAuthProvider as a8, getRegisteredProviders as a9, isOAuthProviderEnabled as aa, issueOneTimeTokenService as ab, listKeysService as ac, loginService as ad, logoutService as ae, oauthCallbackService as af, oauthNativeService as ag, oauthStartService as ah, oauthUnlinkNotifyService as ai, optionalAuth as aj, registerOAuthProvider as ak, registerPublicKeyService as al, registerService as am, requireEnabledProvider as an, resolveAuthenticatedUser as ao, revokeAllKeysService as ap, revokeKeyService as aq, rotateKeyService as ar, runAuthProfile as as, selectAuthProfile as at, sendVerificationCodeService as au, verifyCodeService as av, verifyOneTimeTokenService as aw, type RotateKeyResult as b, type RevokeAllKeysResult as c, type OAuthNativeResult as d, type ProfileInfo as e, type AuthSession as f, PERMISSION_CATEGORIES as g, type PermissionCategory as h, VERIFICATION_TARGET_TYPES as i, type VerificationPurpose as j, type VerificationTargetType as k, type OAuthProvider as l, mainAuthRouter as m, type AuthContext as n, type AuthProfileOutcome as o, type AuthProfileVerifier as p, KEY_FINGERPRINT_PREFIX_LENGTH as q, type LoginParams as r, type LogoutParams as s, type NormalizedIdentity as t, type OAuthCallbackParams as u, type OAuthCallbackResult as v, type OAuthCodeExchangeOptions as w, type OAuthNativeParams as x, type OAuthStartParams as y, type OAuthTokens as z };