@spfn/auth 0.3.0-beta.22 → 0.3.0-beta.24

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 (34) hide show
  1. package/README.md +308 -4
  2. package/dist/client-proof.js +13 -5
  3. package/dist/client-proof.js.map +1 -1
  4. package/dist/client.d.ts +52 -1
  5. package/dist/client.js +40 -0
  6. package/dist/client.js.map +1 -1
  7. package/dist/config.d.ts +120 -0
  8. package/dist/config.js +55 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/crypto.d.ts +1 -1
  11. package/dist/errors.d.ts +208 -3
  12. package/dist/errors.js +131 -2
  13. package/dist/errors.js.map +1 -1
  14. package/dist/index.d.ts +71 -5
  15. package/dist/index.js +134 -3
  16. package/dist/index.js.map +1 -1
  17. package/dist/{machine-principals-B7N8gux0.d.ts → machine-principals-ZJd9anVT.d.ts} +2045 -886
  18. package/dist/nextjs/api.js +156 -6
  19. package/dist/nextjs/api.js.map +1 -1
  20. package/dist/nextjs/server.d.ts +42 -24
  21. package/dist/nextjs/server.js +72 -5
  22. package/dist/nextjs/server.js.map +1 -1
  23. package/dist/server.d.ts +1033 -598
  24. package/dist/server.js +3239 -1541
  25. package/dist/server.js.map +1 -1
  26. package/dist/{session-Dfwu5g2W.d.ts → session-BbhAGZtA.d.ts} +57 -1
  27. package/dist/{types-DYyhze28.d.ts → types-CTdoTOxM.d.ts} +24 -1
  28. package/migrations/20260918162828_handy_titania/migration.sql +35 -0
  29. package/migrations/20260918162828_handy_titania/snapshot.json +5948 -0
  30. package/migrations/20260918184037_happy_mordo/migration.sql +4 -0
  31. package/migrations/20260918184037_happy_mordo/snapshot.json +6000 -0
  32. package/migrations/20260918184152_dear_rictor/migration.sql +3 -0
  33. package/migrations/20260918184152_dear_rictor/snapshot.json +6039 -0
  34. package/package.json +1 -1
@@ -1,7 +1,7 @@
1
1
  import * as _simplewebauthn_server from '@simplewebauthn/server';
2
- import { RegistrationResponseJSON, AuthenticationResponseJSON, PublicKeyCredentialCreationOptionsJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/server';
2
+ import { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON, RegistrationResponseJSON, PublicKeyCredentialCreationOptionsJSON } from '@simplewebauthn/server';
3
+ import { S as SessionBindingType, K as KeyAlgorithmType, h as KeyPlatformType, l as SocialProvider } from './types-CTdoTOxM.js';
3
4
  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
5
  import * as _sinclair_typebox from '@sinclair/typebox';
6
6
  import { Static } from '@sinclair/typebox';
7
7
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
@@ -91,548 +91,29 @@ interface UserProfile {
91
91
  }
92
92
 
93
93
  /**
94
- * @spfn/auth - Auth Service
95
- *
96
- * Core authentication logic: registration, login, logout, password management
97
- */
98
-
99
- interface RegisterParams {
100
- email?: string;
101
- phone?: string;
102
- verificationToken: string;
103
- password: string;
104
- publicKey: string;
105
- keyId: string;
106
- fingerprint: string;
107
- algorithm?: KeyAlgorithmType;
108
- deviceName?: string;
109
- platform?: KeyPlatformType;
110
- metadata?: Record<string, unknown>;
111
- /** Client address of the request, from `deviceProvenance` at the route. */
112
- ip?: string;
113
- /** `user-agent` of the request, already truncated at the route. */
114
- userAgent?: string;
115
- }
116
- interface RegisterResult {
117
- userId: string;
118
- publicId: string;
119
- email?: string;
120
- phone?: string;
121
- }
122
- interface LoginParams {
123
- email?: string;
124
- phone?: string;
125
- password: string;
126
- publicKey: string;
127
- keyId: string;
128
- fingerprint: string;
129
- oldKeyId?: string;
130
- algorithm?: KeyAlgorithmType;
131
- deviceName?: string;
132
- platform?: KeyPlatformType;
133
- /** Client address of the request, from `deviceProvenance` at the route. */
134
- ip?: string;
135
- /** `user-agent` of the request, already truncated at the route. */
136
- userAgent?: string;
137
- }
138
- interface LoginResult {
139
- userId: string;
140
- publicId: string;
141
- email?: string;
142
- phone?: string;
143
- passwordChangeRequired: boolean;
144
- }
145
- interface LogoutParams {
146
- userId: number;
147
- keyId: string;
148
- }
149
- interface ChangePasswordParams {
150
- userId: number;
151
- currentPassword?: string;
152
- newPassword: string;
153
- passwordHash?: string;
154
- }
155
- /**
156
- * Register a new user account
157
- */
158
- declare function registerService(params: RegisterParams): Promise<RegisterResult>;
159
- /**
160
- * Authenticate user and create session
161
- */
162
- declare function loginService(params: LoginParams): Promise<LoginResult>;
163
- /**
164
- * Logout user (revoke current key)
165
- */
166
- declare function logoutService(params: LogoutParams): Promise<void>;
167
- /**
168
- * Change user password
169
- */
170
- declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
171
-
172
- declare const EmailSchema: _sinclair_typebox.TString;
173
- declare const PhoneSchema: _sinclair_typebox.TString;
174
- /**
175
- * Optional device labels a client may send when registering a key.
176
- *
177
- * Display only: the key list uses them to tell one device from another, and
178
- * nothing is authorized or refused by either value, so a client that lies about
179
- * them gains nothing. Both are omitted by every key registered before they
180
- * existed, hence optional rather than defaulted.
181
- */
182
- declare const DeviceNameSchema: _sinclair_typebox.TString;
183
- declare const PlatformSchema: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>;
184
- /**
185
- * Key material as a device sends it, bounded.
186
- *
187
- * The bounds exist for the one route that takes this material from a caller who
188
- * has not authenticated and cannot: `POST /_auth/device/start` persists what it
189
- * is given, and a correctly fingerprinted megabyte of base64 would sit in
190
- * `device_authorizations` until something swept it — and nothing sweeps it.
191
- *
192
- * The numbers are what real key material measures, with room to spare. The
193
- * package's own generators produce SPKI DER in base64: 124 characters for
194
- * ES256 (P-256), 392 for RS256 (RSA-2048). An RSA-4096 key would be 736, an
195
- * RSA-8192 key about 1400, and the same 4096-bit key PEM-armoured about 800 —
196
- * so 2048 admits every shape of key anyone could reasonably present, while a
197
- * megabyte is refused three orders of magnitude before it reaches a row.
198
- *
199
- * `keyId` is a UUID (36) everywhere this package generates one; 64 leaves room
200
- * for a client that prefixes or namespaces its own. `fingerprint` is SHA-256
201
- * hex, exactly 64, and nothing else can ever verify against the public key —
202
- * 128 is the length a longer digest would need, and no more.
203
- */
204
- declare const PublicKeySchema: _sinclair_typebox.TString;
205
- declare const KeyIdSchema: _sinclair_typebox.TString;
206
- declare const FingerprintSchema: _sinclair_typebox.TString;
207
- /**
208
- * The code a person reads off the waiting device and types on their own.
209
- *
210
- * Loose on purpose: 8 characters plus an optional dash is what is shown, but the
211
- * server folds whitespace, dashes and lower case away before looking anything up,
212
- * so refusing those spellings here would refuse a code that is on screen. The
213
- * bounds exist to stop an unbounded string reaching the database, not to spell
214
- * out the format — `USER_CODE_ALPHABET` is the only thing that can match a row.
215
- */
216
- declare const UserCodeSchema: _sinclair_typebox.TString;
217
- /**
218
- * What `POST /_auth/device/poll` answers with.
219
- *
220
- * A union, because the two answers are different kinds of thing rather than one
221
- * shape with optional fields: pending says "ask again in this long", approved is
222
- * a completed login carrying exactly what `/_auth/login` returns. `status` is the
223
- * discriminant, so a generated client narrows on it instead of testing which
224
- * fields happen to be present.
225
- *
226
- * The mobile contract has no union type, so it exports this as one object with
227
- * `status` required and every branch field optional — see
228
- * `deviceAuthorization.pollStatusRule` in the bundle. `intervalMillis` is an
229
- * integer for the same reason: that grammar carries no floating-point scalar,
230
- * and a count of milliseconds never needed one.
231
- *
232
- * That integer is a promise two things keep, because nothing validates a response
233
- * against this schema on the way out. `configureDeviceAuth` refuses an interval
234
- * that is not a whole number of milliseconds, so the only value this branch can
235
- * carry is one; and `contract-export.test.ts` reads this schema to check the
236
- * exported declaration, so writing `Type.Number` here fails the suite instead of
237
- * publishing an integer the server does not send.
238
- */
239
- declare const DeviceAuthPollResponseSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TObject<{
240
- status: _sinclair_typebox.TLiteral<"pending">;
241
- intervalMillis: _sinclair_typebox.TInteger;
242
- }>, _sinclair_typebox.TObject<{
243
- status: _sinclair_typebox.TLiteral<"approved">;
244
- userId: _sinclair_typebox.TString;
245
- publicId: _sinclair_typebox.TString;
246
- email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
247
- phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
248
- passwordChangeRequired: _sinclair_typebox.TBoolean;
249
- }>]>;
250
- declare const PasswordSchema: _sinclair_typebox.TString;
251
- declare const TargetTypeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
252
- type VerificationTargetType = Static<typeof TargetTypeSchema>;
253
- declare const VERIFICATION_TARGET_TYPES: readonly ["email", "phone"];
254
- 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">]>;
255
- type VerificationPurpose = Static<typeof VerificationPurposeSchema>;
256
- declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
257
-
258
- /**
259
- * @spfn/auth - Verification Service
260
- *
261
- * Handles OTP code generation, validation, and delivery
262
- */
263
-
264
- interface SendVerificationCodeParams {
265
- target: string;
266
- targetType: VerificationTargetType;
267
- purpose: VerificationPurpose;
268
- }
269
- interface SendVerificationCodeResult {
270
- success: boolean;
271
- expiresAt: string;
272
- }
273
- interface VerifyCodeParams {
274
- target: string;
275
- targetType: VerificationTargetType;
276
- code: string;
277
- purpose: VerificationPurpose;
278
- }
279
- interface VerifyCodeResult {
280
- valid: boolean;
281
- verificationToken: string;
282
- }
283
- /**
284
- * Send verification code via email or SMS
285
- */
286
- declare function sendVerificationCodeService(params: SendVerificationCodeParams): Promise<SendVerificationCodeResult>;
287
- /**
288
- * Verify OTP code and return verification token
289
- */
290
- declare function verifyCodeService(params: VerifyCodeParams): Promise<{
291
- valid: boolean;
292
- verificationToken: string;
293
- }>;
294
-
295
- /**
296
- * @spfn/auth - Verified-Email Signup Service
297
- *
298
- * A signup where the address is proven before a password exists:
94
+ * @spfn/auth - Passkeys Entity
299
95
  *
300
- * request -> a one-time link is emailed
301
- * confirm -> the link is exchanged for a short-lived password-setup session
302
- * password -> the account is created, the device registered, the user signed in
96
+ * A WebAuthn credential the account owner enrolled on one of their devices.
97
+ * It is a *credential*, not a session: an assertion proves who is asking, and
98
+ * the ordinary device key in `user_public_keys` is what the request afterwards
99
+ * is signed with. The two tables therefore never stand in for each other.
303
100
  *
304
- * The link token and the setup secret are bearer credentials, so neither is ever
305
- * stored. Only their SHA-256 hashes are, and lookup is by hash. A database dump
306
- * therefore yields nothing that can be presented to either step.
101
+ * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
102
+ * construction and `credentialId` is a handle the authenticator hands to any
103
+ * origin that asks — storing either in the clear costs nothing, and the lookup
104
+ * on `credentialId` has to be a plain equality match on an indexed column.
307
105
  *
308
- * The six-digit-code registration path is untouched and remains the default; this
309
- * is a second entry point to the same account creation, not a replacement.
106
+ * Revocation is soft, and `credentialId` stays unique across live and revoked
107
+ * rows alike: a credential someone cut off must never become enrollable again,
108
+ * on this account or on another one.
310
109
  */
311
-
312
- interface RequestSignupLinkParams {
313
- email: string;
314
- returnPath?: string;
315
- }
316
- interface RequestSignupLinkResult {
317
- success: boolean;
318
- expiresAt: string;
319
- }
320
110
  /**
321
- * Step 1 — issue a confirmation link for an address.
322
- *
323
- * Answers identically whether or not the address already has an account. When it
324
- * does, the owner gets a notice instead of a usable link, through the same
325
- * dedupe window the six-digit-code path uses.
326
- *
327
- * Requesting again is how a resend works: every live link for the address is
328
- * superseded first, so the newest link is the only one that opens, and any setup
329
- * session already opened from an older link dies with it.
111
+ * Whether the credential can leave the authenticator that minted it.
330
112
  *
331
- * Neither branch sends mail: both hand it to `auth.link-mail`, so the answer
332
- * costs the same database work whichever one ran. With no pg-boss initialised
333
- * the mail still goes out on this request — see `lib/link-mail-delivery.ts`.
334
- */
335
- declare function requestSignupLinkService(params: RequestSignupLinkParams): Promise<RequestSignupLinkResult>;
336
- interface ConfirmSignupLinkParams {
337
- token: string;
338
- }
339
- interface ConfirmSignupLinkResult {
340
- email: string;
341
- returnPath: string | null;
342
- /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
343
- setupSecret: string;
344
- setupExpiresAt: string;
345
- }
346
- /**
347
- * Step 2 — exchange a link for a password-setup session.
348
- *
349
- * Nothing binds the row to a device or a browser, which is what lets someone
350
- * request the link on a laptop and open it on a phone.
351
- */
352
- declare function confirmSignupLinkService(params: ConfirmSignupLinkParams): Promise<ConfirmSignupLinkResult>;
353
- interface CompleteSignupParams {
354
- setupSecret?: string;
355
- password: string;
356
- publicKey: string;
357
- keyId: string;
358
- fingerprint: string;
359
- algorithm?: KeyAlgorithmType;
360
- deviceName?: string;
361
- platform?: KeyPlatformType;
362
- metadata?: Record<string, unknown>;
363
- /** Client address of the request, from `deviceProvenance` at the route. */
364
- ip?: string;
365
- /** `user-agent` of the request, already truncated at the route. */
366
- userAgent?: string;
367
- }
368
- /**
369
- * Step 3 — set the password, which is what creates the account.
370
- *
371
- * Run under `Transactional()`: the user row, the device key and the completion
372
- * mark commit together. A device-key failure must not leave an account nobody
373
- * can sign into, and a completion mark must not survive a rolled-back account.
374
- *
375
- * A refusal that is the user's to fix — a weak password, an app policy that
376
- * rejects the registration — leaves the setup session usable, so the fix is
377
- * retyping the password rather than requesting a fresh email.
378
- */
379
- declare function completeSignupService(params: CompleteSignupParams): Promise<RegisterResult>;
380
-
381
- /**
382
- * @spfn/auth - Password Reset Service
383
- *
384
- * Getting back into an account whose password is gone, using the address the
385
- * account already proved:
386
- *
387
- * request -> a one-time link is emailed
388
- * confirm -> the link is exchanged for a short-lived password-setup session
389
- * complete -> the new password is written, everything else is signed out,
390
- * and the browser that reset is signed in on a fresh device key
391
- *
392
- * Mirrors the verified-email signup slice deliberately — same credentials, same
393
- * hashing, same supersede-on-resend, same interceptor moves — with two
394
- * differences that matter.
395
- *
396
- * First, the request answers identically for *every* input and sends mail only
397
- * to an account that can be reset. Signup can afford to tell an existing owner
398
- * "you already have an account"; a reset cannot send anything to a stranger's
399
- * mailbox, because the mail itself would be the answer to "does this address
400
- * have an account here".
401
- *
402
- * Second, completing it is a credential change on a live account, so it carries
403
- * the same blast radius as `changePasswordService`: pending device
404
- * authorizations are denied and every active key is revoked. Whoever was signed
405
- * in on the old password is signed out, including the attacker the reset was
406
- * needed for.
407
- */
408
-
409
- interface RequestPasswordResetParams {
410
- email: string;
411
- returnPath?: string;
412
- }
413
- interface RequestPasswordResetResult {
414
- success: boolean;
415
- expiresAt: string;
416
- }
417
- /**
418
- * Step 1 — issue a reset link for an address.
419
- *
420
- * Answers identically for every input: the same status, the same two fields, and
421
- * an `expiresAt` computed the same way whether or not a row was written. An
422
- * address with no account, an account that cannot be reset, and an account that
423
- * can are indistinguishable to the caller — only the first of the three gets
424
- * mail, and it goes to the owner.
425
- *
426
- * Requesting again is how a resend works: every live link for the account is
427
- * superseded first, so the newest link is the only one that opens, and any setup
428
- * session already opened from an older link dies with it.
429
- *
430
- * The eligible branch does not send the mail either — it hands it to
431
- * `auth.link-mail` — so the two branches differ by a few database writes and not
432
- * by a mail provider's round trip. With no pg-boss initialised the mail still
433
- * goes out on this request; see `lib/link-mail-delivery.ts`.
434
- */
435
- declare function requestPasswordResetService(params: RequestPasswordResetParams): Promise<RequestPasswordResetResult>;
436
- interface ConfirmPasswordResetParams {
437
- token: string;
438
- }
439
- interface ConfirmPasswordResetResult {
440
- email: string;
441
- returnPath: string | null;
442
- /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
443
- setupSecret: string;
444
- setupExpiresAt: string;
445
- }
446
- /**
447
- * Step 2 — exchange a link for a password-setup session.
448
- *
449
- * Nothing binds the row to a device or a browser, which is what lets someone ask
450
- * for the link on a laptop and open it on a phone.
451
- */
452
- declare function confirmPasswordResetService(params: ConfirmPasswordResetParams): Promise<ConfirmPasswordResetResult>;
453
- interface CompletePasswordResetParams {
454
- setupSecret?: string;
455
- password: string;
456
- publicKey: string;
457
- keyId: string;
458
- fingerprint: string;
459
- algorithm?: KeyAlgorithmType;
460
- deviceName?: string;
461
- platform?: KeyPlatformType;
462
- /** Client address of the request, from `deviceProvenance` at the route. */
463
- ip?: string;
464
- /** `user-agent` of the request, already truncated at the route. */
465
- userAgent?: string;
466
- }
467
- /**
468
- * Step 3 — set the new password, which is what completes the reset.
469
- *
470
- * Run under `Transactional()`: the password, the revocations, the new device key
471
- * and the completion mark commit together. A key registration that failed after
472
- * the revoke-all would otherwise leave an account with a new password and
473
- * nothing signed in.
474
- *
475
- * A refusal that is the user's to fix — a password the policy rejects, a body
476
- * with no device key — leaves the setup session usable, so the fix is retyping
477
- * the password rather than asking for a fresh email.
478
- */
479
- declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<RegisterResult>;
480
-
481
- /**
482
- * @spfn/auth - Device Auth Service
483
- *
484
- * Device-code login: a device with no key on file yet shows a short code, the
485
- * account owner types that code on a device that is already signed in, and the
486
- * waiting device's key is registered on approval.
487
- *
488
- * There is no token to hand over. Every request in this system is signed by the
489
- * calling device's own key, so "logging a device in" means one thing — getting
490
- * its public key into `user_public_keys` under the right account. That is what
491
- * the poll does, and it is why the poll returns exactly what `loginService`
492
- * returns: from the client's side the two ways in are indistinguishable.
493
- *
494
- * | state ↓ op → | info | approve | deny | poll |
495
- * | --- | --- | --- | --- | --- |
496
- * | pending | device details | → approved | → denied | pending |
497
- * | approved | AlreadyHandled | AlreadyHandled | AlreadyHandled | key registered, → consumed |
498
- * | denied | AlreadyHandled | AlreadyHandled | AlreadyHandled | Denied |
499
- * | consumed | NotFound | NotFound | NotFound | NotFound |
500
- * | expired | Expired | Expired | Expired | Expired |
501
- * | unknown | NotFound | NotFound | NotFound | NotFound |
502
- *
503
- * A global revocation — revoke-all, a password change, a deletion request —
504
- * refuses the account's live records too, as `denied`, so they land in that row
505
- * of the table. See `denyAllActiveByUserId`; the three callers are the three
506
- * places that revoke every key at once.
507
- */
508
-
509
- interface StartDeviceAuthParams {
510
- publicKey: string;
511
- keyId: string;
512
- fingerprint: string;
513
- algorithm?: KeyAlgorithmType;
514
- /** Device label shown to the approver. Display only — nothing is authorized by it. */
515
- deviceName?: string;
516
- platform?: KeyPlatformType;
517
- }
518
- interface StartDeviceAuthResult {
519
- /** Returned once. The waiting device polls with it; the server stores only its hash. */
520
- deviceCode: string;
521
- /** `XXXX-XXXX`, for the waiting device's screen and nowhere else. */
522
- userCode: string;
523
- expiresAtMillis: number;
524
- /** Milliseconds the waiting device should wait between polls. */
525
- intervalMillis: number;
526
- }
527
- interface DeviceAuthInfoParams {
528
- userCode: string;
529
- }
530
- /** What the approver is shown about the device asking to be let in. */
531
- interface DeviceAuthInfoResult {
532
- deviceName?: string;
533
- /** One of `KEY_PLATFORM`, which is what the route accepts and the column stores. */
534
- platform?: KeyPlatformType;
535
- /** First bytes of the pending key's fingerprint, as the device list truncates it. */
536
- fingerprintPrefix: string;
537
- requestedAtMillis: number;
538
- expiresAtMillis: number;
539
- }
540
- interface ApproveDeviceAuthParams {
541
- userCode: string;
542
- /** The approver, read from their session. Never from a request body. */
543
- userId: number;
544
- }
545
- interface DenyDeviceAuthParams {
546
- userCode: string;
547
- }
548
- interface PollDeviceAuthParams {
549
- deviceCode: string;
550
- /** Client address of the request, from `deviceProvenance` at the route. */
551
- ip?: string;
552
- /** `user-agent` of the request, already truncated at the route. */
553
- userAgent?: string;
554
- }
555
- /** Nobody has answered yet. Not an error — the waiting device waits. */
556
- interface DeviceAuthPendingResult {
557
- status: 'pending';
558
- intervalMillis: number;
559
- }
560
- /** Approved and spent: the key is registered and this is the login it produced. */
561
- type DeviceAuthApprovedResult = {
562
- status: 'approved';
563
- } & LoginResult;
564
- type PollDeviceAuthResult = DeviceAuthPendingResult | DeviceAuthApprovedResult;
565
- /**
566
- * Park a new device's key and hand back the codes it needs.
567
- *
568
- * The caller is unauthenticated by definition — this is what a device does before
569
- * it has any way to prove anything — so nothing here is attributed to an account.
570
- * The record gains an owner only when someone approves it.
571
- */
572
- declare function startDeviceAuthService(params: StartDeviceAuthParams): Promise<StartDeviceAuthResult>;
573
- /**
574
- * What the approver sees before deciding.
575
- *
576
- * This is the whole defence against being talked into approving someone else's
577
- * device: the answer names the device that is waiting, so the person holding the
578
- * phone can see that it is not theirs. An approval screen that showed only the
579
- * code would be asking them to confirm a number they were just told.
580
- */
581
- declare function getDeviceAuthInfoService(params: DeviceAuthInfoParams): Promise<DeviceAuthInfoResult>;
582
- /**
583
- * Bind the record to the approving account.
584
- *
585
- * The key is not registered here. The waiting device may never come back, and a
586
- * key registered for a device that stopped listening is a signing credential
587
- * nobody asked for — so approval records the decision and the poll acts on it.
588
- *
589
- * Answers with the same device description `info` returns, so a client that let
590
- * a user approve without looking first can still show them what they just let
591
- * in — which is the moment someone talked into approving an attacker's device
592
- * has to notice and revoke it.
593
- */
594
- declare function approveDeviceAuthService(params: ApproveDeviceAuthParams): Promise<DeviceAuthInfoResult>;
595
- /**
596
- * Refuse the record, so the waiting device is told no instead of timing out.
597
- *
598
- * Denying binds no user: the point of refusing is that the account owner wants
599
- * nothing to do with the request.
600
- */
601
- declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<void>;
602
- /**
603
- * The waiting device asking whether anyone has answered.
604
- *
605
- * Approved is the one branch with a side effect, and it is a one-shot: the record
606
- * is spent by a conditional update that names `approved`, so of two polls that
607
- * arrive together exactly one registers the key. The loser matches nothing and is
608
- * answered as if the code were unknown — which by then it is.
609
- */
610
- declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
611
-
612
- /**
613
- * @spfn/auth - Passkeys Entity
614
- *
615
- * A WebAuthn credential the account owner enrolled on one of their devices.
616
- * It is a *credential*, not a session: an assertion proves who is asking, and
617
- * the ordinary device key in `user_public_keys` is what the request afterwards
618
- * is signed with. The two tables therefore never stand in for each other.
619
- *
620
- * Nothing here is a bearer value, so nothing is hashed. `publicKey` is public by
621
- * construction and `credentialId` is a handle the authenticator hands to any
622
- * origin that asks — storing either in the clear costs nothing, and the lookup
623
- * on `credentialId` has to be a plain equality match on an indexed column.
624
- *
625
- * Revocation is soft, and `credentialId` stays unique across live and revoked
626
- * rows alike: a credential someone cut off must never become enrollable again,
627
- * on this account or on another one.
628
- */
629
- /**
630
- * Whether the credential can leave the authenticator that minted it.
631
- *
632
- * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
633
- * `singleDevice` is bound to one authenticator. Reported by the authenticator at
634
- * enrollment and shown in the management list, because "this one is only on that
635
- * phone" is what the owner needs to know before revoking the other entry.
113
+ * `multiDevice` is a synced passkey (iCloud Keychain, Google Password Manager);
114
+ * `singleDevice` is bound to one authenticator. Reported by the authenticator at
115
+ * enrollment and shown in the management list, because "this one is only on that
116
+ * phone" is what the owner needs to know before revoking the other entry.
636
117
  */
