@spfn/auth 0.3.0-beta.2 → 0.3.0-beta.20

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 (43) hide show
  1. package/README.md +1382 -23
  2. package/dist/client-proof.d.ts +45 -15
  3. package/dist/client-proof.js +198 -4
  4. package/dist/client-proof.js.map +1 -1
  5. package/dist/client.d.ts +92 -1
  6. package/dist/client.js +58 -0
  7. package/dist/client.js.map +1 -1
  8. package/dist/config.d.ts +302 -0
  9. package/dist/config.js +134 -4
  10. package/dist/config.js.map +1 -1
  11. package/dist/errors.d.ts +370 -3
  12. package/dist/errors.js +245 -2
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +185 -2
  15. package/dist/index.js +256 -2
  16. package/dist/index.js.map +1 -1
  17. package/dist/machine-principals-BD4tnASp.d.ts +2739 -0
  18. package/dist/nextjs/api.js +350 -12
  19. package/dist/nextjs/api.js.map +1 -1
  20. package/dist/nextjs/client.d.ts +28 -1
  21. package/dist/nextjs/client.js +24 -3
  22. package/dist/nextjs/client.js.map +1 -1
  23. package/dist/nextjs/server.d.ts +173 -3
  24. package/dist/nextjs/server.js +372 -10
  25. package/dist/nextjs/server.js.map +1 -1
  26. package/dist/server.d.ts +3761 -414
  27. package/dist/server.js +5865 -1043
  28. package/dist/server.js.map +1 -1
  29. package/dist/{session-DTHahDQ9.d.ts → session-Dfwu5g2W.d.ts} +28 -1
  30. package/migrations/20260810112144_colorful_tomorrow_man/migration.sql +18 -0
  31. package/migrations/20260810112144_colorful_tomorrow_man/snapshot.json +3576 -0
  32. package/migrations/20260901091716_fine_arclight/migration.sql +21 -0
  33. package/migrations/20260901091716_fine_arclight/snapshot.json +3849 -0
  34. package/migrations/20260906155957_natural_moonstone/migration.sql +33 -0
  35. package/migrations/20260906155957_natural_moonstone/snapshot.json +4275 -0
  36. package/migrations/20260907020904_giant_eternals/migration.sql +21 -0
  37. package/migrations/20260907020904_giant_eternals/snapshot.json +4561 -0
  38. package/migrations/20260907044807_eminent_angel/migration.sql +2 -0
  39. package/migrations/20260907044807_eminent_angel/snapshot.json +4561 -0
  40. package/migrations/20260918083158_foamy_roughhouse/migration.sql +55 -0
  41. package/migrations/20260918083158_foamy_roughhouse/snapshot.json +5271 -0
  42. package/package.json +9 -6
  43. package/dist/authenticate-55LeXHqZ.d.ts +0 -1447