637
118
  declare const PASSKEY_DEVICE_TYPES: readonly ["singleDevice", "multiDevice"];
638
119
  type PasskeyDeviceType = typeof PASSKEY_DEVICE_TYPES[number];
@@ -822,6 +303,21 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
822
303
  identity: undefined;
823
304
  generated: undefined;
824
305
  }>;
306
+ secondFactor: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
307
+ name: string;
308
+ tableName: "passkeys";
309
+ dataType: "boolean";
310
+ data: boolean;
311
+ driverParam: boolean;
312
+ notNull: true;
313
+ hasDefault: true;
314
+ isPrimaryKey: false;
315
+ isAutoincrement: false;
316
+ hasRuntimeDefault: false;
317
+ enumValues: undefined;
318
+ identity: undefined;
319
+ generated: undefined;
320
+ }>;
825
321
  lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"passkeys", drizzle_orm_pg_core.PgTimestampBuilder, {
826
322
  name: string;
827
323
  tableName: "passkeys";
@@ -873,6 +369,675 @@ declare const passkeys: drizzle_orm_pg_core.PgTableWithColumns<{
873
369
  type Passkey = typeof passkeys.$inferSelect;
874
370
  type NewPasskey = typeof passkeys.$inferInsert;
875
371
 
372
+ /**
373
+ * @spfn/auth - Auth Service
374
+ *
375
+ * Core authentication logic: registration, login, logout, password management
376
+ */
377
+
378
+ interface RegisterParams {
379
+ email?: string;
380
+ phone?: string;
381
+ verificationToken: string;
382
+ password: string;
383
+ publicKey: string;
384
+ keyId: string;
385
+ fingerprint: string;
386
+ algorithm?: KeyAlgorithmType;
387
+ deviceName?: string;
388
+ platform?: KeyPlatformType;
389
+ metadata?: Record<string, unknown>;
390
+ /** Client address of the request, from `deviceProvenance` at the route. */
391
+ ip?: string;
392
+ /** `user-agent` of the request, already truncated at the route. */
393
+ userAgent?: string;
394
+ /**
395
+ * Whether proxy-guard recognised the trusted Next.js proxy, from the same
396
+ * helper. Carried for shape rather than effect: a brand-new account is on the
397
+ * default `session_binding: 'none'`, so its first key is never bound.
398
+ */
399
+ webProxy?: boolean;
400
+ }
401
+ interface RegisterResult {
402
+ userId: string;
403
+ publicId: string;
404
+ email?: string;
405
+ phone?: string;
406
+ }
407
+ interface LoginParams {
408
+ email?: string;
409
+ phone?: string;
410
+ password: string;
411
+ publicKey: string;
412
+ keyId: string;
413
+ fingerprint: string;
414
+ oldKeyId?: string;
415
+ algorithm?: KeyAlgorithmType;
416
+ deviceName?: string;
417
+ platform?: KeyPlatformType;
418
+ /** Client address of the request, from `deviceProvenance` at the route. */
419
+ ip?: string;
420
+ /** `user-agent` of the request, already truncated at the route. */
421
+ userAgent?: string;
422
+ /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
423
+ webProxy?: boolean;
424
+ }
425
+ /**
426
+ * What a sign-in answers with, on every path that starts a session.
427
+ *
428
+ * The last two fields are the carrier #97 needed. The Next.js proxy generated
429
+ * the device key and sealed the cookie, but only the backend knows whether the
430
+ * account asked for a bound session and when the key it just registered runs
431
+ * out — so the sign-in says it here and the interceptor copies both into
432
+ * `SessionData`. A response without them seals an unbound session, which is what
433
+ * every account that did not opt in gets and what every path predating this
434
+ * change keeps getting.
435
+ */
436
+ interface LoginResult {
437
+ userId: string;
438
+ publicId: string;
439
+ email?: string;
440
+ phone?: string;
441
+ passwordChangeRequired: boolean;
442
+ /** `'passkey'` when the key registered by this sign-in is bound. Absent otherwise. */
443
+ sessionBinding?: SessionBindingType;
444
+ /** Epoch milliseconds that key expires at. Only sent alongside `sessionBinding`. */
445
+ keyExpiresAtMillis?: number;
446
+ }
447
+ /**
448
+ * The binding half of a sign-in answer, as a type.
449
+ *
450
+ * Named because more than one result carries it: a password reset registers a
451
+ * device key exactly as a sign-in does, so its answer has to say so too or the
452
+ * proxy seals a cookie that does not know the key it holds is short-lived.
453
+ */
454
+ type LoginBindingFields = Pick<LoginResult, 'sessionBinding' | 'keyExpiresAtMillis'>;
455
+ interface LogoutParams {
456
+ userId: number;
457
+ keyId: string;
458
+ }
459
+ interface ChangePasswordParams {
460
+ userId: number;
461
+ /**
462
+ * The device key this request is signed with.
463
+ *
464
+ * Only read to measure the second-factor window of an enrolled account —
465
+ * an account with nothing enrolled is answered exactly as before, so this
466
+ * adds no refusal for anybody who has not opted in.
467
+ */
468
+ keyId: string;
469
+ currentPassword?: string;
470
+ newPassword: string;
471
+ passwordHash?: string;
472
+ }
473
+ /**
474
+ * Register a new user account
475
+ */
476
+ declare function registerService(params: RegisterParams): Promise<RegisterResult>;
477
+ /**
478
+ * Authenticate user and create session
479
+ */
480
+ declare function loginService(params: LoginParams): Promise<LoginResult>;
481
+ /**
482
+ * Logout user (revoke current key)
483
+ */
484
+ declare function logoutService(params: LogoutParams): Promise<void>;
485
+ /**
486
+ * Change user password
487
+ *
488
+ * An enrolled account steps up first (#95): a stolen session must not be able
489
+ * to take the account over by setting a new password. An unenrolled account is
490
+ * unaffected — including the OAuth-only account with no password and a key
491
+ * older than ten minutes, which still sets a first password and gets a 200.
492
+ */
493
+ declare function changePasswordService(params: ChangePasswordParams): Promise<void>;
494
+
495
+ /**
496
+ * @spfn/auth - Session Renewal Service
497
+ *
498
+ * What a bound session does when its key runs out: prove, with a fresh WebAuthn
499
+ * assertion, that the person who enrolled the passkey is still at the machine,
500
+ * and get a new short-lived key sealed into the cookie.
501
+ *
502
+ * Neither step is public. The expiring key is named by `expiredKeyId`, and that
503
+ * value reaches the service from `authenticateForRenewal` — the `keyId` of a
504
+ * bearer JWT this very key signed — rather than from the request body, so a
505
+ * caller who does not hold the private half cannot name a key at all. The
506
+ * assertion still has to be signed by a passkey that key's owner enrolled: the
507
+ * signature proves the cookie, and the cookie is the thing that may have been
508
+ * copied.
509
+ *
510
+ * The admission below is run again here all the same. The middleware and the
511
+ * service ask the same four questions of the row, and a service that trusted its
512
+ * caller to have asked them would be one refactor away from not being asked at
513
+ * all.
514
+ *
515
+ * Every refusal is the same refusal. A key that never existed, a stranger's key,
516
+ * an unbound key, a revoked one, one past its grace, an inactive account, a spent
517
+ * challenge, an assertion that did not verify — all `SessionRenewalRefusedError`,
518
+ * with the same body, because anything finer would answer "is this key id live"
519
+ * to whoever asked.
520
+ *
521
+ * Renewal announces nothing. No `auth.login`, no `auth.device.registered`, and
522
+ * `lastLoginAt` does not move: this is the same person on the same device
523
+ * continuing the session they already had, and a subscriber mailing "new sign-in"
524
+ * once a day per device would train its reader to ignore the notice that matters.
525
+ * A `lastLoginAt` that moved every day would make dormant-account detection
526
+ * meaningless for exactly the accounts that turned this protection on.
527
+ */
528
+
529
+ interface StartSessionRenewParams {
530
+ /** The key that ran out, read off the JWT the request was signed with. */
531
+ expiredKeyId: string;
532
+ }
533
+ interface FinishSessionRenewParams extends StartSessionRenewParams {
534
+ /** The assertion, from `navigator.credentials.get()`. */
535
+ response: AuthenticationResponseJSON;
536
+ /**
537
+ * The new key pair, in the vocabulary the Next.js login interceptor already
538
+ * writes: `renew/verify` is on that interceptor's path list, so these arrive
539
+ * exactly as they do on a login.
540
+ */
541
+ keyId: string;
542
+ publicKey: string;
543
+ fingerprint: string;
544
+ algorithm?: KeyAlgorithmType;
545
+ }
546
+ /**
547
+ * What a completed renewal answers: a sign-in result, plus the new key's id.
548
+ *
549
+ * The id is the one thing a renewal has that a sign-in does not need to say —
550
+ * `renewSession()` promises it to the app, which has no other way to learn it
551
+ * (the key pair is minted in the proxy and the private half never leaves the
552
+ * cookie). It is not a contract operation, so nothing generated reads it.
553
+ */
554
+ interface SessionRenewResult extends LoginResult {
555
+ /** The key this renewal registered, the one the session now signs with. */
556
+ keyId: string;
557
+ }
558
+ /**
559
+ * Step 1 — the challenge the authenticator signs.
560
+ *
561
+ * `allowCredentials` is empty and the account lives only on the challenge row.
562
+ * See `startRenewalCeremonyService`.
563
+ *
564
+ * @throws SessionRenewalRefusedError 갱신할 수 없는 키·계정일 때 (모든 사유 동일)
565
+ */
566
+ declare function startSessionRenewService(params: StartSessionRenewParams): Promise<PublicKeyCredentialRequestOptionsJSON>;
567
+ /**
568
+ * Step 2 — verify the assertion, put a new bound key in place of the old one.
569
+ *
570
+ * The revocation runs first and its answer is the race winner: two verifies that
571
+ * both got past their own challenges meet at the same conditional UPDATE, and
572
+ * only the one that actually revoked the key goes on to register a replacement.
573
+ *
574
+ * The new key inherits the old row's provenance, so the device list keeps saying
575
+ * where this device first appeared rather than re-stamping itself every day. Its
576
+ * expiry is a fresh window from now — renewal is a renewal, not an extension of
577
+ * what the old key had.
578
+ *
579
+ * @throws SessionRenewalRefusedError 갱신할 수 없을 때 (증명 실패 포함, 모든 사유 동일)
580
+ */
581
+ declare function finishSessionRenewService(params: FinishSessionRenewParams): Promise<SessionRenewResult>;
582
+
583
+ declare const EmailSchema: _sinclair_typebox.TString;
584
+ declare const PhoneSchema: _sinclair_typebox.TString;
585
+ /**
586
+ * Optional device labels a client may send when registering a key.
587
+ *
588
+ * Display only: the key list uses them to tell one device from another, and
589
+ * nothing is authorized or refused by either value, so a client that lies about
590
+ * them gains nothing. Both are omitted by every key registered before they
591
+ * existed, hence optional rather than defaulted.
592
+ */
593
+ declare const DeviceNameSchema: _sinclair_typebox.TString;
594
+ declare const PlatformSchema: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>;
595
+ /**
596
+ * Key material as a device sends it, bounded.
597
+ *
598
+ * The bounds exist for the one route that takes this material from a caller who
599
+ * has not authenticated and cannot: `POST /_auth/device/start` persists what it
600
+ * is given, and a correctly fingerprinted megabyte of base64 would sit in
601
+ * `device_authorizations` until something swept it — and nothing sweeps it.
602
+ *
603
+ * The numbers are what real key material measures, with room to spare. The
604
+ * package's own generators produce SPKI DER in base64: 124 characters for
605
+ * ES256 (P-256), 392 for RS256 (RSA-2048). An RSA-4096 key would be 736, an
606
+ * RSA-8192 key about 1400, and the same 4096-bit key PEM-armoured about 800 —
607
+ * so 2048 admits every shape of key anyone could reasonably present, while a
608
+ * megabyte is refused three orders of magnitude before it reaches a row.
609
+ *
610
+ * `keyId` is a UUID (36) everywhere this package generates one; 64 leaves room
611
+ * for a client that prefixes or namespaces its own. `fingerprint` is SHA-256
612
+ * hex, exactly 64, and nothing else can ever verify against the public key —
613
+ * 128 is the length a longer digest would need, and no more.
614
+ */
615
+ declare const PublicKeySchema: _sinclair_typebox.TString;
616
+ declare const KeyIdSchema: _sinclair_typebox.TString;
617
+ declare const FingerprintSchema: _sinclair_typebox.TString;
618
+ /**
619
+ * The code a person reads off the waiting device and types on their own.
620
+ *
621
+ * Loose on purpose: 8 characters plus an optional dash is what is shown, but the
622
+ * server folds whitespace, dashes and lower case away before looking anything up,
623
+ * so refusing those spellings here would refuse a code that is on screen. The
624
+ * bounds exist to stop an unbounded string reaching the database, not to spell
625
+ * out the format — `USER_CODE_ALPHABET` is the only thing that can match a row.
626
+ */
627
+ declare const UserCodeSchema: _sinclair_typebox.TString;
628
+ /**
629
+ * What `POST /_auth/device/poll` answers with.
630
+ *
631
+ * A union, because the two answers are different kinds of thing rather than one
632
+ * shape with optional fields: pending says "ask again in this long", approved is
633
+ * a completed login carrying exactly what `/_auth/login` returns. `status` is the
634
+ * discriminant, so a generated client narrows on it instead of testing which
635
+ * fields happen to be present.
636
+ *
637
+ * The mobile contract has no union type, so it exports this as one object with
638
+ * `status` required and every branch field optional — see
639
+ * `deviceAuthorization.pollStatusRule` in the bundle. `intervalMillis` is an
640
+ * integer for the same reason: that grammar carries no floating-point scalar,
641
+ * and a count of milliseconds never needed one.
642
+ *
643
+ * That integer is a promise two things keep, because nothing validates a response
644
+ * against this schema on the way out. `configureDeviceAuth` refuses an interval
645
+ * that is not a whole number of milliseconds, so the only value this branch can
646
+ * carry is one; and `contract-export.test.ts` reads this schema to check the
647
+ * exported declaration, so writing `Type.Number` here fails the suite instead of
648
+ * publishing an integer the server does not send.
649
+ */
650
+ declare const DeviceAuthPollResponseSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TObject<{
651
+ status: _sinclair_typebox.TLiteral<"pending">;
652
+ intervalMillis: _sinclair_typebox.TInteger;
653
+ }>, _sinclair_typebox.TObject<{
654
+ status: _sinclair_typebox.TLiteral<"approved">;
655
+ userId: _sinclair_typebox.TString;
656
+ publicId: _sinclair_typebox.TString;
657
+ email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
658
+ phone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
659
+ passwordChangeRequired: _sinclair_typebox.TBoolean;
660
+ sessionBinding: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"none" | "passkey">[]>>;
661
+ keyExpiresAtMillis: _sinclair_typebox.TOptional<_sinclair_typebox.TInteger>;
662
+ }>]>;
663
+ declare const PasswordSchema: _sinclair_typebox.TString;
664
+ declare const TargetTypeSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"email">, _sinclair_typebox.TLiteral<"phone">]>;
665
+ type VerificationTargetType = Static<typeof TargetTypeSchema>;
666
+ declare const VERIFICATION_TARGET_TYPES: readonly ["email", "phone"];
667
+ 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">]>;
668
+ type VerificationPurpose = Static<typeof VerificationPurposeSchema>;
669
+ declare const VERIFICATION_PURPOSES: readonly ["registration", "login", "password_reset", "email_change", "phone_change", "account_deletion"];
670
+
671
+ /**
672
+ * @spfn/auth - Verification Service
673
+ *
674
+ * Handles OTP code generation, validation, and delivery
675
+ */
676
+
677
+ interface SendVerificationCodeParams {
678
+ target: string;
679
+ targetType: VerificationTargetType;
680
+ purpose: VerificationPurpose;
681
+ }
682
+ interface SendVerificationCodeResult {
683
+ success: boolean;
684
+ expiresAt: string;
685
+ }
686
+ interface VerifyCodeParams {
687
+ target: string;
688
+ targetType: VerificationTargetType;
689
+ code: string;
690
+ purpose: VerificationPurpose;
691
+ }
692
+ interface VerifyCodeResult {
693
+ valid: boolean;
694
+ verificationToken: string;
695
+ }
696
+ /**
697
+ * Send verification code via email or SMS
698
+ */
699
+ declare function sendVerificationCodeService(params: SendVerificationCodeParams): Promise<SendVerificationCodeResult>;
700
+ /**
701
+ * Verify OTP code and return verification token
702
+ */
703
+ declare function verifyCodeService(params: VerifyCodeParams): Promise<{
704
+ valid: boolean;
705
+ verificationToken: string;
706
+ }>;
707
+
708
+ /**
709
+ * @spfn/auth - Verified-Email Signup Service
710
+ *
711
+ * A signup where the address is proven before a password exists:
712
+ *
713
+ * request -> a one-time link is emailed
714
+ * confirm -> the link is exchanged for a short-lived password-setup session
715
+ * password -> the account is created, the device registered, the user signed in
716
+ *
717
+ * The link token and the setup secret are bearer credentials, so neither is ever
718
+ * stored. Only their SHA-256 hashes are, and lookup is by hash. A database dump
719
+ * therefore yields nothing that can be presented to either step.
720
+ *
721
+ * The six-digit-code registration path is untouched and remains the default; this
722
+ * is a second entry point to the same account creation, not a replacement.
723
+ */
724
+
725
+ interface RequestSignupLinkParams {
726
+ email: string;
727
+ returnPath?: string;
728
+ }
729
+ interface RequestSignupLinkResult {
730
+ success: boolean;
731
+ expiresAt: string;
732
+ }
733
+ /**
734
+ * Step 1 — issue a confirmation link for an address.
735
+ *
736
+ * Answers identically whether or not the address already has an account. When it
737
+ * does, the owner gets a notice instead of a usable link, through the same
738
+ * dedupe window the six-digit-code path uses.
739
+ *
740
+ * Requesting again is how a resend works: every live link for the address is
741
+ * superseded first, so the newest link is the only one that opens, and any setup
742
+ * session already opened from an older link dies with it.
743
+ *
744
+ * Neither branch sends mail: both hand it to `auth.link-mail`, so the answer
745
+ * costs the same database work whichever one ran. With no pg-boss initialised
746
+ * the mail still goes out on this request — see `lib/link-mail-delivery.ts`.
747
+ */
748
+ declare function requestSignupLinkService(params: RequestSignupLinkParams): Promise<RequestSignupLinkResult>;
749
+ interface ConfirmSignupLinkParams {
750
+ token: string;
751
+ }
752
+ interface ConfirmSignupLinkResult {
753
+ email: string;
754
+ returnPath: string | null;
755
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
756
+ setupSecret: string;
757
+ setupExpiresAt: string;
758
+ }
759
+ /**
760
+ * Step 2 — exchange a link for a password-setup session.
761
+ *
762
+ * Nothing binds the row to a device or a browser, which is what lets someone
763
+ * request the link on a laptop and open it on a phone.
764
+ */
765
+ declare function confirmSignupLinkService(params: ConfirmSignupLinkParams): Promise<ConfirmSignupLinkResult>;
766
+ interface CompleteSignupParams {
767
+ setupSecret?: string;
768
+ password: string;
769
+ publicKey: string;
770
+ keyId: string;
771
+ fingerprint: string;
772
+ algorithm?: KeyAlgorithmType;
773
+ deviceName?: string;
774
+ platform?: KeyPlatformType;
775
+ metadata?: Record<string, unknown>;
776
+ /** Client address of the request, from `deviceProvenance` at the route. */
777
+ ip?: string;
778
+ /** `user-agent` of the request, already truncated at the route. */
779
+ userAgent?: string;
780
+ }
781
+ /**
782
+ * Step 3 — set the password, which is what creates the account.
783
+ *
784
+ * Run under `Transactional()`: the user row, the device key and the completion
785
+ * mark commit together. A device-key failure must not leave an account nobody
786
+ * can sign into, and a completion mark must not survive a rolled-back account.
787
+ *
788
+ * A refusal that is the user's to fix — a weak password, an app policy that
789
+ * rejects the registration — leaves the setup session usable, so the fix is
790
+ * retyping the password rather than requesting a fresh email.
791
+ */
792
+ declare function completeSignupService(params: CompleteSignupParams): Promise<RegisterResult>;
793
+
794
+ /**
795
+ * @spfn/auth - Password Reset Service
796
+ *
797
+ * Getting back into an account whose password is gone, using the address the
798
+ * account already proved:
799
+ *
800
+ * request -> a one-time link is emailed
801
+ * confirm -> the link is exchanged for a short-lived password-setup session
802
+ * complete -> the new password is written, everything else is signed out,
803
+ * and the browser that reset is signed in on a fresh device key
804
+ *
805
+ * Mirrors the verified-email signup slice deliberately — same credentials, same
806
+ * hashing, same supersede-on-resend, same interceptor moves — with two
807
+ * differences that matter.
808
+ *
809
+ * First, the request answers identically for *every* input and sends mail only
810
+ * to an account that can be reset. Signup can afford to tell an existing owner
811
+ * "you already have an account"; a reset cannot send anything to a stranger's
812
+ * mailbox, because the mail itself would be the answer to "does this address
813
+ * have an account here".
814
+ *
815
+ * Second, completing it is a credential change on a live account, so it carries
816
+ * the same blast radius as `changePasswordService`: pending device
817
+ * authorizations are denied and every active key is revoked. Whoever was signed
818
+ * in on the old password is signed out, including the attacker the reset was
819
+ * needed for.
820
+ */
821
+
822
+ interface RequestPasswordResetParams {
823
+ email: string;
824
+ returnPath?: string;
825
+ }
826
+ interface RequestPasswordResetResult {
827
+ success: boolean;
828
+ expiresAt: string;
829
+ }
830
+ /**
831
+ * Step 1 — issue a reset link for an address.
832
+ *
833
+ * Answers identically for every input: the same status, the same two fields, and
834
+ * an `expiresAt` computed the same way whether or not a row was written. An
835
+ * address with no account, an account that cannot be reset, and an account that
836
+ * can are indistinguishable to the caller — only the first of the three gets
837
+ * mail, and it goes to the owner.
838
+ *
839
+ * Requesting again is how a resend works: every live link for the account is
840
+ * superseded first, so the newest link is the only one that opens, and any setup
841
+ * session already opened from an older link dies with it.
842
+ *
843
+ * The eligible branch does not send the mail either — it hands it to
844
+ * `auth.link-mail` — so the two branches differ by a few database writes and not
845
+ * by a mail provider's round trip. With no pg-boss initialised the mail still
846
+ * goes out on this request; see `lib/link-mail-delivery.ts`.
847
+ */
848
+ declare function requestPasswordResetService(params: RequestPasswordResetParams): Promise<RequestPasswordResetResult>;
849
+ interface ConfirmPasswordResetParams {
850
+ token: string;
851
+ }
852
+ interface ConfirmPasswordResetResult {
853
+ email: string;
854
+ returnPath: string | null;
855
+ /** Handed to the proxy interceptor, which moves it into an HttpOnly cookie. */
856
+ setupSecret: string;
857
+ setupExpiresAt: string;
858
+ }
859
+ /**
860
+ * Step 2 — exchange a link for a password-setup session.
861
+ *
862
+ * Nothing binds the row to a device or a browser, which is what lets someone ask
863
+ * for the link on a laptop and open it on a phone.
864
+ */
865
+ declare function confirmPasswordResetService(params: ConfirmPasswordResetParams): Promise<ConfirmPasswordResetResult>;
866
+ interface CompletePasswordResetParams {
867
+ setupSecret?: string;
868
+ password: string;
869
+ publicKey: string;
870
+ keyId: string;
871
+ fingerprint: string;
872
+ algorithm?: KeyAlgorithmType;
873
+ deviceName?: string;
874
+ platform?: KeyPlatformType;
875
+ /** Client address of the request, from `deviceProvenance` at the route. */
876
+ ip?: string;
877
+ /** `user-agent` of the request, already truncated at the route. */
878
+ userAgent?: string;
879
+ /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
880
+ webProxy?: boolean;
881
+ }
882
+ /**
883
+ * Step 3 — set the new password, which is what completes the reset.
884
+ *
885
+ * Run under `Transactional()`: the password, the revocations, the new device key
886
+ * and the completion mark commit together. A key registration that failed after
887
+ * the revoke-all would otherwise leave an account with a new password and
888
+ * nothing signed in.
889
+ *
890
+ * A refusal that is the user's to fix — a password the policy rejects, a body
891
+ * with no device key — leaves the setup session usable, so the fix is retyping
892
+ * the password rather than asking for a fresh email.
893
+ *
894
+ * The answer carries the binding fields, like every other path that starts a
895
+ * session. A reset on an account that opted in registers a bound key — 24 hours,
896
+ * renewable only by a passkey — and this route is on the sealing interceptor's
897
+ * list, so a body that did not say so would have the proxy seal a cookie
898
+ * believing the session unbound: no user-agent check for the life of that key,
899
+ * and a sign-out a day later instead of the renewal prompt.
900
+ */
901
+ declare function completePasswordResetService(params: CompletePasswordResetParams): Promise<RegisterResult & LoginBindingFields>;
902
+
903
+ /**
904
+ * @spfn/auth - Device Auth Service
905
+ *
906
+ * Device-code login: a device with no key on file yet shows a short code, the
907
+ * account owner types that code on a device that is already signed in, and the
908
+ * waiting device's key is registered on approval.
909
+ *
910
+ * There is no token to hand over. Every request in this system is signed by the
911
+ * calling device's own key, so "logging a device in" means one thing — getting
912
+ * its public key into `user_public_keys` under the right account. That is what
913
+ * the poll does, and it is why the poll returns exactly what `loginService`
914
+ * returns: from the client's side the two ways in are indistinguishable.
915
+ *
916
+ * | state ↓ op → | info | approve | deny | poll |
917
+ * | --- | --- | --- | --- | --- |
918
+ * | pending | device details | → approved | → denied | pending |
919
+ * | approved | AlreadyHandled | AlreadyHandled | AlreadyHandled | key registered, → consumed |
920
+ * | denied | AlreadyHandled | AlreadyHandled | AlreadyHandled | Denied |
921
+ * | consumed | NotFound | NotFound | NotFound | NotFound |
922
+ * | expired | Expired | Expired | Expired | Expired |
923
+ * | unknown | NotFound | NotFound | NotFound | NotFound |
924
+ *
925
+ * A global revocation — revoke-all, a password change, a deletion request —
926
+ * refuses the account's live records too, as `denied`, so they land in that row
927
+ * of the table. See `denyAllActiveByUserId`; the three callers are the three
928
+ * places that revoke every key at once.
929
+ */
930
+
931
+ interface StartDeviceAuthParams {
932
+ publicKey: string;
933
+ keyId: string;
934
+ fingerprint: string;
935
+ algorithm?: KeyAlgorithmType;
936
+ /** Device label shown to the approver. Display only — nothing is authorized by it. */
937
+ deviceName?: string;
938
+ platform?: KeyPlatformType;
939
+ }
940
+ interface StartDeviceAuthResult {
941
+ /** Returned once. The waiting device polls with it; the server stores only its hash. */
942
+ deviceCode: string;
943
+ /** `XXXX-XXXX`, for the waiting device's screen and nowhere else. */
944
+ userCode: string;
945
+ expiresAtMillis: number;
946
+ /** Milliseconds the waiting device should wait between polls. */
947
+ intervalMillis: number;
948
+ }
949
+ interface DeviceAuthInfoParams {
950
+ userCode: string;
951
+ }
952
+ /** What the approver is shown about the device asking to be let in. */
953
+ interface DeviceAuthInfoResult {
954
+ deviceName?: string;
955
+ /** One of `KEY_PLATFORM`, which is what the route accepts and the column stores. */
956
+ platform?: KeyPlatformType;
957
+ /** First bytes of the pending key's fingerprint, as the device list truncates it. */
958
+ fingerprintPrefix: string;
959
+ requestedAtMillis: number;
960
+ expiresAtMillis: number;
961
+ }
962
+ interface ApproveDeviceAuthParams {
963
+ userCode: string;
964
+ /** The approver, read from their session. Never from a request body. */
965
+ userId: number;
966
+ }
967
+ interface DenyDeviceAuthParams {
968
+ userCode: string;
969
+ }
970
+ interface PollDeviceAuthParams {
971
+ deviceCode: string;
972
+ /** Client address of the request, from `deviceProvenance` at the route. */
973
+ ip?: string;
974
+ /** `user-agent` of the request, already truncated at the route. */
975
+ userAgent?: string;
976
+ /**
977
+ * Whether proxy-guard recognised the trusted Next.js proxy, from the same
978
+ * helper. A waiting device polls the backend itself, so this is false there
979
+ * and the key it collects is unbound — which is what a device with no browser
980
+ * to run a WebAuthn ceremony in needs.
981
+ */
982
+ webProxy?: boolean;
983
+ }
984
+ /** Nobody has answered yet. Not an error — the waiting device waits. */
985
+ interface DeviceAuthPendingResult {
986
+ status: 'pending';
987
+ intervalMillis: number;
988
+ }
989
+ /** Approved and spent: the key is registered and this is the login it produced. */
990
+ type DeviceAuthApprovedResult = {
991
+ status: 'approved';
992
+ } & LoginResult;
993
+ type PollDeviceAuthResult = DeviceAuthPendingResult | DeviceAuthApprovedResult;
994
+ /**
995
+ * Park a new device's key and hand back the codes it needs.
996
+ *
997
+ * The caller is unauthenticated by definition — this is what a device does before
998
+ * it has any way to prove anything — so nothing here is attributed to an account.
999
+ * The record gains an owner only when someone approves it.
1000
+ */
1001
+ declare function startDeviceAuthService(params: StartDeviceAuthParams): Promise<StartDeviceAuthResult>;
1002
+ /**
1003
+ * What the approver sees before deciding.
1004
+ *
1005
+ * This is the whole defence against being talked into approving someone else's
1006
+ * device: the answer names the device that is waiting, so the person holding the
1007
+ * phone can see that it is not theirs. An approval screen that showed only the
1008
+ * code would be asking them to confirm a number they were just told.
1009
+ */
1010
+ declare function getDeviceAuthInfoService(params: DeviceAuthInfoParams): Promise<DeviceAuthInfoResult>;
1011
+ /**
1012
+ * Bind the record to the approving account.
1013
+ *
1014
+ * The key is not registered here. The waiting device may never come back, and a
1015
+ * key registered for a device that stopped listening is a signing credential
1016
+ * nobody asked for — so approval records the decision and the poll acts on it.
1017
+ *
1018
+ * Answers with the same device description `info` returns, so a client that let
1019
+ * a user approve without looking first can still show them what they just let
1020
+ * in — which is the moment someone talked into approving an attacker's device
1021
+ * has to notice and revoke it.
1022
+ */
1023
+ declare function approveDeviceAuthService(params: ApproveDeviceAuthParams): Promise<DeviceAuthInfoResult>;
1024
+ /**
1025
+ * Refuse the record, so the waiting device is told no instead of timing out.
1026
+ *
1027
+ * Denying binds no user: the point of refusing is that the account owner wants
1028
+ * nothing to do with the request.
1029
+ */
1030
+ declare function denyDeviceAuthService(params: DenyDeviceAuthParams): Promise<void>;
1031
+ /**
1032
+ * The waiting device asking whether anyone has answered.
1033
+ *
1034
+ * Approved is the one branch with a side effect, and it is a one-shot: the record
1035
+ * is spent by a conditional update that names `approved`, so of two polls that
1036
+ * arrive together exactly one registers the key. The loser matches nothing and is
1037
+ * answered as if the code were unknown — which by then it is.
1038
+ */
1039
+ declare function pollDeviceAuthService(params: PollDeviceAuthParams): Promise<PollDeviceAuthResult>;
1040
+
876
1041
  /**
877
1042
  * @spfn/auth - Passkey Service
878
1043
  *
@@ -953,6 +1118,11 @@ interface StartPasskeyEnrollmentParams {
953
1118
  /**
954
1119
  * Step 1 of enrollment — options for `navigator.credentials.create()`.
955
1120
  *
1121
+ * An enrolled account steps up first, and every account then meets the
1122
+ * recent-authentication rule this route has always had (#95). The order is what
1123
+ * makes the two independent: `assertStepUp` is a no-op for an unenrolled
1124
+ * account, so the answer such a caller gets is byte-for-byte today's.
1125
+ *
956
1126
  * `excludeCredentials` lists the caller's **live** passkeys only, so the
957
1127
  * authenticator quietly refuses one already enrolled here. Revoked ones are left
958
1128
  * out on purpose: they must not be re-enrolled either, and the check that
@@ -1000,6 +1170,8 @@ interface FinishPasskeyLoginParams {
1000
1170
  ip?: string;
1001
1171
  /** `user-agent` of the request, already truncated at the route. */
1002
1172
  userAgent?: string;
1173
+ /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
1174
+ webProxy?: boolean;
1003
1175
  }
1004
1176
  /**
1005
1177
  * Step 2 of sign-in — verify the assertion, then sign in exactly as a password
@@ -1040,8 +1212,10 @@ interface RevokePasskeyParams {
1040
1212
  /**
1041
1213
  * Retire a passkey.
1042
1214
  *
1043
- * Gated on recent authentication, because someone who walked up to an unlocked
1044
- * laptop should not be able to strip the account's credentials; and on the
1215
+ * Gated on the second factor for an enrolled account, and then — for every
1216
+ * account, enrolled or not — on recent authentication, because someone who
1217
+ * walked up to an unlocked laptop should not be able to strip the account's
1218
+ * credentials; and on the
1045
1219
  * last-recovery-credential guard, because there is no undo for the state that
1046
1220
  * would leave.
1047
1221
  *
@@ -1056,6 +1230,318 @@ declare function revokePasskeyService(params: RevokePasskeyParams): Promise<{
1056
1230
  passkeyId: string;
1057
1231
  }>;
1058
1232
 
1233
+ /**
1234
+ * @spfn/auth - Second-Factor Verification Entity
1235
+ *
1236
+ * When each device last proved the account's second factor. One row per device
1237
+ * key, which is what makes the step-up window mean "this device, recently"
1238
+ * rather than "somebody, somewhere, recently".
1239
+ *
1240
+ * `key_id` is the primary key and a foreign key onto `user_public_keys.key_id`,
1241
+ * so the row dies with the device it belongs to — a revoked key cannot leave a
1242
+ * verification behind for the next key that reuses its id, and deleting an
1243
+ * account takes these with it. A rotation is the one moment the row moves
1244
+ * rather than dying: rotating is already proof of the same device, so
1245
+ * `rotateKeyService` and the login rotation carry the verification across, or
1246
+ * the window would silently expire every time the proxy rotated a key.
1247
+ */
1248
+ /**
1249
+ * Which credential satisfied the step-up.
1250
+ *
1251
+ * Recorded for the owner-facing audit an app may build on it, and for support:
1252
+ * "recovery" is the one that should be rare, and an account stepping up with
1253
+ * recovery codes repeatedly has lost its authenticator.
1254
+ */
1255
+ declare const MFA_VERIFICATION_METHODS: readonly ["totp", "recovery", "passkey"];
1256
+ type MfaVerificationMethod = typeof MFA_VERIFICATION_METHODS[number];
1257
+ declare const mfaVerifications: drizzle_orm_pg_core.PgTableWithColumns<{
1258
+ name: "mfa_verifications";
1259
+ schema: string;
1260
+ columns: {
1261
+ keyId: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
1262
+ name: string;
1263
+ tableName: "mfa_verifications";
1264
+ dataType: "string";
1265
+ data: string;
1266
+ driverParam: string;
1267
+ notNull: true;
1268
+ hasDefault: false;
1269
+ isPrimaryKey: false;
1270
+ isAutoincrement: false;
1271
+ hasRuntimeDefault: false;
1272
+ enumValues: undefined;
1273
+ identity: undefined;
1274
+ generated: undefined;
1275
+ }>;
1276
+ userId: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
1277
+ name: string;
1278
+ tableName: "mfa_verifications";
1279
+ dataType: "number int53";
1280
+ data: number;
1281
+ driverParam: string | number;
1282
+ notNull: true;
1283
+ hasDefault: false;
1284
+ isPrimaryKey: false;
1285
+ isAutoincrement: false;
1286
+ hasRuntimeDefault: false;
1287
+ enumValues: undefined;
1288
+ identity: undefined;
1289
+ generated: undefined;
1290
+ }>;
1291
+ method: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["totp", "recovery", "passkey"] & [string, ...string[]]>>, {
1292
+ name: string;
1293
+ tableName: "mfa_verifications";
1294
+ dataType: "string enum";
1295
+ data: "passkey" | "totp" | "recovery";
1296
+ driverParam: string;
1297
+ notNull: true;
1298
+ hasDefault: false;
1299
+ isPrimaryKey: false;
1300
+ isAutoincrement: false;
1301
+ hasRuntimeDefault: false;
1302
+ enumValues: ["totp", "recovery", "passkey"] & [string, ...string[]];
1303
+ identity: undefined;
1304
+ generated: undefined;
1305
+ }>;
1306
+ verifiedAt: drizzle_orm_pg_core.PgBuildColumn<"mfa_verifications", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>>, {
1307
+ name: string;
1308
+ tableName: "mfa_verifications";
1309
+ dataType: "object date";
1310
+ data: Date;
1311
+ driverParam: string;
1312
+ notNull: true;
1313
+ hasDefault: true;
1314
+ isPrimaryKey: false;
1315
+ isAutoincrement: false;
1316
+ hasRuntimeDefault: false;
1317
+ enumValues: undefined;
1318
+ identity: undefined;
1319
+ generated: undefined;
1320
+ }>;
1321
+ };
1322
+ dialect: "pg";
1323
+ }>;
1324
+ type MfaVerification = typeof mfaVerifications.$inferSelect;
1325
+ type NewMfaVerification = typeof mfaVerifications.$inferInsert;
1326
+
1327
+ /**
1328
+ * @spfn/auth - Second Factor Service
1329
+ *
1330
+ * An optional second step the owner enrols, that the package then asks for at
1331
+ * the moments a stolen first credential would be used. Two forms: a TOTP
1332
+ * authenticator app, or a passkey the owner already enrolled and has marked as
1333
+ * satisfying a step-up. Ten single-use recovery codes come with either.
1334
+ *
1335
+ * The rule the whole feature hangs on is the one in `assertStepUp`: an account
1336
+ * that never enrolled is answered exactly as it is today, on every route. That
1337
+ * is why the enrolment check comes first and returns, before any passkey
1338
+ * configuration is read and before any second query is made — an app with no
1339
+ * passkeys configured must still be able to change a password.
1340
+ *
1341
+ * The step-up window is per **device key**, not per account: "this device
1342
+ * proved the second factor recently" is the claim a sensitive change needs, and
1343
+ * a verification made on a phone must not authorize a change from a laptop. A
1344
+ * key rotation carries the row across, because rotating is already proof of the
1345
+ * same device.
1346
+ *
1347
+ * A secret and a recovery code appear in exactly three response bodies —
1348
+ * `totp/enroll`, `totp/confirm` and `recovery/regenerate` — and nowhere else:
1349
+ * not in `status`, not in an event, and not in a log line.
1350
+ */
1351
+
1352
+ /**
1353
+ * Does this account have a second factor at all?
1354
+ *
1355
+ * One statement (`repositories/mfa-enrolment.repository.ts`), because every
1356
+ * sensitive route and every device registration asks it, including the
1357
+ * overwhelming majority that will answer false.
1358
+ */
1359
+ declare function mfaEnrolledForUser(userId: number): Promise<boolean>;
1360
+ interface AssertStepUpParams {
1361
+ userId: number;
1362
+ /** The device key this request is signed with — the window is its window. */
1363
+ keyId: string;
1364
+ /** Override the configured window. Callers needing a tighter one pass it. */
1365
+ maxAgeMs?: number;
1366
+ }
1367
+ /**
1368
+ * Refuse a sensitive change unless this device proved the second factor recently.
1369
+ *
1370
+ * **No-op for an account with no second factor.** That is the contract of the
1371
+ * whole feature: `changePassword` and `revokeAllKeys` have no recency gate
1372
+ * today, and adding one for people who never opted in would be a new refusal on
1373
+ * routes that never had one. Two of the callers do have a gate of their own —
1374
+ * the passkey routes' `assertRecentAuthentication` — and they still run it
1375
+ * afterwards; this adds a rule for enrolled accounts rather than replacing one.
1376
+ *
1377
+ * The unenrolled path therefore costs one indexed lookup and touches no passkey
1378
+ * configuration, which is what lets an app with no passkeys at all call it.
1379
+ *
1380
+ * @throws StepUpRequiredError when the account is enrolled and the window has passed
1381
+ */
1382
+ declare function assertStepUp(params: AssertStepUpParams): Promise<void>;
1383
+ /**
1384
+ * Carry a device's verification onto the key that replaces it.
1385
+ *
1386
+ * Called from the two rotation seams — `rotateKeyService`, and the `oldKeyId`
1387
+ * rotation every login path runs through `registerPublicKeyService`. Without
1388
+ * it the window would expire silently on every rotation, which the web proxy
1389
+ * does at each login: a user who stepped up a minute ago would be asked again
1390
+ * with nothing to connect it to.
1391
+ */
1392
+ declare function carryStepUpVerification(userId: number, fromKeyId: string, toKeyId: string): Promise<void>;
1393
+ interface TotpEnrolmentResult {
1394
+ /** The base32 secret, shown once. Never logged, never returned again. */
1395
+ secret: string;
1396
+ /** The same secret as the URI an authenticator app scans. */
1397
+ otpauthUri: string;
1398
+ }
1399
+ /**
1400
+ * Mint a secret and park it unconfirmed.
1401
+ *
1402
+ * Calling this twice replaces the unconfirmed row and its failure counter — a
1403
+ * user who closed the app before scanning starts over, which is also the
1404
+ * documented remedy for a row that spent its five confirm attempts. A
1405
+ * **confirmed** enrolment is refused instead: replacing a working second factor
1406
+ * is `disable` followed by a fresh enrolment, both step-up gated, so nobody
1407
+ * holding only a stolen session can swap an authenticator for their own.
1408
+ *
1409
+ * @throws MfaAlreadyEnrolledError when a confirmed enrolment already exists
1410
+ */
1411
+ declare function startTotpEnrolmentService(userId: number): Promise<TotpEnrolmentResult>;
1412
+ interface ConfirmTotpParams {
1413
+ userId: number;
1414
+ /** The device this enrolment is being confirmed from — it gets the first verification. */
1415
+ keyId: string;
1416
+ code: string;
1417
+ }
1418
+ interface ConfirmTotpResult {
1419
+ /** The ten plaintext codes, shown once. */
1420
+ recoveryCodes: string[];
1421
+ }
1422
+ /**
1423
+ * Spend the first code, which is what turns an enrolment into a second factor.
1424
+ *
1425
+ * Five wrong codes delete the unconfirmed row: at that point the person is
1426
+ * reading the wrong entry in their app, and a fresh `enroll` is both the remedy
1427
+ * and what resets the counter. A confirmed row is never deleted this way.
1428
+ *
1429
+ * The route deliberately runs this outside a transaction, so the counter
1430
+ * survives the refusal that raised it; the success path opens its own, because
1431
+ * the confirmation, the first verification and the recovery codes have to
1432
+ * commit together or not at all.
1433
+ *
1434
+ * @throws MfaNotEnrolledError | MfaVerificationFailedError
1435
+ */
1436
+ declare function confirmTotpEnrolmentService(params: ConfirmTotpParams): Promise<ConfirmTotpResult>;
1437
+ /**
1438
+ * Replace the account's recovery codes, retiring every earlier one.
1439
+ *
1440
+ * The old generation's rows stay, unspent and unreachable: a code written down
1441
+ * last year is refused with the same body as one that never existed, and the
1442
+ * record of which generation a spent code came from survives.
1443
+ *
1444
+ * @throws MfaNotEnrolledError on an account with no second factor
1445
+ */
1446
+ declare function regenerateRecoveryCodesService(userId: number): Promise<{
1447
+ recoveryCodes: string[];
1448
+ }>;
1449
+ /**
1450
+ * Take the second factor off the account entirely.
1451
+ *
1452
+ * Idempotent: an account with nothing enrolled is a success, because "make sure
1453
+ * MFA is off" has already happened. The passkeys themselves are untouched —
1454
+ * only their second-factor marks go, since a credential that signs the owner in
1455
+ * is not something this route may remove.
1456
+ */
1457
+ declare function disableMfaService(userId: number): Promise<void>;
1458
+ interface MarkPasskeyParams {
1459
+ userId: number;
1460
+ passkeyId: string;
1461
+ secondFactor: boolean;
1462
+ }
1463
+ /**
1464
+ * Mark or unmark one of the caller's passkeys as their second factor.
1465
+ *
1466
+ * Unmarking the last one is allowed, and leaves the account unenrolled. It is
1467
+ * deliberately independent of `assertNotLastRecoveryCredential`, which guards
1468
+ * `passkeys/revoke`: that rule protects a way *into* the account, and a
1469
+ * second-factor mark is not one — the credential still signs the owner in
1470
+ * afterwards, unchanged.
1471
+ *
1472
+ * @throws PasskeyNotFoundError for a credential that is not theirs, or revoked
1473
+ */
1474
+ declare function markPasskeySecondFactorService(params: MarkPasskeyParams): Promise<MfaStatus>;
1475
+ /** What the account surface shows about the second factor. No secret is in it. */
1476
+ interface MfaStatus {
1477
+ enrolled: boolean;
1478
+ /** Which second factors are live: `'totp'`, `'passkey'`, or both. */
1479
+ methods: ('totp' | 'passkey')[];
1480
+ /** Unspent codes of the current generation, for the "2 left" warning. */
1481
+ recoveryCodesRemaining: number;
1482
+ }
1483
+ /**
1484
+ * The account's second-factor state.
1485
+ *
1486
+ * Carries no secret, no otpauth URI and no recovery code — only the counts and
1487
+ * names an account screen needs. An unconfirmed enrolment is not a method: it
1488
+ * gates nothing, so reporting it would tell the owner they are protected when
1489
+ * they are not.
1490
+ */
1491
+ declare function mfaStatusService(userId: number): Promise<MfaStatus>;
1492
+ /**
1493
+ * Options for a step-up by passkey assertion.
1494
+ *
1495
+ * `allowCredentials` is empty, as it is for a sign-in and for the same reason
1496
+ * (D3): the discoverable credential on the device names itself, and the owner
1497
+ * and the second-factor mark are checked when the assertion comes back. The
1498
+ * challenge is minted with kind `'mfa'` and this account's id, so it cannot be
1499
+ * presented to `passkeys/login/verify` and a sign-in challenge cannot be
1500
+ * presented here.
1501
+ */
1502
+ declare function startStepUpService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
1503
+ interface StepUpParams {
1504
+ userId: number;
1505
+ /** The device the verification is recorded against. */
1506
+ keyId: string;
1507
+ code?: string;
1508
+ recoveryCode?: string;
1509
+ response?: AuthenticationResponseJSON;
1510
+ }
1511
+ /**
1512
+ * Re-prove the second factor on this device, refreshing its window.
1513
+ *
1514
+ * The escape hatch the exempt registration channels need: a device-code
1515
+ * approval and a passkey sign-in register a key with no verification against
1516
+ * it, by design, so the session they create would otherwise fail every
1517
+ * sensitive change with no way forward.
1518
+ *
1519
+ * @throws ValidationError when the body names none or more than one input
1520
+ * @throws MfaNotEnrolledError | MfaVerificationFailedError
1521
+ */
1522
+ declare function stepUpService(params: StepUpParams): Promise<void>;
1523
+ /**
1524
+ * Check exactly one of the three proofs, and say which one it was.
1525
+ *
1526
+ * Exactly one: two inputs in a body is a caller trying combinations, and none
1527
+ * is a malformed request. Neither is a failed verification, so both are a 400
1528
+ * rather than the uniform 401 a wrong proof gets.
1529
+ *
1530
+ * @throws ValidationError | MfaVerificationFailedError
1531
+ */
1532
+ declare function verifySecondFactor(params: StepUpParams): Promise<MfaVerificationMethod>;
1533
+ /**
1534
+ * Drop enrolments nobody ever confirmed.
1535
+ *
1536
+ * A secret handed out and abandoned is a credential sitting in a table doing
1537
+ * nothing; a day is long enough for anyone who meant to finish.
1538
+ *
1539
+ * @returns number of rows deleted
1540
+ */
1541
+ declare function sweepUnconfirmedMfaService(): Promise<{
1542
+ deleted: number;
1543
+ }>;
1544
+
1059
1545
  /**
1060
1546
  * Auth provider type
1061
1547
  *
@@ -1070,10 +1556,16 @@ declare const AuthProviderSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TL
1070
1556
  * - OAuth 기존 사용자 로그인 시
1071
1557
  * - 기기 코드 승인이 소비되어 새 기기 키가 등록될 때 (provider: 'device')
1072
1558
  *
1559
+ * `mfaEnrolled` is computed as the event is emitted (#95) and is the whole of
1560
+ * the package's opinion about the second factor: it never blocks an account
1561
+ * that has none, and this is the hook an app uses to offer enrolment at a first
1562
+ * login. It says nothing about *which* factor and carries no secret.
1563
+ *
1073
1564
  * @example
1074
1565
  * ```typescript
1075
1566
  * authLoginEvent.subscribe(async (payload) => {
1076
1567
  * await analytics.trackLogin(payload.userId, payload.provider);
1568
+ * if (!payload.mfaEnrolled) await suggestSecondFactor(payload.userId);
1077
1569
  * });
1078
1570
  * ```
1079
1571
  */