@@ -0,0 +1,2739 @@
1
+ import * as _simplewebauthn_server from '@simplewebauthn/server';
2
+ import { RegistrationResponseJSON, AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
3
+ import * as _spfn_core_route from '@spfn/core/route';
4
+ import { K as KeyAlgorithmType, h as KeyPlatformType, j as SocialProvider } from './types-DYyhze28.js';
5
+ import * as _sinclair_typebox from '@sinclair/typebox';
6
+ import { Static } from '@sinclair/typebox';
7
+ import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
8
+ import { Context } from 'hono';
9
+ import { User } from '@spfn/auth/server';
10
+
11
+ /**
12
+ * Role information for client/API responses
13
+ */
14
+ interface Role {
15
+ id: number;
16
+ name: string;
17
+ displayName: string;
18
+ description: string | null;
19
+ isBuiltin: boolean;
20
+ isSystem: boolean;
21
+ isActive: boolean;
22
+ priority: number;
23
+ createdAt: Date;
24
+ updatedAt: Date;
25
+ }
26
+ /**
27
+ * Permission information for client/API responses
28
+ */
29
+ interface Permission {
30
+ id: number;
31
+ name: string;
32
+ displayName: string;
33
+ description: string | null;
34
+ category: string | null;
35
+ isBuiltin: boolean;
36
+ isSystem: boolean;
37
+ isActive: boolean;
38
+ metadata: Record<string, any> | null;
39
+ createdAt: Date;
40
+ updatedAt: Date;
41
+ }
42
+ interface AuthSession {
43
+ userId: number;
44
+ publicId: string;
45
+ email: string | null;
46
+ emailVerified: boolean;
47
+ phoneVerified: boolean;
48
+ hasPassword: boolean;
49
+ role: Role;
50
+ permissions: Permission[];
51
+ }
52
+ interface ProfileInfo {
53
+ profileId: number;
54
+ displayName: string | null;
55
+ firstName: string | null;
56
+ lastName: string | null;
57
+ avatarUrl: string | null;
58
+ bio: string | null;
59
+ locale: string;
60
+ timezone: string;
61
+ website: string | null;
62
+ location: string | null;
63
+ company: string | null;
64
+ jobTitle: string | null;
65
+ metadata: Record<string, any> | null;
66
+ createdAt: Date;
67
+ updatedAt: Date;
68
+ }
69
+ /**
70
+ * User Profile Response
71
+ *
72
+ * Complete user data including:
73
+ * - User fields at top level (userId, email, etc.)
74
+ * - Profile data as nested field (optional)
75
+ *
76
+ * Excludes:
77
+ * - Role and permissions (use auth session API)
78
+ */
79
+ interface UserProfile {
80
+ userId: number;
81
+ publicId: string;
82
+ email: string | null;
83
+ username: string | null;
84
+ emailVerified: boolean;
85
+ phoneVerified: boolean;
86
+ lastLoginAt: Date | null;
87
+ createdAt: Date;
88
+ updatedAt: Date;
89
+ profile: ProfileInfo | null;
90
+ }
91
+
92
+ /**
93
+ * @spfn/auth - Auth Service
94
+ *
95
+ * Core authentication logic: registration, login, logout, password management
96
+ */
97
+
98
+ interface RegisterParams {
99
+ email?: string;
100
+ phone?: string;
101
+ verificationToken: string;
102
+ password: string;
103
+ publicKey: string;
104
+ keyId: string;
105
+ fingerprint: string;
106
+ algorithm?: KeyAlgorithmType;
107
+ deviceName?: string;
108
+ platform?: KeyPlatformType;
109
+ metadata?: Record<string, unknown>;
110
+ }
111
+ interface RegisterResult {
112
+ userId: string;
113
+ publicId: string;
114
+ email?: string;
115
+ phone?: string;
116
+ }
117
+ interface LoginParams {
118
+ email?: string;
119
+ phone?: string;
120
+ password: string;
121
+ publicKey: string;
122
+ keyId: string;
123
+ fingerprint: string;
124
+ oldKeyId?: string;
125
+ algorithm?: KeyAlgorithmType;
126
+ deviceName?: string;
127
+ platform?: KeyPlatformType;
128
+ }
129
+ interface LoginResult {
130
+ userId: string;
131
+ publicId: string;
132
+ email?: string;
133
+ phone?: string;
134
+ passwordChangeRequired: boolean;
135
+ }
136
+ interface LogoutParams {
137
+ userId: number;
138
+ keyId: string;
139
+ }
140
+ interface ChangePasswordParams {
141
+ userId: number;
142
+ currentPassword?: string;
143
+ newPassword: string;
144
+ passwordHash?: string;
145
+ }
146
+ /**
147
+ * Register a new user account
148
+ */
149
+ declare function registerService(params: RegisterParams): Promise<RegisterResult>;
150
+ /**
151
+ * Authenticate user and create session
152
+ */
153
+ declare function loginService(params: LoginParams): Promise<LoginResult>;
154
+ /**
155
+ * Logout user (revoke current key)
156
+ */
157
+ declare function logoutService(params: LogoutParams): Promise<void>;
158
+ /**
159
+ * Change user password
160
+ */
161
+ declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
162
+
163
+ declare const EmailSchema: _sinclair_typebox.TString;
164
+ declare const PhoneSchema: _sinclair_typebox.TString;
165
+ /**
166
+ * Optional device labels a client may send when registering a key.
167
+ *
168
+ * Display only: the key list uses them to tell one device from another, and
169
+ * nothing is authorized or refused by either value, so a client that lies about
170
+ * them gains nothing. Both are omitted by every key registered before they
171
+ * existed, hence optional rather than defaulted.
172
+ */
173
+ declare const DeviceNameSchema: _sinclair_typebox.TString;
174
+ declare const PlatformSchema: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>;
175
+ /**
176
+ * Key material as a device sends it, bounded.
177
+ *
178
+ * The bounds exist for the one route that takes this material from a caller who
179
+ * has not authenticated and cannot: `POST /_auth/device/start` persists what it
180
+ * is given, and a correctly fingerprinted megabyte of base64 would sit in
181
+ * `device_authorizations` until something swept it — and nothing sweeps it.
182
+ *
183
+ * The numbers are what real key material measures, with room to spare. The
184
+ * package's own generators produce SPKI DER in base64: 124 characters for
185
+ * ES256 (P-256), 392 for RS256 (RSA-2048). An RSA-4096 key would be 736, an
186
+ * RSA-8192 key about 1400, and the same 4096-bit key PEM-armoured about 800 —
187
+ * so 2048 admits every shape of key anyone could reasonably present, while a
188
+ * megabyte is refused three orders of magnitude before it reaches a row.
189
+ *
190
+ * `keyId` is a UUID (36) everywhere this package generates one; 64 leaves room
191
+ * for a client that prefixes or namespaces its own. `fingerprint` is SHA-256
192
+ * hex, exactly 64, and nothing else can ever verify against the public key —
193
+ * 128 is the length a longer digest would need, and no more.
194
+ */
195
+ declare const PublicKeySchema: _sinclair_typebox.TString;
196
+ declare const KeyIdSchema: _sinclair_typebox.TString;
197
+ declare const FingerprintSchema: _sinclair_typebox.TString;
198
+ /**
199
+ * The code a person reads off the waiting device and types on their own.
200
+ *
201
+ * Loose on purpose: 8 characters plus an optional dash is what is shown, but the
202
+ * server folds whitespace, dashes and lower case away before looking anything up,
203
+ * so refusing those spellings here would refuse a code that is on screen. The
204
+ * bounds exist to stop an unbounded string reaching the database, not to spell
205
+ * out the format — `USER_CODE_ALPHABET` is the only thing that can match a row.
206
+ */
207
+ declare const UserCodeSchema: _sinclair_typebox.TString;
208
+ /**
209
+ * What `POST /_auth/device/poll` answers with.
210
+ *
211
+ * A union, because the two answers are different kinds of thing rather than one
212
+ * shape with optional fields: pending says "ask again in this long", approved is
213
+ * a completed login carrying exactly what `/_auth/login` returns. `status` is the
214
+ * discriminant, so a generated client narrows on it instead of testing which
215
+ * fields happen to be present.
216
+ *
217
+ * The mobile contract has no union type, so it exports this as one object with
218
+ * `status` required and every branch field optional — see
219
+ * `deviceAuthorization.pollStatusRule` in the bundle. `intervalMillis` is an
220
+ * integer for the same reason: that grammar carries no floating-point scalar,
221
+ * and a count of milliseconds never needed one.
222
+ *
223
+ * That integer is a promise two things keep, because nothing validates a response
224
+ * against this schema on the way out. `configureDeviceAuth` refuses an interval
225
+ * that is not a whole number of milliseconds, so the only value this branch can
226
+ * carry is one; and `contract-export.test.ts` reads this schema to check the
227
+ * exported declaration, so writing `Type.Number` here fails the suite instead of
228
+ * publishing an integer the server does not send.
229
+ */
230
+ declare const DeviceAuthPollResponseSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TObject<{
231
+ status: _sinclair_typebox.TLiteral<"pending">;
232
+ intervalMillis: _sinclair_typebox.TInteger;
233
+ }>, _sinclair_typebox.TObject<{
234
+ status: _sinclair_typebox.TLiteral<"approved">;
235
+ userId: _sinclair_typebox.TString;
236
+ publicId: _sinclair_typebox.TString;
237
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
238
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
239
+ passwordChangeRequired: _sinclair_typebox.TBoolean;
240
+ }>]>;
241
+ declare const PasswordSchema: _sinclair_typebox.TString;
242
+ declare const TargetTypeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
243
+ type VerificationTargetType = Static<typeof TargetTypeSchema>;
244
+ declare const VERIFICATION_TARGET_TYPES: readonly ["email", "phone"];
245
+ 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">]>;
246
+ type VerificationPurpose = Static<typeof VerificationPurposeSchema>;
247
+ declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
248
+
249
+ /**
250
+ * @spfn/auth - Verification Service
251
+ *
252
+ * Handles OTP code generation, validation, and delivery
253
+ */
254
+
255
+ interface SendVerificationCodeParams {
256
+ target: string;
257
+ targetType: VerificationTargetType;
258
+ purpose: VerificationPurpose;
259
+ }
260
+ interface SendVerificationCodeResult {
261
+ success: boolean;
262
+ expiresAt: string;
263
+ }
264
+ interface VerifyCodeParams {
265
+ target: string;
266
+ targetType: VerificationTargetType;
267
+ code: string;
268
+ purpose: VerificationPurpose;
269
+ }
270
+ interface VerifyCodeResult {
271
+ valid: boolean;
272
+ verificationToken: string;
273
+ }
274
+ /**
275
+ * Send verification code via email or SMS
276
+ */
277
+ declare function sendVerificationCodeService(params: SendVerificationCodeParams): Promise<SendVerificationCodeResult>;
278
+ /**
279
+ * Verify OTP code and return verification token
280
+ */
281
+ declare function verifyCodeService(params: VerifyCodeParams): Promise<{
282
+ valid: boolean;
283
+ verificationToken: string;
284
+ }>;
285
+
286
+ /**
287
+ * @spfn/auth - Verified-Email Signup Service
288
+ *
289
+ * A signup where the address is proven before a password exists:
290
+ *
291
+ * request -> a one-time link is emailed
292
+ * confirm -> the link is exchanged for a short-lived password-setup session
293
+ * password -> the account is created, the device registered, the user signed in
294
+ *
295
+ * The link token and the setup secret are bearer credentials, so neither is ever
296
+ * stored. Only their SHA-256 hashes are, and lookup is by hash. A database dump
297
+ * therefore yields nothing that can be presented to either step.
298
+ *
299
+ * The six-digit-code registration path is untouched and remains the default; this
300
+ * is a second entry point to the same account creation, not a replacement.
301
+ */
302
+
303
+ interface RequestSignupLinkParams {
304
+ email: string;
305
+ returnPath?: string;
306
+ }
307
+ interface RequestSignupLinkResult {
308
+ success: boolean;
309
+ expiresAt: string;
310
+ }
311
+ /**
312
+ * Step 1 — issue a confirmation link for an address.
313
+ *
314
+ * Answers identically whether or not the address already has an account. When it
315
+ * does, the owner gets a notice instead of a usable link, through the same
316
+ * dedupe window the six-digit-code path uses.
317
+ *
318
+ * Requesting again is how a resend works: every live link for the address is
319
+ * superseded first, so the newest link is the only one that opens, and any setup
320
+ * session already opened from an older link dies with it.
321
+ *
322
+ * Neither branch sends mail: both hand it to `auth.link-mail`, so the answer
323
+ * costs the same database work whichever one ran. With no pg-boss initialised
324
+ * the mail still goes out on this request — see `lib/link-mail-delivery.ts`.
325
+ */
326
+ declare function requestSignupLinkService(params: RequestSignupLinkParams): Promise<RequestSignupLinkResult>;
327
+ interface ConfirmSignupLinkParams {
328
+ token: string;
329
+ }
330
+ interface ConfirmSignupLinkResult {
331
+ email: string;
332
+ returnPath: string | null;
333
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
334
+ setupSecret: string;
335
+ setupExpiresAt: string;
336
+ }
337
+ /**
338
+ * Step 2 — exchange a link for a password-setup session.
339
+ *
340
+ * Nothing binds the row to a device or a browser, which is what lets someone
341
+ * request the link on a laptop and open it on a phone.
342
+ */
343
+ declare function confirmSignupLinkService(params: ConfirmSignupLinkParams): Promise<ConfirmSignupLinkResult>;
344
+ interface CompleteSignupParams {
345
+ setupSecret?: string;
346
+ password: string;
347
+ publicKey: string;
348
+ keyId: string;
349
+ fingerprint: string;
350
+ algorithm?: KeyAlgorithmType;
351
+ deviceName?: string;
352
+ platform?: KeyPlatformType;
353
+ metadata?: Record<string, unknown>;
354
+ }
355
+ /**
356
+ * Step 3 — set the password, which is what creates the account.
357
+ *
358
+ * Run under `Transactional()`: the user row, the device key and the completion
359
+ * mark commit together. A device-key failure must not leave an account nobody
360
+ * can sign into, and a completion mark must not survive a rolled-back account.
361
+ *
362
+ * A refusal that is the user's to fix — a weak password, an app policy that
363
+ * rejects the registration — leaves the setup session usable, so the fix is
364
+ * retyping the password rather than requesting a fresh email.
365
+ */
366
+ declare function completeSignupService(params: CompleteSignupParams): Promise<RegisterResult>;
367
+
368
+ /**
369
+ * @spfn/auth - Password Reset Service
370
+ *
371
+ * Getting back into an account whose password is gone, using the address the
372
+ * account already proved:
373
+ *
374
+ * request -> a one-time link is emailed
375
+ * confirm -> the link is exchanged for a short-lived password-setup session
376
+ * complete -> the new password is written, everything else is signed out,
377
+ * and the browser that reset is signed in on a fresh device key
378
+ *
379
+ * Mirrors the verified-email signup slice deliberately — same credentials, same
380
+ * hashing, same supersede-on-resend, same interceptor moves — with two
381
+ * differences that matter.
382
+ *
383
+ * First, the request answers identically for *every* input and sends mail only
384
+ * to an account that can be reset. Signup can afford to tell an existing owner
385
+ * "you already have an account"; a reset cannot send anything to a stranger's
386
+ * mailbox, because the mail itself would be the answer to "does this address
387
+ * have an account here".
388
+ *
389
+ * Second, completing it is a credential change on a live account, so it carries
390
+ * the same blast radius as `changePasswordService`: pending device
391
+ * authorizations are denied and every active key is revoked. Whoever was signed
392
+ * in on the old password is signed out, including the attacker the reset was
393
+ * needed for.
394
+ */
395
+
396
+ interface RequestPasswordResetParams {
397
+ email: string;
398
+ returnPath?: string;
399
+ }
400
+ interface RequestPasswordResetResult {
401
+ success: boolean;
402
+ expiresAt: string;
403
+ }
404
+ /**
405
+ * Step 1 — issue a reset link for an address.
406
+ *
407
+ * Answers identically for every input: the same status, the same two fields, and
408
+ * an `expiresAt` computed the same way whether or not a row was written. An
409
+ * address with no account, an account that cannot be reset, and an account that
410
+ * can are indistinguishable to the caller — only the first of the three gets
411
+ * mail, and it goes to the owner.
412
+ *
413
+ * Requesting again is how a resend works: every live link for the account is
414
+ * superseded first, so the newest link is the only one that opens, and any setup
415
+ * session already opened from an older link dies with it.
416
+ *
417
+ * The eligible branch does not send the mail either — it hands it to
418
+ * `auth.link-mail` — so the two branches differ by a few database writes and not
419
+ * by a mail provider's round trip. With no pg-boss initialised the mail still
420
+ * goes out on this request; see `lib/link-mail-delivery.ts`.
421
+ */
422
+ declare function requestPasswordResetService(params: RequestPasswordResetParams): Promise<RequestPasswordResetResult>;
423
+ interface ConfirmPasswordResetParams {
424
+ token: string;
425
+ }
426
+ interface ConfirmPasswordResetResult {
427
+ email: string;
428
+ returnPath: string | null;
429
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
430
+ setupSecret: string;
431
+ setupExpiresAt: string;
432
+ }
433
+ /**
434
+ * Step 2 — exchange a link for a password-setup session.
435
+ *
436
+ * Nothing binds the row to a device or a browser, which is what lets someone ask
437
+ * for the link on a laptop and open it on a phone.
438
+ */
439
+ declare function confirmPasswordResetService(params: ConfirmPasswordResetParams): Promise<ConfirmPasswordResetResult>;
440
+ interface CompletePasswordResetParams {
441
+ setupSecret?: string;
442
+ password: string;
443
+ publicKey: string;
444
+ keyId: string;
445
+ fingerprint: string;
446
+ algorithm?: KeyAlgorithmType;
447
+ deviceName?: string;
448
+ platform?: KeyPlatformType;
449
+ }
450
+ /**
451
+ * Step 3 — set the new password, which is what completes the reset.
452
+ *
453
+ * Run under `Transactional()`: the password, the revocations, the new device key
454
+ * and the completion mark commit together. A key registration that failed after
455
+ * the revoke-all would otherwise leave an account with a new password and
456
+ * nothing signed in.
457
+ *
458
+ * A refusal that is the user's to fix — a password the policy rejects, a body
459
+ * with no device key — leaves the setup session usable, so the fix is retyping
460
+ * the password rather than asking for a fresh email.
461
+ */
462
+ declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<RegisterResult>;
463
+
464
+ /**
465
+ * @spfn/auth - Device Auth Service
466
+ *
467
+ * Device-code login: a device with no key on file yet shows a short code, the
468
+ * account owner types that code on a device that is already signed in, and the
469
+ * waiting device's key is registered on approval.
470
+ *
471
+ * There is no token to hand over. Every request in this system is signed by the
472
+ * calling device's own key, so "logging a device in" means one thing — getting
473
+ * its public key into `user_public_keys` under the right account. That is what
474
+ * the poll does, and it is why the poll returns exactly what `loginService`
475
+ * returns: from the client's side the two ways in are indistinguishable.
476
+ *
477
+ * | state ↓ op → | info | approve | deny | poll |
478
+ * | --- | --- | --- | --- | --- |
479
+ * | pending | device details | → approved | → denied | pending |
480
+ * | approved | AlreadyHandled | AlreadyHandled | AlreadyHandled | key registered, → consumed |
481
+ * | denied | AlreadyHandled | AlreadyHandled | AlreadyHandled | Denied |
482
+ * | consumed | NotFound | NotFound | NotFound | NotFound |
483
+ * | expired | Expired | Expired | Expired | Expired |
484
+ * | unknown | NotFound | NotFound | NotFound | NotFound |
485
+ *
486
+ * A global revocation — revoke-all, a password change, a deletion request —
487
+ * refuses the account's live records too, as `denied`, so they land in that row
488
+ * of the table. See `denyAllActiveByUserId`; the three callers are the three
489
+ * places that revoke every key at once.
490
+ */
491
+
492
+ interface StartDeviceAuthParams {
493
+ publicKey: string;
494
+ keyId: string;
495
+ fingerprint: string;
496
+ algorithm?: KeyAlgorithmType;
497
+ /** Device label shown to the approver. Display only — nothing is authorized by it. */
498
+ deviceName?: string;
499
+ platform?: KeyPlatformType;
500
+ }
501
+ interface StartDeviceAuthResult {
502
+ /** Returned once. The waiting device polls with it; the server stores only its hash. */
503
+ deviceCode: string;
504
+ /** `XXXX-XXXX`, for the waiting device's screen and nowhere else. */
505
+ userCode: string;
506
+ expiresAtMillis: number;
507
+ /** Milliseconds the waiting device should wait between polls. */
508
+ intervalMillis: number;
509
+ }
510
+ interface DeviceAuthInfoParams {
511
+ userCode: string;
512
+ }
513
+ /** What the approver is shown about the device asking to be let in. */
514
+ interface DeviceAuthInfoResult {
515
+ deviceName?: string;
516
+ /** One of `KEY_PLATFORM`, which is what the route accepts and the column stores. */
517
+ platform?: KeyPlatformType;
518
+ /** First bytes of the pending key's fingerprint, as the device list truncates it. */
519
+ fingerprintPrefix: string;
520
+ requestedAtMillis: number;
521
+ expiresAtMillis: number;
522
+ }
523
+ interface ApproveDeviceAuthParams {
524
+ userCode: string;
525
+ /** The approver, read from their session. Never from a request body. */
526
+ userId: number;
527
+ }
528
+ interface DenyDeviceAuthParams {
529
+ userCode: string;
530
+ }
531
+ interface PollDeviceAuthParams {
532
+ deviceCode: string;
533
+ }
534
+ /** Nobody has answered yet. Not an error — the waiting device waits. */
535
+ interface DeviceAuthPendingResult {
536
+ status: 'pending';
537
+ intervalMillis: number;
538
+ }
539
+ /** Approved and spent: the key is registered and this is the login it produced. */
540
+ type DeviceAuthApprovedResult = {
541
+ status: 'approved';
542
+ } & LoginResult;
543
+ type PollDeviceAuthResult = DeviceAuthPendingResult | DeviceAuthApprovedResult;
544
+ /**
545
+ * Park a new device's key and hand back the codes it needs.
546
+ *
547
+ * The caller is unauthenticated by definition — this is what a device does before
548
+ * it has any way to prove anything — so nothing here is attributed to an account.
549
+ * The record gains an owner only when someone approves it.
550
+ */
551
+ declare function startDeviceAuthService(params: StartDeviceAuthParams): Promise<StartDeviceAuthResult>;
552
+ /**
553
+ * What the approver sees before deciding.
554
+ *
555
+ * This is the whole defence against being talked into approving someone else's
556
+ * device: the answer names the device that is waiting, so the person holding the
557
+ * phone can see that it is not theirs. An approval screen that showed only the
558
+ * code would be asking them to confirm a number they were just told.
559
+ */
560
+ declare function getDeviceAuthInfoService(params: DeviceAuthInfoParams): Promise<DeviceAuthInfoResult>;
561
+ /**
562
+ * Bind the record to the approving account.
563
+ *
564
+ * The key is not registered here. The waiting device may never come back, and a
565
+ * key registered for a device that stopped listening is a signing credential
566
+ * nobody asked for — so approval records the decision and the poll acts on it.
567
+ *
568
+ * Answers with the same device description `info` returns, so a client that let
569
+ * a user approve without looking first can still show them what they just let
570
+ * in — which is the moment someone talked into approving an attacker's device
571
+ * has to notice and revoke it.
572
+ */
573
+ declare function approveDeviceAuthService(params: ApproveDeviceAuthParams): Promise<DeviceAuthInfoResult>;
574
+ /**
575
+ * Refuse the record, so the waiting device is told no instead of timing out.
576
+ *
577
+ * Denying binds no user: the point of refusing is that the account owner wants
578
+ * nothing to do with the request.
579
+ */
580
+ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<void>;
581
+ /**
582
+ * The waiting device asking whether anyone has answered.
583
+ *
584
+ * Approved is the one branch with a side effect, and it is a one-shot: the record
585
+ * is spent by a conditional update that names `approved`, so of two polls that
586
+ * arrive together exactly one registers the key. The loser matches nothing and is
587
+ * answered as if the code were unknown — which by then it is.
588
+ */
589
+ declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
590
+
591
+ /**
592
+ * @spfn/auth - Passkeys Entity
593
+ *
594
+ * A WebAuthn credential the account owner enrolled on one of their devices.
595
+ * It is a *credential*, not a session: an assertion proves who is asking, and
596
+ * the ordinary device key in `user_public_keys` is what the request afterwards
597
+ * is signed with. The two tables therefore never stand in for each other.
598
+ *
599
+ * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
600
+ * construction and `credentialId` is a handle the authenticator hands to any
601
+ * origin that asks — storing either in the clear costs nothing, and the lookup
602
+ * on `credentialId` has to be a plain equality match on an indexed column.
603
+ *
604
+ * Revocation is soft, and `credentialId` stays unique across live and revoked
605
+ * rows alike: a credential someone cut off must never become enrollable again,
606
+ * on this account or on another one.
607
+ */
608
+ /**
609
+ * Whether the credential can leave the authenticator that minted it.
610
+ *
611
+ * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
612
+ * `singleDevice` is bound to one authenticator. Reported by the authenticator at
613
+ * enrollment and shown in the management list, because "this one is only on that
614
+ * phone" is what the owner needs to know before revoking the other entry.
615
+ */
616
+ declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
617
+ type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
618
+ /** How long a label may be — the key list's `deviceName` bound, for the same reason. */
619
+ declare const PASSKEY_LABEL_MAX_LENGTH = 64;
620
+ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
621
+ name: "passkeys";
622
+ schema: string;
623
+ columns: {
624
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>, {
625
+ name: string;
626
+ tableName: "passkeys";
627
+ dataType: "object date";
628
+ data: Date;
629
+ driverParam: string;
630
+ notNull: true;
631
+ hasDefault: true;
632
+ isPrimaryKey: false;
633
+ isAutoincrement: false;
634
+ hasRuntimeDefault: false;
635
+ enumValues: undefined;
636
+ identity: undefined;
637
+ generated: undefined;
638
+ }>;
639
+ updatedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.PgTimestampBuilder>>>, {
640
+ name: string;
641
+ tableName: "passkeys";
642
+ dataType: "object date";
643
+ data: Date;
644
+ driverParam: string;
645
+ notNull: true;
646
+ hasDefault: true;
647
+ isPrimaryKey: false;
648
+ isAutoincrement: false;
649
+ hasRuntimeDefault: false;
650
+ enumValues: undefined;
651
+ identity: undefined;
652
+ generated: undefined;
653
+ }>;
654
+ id: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
655
+ name: string;
656
+ tableName: "passkeys";
657
+ dataType: "number int53";
658
+ data: number;
659
+ driverParam: number;
660
+ notNull: true;
661
+ hasDefault: true;
662
+ isPrimaryKey: false;
663
+ isAutoincrement: false;
664
+ hasRuntimeDefault: false;
665
+ enumValues: undefined;
666
+ identity: undefined;
667
+ generated: undefined;
668
+ }>;
669
+ userId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
670
+ name: string;
671
+ tableName: "passkeys";
672
+ dataType: "number int53";
673
+ data: number;
674
+ driverParam: string | number;
675
+ notNull: true;
676
+ hasDefault: false;
677
+ isPrimaryKey: false;
678
+ isAutoincrement: false;
679
+ hasRuntimeDefault: false;
680
+ enumValues: undefined;
681
+ identity: undefined;
682
+ generated: undefined;
683
+ }>;
684
+ credentialId: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
685
+ name: string;
686
+ tableName: "passkeys";
687
+ dataType: "string";
688
+ data: string;
689
+ driverParam: string;
690
+ notNull: true;
691
+ hasDefault: false;
692
+ isPrimaryKey: false;
693
+ isAutoincrement: false;
694
+ hasRuntimeDefault: false;
695
+ enumValues: undefined;
696
+ identity: undefined;
697
+ generated: undefined;
698
+ }>;
699
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
700
+ name: string;
701
+ tableName: "passkeys";
702
+ dataType: "string";
703
+ data: string;
704
+ driverParam: string;
705
+ notNull: true;
706
+ hasDefault: false;
707
+ isPrimaryKey: false;
708
+ isAutoincrement: false;
709
+ hasRuntimeDefault: false;
710
+ enumValues: undefined;
711
+ identity: undefined;
712
+ generated: undefined;
713
+ }>;
714
+ counter: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgIntegerBuilder>>, {
715
+ name: string;
716
+ tableName: "passkeys";
717
+ dataType: "number int32";
718
+ data: number;
719
+ driverParam: string | number;
720
+ notNull: true;
721
+ hasDefault: true;
722
+ isPrimaryKey: false;
723
+ isAutoincrement: false;
724
+ hasRuntimeDefault: false;
725
+ enumValues: undefined;
726
+ identity: undefined;
727
+ generated: undefined;
728
+ }>;
729
+ transports: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetDimensions<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, 1>, {
730
+ name: string;
731
+ tableName: "passkeys";
732
+ dataType: "string";
733
+ data: string[];
734
+ driverParam: string | string[];
735
+ notNull: false;
736
+ hasDefault: false;
737
+ isPrimaryKey: false;
738
+ isAutoincrement: false;
739
+ hasRuntimeDefault: false;
740
+ enumValues: undefined;
741
+ identity: undefined;
742
+ generated: undefined;
743
+ }>;
744
+ deviceType: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["singleDevice", "multiDevice"] & [string, ...string[]]>>, {
745
+ name: string;
746
+ tableName: "passkeys";
747
+ dataType: "string enum";
748
+ data: "singleDevice" | "multiDevice";
749
+ driverParam: string;
750
+ notNull: true;
751
+ hasDefault: false;
752
+ isPrimaryKey: false;
753
+ isAutoincrement: false;
754
+ hasRuntimeDefault: false;
755
+ enumValues: ["singleDevice", "multiDevice"] & [string, ...string[]];
756
+ identity: undefined;
757
+ generated: undefined;
758
+ }>;
759
+ backedUp: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
760
+ name: string;
761
+ tableName: "passkeys";
762
+ dataType: "boolean";
763
+ data: boolean;
764
+ driverParam: boolean;
765
+ notNull: true;
766
+ hasDefault: true;
767
+ isPrimaryKey: false;
768
+ isAutoincrement: false;
769
+ hasRuntimeDefault: false;
770
+ enumValues: undefined;
771
+ identity: undefined;
772
+ generated: undefined;
773
+ }>;
774
+ aaguid: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
775
+ name: string;
776
+ tableName: "passkeys";
777
+ dataType: "string";
778
+ data: string;
779
+ driverParam: string;
780
+ notNull: false;
781
+ hasDefault: false;
782
+ isPrimaryKey: false;
783
+ isAutoincrement: false;
784
+ hasRuntimeDefault: false;
785
+ enumValues: undefined;
786
+ identity: undefined;
787
+ generated: undefined;
788
+ }>;
789
+ label: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
790
+ name: string;
791
+ tableName: "passkeys";
792
+ dataType: "string";
793
+ data: string;
794
+ driverParam: string;
795
+ notNull: false;
796
+ hasDefault: false;
797
+ isPrimaryKey: false;
798
+ isAutoincrement: false;
799
+ hasRuntimeDefault: false;
800
+ enumValues: undefined;
801
+ identity: undefined;
802
+ generated: undefined;
803
+ }>;
804
+ lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
805
+ name: string;
806
+ tableName: "passkeys";
807
+ dataType: "object date";
808
+ data: Date;
809
+ driverParam: string;
810
+ notNull: false;
811
+ hasDefault: false;
812
+ isPrimaryKey: false;
813
+ isAutoincrement: false;
814
+ hasRuntimeDefault: false;
815
+ enumValues: undefined;
816
+ identity: undefined;
817
+ generated: undefined;
818
+ }>;
819
+ revokedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
820
+ name: string;
821
+ tableName: "passkeys";
822
+ dataType: "object date";
823
+ data: Date;
824
+ driverParam: string;
825
+ notNull: false;
826
+ hasDefault: false;
827
+ isPrimaryKey: false;
828
+ isAutoincrement: false;
829
+ hasRuntimeDefault: false;
830
+ enumValues: undefined;
831
+ identity: undefined;
832
+ generated: undefined;
833
+ }>;
834
+ revokedReason: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
835
+ name: string;
836
+ tableName: "passkeys";
837
+ dataType: "string";
838
+ data: string;
839
+ driverParam: string;
840
+ notNull: false;
841
+ hasDefault: false;
842
+ isPrimaryKey: false;
843
+ isAutoincrement: false;
844
+ hasRuntimeDefault: false;
845
+ enumValues: undefined;
846
+ identity: undefined;
847
+ generated: undefined;
848
+ }>;
849
+ };
850
+ dialect: "pg";
851
+ }>;
852
+ type Passkey = typeof passkeys.$inferSelect;
853
+ type NewPasskey = typeof passkeys.$inferInsert;
854
+
855
+ /**
856
+ * @spfn/auth - Passkey Service
857
+ *
858
+ * WebAuthn passkeys as an optional account credential, alongside a password and
859
+ * a linked social account rather than in place of either.
860
+ *
861
+ * enroll -> options on an identified session, then verify the attestation
862
+ * sign in -> options with no identifier at all, then verify the assertion
863
+ * manage -> list, rename, revoke
864
+ *
865
+ * A passkey is not a device key. The assertion proves *who* is asking; the
866
+ * device key registered right after it is what every later request is signed
867
+ * with, exactly as after a password login (D2). Nothing in clientProofV1 or in
868
+ * the JWT path changes because a session started this way.
869
+ *
870
+ * Challenges are rows, spent by one conditional UPDATE (D7). Two verifies
871
+ * arriving with the same challenge therefore produce one winner and one refusal,
872
+ * across instances, rather than both reading it as live.
873
+ *
874
+ * Revoking the last thing an account can sign in with is refused (D6) rather
875
+ * than warned about, because that state has no undo. A verified email address
876
+ * counts as one of those things: the password reset flow can always give such an
877
+ * account a password back.
878
+ */
879
+
880
+ /** One enrolled credential as the management surface shows it. */
881
+ interface PasskeySummary {
882
+ passkeyId: string;
883
+ label: string | null;
884
+ deviceType: PasskeyDeviceType;
885
+ backedUp: boolean;
886
+ transports: string[];
887
+ createdAt: string;
888
+ lastUsedAt: string | null;
889
+ }
890
+ interface RecentAuthenticationParams {
891
+ userId: number;
892
+ /** The device key this request is signed with — its age is the signal. */
893
+ keyId: string;
894
+ currentPassword?: string;
895
+ }
896
+ /**
897
+ * Refuse a passkey change unless the caller has recently proved themselves (D4).
898
+ *
899
+ * Two ways to satisfy it. The device key this request is signed with was
900
+ * registered within the window — that is when this device last presented a
901
+ * credential, and it needs no new state. Or the body carries the account
902
+ * password.
903
+ *
904
+ * An account with no password stored cannot satisfy it with a password, however
905
+ * plausible the value (E5): the comparison still runs, against a dummy hash, so
906
+ * "no password on file" costs exactly what "wrong password" costs. Skipping it
907
+ * would turn response time into an oracle for which accounts are OAuth-only.
908
+ *
909
+ * @throws RecentAuthenticationRequiredError
910
+ */
911
+ declare function assertRecentAuthentication(params: RecentAuthenticationParams): Promise<void>;
912
+ /**
913
+ * Refuse to remove the only thing an account can sign in with (D6).
914
+ *
915
+ * The recovery paths are: another live passkey, a password, a linked social
916
+ * account, and a verified email address. The last one is new — a password reset
917
+ * now exists in this package, and an account that can be reset by email can
918
+ * always get a password back, so the refusal has nothing left to protect.
919
+ *
920
+ * What remains refused is the account with none of the four: no other passkey,
921
+ * no password, no social account, and no verified email — a phone-only account
922
+ * among them. Nobody, support included, could undo that state.
923
+ *
924
+ * @throws LastRecoveryCredentialError
925
+ */
926
+ declare function assertNotLastRecoveryCredential(userId: number): Promise<void>;
927
+ interface StartPasskeyEnrollmentParams {
928
+ userId: number;
929
+ keyId: string;
930
+ currentPassword?: string;
931
+ }
932
+ /**
933
+ * Step 1 of enrollment — options for `navigator.credentials.create()`.
934
+ *
935
+ * `excludeCredentials` lists the caller's **live** passkeys only, so the
936
+ * authenticator quietly refuses one already enrolled here. Revoked ones are left
937
+ * out on purpose: they must not be re-enrolled either, and the check that
938
+ * refuses them is the global uniqueness check at verify (E11/M10) — listing them
939
+ * here would hand out credential ids the account no longer uses.
940
+ */
941
+ declare function startPasskeyEnrollmentService(params: StartPasskeyEnrollmentParams): Promise<PublicKeyCredentialCreationOptionsJSON>;
942
+ interface FinishPasskeyEnrollmentParams {
943
+ userId: number;
944
+ response: RegistrationResponseJSON;
945
+ label?: string;
946
+ }
947
+ interface FinishPasskeyEnrollmentResult {
948
+ passkeyId: string;
949
+ label: string | null;
950
+ createdAt: string;
951
+ }
952
+ /**
953
+ * Step 2 of enrollment — verify the attestation and keep the credential.
954
+ *
955
+ * Runs under `Transactional()`: the challenge is spent and the row written
956
+ * together, so a failure after the spend leaves the challenge live and the
957
+ * ceremony retryable, while a success can never be replayed.
958
+ */
959
+ declare function finishPasskeyEnrollmentService(params: FinishPasskeyEnrollmentParams): Promise<FinishPasskeyEnrollmentResult>;
960
+ /**
961
+ * Step 1 of sign-in — options for `navigator.credentials.get()`.
962
+ *
963
+ * Takes nothing and returns the same shape to everyone: `allowCredentials` is
964
+ * always empty and the challenge row names no account (D3). There is no input
965
+ * that could make this answer differ by whether an account exists, which is the
966
+ * point — the discoverable credential on the device is what names the owner.
967
+ */
968
+ declare function startPasskeyLoginService(): Promise<PublicKeyCredentialRequestOptionsJSON>;
969
+ interface FinishPasskeyLoginParams {
970
+ response: AuthenticationResponseJSON;
971
+ publicKey: string;
972
+ keyId: string;
973
+ fingerprint: string;
974
+ algorithm?: KeyAlgorithmType;
975
+ oldKeyId?: string;
976
+ deviceName?: string;
977
+ platform?: KeyPlatformType;
978
+ }
979
+ /**
980
+ * Step 2 of sign-in — verify the assertion, then sign in exactly as a password
981
+ * login does.
982
+ *
983
+ * The tail from the active-status check onward is the one every identified
984
+ * sign-in runs (`loginService`, the OAuth flows): revoke the key being replaced,
985
+ * register the new device key, stamp the last login, announce it after commit.
986
+ * Passkeys add a way to prove identity, not a second way to hold a session.
987
+ */
988
+ declare function finishPasskeyLoginService(params: FinishPasskeyLoginParams): Promise<LoginResult>;
989
+ /**
990
+ * The caller's live passkeys, newest first.
991
+ */
992
+ declare function listPasskeysService(userId: number): Promise<PasskeySummary[]>;
993
+ interface RenamePasskeyParams {
994
+ userId: number;
995
+ passkeyId: string;
996
+ label: string;
997
+ }
998
+ /**
999
+ * Rename a passkey. Owner-scoped, so someone else's id is a 404 and nothing
1000
+ * about it is disclosed.
1001
+ *
1002
+ * No recent-authentication gate: a label is display only, and nothing is
1003
+ * authorized by it.
1004
+ */
1005
+ declare function renamePasskeyService(params: RenamePasskeyParams): Promise<{
1006
+ passkeyId: string;
1007
+ label: string;
1008
+ }>;
1009
+ interface RevokePasskeyParams {
1010
+ userId: number;
1011
+ keyId: string;
1012
+ passkeyId: string;
1013
+ currentPassword?: string;
1014
+ }
1015
+ /**
1016
+ * Retire a passkey.
1017
+ *
1018
+ * Gated on recent authentication, because someone who walked up to an unlocked
1019
+ * laptop should not be able to strip the account's credentials; and on the
1020
+ * last-recovery-credential guard, because there is no undo for the state that
1021
+ * would leave.
1022
+ *
1023
+ * The owner row is locked before the guard runs, and the route's
1024
+ * `Transactional()` is what holds that lock to commit. Without it the guard is a
1025
+ * read-modify-write with a gap: an owner with two passkeys and nothing else who
1026
+ * fires two revokes at once has both count two live credentials, both pass, and
1027
+ * both revoke — the exact state the guard exists to refuse. Locking makes the
1028
+ * second revoke count one.
1029
+ */
1030
+ declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
1031
+ passkeyId: string;
1032
+ }>;
1033
+
1034
+ /**
1035
+ * @spfn/auth - Key Service
1036
+ *
1037
+ * Handles public key registration, rotation, and revocation
1038
+ */
1039
+
1040
+ interface RegisterPublicKeyParams {
1041
+ userId: number;
1042
+ keyId: string;
1043
+ publicKey: string;
1044
+ fingerprint: string;
1045
+ algorithm?: KeyAlgorithmType;
1046
+ /** Device label for the key list. Display only — nothing is authorized by it. */
1047
+ deviceName?: string;
1048
+ platform?: KeyPlatformType;
1049
+ }
1050
+ interface RotateKeyParams {
1051
+ userId: number;
1052
+ oldKeyId: string;
1053
+ newKeyId: string;
1054
+ newPublicKey: string;
1055
+ fingerprint: string;
1056
+ algorithm?: KeyAlgorithmType;
1057
+ /** Omitted: the replaced key's label carries over, so rotation keeps its name. */
1058
+ deviceName?: string;
1059
+ platform?: KeyPlatformType;
1060
+ }
1061
+ interface RotateKeyResult {
1062
+ success: boolean;
1063
+ keyId: string;
1064
+ }
1065
+ interface RevokeKeyParams {
1066
+ userId: number;
1067
+ keyId: string;
1068
+ reason: string;
1069
+ }
1070
+ interface RevokeAllKeysParams {
1071
+ userId: number;
1072
+ /** The key the request itself is signed with — spared unless includeCurrent. */
1073
+ currentKeyId: string;
1074
+ /** true signs the caller out too. Default false: "my other devices". */
1075
+ includeCurrent?: boolean;
1076
+ reason: string;
1077
+ }
1078
+ interface RevokeAllKeysResult {
1079
+ revokedCount: number;
1080
+ currentKeyRevoked: boolean;
1081
+ }
1082
+ /** One registered device as the account surface shows it. */
1083
+ interface KeySummary {
1084
+ keyId: string;
1085
+ deviceName?: string;
1086
+ /** One of `KEY_PLATFORM`, which is what the routes accept and the column stores. */
1087
+ platform?: KeyPlatformType;
1088
+ algorithm: KeyAlgorithmType;
1089
+ /** First bytes of the fingerprint — enough to tell two entries apart. */
1090
+ fingerprintPrefix: string;
1091
+ /**
1092
+ * Milliseconds since the Unix epoch, not an ISO string.
1093
+ *
1094
+ * One representation of a moment across the whole surface: a generated Swift
1095
+ * or Kotlin client reads an integer with no date formatter, and
1096
+ * `ISO8601DateFormatter` rejecting fractional seconds by default stops being
1097
+ * a way for the two SDKs to disagree about the same value.
1098
+ */
1099
+ createdAtMillis: number;
1100
+ lastUsedAtMillis?: number;
1101
+ expiresAtMillis?: number;
1102
+ /** The TTL has run out. The key still reads as active; authenticate refuses it. */
1103
+ isExpired: boolean;
1104
+ /** False once revoked. Only ever false when the caller asked for revoked keys. */
1105
+ isActive: boolean;
1106
+ /** When it was revoked, for the "what did I cut off, and when" reading. */
1107
+ revokedAtMillis?: number;
1108
+ }
1109
+ interface ListKeysParams {
1110
+ userId: number;
1111
+ /** Also return keys already revoked. Default false: only what can still sign. */
1112
+ includeRevoked?: boolean;
1113
+ }
1114
+ /** How much of the fingerprint the list returns. */
1115
+ declare const KEY_FINGERPRINT_PREFIX_LENGTH = 8;
1116
+ /**
1117
+ * Register a new public key for a user
1118
+ *
1119
+ * `keyId` is UNIQUE across all users, so the lookup must ignore `isActive` —
1120
+ * filtering on it misses a revoked row and the insert then fails on the unique
1121
+ * index, rolling the whole login transaction back into a 500. Reuse is refused
1122
+ * with a domain error instead, telling the client to generate a fresh keyId.
1123
+ *
1124
+ * @throws KeyIdAlreadyRegisteredError keyId가 이미 쓰인 값일 때 (자기 폐기 키 재사용 · 남의 키)
1125
+ * @throws InvalidKeyFingerprintError fingerprint가 publicKey와 맞지 않을 때
1126
+ * @throws KeyAlgorithmMismatchError 키의 SPKI 타입이 선언된 algorithm과 다를 때
1127
+ */
1128
+ declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<void>;
1129
+ /**
1130
+ * Rotate user's public key (revoke old, register new)
1131
+ *
1132
+ * @throws InvalidKeyFingerprintError fingerprint가 newPublicKey와 맞지 않을 때
1133
+ * @throws KeyAlgorithmMismatchError 새 키의 SPKI 타입이 선언된 algorithm과 다를 때
1134
+ */
1135
+ declare function rotateKeyService(params: RotateKeyParams): Promise<RotateKeyResult>;
1136
+ /**
1137
+ * Revoke a user's public key.
1138
+ *
1139
+ * Returns false when the key does not belong to this user, so a caller acting
1140
+ * on a key id from outside (the device list) can answer "not found" instead of
1141
+ * reporting a revocation that never happened. The repository already scopes the
1142
+ * update by userId, so someone else's key is never touched either way.
1143
+ */
1144
+ declare function revokeKeyService(params: RevokeKeyParams): Promise<boolean>;
1145
+ /**
1146
+ * List the caller's active keys — one entry per device that can sign for them.
1147
+ *
1148
+ * `isExpired` is computed rather than stored: an expired key keeps `isActive`
1149
+ * true (nothing flips it), and `authenticate` refuses it at request time. A list
1150
+ * that showed it as simply "active" would be telling the user something the
1151
+ * server does not act on.
1152
+ *
1153
+ * The fingerprint is truncated. Its full value is what a native sign-in must
1154
+ * send as its nonce (issue #63), and an account page has no use for it beyond
1155
+ * telling two entries apart.
1156
+ */
1157
+ declare function listKeysService(params: ListKeysParams): Promise<KeySummary[]>;
1158
+ /**
1159
+ * Revoke every active key the user has, optionally sparing the current one.
1160
+ *
1161
+ * The caller's own key is spared by default, so "sign out my other devices"
1162
+ * does not also end the session making the request. Passing
1163
+ * `includeCurrent: true` is the full sign-out, which until now was reachable
1164
+ * only as a side effect of changing a password.
1165
+ *
1166
+ * Live device authorizations are refused as well, in both modes. A device
1167
+ * waiting on an approved code has no key yet, so it is never the caller's own
1168
+ * device and never the one being spared — but its next poll would register a
1169
+ * brand-new active key, which would undo the revocation seconds after it ran.
1170
+ * That is the whole point of the call: the user has decided nothing else is to
1171
+ * stay signed in.
1172
+ *
1173
+ * `revokedCount` counts keys only, since that is the number the caller's screen
1174
+ * means by "devices signed out"; a code nobody had collected was never a session.
1175
+ */
1176
+ declare function revokeAllKeysService(params: RevokeAllKeysParams): Promise<RevokeAllKeysResult>;
1177
+
1178
+ /**
1179
+ * @spfn/auth - RBAC Type Definitions
1180
+ *
1181
+ * Type definitions for role and permission configuration
1182
+ */
1183
+ /**
1184
+ * Permission category enum values
1185
+ * Single source of truth for permission categories
1186
+ */
1187
+ declare const PERMISSION_CATEGORIES: readonly ["auth", "user", "rbac", "system", "custom"];
1188
+ /**
1189
+ * Permission category type derived from the const array
1190
+ */
1191
+ type PermissionCategory = typeof PERMISSION_CATEGORIES[number];
1192
+ interface RoleConfig {
1193
+ name: string;
1194
+ displayName: string;
1195
+ description?: string;
1196
+ priority?: number;
1197
+ isSystem?: boolean;
1198
+ isBuiltin?: boolean;
1199
+ }
1200
+ interface PermissionConfig {
1201
+ name: string;
1202
+ displayName: string;
1203
+ description?: string;
1204
+ category?: PermissionCategory;
1205
+ isSystem?: boolean;
1206
+ isBuiltin?: boolean;
1207
+ }
1208
+ interface AuthInitOptions {
1209
+ /**
1210
+ * Additional roles to create
1211
+ * Built-in roles (user, admin, superadmin) are automatically included
1212
+ */
1213
+ roles?: RoleConfig[];
1214
+ /**
1215
+ * Additional permissions to create
1216
+ * Built-in permissions are automatically included
1217
+ */
1218
+ permissions?: PermissionConfig[];
1219
+ /**
1220
+ * Role-Permission mappings
1221
+ * Built-in mappings are automatically included
1222
+ * You can extend built-in roles or define mappings for custom roles
1223
+ *
1224
+ * @example
1225
+ * ```typescript
1226
+ * {
1227
+ * // Extend built-in admin role
1228
+ * admin: ['project:create', 'project:delete'],
1229
+ *
1230
+ * // Define custom role permissions
1231
+ * 'project-manager': ['project:create', 'task:assign'],
1232
+ * }
1233
+ * ```
1234
+ */
1235
+ rolePermissions?: Record<string, string[]>;
1236
+ /**
1237
+ * Default role name for new users
1238
+ * Must be a valid role name that exists after initialization
1239
+ * @default 'user'
1240
+ */
1241
+ defaultRole?: string;
1242
+ /**
1243
+ * Default session TTL (Time To Live)
1244
+ *
1245
+ * Supports:
1246
+ * - Number: seconds (e.g., 2592000)
1247
+ * - String: duration format ('30d', '12h', '45m', '3600s')
1248
+ *
1249
+ * Can be overridden at runtime with `remember` parameter.
1250
+ *
1251
+ * @default '7d' (7 days)
1252
+ *
1253
+ * @example
1254
+ * ```typescript
1255
+ * {
1256
+ * sessionTtl: '30d', // 30 days
1257
+ * }
1258
+ * ```
1259
+ */
1260
+ sessionTtl?: string | number;
1261
+ }
1262
+
1263
+ /**
1264
+ * One-Time Token Service
1265
+ *
1266
+ * Issues and verifies one-time tokens for direct API access.
1267
+ */
1268
+ interface IssueOneTimeTokenResult {
1269
+ token: string;
1270
+ expiresAt: string;
1271
+ }
1272
+ /**
1273
+ * Issue a one-time token for the authenticated user
1274
+ *
1275
+ * @param userId - Authenticated user's ID
1276
+ * @returns Token string and ISO expiration timestamp
1277
+ */
1278
+ declare function issueOneTimeTokenService(userId: string): Promise<IssueOneTimeTokenResult>;
1279
+ /**
1280
+ * Verify and consume a one-time token
1281
+ *
1282
+ * @param token - The one-time token to verify
1283
+ * @returns userId if valid, null if invalid/expired/consumed
1284
+ */
1285
+ declare function verifyOneTimeTokenService(token: string): Promise<string | null>;
1286
+
1287
+ /**
1288
+ * OAuth Provider 추상화
1289
+ *
1290
+ * Provider별로 하드코딩된 분기를 제거하기 위한 공통 인터페이스와 registry.
1291
+ * - 내장 provider(google)는 패키지 로드 시점에 자기 등록(dogfood)
1292
+ * - 외부 패키지(@superself/auth 등)는 registerOAuthProvider()로 런타임 등록
1293
+ *
1294
+ * @spfn/auth는 토큰 issuer가 아니라 소비(client) 측이므로, 이 추상화는
1295
+ * web 흐름("authorize URL 생성 → code 교환 → 사용자 정보 정규화")과
1296
+ * native 흐름(네이티브/웹 SDK가 받은 id_token 직접 검증)을 다룬다.
1297
+ */
1298
+
1299
+ /**
1300
+ * Provider 사용자 정보를 공통 형태로 정규화한 신원
1301
+ *
1302
+ * provider별 응답 형태(snake_case 등)를 service에 노출하지 않기 위한 경계.
1303
+ */
1304
+ interface NormalizedIdentity {
1305
+ providerUserId: string;
1306
+ email: string | null;
1307
+ emailVerified: boolean;
1308
+ name?: string;
1309
+ avatar?: string;
1310
+ }
1311
+ /**
1312
+ * 정규화된 OAuth 토큰 응답
1313
+ *
1314
+ * @property expiresIn - access token 만료까지 남은 초(seconds)
1315
+ */
1316
+ interface OAuthTokens {
1317
+ accessToken: string;
1318
+ refreshToken?: string;
1319
+ expiresIn: number;
1320
+ }
1321
+ /**
1322
+ * 네이티브 id_token 검증 옵션
1323
+ */
1324
+ interface NativeVerifyOptions {
1325
+ /** 클라이언트가 생성한 raw nonce. provider별 규약(raw 또는 SHA-256 해시)으로 대조된다. */
1326
+ nonce: string;
1327
+ /**
1328
+ * 같은 로그인에서 SDK가 함께 받은 provider access token (선택).
1329
+ *
1330
+ * id_token만으로는 알 수 없는 claim을 provider API로 보강하려는 provider가 쓴다
1331
+ * (카카오: id_token에 email_verified가 없어 /v2/user/me의 이메일 유효·인증 플래그를 본다).
1332
+ *
1333
+ * ⚠️ 클라이언트가 보낸 검증되지 않은 값이다. 다른 사용자의 토큰일 수 있으므로,
1334
+ * 이 값으로 조회한 신원은 반드시 id_token의 sub와 대조한 뒤에만 신뢰해야 한다.
1335
+ */
1336
+ accessToken?: string;
1337
+ }
1338
+ interface OAuthCodeExchangeOptions {
1339
+ /** Provider가 callback에 돌려준 원본 state. 일부 provider는 token 교환에도 요구한다. */
1340
+ state: string;
1341
+ }
1342
+ /**
1343
+ * Provider가 서비스로 보내는 연동 해제 알림의 원재료
1344
+ *
1345
+ * provider마다 전달 방식(query/form/JSON, 헤더 인증)이 달라 route가 정규화해 넘긴다.
1346
+ * fields는 query string과 body(form/JSON)를 병합한 문자열 맵이다.
1347
+ */
1348
+ interface UnlinkNotifyRequest {
1349
+ /** Authorization 헤더 원문 (없으면 null) */
1350
+ authorization: string | null;
1351
+ fields: Record<string, string>;
1352
+ }
1353
+ /**
1354
+ * 검증에 성공한 연동 해제 알림
1355
+ */
1356
+ interface UnlinkNotification {
1357
+ providerUserId: string;
1358
+ /** provider가 전달한 해제 경로 (kakao referrer_type 등) */
1359
+ reason?: string;
1360
+ }
1361
+ /**
1362
+ * 연동 해제 알림 검증 실패
1363
+ *
1364
+ * API 에러 응답 체계를 타지 않는다 — route가 잡아 status만 반환한다.
1365
+ * (provider 웹훅은 사람이 아닌 provider 서버가 호출자라서 에러 본문이 무의미하다)
1366
+ */
1367
+ declare class UnlinkNotifyRejection extends Error {
1368
+ readonly status: 400 | 401 | 403;
1369
+ constructor(status: 400 | 401 | 403, message: string);
1370
+ }
1371
+ /**
1372
+ * OAuth provider 구현 인터페이스
1373
+ *
1374
+ * google, superself 등 모든 provider가 이 형태를 만족해야 registry에 등록된다.
1375
+ */
1376
+ interface OAuthProvider {
1377
+ id: SocialProvider;
1378
+ /**
1379
+ * provider가 사용 가능한 상태인지(필수 env 등) 확인
1380
+ */
1381
+ isEnabled(): boolean;
1382
+ /**
1383
+ * provider 로그인 페이지로 보낼 authorization URL 생성
1384
+ *
1385
+ * @param state - CSRF 방지용 암호화 state
1386
+ * @param scopes - 요청할 scope (미지정 시 provider 기본값)
1387
+ */
1388
+ getAuthUrl(state: string, scopes?: string[]): string;
1389
+ /**
1390
+ * authorization code를 토큰으로 교환
1391
+ */
1392
+ exchangeCodeForTokens(code: string, options: OAuthCodeExchangeOptions): Promise<OAuthTokens>;
1393
+ /**
1394
+ * access token으로 사용자 정보를 조회하고 공통 형태로 정규화
1395
+ */
1396
+ getUserInfo(accessToken: string): Promise<NormalizedIdentity>;
1397
+ /**
1398
+ * refresh token으로 access token 갱신 (provider가 지원하는 경우)
1399
+ *
1400
+ * 저장된 provider 토큰을 이후 API 호출에 재사용할 때 사용한다.
1401
+ * 미구현 provider는 갱신 불가로 간주한다.
1402
+ */
1403
+ refreshTokens?(refreshToken: string): Promise<OAuthTokens>;
1404
+ /**
1405
+ * 네이티브/웹 SDK가 받은 id_token을 직접 검증하고 신원을 정규화한다.
1406
+ *
1407
+ * authorization code 교환 없이 provider JWKS로 서명을 검증하므로 client secret이
1408
+ * 필요 없다. native sign-in을 지원하는 provider만 구현한다(Apple은 web SDK 부재로
1409
+ * Android·웹도 이 경로를 쓴다).
1410
+ */
1411
+ verifyNativeIdToken?(idToken: string, options: NativeVerifyOptions): Promise<NormalizedIdentity>;
1412
+ /**
1413
+ * Provider발 연동 해제 알림(웹훅)을 검증하고 대상 사용자를 식별한다.
1414
+ *
1415
+ * 인증 없는 공개 엔드포인트로 들어오므로 provider별 규격(카카오: 어드민 키 헤더,
1416
+ * 네이버: HMAC 서명 + AES 복호화)의 검증을 통과해야만 처리된다.
1417
+ * 검증 실패는 UnlinkNotifyRejection을 던진다.
1418
+ */
1419
+ verifyUnlinkNotification?(request: UnlinkNotifyRequest): Promise<UnlinkNotification>;
1420
+ /**
1421
+ * 연동 해제 알림 처리 성공 시 응답할 HTTP status (미지정 시 200)
1422
+ *
1423
+ * 네이버는 204 No Content를 요구한다.
1424
+ */
1425
+ unlinkNotifyAckStatus?: 200 | 204;
1426
+ }
1427
+ /**
1428
+ * OAuth provider 등록 (public)
1429
+ *
1430
+ * 동일 id로 다시 등록하면 덮어쓴다(외부 패키지의 override 허용).
1431
+ */
1432
+ declare function registerOAuthProvider(provider: OAuthProvider): void;
1433
+ /**
1434
+ * 등록된 provider 조회. 미등록이면 undefined.
1435
+ */
1436
+ declare function getOAuthProvider(id: SocialProvider): OAuthProvider | undefined;
1437
+ /**
1438
+ * 등록된 모든 provider 목록
1439
+ */
1440
+ declare function getRegisteredProviders(): OAuthProvider[];
1441
+
1442
+ /**
1443
+ * @spfn/auth - OAuth Service
1444
+ *
1445
+ * OAuth 인증 비즈니스 로직
1446
+ * - Google OAuth Authorization Code Flow
1447
+ * - 소셜 계정 연결/생성
1448
+ * - publicKey는 state에서 추출하여 등록
1449
+ */
1450
+
1451
+ interface OAuthStartParams {
1452
+ provider: SocialProvider;
1453
+ returnUrl: string;
1454
+ publicKey: string;
1455
+ keyId: string;
1456
+ fingerprint: string;
1457
+ algorithm: KeyAlgorithmType;
1458
+ metadata?: Record<string, unknown>;
1459
+ /** CSRF nonce bound into the state; the route sets the matching oauth_csrf cookie. */
1460
+ nonce?: string;
1461
+ }
1462
+ interface OAuthStartResult {
1463
+ authUrl: string;
1464
+ }
1465
+ interface OAuthCallbackParams {
1466
+ provider: SocialProvider;
1467
+ code: string;
1468
+ state: string;
1469
+ /**
1470
+ * Value(s) of the oauth_csrf cookie from the callback request. One of them
1471
+ * must equal the nonce bound into the (encrypted) state — otherwise the flow
1472
+ * wasn't initiated by this browser (login CSRF). An array arises because the
1473
+ * cookie name carries the PORT suffix of the process that set it (the Next.js
1474
+ * web process), which differs from the API process in a split deployment, so
1475
+ * the callback collects every spfn_oauth_csrf* candidate. Pass `undefined` or
1476
+ * an empty array when absent; verification then fails closed.
1477
+ */
1478
+ expectedNonce: string | string[] | undefined;
1479
+ }
1480
+ interface OAuthCallbackResult {
1481
+ redirectUrl: string;
1482
+ userId: string;
1483
+ keyId: string;
1484
+ isNewUser: boolean;
1485
+ }
1486
+ /**
1487
+ * registry에서 provider를 찾아 사용 가능한지 검증 후 반환
1488
+ *
1489
+ * 미등록과 비활성을 구분해 디버깅 신호를 남긴다.
1490
+ * 라우트 레이어에서도 재사용한다(중복 조회/non-null 단언 제거).
1491
+ */
1492
+ declare function requireEnabledProvider(provider: SocialProvider): OAuthProvider;
1493
+ /**
1494
+ * OAuth 로그인 시작 - Provider 로그인 페이지로 리다이렉트할 URL 생성
1495
+ *
1496
+ * Next.js에서 키쌍을 생성한 후, publicKey를 state에 포함하여 호출
1497
+ */
1498
+ declare function oauthStartService(params: OAuthStartParams): Promise<OAuthStartResult>;
1499
+ /**
1500
+ * OAuth 콜백 처리 - Code를 Token으로 교환하고 사용자 생성/연결
1501
+ *
1502
+ * state에서 publicKey를 추출하여 서버에 등록
1503
+ * Next.js는 반환된 userId, keyId로 세션을 구성
1504
+ */
1505
+ declare function oauthCallbackService(params: OAuthCallbackParams): Promise<OAuthCallbackResult>;
1506
+ /**
1507
+ * OAuth 에러 리다이렉트 URL 생성
1508
+ */
1509
+ declare function buildOAuthErrorUrl(error: string): string;
1510
+ /**
1511
+ * OAuth provider가 등록되어 있고 활성화되어 있는지 확인
1512
+ */
1513
+ declare function isOAuthProviderEnabled(provider: SocialProvider): boolean;
1514
+ /**
1515
+ * 활성화된 모든 OAuth provider 목록 (registry 기반)
1516
+ */
1517
+ declare function getEnabledOAuthProviders(): SocialProvider[];
1518
+ /**
1519
+ * Google access token 조회 (만료 시 자동 리프레시)
1520
+ *
1521
+ * 저장된 토큰이 만료 임박(5분 이내) 또는 만료 상태이면
1522
+ * refresh token으로 자동 갱신 후 DB 업데이트하여 유효한 토큰 반환.
1523
+ *
1524
+ * @param userId - 사용자 ID
1525
+ * @returns 유효한 Google access token
1526
+ */
1527
+ declare function getGoogleAccessToken(userId: number): Promise<string>;
1528
+ interface UnlinkNotifyResult {
1529
+ /** provider 규격이 요구하는 성공 응답 status */
1530
+ ackStatus: 200 | 204;
1531
+ /** 대상 소셜 계정을 찾아 삭제했는지 (미존재·이미 삭제면 false) */
1532
+ handled: boolean;
1533
+ }
1534
+ /**
1535
+ * Provider발 연동 해제 알림(unlink-notify) 처리
1536
+ *
1537
+ * 검증(verifyUnlinkNotification)을 통과한 요청만 여기 도달한다.
1538
+ * 소셜 계정 연결 row를 삭제해 저장 토큰(access/refresh)까지 함께 파기하고,
1539
+ * 후속 정책(계정 탈퇴 연계 등)은 auth.oauth.unlinked 이벤트 구독에 맡긴다.
1540
+ *
1541
+ * 대상 계정이 없어도 성공으로 응답한다 — provider 재전송·이미 해제된 계정에
1542
+ * 대한 알림은 정상 시나리오다.
1543
+ */
1544
+ declare function oauthUnlinkNotifyService(provider: SocialProvider, notification: UnlinkNotification): Promise<UnlinkNotifyResult>;
1545
+
1546
+ /**
1547
+ * @spfn/auth - Native Social Login Service
1548
+ *
1549
+ * 네이티브/웹 SDK가 받은 id_token을 JWKS로 검증하고, 검증된 신원에 클라이언트가 만든
1550
+ * 공개키를 등록한다. 토큰은 발급하지 않는다 — 클라이언트가 등록한 키로 client token을
1551
+ * 직접 서명해 Bearer로 사용한다(client-signs / server-verifies 모델).
1552
+ *
1553
+ * 흐름은 두 단계로 분리한다:
1554
+ * 1) id_token 검증 — 외부 JWKS 네트워크 조회. DB 트랜잭션 밖에서 수행한다.
1555
+ * 2) persist — 사용자 link/create + 공개키 등록을 한 트랜잭션으로. 이벤트는 커밋 후 발행.
1556
+ */
1557
+
1558
+ interface OAuthNativeParams {
1559
+ provider: SocialProvider;
1560
+ idToken: string;
1561
+ nonce: string;
1562
+ publicKey: string;
1563
+ keyId: string;
1564
+ fingerprint: string;
1565
+ algorithm: KeyAlgorithmType;
1566
+ /** 키 목록에 보일 기기 라벨 (선택). 표시용이라 권한 판정에 쓰이지 않는다. */
1567
+ deviceName?: string;
1568
+ platform?: KeyPlatformType;
1569
+ /**
1570
+ * SDK가 id_token과 함께 받은 provider access token (선택).
1571
+ *
1572
+ * provider가 id_token만으로 확인할 수 없는 claim을 보강할 때만 쓴다. 없으면 provider는
1573
+ * id_token이 담은 정보만으로 신원을 정규화한다.
1574
+ */
1575
+ accessToken?: string;
1576
+ /** Apple은 첫 로그인에만 이름을 별도로 주므로 클라이언트가 전달할 수 있다. */
1577
+ profile?: {
1578
+ name?: string;
1579
+ };
1580
+ metadata?: Record<string, unknown>;
1581
+ }
1582
+ interface OAuthNativeResult {
1583
+ userId: string;
1584
+ keyId: string;
1585
+ isNewUser: boolean;
1586
+ }
1587
+ /**
1588
+ * native id_token 로그인 처리
1589
+ *
1590
+ * @throws NativeSignInUnsupportedError provider가 native sign-in을 지원하지 않을 때
1591
+ * @throws InvalidSocialTokenError id_token 검증 실패 시
1592
+ */
1593
+ declare function oauthNativeService(params: OAuthNativeParams): Promise<OAuthNativeResult>;
1594
+
1595
+ /**
1596
+ * OAuth 2.1 Authorize Service
1597
+ *
1598
+ * The API half of the consent screen. The screen itself is a page on the web
1599
+ * app, because that is where the session cookie is; it has no database, so it
1600
+ * asks here what to draw (`describeOAuth2AuthorizeRequest`) and tells here what
1601
+ * the user decided (`approveOAuth2Authorize` / `denyOAuth2Authorize`). Both
1602
+ * calls validate the request from scratch — the second must never trust what the
1603
+ * first was shown, since a form can be edited between them.
1604
+ *
1605
+ * Refusals come in two kinds and the split is the security property, not a
1606
+ * presentation choice:
1607
+ *
1608
+ * - **Not redirectable.** An unknown `client_id`, or a `redirect_uri` the client
1609
+ * never registered. There is no vetted URI to send the error to, and sending
1610
+ * it to the one the request supplied is precisely the open redirect the
1611
+ * registration check exists to prevent. These are shown on the screen.
1612
+ * - **Redirectable.** Everything else — a missing PKCE challenge, a missing
1613
+ * `resource`, an unknown scope, and the user saying no. The client and its
1614
+ * URI are both vetted by then, so RFC 6749 §4.1.2.1 puts the error back on
1615
+ * that URI as query parameters, which is the only form the waiting CLI can
1616
+ * read.
1617
+ */
1618
+ /** An authorize request as the web handler forwards it, before anything is trusted. */
1619
+ interface OAuth2AuthorizeParams {
1620
+ clientId: string;
1621
+ redirectUri: string;
1622
+ codeChallenge?: string;
1623
+ codeChallengeMethod?: string;
1624
+ resource?: string;
1625
+ scope?: string;
1626
+ state?: string;
1627
+ }
1628
+ /** One scope, with the sentence the consent screen shows for it. */
1629
+ interface OAuth2ScopeDescription {
1630
+ name: string;
1631
+ description: string;
1632
+ }
1633
+ /** Everything the consent screen needs, and nothing it does not. */
1634
+ interface OAuth2ConsentView {
1635
+ clientName: string;
1636
+ /** Host the code would be sent to — the one fact about the client that is checkable. */
1637
+ redirectHost: string;
1638
+ scopes: OAuth2ScopeDescription[];
1639
+ resource: string;
1640
+ }
1641
+ /** What the web handler turns into the success redirect. */
1642
+ interface OAuth2AuthorizationCodeIssued {
1643
+ code: string;
1644
+ /** The presented URI, which matched a registered one. Safe to redirect to. */
1645
+ redirectUri: string;
1646
+ /** Echoed back verbatim, or absent when the request carried none. */
1647
+ state?: string;
1648
+ }
1649
+ /**
1650
+ * What to draw on the consent screen for this request.
1651
+ *
1652
+ * Read-only: nothing is recorded by looking, so a user who closes the tab has
1653
+ * consented to nothing and left nothing behind.
1654
+ */
1655
+ declare function describeOAuth2AuthorizeRequestService(params: OAuth2AuthorizeParams): Promise<OAuth2ConsentView>;
1656
+ /**
1657
+ * Record the consent and mint the code.
1658
+ *
1659
+ * The whole request is validated again rather than carried over from the GET:
1660
+ * the form between the two is in the user's browser, and a parameter changed
1661
+ * there must be caught here and not honoured because the screen once looked
1662
+ * right.
1663
+ *
1664
+ * `userId` comes from the approving session. Never from a request body — that
1665
+ * would be the entire authorization.
1666
+ */
1667
+ declare function approveOAuth2AuthorizeService(params: OAuth2AuthorizeParams, userId: number): Promise<OAuth2AuthorizationCodeIssued>;
1668
+ /**
1669
+ * The user said no.
1670
+ *
1671
+ * Validated first, and validated in full — the same `validate` the approval
1672
+ * runs. `access_denied` goes back to the client on its redirect URI like any
1673
+ * other redirectable error, so the URI has to be one the client registered
1674
+ * before anybody is sent to it; and a request that was malformed was malformed
1675
+ * whichever button was pressed, so answering `access_denied` to it would tell
1676
+ * the waiting client the user refused when in fact it never asked properly.
1677
+ * Nothing is recorded — a refusal is not a grant with a flag on it.
1678
+ */
1679
+ declare function denyOAuth2AuthorizeService(params: OAuth2AuthorizeParams): Promise<never>;
1680
+
1681
+ /**
1682
+ * OAuth 2.1 Grant Service
1683
+ *
1684
+ * The user's side of the authorization server: what is connected, and the button
1685
+ * that disconnects it. A grant is the unit because it is the thing a person can
1686
+ * recognise — "Claude Code, on this API, with these permissions" — and because
1687
+ * revoking it takes every code and token underneath with it.
1688
+ *
1689
+ * `revokeAllOAuth2GrantsForUser` is the same act performed on the user's behalf
1690
+ * rather than by them, and it sits at the four places that revoke everything:
1691
+ * revoke-all, a password change, a completed password reset, a deletion request.
1692
+ * A global revocation that left a grant alive would leave a refresh token alive,
1693
+ * and a CLI holding one would be signed in again within the hour — which is
1694
+ * exactly the device the user was cutting off.
1695
+ */
1696
+ /** One connected client, as the account settings screen lists it. */
1697
+ interface OAuth2GrantSummary {
1698
+ id: number;
1699
+ clientId: string;
1700
+ clientName: string;
1701
+ resource: string;
1702
+ scopes: string[];
1703
+ createdAtMillis: number;
1704
+ lastUsedAtMillis?: number;
1705
+ }
1706
+ /** What a user has connected. Revoked grants are not listed — they are gone. */
1707
+ declare function listOAuth2GrantsService(userId: number): Promise<OAuth2GrantSummary[]>;
1708
+ /**
1709
+ * Disconnect one client.
1710
+ *
1711
+ * The user id is part of the statement's condition, not a check before it: the
1712
+ * id comes from a URL, and a grant belonging to somebody else must answer as if
1713
+ * it did not exist rather than as if it were merely not theirs.
1714
+ */
1715
+ declare function revokeOAuth2GrantService(id: number, userId: number): Promise<void>;
1716
+ /**
1717
+ * Revoke every grant a user has — the authorization-server half of a global
1718
+ * revocation, called beside `deviceAuthorizationsRepository.denyAllActiveByUserId`.
1719
+ *
1720
+ * Tokens are revoked as well as the grants. Verification already refuses a token
1721
+ * whose grant is dead, so this changes no decision; it means a `SELECT` against
1722
+ * `oauth2_tokens` after a revoke-all does not show live-looking rows, which is
1723
+ * the sort of thing that gets read as a hole.
1724
+ */
1725
+ declare function revokeAllOAuth2GrantsForUser(userId: number): Promise<void>;
1726
+
1727
+ /**
1728
+ * @spfn/auth - Main Router
1729
+ *
1730
+ * Combines all auth-related routes into a single router
1731
+ */
1732
+ /**
1733
+ * Main auth router
1734
+ * Exports all authentication-related routes
1735
+ *
1736
+ * Routes:
1737
+ * - Auth: /_auth/codes, /_auth/login, /_auth/logout, etc.
1738
+ * - OAuth: /_auth/oauth/google, /_auth/oauth/google/callback, etc.
1739
+ * - Invitations: /_auth/invitations/*
1740
+ * - Users: /_auth/users/*
1741
+ * - Deletion: /_auth/deletion/request, /_auth/deletion/cancel
1742
+ * - Admin: /_auth/admin/* (superadmin only)
1743
+ * - OAuth 2.1 authorization server: /_auth/oauth2/*, /.well-known/oauth-authorization-server
1744
+ */
1745
+ declare const mainAuthRouter: _spfn_core_route.Router<{
1746
+ sendVerificationCode: _spfn_core_route.RouteDef<{
1747
+ body: _sinclair_typebox.TObject<{
1748
+ target: _sinclair_typebox.TString;
1749
+ targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
1750
+ 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">]>;
1751
+ }>;
1752
+ }, {}, SendVerificationCodeResult>;
1753
+ verifyCode: _spfn_core_route.RouteDef<{
1754
+ body: _sinclair_typebox.TObject<{
1755
+ target: _sinclair_typebox.TString;
1756
+ targetType: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
1757
+ code: _sinclair_typebox.TString;
1758
+ 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">]>;
1759
+ }>;
1760
+ }, {}, {
1761
+ valid: boolean;
1762
+ verificationToken: string;
1763
+ }>;
1764
+ register: _spfn_core_route.RouteDef<{
1765
+ body: _sinclair_typebox.TObject<{
1766
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1767
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1768
+ verificationToken: _sinclair_typebox.TString;
1769
+ password: _sinclair_typebox.TString;
1770
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
1771
+ }>;
1772
+ }, {
1773
+ body: _sinclair_typebox.TObject<{
1774
+ publicKey: _sinclair_typebox.TString;
1775
+ keyId: _sinclair_typebox.TString;
1776
+ fingerprint: _sinclair_typebox.TString;
1777
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1778
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1779
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1780
+ }>;
1781
+ }, RegisterResult>;
1782
+ requestSignupLink: _spfn_core_route.RouteDef<{
1783
+ body: _sinclair_typebox.TObject<{
1784
+ email: _sinclair_typebox.TString;
1785
+ returnPath: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1786
+ }>;
1787
+ }, {}, RequestSignupLinkResult>;
1788
+ confirmSignupLink: _spfn_core_route.RouteDef<{
1789
+ body: _sinclair_typebox.TObject<{
1790
+ token: _sinclair_typebox.TString;
1791
+ }>;
1792
+ }, {}, ConfirmSignupLinkResult>;
1793
+ completeSignup: _spfn_core_route.RouteDef<{
1794
+ body: _sinclair_typebox.TObject<{
1795
+ password: _sinclair_typebox.TString;
1796
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
1797
+ }>;
1798
+ }, {
1799
+ body: _sinclair_typebox.TObject<{
1800
+ setupSecret: _sinclair_typebox.TString;
1801
+ publicKey: _sinclair_typebox.TString;
1802
+ keyId: _sinclair_typebox.TString;
1803
+ fingerprint: _sinclair_typebox.TString;
1804
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1805
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1806
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1807
+ }>;
1808
+ }, RegisterResult>;
1809
+ requestPasswordReset: _spfn_core_route.RouteDef<{
1810
+ body: _sinclair_typebox.TObject<{
1811
+ email: _sinclair_typebox.TString;
1812
+ returnPath: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1813
+ }>;
1814
+ }, {}, RequestPasswordResetResult>;
1815
+ confirmPasswordReset: _spfn_core_route.RouteDef<{
1816
+ body: _sinclair_typebox.TObject<{
1817
+ token: _sinclair_typebox.TString;
1818
+ }>;
1819
+ }, {}, ConfirmPasswordResetResult>;
1820
+ completePasswordReset: _spfn_core_route.RouteDef<{
1821
+ body: _sinclair_typebox.TObject<{
1822
+ password: _sinclair_typebox.TString;
1823
+ }>;
1824
+ }, {
1825
+ body: _sinclair_typebox.TObject<{
1826
+ setupSecret: _sinclair_typebox.TString;
1827
+ publicKey: _sinclair_typebox.TString;
1828
+ keyId: _sinclair_typebox.TString;
1829
+ fingerprint: _sinclair_typebox.TString;
1830
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1831
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1832
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1833
+ }>;
1834
+ }, RegisterResult>;
1835
+ login: _spfn_core_route.RouteDef<{
1836
+ body: _sinclair_typebox.TObject<{
1837
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1838
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1839
+ password: _sinclair_typebox.TString;
1840
+ }>;
1841
+ }, {
1842
+ body: _sinclair_typebox.TObject<{
1843
+ publicKey: _sinclair_typebox.TString;
1844
+ keyId: _sinclair_typebox.TString;
1845
+ fingerprint: _sinclair_typebox.TString;
1846
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1847
+ oldKeyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1848
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1849
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1850
+ }>;
1851
+ }, LoginResult>;
1852
+ startDeviceAuth: _spfn_core_route.RouteDef<{
1853
+ body: _sinclair_typebox.TObject<{
1854
+ publicKey: _sinclair_typebox.TString;
1855
+ keyId: _sinclair_typebox.TString;
1856
+ fingerprint: _sinclair_typebox.TString;
1857
+ algorithm: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>>;
1858
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1859
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1860
+ }>;
1861
+ }, {}, StartDeviceAuthResult>;
1862
+ pollDeviceAuth: _spfn_core_route.RouteDef<{
1863
+ body: _sinclair_typebox.TObject<{
1864
+ deviceCode: _sinclair_typebox.TString;
1865
+ }>;
1866
+ }, {}, {
1867
+ status: "pending";
1868
+ intervalMillis: number;
1869
+ } | {
1870
+ email?: string | undefined;
1871
+ phone?: string | undefined;
1872
+ status: "approved";
1873
+ userId: string;
1874
+ publicId: string;
1875
+ passwordChangeRequired: boolean;
1876
+ }>;
1877
+ getDeviceAuthInfo: _spfn_core_route.RouteDef<{
1878
+ body: _sinclair_typebox.TObject<{
1879
+ userCode: _sinclair_typebox.TString;
1880
+ }>;
1881
+ }, {}, DeviceAuthInfoResult>;
1882
+ approveDeviceAuth: _spfn_core_route.RouteDef<{
1883
+ body: _sinclair_typebox.TObject<{
1884
+ userCode: _sinclair_typebox.TString;
1885
+ }>;
1886
+ }, {}, DeviceAuthInfoResult>;
1887
+ denyDeviceAuth: _spfn_core_route.RouteDef<{
1888
+ body: _sinclair_typebox.TObject<{
1889
+ userCode: _sinclair_typebox.TString;
1890
+ }>;
1891
+ }, {}, void>;
1892
+ passkeyRegisterOptions: _spfn_core_route.RouteDef<{
1893
+ body: _sinclair_typebox.TObject<{
1894
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1895
+ }>;
1896
+ }, {}, _simplewebauthn_server.PublicKeyCredentialCreationOptionsJSON>;
1897
+ passkeyRegisterVerify: _spfn_core_route.RouteDef<{
1898
+ body: _sinclair_typebox.TObject<{
1899
+ response: _sinclair_typebox.TUnknown;
1900
+ label: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1901
+ }>;
1902
+ }, {}, FinishPasskeyEnrollmentResult>;
1903
+ passkeyLoginOptions: _spfn_core_route.RouteDef<{
1904
+ body: _sinclair_typebox.TObject<{}>;
1905
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
1906
+ passkeyLoginVerify: _spfn_core_route.RouteDef<{
1907
+ body: _sinclair_typebox.TObject<{
1908
+ response: _sinclair_typebox.TUnknown;
1909
+ }>;
1910
+ }, {
1911
+ body: _sinclair_typebox.TObject<{
1912
+ publicKey: _sinclair_typebox.TString;
1913
+ keyId: _sinclair_typebox.TString;
1914
+ fingerprint: _sinclair_typebox.TString;
1915
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1916
+ oldKeyId: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1917
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1918
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1919
+ }>;
1920
+ }, LoginResult>;
1921
+ listPasskeys: _spfn_core_route.RouteDef<{
1922
+ body: _sinclair_typebox.TObject<{}>;
1923
+ }, {}, {
1924
+ passkeys: PasskeySummary[];
1925
+ }>;
1926
+ renamePasskey: _spfn_core_route.RouteDef<{
1927
+ body: _sinclair_typebox.TObject<{
1928
+ passkeyId: _sinclair_typebox.TString;
1929
+ label: _sinclair_typebox.TString;
1930
+ }>;
1931
+ }, {}, {
1932
+ passkeyId: string;
1933
+ label: string;
1934
+ }>;
1935
+ revokePasskey: _spfn_core_route.RouteDef<{
1936
+ body: _sinclair_typebox.TObject<{
1937
+ passkeyId: _sinclair_typebox.TString;
1938
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1939
+ }>;
1940
+ }, {}, {
1941
+ passkeyId: string;
1942
+ }>;
1943
+ logout: _spfn_core_route.RouteDef<{}, {}, void>;
1944
+ rotateKey: _spfn_core_route.RouteDef<{}, {
1945
+ body: _sinclair_typebox.TObject<{
1946
+ publicKey: _sinclair_typebox.TString;
1947
+ keyId: _sinclair_typebox.TString;
1948
+ fingerprint: _sinclair_typebox.TString;
1949
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
1950
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1951
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
1952
+ }>;
1953
+ }, RotateKeyResult>;
1954
+ listKeys: _spfn_core_route.RouteDef<{
1955
+ body: _sinclair_typebox.TObject<{
1956
+ includeRevoked: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
1957
+ }>;
1958
+ }, {}, {
1959
+ keys: KeySummary[];
1960
+ }>;
1961
+ revokeKey: _spfn_core_route.RouteDef<{
1962
+ body: _sinclair_typebox.TObject<{
1963
+ keyId: _sinclair_typebox.TString;
1964
+ }>;
1965
+ }, {}, {
1966
+ keyId: string;
1967
+ selfRevoked: boolean;
1968
+ }>;
1969
+ revokeAllKeys: _spfn_core_route.RouteDef<{
1970
+ body: _sinclair_typebox.TObject<{
1971
+ includeCurrent: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
1972
+ }>;
1973
+ }, {}, RevokeAllKeysResult>;
1974
+ changePassword: _spfn_core_route.RouteDef<{
1975
+ body: _sinclair_typebox.TObject<{
1976
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
1977
+ newPassword: _sinclair_typebox.TString;
1978
+ }>;
1979
+ }, {}, void>;
1980
+ getAuthSession: _spfn_core_route.RouteDef<{}, {}, {
1981
+ role: {
1982
+ id: number;
1983
+ name: string;
1984
+ displayName: string;
1985
+ priority: number;
1986
+ };
1987
+ permissions: {
1988
+ id: number;
1989
+ name: string;
1990
+ displayName: string;
1991
+ category: "auth" | "custom" | "user" | "rbac" | "system" | undefined;
1992
+ }[];
1993
+ userId: number;
1994
+ publicId: string;
1995
+ email: string | null;
1996
+ emailVerified: boolean;
1997
+ phoneVerified: boolean;
1998
+ hasPassword: boolean;
1999
+ }>;
2000
+ issueOneTimeToken: _spfn_core_route.RouteDef<{}, {}, IssueOneTimeTokenResult>;
2001
+ requestAccountDeletion: _spfn_core_route.RouteDef<{
2002
+ body: _sinclair_typebox.TObject<{
2003
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2004
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2005
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2006
+ immediate: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2007
+ }>;
2008
+ }, {}, {
2009
+ purgeScheduledAt: string;
2010
+ }>;
2011
+ cancelAccountDeletion: _spfn_core_route.RouteDef<{
2012
+ body: _sinclair_typebox.TObject<{
2013
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2014
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2015
+ password: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2016
+ verificationToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2017
+ }>;
2018
+ }, {}, void>;
2019
+ oauthGoogleStart: _spfn_core_route.RouteDef<{
2020
+ query: _sinclair_typebox.TObject<{
2021
+ state: _sinclair_typebox.TString;
2022
+ }>;
2023
+ }, {}, Response>;
2024
+ oauthGoogleCallback: _spfn_core_route.RouteDef<{
2025
+ query: _sinclair_typebox.TObject<{
2026
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2027
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2028
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2029
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2030
+ }>;
2031
+ }, {}, Response>;
2032
+ oauthStart: _spfn_core_route.RouteDef<{
2033
+ body: _sinclair_typebox.TObject<{
2034
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2035
+ returnUrl: _sinclair_typebox.TString;
2036
+ publicKey: _sinclair_typebox.TString;
2037
+ keyId: _sinclair_typebox.TString;
2038
+ fingerprint: _sinclair_typebox.TString;
2039
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2040
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2041
+ }>;
2042
+ }, {}, OAuthStartResult>;
2043
+ oauthProviders: _spfn_core_route.RouteDef<{}, {}, {
2044
+ providers: ("google" | "apple" | "github" | "kakao" | "naver" | "superself")[];
2045
+ }>;
2046
+ getGoogleOAuthUrl: _spfn_core_route.RouteDef<{
2047
+ body: _sinclair_typebox.TObject<{
2048
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2049
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2050
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2051
+ }>;
2052
+ }, {}, {
2053
+ authUrl: string;
2054
+ }>;
2055
+ oauthFinalize: _spfn_core_route.RouteDef<{
2056
+ body: _sinclair_typebox.TObject<{
2057
+ userId: _sinclair_typebox.TString;
2058
+ keyId: _sinclair_typebox.TString;
2059
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2060
+ }>;
2061
+ }, {}, {
2062
+ success: boolean;
2063
+ userId: string;
2064
+ keyId: string;
2065
+ returnUrl: string;
2066
+ }>;
2067
+ oauthProviderStart: _spfn_core_route.RouteDef<{
2068
+ params: _sinclair_typebox.TObject<{
2069
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2070
+ }>;
2071
+ query: _sinclair_typebox.TObject<{
2072
+ state: _sinclair_typebox.TString;
2073
+ }>;
2074
+ }, {}, Response>;
2075
+ oauthProviderCallback: _spfn_core_route.RouteDef<{
2076
+ params: _sinclair_typebox.TObject<{
2077
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2078
+ }>;
2079
+ query: _sinclair_typebox.TObject<{
2080
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2081
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2082
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2083
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2084
+ }>;
2085
+ }, {}, Response>;
2086
+ getProviderOAuthUrl: _spfn_core_route.RouteDef<{
2087
+ params: _sinclair_typebox.TObject<{
2088
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2089
+ }>;
2090
+ body: _sinclair_typebox.TObject<{
2091
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2092
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2093
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2094
+ }>;
2095
+ }, {}, {
2096
+ authUrl: string;
2097
+ }>;
2098
+ oauthNative: _spfn_core_route.RouteDef<{
2099
+ params: _sinclair_typebox.TObject<{
2100
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2101
+ }>;
2102
+ body: _sinclair_typebox.TObject<{
2103
+ idToken: _sinclair_typebox.TString;
2104
+ nonce: _sinclair_typebox.TString;
2105
+ accessToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2106
+ publicKey: _sinclair_typebox.TString;
2107
+ keyId: _sinclair_typebox.TString;
2108
+ fingerprint: _sinclair_typebox.TString;
2109
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2110
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2111
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2112
+ profile: _sinclair_typebox.TOptional<_sinclair_typebox.TObject<{
2113
+ name: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2114
+ }>>;
2115
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2116
+ }>;
2117
+ }, {}, OAuthNativeResult>;
2118
+ oauthUnlinkNotify: _spfn_core_route.RouteDef<{
2119
+ params: _sinclair_typebox.TObject<{
2120
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2121
+ }>;
2122
+ }, {}, void | Response>;
2123
+ oauthUnlinkNotifyGet: _spfn_core_route.RouteDef<{
2124
+ params: _sinclair_typebox.TObject<{
2125
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2126
+ }>;
2127
+ }, {}, void | Response>;
2128
+ getInvitation: _spfn_core_route.RouteDef<{
2129
+ params: _sinclair_typebox.TObject<{
2130
+ token: _sinclair_typebox.TString;
2131
+ }>;
2132
+ }, {}, {
2133
+ email: string;
2134
+ role: string;
2135
+ roleDisplayName: string;
2136
+ invitedBy: string;
2137
+ expiresAt: string;
2138
+ metadata: Record<string, any> | undefined;
2139
+ }>;
2140
+ acceptInvitation: _spfn_core_route.RouteDef<{
2141
+ body: _sinclair_typebox.TObject<{
2142
+ token: _sinclair_typebox.TString;
2143
+ password: _sinclair_typebox.TString;
2144
+ }>;
2145
+ }, {
2146
+ body: _sinclair_typebox.TObject<{
2147
+ publicKey: _sinclair_typebox.TString;
2148
+ keyId: _sinclair_typebox.TString;
2149
+ fingerprint: _sinclair_typebox.TString;
2150
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2151
+ }>;
2152
+ }, {
2153
+ userId: number;
2154
+ email: string;
2155
+ role: string;
2156
+ }>;
2157
+ createInvitation: _spfn_core_route.RouteDef<{
2158
+ body: _sinclair_typebox.TObject<{
2159
+ email: _sinclair_typebox.TString;
2160
+ roleId: _sinclair_typebox.TNumber;
2161
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2162
+ expiresAt: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2163
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TAny>;
2164
+ }>;
2165
+ }, {}, {
2166
+ id: number;
2167
+ email: string;
2168
+ token: string;
2169
+ roleId: number;
2170
+ expiresAt: string;
2171
+ invitationUrl: string;
2172
+ }>;
2173
+ listInvitations: _spfn_core_route.RouteDef<{
2174
+ query: _sinclair_typebox.TObject<{
2175
+ status: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"pending" | "accepted" | "expired" | "cancelled">[]>>;
2176
+ page: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2177
+ limit: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2178
+ }>;
2179
+ }, {}, {
2180
+ invitations: {
2181
+ id: number;
2182
+ email: string;
2183
+ token: string;
2184
+ roleId: number;
2185
+ invitedBy: number;
2186
+ status: "pending" | "accepted" | "expired" | "cancelled";
2187
+ expiresAt: Date;
2188
+ acceptedAt: Date | null;
2189
+ cancelledAt: Date | null;
2190
+ metadata: Record<string, any> | null;
2191
+ createdAt: Date;
2192
+ updatedAt: Date;
2193
+ role: {
2194
+ id: number;
2195
+ name: string;
2196
+ displayName: string;
2197
+ };
2198
+ inviter: {
2199
+ id: number;
2200
+ email: string | null;
2201
+ };
2202
+ }[];
2203
+ total: number;
2204
+ page: number;
2205
+ limit: number;
2206
+ totalPages: number;
2207
+ }>;
2208
+ cancelInvitation: _spfn_core_route.RouteDef<{
2209
+ body: _sinclair_typebox.TObject<{
2210
+ id: _sinclair_typebox.TNumber;
2211
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2212
+ }>;
2213
+ }, {}, {
2214
+ cancelledAt: string;
2215
+ }>;
2216
+ resendInvitation: _spfn_core_route.RouteDef<{
2217
+ body: _sinclair_typebox.TObject<{
2218
+ id: _sinclair_typebox.TNumber;
2219
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2220
+ }>;
2221
+ }, {}, {
2222
+ expiresAt: string;
2223
+ }>;
2224
+ deleteInvitation: _spfn_core_route.RouteDef<{
2225
+ body: _sinclair_typebox.TObject<{
2226
+ id: _sinclair_typebox.TNumber;
2227
+ }>;
2228
+ }, {}, void>;
2229
+ getUserProfile: _spfn_core_route.RouteDef<{}, {}, UserProfile>;
2230
+ updateUserProfile: _spfn_core_route.RouteDef<{
2231
+ body: _sinclair_typebox.TObject<{
2232
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2233
+ firstName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2234
+ lastName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2235
+ avatarUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2236
+ bio: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2237
+ locale: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2238
+ timezone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2239
+ dateOfBirth: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2240
+ gender: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2241
+ website: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2242
+ location: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2243
+ company: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2244
+ jobTitle: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2245
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TAny>>;
2246
+ }>;
2247
+ }, {}, ProfileInfo>;
2248
+ checkUsername: _spfn_core_route.RouteDef<{
2249
+ query: _sinclair_typebox.TObject<{
2250
+ username: _sinclair_typebox.TString;
2251
+ }>;
2252
+ }, {}, {
2253
+ available: boolean;
2254
+ }>;
2255
+ updateUsername: _spfn_core_route.RouteDef<{
2256
+ body: _sinclair_typebox.TObject<{
2257
+ username: _sinclair_typebox.TUnion<[_sinclair_typebox.TString, _sinclair_typebox.TNull]>;
2258
+ }>;
2259
+ }, {}, {
2260
+ deletedAt: Date | null;
2261
+ deletedBy: string | null;
2262
+ createdAt: Date;
2263
+ updatedAt: Date;
2264
+ id: number;
2265
+ publicId: string;
2266
+ email: string | null;
2267
+ phone: string | null;
2268
+ username: string | null;
2269
+ passwordHash: string | null;
2270
+ passwordChangeRequired: boolean;
2271
+ roleId: number;
2272
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
2273
+ emailVerifiedAt: Date | null;
2274
+ phoneVerifiedAt: Date | null;
2275
+ lastLoginAt: Date | null;
2276
+ }>;
2277
+ updateLocale: _spfn_core_route.RouteDef<{
2278
+ body: _sinclair_typebox.TObject<{
2279
+ locale: _sinclair_typebox.TString;
2280
+ }>;
2281
+ }, {}, {
2282
+ locale: string;
2283
+ }>;
2284
+ listRoles: _spfn_core_route.RouteDef<{
2285
+ query: _sinclair_typebox.TObject<{
2286
+ includeInactive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2287
+ }>;
2288
+ }, {}, {
2289
+ roles: {
2290
+ description: string | null;
2291
+ name: string;
2292
+ id: number;
2293
+ displayName: string;
2294
+ isBuiltin: boolean;
2295
+ isSystem: boolean;
2296
+ isActive: boolean;
2297
+ priority: number;
2298
+ createdAt: Date;
2299
+ updatedAt: Date;
2300
+ }[];
2301
+ }>;
2302
+ createAdminRole: _spfn_core_route.RouteDef<{
2303
+ body: _sinclair_typebox.TObject<{
2304
+ name: _sinclair_typebox.TString;
2305
+ displayName: _sinclair_typebox.TString;
2306
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2307
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2308
+ permissionIds: _sinclair_typebox.TOptional<_sinclair_typebox.TArray<_sinclair_typebox.TNumber>>;
2309
+ }>;
2310
+ }, {}, {
2311
+ role: {
2312
+ description: string | null;
2313
+ name: string;
2314
+ id: number;
2315
+ displayName: string;
2316
+ isBuiltin: boolean;
2317
+ isSystem: boolean;
2318
+ isActive: boolean;
2319
+ priority: number;
2320
+ createdAt: Date;
2321
+ updatedAt: Date;
2322
+ };
2323
+ }>;
2324
+ updateAdminRole: _spfn_core_route.RouteDef<{
2325
+ params: _sinclair_typebox.TObject<{
2326
+ id: _sinclair_typebox.TNumber;
2327
+ }>;
2328
+ body: _sinclair_typebox.TObject<{
2329
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2330
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2331
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2332
+ isActive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2333
+ }>;
2334
+ }, {}, {
2335
+ role: {
2336
+ description: string | null;
2337
+ name: string;
2338
+ id: number;
2339
+ displayName: string;
2340
+ isBuiltin: boolean;
2341
+ isSystem: boolean;
2342
+ isActive: boolean;
2343
+ priority: number;
2344
+ createdAt: Date;
2345
+ updatedAt: Date;
2346
+ };
2347
+ }>;
2348
+ deleteAdminRole: _spfn_core_route.RouteDef<{
2349
+ params: _sinclair_typebox.TObject<{
2350
+ id: _sinclair_typebox.TNumber;
2351
+ }>;
2352
+ }, {}, void>;
2353
+ updateUserRole: _spfn_core_route.RouteDef<{
2354
+ params: _sinclair_typebox.TObject<{
2355
+ userId: _sinclair_typebox.TNumber;
2356
+ }>;
2357
+ body: _sinclair_typebox.TObject<{
2358
+ roleId: _sinclair_typebox.TNumber;
2359
+ }>;
2360
+ }, {}, {
2361
+ userId: number;
2362
+ roleId: number;
2363
+ }>;
2364
+ issueOpsToken: _spfn_core_route.RouteDef<{
2365
+ body: _sinclair_typebox.TObject<{
2366
+ name: _sinclair_typebox.TString;
2367
+ scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
2368
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
2369
+ }>;
2370
+ }, {}, {
2371
+ token: string;
2372
+ opsToken: {
2373
+ id: number;
2374
+ name: string;
2375
+ scopes: string[];
2376
+ expiresAt: string | null;
2377
+ revokedAt: string | null;
2378
+ lastUsedAt: string | null;
2379
+ createdAt: string | null;
2380
+ };
2381
+ }>;
2382
+ listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
2383
+ opsTokens: {
2384
+ id: number;
2385
+ name: string;
2386
+ scopes: string[];
2387
+ expiresAt: string | null;
2388
+ revokedAt: string | null;
2389
+ lastUsedAt: string | null;
2390
+ createdAt: string | null;
2391
+ }[];
2392
+ }>;
2393
+ revokeOpsToken: _spfn_core_route.RouteDef<{
2394
+ params: _sinclair_typebox.TObject<{
2395
+ id: _sinclair_typebox.TNumber;
2396
+ }>;
2397
+ }, {}, {
2398
+ opsToken: {
2399
+ id: number;
2400
+ name: string;
2401
+ scopes: string[];
2402
+ expiresAt: string | null;
2403
+ revokedAt: string | null;
2404
+ lastUsedAt: string | null;
2405
+ createdAt: string | null;
2406
+ };
2407
+ }>;
2408
+ registerOAuth2Client: _spfn_core_route.RouteDef<{}, {}, Response>;
2409
+ getOAuth2Authorize: _spfn_core_route.RouteDef<{
2410
+ query: _sinclair_typebox.TObject<{
2411
+ client_id: _sinclair_typebox.TString;
2412
+ redirect_uri: _sinclair_typebox.TString;
2413
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2414
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2415
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2416
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2417
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2418
+ }>;
2419
+ }, {}, OAuth2ConsentView>;
2420
+ createOAuth2AuthorizationCode: _spfn_core_route.RouteDef<{
2421
+ body: _sinclair_typebox.TObject<{
2422
+ approve: _sinclair_typebox.TBoolean;
2423
+ client_id: _sinclair_typebox.TString;
2424
+ redirect_uri: _sinclair_typebox.TString;
2425
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2426
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2427
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2428
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2429
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2430
+ }>;
2431
+ }, {}, OAuth2AuthorizationCodeIssued>;
2432
+ oauth2Token: _spfn_core_route.RouteDef<{}, {}, Response>;
2433
+ oauth2Revoke: _spfn_core_route.RouteDef<{}, {}, Response>;
2434
+ listOAuth2Grants: _spfn_core_route.RouteDef<{}, {}, {
2435
+ grants: OAuth2GrantSummary[];
2436
+ }>;
2437
+ revokeOAuth2Grant: _spfn_core_route.RouteDef<{
2438
+ params: _sinclair_typebox.TObject<{
2439
+ id: _sinclair_typebox.TNumber;
2440
+ }>;
2441
+ }, {}, {
2442
+ revoked: boolean;
2443
+ }>;
2444
+ oauth2AuthorizationServerMetadata: _spfn_core_route.RouteDef<{}, {}, Response>;
2445
+ }>;
2446
+
2447
+ /**
2448
+ * The auth-profile registry the authenticate middleware dispatches on.
2449
+ *
2450
+ * A request that names `x-spfn-auth-profile` is answered by the verifier
2451
+ * registered for that profile — an O(1) map lookup, never a per-profile if
2452
+ * chain in the middleware body. A request that names no profile falls through
2453
+ * to the existing Bearer path untouched.
2454
+ *
2455
+ * Every verifier converges on the same `AuthContext` the Bearer path sets, so
2456
+ * downstream permission/tenant code consumes one principal shape and never
2457
+ * branches on how it was authenticated.
2458
+ *
2459
+ * An app adds its own scheme with `registerAuthProfile` at boot. The dispatch
2460
+ * it joins is the one below, unchanged: a name nobody registered is still
2461
+ * refused, and profile credentials mixed with an Authorization header are
2462
+ * still refused before either path runs.
2463
+ *
2464
+ * The clientProofV1 verifier reuses the phase-1 admission pieces (header
2465
+ * shape, canonical body, proof-input assembly, ECDSA verification) with two
2466
+ * production substitutions: the key directory is `user_public_keys` via
2467
+ * `keysRepository`, and the replay ledger is the pluggable store from
2468
+ * `client-proof/replay-store` (memory default, Redis opt-in). The admission
2469
+ * order is the contract's — revocation → session → expiry → replay → proof —
2470
+ * and the non-disclosure rule holds: an unregistered keyId shares
2471
+ * PROOF_INVALID with a failed signature, while a revoked or expired key
2472
+ * answers SESSION_REVOKED before the proof is ever examined.
2473
+ *
2474
+ * @module server/middleware/auth-profiles
2475
+ */
2476
+
2477
+ /** What a verified request leaves in the context — one shape for every scheme. */
2478
+ interface AuthContext {
2479
+ user: User;
2480
+ userId: string;
2481
+ keyId: string;
2482
+ role: string | null;
2483
+ locale: string;
2484
+ /**
2485
+ * How the principal was authenticated. Informational — downstream code
2486
+ * never branches on it. The union stays open for the profiles an app
2487
+ * registers itself: the built-in names keep their autocomplete, and a
2488
+ * registered profile names its own scheme without editing this file.
2489
+ */
2490
+ scheme: 'bearer' | 'clientProofV1' | 'oneTimeToken' | (string & {});
2491
+ }
2492
+ /** A profile's verifier: admits the request and returns the principal, or throws. */
2493
+ interface AuthProfileVerifier {
2494
+ verify(c: Context): Promise<AuthContext>;
2495
+ }
2496
+ /**
2497
+ * Routes a request to its profile verifier.
2498
+ *
2499
+ * - no profile header → null: the caller continues on the Bearer path;
2500
+ * - profile header + Authorization header → rejected (mixing prohibited);
2501
+ * - unknown profile value → rejected (unknownProfilePolicy: reject).
2502
+ *
2503
+ * Shared by authenticate and optionalAuth so "presented but invalid" refuses
2504
+ * identically on both — only the "presented nothing" outcome differs.
2505
+ */
2506
+ declare function selectAuthProfile(c: Context): AuthProfileVerifier | null;
2507
+ /**
2508
+ * Loads the user for an authenticated key and applies the account-status
2509
+ * rules. One implementation for every scheme: the Bearer path and the profile
2510
+ * verifiers call this, so a status added here gates both identically.
2511
+ */
2512
+ declare function resolveAuthenticatedUser(userId: number): Promise<{
2513
+ user: User;
2514
+ role: string | null;
2515
+ locale: string;
2516
+ }>;
2517
+ /** What the profile path produced for one request. */
2518
+ type AuthProfileOutcome = {
2519
+ kind: 'none';
2520
+ } | {
2521
+ kind: 'authenticated';
2522
+ auth: AuthContext;
2523
+ } | {
2524
+ kind: 'refused';
2525
+ response: Response;
2526
+ };
2527
+ /**
2528
+ * The profile path from dispatch to answer — what `authenticate` and
2529
+ * `optionalAuth` both run before their own Bearer code.
2530
+ *
2531
+ * `none` means the request named no profile and the caller continues on the
2532
+ * Bearer path. A refusal comes back as a built response rather than a throw:
2533
+ * the answer a proven call gets is the contract's own envelope, and an error
2534
+ * handed to the generic error handler is classified by its class name instead.
2535
+ */
2536
+ declare function runAuthProfile(c: Context): Promise<AuthProfileOutcome>;
2537
+ /**
2538
+ * Registers an app's own verifier under a profile name.
2539
+ *
2540
+ * Call it at boot, before the first request: the registry is a module-global
2541
+ * read on every dispatch, so a profile registered later is simply a profile
2542
+ * the requests before it did not have. There is no freeze and no
2543
+ * unregistration — an auth surface that can be rearranged at runtime is a
2544
+ * surface an app bug can rearrange.
2545
+ *
2546
+ * A duplicate name throws rather than replacing the verifier that holds it,
2547
+ * `clientProofV1` included. A silent override is how a second import order, or
2548
+ * a copied profile name, quietly swaps the code that decides who is admitted.
2549
+ *
2550
+ * The verifier must expose a callable `verify` — a value that cannot admit
2551
+ * anyone is refused at boot rather than becoming a registry entry the dispatch
2552
+ * reads as "no profile header", which is anonymous passage under
2553
+ * `optionalAuth` for a request that presented profile credentials.
2554
+ *
2555
+ * The verifier returns the same `AuthContext` the Bearer path sets and refuses
2556
+ * by throwing. A resolve that carries no `userId` is refused as a throw too —
2557
+ * "no user" is a refusal, never a principal. A throw is not caught here:
2558
+ * `runAuthProfile` answers the internal clientProofV1 contract refusal and
2559
+ * nothing else, so a verifier's own error reaches the app's generic error
2560
+ * handler exactly as the Bearer path's `UnauthorizedError` does — and never
2561
+ * becomes anonymous passage, not even under `optionalAuth`.
2562
+ *
2563
+ * @example
2564
+ * ```typescript
2565
+ * registerAuthProfile('serviceTokenV1', {
2566
+ * verify: async (c) =>
2567
+ * {
2568
+ * const user = await authenticateServiceToken(c.req.header('x-acme-service-token'));
2569
+ * if (user === null)
2570
+ * {
2571
+ * throw new UnauthorizedError({ message: 'Invalid service token' });
2572
+ * }
2573
+ *
2574
+ * return { user, userId: String(user.id), keyId: 'service', role: null, locale: 'en', scheme: 'serviceTokenV1' };
2575
+ * },
2576
+ * });
2577
+ * ```
2578
+ */
2579
+ declare function registerAuthProfile(profileId: string, verifier: AuthProfileVerifier): void;
2580
+
2581
+ declare module 'hono' {
2582
+ interface ContextVariableMap {
2583
+ auth: AuthContext;
2584
+ }
2585
+ }
2586
+ /**
2587
+ * Authentication middleware
2588
+ *
2589
+ * Verifies client-signed JWT token using stored public key
2590
+ * Must be applied to routes that require authentication
2591
+ *
2592
+ * @example
2593
+ * ```typescript
2594
+ * // In server.config.ts
2595
+ * import { authenticate } from '@spfn/auth/server/middleware';
2596
+ *
2597
+ * export default defineServerConfig()
2598
+ * .middlewares([authenticate])
2599
+ * .routes(appRouter)
2600
+ * .build();
2601
+ *
2602
+ * // In route file - skip auth for public routes
2603
+ * export const publicRoute = route.get('/status')
2604
+ * .skip(['auth']) // Type-safe skip
2605
+ * .handler(async (c) => c.success({ status: 'ok' }));
2606
+ *
2607
+ * // Protected route - auth applied automatically
2608
+ * export const protectedRoute = route.get('/profile')
2609
+ * .handler(async (c) => {
2610
+ * const auth = c.get('auth'); // Get auth context
2611
+ * const { user, userId, keyId } = auth;
2612
+ * // Or access directly: c.get('auth').user
2613
+ * });
2614
+ * ```
2615
+ */
2616
+ declare const authenticate: _spfn_core_route.NamedMiddleware<"auth">;
2617
+ /**
2618
+ * Optional authentication middleware
2619
+ *
2620
+ * Same as `authenticate` but does NOT reject unauthenticated requests.
2621
+ * - No token → continues without auth context
2622
+ * - Invalid token → continues without auth context
2623
+ * - Valid token → sets auth context normally
2624
+ *
2625
+ * Auto-skips the global 'auth' middleware when used at route level.
2626
+ *
2627
+ * @example
2628
+ * ```typescript
2629
+ * // No need for .skip(['auth']) — handled automatically
2630
+ * export const getProducts = route.get('/products')
2631
+ * .use([optionalAuth])
2632
+ * .handler(async (c) => {
2633
+ * const auth = getOptionalAuth(c); // AuthContext | undefined
2634
+ *
2635
+ * if (auth)
2636
+ * {
2637
+ * return getPersonalizedProducts(auth.userId);
2638
+ * }
2639
+ *
2640
+ * return getPublicProducts();
2641
+ * });
2642
+ * ```
2643
+ */
2644
+ declare const optionalAuth: _spfn_core_route.NamedMiddleware<"optionalAuth">;
2645
+
2646
+ /** What a verified machine request acts as. Not a user, by construction. */
2647
+ interface MachinePrincipal {
2648
+ /** e.g. 'account' | 'service' | registrant-defined */
2649
+ subjectType: string;
2650
+ subjectId: string;
2651
+ scopes: string[];
2652
+ /** verifier-defined extras (claims, token id for audit, …) */
2653
+ claims?: Record<string, unknown>;
2654
+ /** which registered verifier admitted it */
2655
+ scheme: string;
2656
+ }
2657
+ interface MachineVerifierRegistration {
2658
+ /** unique id; becomes MachinePrincipal.scheme */
2659
+ id: string;
2660
+ /** exactly one discriminator */
2661
+ match: {
2662
+ tokenPrefix: string;
2663
+ } | {
2664
+ kidPrefix: string;
2665
+ };
2666
+ verify(token: string, c: Context): Promise<MachinePrincipal>;
2667
+ }
2668
+ declare module 'hono' {
2669
+ interface ContextVariableMap {
2670
+ machinePrincipal: MachinePrincipal;
2671
+ }
2672
+ }
2673
+ /** Read the verified machine principal a handler runs under. */
2674
+ declare function getMachinePrincipal(c: Context): MachinePrincipal | null;
2675
+ /**
2676
+ * Registers a verifier for one machine credential namespace. Call it at boot.
2677
+ *
2678
+ * Refused at registration: a duplicate `id`, a `match` that does not name
2679
+ * exactly one non-empty discriminator, a verifier without a callable `verify`,
2680
+ * and — the point of the check — a discriminator that shadows or is shadowed by
2681
+ * an already-registered one of the same kind. Two verifiers a token could match
2682
+ * would make admission depend on registration order; that is a boot-time bug,
2683
+ * not something the dispatch should resolve on every request.
2684
+ *
2685
+ * @example
2686
+ * ```typescript
2687
+ * registerMachineVerifier({
2688
+ * id: 'runtimeJwsV1',
2689
+ * match: { kidPrefix: 'machine:runtime:' },
2690
+ * verify: async (token) =>
2691
+ * {
2692
+ * const { payload } = await jwtVerify(token, RUNTIME_JWKS);
2693
+ *
2694
+ * return {
2695
+ * subjectType: 'account',
2696
+ * subjectId: String(payload.sub),
2697
+ * scopes: String(payload.scope ?? '').split(' ').filter(Boolean),
2698
+ * scheme: 'runtimeJwsV1',
2699
+ * };
2700
+ * },
2701
+ * });
2702
+ * ```
2703
+ */
2704
+ declare function registerMachineVerifier(reg: MachineVerifierRegistration): void;
2705
+ /**
2706
+ * Admits a machine credential, or refuses. Sets `machinePrincipal`; `auth`
2707
+ * stays unset, so `getAuth(c)` is as empty here as on an anonymous request.
2708
+ *
2709
+ * Auto-skips the global 'auth' middleware, like `opsTokenAuth`.
2710
+ *
2711
+ * @example
2712
+ * ```typescript
2713
+ * export const ingest = route.post('/v1/ingest')
2714
+ * .use([machineAuth, requireMachineScope('events:write')])
2715
+ * .handler(async (c) => {
2716
+ * const { subjectType, subjectId } = getMachinePrincipal(c.raw)!;
2717
+ * // ...
2718
+ * });
2719
+ * ```
2720
+ */
2721
+ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
2722
+ /**
2723
+ * Require the verified machine principal to carry every named scope.
2724
+ *
2725
+ * Fails closed: no principal in the context is a 401, not a pass — a route that
2726
+ * mounted this without `machineAuth` refuses rather than running unauthenticated,
2727
+ * exactly as `requireOpsScope` does. Scopes match exactly; there is no wildcard,
2728
+ * because what a scope string means belongs to the verifier that issued it.
2729
+ *
2730
+ * @example
2731
+ * ```ts
2732
+ * export const ingest = route.post('/v1/ingest')
2733
+ * .use([machineAuth, requireMachineScope('events:write')])
2734
+ * .handler(async () => { ... });
2735
+ * ```
2736
+ */
2737
+ declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
2738
+
2739
+ export { type FinishPasskeyEnrollmentParams as $, type AuthInitOptions as A, type ChangePasswordParams as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type CompletePasswordResetParams as E, type FinishPasskeyEnrollmentResult as F, type CompleteSignupParams as G, type ConfirmPasswordResetParams as H, type IssueOneTimeTokenResult as I, type ConfirmSignupLinkParams as J, type KeySummary as K, type LoginResult as L, type DenyDeviceAuthParams as M, type NewPasskey as N, type OAuthStartResult as O, type PermissionConfig as P, type DeviceAuthApprovedResult as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type DeviceAuthInfoParams as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type DeviceAuthPendingResult as W, DeviceAuthPollResponseSchema as X, DeviceNameSchema as Y, EmailSchema as Z, FingerprintSchema as _, type RegisterResult as a, finishPasskeyLoginService as a$, type FinishPasskeyLoginParams as a0, KEY_FINGERPRINT_PREFIX_LENGTH as a1, KeyIdSchema as a2, type LoginParams as a3, type LogoutParams as a4, type MachinePrincipal as a5, type MachineVerifierRegistration as a6, type NativeVerifyOptions as a7, type NormalizedIdentity as a8, type OAuth2AuthorizeParams as a9, type SendVerificationCodeParams as aA, type StartDeviceAuthParams as aB, type StartPasskeyEnrollmentParams as aC, TargetTypeSchema as aD, type UnlinkNotification as aE, UnlinkNotifyRejection as aF, type UnlinkNotifyRequest as aG, type UnlinkNotifyResult as aH, UserCodeSchema as aI, VerificationPurposeSchema as aJ, type VerifyCodeParams as aK, type VerifyCodeResult as aL, approveDeviceAuthService as aM, approveOAuth2AuthorizeService as aN, assertNotLastRecoveryCredential as aO, assertRecentAuthentication as aP, authenticate as aQ, buildOAuthErrorUrl as aR, changePasswordService as aS, completePasswordResetService as aT, completeSignupService as aU, confirmPasswordResetService as aV, confirmSignupLinkService as aW, denyDeviceAuthService as aX, denyOAuth2AuthorizeService as aY, describeOAuth2AuthorizeRequestService as aZ, finishPasskeyEnrollmentService as a_, type OAuth2ScopeDescription as aa, type OAuthCallbackParams as ab, type OAuthCallbackResult as ac, type OAuthCodeExchangeOptions as ad, type OAuthNativeParams as ae, type OAuthStartParams as af, type OAuthTokens as ag, PASSKEY_DEVICE_TYPES as ah, PASSKEY_LABEL_MAX_LENGTH as ai, type PasskeyDeviceType as aj, PasswordSchema as ak, PhoneSchema as al, PlatformSchema as am, type PollDeviceAuthParams as an, type PollDeviceAuthResult as ao, PublicKeySchema as ap, type RecentAuthenticationParams as aq, type RegisterParams as ar, type RegisterPublicKeyParams as as, type RenamePasskeyParams as at, type RequestPasswordResetParams as au, type RequestSignupLinkParams as av, type RevokeAllKeysParams as aw, type RevokeKeyParams as ax, type RevokePasskeyParams as ay, type RotateKeyParams as az, type RequestSignupLinkResult as b, getDeviceAuthInfoService as b0, getEnabledOAuthProviders as b1, getGoogleAccessToken as b2, getMachinePrincipal as b3, getOAuthProvider as b4, getRegisteredProviders as b5, isOAuthProviderEnabled as b6, issueOneTimeTokenService as b7, listKeysService as b8, listOAuth2GrantsService as b9, revokePasskeyService as bA, rotateKeyService as bB, runAuthProfile as bC, selectAuthProfile as bD, sendVerificationCodeService as bE, startDeviceAuthService as bF, startPasskeyEnrollmentService as bG, startPasskeyLoginService as bH, verifyCodeService as bI, verifyOneTimeTokenService as bJ, listPasskeysService as ba, loginService as bb, logoutService as bc, machineAuth as bd, oauthCallbackService as be, oauthNativeService as bf, oauthStartService as bg, oauthUnlinkNotifyService as bh, optionalAuth as bi, passkeys as bj, pollDeviceAuthService as bk, registerAuthProfile as bl, registerMachineVerifier as bm, registerOAuthProvider as bn, registerPublicKeyService as bo, registerService as bp, renamePasskeyService as bq, requestPasswordResetService as br, requestSignupLinkService as bs, requireEnabledProvider as bt, requireMachineScope as bu, resolveAuthenticatedUser as bv, revokeAllKeysService as bw, revokeAllOAuth2GrantsForUser as bx, revokeKeyService as by, revokeOAuth2GrantService as bz, type RequestPasswordResetResult as c, type ConfirmPasswordResetResult as d, type StartDeviceAuthResult as e, type PasskeySummary as f, type RotateKeyResult as g, type RevokeAllKeysResult as h, type OAuthNativeResult as i, type ProfileInfo as j, type OAuth2ConsentView as k, type OAuth2AuthorizationCodeIssued as l, mainAuthRouter as m, type OAuth2GrantSummary as n, type AuthSession as o, PERMISSION_CATEGORIES as p, type PermissionCategory as q, VERIFICATION_TARGET_TYPES as r, type VerificationPurpose as s, type VerificationTargetType as t, type OAuthProvider as u, type Passkey as v, type AuthContext as w, type ApproveDeviceAuthParams as x, type AuthProfileOutcome as y, type AuthProfileVerifier as z };