@@ -1081,7 +1573,8 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1081
1573
  email?: string | undefined;
1082
1574
  phone?: string | undefined;
1083
1575
  userId: string;
1084
- provider: "email" | "phone" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device" | "passkey";
1576
+ provider: "email" | "phone" | "passkey" | "google" | "apple" | "github" | "kakao" | "naver" | "superself" | "device";
1577
+ mfaEnrolled: boolean;
1085
1578
  }>;
1086
1579
  /**
1087
1580
  * Where a device key was registered — the door the new device came through.
@@ -1092,15 +1585,15 @@ declare const authLoginEvent: _spfn_core_event.EventDef<{
1092
1585
  * both arrive at `createVerifiedAccount`, so the two name themselves there;
1093
1586
  * `'invitation'` is the one path that stores a key without the key service.
1094
1587
  */
1095
- declare const DeviceRegistrationChannelSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"register">, _sinclair_typebox.TLiteral<"signup-link">, _sinclair_typebox.TLiteral<"invitation">, _sinclair_typebox.TLiteral<"password">, _sinclair_typebox.TLiteral<"oauth">, _sinclair_typebox.TLiteral<"oauth-native">, _sinclair_typebox.TLiteral<"device-code">, _sinclair_typebox.TLiteral<"password-reset">, _sinclair_typebox.TLiteral<"passkey">]>;
1096
- /** The nine doors a device key is registered through. */
1588
+ declare const DeviceRegistrationChannelSchema: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"register">, _sinclair_typebox.TLiteral<"signup-link">, _sinclair_typebox.TLiteral<"invitation">, _sinclair_typebox.TLiteral<"password">, _sinclair_typebox.TLiteral<"oauth">, _sinclair_typebox.TLiteral<"oauth-native">, _sinclair_typebox.TLiteral<"device-code">, _sinclair_typebox.TLiteral<"password-reset">, _sinclair_typebox.TLiteral<"passkey">, _sinclair_typebox.TLiteral<"renewal">]>;
1589
+ /** The ten doors a device key is registered through. */
1097
1590
  type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1098
1591
  /**
1099
1592
  * auth.device.registered — a new device key was added to an account
1100
1593
  *
1101
1594
  * 발행 시점:
1102
1595
  * - a key row was created for an account and the transaction that created it
1103
- * committed, on every one of the nine channels above
1596
+ * committed, on every one of the ten channels above
1104
1597
  *
1105
1598
  * This is the notice an account owner needs and could not get before: a stolen
1106
1599
  * password used to sign in on a new device was silent, because a login event
@@ -1116,6 +1609,10 @@ type DeviceRegistrationChannel = Static<typeof DeviceRegistrationChannelSchema>;
1116
1609
  * Neither the full fingerprint nor the public key is carried. The prefix is
1117
1610
  * enough to point at one entry of `listKeys`, which is what a notice needs.
1118
1611
  *
1612
+ * `mfaEnrolled` is computed as the event is emitted (#95), so a notice about a
1613
+ * new device can also be the moment an app offers a second factor to the
1614
+ * accounts that have none.
1615
+ *
1119
1616
  * @example
1120
1617
  * ```typescript
1121
1618
  * authDeviceRegisteredEvent.subscribe(async ({ userId, deviceName, ip, channel }) => {
@@ -1133,7 +1630,8 @@ declare const authDeviceRegisteredEvent: _spfn_core_event.EventDef<{
1133
1630
  userId: string;
1134
1631
  fingerprintPrefix: string;
1135
1632
  createdAtMillis: number;
1136
- channel: "password" | "register" | "oauth-native" | "oauth" | "invitation" | "signup-link" | "password-reset" | "passkey" | "device-code";
1633
+ mfaEnrolled: boolean;
1634
+ channel: "password" | "register" | "passkey" | "oauth-native" | "renewal" | "oauth" | "invitation" | "signup-link" | "password-reset" | "device-code";
1137
1635
  }>;
1138
1636
  /**
1139
1637
  * auth.register - 회원가입 성공 이벤트
@@ -1338,6 +1836,18 @@ interface RegisterPublicKeyParams {
1338
1836
  ip?: string;
1339
1837
  /** `user-agent` of the registering request, already truncated. */
1340
1838
  userAgent?: string;
1839
+ /**
1840
+ * Whether this key is bound to a passkey, as the registering path decided.
1841
+ *
1842
+ * Decided by the caller from two facts it is the only one holding: the
1843
+ * owner's `session_binding` setting, and whether `proxy-guard` recognised the
1844
+ * request as coming through the trusted Next.js proxy (`decideKeyBinding`).
1845
+ * Never read off a request body, and `platform` is not consulted — that field
1846
+ * is display-only, usually absent on a proxy-made key, and a native client
1847
+ * may declare `'web'` freely. Omitted means `'none'`, which is what every
1848
+ * path that has no opinion gets.
1849
+ */
1850
+ binding?: SessionBindingType;
1341
1851
  /**
1342
1852
  * The key this one replaces on the same device, when a revocation actually
1343
1853
  * happened. Its presence is what makes this a rotation rather than a new
@@ -1426,6 +1936,28 @@ interface KeySummary {
1426
1936
  registeredIp?: string;
1427
1937
  /** `user-agent` of the registering request, on the same terms as above. */
1428
1938
  registeredUserAgent?: string;
1939
+ /**
1940
+ * `'passkey'` when this key is bound to one, absent when it is not.
1941
+ *
1942
+ * Absent rather than `'none'`, so a deployment where nobody opted in answers
1943
+ * exactly the list it always did, and so every consumer reads "unbound" the
1944
+ * same way it reads it off a sign-in response.
1945
+ */
1946
+ binding?: SessionBindingType;
1947
+ /**
1948
+ * When this key was last seen from two client addresses inside the
1949
+ * concurrent-use window, absent when that has never been observed.
1950
+ *
1951
+ * A signal for the owner, not a refusal: addresses change legitimately, so
1952
+ * nothing is blocked by it and a device list that shows it is telling someone
1953
+ * to look rather than telling them something happened. The addresses
1954
+ * themselves are never returned.
1955
+ *
1956
+ * Only meaningful where proxy-guard is configured. Without it every web
1957
+ * request carries the Next.js server's own address, so two browsers on two
1958
+ * continents share one and this never fires.
1959
+ */
1960
+ concurrentUseAtMillis?: number;
1429
1961
  }
1430
1962
  interface ListKeysParams {
1431
1963
  userId: number;
@@ -1434,6 +1966,20 @@ interface ListKeysParams {
1434
1966
  }
1435
1967
  /** How much of the fingerprint the list returns. */
1436
1968
  declare const KEY_FINGERPRINT_PREFIX_LENGTH = 8;
1969
+ /**
1970
+ * What a registration settled on, for the caller that has to tell the browser.
1971
+ *
1972
+ * The sign-in paths put these two into their `LoginResult`, the Next.js proxy
1973
+ * copies them into the sealed cookie, and that is the whole carrier by which the
1974
+ * proxy learns a session is bound and when its key runs out. Returned rather than
1975
+ * looked up again: the row was just written (or just read), so a second query
1976
+ * would be a second answer to a question already settled.
1977
+ */
1978
+ interface RegisteredKeyBinding {
1979
+ binding: SessionBindingType;
1980
+ /** null only for a key registered before expiry was stamped at all. */
1981
+ expiresAt: Date | null;
1982
+ }
1437
1983
  /**
1438
1984
  * Register a new public key for a user
1439
1985
  *
@@ -1446,7 +1992,7 @@ declare const KEY_FINGERPRINT_PREFIX_LENGTH = 8;
1446
1992
  * @throws InvalidKeyFingerprintError fingerprint가 publicKey와 맞지 않을 때
1447
1993
  * @throws KeyAlgorithmMismatchError 키의 SPKI 타입이 선언된 algorithm과 다를 때
1448
1994
  */
1449
- declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<void>;
1995
+ declare function registerPublicKeyService(params: RegisterPublicKeyParams): Promise<RegisteredKeyBinding>;
1450
1996
  /**
1451
1997
  * Rotate user's public key (revoke old, register new)
1452
1998
  *
@@ -1804,6 +2350,8 @@ interface OAuthCallbackParams {
1804
2350
  ip?: string;
1805
2351
  /** `user-agent` of the callback request, already truncated at the route. */
1806
2352
  userAgent?: string;
2353
+ /** Whether proxy-guard recognised the trusted Next.js proxy, from the same helper. */
2354
+ webProxy?: boolean;
1807
2355
  }
1808
2356
  interface OAuthCallbackResult {
1809
2357
  redirectUrl: string;
@@ -1905,6 +2453,13 @@ interface OAuthNativeParams {
1905
2453
  ip?: string;
1906
2454
  /** `user-agent` of the request, already truncated at the route. */
1907
2455
  userAgent?: string;
2456
+ /**
2457
+ * Whether proxy-guard recognised the trusted Next.js proxy, from the same
2458
+ * helper. Accepted because every registering route spreads the whole
2459
+ * provenance, and read by nothing: a native sign-in never produces a bound
2460
+ * key — see the note at the registration below.
2461
+ */
2462
+ webProxy?: boolean;
1908
2463
  /** Apple은 첫 로그인에만 이름을 별도로 주므로 클라이언트가 전달할 수 있다. */
1909
2464
  profile?: {
1910
2465
  name?: string;
@@ -2056,6 +2611,110 @@ declare function revokeOAuth2GrantService(id: number, userId: number): Promise<v
2056
2611
  */
2057
2612
  declare function revokeAllOAuth2GrantsForUser(userId: number): Promise<void>;
2058
2613
 
2614
+ /**
2615
+ * @spfn/auth - Session Binding Service
2616
+ *
2617
+ * The opt-in from #97: an account says its web sessions should run on a key that
2618
+ * expires in hours and can only be renewed by a fresh WebAuthn assertion. A
2619
+ * session cookie copied off that machine — out of a browser profile, out of
2620
+ * DevTools, by malware — still signs like the original until that key runs out,
2621
+ * and then stops, because the copy cannot produce the assertion.
2622
+ *
2623
+ * Three rules shape this file.
2624
+ *
2625
+ * It can only be turned on where the backend can recognise the trusted Next.js
2626
+ * proxy. `clientType` is that recognition and `proxy-guard` is what sets it;
2627
+ * without it every key would be registered unbound and the setting would be a
2628
+ * switch that reports success and protects nothing, so the request is refused as
2629
+ * the configuration error it is.
2630
+ *
2631
+ * Turning it *off* is the privileged direction, which is the reverse of the
2632
+ * usual posture. `assertRecentAuthentication` is satisfied by the age of the
2633
+ * device key this request is signed with — and a cookie copied in the ten
2634
+ * minutes after a sign-in carries exactly that. So leaving `'passkey'` asks for
2635
+ * a credential the copy does not hold: a renewal-grade assertion, or the account
2636
+ * password.
2637
+ *
2638
+ * Enabling twice is idempotent. Recomputing the expiry on a repeat call would
2639
+ * make this route a way to extend a bound key without presenting anything, which
2640
+ * is the thing the short life exists to prevent.
2641
+ */
2642
+
2643
+ /** What every one of the three reads and writes here answers with. */
2644
+ interface SessionBindingResult {
2645
+ mode: SessionBindingType;
2646
+ /**
2647
+ * When the key this request is signed with expires, for a bound session.
2648
+ *
2649
+ * Absent when the account is unbound, and absent for a bound account asking
2650
+ * from a key that is not itself bound — a native client, say, whose key was
2651
+ * registered on a channel the proxy never touched. The response is the input
2652
+ * the Next.js interceptor re-seals the cookie from, so it describes this
2653
+ * session rather than the account.
2654
+ */
2655
+ keyExpiresAtMillis?: number;
2656
+ }
2657
+ interface SessionBindingParams {
2658
+ userId: number;
2659
+ /** The device key this request is signed with — the one binding is applied to. */
2660
+ keyId: string;
2661
+ /** Whether proxy-guard recognised the trusted Next.js proxy. */
2662
+ webProxy?: boolean;
2663
+ }
2664
+ interface DisableSessionBindingParams extends SessionBindingParams {
2665
+ /** A renewal-grade assertion, from `binding/disable/options`. */
2666
+ response?: AuthenticationResponseJSON;
2667
+ /** The account password, for a browser with no passkey to hand. */
2668
+ currentPassword?: string;
2669
+ }
2670
+ /** What binding is on this account, and when this session's key runs out. */
2671
+ declare function getSessionBindingService(params: SessionBindingParams): Promise<SessionBindingResult>;
2672
+ /**
2673
+ * The binding facts of one key, by key id alone.
2674
+ *
2675
+ * For `/_auth/oauth/finalize`, which is the one sealing path with no session and
2676
+ * no `LoginResult` to read them off: the browser arrives at the callback page
2677
+ * holding a key id the OAuth state minted, and the interceptor seals a session
2678
+ * from the answer. Reflecting the caller's own claim about binding would let a
2679
+ * caller ask for an unbound cookie over a bound key, so the row is read instead.
2680
+ *
2681
+ * Scoped by key id and nothing else. The id is a server-minted UUID that only
2682
+ * ever travelled inside the sealed state, so a caller who holds one holds it
2683
+ * because the flow gave it to them; adding the caller's own `userId` to the
2684
+ * lookup would make the answer vary by a second guessable value without
2685
+ * withholding anything from someone who already has the first.
2686
+ */
2687
+ declare function keySessionBindingService(keyId: string): Promise<{
2688
+ sessionBinding?: SessionBindingType;
2689
+ keyExpiresAtMillis?: number;
2690
+ }>;
2691
+ /**
2692
+ * Turn session binding on, and bind the key this request is signed with.
2693
+ *
2694
+ * The current key is bound here rather than only at the next sign-in, because
2695
+ * otherwise the setting would not apply to the session that asked for it until
2696
+ * the person signed in again — and the response is what tells the Next.js proxy
2697
+ * to re-seal the cookie with the new expiry, so the browser and the key row
2698
+ * agree from this moment on.
2699
+ *
2700
+ * @throws SessionBindingUnavailableError proxy-guard가 설정되지 않은 배포일 때
2701
+ * @throws ValidationError 살아 있는 패스키가 하나도 없을 때
2702
+ * @throws RecentAuthenticationRequiredError 최근 인증이 없을 때
2703
+ */
2704
+ declare function enableSessionBindingService(params: SessionBindingParams): Promise<SessionBindingResult>;
2705
+ /**
2706
+ * Turn session binding off, once the caller has proved they are still the owner.
2707
+ *
2708
+ * Every bound key returns to an ordinary ninety-day life in the same call. A row
2709
+ * left marked `'passkey'` would keep expiring in hours with nothing left to
2710
+ * renew it, which is the state the person just asked not to be in.
2711
+ *
2712
+ * @throws RecentAuthenticationRequiredError 새 자격증명 없이 껐을 때
2713
+ */
2714
+ declare function disableSessionBindingService(params: DisableSessionBindingParams): Promise<SessionBindingResult>;
2715
+ /** The challenge the disabling ceremony signs. Same ceremony renewal uses. */
2716
+ declare function startSessionBindingDisableService(userId: number): Promise<PublicKeyCredentialRequestOptionsJSON>;
2717
+
2059
2718
  /**
2060
2719
  * @spfn/auth - Main Router
2061
2720
  *
@@ -2163,7 +2822,7 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2163
2822
  deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2164
2823
  platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2165
2824
  }>;
2166
- }, RegisterResult>;
2825
+ }, RegisterResult & LoginBindingFields>;
2167
2826
  login: _spfn_core_route.RouteDef<{
2168
2827
  body: _sinclair_typebox.TObject<{
2169
2828
  email: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
@@ -2201,6 +2860,8 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2201
2860
  } | {
2202
2861
  email?: string | undefined;
2203
2862
  phone?: string | undefined;
2863
+ sessionBinding?: "none" | "passkey" | undefined;
2864
+ keyExpiresAtMillis?: number | undefined;
2204
2865
  status: "approved";
2205
2866
  userId: string;
2206
2867
  publicId: string;
@@ -2272,6 +2933,39 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2272
2933
  }, {}, {
2273
2934
  passkeyId: string;
2274
2935
  }>;
2936
+ mfaTotpEnroll: _spfn_core_route.RouteDef<{
2937
+ body: _sinclair_typebox.TObject<{}>;
2938
+ }, {}, TotpEnrolmentResult>;
2939
+ mfaTotpConfirm: _spfn_core_route.RouteDef<{
2940
+ body: _sinclair_typebox.TObject<{
2941
+ code: _sinclair_typebox.TString;
2942
+ }>;
2943
+ }, {}, ConfirmTotpResult>;
2944
+ mfaDisable: _spfn_core_route.RouteDef<{
2945
+ body: _sinclair_typebox.TObject<{}>;
2946
+ }, {}, void>;
2947
+ mfaMarkPasskey: _spfn_core_route.RouteDef<{
2948
+ body: _sinclair_typebox.TObject<{
2949
+ passkeyId: _sinclair_typebox.TString;
2950
+ secondFactor: _sinclair_typebox.TBoolean;
2951
+ }>;
2952
+ }, {}, MfaStatus>;
2953
+ mfaRegenerateRecoveryCodes: _spfn_core_route.RouteDef<{
2954
+ body: _sinclair_typebox.TObject<{}>;
2955
+ }, {}, {
2956
+ recoveryCodes: string[];
2957
+ }>;
2958
+ mfaStatus: _spfn_core_route.RouteDef<{}, {}, MfaStatus>;
2959
+ mfaStepUp: _spfn_core_route.RouteDef<{
2960
+ body: _sinclair_typebox.TObject<{
2961
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2962
+ recoveryCode: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2963
+ response: _sinclair_typebox.TOptional<_sinclair_typebox.TUnknown>;
2964
+ }>;
2965
+ }, {}, void>;
2966
+ mfaStepUpOptions: _spfn_core_route.RouteDef<{
2967
+ body: _sinclair_typebox.TObject<{}>;
2968
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
2275
2969
  logout: _spfn_core_route.RouteDef<{}, {}, void>;
2276
2970
  rotateKey: _spfn_core_route.RouteDef<{}, {
2277
2971
  body: _sinclair_typebox.TObject<{
@@ -2344,6 +3038,32 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2344
3038
  phoneVerified: boolean;
2345
3039
  hasPassword: boolean;
2346
3040
  }>;
3041
+ setSessionBinding: _spfn_core_route.RouteDef<{
3042
+ body: _sinclair_typebox.TObject<{
3043
+ mode: _sinclair_typebox.TUnion<[_sinclair_typebox.TLiteral<"passkey">, _sinclair_typebox.TLiteral<"none">]>;
3044
+ response: _sinclair_typebox.TOptional<_sinclair_typebox.TUnknown>;
3045
+ currentPassword: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3046
+ }>;
3047
+ }, {}, SessionBindingResult>;
3048
+ getSessionBinding: _spfn_core_route.RouteDef<{}, {}, SessionBindingResult>;
3049
+ sessionBindingDisableOptions: _spfn_core_route.RouteDef<{
3050
+ body: _sinclair_typebox.TObject<{}>;
3051
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
3052
+ sessionRenewOptions: _spfn_core_route.RouteDef<{
3053
+ body: _sinclair_typebox.TObject<{}>;
3054
+ }, {}, _simplewebauthn_server.PublicKeyCredentialRequestOptionsJSON>;
3055
+ sessionRenewVerify: _spfn_core_route.RouteDef<{
3056
+ body: _sinclair_typebox.TObject<{
3057
+ response: _sinclair_typebox.TUnknown;
3058
+ }>;
3059
+ }, {
3060
+ body: _sinclair_typebox.TObject<{
3061
+ publicKey: _sinclair_typebox.TString;
3062
+ keyId: _sinclair_typebox.TString;
3063
+ fingerprint: _sinclair_typebox.TString;
3064
+ algorithm: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>>;
3065
+ }>;
3066
+ }, SessionRenewResult>;
2347
3067
  issueOneTimeToken: _spfn_core_route.RouteDef<{}, {}, IssueOneTimeTokenResult>;
2348
3068
  requestAccountDeletion: _spfn_core_route.RouteDef<{
2349
3069
  body: _sinclair_typebox.TObject<{
@@ -2399,398 +3119,797 @@ declare const mainAuthRouter: _spfn_core_route.Router<{
2399
3119
  }, {}, {
2400
3120
  authUrl: string;
2401
3121
  }>;
2402
- oauthFinalize: _spfn_core_route.RouteDef<{
3122
+ oauthFinalize: _spfn_core_route.RouteDef<{
3123
+ body: _sinclair_typebox.TObject<{
3124
+ userId: _sinclair_typebox.TString;
3125
+ keyId: _sinclair_typebox.TString;
3126
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3127
+ }>;
3128
+ }, {}, {
3129
+ sessionBinding?: SessionBindingType;
3130
+ keyExpiresAtMillis?: number;
3131
+ success: boolean;
3132
+ userId: string;
3133
+ keyId: string;
3134
+ returnUrl: string;
3135
+ }>;
3136
+ oauthProviderStart: _spfn_core_route.RouteDef<{
3137
+ params: _sinclair_typebox.TObject<{
3138
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3139
+ }>;
3140
+ query: _sinclair_typebox.TObject<{
3141
+ state: _sinclair_typebox.TString;
3142
+ }>;
3143
+ }, {}, Response>;
3144
+ oauthProviderCallback: _spfn_core_route.RouteDef<{
3145
+ params: _sinclair_typebox.TObject<{
3146
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3147
+ }>;
3148
+ query: _sinclair_typebox.TObject<{
3149
+ code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3150
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3151
+ error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3152
+ error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3153
+ }>;
3154
+ }, {}, Response>;
3155
+ getProviderOAuthUrl: _spfn_core_route.RouteDef<{
3156
+ params: _sinclair_typebox.TObject<{
3157
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3158
+ }>;
3159
+ body: _sinclair_typebox.TObject<{
3160
+ returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3161
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
3162
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3163
+ }>;
3164
+ }, {}, {
3165
+ authUrl: string;
3166
+ }>;
3167
+ oauthNative: _spfn_core_route.RouteDef<{
3168
+ params: _sinclair_typebox.TObject<{
3169
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3170
+ }>;
3171
+ body: _sinclair_typebox.TObject<{
3172
+ idToken: _sinclair_typebox.TString;
3173
+ nonce: _sinclair_typebox.TString;
3174
+ accessToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3175
+ publicKey: _sinclair_typebox.TString;
3176
+ keyId: _sinclair_typebox.TString;
3177
+ fingerprint: _sinclair_typebox.TString;
3178
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
3179
+ deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3180
+ platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
3181
+ profile: _sinclair_typebox.TOptional<_sinclair_typebox.TObject<{
3182
+ name: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3183
+ }>>;
3184
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
3185
+ }>;
3186
+ }, {}, OAuthNativeResult>;
3187
+ oauthUnlinkNotify: _spfn_core_route.RouteDef<{
3188
+ params: _sinclair_typebox.TObject<{
3189
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3190
+ }>;
3191
+ }, {}, void | Response>;
3192
+ oauthUnlinkNotifyGet: _spfn_core_route.RouteDef<{
3193
+ params: _sinclair_typebox.TObject<{
3194
+ provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3195
+ }>;
3196
+ }, {}, void | Response>;
3197
+ getInvitation: _spfn_core_route.RouteDef<{
3198
+ params: _sinclair_typebox.TObject<{
3199
+ token: _sinclair_typebox.TString;
3200
+ }>;
3201
+ }, {}, {
3202
+ email: string;
3203
+ role: string;
3204
+ roleDisplayName: string;
3205
+ invitedBy: string;
3206
+ expiresAt: string;
3207
+ metadata: Record<string, any> | undefined;
3208
+ }>;
3209
+ acceptInvitation: _spfn_core_route.RouteDef<{
3210
+ body: _sinclair_typebox.TObject<{
3211
+ token: _sinclair_typebox.TString;
3212
+ password: _sinclair_typebox.TString;
3213
+ }>;
3214
+ }, {
3215
+ body: _sinclair_typebox.TObject<{
3216
+ publicKey: _sinclair_typebox.TString;
3217
+ keyId: _sinclair_typebox.TString;
3218
+ fingerprint: _sinclair_typebox.TString;
3219
+ algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
3220
+ }>;
3221
+ }, {
3222
+ userId: number;
3223
+ email: string;
3224
+ role: string;
3225
+ }>;
3226
+ createInvitation: _spfn_core_route.RouteDef<{
3227
+ body: _sinclair_typebox.TObject<{
3228
+ email: _sinclair_typebox.TString;
3229
+ roleId: _sinclair_typebox.TNumber;
3230
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3231
+ expiresAt: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3232
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TAny>;
3233
+ }>;
3234
+ }, {}, {
3235
+ id: number;
3236
+ email: string;
3237
+ token: string;
3238
+ roleId: number;
3239
+ expiresAt: string;
3240
+ invitationUrl: string;
3241
+ }>;
3242
+ listInvitations: _spfn_core_route.RouteDef<{
3243
+ query: _sinclair_typebox.TObject<{
3244
+ status: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"pending" | "accepted" | "expired" | "cancelled">[]>>;
3245
+ page: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3246
+ limit: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3247
+ }>;
3248
+ }, {}, {
3249
+ invitations: {
3250
+ id: number;
3251
+ email: string;
3252
+ token: string;
3253
+ roleId: number;
3254
+ invitedBy: number;
3255
+ status: "pending" | "accepted" | "expired" | "cancelled";
3256
+ expiresAt: Date;
3257
+ acceptedAt: Date | null;
3258
+ cancelledAt: Date | null;
3259
+ metadata: Record<string, any> | null;
3260
+ createdAt: Date;
3261
+ updatedAt: Date;
3262
+ role: {
3263
+ id: number;
3264
+ name: string;
3265
+ displayName: string;
3266
+ };
3267
+ inviter: {
3268
+ id: number;
3269
+ email: string | null;
3270
+ };
3271
+ }[];
3272
+ total: number;
3273
+ page: number;
3274
+ limit: number;
3275
+ totalPages: number;
3276
+ }>;
3277
+ cancelInvitation: _spfn_core_route.RouteDef<{
3278
+ body: _sinclair_typebox.TObject<{
3279
+ id: _sinclair_typebox.TNumber;
3280
+ reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3281
+ }>;
3282
+ }, {}, {
3283
+ cancelledAt: string;
3284
+ }>;
3285
+ resendInvitation: _spfn_core_route.RouteDef<{
3286
+ body: _sinclair_typebox.TObject<{
3287
+ id: _sinclair_typebox.TNumber;
3288
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3289
+ }>;
3290
+ }, {}, {
3291
+ expiresAt: string;
3292
+ }>;
3293
+ deleteInvitation: _spfn_core_route.RouteDef<{
3294
+ body: _sinclair_typebox.TObject<{
3295
+ id: _sinclair_typebox.TNumber;
3296
+ }>;
3297
+ }, {}, void>;
3298
+ getUserProfile: _spfn_core_route.RouteDef<{}, {}, UserProfile>;
3299
+ updateUserProfile: _spfn_core_route.RouteDef<{
3300
+ body: _sinclair_typebox.TObject<{
3301
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3302
+ firstName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3303
+ lastName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3304
+ avatarUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3305
+ bio: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3306
+ locale: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3307
+ timezone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3308
+ dateOfBirth: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3309
+ gender: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3310
+ website: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3311
+ location: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3312
+ company: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3313
+ jobTitle: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3314
+ metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TAny>>;
3315
+ }>;
3316
+ }, {}, ProfileInfo>;
3317
+ checkUsername: _spfn_core_route.RouteDef<{
3318
+ query: _sinclair_typebox.TObject<{
3319
+ username: _sinclair_typebox.TString;
3320
+ }>;
3321
+ }, {}, {
3322
+ available: boolean;
3323
+ }>;
3324
+ updateUsername: _spfn_core_route.RouteDef<{
3325
+ body: _sinclair_typebox.TObject<{
3326
+ username: _sinclair_typebox.TUnion<[_sinclair_typebox.TString, _sinclair_typebox.TNull]>;
3327
+ }>;
3328
+ }, {}, {
3329
+ deletedAt: Date | null;
3330
+ deletedBy: string | null;
3331
+ createdAt: Date;
3332
+ updatedAt: Date;
3333
+ id: number;
3334
+ publicId: string;
3335
+ email: string | null;
3336
+ phone: string | null;
3337
+ username: string | null;
3338
+ passwordHash: string | null;
3339
+ passwordChangeRequired: boolean;
3340
+ roleId: number;
3341
+ status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
3342
+ emailVerifiedAt: Date | null;
3343
+ phoneVerifiedAt: Date | null;
3344
+ keyEpoch: number;
3345
+ sessionBinding: "none" | "passkey";
3346
+ sessionBindingChangedAt: Date | null;
3347
+ lastLoginAt: Date | null;
3348
+ }>;
3349
+ updateLocale: _spfn_core_route.RouteDef<{
3350
+ body: _sinclair_typebox.TObject<{
3351
+ locale: _sinclair_typebox.TString;
3352
+ }>;
3353
+ }, {}, {
3354
+ locale: string;
3355
+ }>;
3356
+ listRoles: _spfn_core_route.RouteDef<{
3357
+ query: _sinclair_typebox.TObject<{
3358
+ includeInactive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
3359
+ }>;
3360
+ }, {}, {
3361
+ roles: {
3362
+ description: string | null;
3363
+ name: string;
3364
+ id: number;
3365
+ displayName: string;
3366
+ isBuiltin: boolean;
3367
+ isSystem: boolean;
3368
+ isActive: boolean;
3369
+ priority: number;
3370
+ createdAt: Date;
3371
+ updatedAt: Date;
3372
+ }[];
3373
+ }>;
3374
+ createAdminRole: _spfn_core_route.RouteDef<{
2403
3375
  body: _sinclair_typebox.TObject<{
2404
- userId: _sinclair_typebox.TString;
2405
- keyId: _sinclair_typebox.TString;
2406
- returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3376
+ name: _sinclair_typebox.TString;
3377
+ displayName: _sinclair_typebox.TString;
3378
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3379
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3380
+ permissionIds: _sinclair_typebox.TOptional<_sinclair_typebox.TArray<_sinclair_typebox.TNumber>>;
2407
3381
  }>;
2408
3382
  }, {}, {
2409
- success: boolean;
2410
- userId: string;
2411
- keyId: string;
2412
- returnUrl: string;
3383
+ role: {
3384
+ description: string | null;
3385
+ name: string;
3386
+ id: number;
3387
+ displayName: string;
3388
+ isBuiltin: boolean;
3389
+ isSystem: boolean;
3390
+ isActive: boolean;
3391
+ priority: number;
3392
+ createdAt: Date;
3393
+ updatedAt: Date;
3394
+ };
2413
3395
  }>;
2414
- oauthProviderStart: _spfn_core_route.RouteDef<{
3396
+ updateAdminRole: _spfn_core_route.RouteDef<{
2415
3397
  params: _sinclair_typebox.TObject<{
2416
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3398
+ id: _sinclair_typebox.TNumber;
2417
3399
  }>;
2418
- query: _sinclair_typebox.TObject<{
2419
- state: _sinclair_typebox.TString;
3400
+ body: _sinclair_typebox.TObject<{
3401
+ displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3402
+ description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3403
+ priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3404
+ isActive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
2420
3405
  }>;
2421
- }, {}, Response>;
2422
- oauthProviderCallback: _spfn_core_route.RouteDef<{
3406
+ }, {}, {
3407
+ role: {
3408
+ description: string | null;
3409
+ name: string;
3410
+ id: number;
3411
+ displayName: string;
3412
+ isBuiltin: boolean;
3413
+ isSystem: boolean;
3414
+ isActive: boolean;
3415
+ priority: number;
3416
+ createdAt: Date;
3417
+ updatedAt: Date;
3418
+ };
3419
+ }>;
3420
+ deleteAdminRole: _spfn_core_route.RouteDef<{
2423
3421
  params: _sinclair_typebox.TObject<{
2424
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2425
- }>;
2426
- query: _sinclair_typebox.TObject<{
2427
- code: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2428
- state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2429
- error: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2430
- error_description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3422
+ id: _sinclair_typebox.TNumber;
2431
3423
  }>;
2432
- }, {}, Response>;
2433
- getProviderOAuthUrl: _spfn_core_route.RouteDef<{
3424
+ }, {}, void>;
3425
+ updateUserRole: _spfn_core_route.RouteDef<{
2434
3426
  params: _sinclair_typebox.TObject<{
2435
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3427
+ userId: _sinclair_typebox.TNumber;
2436
3428
  }>;
2437
3429
  body: _sinclair_typebox.TObject<{
2438
- returnUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2439
- metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
2440
- state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3430
+ roleId: _sinclair_typebox.TNumber;
2441
3431
  }>;
2442
3432
  }, {}, {
2443
- authUrl: string;
3433
+ userId: number;
3434
+ roleId: number;
2444
3435
  }>;
2445
- oauthNative: _spfn_core_route.RouteDef<{
2446
- params: _sinclair_typebox.TObject<{
2447
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
2448
- }>;
3436
+ issueOpsToken: _spfn_core_route.RouteDef<{
2449
3437
  body: _sinclair_typebox.TObject<{
2450
- idToken: _sinclair_typebox.TString;
2451
- nonce: _sinclair_typebox.TString;
2452
- accessToken: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2453
- publicKey: _sinclair_typebox.TString;
2454
- keyId: _sinclair_typebox.TString;
2455
- fingerprint: _sinclair_typebox.TString;
2456
- algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
2457
- deviceName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2458
- platform: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ios" | "android" | "web" | "desktop">[]>>;
2459
- profile: _sinclair_typebox.TOptional<_sinclair_typebox.TObject<{
2460
- name: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2461
- }>>;
2462
- metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TUnknown>>;
3438
+ name: _sinclair_typebox.TString;
3439
+ scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
3440
+ expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
2463
3441
  }>;
2464
- }, {}, OAuthNativeResult>;
2465
- oauthUnlinkNotify: _spfn_core_route.RouteDef<{
3442
+ }, {}, {
3443
+ token: string;
3444
+ opsToken: {
3445
+ id: number;
3446
+ name: string;
3447
+ scopes: string[];
3448
+ expiresAt: string | null;
3449
+ revokedAt: string | null;
3450
+ lastUsedAt: string | null;
3451
+ createdAt: string | null;
3452
+ };
3453
+ }>;
3454
+ listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
3455
+ opsTokens: {
3456
+ id: number;
3457
+ name: string;
3458
+ scopes: string[];
3459
+ expiresAt: string | null;
3460
+ revokedAt: string | null;
3461
+ lastUsedAt: string | null;
3462
+ createdAt: string | null;
3463
+ }[];
3464
+ }>;
3465
+ revokeOpsToken: _spfn_core_route.RouteDef<{
2466
3466
  params: _sinclair_typebox.TObject<{
2467
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3467
+ id: _sinclair_typebox.TNumber;
2468
3468
  }>;
2469
- }, {}, void | Response>;
2470
- oauthUnlinkNotifyGet: _spfn_core_route.RouteDef<{
2471
- params: _sinclair_typebox.TObject<{
2472
- provider: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"google" | "apple" | "github" | "kakao" | "naver" | "superself">[]>;
3469
+ }, {}, {
3470
+ opsToken: {
3471
+ id: number;
3472
+ name: string;
3473
+ scopes: string[];
3474
+ expiresAt: string | null;
3475
+ revokedAt: string | null;
3476
+ lastUsedAt: string | null;
3477
+ createdAt: string | null;
3478
+ };
3479
+ }>;
3480
+ registerOAuth2Client: _spfn_core_route.RouteDef<{}, {}, Response>;
3481
+ getOAuth2Authorize: _spfn_core_route.RouteDef<{
3482
+ query: _sinclair_typebox.TObject<{
3483
+ client_id: _sinclair_typebox.TString;
3484
+ redirect_uri: _sinclair_typebox.TString;
3485
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3486
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3487
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3488
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3489
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2473
3490
  }>;
2474
- }, {}, void | Response>;
2475
- getInvitation: _spfn_core_route.RouteDef<{
3491
+ }, {}, OAuth2ConsentView>;
3492
+ createOAuth2AuthorizationCode: _spfn_core_route.RouteDef<{
3493
+ body: _sinclair_typebox.TObject<{
3494
+ approve: _sinclair_typebox.TBoolean;
3495
+ client_id: _sinclair_typebox.TString;
3496
+ redirect_uri: _sinclair_typebox.TString;
3497
+ code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3498
+ code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3499
+ resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3500
+ scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3501
+ state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3502
+ }>;
3503
+ }, {}, OAuth2AuthorizationCodeIssued>;
3504
+ oauth2Token: _spfn_core_route.RouteDef<{}, {}, Response>;
3505
+ oauth2Revoke: _spfn_core_route.RouteDef<{}, {}, Response>;
3506
+ listOAuth2Grants: _spfn_core_route.RouteDef<{}, {}, {
3507
+ grants: OAuth2GrantSummary[];
3508
+ }>;
3509
+ revokeOAuth2Grant: _spfn_core_route.RouteDef<{
2476
3510
  params: _sinclair_typebox.TObject<{
2477
- token: _sinclair_typebox.TString;
3511
+ id: _sinclair_typebox.TNumber;
2478
3512
  }>;
2479
3513
  }, {}, {
2480
- email: string;
2481
- role: string;
2482
- roleDisplayName: string;
2483
- invitedBy: string;
2484
- expiresAt: string;
2485
- metadata: Record<string, any> | undefined;
3514
+ revoked: boolean;
2486
3515
  }>;
2487
- acceptInvitation: _spfn_core_route.RouteDef<{
2488
- body: _sinclair_typebox.TObject<{
2489
- token: _sinclair_typebox.TString;
2490
- password: _sinclair_typebox.TString;
3516
+ oauth2AuthorizationServerMetadata: _spfn_core_route.RouteDef<{}, {}, Response>;
3517
+ }>;
3518
+
3519
+ /**
3520
+ * @spfn/auth - User Public Keys Entity
3521
+ *
3522
+ * Stores client-generated public keys for JWT verification
3523
+ * Supports key rotation and multi-key management per user
3524
+ */
3525
+ /**
3526
+ * User Public Keys Table
3527
+ * Each user can have multiple public keys (for rotation)
3528
+ */
3529
+ declare const userPublicKeys: drizzle_orm_pg_core.PgTableWithColumns<{
3530
+ name: "user_public_keys";
3531
+ schema: string;
3532
+ columns: {
3533
+ id: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
3534
+ name: string;
3535
+ tableName: "user_public_keys";
3536
+ dataType: "number int53";
3537
+ data: number;
3538
+ driverParam: number;
3539
+ notNull: true;
3540
+ hasDefault: true;
3541
+ isPrimaryKey: false;
3542
+ isAutoincrement: false;
3543
+ hasRuntimeDefault: false;
3544
+ enumValues: undefined;
3545
+ identity: undefined;
3546
+ generated: undefined;
2491
3547
  }>;
2492
- }, {
2493
- body: _sinclair_typebox.TObject<{
2494
- publicKey: _sinclair_typebox.TString;
2495
- keyId: _sinclair_typebox.TString;
2496
- fingerprint: _sinclair_typebox.TString;
2497
- algorithm: _sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"ES256" | "RS256">[]>;
3548
+ userId: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
3549
+ name: string;
3550
+ tableName: "user_public_keys";
3551
+ dataType: "number int53";
3552
+ data: number;
3553
+ driverParam: string | number;
3554
+ notNull: true;
3555
+ hasDefault: false;
3556
+ isPrimaryKey: false;
3557
+ isAutoincrement: false;
3558
+ hasRuntimeDefault: false;
3559
+ enumValues: undefined;
3560
+ identity: undefined;
3561
+ generated: undefined;
2498
3562
  }>;
2499
- }, {
2500
- userId: number;
2501
- email: string;
2502
- role: string;
2503
- }>;
2504
- createInvitation: _spfn_core_route.RouteDef<{
2505
- body: _sinclair_typebox.TObject<{
2506
- email: _sinclair_typebox.TString;
2507
- roleId: _sinclair_typebox.TNumber;
2508
- expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2509
- expiresAt: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2510
- metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TAny>;
3563
+ keyId: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3564
+ name: string;
3565
+ tableName: "user_public_keys";
3566
+ dataType: "string";
3567
+ data: string;
3568
+ driverParam: string;
3569
+ notNull: true;
3570
+ hasDefault: false;
3571
+ isPrimaryKey: false;
3572
+ isAutoincrement: false;
3573
+ hasRuntimeDefault: false;
3574
+ enumValues: undefined;
3575
+ identity: undefined;
3576
+ generated: undefined;
3577
+ }>;
3578
+ publicKey: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3579
+ name: string;
3580
+ tableName: "user_public_keys";
3581
+ dataType: "string";
3582
+ data: string;
3583
+ driverParam: string;
3584
+ notNull: true;
3585
+ hasDefault: false;
3586
+ isPrimaryKey: false;
3587
+ isAutoincrement: false;
3588
+ hasRuntimeDefault: false;
3589
+ enumValues: undefined;
3590
+ identity: undefined;
3591
+ generated: undefined;
2511
3592
  }>;
2512
- }, {}, {
2513
- id: number;
2514
- email: string;
2515
- token: string;
2516
- roleId: number;
2517
- expiresAt: string;
2518
- invitationUrl: string;
2519
- }>;
2520
- listInvitations: _spfn_core_route.RouteDef<{
2521
- query: _sinclair_typebox.TObject<{
2522
- status: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<_sinclair_typebox.TLiteral<"pending" | "accepted" | "expired" | "cancelled">[]>>;
2523
- page: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2524
- limit: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3593
+ algorithm: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["ES256", "RS256"] & [string, ...string[]]>>>, {
3594
+ name: string;
3595
+ tableName: "user_public_keys";
3596
+ dataType: "string enum";
3597
+ data: "ES256" | "RS256";
3598
+ driverParam: string;
3599
+ notNull: true;
3600
+ hasDefault: true;
3601
+ isPrimaryKey: false;
3602
+ isAutoincrement: false;
3603
+ hasRuntimeDefault: false;
3604
+ enumValues: ["ES256", "RS256"] & [string, ...string[]];
3605
+ identity: undefined;
3606
+ generated: undefined;
2525
3607
  }>;
2526
- }, {}, {
2527
- invitations: {
2528
- id: number;
2529
- email: string;
2530
- token: string;
2531
- roleId: number;
2532
- invitedBy: number;
2533
- status: "pending" | "accepted" | "expired" | "cancelled";
2534
- expiresAt: Date;
2535
- acceptedAt: Date | null;
2536
- cancelledAt: Date | null;
2537
- metadata: Record<string, any> | null;
2538
- createdAt: Date;
2539
- updatedAt: Date;
2540
- role: {
2541
- id: number;
2542
- name: string;
2543
- displayName: string;
2544
- };
2545
- inviter: {
2546
- id: number;
2547
- email: string | null;
2548
- };
2549
- }[];
2550
- total: number;
2551
- page: number;
2552
- limit: number;
2553
- totalPages: number;
2554
- }>;
2555
- cancelInvitation: _spfn_core_route.RouteDef<{
2556
- body: _sinclair_typebox.TObject<{
2557
- id: _sinclair_typebox.TNumber;
2558
- reason: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3608
+ fingerprint: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3609
+ name: string;
3610
+ tableName: "user_public_keys";
3611
+ dataType: "string";
3612
+ data: string;
3613
+ driverParam: string;
3614
+ notNull: true;
3615
+ hasDefault: false;
3616
+ isPrimaryKey: false;
3617
+ isAutoincrement: false;
3618
+ hasRuntimeDefault: false;
3619
+ enumValues: undefined;
3620
+ identity: undefined;
3621
+ generated: undefined;
2559
3622
  }>;
2560
- }, {}, {
2561
- cancelledAt: string;
2562
- }>;
2563
- resendInvitation: _spfn_core_route.RouteDef<{
2564
- body: _sinclair_typebox.TObject<{
2565
- id: _sinclair_typebox.TNumber;
2566
- expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
3623
+ deviceName: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3624
+ name: string;
3625
+ tableName: "user_public_keys";
3626
+ dataType: "string";
3627
+ data: string;
3628
+ driverParam: string;
3629
+ notNull: false;
3630
+ hasDefault: false;
3631
+ isPrimaryKey: false;
3632
+ isAutoincrement: false;
3633
+ hasRuntimeDefault: false;
3634
+ enumValues: undefined;
3635
+ identity: undefined;
3636
+ generated: undefined;
2567
3637
  }>;
2568
- }, {}, {
2569
- expiresAt: string;
2570
- }>;
2571
- deleteInvitation: _spfn_core_route.RouteDef<{
2572
- body: _sinclair_typebox.TObject<{
2573
- id: _sinclair_typebox.TNumber;
3638
+ platform: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<["ios", "android", "web", "desktop"] & [string, ...string[]]>, {
3639
+ name: string;
3640
+ tableName: "user_public_keys";
3641
+ dataType: "string enum";
3642
+ data: "ios" | "android" | "web" | "desktop";
3643
+ driverParam: string;
3644
+ notNull: false;
3645
+ hasDefault: false;
3646
+ isPrimaryKey: false;
3647
+ isAutoincrement: false;
3648
+ hasRuntimeDefault: false;
3649
+ enumValues: ["ios", "android", "web", "desktop"] & [string, ...string[]];
3650
+ identity: undefined;
3651
+ generated: undefined;
2574
3652
  }>;
2575
- }, {}, void>;
2576
- getUserProfile: _spfn_core_route.RouteDef<{}, {}, UserProfile>;
2577
- updateUserProfile: _spfn_core_route.RouteDef<{
2578
- body: _sinclair_typebox.TObject<{
2579
- displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2580
- firstName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2581
- lastName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2582
- avatarUrl: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2583
- bio: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2584
- locale: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2585
- timezone: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2586
- dateOfBirth: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2587
- gender: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2588
- website: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2589
- location: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2590
- company: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2591
- jobTitle: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2592
- metadata: _sinclair_typebox.TOptional<_sinclair_typebox.TRecord<_sinclair_typebox.TString, _sinclair_typebox.TAny>>;
3653
+ registeredIp: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3654
+ name: string;
3655
+ tableName: "user_public_keys";
3656
+ dataType: "string";
3657
+ data: string;
3658
+ driverParam: string;
3659
+ notNull: false;
3660
+ hasDefault: false;
3661
+ isPrimaryKey: false;
3662
+ isAutoincrement: false;
3663
+ hasRuntimeDefault: false;
3664
+ enumValues: undefined;
3665
+ identity: undefined;
3666
+ generated: undefined;
2593
3667
  }>;
2594
- }, {}, ProfileInfo>;
2595
- checkUsername: _spfn_core_route.RouteDef<{
2596
- query: _sinclair_typebox.TObject<{
2597
- username: _sinclair_typebox.TString;
3668
+ registeredUserAgent: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3669
+ name: string;
3670
+ tableName: "user_public_keys";
3671
+ dataType: "string";
3672
+ data: string;
3673
+ driverParam: string;
3674
+ notNull: false;
3675
+ hasDefault: false;
3676
+ isPrimaryKey: false;
3677
+ isAutoincrement: false;
3678
+ hasRuntimeDefault: false;
3679
+ enumValues: undefined;
3680
+ identity: undefined;
3681
+ generated: undefined;
2598
3682
  }>;
2599
- }, {}, {
2600
- available: boolean;
2601
- }>;
2602
- updateUsername: _spfn_core_route.RouteDef<{
2603
- body: _sinclair_typebox.TObject<{
2604
- username: _sinclair_typebox.TUnion<[_sinclair_typebox.TString, _sinclair_typebox.TNull]>;
3683
+ registeredUaFamily: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3684
+ name: string;
3685
+ tableName: "user_public_keys";
3686
+ dataType: "string";
3687
+ data: string;
3688
+ driverParam: string;
3689
+ notNull: false;
3690
+ hasDefault: false;
3691
+ isPrimaryKey: false;
3692
+ isAutoincrement: false;
3693
+ hasRuntimeDefault: false;
3694
+ enumValues: undefined;
3695
+ identity: undefined;
3696
+ generated: undefined;
2605
3697
  }>;
2606
- }, {}, {
2607
- deletedAt: Date | null;
2608
- deletedBy: string | null;
2609
- createdAt: Date;
2610
- updatedAt: Date;
2611
- id: number;
2612
- publicId: string;
2613
- email: string | null;
2614
- phone: string | null;
2615
- username: string | null;
2616
- passwordHash: string | null;
2617
- passwordChangeRequired: boolean;
2618
- roleId: number;
2619
- status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
2620
- emailVerifiedAt: Date | null;
2621
- phoneVerifiedAt: Date | null;
2622
- keyEpoch: number;
2623
- lastLoginAt: Date | null;
2624
- }>;
2625
- updateLocale: _spfn_core_route.RouteDef<{
2626
- body: _sinclair_typebox.TObject<{
2627
- locale: _sinclair_typebox.TString;
3698
+ lastSeenIp: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3699
+ name: string;
3700
+ tableName: "user_public_keys";
3701
+ dataType: "string";
3702
+ data: string;
3703
+ driverParam: string;
3704
+ notNull: false;
3705
+ hasDefault: false;
3706
+ isPrimaryKey: false;
3707
+ isAutoincrement: false;
3708
+ hasRuntimeDefault: false;
3709
+ enumValues: undefined;
3710
+ identity: undefined;
3711
+ generated: undefined;
2628
3712
  }>;
2629
- }, {}, {
2630
- locale: string;
2631
- }>;
2632
- listRoles: _spfn_core_route.RouteDef<{
2633
- query: _sinclair_typebox.TObject<{
2634
- includeInactive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
3713
+ lastSeenAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3714
+ name: string;
3715
+ tableName: "user_public_keys";
3716
+ dataType: "object date";
3717
+ data: Date;
3718
+ driverParam: string;
3719
+ notNull: false;
3720
+ hasDefault: false;
3721
+ isPrimaryKey: false;
3722
+ isAutoincrement: false;
3723
+ hasRuntimeDefault: false;
3724
+ enumValues: undefined;
3725
+ identity: undefined;
3726
+ generated: undefined;
2635
3727
  }>;
2636
- }, {}, {
2637
- roles: {
2638
- description: string | null;
3728
+ concurrentUseAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
2639
3729
  name: string;
2640
- id: number;
2641
- displayName: string;
2642
- isBuiltin: boolean;
2643
- isSystem: boolean;
2644
- isActive: boolean;
2645
- priority: number;
2646
- createdAt: Date;
2647
- updatedAt: Date;
2648
- }[];
2649
- }>;
2650
- createAdminRole: _spfn_core_route.RouteDef<{
2651
- body: _sinclair_typebox.TObject<{
2652
- name: _sinclair_typebox.TString;
2653
- displayName: _sinclair_typebox.TString;
2654
- description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2655
- priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2656
- permissionIds: _sinclair_typebox.TOptional<_sinclair_typebox.TArray<_sinclair_typebox.TNumber>>;
3730
+ tableName: "user_public_keys";
3731
+ dataType: "object date";
3732
+ data: Date;
3733
+ driverParam: string;
3734
+ notNull: false;
3735
+ hasDefault: false;
3736
+ isPrimaryKey: false;
3737
+ isAutoincrement: false;
3738
+ hasRuntimeDefault: false;
3739
+ enumValues: undefined;
3740
+ identity: undefined;
3741
+ generated: undefined;
2657
3742
  }>;
2658
- }, {}, {
2659
- role: {
2660
- description: string | null;
3743
+ binding: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["none", "passkey"] & [string, ...string[]]>>>, {
2661
3744
  name: string;
2662
- id: number;
2663
- displayName: string;
2664
- isBuiltin: boolean;
2665
- isSystem: boolean;
2666
- isActive: boolean;
2667
- priority: number;
2668
- createdAt: Date;
2669
- updatedAt: Date;
2670
- };
2671
- }>;
2672
- updateAdminRole: _spfn_core_route.RouteDef<{
2673
- params: _sinclair_typebox.TObject<{
2674
- id: _sinclair_typebox.TNumber;
3745
+ tableName: "user_public_keys";
3746
+ dataType: "string enum";
3747
+ data: "none" | "passkey";
3748
+ driverParam: string;
3749
+ notNull: true;
3750
+ hasDefault: true;
3751
+ isPrimaryKey: false;
3752
+ isAutoincrement: false;
3753
+ hasRuntimeDefault: false;
3754
+ enumValues: ["none", "passkey"] & [string, ...string[]];
3755
+ identity: undefined;
3756
+ generated: undefined;
2675
3757
  }>;
2676
- body: _sinclair_typebox.TObject<{
2677
- displayName: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2678
- description: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2679
- priority: _sinclair_typebox.TOptional<_sinclair_typebox.TNumber>;
2680
- isActive: _sinclair_typebox.TOptional<_sinclair_typebox.TBoolean>;
3758
+ clientKind: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<["web", "ios", "android"] & [string, ...string[]]>, {
3759
+ name: string;
3760
+ tableName: "user_public_keys";
3761
+ dataType: "string enum";
3762
+ data: "ios" | "android" | "web";
3763
+ driverParam: string;
3764
+ notNull: false;
3765
+ hasDefault: false;
3766
+ isPrimaryKey: false;
3767
+ isAutoincrement: false;
3768
+ hasRuntimeDefault: false;
3769
+ enumValues: ["web", "ios", "android"] & [string, ...string[]];
3770
+ identity: undefined;
3771
+ generated: undefined;
2681
3772
  }>;
2682
- }, {}, {
2683
- role: {
2684
- description: string | null;
3773
+ clientVersion: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
2685
3774
  name: string;
2686
- id: number;
2687
- displayName: string;
2688
- isBuiltin: boolean;
2689
- isSystem: boolean;
2690
- isActive: boolean;
2691
- priority: number;
2692
- createdAt: Date;
2693
- updatedAt: Date;
2694
- };
2695
- }>;
2696
- deleteAdminRole: _spfn_core_route.RouteDef<{
2697
- params: _sinclair_typebox.TObject<{
2698
- id: _sinclair_typebox.TNumber;
3775
+ tableName: "user_public_keys";
3776
+ dataType: "string";
3777
+ data: string;
3778
+ driverParam: string;
3779
+ notNull: false;
3780
+ hasDefault: false;
3781
+ isPrimaryKey: false;
3782
+ isAutoincrement: false;
3783
+ hasRuntimeDefault: false;
3784
+ enumValues: undefined;
3785
+ identity: undefined;
3786
+ generated: undefined;
2699
3787
  }>;
2700
- }, {}, void>;
2701
- updateUserRole: _spfn_core_route.RouteDef<{
2702
- params: _sinclair_typebox.TObject<{
2703
- userId: _sinclair_typebox.TNumber;
3788
+ clientContractVersion: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3789
+ name: string;
3790
+ tableName: "user_public_keys";
3791
+ dataType: "string";
3792
+ data: string;
3793
+ driverParam: string;
3794
+ notNull: false;
3795
+ hasDefault: false;
3796
+ isPrimaryKey: false;
3797
+ isAutoincrement: false;
3798
+ hasRuntimeDefault: false;
3799
+ enumValues: undefined;
3800
+ identity: undefined;
3801
+ generated: undefined;
2704
3802
  }>;
2705
- body: _sinclair_typebox.TObject<{
2706
- roleId: _sinclair_typebox.TNumber;
3803
+ clientSeenAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3804
+ name: string;
3805
+ tableName: "user_public_keys";
3806
+ dataType: "object date";
3807
+ data: Date;
3808
+ driverParam: string;
3809
+ notNull: false;
3810
+ hasDefault: false;
3811
+ isPrimaryKey: false;
3812
+ isAutoincrement: false;
3813
+ hasRuntimeDefault: false;
3814
+ enumValues: undefined;
3815
+ identity: undefined;
3816
+ generated: undefined;
2707
3817
  }>;
2708
- }, {}, {
2709
- userId: number;
2710
- roleId: number;
2711
- }>;
2712
- issueOpsToken: _spfn_core_route.RouteDef<{
2713
- body: _sinclair_typebox.TObject<{
2714
- name: _sinclair_typebox.TString;
2715
- scopes: _sinclair_typebox.TArray<_sinclair_typebox.TString>;
2716
- expiresInDays: _sinclair_typebox.TOptional<_sinclair_typebox.TUnion<[_sinclair_typebox.TNumber, _sinclair_typebox.TNull]>>;
3818
+ isActive: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
3819
+ name: string;
3820
+ tableName: "user_public_keys";
3821
+ dataType: "boolean";
3822
+ data: boolean;
3823
+ driverParam: boolean;
3824
+ notNull: true;
3825
+ hasDefault: true;
3826
+ isPrimaryKey: false;
3827
+ isAutoincrement: false;
3828
+ hasRuntimeDefault: false;
3829
+ enumValues: undefined;
3830
+ identity: undefined;
3831
+ generated: undefined;
2717
3832
  }>;
2718
- }, {}, {
2719
- token: string;
2720
- opsToken: {
2721
- id: number;
3833
+ createdAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>>, {
2722
3834
  name: string;
2723
- scopes: string[];
2724
- expiresAt: string | null;
2725
- revokedAt: string | null;
2726
- lastUsedAt: string | null;
2727
- createdAt: string | null;
2728
- };
2729
- }>;
2730
- listOpsTokens: _spfn_core_route.RouteDef<{}, {}, {
2731
- opsTokens: {
2732
- id: number;
3835
+ tableName: "user_public_keys";
3836
+ dataType: "object date";
3837
+ data: Date;
3838
+ driverParam: string;
3839
+ notNull: true;
3840
+ hasDefault: true;
3841
+ isPrimaryKey: false;
3842
+ isAutoincrement: false;
3843
+ hasRuntimeDefault: false;
3844
+ enumValues: undefined;
3845
+ identity: undefined;
3846
+ generated: undefined;
3847
+ }>;
3848
+ lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
2733
3849
  name: string;
2734
- scopes: string[];
2735
- expiresAt: string | null;
2736
- revokedAt: string | null;
2737
- lastUsedAt: string | null;
2738
- createdAt: string | null;
2739
- }[];
2740
- }>;
2741
- revokeOpsToken: _spfn_core_route.RouteDef<{
2742
- params: _sinclair_typebox.TObject<{
2743
- id: _sinclair_typebox.TNumber;
3850
+ tableName: "user_public_keys";
3851
+ dataType: "object date";
3852
+ data: Date;
3853
+ driverParam: string;
3854
+ notNull: false;
3855
+ hasDefault: false;
3856
+ isPrimaryKey: false;
3857
+ isAutoincrement: false;
3858
+ hasRuntimeDefault: false;
3859
+ enumValues: undefined;
3860
+ identity: undefined;
3861
+ generated: undefined;
2744
3862
  }>;
2745
- }, {}, {
2746
- opsToken: {
2747
- id: number;
3863
+ expiresAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
2748
3864
  name: string;
2749
- scopes: string[];
2750
- expiresAt: string | null;
2751
- revokedAt: string | null;
2752
- lastUsedAt: string | null;
2753
- createdAt: string | null;
2754
- };
2755
- }>;
2756
- registerOAuth2Client: _spfn_core_route.RouteDef<{}, {}, Response>;
2757
- getOAuth2Authorize: _spfn_core_route.RouteDef<{
2758
- query: _sinclair_typebox.TObject<{
2759
- client_id: _sinclair_typebox.TString;
2760
- redirect_uri: _sinclair_typebox.TString;
2761
- code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2762
- code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2763
- resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2764
- scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2765
- state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3865
+ tableName: "user_public_keys";
3866
+ dataType: "object date";
3867
+ data: Date;
3868
+ driverParam: string;
3869
+ notNull: false;
3870
+ hasDefault: false;
3871
+ isPrimaryKey: false;
3872
+ isAutoincrement: false;
3873
+ hasRuntimeDefault: false;
3874
+ enumValues: undefined;
3875
+ identity: undefined;
3876
+ generated: undefined;
2766
3877
  }>;
2767
- }, {}, OAuth2ConsentView>;
2768
- createOAuth2AuthorizationCode: _spfn_core_route.RouteDef<{
2769
- body: _sinclair_typebox.TObject<{
2770
- approve: _sinclair_typebox.TBoolean;
2771
- client_id: _sinclair_typebox.TString;
2772
- redirect_uri: _sinclair_typebox.TString;
2773
- code_challenge: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2774
- code_challenge_method: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2775
- resource: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2776
- scope: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
2777
- state: _sinclair_typebox.TOptional<_sinclair_typebox.TString>;
3878
+ revokedAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3879
+ name: string;
3880
+ tableName: "user_public_keys";
3881
+ dataType: "object date";
3882
+ data: Date;
3883
+ driverParam: string;
3884
+ notNull: false;
3885
+ hasDefault: false;
3886
+ isPrimaryKey: false;
3887
+ isAutoincrement: false;
3888
+ hasRuntimeDefault: false;
3889
+ enumValues: undefined;
3890
+ identity: undefined;
3891
+ generated: undefined;
2778
3892
  }>;
2779
- }, {}, OAuth2AuthorizationCodeIssued>;
2780
- oauth2Token: _spfn_core_route.RouteDef<{}, {}, Response>;
2781
- oauth2Revoke: _spfn_core_route.RouteDef<{}, {}, Response>;
2782
- listOAuth2Grants: _spfn_core_route.RouteDef<{}, {}, {
2783
- grants: OAuth2GrantSummary[];
2784
- }>;
2785
- revokeOAuth2Grant: _spfn_core_route.RouteDef<{
2786
- params: _sinclair_typebox.TObject<{
2787
- id: _sinclair_typebox.TNumber;
3893
+ revokedReason: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3894
+ name: string;
3895
+ tableName: "user_public_keys";
3896
+ dataType: "string";
3897
+ data: string;
3898
+ driverParam: string;
3899
+ notNull: false;
3900
+ hasDefault: false;
3901
+ isPrimaryKey: false;
3902
+ isAutoincrement: false;
3903
+ hasRuntimeDefault: false;
3904
+ enumValues: undefined;
3905
+ identity: undefined;
3906
+ generated: undefined;
2788
3907
  }>;
2789
- }, {}, {
2790
- revoked: boolean;
2791
- }>;
2792
- oauth2AuthorizationServerMetadata: _spfn_core_route.RouteDef<{}, {}, Response>;
3908
+ };
3909
+ dialect: "pg";
2793
3910
  }>;
3911
+ type UserPublicKey = typeof userPublicKeys.$inferSelect;
3912
+ type NewUserPublicKey = typeof userPublicKeys.$inferInsert;
2794
3913
 
2795
3914
  /**
2796
3915
  * The auth-profile registry the authenticate middleware dispatches on.
@@ -2931,6 +4050,46 @@ declare module 'hono' {
2931
4050
  auth: AuthContext;
2932
4051
  }
2933
4052
  }
4053
+ /** Why a Bearer credential was not admitted, in the order the steps run. */
4054
+ type BearerRefusal = 'absent' | 'machine' | 'undecodable' | 'unknown' | 'expired' | 'token-expired' | 'bad-signature' | 'unverifiable';
4055
+ /** What a Bearer credential resolved to: the key row it named, or why not. */
4056
+ type BearerOutcome = {
4057
+ key: UserPublicKey;
4058
+ } | {
4059
+ refused: BearerRefusal;
4060
+ };
4061
+ /**
4062
+ * Admit the Bearer credential on this request, or say what stopped it.
4063
+ *
4064
+ * The one lookup-and-verify path every Bearer middleware takes — header, machine
4065
+ * discriminator, decode, key row, expiry, signature — kept in one place because
4066
+ * the order is itself a rule: a machine credential never reaches a decode, and a
4067
+ * key row is found before its signature is checked so that an unknown key and a
4068
+ * forged one cost the same work.
4069
+ *
4070
+ * It answers rather than throws, which is what lets two middlewares share it.
4071
+ * `authenticate` turns each refusal into the error that step has always
4072
+ * answered with; `authenticateForRenewal` turns every one of them into a single
4073
+ * refusal, so a caller holding no private key cannot tell a live key id from a
4074
+ * dead one. A shared step that threw would have to be unwound to get there.
4075
+ *
4076
+ * @param c - the Hono context of the request being authenticated
4077
+ * @param admitsExpired - whether a key past its `expiresAt` may still be
4078
+ * admitted. Renewal is the only caller that says yes, and only for a bound key
4079
+ * inside its grace.
4080
+ */
4081
+ declare function admitBearerKey(c: Context, admitsExpired: (key: UserPublicKey) => boolean): Promise<BearerOutcome>;
4082
+ /**
4083
+ * The principal a verified Bearer key resolves to.
4084
+ *
4085
+ * The one place the Bearer path builds an `AuthContext`, so that a middleware
4086
+ * added beside `authenticate` cannot invent a second shape of it.
4087
+ */
4088
+ declare function bearerAuthContext(keyId: string, resolved: {
4089
+ user: User;
4090
+ role: string | null;
4091
+ locale: string;
4092
+ }): AuthContext;
2934
4093
  /**
2935
4094
  * Authentication middleware
2936
4095
  *
@@ -3084,4 +4243,4 @@ declare const machineAuth: _spfn_core_route.NamedMiddleware<"machineAuth">;
3084
4243
  */
3085
4244
  declare const requireMachineScope: _spfn_core_route.NamedMiddlewareFactory<"machineScope", string[]>;
3086
4245
 
3087
- export { type DenyDeviceAuthParams as $, type AuthInitOptions as A, type AuthDeletionRequestedPayload as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type AuthDeviceRegisteredPayload as E, type FinishPasskeyEnrollmentResult as F, type AuthLoginPayload as G, type AuthPasswordResetPayload as H, type IssueOneTimeTokenResult as I, type AuthProfileOutcome as J, type KeySummary as K, type LoginResult as L, type AuthProfileVerifier as M, type NewPasskey as N, type OAuthStartResult as O, type PermissionConfig as P, AuthProviderSchema as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type AuthRegisterPayload as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type ChangePasswordParams as W, type CompletePasswordResetParams as X, type CompleteSignupParams as Y, type ConfirmPasswordResetParams as Z, type ConfirmSignupLinkParams as _, type RegisterResult as a, assertRecentAuthentication as a$, type DeviceAuthApprovedResult as a0, type DeviceAuthInfoParams as a1, type DeviceAuthPendingResult as a2, DeviceAuthPollResponseSchema as a3, DeviceNameSchema as a4, type DeviceRegistrationChannel as a5, EmailSchema as a6, FingerprintSchema as a7, type FinishPasskeyEnrollmentParams as a8, type FinishPasskeyLoginParams as a9, type PollDeviceAuthResult as aA, PublicKeySchema as aB, type RecentAuthenticationParams as aC, type RegisterParams as aD, type RegisterPublicKeyParams as aE, type RenamePasskeyParams as aF, type RequestPasswordResetParams as aG, type RequestSignupLinkParams as aH, type RevokeAllKeysParams as aI, type RevokeKeyParams as aJ, type RevokePasskeyParams as aK, type RotateKeyParams as aL, type SendVerificationCodeParams as aM, type StartDeviceAuthParams as aN, type StartPasskeyEnrollmentParams as aO, TargetTypeSchema as aP, type UnlinkNotification as aQ, UnlinkNotifyRejection as aR, type UnlinkNotifyRequest as aS, type UnlinkNotifyResult as aT, UserCodeSchema as aU, VerificationPurposeSchema as aV, type VerifyCodeParams as aW, type VerifyCodeResult as aX, approveDeviceAuthService as aY, approveOAuth2AuthorizeService as aZ, assertNotLastRecoveryCredential as a_, type InvitationAcceptedPayload as aa, type InvitationCreatedPayload as ab, KEY_FINGERPRINT_PREFIX_LENGTH as ac, KeyIdSchema as ad, type LoginParams as ae, type LogoutParams as af, type MachinePrincipal as ag, type MachineVerifierRegistration as ah, type NativeVerifyOptions as ai, type NormalizedIdentity as aj, type OAuth2AuthorizeParams as ak, type OAuth2ScopeDescription as al, type OAuthCallbackParams as am, type OAuthCallbackResult as an, type OAuthCodeExchangeOptions as ao, type OAuthNativeParams as ap, type OAuthStartParams as aq, type OAuthTokens as ar, type OAuthUnlinkedPayload as as, PASSKEY_DEVICE_TYPES as at, PASSKEY_LABEL_MAX_LENGTH as au, type PasskeyDeviceType as av, PasswordSchema as aw, PhoneSchema as ax, PlatformSchema as ay, type PollDeviceAuthParams as az, type RequestSignupLinkResult as b, startDeviceAuthService as b$, authDeletionCancelledEvent as b0, authDeletionCompletedEvent as b1, authDeletionRequestedEvent as b2, authDeviceRegisteredEvent as b3, authLoginEvent as b4, authPasswordResetEvent as b5, authRegisterEvent as b6, authenticate as b7, buildOAuthErrorUrl as b8, changePasswordService as b9, oauthNativeService as bA, oauthStartService as bB, oauthUnlinkNotifyService as bC, oauthUnlinkedEvent as bD, optionalAuth as bE, passkeys as bF, pollDeviceAuthService as bG, registerAuthProfile as bH, registerMachineVerifier as bI, registerOAuthProvider as bJ, registerPublicKeyService as bK, registerService as bL, renamePasskeyService as bM, requestPasswordResetService as bN, requestSignupLinkService as bO, requireEnabledProvider as bP, requireMachineScope as bQ, resolveAuthenticatedUser as bR, revokeAllKeysService as bS, revokeAllOAuth2GrantsForUser as bT, revokeKeyService as bU, revokeOAuth2GrantService as bV, revokePasskeyService as bW, rotateKeyService as bX, runAuthProfile as bY, selectAuthProfile as bZ, sendVerificationCodeService as b_, completePasswordResetService as ba, completeSignupService as bb, confirmPasswordResetService as bc, confirmSignupLinkService as bd, denyDeviceAuthService as be, denyOAuth2AuthorizeService as bf, describeOAuth2AuthorizeRequestService as bg, finishPasskeyEnrollmentService as bh, finishPasskeyLoginService as bi, getDeviceAuthInfoService as bj, getEnabledOAuthProviders as bk, getGoogleAccessToken as bl, getMachinePrincipal as bm, getOAuthProvider as bn, getRegisteredProviders as bo, invitationAcceptedEvent as bp, invitationCreatedEvent as bq, isOAuthProviderEnabled as br, issueOneTimeTokenService as bs, listKeysService as bt, listOAuth2GrantsService as bu, listPasskeysService as bv, loginService as bw, logoutService as bx, machineAuth as by, oauthCallbackService as bz, type RequestPasswordResetResult as c, startPasskeyEnrollmentService as c0, startPasskeyLoginService as c1, verifyCodeService as c2, verifyOneTimeTokenService as c3, 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 AuthDeletionCancelledPayload as y, type AuthDeletionCompletedPayload as z };
4246
+ export { type AuthLoginPayload as $, type AuthInitOptions as A, type Passkey as B, type ConfirmSignupLinkResult as C, type DeviceAuthInfoResult as D, type MfaVerification as E, type FinishPasskeyEnrollmentResult as F, type MfaVerificationMethod as G, type UserPublicKey as H, type IssueOneTimeTokenResult as I, type AuthContext as J, type KeySummary as K, type LoginBindingFields as L, type MfaStatus as M, type NewUserPublicKey as N, type OAuthStartResult as O, type PermissionConfig as P, type ApproveDeviceAuthParams as Q, type RoleConfig as R, type SendVerificationCodeResult as S, type TotpEnrolmentResult as T, type UserProfile as U, VERIFICATION_PURPOSES as V, type AssertStepUpParams as W, type AuthDeletionCancelledPayload as X, type AuthDeletionCompletedPayload as Y, type AuthDeletionRequestedPayload as Z, type AuthDeviceRegisteredPayload as _, type RegisterResult as a, type RevokeAllKeysParams as a$, type AuthPasswordResetPayload as a0, type AuthProfileOutcome as a1, type AuthProfileVerifier as a2, AuthProviderSchema as a3, type AuthRegisterPayload as a4, type BearerOutcome as a5, type BearerRefusal as a6, type ChangePasswordParams as a7, type CompletePasswordResetParams as a8, type CompleteSignupParams as a9, type NativeVerifyOptions as aA, type NewMfaVerification as aB, type NormalizedIdentity as aC, type OAuth2AuthorizeParams as aD, type OAuth2ScopeDescription as aE, type OAuthCallbackParams as aF, type OAuthCallbackResult as aG, type OAuthCodeExchangeOptions as aH, type OAuthNativeParams as aI, type OAuthStartParams as aJ, type OAuthTokens as aK, type OAuthUnlinkedPayload as aL, PASSKEY_DEVICE_TYPES as aM, PASSKEY_LABEL_MAX_LENGTH as aN, type PasskeyDeviceType as aO, PasswordSchema as aP, PhoneSchema as aQ, PlatformSchema as aR, type PollDeviceAuthParams as aS, type PollDeviceAuthResult as aT, PublicKeySchema as aU, type RecentAuthenticationParams as aV, type RegisterParams as aW, type RegisterPublicKeyParams as aX, type RenamePasskeyParams as aY, type RequestPasswordResetParams as aZ, type RequestSignupLinkParams as a_, type ConfirmPasswordResetParams as aa, type ConfirmSignupLinkParams as ab, type ConfirmTotpParams as ac, type DenyDeviceAuthParams as ad, type DeviceAuthApprovedResult as ae, type DeviceAuthInfoParams as af, type DeviceAuthPendingResult as ag, DeviceAuthPollResponseSchema as ah, DeviceNameSchema as ai, type DeviceRegistrationChannel as aj, type DisableSessionBindingParams as ak, EmailSchema as al, FingerprintSchema as am, type FinishPasskeyEnrollmentParams as an, type FinishPasskeyLoginParams as ao, type FinishSessionRenewParams as ap, type InvitationAcceptedPayload as aq, type InvitationCreatedPayload as ar, KEY_FINGERPRINT_PREFIX_LENGTH as as, KeyIdSchema as at, type LoginParams as au, type LogoutParams as av, MFA_VERIFICATION_METHODS as aw, type MachinePrincipal as ax, type MachineVerifierRegistration as ay, type MarkPasskeyParams as az, type RequestSignupLinkResult as b, listOAuth2GrantsService as b$, type RevokeKeyParams as b0, type RevokePasskeyParams as b1, type RotateKeyParams as b2, type SendVerificationCodeParams as b3, type SessionBindingParams as b4, type StartDeviceAuthParams as b5, type StartPasskeyEnrollmentParams as b6, type StartSessionRenewParams as b7, type StepUpParams as b8, TargetTypeSchema as b9, completePasswordResetService as bA, completeSignupService as bB, confirmPasswordResetService as bC, confirmSignupLinkService as bD, confirmTotpEnrolmentService as bE, denyDeviceAuthService as bF, denyOAuth2AuthorizeService as bG, describeOAuth2AuthorizeRequestService as bH, disableMfaService as bI, disableSessionBindingService as bJ, enableSessionBindingService as bK, finishPasskeyEnrollmentService as bL, finishPasskeyLoginService as bM, finishSessionRenewService as bN, getDeviceAuthInfoService as bO, getEnabledOAuthProviders as bP, getGoogleAccessToken as bQ, getMachinePrincipal as bR, getOAuthProvider as bS, getRegisteredProviders as bT, getSessionBindingService as bU, invitationAcceptedEvent as bV, invitationCreatedEvent as bW, isOAuthProviderEnabled as bX, issueOneTimeTokenService as bY, keySessionBindingService as bZ, listKeysService as b_, type UnlinkNotification as ba, UnlinkNotifyRejection as bb, type UnlinkNotifyRequest as bc, type UnlinkNotifyResult as bd, UserCodeSchema as be, VerificationPurposeSchema as bf, type VerifyCodeParams as bg, type VerifyCodeResult as bh, admitBearerKey as bi, approveDeviceAuthService as bj, approveOAuth2AuthorizeService as bk, assertNotLastRecoveryCredential as bl, assertRecentAuthentication as bm, assertStepUp as bn, authDeletionCancelledEvent as bo, authDeletionCompletedEvent as bp, authDeletionRequestedEvent as bq, authDeviceRegisteredEvent as br, authLoginEvent as bs, authPasswordResetEvent as bt, authRegisterEvent as bu, authenticate as bv, bearerAuthContext as bw, buildOAuthErrorUrl as bx, carryStepUpVerification as by, changePasswordService as bz, type RequestPasswordResetResult as c, listPasskeysService as c0, loginService as c1, logoutService as c2, machineAuth as c3, markPasskeySecondFactorService as c4, mfaEnrolledForUser as c5, mfaStatusService as c6, mfaVerifications as c7, oauthCallbackService as c8, oauthNativeService as c9, sendVerificationCodeService as cA, startDeviceAuthService as cB, startPasskeyEnrollmentService as cC, startPasskeyLoginService as cD, startSessionBindingDisableService as cE, startSessionRenewService as cF, startStepUpService as cG, startTotpEnrolmentService as cH, stepUpService as cI, sweepUnconfirmedMfaService as cJ, userPublicKeys as cK, verifyCodeService as cL, verifyOneTimeTokenService as cM, verifySecondFactor as cN, oauthStartService as ca, oauthUnlinkNotifyService as cb, oauthUnlinkedEvent as cc, optionalAuth as cd, passkeys as ce, pollDeviceAuthService as cf, regenerateRecoveryCodesService as cg, registerAuthProfile as ch, registerMachineVerifier as ci, registerOAuthProvider as cj, registerPublicKeyService as ck, registerService as cl, renamePasskeyService as cm, requestPasswordResetService as cn, requestSignupLinkService as co, requireEnabledProvider as cp, requireMachineScope as cq, resolveAuthenticatedUser as cr, revokeAllKeysService as cs, revokeAllOAuth2GrantsForUser as ct, revokeKeyService as cu, revokeOAuth2GrantService as cv, revokePasskeyService as cw, rotateKeyService as cx, runAuthProfile as cy, selectAuthProfile as cz, type ConfirmPasswordResetResult as d, type LoginResult as e, type StartDeviceAuthResult as f, type PasskeySummary as g, type ConfirmTotpResult as h, type RotateKeyResult as i, type RevokeAllKeysResult as j, type SessionBindingResult as k, type SessionRenewResult as l, mainAuthRouter as m, type OAuthNativeResult as n, type ProfileInfo as o, type OAuth2ConsentView as p, type OAuth2AuthorizationCodeIssued as q, type OAuth2GrantSummary as r, type AuthSession as s, PERMISSION_CATEGORIES as t, type PermissionCategory as u, VERIFICATION_TARGET_TYPES as v, type VerificationPurpose as w, type VerificationTargetType as x, type OAuthProvider as y, type NewPasskey as z };