@spfn/auth 0.3.0-beta.23 → 0.3.0-beta.25

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 (35) hide show
  1. package/README.md +328 -7
  2. package/dist/client-proof.d.ts +10 -1
  3. package/dist/client-proof.js +136 -11
  4. package/dist/client-proof.js.map +1 -1
  5. package/dist/client.d.ts +101 -1
  6. package/dist/client.js +65 -0
  7. package/dist/client.js.map +1 -1
  8. package/dist/config.d.ts +122 -0
  9. package/dist/config.js +53 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/crypto.d.ts +1 -1
  12. package/dist/errors.d.ts +159 -3
  13. package/dist/errors.js +95 -2
  14. package/dist/errors.js.map +1 -1
  15. package/dist/index.d.ts +52 -12
  16. package/dist/index.js +104 -3
  17. package/dist/index.js.map +1 -1
  18. package/dist/{machine-principals-CaEFq61K.d.ts → machine-principals-CdEgxOB1.d.ts} +2049 -771
  19. package/dist/nextjs/api.js +329 -37
  20. package/dist/nextjs/api.js.map +1 -1
  21. package/dist/nextjs/server.d.ts +59 -24
  22. package/dist/nextjs/server.js +105 -11
  23. package/dist/nextjs/server.js.map +1 -1
  24. package/dist/server.d.ts +415 -332
  25. package/dist/server.js +2933 -1545
  26. package/dist/server.js.map +1 -1
  27. package/dist/{session-Dfwu5g2W.d.ts → session-BbhAGZtA.d.ts} +57 -1
  28. package/dist/{types-DYyhze28.d.ts → types-CTdoTOxM.d.ts} +24 -1
  29. package/migrations/20260918184037_happy_mordo/migration.sql +4 -0
  30. package/migrations/20260918184037_happy_mordo/snapshot.json +6000 -0
  31. package/migrations/20260918184152_dear_rictor/migration.sql +3 -0
  32. package/migrations/20260918184152_dear_rictor/snapshot.json +6039 -0
  33. package/migrations/20260919023107_even_mikhail_rasputin/migration.sql +20 -0
  34. package/migrations/20260919023107_even_mikhail_rasputin/snapshot.json +6300 -0
  35. package/package.json +1 -1
package/dist/server.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { A as AuthInitOptions, v as OAuthProvider, t as VerificationPurpose, N as NewPasskey, w as Passkey, x as MfaVerification, y as MfaVerificationMethod, r as PermissionCategory, z as AuthContext } from './machine-principals-CaEFq61K.js';
2
- export { B as ApproveDeviceAuthParams, E as AssertStepUpParams, G as AuthDeletionCancelledPayload, H as AuthDeletionCompletedPayload, J as AuthDeletionRequestedPayload, Q as AuthDeviceRegisteredPayload, W as AuthLoginPayload, X as AuthPasswordResetPayload, Y as AuthProfileOutcome, Z as AuthProfileVerifier, _ as AuthProviderSchema, $ as AuthRegisterPayload, a0 as ChangePasswordParams, a1 as CompletePasswordResetParams, a2 as CompleteSignupParams, a3 as ConfirmPasswordResetParams, d as ConfirmPasswordResetResult, a4 as ConfirmSignupLinkParams, C as ConfirmSignupLinkResult, a5 as ConfirmTotpParams, g as ConfirmTotpResult, a6 as DenyDeviceAuthParams, a7 as DeviceAuthApprovedResult, a8 as DeviceAuthInfoParams, D as DeviceAuthInfoResult, a9 as DeviceAuthPendingResult, aa as DeviceAuthPollResponseSchema, ab as DeviceNameSchema, ac as DeviceRegistrationChannel, ad as EmailSchema, ae as FingerprintSchema, af as FinishPasskeyEnrollmentParams, F as FinishPasskeyEnrollmentResult, ag as FinishPasskeyLoginParams, ah as InvitationAcceptedPayload, ai as InvitationCreatedPayload, I as IssueOneTimeTokenResult, aj as KEY_FINGERPRINT_PREFIX_LENGTH, ak as KeyIdSchema, K as KeySummary, al as LoginParams, L as LoginResult, am as LogoutParams, an as MFA_VERIFICATION_METHODS, ao as MachinePrincipal, ap as MachineVerifierRegistration, aq as MarkPasskeyParams, M as MfaStatus, ar as NativeVerifyOptions, as as NewMfaVerification, at as NormalizedIdentity, n as OAuth2AuthorizationCodeIssued, au as OAuth2AuthorizeParams, l as OAuth2ConsentView, o as OAuth2GrantSummary, av as OAuth2ScopeDescription, aw as OAuthCallbackParams, ax as OAuthCallbackResult, ay as OAuthCodeExchangeOptions, az as OAuthNativeParams, j as OAuthNativeResult, aA as OAuthStartParams, O as OAuthStartResult, aB as OAuthTokens, aC as OAuthUnlinkedPayload, aD as PASSKEY_DEVICE_TYPES, aE as PASSKEY_LABEL_MAX_LENGTH, aF as PasskeyDeviceType, f as PasskeySummary, aG as PasswordSchema, aH as PhoneSchema, aI as PlatformSchema, aJ as PollDeviceAuthParams, aK as PollDeviceAuthResult, aL as PublicKeySchema, aM as RecentAuthenticationParams, aN as RegisterParams, aO as RegisterPublicKeyParams, a as RegisterResult, aP as RenamePasskeyParams, aQ as RequestPasswordResetParams, c as RequestPasswordResetResult, aR as RequestSignupLinkParams, b as RequestSignupLinkResult, aS as RevokeAllKeysParams, i as RevokeAllKeysResult, aT as RevokeKeyParams, aU as RevokePasskeyParams, aV as RotateKeyParams, h as RotateKeyResult, aW as SendVerificationCodeParams, S as SendVerificationCodeResult, aX as StartDeviceAuthParams, e as StartDeviceAuthResult, aY as StartPasskeyEnrollmentParams, aZ as StepUpParams, a_ as TargetTypeSchema, T as TotpEnrolmentResult, a$ as UnlinkNotification, b0 as UnlinkNotifyRejection, b1 as UnlinkNotifyRequest, b2 as UnlinkNotifyResult, b3 as UserCodeSchema, V as VERIFICATION_PURPOSES, s as VERIFICATION_TARGET_TYPES, b4 as VerificationPurposeSchema, u as VerificationTargetType, b5 as VerifyCodeParams, b6 as VerifyCodeResult, b7 as approveDeviceAuthService, b8 as approveOAuth2AuthorizeService, b9 as assertNotLastRecoveryCredential, ba as assertRecentAuthentication, bb as assertStepUp, bc as authDeletionCancelledEvent, bd as authDeletionCompletedEvent, be as authDeletionRequestedEvent, bf as authDeviceRegisteredEvent, bg as authLoginEvent, bh as authPasswordResetEvent, bi as authRegisterEvent, m as authRouter, bj as authenticate, bk as buildOAuthErrorUrl, bl as carryStepUpVerification, bm as changePasswordService, bn as completePasswordResetService, bo as completeSignupService, bp as confirmPasswordResetService, bq as confirmSignupLinkService, br as confirmTotpEnrolmentService, bs as denyDeviceAuthService, bt as denyOAuth2AuthorizeService, bu as describeOAuth2AuthorizeRequestService, bv as disableMfaService, bw as finishPasskeyEnrollmentService, bx as finishPasskeyLoginService, by as getDeviceAuthInfoService, bz as getEnabledOAuthProviders, bA as getGoogleAccessToken, bB as getMachinePrincipal, bC as getOAuthProvider, bD as getRegisteredProviders, bE as invitationAcceptedEvent, bF as invitationCreatedEvent, bG as isOAuthProviderEnabled, bH as issueOneTimeTokenService, bI as listKeysService, bJ as listOAuth2GrantsService, bK as listPasskeysService, bL as loginService, bM as logoutService, bN as machineAuth, bO as markPasskeySecondFactorService, bP as mfaEnrolledForUser, bQ as mfaStatusService, bR as mfaVerifications, bS as oauthCallbackService, bT as oauthNativeService, bU as oauthStartService, bV as oauthUnlinkNotifyService, bW as oauthUnlinkedEvent, bX as optionalAuth, bY as passkeys, bZ as pollDeviceAuthService, b_ as regenerateRecoveryCodesService, b$ as registerAuthProfile, c0 as registerMachineVerifier, c1 as registerOAuthProvider, c2 as registerPublicKeyService, c3 as registerService, c4 as renamePasskeyService, c5 as requestPasswordResetService, c6 as requestSignupLinkService, c7 as requireEnabledProvider, c8 as requireMachineScope, c9 as resolveAuthenticatedUser, ca as revokeAllKeysService, cb as revokeAllOAuth2GrantsForUser, cc as revokeKeyService, cd as revokeOAuth2GrantService, ce as revokePasskeyService, cf as rotateKeyService, cg as runAuthProfile, ch as selectAuthProfile, ci as sendVerificationCodeService, cj as startDeviceAuthService, ck as startPasskeyEnrollmentService, cl as startPasskeyLoginService, cm as startStepUpService, cn as startTotpEnrolmentService, co as stepUpService, cp as sweepUnconfirmedMfaService, cq as verifyCodeService, cr as verifyOneTimeTokenService, cs as verifySecondFactor } from './machine-principals-CaEFq61K.js';
1
+ import { A as AuthInitOptions, z as OAuthProvider, N as NewUserPublicKey, x as VerificationPurpose, B as NewPasskey, E as Passkey, G as MfaVerification, H as MfaVerificationMethod, J as NewMfaChallenge, Q as MfaChallenge, u as PermissionCategory, W as UserPublicKey, X as AuthContext } from './machine-principals-CdEgxOB1.js';
2
+ export { Y as ApproveDeviceAuthParams, Z as AssertStepUpParams, _ as AuthDeletionCancelledPayload, $ as AuthDeletionCompletedPayload, a0 as AuthDeletionRequestedPayload, a1 as AuthDeviceRegisteredPayload, a2 as AuthLoginPayload, a3 as AuthPasswordResetPayload, a4 as AuthProfileOutcome, a5 as AuthProfileVerifier, a6 as AuthProviderSchema, a7 as AuthRegisterPayload, a8 as BearerOutcome, a9 as BearerRefusal, aa as ChangePasswordParams, ab as CompletePasswordResetParams, ac as CompleteSignupParams, ad as ConfirmPasswordResetParams, d as ConfirmPasswordResetResult, ae as ConfirmSignupLinkParams, C as ConfirmSignupLinkResult, af as ConfirmTotpParams, g as ConfirmTotpResult, ag as DeferredLoginEvent, ah as DenyDeviceAuthParams, ai as DeviceAuthApprovedResult, aj as DeviceAuthInfoParams, D as DeviceAuthInfoResult, ak as DeviceAuthPendingResult, al as DeviceAuthPollResponseSchema, am as DeviceNameSchema, an as DeviceRegistrationChannel, ao as DisableSessionBindingParams, ap as EmailSchema, aq as FingerprintSchema, ar as FinishPasskeyEnrollmentParams, F as FinishPasskeyEnrollmentResult, as as FinishPasskeyLoginParams, at as FinishSessionRenewParams, au as InvitationAcceptedPayload, av as InvitationCreatedPayload, I as IssueOneTimeTokenResult, aw as KEY_FINGERPRINT_PREFIX_LENGTH, ax as KeyIdSchema, K as KeySummary, ay as LoginParams, L as LoginResult, az as LogoutParams, aA as MFA_CHALLENGE_ATTEMPT_LIMIT, aB as MFA_CHALLENGE_CHANNELS, aC as MFA_VERIFICATION_METHODS, aD as MachinePrincipal, aE as MachineVerifierRegistration, aF as MarkPasskeyParams, aG as MfaChallengeChannel, aH as MfaChallengeHandle, M as MfaStatus, aI as NativeVerifyOptions, aJ as NewMfaVerification, aK as NormalizedIdentity, q as OAuth2AuthorizationCodeIssued, aL as OAuth2AuthorizeParams, p as OAuth2ConsentView, r as OAuth2GrantSummary, aM as OAuth2ScopeDescription, aN as OAuthCallbackParams, aO as OAuthCallbackResult, aP as OAuthCodeExchangeOptions, aQ as OAuthNativeParams, n as OAuthNativeResult, aR as OAuthStartParams, O as OAuthStartResult, aS as OAuthTokens, aT as OAuthUnlinkedPayload, aU as OpenStepUpChallengeParams, aV as PASSKEY_DEVICE_TYPES, aW as PASSKEY_LABEL_MAX_LENGTH, aX as PasskeyDeviceType, f as PasskeySummary, aY as PasswordSchema, aZ as PhoneSchema, a_ as PlatformSchema, a$ as PollDeviceAuthParams, b0 as PollDeviceAuthResult, b1 as PublicKeySchema, b2 as RecentAuthenticationParams, b3 as RegisterParams, b4 as RegisterPublicKeyParams, b5 as RegisterPublicKeyResult, a as RegisterResult, b6 as RenamePasskeyParams, b7 as RequestPasswordResetParams, c as RequestPasswordResetResult, b8 as RequestSignupLinkParams, b as RequestSignupLinkResult, b9 as RevokeAllKeysParams, i as RevokeAllKeysResult, ba as RevokeKeyParams, bb as RevokePasskeyParams, bc as RotateKeyParams, h as RotateKeyResult, bd as SendVerificationCodeParams, S as SendVerificationCodeResult, be as SessionBindingParams, j as SessionBindingResult, bf as StartDeviceAuthParams, e as StartDeviceAuthResult, bg as StartPasskeyEnrollmentParams, bh as StartSessionRenewParams, bi as StepUpParams, bj as TargetTypeSchema, T as TotpEnrolmentResult, bk as UnlinkNotification, bl as UnlinkNotifyRejection, bm as UnlinkNotifyRequest, bn as UnlinkNotifyResult, bo as UserCodeSchema, v as VERIFICATION_PURPOSES, w as VERIFICATION_TARGET_TYPES, bp as VerificationPurposeSchema, y as VerificationTargetType, bq as VerifyCodeParams, br as VerifyCodeResult, bs as VerifyMfaChallengeParams, V as VerifyMfaChallengeResult, bt as admitBearerKey, bu as approveDeviceAuthService, bv as approveOAuth2AuthorizeService, bw as assertNotLastRecoveryCredential, bx as assertRecentAuthentication, by as assertStepUp, bz as attemptSecondFactor, bA as authDeletionCancelledEvent, bB as authDeletionCompletedEvent, bC as authDeletionRequestedEvent, bD as authDeviceRegisteredEvent, bE as authLoginEvent, bF as authPasswordResetEvent, bG as authRegisterEvent, m as authRouter, bH as authenticate, bI as bearerAuthContext, bJ as buildOAuthErrorUrl, bK as carryStepUpVerification, bL as changePasswordService, bM as completePasswordResetService, bN as completeSignupService, bO as confirmPasswordResetService, bP as confirmSignupLinkService, bQ as confirmTotpEnrolmentService, bR as denyDeviceAuthService, bS as denyOAuth2AuthorizeService, bT as describeOAuth2AuthorizeRequestService, bU as disableMfaService, bV as disableSessionBindingService, bW as enableSessionBindingService, bX as finishPasskeyEnrollmentService, bY as finishPasskeyLoginService, bZ as finishSessionRenewService, b_ as getDeviceAuthInfoService, b$ as getEnabledOAuthProviders, c0 as getGoogleAccessToken, c1 as getMachinePrincipal, c2 as getOAuthProvider, c3 as getRegisteredProviders, c4 as getSessionBindingService, c5 as invitationAcceptedEvent, c6 as invitationCreatedEvent, c7 as isOAuthProviderEnabled, c8 as issueOneTimeTokenService, c9 as keySessionBindingService, ca as listKeysService, cb as listOAuth2GrantsService, cc as listPasskeysService, cd as loginService, ce as logoutService, cf as machineAuth, cg as markPasskeySecondFactorService, ch as mfaChallenges, ci as mfaEnrolledForUser, cj as mfaStatusService, ck as mfaVerifications, cl as oauthCallbackService, cm as oauthNativeService, cn as oauthStartService, co as oauthUnlinkNotifyService, cp as oauthUnlinkedEvent, cq as openStepUpChallengeService, cr as optionalAuth, cs as passkeys, ct as pollDeviceAuthService, cu as regenerateRecoveryCodesService, cv as registerAuthProfile, cw as registerMachineVerifier, cx as registerOAuthProvider, cy as registerPublicKeyService, cz as registerService, cA as registeredBinding, cB as renamePasskeyService, cC as requestPasswordResetService, cD as requestSignupLinkService, cE as requireEnabledProvider, cF as requireMachineScope, cG as resolveAuthenticatedUser, cH as resumeStepUpChallengeService, cI as revokeAllKeysService, cJ as revokeAllOAuth2GrantsForUser, cK as revokeKeyService, cL as revokeOAuth2GrantService, cM as revokePasskeyService, cN as rotateKeyService, cO as runAuthProfile, cP as selectAuthProfile, cQ as sendVerificationCodeService, cR as startDeviceAuthService, cS as startMfaChallengeAssertionService, cT as startPasskeyEnrollmentService, cU as startPasskeyLoginService, cV as startSessionBindingDisableService, cW as startSessionRenewService, cX as startStepUpService, cY as startTotpEnrolmentService, cZ as stepUpService, c_ as sweepMfaChallengesService, c$ as sweepUnconfirmedMfaService, d0 as userPublicKeys, d1 as verifyCodeService, d2 as verifyMfaChallengeService, d3 as verifyOneTimeTokenService, d4 as verifySecondFactor } from './machine-principals-CdEgxOB1.js';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
- import { j as SocialProvider, K as KeyAlgorithmType, d as InvitationStatus, c as AccountDeletionRequestedBy, i as PurgeStrategy } from './types-DYyhze28.js';
5
- export { A as ACCOUNT_DELETION_REQUESTED_BY, a as ACCOUNT_DELETION_REQUEST_STATUSES, b as AccountDeletionRequestStatus, I as INVITATION_STATUSES, e as KEY_ALGORITHM, f as KEY_DEVICE_NAME_MAX_LENGTH, g as KEY_PLATFORM, h as KeyPlatformType, P as PURGE_STRATEGIES, S as SOCIAL_PROVIDERS, U as USER_STATUSES, k as UserStatus } from './types-DYyhze28.js';
4
+ import { l as SocialProvider, K as KeyAlgorithmType, d as InvitationStatus, c as AccountDeletionRequestedBy, i as PurgeStrategy } from './types-CTdoTOxM.js';
5
+ export { A as ACCOUNT_DELETION_REQUESTED_BY, a as ACCOUNT_DELETION_REQUEST_STATUSES, b as AccountDeletionRequestStatus, I as INVITATION_STATUSES, e as KEY_ALGORITHM, f as KEY_DEVICE_NAME_MAX_LENGTH, g as KEY_PLATFORM, h as KeyPlatformType, P as PURGE_STRATEGIES, j as SESSION_BINDINGS, k as SOCIAL_PROVIDERS, S as SessionBindingType, U as USER_STATUSES, m as UserStatus } from './types-CTdoTOxM.js';
6
6
  import { UserProfile as UserProfile$1, ProfileInfo } from '@spfn/auth';
7
7
  import { BaseRepository } from '@spfn/core/db';
8
8
  import { c as ClientIdentity } from './wire-version-CtzMKvBB.js';
@@ -10,7 +10,7 @@ import { Context } from 'hono';
10
10
  import * as _spfn_core_route from '@spfn/core/route';
11
11
  import { NamedMiddleware } from '@spfn/core/route';
12
12
  export { KeyPair, generateClientToken, generateKeyPair, generateKeyPairES256, generateKeyPairRS256, getKeySize, shouldRotateKey } from './crypto.js';
13
- export { S as SessionData, g as getSessionInfo, i as isSafeReturnPath, s as sealSession, a as shouldRefreshSession, u as unsealSession } from './session-Dfwu5g2W.js';
13
+ export { S as SessionData, g as getSessionInfo, i as isSafeReturnPath, s as sealSession, a as shouldRefreshSession, u as unsealSession } from './session-BbhAGZtA.js';
14
14
  import { JWTPayload } from 'jose';
15
15
  import { SSETokenManager, SSETokenStore } from '@spfn/core/event/sse';
16
16
  import * as _spfn_core_logger from '@spfn/core/logger';
@@ -129,6 +129,16 @@ declare const COOKIE_NAMES: {
129
129
  readonly SESSION_KEY_ID: string;
130
130
  /** Pending OAuth session (privateKey, keyId, algorithm) - temporary during OAuth flow */
131
131
  readonly OAUTH_PENDING: string;
132
+ /**
133
+ * Pending second-factor session (privateKey, keyId, challengeHash) (#95)
134
+ *
135
+ * Its own name and its own audience, separate from OAUTH_PENDING. The two
136
+ * coexist: a person who starts a social login in one tab while a password
137
+ * step-up is outstanding in another has both flows live, and one name would
138
+ * mean the second overwrote the first — sealing a session with a private key
139
+ * that does not match the key being activated.
140
+ */
141
+ readonly MFA_PENDING: string;
132
142
  /** OAuth CSRF nonce — double-submit against the (encrypted) state.nonce at callback */
133
143
  readonly OAUTH_CSRF: string;
134
144
  /** Password-setup session for verified-email signup — temporary, single-purpose */
@@ -341,6 +351,27 @@ declare function getCsrfMode(): CsrfMode;
341
351
  * Get the paths exempted from the CSRF check (exact match, backend route paths)
342
352
  */
343
353
  declare function getCsrfExemptPaths(): string[];
354
+ /**
355
+ * How long a key bound to a passkey lives, in milliseconds.
356
+ *
357
+ * A positive override is honoured; anything else falls back to the policy
358
+ * constant. Nothing refuses boot over it — unlike the passkey relying party, a
359
+ * nonsensical value here does not make every ceremony fail, it just means the
360
+ * default applies.
361
+ */
362
+ declare function getBoundKeyTtlMs(): number;
363
+ /** How long past expiry a bound key may still be renewed, in milliseconds. */
364
+ declare function getBoundKeyRenewGraceMs(): number;
365
+ /** How close two sightings from two addresses must be to count as concurrent. */
366
+ declare function getConcurrentUseWindowMs(): number;
367
+ /**
368
+ * The page a bound session whose key expired is sent to.
369
+ *
370
+ * `RequireAuth` redirects here instead of to the sign-in page; the app renders a
371
+ * client component there that calls `renewSession(api)` and returns the person to
372
+ * where they were.
373
+ */
374
+ declare function getSessionRenewPath(): string;
344
375
  /**
345
376
  * The relying party this deployment presents to authenticators, resolved.
346
377
  *
@@ -405,6 +436,15 @@ interface MfaConfig {
405
436
  issuer: string;
406
437
  /** How long a device's step-up stays good for a sensitive change. */
407
438
  stepUpWindowMs: number;
439
+ /**
440
+ * How long a new-device step-up challenge stays spendable.
441
+ *
442
+ * The window a person has to reach for their authenticator, and the window
443
+ * an attacker who has the password has to get past the second factor. Ten
444
+ * minutes is the same number the OAuth pending cookie and the link flows
445
+ * use, and the proxy's pending cookie is sealed for exactly this long.
446
+ */
447
+ challengeTtlMs: number;
408
448
  }
409
449
  /**
410
450
  * Resolve the second-factor configuration.
@@ -678,6 +718,36 @@ declare const users: drizzle_orm_pg_core.PgTableWithColumns<{
678
718
  identity: undefined;
679
719
  generated: undefined;
680
720
  }>;
721
+ sessionBinding: drizzle_orm_pg_core.PgBuildColumn<"users", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["none", "passkey"] & [string, ...string[]]>>>, {
722
+ name: string;
723
+ tableName: "users";
724
+ dataType: "string enum";
725
+ data: "none" | "passkey";
726
+ driverParam: string;
727
+ notNull: true;
728
+ hasDefault: true;
729
+ isPrimaryKey: false;
730
+ isAutoincrement: false;
731
+ hasRuntimeDefault: false;
732
+ enumValues: ["none", "passkey"] & [string, ...string[]];
733
+ identity: undefined;
734
+ generated: undefined;
735
+ }>;
736
+ sessionBindingChangedAt: drizzle_orm_pg_core.PgBuildColumn<"users", drizzle_orm_pg_core.PgTimestampBuilder, {
737
+ name: string;
738
+ tableName: "users";
739
+ dataType: "object date";
740
+ data: Date;
741
+ driverParam: string;
742
+ notNull: false;
743
+ hasDefault: false;
744
+ isPrimaryKey: false;
745
+ isAutoincrement: false;
746
+ hasRuntimeDefault: false;
747
+ enumValues: undefined;
748
+ identity: undefined;
749
+ generated: undefined;
750
+ }>;
681
751
  lastLoginAt: drizzle_orm_pg_core.PgBuildColumn<"users", drizzle_orm_pg_core.PgTimestampBuilder, {
682
752
  name: string;
683
753
  tableName: "users";
@@ -725,6 +795,8 @@ declare function getUserByIdService(userId: number): Promise<{
725
795
  emailVerifiedAt: Date | null;
726
796
  phoneVerifiedAt: Date | null;
727
797
  keyEpoch: number;
798
+ sessionBinding: "none" | "passkey";
799
+ sessionBindingChangedAt: Date | null;
728
800
  lastLoginAt: Date | null;
729
801
  }>;
730
802
  /**
@@ -747,6 +819,8 @@ declare function getUserByEmailService(email: string): Promise<{
747
819
  emailVerifiedAt: Date | null;
748
820
  phoneVerifiedAt: Date | null;
749
821
  keyEpoch: number;
822
+ sessionBinding: "none" | "passkey";
823
+ sessionBindingChangedAt: Date | null;
750
824
  lastLoginAt: Date | null;
751
825
  }>;
752
826
  /**
@@ -769,6 +843,8 @@ declare function getUserByPhoneService(phone: string): Promise<{
769
843
  emailVerifiedAt: Date | null;
770
844
  phoneVerifiedAt: Date | null;
771
845
  keyEpoch: number;
846
+ sessionBinding: "none" | "passkey";
847
+ sessionBindingChangedAt: Date | null;
772
848
  lastLoginAt: Date | null;
773
849
  }>;
774
850
  /**
@@ -810,6 +886,8 @@ declare function updateUsernameService(userId: string | number | bigint, usernam
810
886
  emailVerifiedAt: Date | null;
811
887
  phoneVerifiedAt: Date | null;
812
888
  keyEpoch: number;
889
+ sessionBinding: "none" | "passkey";
890
+ sessionBindingChangedAt: Date | null;
813
891
  lastLoginAt: Date | null;
814
892
  }>;
815
893
 
@@ -3158,326 +3236,6 @@ declare const userProfiles: drizzle_orm_pg_core.PgTableWithColumns<{
3158
3236
  type UserProfile = typeof userProfiles.$inferSelect;
3159
3237
  type NewUserProfile = typeof userProfiles.$inferInsert;
3160
3238
 
3161
- /**
3162
- * @spfn/auth - User Public Keys Entity
3163
- *
3164
- * Stores client-generated public keys for JWT verification
3165
- * Supports key rotation and multi-key management per user
3166
- */
3167
- /**
3168
- * User Public Keys Table
3169
- * Each user can have multiple public keys (for rotation)
3170
- */
3171
- declare const userPublicKeys: drizzle_orm_pg_core.PgTableWithColumns<{
3172
- name: "user_public_keys";
3173
- schema: string;
3174
- columns: {
3175
- id: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetIsPrimaryKey<drizzle_orm_pg_core.PgBigSerial53Builder>, {
3176
- name: string;
3177
- tableName: "user_public_keys";
3178
- dataType: "number int53";
3179
- data: number;
3180
- driverParam: number;
3181
- notNull: true;
3182
- hasDefault: true;
3183
- isPrimaryKey: false;
3184
- isAutoincrement: false;
3185
- hasRuntimeDefault: false;
3186
- enumValues: undefined;
3187
- identity: undefined;
3188
- generated: undefined;
3189
- }>;
3190
- userId: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBigInt53Builder>, {
3191
- name: string;
3192
- tableName: "user_public_keys";
3193
- dataType: "number int53";
3194
- data: number;
3195
- driverParam: string | number;
3196
- notNull: true;
3197
- hasDefault: false;
3198
- isPrimaryKey: false;
3199
- isAutoincrement: false;
3200
- hasRuntimeDefault: false;
3201
- enumValues: undefined;
3202
- identity: undefined;
3203
- generated: undefined;
3204
- }>;
3205
- keyId: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3206
- name: string;
3207
- tableName: "user_public_keys";
3208
- dataType: "string";
3209
- data: string;
3210
- driverParam: string;
3211
- notNull: true;
3212
- hasDefault: false;
3213
- isPrimaryKey: false;
3214
- isAutoincrement: false;
3215
- hasRuntimeDefault: false;
3216
- enumValues: undefined;
3217
- identity: undefined;
3218
- generated: undefined;
3219
- }>;
3220
- publicKey: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3221
- name: string;
3222
- tableName: "user_public_keys";
3223
- dataType: "string";
3224
- data: string;
3225
- driverParam: string;
3226
- notNull: true;
3227
- hasDefault: false;
3228
- isPrimaryKey: false;
3229
- isAutoincrement: false;
3230
- hasRuntimeDefault: false;
3231
- enumValues: undefined;
3232
- identity: undefined;
3233
- generated: undefined;
3234
- }>;
3235
- 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[]]>>>, {
3236
- name: string;
3237
- tableName: "user_public_keys";
3238
- dataType: "string enum";
3239
- data: "ES256" | "RS256";
3240
- driverParam: string;
3241
- notNull: true;
3242
- hasDefault: true;
3243
- isPrimaryKey: false;
3244
- isAutoincrement: false;
3245
- hasRuntimeDefault: false;
3246
- enumValues: ["ES256", "RS256"] & [string, ...string[]];
3247
- identity: undefined;
3248
- generated: undefined;
3249
- }>;
3250
- fingerprint: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>>, {
3251
- name: string;
3252
- tableName: "user_public_keys";
3253
- dataType: "string";
3254
- data: string;
3255
- driverParam: string;
3256
- notNull: true;
3257
- hasDefault: false;
3258
- isPrimaryKey: false;
3259
- isAutoincrement: false;
3260
- hasRuntimeDefault: false;
3261
- enumValues: undefined;
3262
- identity: undefined;
3263
- generated: undefined;
3264
- }>;
3265
- deviceName: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3266
- name: string;
3267
- tableName: "user_public_keys";
3268
- dataType: "string";
3269
- data: string;
3270
- driverParam: string;
3271
- notNull: false;
3272
- hasDefault: false;
3273
- isPrimaryKey: false;
3274
- isAutoincrement: false;
3275
- hasRuntimeDefault: false;
3276
- enumValues: undefined;
3277
- identity: undefined;
3278
- generated: undefined;
3279
- }>;
3280
- platform: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<["ios", "android", "web", "desktop"] & [string, ...string[]]>, {
3281
- name: string;
3282
- tableName: "user_public_keys";
3283
- dataType: "string enum";
3284
- data: "ios" | "android" | "web" | "desktop";
3285
- driverParam: string;
3286
- notNull: false;
3287
- hasDefault: false;
3288
- isPrimaryKey: false;
3289
- isAutoincrement: false;
3290
- hasRuntimeDefault: false;
3291
- enumValues: ["ios", "android", "web", "desktop"] & [string, ...string[]];
3292
- identity: undefined;
3293
- generated: undefined;
3294
- }>;
3295
- registeredIp: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3296
- name: string;
3297
- tableName: "user_public_keys";
3298
- dataType: "string";
3299
- data: string;
3300
- driverParam: string;
3301
- notNull: false;
3302
- hasDefault: false;
3303
- isPrimaryKey: false;
3304
- isAutoincrement: false;
3305
- hasRuntimeDefault: false;
3306
- enumValues: undefined;
3307
- identity: undefined;
3308
- generated: undefined;
3309
- }>;
3310
- registeredUserAgent: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3311
- name: string;
3312
- tableName: "user_public_keys";
3313
- dataType: "string";
3314
- data: string;
3315
- driverParam: string;
3316
- notNull: false;
3317
- hasDefault: false;
3318
- isPrimaryKey: false;
3319
- isAutoincrement: false;
3320
- hasRuntimeDefault: false;
3321
- enumValues: undefined;
3322
- identity: undefined;
3323
- generated: undefined;
3324
- }>;
3325
- clientKind: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<["web", "ios", "android"] & [string, ...string[]]>, {
3326
- name: string;
3327
- tableName: "user_public_keys";
3328
- dataType: "string enum";
3329
- data: "ios" | "android" | "web";
3330
- driverParam: string;
3331
- notNull: false;
3332
- hasDefault: false;
3333
- isPrimaryKey: false;
3334
- isAutoincrement: false;
3335
- hasRuntimeDefault: false;
3336
- enumValues: ["web", "ios", "android"] & [string, ...string[]];
3337
- identity: undefined;
3338
- generated: undefined;
3339
- }>;
3340
- clientVersion: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3341
- name: string;
3342
- tableName: "user_public_keys";
3343
- dataType: "string";
3344
- data: string;
3345
- driverParam: string;
3346
- notNull: false;
3347
- hasDefault: false;
3348
- isPrimaryKey: false;
3349
- isAutoincrement: false;
3350
- hasRuntimeDefault: false;
3351
- enumValues: undefined;
3352
- identity: undefined;
3353
- generated: undefined;
3354
- }>;
3355
- clientContractVersion: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3356
- name: string;
3357
- tableName: "user_public_keys";
3358
- dataType: "string";
3359
- data: string;
3360
- driverParam: string;
3361
- notNull: false;
3362
- hasDefault: false;
3363
- isPrimaryKey: false;
3364
- isAutoincrement: false;
3365
- hasRuntimeDefault: false;
3366
- enumValues: undefined;
3367
- identity: undefined;
3368
- generated: undefined;
3369
- }>;
3370
- clientSeenAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3371
- name: string;
3372
- tableName: "user_public_keys";
3373
- dataType: "object date";
3374
- data: Date;
3375
- driverParam: string;
3376
- notNull: false;
3377
- hasDefault: false;
3378
- isPrimaryKey: false;
3379
- isAutoincrement: false;
3380
- hasRuntimeDefault: false;
3381
- enumValues: undefined;
3382
- identity: undefined;
3383
- generated: undefined;
3384
- }>;
3385
- isActive: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgBooleanBuilder>>, {
3386
- name: string;
3387
- tableName: "user_public_keys";
3388
- dataType: "boolean";
3389
- data: boolean;
3390
- driverParam: boolean;
3391
- notNull: true;
3392
- hasDefault: true;
3393
- isPrimaryKey: false;
3394
- isAutoincrement: false;
3395
- hasRuntimeDefault: false;
3396
- enumValues: undefined;
3397
- identity: undefined;
3398
- generated: undefined;
3399
- }>;
3400
- createdAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.SetHasDefault<drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTimestampBuilder>>, {
3401
- name: string;
3402
- tableName: "user_public_keys";
3403
- dataType: "object date";
3404
- data: Date;
3405
- driverParam: string;
3406
- notNull: true;
3407
- hasDefault: true;
3408
- isPrimaryKey: false;
3409
- isAutoincrement: false;
3410
- hasRuntimeDefault: false;
3411
- enumValues: undefined;
3412
- identity: undefined;
3413
- generated: undefined;
3414
- }>;
3415
- lastUsedAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3416
- name: string;
3417
- tableName: "user_public_keys";
3418
- dataType: "object date";
3419
- data: Date;
3420
- driverParam: string;
3421
- notNull: false;
3422
- hasDefault: false;
3423
- isPrimaryKey: false;
3424
- isAutoincrement: false;
3425
- hasRuntimeDefault: false;
3426
- enumValues: undefined;
3427
- identity: undefined;
3428
- generated: undefined;
3429
- }>;
3430
- expiresAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3431
- name: string;
3432
- tableName: "user_public_keys";
3433
- dataType: "object date";
3434
- data: Date;
3435
- driverParam: string;
3436
- notNull: false;
3437
- hasDefault: false;
3438
- isPrimaryKey: false;
3439
- isAutoincrement: false;
3440
- hasRuntimeDefault: false;
3441
- enumValues: undefined;
3442
- identity: undefined;
3443
- generated: undefined;
3444
- }>;
3445
- revokedAt: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTimestampBuilder, {
3446
- name: string;
3447
- tableName: "user_public_keys";
3448
- dataType: "object date";
3449
- data: Date;
3450
- driverParam: string;
3451
- notNull: false;
3452
- hasDefault: false;
3453
- isPrimaryKey: false;
3454
- isAutoincrement: false;
3455
- hasRuntimeDefault: false;
3456
- enumValues: undefined;
3457
- identity: undefined;
3458
- generated: undefined;
3459
- }>;
3460
- revokedReason: drizzle_orm_pg_core.PgBuildColumn<"user_public_keys", drizzle_orm_pg_core.PgTextBuilder<[string, ...string[]]>, {
3461
- name: string;
3462
- tableName: "user_public_keys";
3463
- dataType: "string";
3464
- data: string;
3465
- driverParam: string;
3466
- notNull: false;
3467
- hasDefault: false;
3468
- isPrimaryKey: false;
3469
- isAutoincrement: false;
3470
- hasRuntimeDefault: false;
3471
- enumValues: undefined;
3472
- identity: undefined;
3473
- generated: undefined;
3474
- }>;
3475
- };
3476
- dialect: "pg";
3477
- }>;
3478
- type UserPublicKey = typeof userPublicKeys.$inferSelect;
3479
- type NewUserPublicKey = typeof userPublicKeys.$inferInsert;
3480
-
3481
3239
  /**
3482
3240
  * @spfn/auth - User Social Accounts Entity
3483
3241
  *
@@ -4429,8 +4187,15 @@ type NewKeyRevokeAllToken = typeof keyRevokeAllTokens.$inferInsert;
4429
4187
  * direction that matters most — an assertion collected as a second factor is
4430
4188
  * not a sign-in, and `passkeys/login/verify` refuses it for exactly the reason
4431
4189
  * the column exists.
4190
+ *
4191
+ * `'renewal'` is the fourth: the assertion that renews a bound session key, and
4192
+ * the one that lets an account leave `session_binding: 'passkey'`. It is its own
4193
+ * kind rather than a reused `'authentication'` because that kind's challenge is
4194
+ * minted for nobody in particular — a discoverable sign-in names no account —
4195
+ * while a renewal challenge is bound to the account it was issued for, and
4196
+ * spending one where the other was expected would cross those two rules.
4432
4197
  */
4433
- declare const WEBAUTHN_CHALLENGE_KINDS: readonly ["registration", "authentication", "mfa"];
4198
+ declare const WEBAUTHN_CHALLENGE_KINDS: readonly ["registration", "authentication", "mfa", "renewal"];
4434
4199
  type WebAuthnChallengeKind = typeof WEBAUTHN_CHALLENGE_KINDS[number];
4435
4200
  declare const webauthnChallenges: drizzle_orm_pg_core.PgTableWithColumns<{
4436
4201
  name: "webauthn_challenges";
@@ -4466,18 +4231,18 @@ declare const webauthnChallenges: drizzle_orm_pg_core.PgTableWithColumns<{
4466
4231
  identity: undefined;
4467
4232
  generated: undefined;
4468
4233
  }>;
4469
- kind: drizzle_orm_pg_core.PgBuildColumn<"webauthn_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["registration", "authentication", "mfa"] & [string, ...string[]]>>, {
4234
+ kind: drizzle_orm_pg_core.PgBuildColumn<"webauthn_challenges", drizzle_orm_pg_core.SetNotNull<drizzle_orm_pg_core.PgTextBuilder<["registration", "authentication", "mfa", "renewal"] & [string, ...string[]]>>, {
4470
4235
  name: string;
4471
4236
  tableName: "webauthn_challenges";
4472
4237
  dataType: "string enum";
4473
- data: "registration" | "authentication" | "mfa";
4238
+ data: "registration" | "authentication" | "mfa" | "renewal";
4474
4239
  driverParam: string;
4475
4240
  notNull: true;
4476
4241
  hasDefault: false;
4477
4242
  isPrimaryKey: false;
4478
4243
  isAutoincrement: false;
4479
4244
  hasRuntimeDefault: false;
4480
- enumValues: ["registration", "authentication", "mfa"] & [string, ...string[]];
4245
+ enumValues: ["registration", "authentication", "mfa", "renewal"] & [string, ...string[]];
4481
4246
  identity: undefined;
4482
4247
  generated: undefined;
4483
4248
  }>;
@@ -6273,6 +6038,17 @@ type NewAuthMetadataEntity = typeof authMetadata.$inferInsert;
6273
6038
  * - 타입 안전성
6274
6039
  */
6275
6040
  declare class UsersRepository extends BaseRepository {
6041
+ /**
6042
+ * The account's current key generation.
6043
+ *
6044
+ * Read from the primary, and this is the one place that matters: the value
6045
+ * is stamped onto a second-factor challenge that must die with the
6046
+ * generation it was minted in, and a replica lagging behind a revoke-all
6047
+ * would stamp the generation that has just ended.
6048
+ *
6049
+ * Write primary 사용
6050
+ */
6051
+ currentKeyEpoch(id: number): Promise<number>;
6276
6052
  /**
6277
6053
  * ID로 사용자 조회
6278
6054
  * Read replica 사용
@@ -6294,6 +6070,8 @@ declare class UsersRepository extends BaseRepository {
6294
6070
  emailVerifiedAt: Date | null;
6295
6071
  phoneVerifiedAt: Date | null;
6296
6072
  keyEpoch: number;
6073
+ sessionBinding: "none" | "passkey";
6074
+ sessionBindingChangedAt: Date | null;
6297
6075
  lastLoginAt: Date | null;
6298
6076
  }>;
6299
6077
  /**
@@ -6320,6 +6098,8 @@ declare class UsersRepository extends BaseRepository {
6320
6098
  emailVerifiedAt: Date | null;
6321
6099
  phoneVerifiedAt: Date | null;
6322
6100
  keyEpoch: number;
6101
+ sessionBinding: "none" | "passkey";
6102
+ sessionBindingChangedAt: Date | null;
6323
6103
  lastLoginAt: Date | null;
6324
6104
  }>;
6325
6105
  /**
@@ -6357,6 +6137,8 @@ declare class UsersRepository extends BaseRepository {
6357
6137
  emailVerifiedAt: Date | null;
6358
6138
  phoneVerifiedAt: Date | null;
6359
6139
  keyEpoch: number;
6140
+ sessionBinding: "none" | "passkey";
6141
+ sessionBindingChangedAt: Date | null;
6360
6142
  lastLoginAt: Date | null;
6361
6143
  }>;
6362
6144
  /**
@@ -6390,6 +6172,8 @@ declare class UsersRepository extends BaseRepository {
6390
6172
  emailVerifiedAt: Date | null;
6391
6173
  phoneVerifiedAt: Date | null;
6392
6174
  keyEpoch: number;
6175
+ sessionBinding: "none" | "passkey";
6176
+ sessionBindingChangedAt: Date | null;
6393
6177
  lastLoginAt: Date | null;
6394
6178
  }>;
6395
6179
  /**
@@ -6413,6 +6197,8 @@ declare class UsersRepository extends BaseRepository {
6413
6197
  emailVerifiedAt: Date | null;
6414
6198
  phoneVerifiedAt: Date | null;
6415
6199
  keyEpoch: number;
6200
+ sessionBinding: "none" | "passkey";
6201
+ sessionBindingChangedAt: Date | null;
6416
6202
  lastLoginAt: Date | null;
6417
6203
  }>;
6418
6204
  /**
@@ -6436,6 +6222,8 @@ declare class UsersRepository extends BaseRepository {
6436
6222
  emailVerifiedAt: Date | null;
6437
6223
  phoneVerifiedAt: Date | null;
6438
6224
  keyEpoch: number;
6225
+ sessionBinding: "none" | "passkey";
6226
+ sessionBindingChangedAt: Date | null;
6439
6227
  lastLoginAt: Date | null;
6440
6228
  }>;
6441
6229
  /**
@@ -6459,6 +6247,8 @@ declare class UsersRepository extends BaseRepository {
6459
6247
  emailVerifiedAt: Date | null;
6460
6248
  phoneVerifiedAt: Date | null;
6461
6249
  keyEpoch: number;
6250
+ sessionBinding: "none" | "passkey";
6251
+ sessionBindingChangedAt: Date | null;
6462
6252
  lastLoginAt: Date | null;
6463
6253
  }>;
6464
6254
  /**
@@ -6482,6 +6272,8 @@ declare class UsersRepository extends BaseRepository {
6482
6272
  emailVerifiedAt: Date | null;
6483
6273
  phoneVerifiedAt: Date | null;
6484
6274
  keyEpoch: number;
6275
+ sessionBinding: "none" | "passkey";
6276
+ sessionBindingChangedAt: Date | null;
6485
6277
  lastLoginAt: Date | null;
6486
6278
  } | null>;
6487
6279
  /**
@@ -6508,6 +6300,8 @@ declare class UsersRepository extends BaseRepository {
6508
6300
  emailVerifiedAt: Date | null;
6509
6301
  phoneVerifiedAt: Date | null;
6510
6302
  keyEpoch: number;
6303
+ sessionBinding: "none" | "passkey";
6304
+ sessionBindingChangedAt: Date | null;
6511
6305
  lastLoginAt: Date | null;
6512
6306
  };
6513
6307
  role: {
@@ -6526,6 +6320,7 @@ declare class UsersRepository extends BaseRepository {
6526
6320
  status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
6527
6321
  publicId: string;
6528
6322
  passwordChangeRequired: boolean;
6323
+ sessionBinding: "none" | "passkey";
6529
6324
  id: number;
6530
6325
  username: string | null;
6531
6326
  passwordHash: string | null;
@@ -6535,6 +6330,7 @@ declare class UsersRepository extends BaseRepository {
6535
6330
  emailVerifiedAt: Date | null;
6536
6331
  phoneVerifiedAt: Date | null;
6537
6332
  keyEpoch: number;
6333
+ sessionBindingChangedAt: Date | null;
6538
6334
  lastLoginAt: Date | null;
6539
6335
  deletedAt: Date | null;
6540
6336
  deletedBy: string | null;
@@ -6605,6 +6401,8 @@ declare class UsersRepository extends BaseRepository {
6605
6401
  emailVerifiedAt: Date | null;
6606
6402
  phoneVerifiedAt: Date | null;
6607
6403
  keyEpoch: number;
6404
+ sessionBinding: "none" | "passkey";
6405
+ sessionBindingChangedAt: Date | null;
6608
6406
  lastLoginAt: Date | null;
6609
6407
  }>;
6610
6408
  /**
@@ -6633,6 +6431,8 @@ declare class UsersRepository extends BaseRepository {
6633
6431
  emailVerifiedAt: Date | null;
6634
6432
  phoneVerifiedAt: Date | null;
6635
6433
  keyEpoch: number;
6434
+ sessionBinding: "none" | "passkey";
6435
+ sessionBindingChangedAt: Date | null;
6636
6436
  lastLoginAt: Date | null;
6637
6437
  }>;
6638
6438
  /**
@@ -6656,6 +6456,8 @@ declare class UsersRepository extends BaseRepository {
6656
6456
  emailVerifiedAt: Date | null;
6657
6457
  phoneVerifiedAt: Date | null;
6658
6458
  keyEpoch: number;
6459
+ sessionBinding: "none" | "passkey";
6460
+ sessionBindingChangedAt: Date | null;
6659
6461
  lastLoginAt: Date | null;
6660
6462
  }>;
6661
6463
  /**
@@ -6679,6 +6481,8 @@ declare class UsersRepository extends BaseRepository {
6679
6481
  emailVerifiedAt: Date | null;
6680
6482
  phoneVerifiedAt: Date | null;
6681
6483
  keyEpoch: number;
6484
+ sessionBinding: "none" | "passkey";
6485
+ sessionBindingChangedAt: Date | null;
6682
6486
  lastLoginAt: Date | null;
6683
6487
  }>;
6684
6488
  /**
@@ -6691,6 +6495,7 @@ declare class UsersRepository extends BaseRepository {
6691
6495
  status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
6692
6496
  publicId: string;
6693
6497
  passwordChangeRequired: boolean;
6498
+ sessionBinding: "none" | "passkey";
6694
6499
  id: number;
6695
6500
  username: string | null;
6696
6501
  passwordHash: string | null;
@@ -6700,6 +6505,7 @@ declare class UsersRepository extends BaseRepository {
6700
6505
  emailVerifiedAt: Date | null;
6701
6506
  phoneVerifiedAt: Date | null;
6702
6507
  keyEpoch: number;
6508
+ sessionBindingChangedAt: Date | null;
6703
6509
  lastLoginAt: Date | null;
6704
6510
  deletedAt: Date | null;
6705
6511
  deletedBy: string | null;
@@ -6803,11 +6609,17 @@ declare class KeysRepository extends BaseRepository {
6803
6609
  platform: "ios" | "android" | "web" | "desktop" | null;
6804
6610
  registeredIp: string | null;
6805
6611
  registeredUserAgent: string | null;
6612
+ registeredUaFamily: string | null;
6613
+ lastSeenIp: string | null;
6614
+ lastSeenAt: Date | null;
6615
+ concurrentUseAt: Date | null;
6616
+ binding: "none" | "passkey";
6806
6617
  clientKind: "ios" | "android" | "web" | null;
6807
6618
  clientVersion: string | null;
6808
6619
  clientContractVersion: string | null;
6809
6620
  clientSeenAt: Date | null;
6810
6621
  isActive: boolean;
6622
+ pendingMfaChallengeId: number | null;
6811
6623
  createdAt: Date;
6812
6624
  lastUsedAt: Date | null;
6813
6625
  expiresAt: Date | null;
@@ -6829,11 +6641,17 @@ declare class KeysRepository extends BaseRepository {
6829
6641
  platform: "ios" | "android" | "web" | "desktop" | null;
6830
6642
  registeredIp: string | null;
6831
6643
  registeredUserAgent: string | null;
6644
+ registeredUaFamily: string | null;
6645
+ lastSeenIp: string | null;
6646
+ lastSeenAt: Date | null;
6647
+ concurrentUseAt: Date | null;
6648
+ binding: "none" | "passkey";
6832
6649
  clientKind: "ios" | "android" | "web" | null;
6833
6650
  clientVersion: string | null;
6834
6651
  clientContractVersion: string | null;
6835
6652
  clientSeenAt: Date | null;
6836
6653
  isActive: boolean;
6654
+ pendingMfaChallengeId: number | null;
6837
6655
  createdAt: Date;
6838
6656
  lastUsedAt: Date | null;
6839
6657
  expiresAt: Date | null;
@@ -6855,11 +6673,17 @@ declare class KeysRepository extends BaseRepository {
6855
6673
  platform: "ios" | "android" | "web" | "desktop" | null;
6856
6674
  registeredIp: string | null;
6857
6675
  registeredUserAgent: string | null;
6676
+ registeredUaFamily: string | null;
6677
+ lastSeenIp: string | null;
6678
+ lastSeenAt: Date | null;
6679
+ concurrentUseAt: Date | null;
6680
+ binding: "none" | "passkey";
6858
6681
  clientKind: "ios" | "android" | "web" | null;
6859
6682
  clientVersion: string | null;
6860
6683
  clientContractVersion: string | null;
6861
6684
  clientSeenAt: Date | null;
6862
6685
  isActive: boolean;
6686
+ pendingMfaChallengeId: number | null;
6863
6687
  createdAt: Date;
6864
6688
  lastUsedAt: Date | null;
6865
6689
  expiresAt: Date | null;
@@ -6874,6 +6698,16 @@ declare class KeysRepository extends BaseRepository {
6874
6698
  *
6875
6699
  * `includeRevoked`는 이미 끊은 기기까지 보여준다 — "내가 언제 무엇을 끊었나"를
6876
6700
  * 확인하는 용도라, 폐기 시각과 사유를 함께 고른다.
6701
+ *
6702
+ * `lastSeenIp` is not selected, deliberately. The concurrent-use moment is
6703
+ * what the owner acts on; the trail of addresses behind it is stored PII the
6704
+ * account surface has no use for.
6705
+ *
6706
+ * A key still waiting on a second factor is excluded in **both** modes
6707
+ * (#95). It is not a device that can sign, so it does not belong in the
6708
+ * default list; and it is not a device the owner signed out either, so it
6709
+ * does not belong in the revoked one — where it would read exactly like a
6710
+ * revoked key with no revocation time, which is a different thing entirely.
6877
6711
  * Read replica 사용
6878
6712
  */
6879
6713
  listForUser(userId: number, includeRevoked?: boolean): Promise<{
@@ -6889,6 +6723,8 @@ declare class KeysRepository extends BaseRepository {
6889
6723
  revokedAt: Date | null;
6890
6724
  registeredIp: string | null;
6891
6725
  registeredUserAgent: string | null;
6726
+ binding: "none" | "passkey";
6727
+ concurrentUseAt: Date | null;
6892
6728
  }[]>;
6893
6729
  /**
6894
6730
  * 공개키 생성
@@ -6910,10 +6746,16 @@ declare class KeysRepository extends BaseRepository {
6910
6746
  lastUsedAt: Date | null;
6911
6747
  registeredIp: string | null;
6912
6748
  registeredUserAgent: string | null;
6749
+ registeredUaFamily: string | null;
6750
+ lastSeenIp: string | null;
6751
+ lastSeenAt: Date | null;
6752
+ concurrentUseAt: Date | null;
6753
+ binding: "none" | "passkey";
6913
6754
  clientKind: "ios" | "android" | "web" | null;
6914
6755
  clientVersion: string | null;
6915
6756
  clientContractVersion: string | null;
6916
6757
  clientSeenAt: Date | null;
6758
+ pendingMfaChallengeId: number | null;
6917
6759
  revokedReason: string | null;
6918
6760
  }>;
6919
6761
  /**
@@ -6937,11 +6779,17 @@ declare class KeysRepository extends BaseRepository {
6937
6779
  platform: "ios" | "android" | "web" | "desktop" | null;
6938
6780
  registeredIp: string | null;
6939
6781
  registeredUserAgent: string | null;
6782
+ registeredUaFamily: string | null;
6783
+ lastSeenIp: string | null;
6784
+ lastSeenAt: Date | null;
6785
+ concurrentUseAt: Date | null;
6786
+ binding: "none" | "passkey";
6940
6787
  clientKind: "ios" | "android" | "web" | null;
6941
6788
  clientVersion: string | null;
6942
6789
  clientContractVersion: string | null;
6943
6790
  clientSeenAt: Date | null;
6944
6791
  isActive: boolean;
6792
+ pendingMfaChallengeId: number | null;
6945
6793
  createdAt: Date;
6946
6794
  lastUsedAt: Date | null;
6947
6795
  expiresAt: Date | null;
@@ -6977,6 +6825,17 @@ declare class KeysRepository extends BaseRepository {
6977
6825
  * active keys still had its generation ended, and an outstanding
6978
6826
  * sign-out-everywhere link must die with it.
6979
6827
  *
6828
+ * The two second-factor CTEs are here for the same "no caller can forget"
6829
+ * reason (#95). A pending key is `is_active = false`, so the UPDATE below
6830
+ * does not touch it and neither would anything else: the owner who sees an
6831
+ * unexpected second-factor prompt and does exactly what the notice says —
6832
+ * change the password, sign out everywhere, open the revoke-all link, run a
6833
+ * reset — would otherwise leave the attacker's pending key and its live
6834
+ * challenge untouched. Deleting the key cascades onto its challenge; the
6835
+ * expiry beside it catches a challenge whose key is already gone. A pending
6836
+ * row and an active row are disjoint sets, so the two statements never
6837
+ * contend for the same row.
6838
+ *
6980
6839
  * Write primary 사용
6981
6840
  */
6982
6841
  private revokeActive;
@@ -7000,10 +6859,16 @@ declare class KeysRepository extends BaseRepository {
7000
6859
  lastUsedAt: Date | null;
7001
6860
  registeredIp: string | null;
7002
6861
  registeredUserAgent: string | null;
6862
+ registeredUaFamily: string | null;
6863
+ lastSeenIp: string | null;
6864
+ lastSeenAt: Date | null;
6865
+ concurrentUseAt: Date | null;
6866
+ binding: "none" | "passkey";
7003
6867
  clientKind: "ios" | "android" | "web" | null;
7004
6868
  clientVersion: string | null;
7005
6869
  clientContractVersion: string | null;
7006
6870
  clientSeenAt: Date | null;
6871
+ pendingMfaChallengeId: number | null;
7007
6872
  revokedReason: string | null;
7008
6873
  }>;
7009
6874
  /**
@@ -7029,11 +6894,17 @@ declare class KeysRepository extends BaseRepository {
7029
6894
  platform: "ios" | "android" | "web" | "desktop" | null;
7030
6895
  registeredIp: string | null;
7031
6896
  registeredUserAgent: string | null;
6897
+ registeredUaFamily: string | null;
6898
+ lastSeenIp: string | null;
6899
+ lastSeenAt: Date | null;
6900
+ concurrentUseAt: Date | null;
6901
+ binding: "none" | "passkey";
7032
6902
  clientKind: "ios" | "android" | "web" | null;
7033
6903
  clientVersion: string | null;
7034
6904
  clientContractVersion: string | null;
7035
6905
  clientSeenAt: Date | null;
7036
6906
  isActive: boolean;
6907
+ pendingMfaChallengeId: number | null;
7037
6908
  createdAt: Date;
7038
6909
  lastUsedAt: Date | null;
7039
6910
  expiresAt: Date | null;
@@ -7055,17 +6926,68 @@ declare class KeysRepository extends BaseRepository {
7055
6926
  platform: "ios" | "android" | "web" | "desktop" | null;
7056
6927
  registeredIp: string | null;
7057
6928
  registeredUserAgent: string | null;
6929
+ registeredUaFamily: string | null;
6930
+ lastSeenIp: string | null;
6931
+ lastSeenAt: Date | null;
6932
+ concurrentUseAt: Date | null;
6933
+ binding: "none" | "passkey";
6934
+ clientKind: "ios" | "android" | "web" | null;
6935
+ clientVersion: string | null;
6936
+ clientContractVersion: string | null;
6937
+ clientSeenAt: Date | null;
6938
+ isActive: boolean;
6939
+ pendingMfaChallengeId: number | null;
6940
+ createdAt: Date;
6941
+ lastUsedAt: Date | null;
6942
+ expiresAt: Date | null;
6943
+ revokedAt: Date | null;
6944
+ revokedReason: string | null;
6945
+ }>;
6946
+ /**
6947
+ * Bind one live key to the account's passkey, and give it the short life
6948
+ * that goes with it.
6949
+ *
6950
+ * Scoped by user and by `isActive`, like every other targeted update here, so
6951
+ * the answer is "this call bound something" rather than "a row exists".
6952
+ * Write primary 사용
6953
+ */
6954
+ bindByKeyIdAndUserId(keyId: string, userId: number, expiresAt: Date): Promise<{
6955
+ id: number;
6956
+ userId: number;
6957
+ keyId: string;
6958
+ publicKey: string;
6959
+ algorithm: "ES256" | "RS256";
6960
+ fingerprint: string;
6961
+ deviceName: string | null;
6962
+ platform: "ios" | "android" | "web" | "desktop" | null;
6963
+ registeredIp: string | null;
6964
+ registeredUserAgent: string | null;
6965
+ registeredUaFamily: string | null;
6966
+ lastSeenIp: string | null;
6967
+ lastSeenAt: Date | null;
6968
+ concurrentUseAt: Date | null;
6969
+ binding: "none" | "passkey";
7058
6970
  clientKind: "ios" | "android" | "web" | null;
7059
6971
  clientVersion: string | null;
7060
6972
  clientContractVersion: string | null;
7061
6973
  clientSeenAt: Date | null;
7062
6974
  isActive: boolean;
6975
+ pendingMfaChallengeId: number | null;
7063
6976
  createdAt: Date;
7064
6977
  lastUsedAt: Date | null;
7065
6978
  expiresAt: Date | null;
7066
6979
  revokedAt: Date | null;
7067
6980
  revokedReason: string | null;
7068
6981
  }>;
6982
+ /**
6983
+ * Return every one of a user's bound keys to an ordinary long-lived key.
6984
+ *
6985
+ * One statement, because turning the setting off has to leave no key behind:
6986
+ * a row still marked `'passkey'` would keep expiring in hours with nothing
6987
+ * left to renew it, and its owner has just said they do not want that.
6988
+ * Write primary 사용
6989
+ */
6990
+ unbindActiveByUserId(userId: number, expiresAt: Date): Promise<number>;
7069
6991
  /**
7070
6992
  * Key ID로 공개키 조회 — 활성 여부 무관 (clientProofV1 admission의 revocation 판정용)
7071
6993
  *
@@ -7084,11 +7006,17 @@ declare class KeysRepository extends BaseRepository {
7084
7006
  platform: "ios" | "android" | "web" | "desktop" | null;
7085
7007
  registeredIp: string | null;
7086
7008
  registeredUserAgent: string | null;
7009
+ registeredUaFamily: string | null;
7010
+ lastSeenIp: string | null;
7011
+ lastSeenAt: Date | null;
7012
+ concurrentUseAt: Date | null;
7013
+ binding: "none" | "passkey";
7087
7014
  clientKind: "ios" | "android" | "web" | null;
7088
7015
  clientVersion: string | null;
7089
7016
  clientContractVersion: string | null;
7090
7017
  clientSeenAt: Date | null;
7091
7018
  isActive: boolean;
7019
+ pendingMfaChallengeId: number | null;
7092
7020
  createdAt: Date;
7093
7021
  lastUsedAt: Date | null;
7094
7022
  expiresAt: Date | null;
@@ -7110,11 +7038,17 @@ declare class KeysRepository extends BaseRepository {
7110
7038
  platform: "ios" | "android" | "web" | "desktop" | null;
7111
7039
  registeredIp: string | null;
7112
7040
  registeredUserAgent: string | null;
7041
+ registeredUaFamily: string | null;
7042
+ lastSeenIp: string | null;
7043
+ lastSeenAt: Date | null;
7044
+ concurrentUseAt: Date | null;
7045
+ binding: "none" | "passkey";
7113
7046
  clientKind: "ios" | "android" | "web" | null;
7114
7047
  clientVersion: string | null;
7115
7048
  clientContractVersion: string | null;
7116
7049
  clientSeenAt: Date | null;
7117
7050
  isActive: boolean;
7051
+ pendingMfaChallengeId: number | null;
7118
7052
  createdAt: Date;
7119
7053
  lastUsedAt: Date | null;
7120
7054
  expiresAt: Date | null;
@@ -7140,8 +7074,28 @@ declare class KeysRepository extends BaseRepository {
7140
7074
  * `clientSeenAt` moves only when one of the three values differs from what is
7141
7075
  * stored, so it answers "since when has this device been on this release"
7142
7076
  * rather than "when was it last seen", which lastUsedAt already answers.
7077
+ *
7078
+ * `ip` is the client address this request resolved to, or null when none did.
7079
+ * It joins this statement rather than getting one of its own: the rule the
7080
+ * concurrent-use signal needs — write the address, and move
7081
+ * `concurrentUseAt` when it differs from the stored one inside the window —
7082
+ * is the rule already implemented here, and a second fire-and-forget write
7083
+ * beside it would double the per-request write on the authenticated path. It
7084
+ * is also why the comparison is a `CASE` over the stored value rather than a
7085
+ * read followed by a write: the row comes off the read replica, and a
7086
+ * replica-lagged address compared in application code both misses real
7087
+ * switches and invents ones that did not happen.
7088
+ */
7089
+ updateLastUsedById(id: number, identity?: ClientIdentity | null, ip?: string | null): Promise<void>;
7090
+ /**
7091
+ * The boundary a previous sighting has to be newer than to count as concurrent.
7092
+ *
7093
+ * Computed here rather than written as `now() - interval` so that the moment
7094
+ * the row is stamped with and the moment the window is measured from are the
7095
+ * same one — a statement that used the database clock for the boundary and
7096
+ * ours for the value would disagree with itself by the round trip.
7143
7097
  */
7144
- updateLastUsedById(id: number, identity?: ClientIdentity | null): Promise<void>;
7098
+ private concurrentUseSince;
7145
7099
  }
7146
7100
  declare const keysRepository: KeysRepository;
7147
7101
 
@@ -7791,6 +7745,108 @@ declare class MfaVerificationsRepository extends BaseRepository {
7791
7745
  }
7792
7746
  declare const mfaVerificationsRepository: MfaVerificationsRepository;
7793
7747
 
7748
+ /**
7749
+ * MFA Challenges Repository
7750
+ *
7751
+ * Data access for the row that stands between a registered-but-inactive key and
7752
+ * a session. Extends BaseRepository for transaction-context detection and
7753
+ * read/write splitting.
7754
+ *
7755
+ * Every lookup here is by `challenge_hash` and never by id. The secret is what
7756
+ * the caller holds; the hash is what this table has, so a row can only be found
7757
+ * by someone who already had the secret — a guess reaches no row at all, which
7758
+ * is what keeps a guess from touching another challenge's `attempts`.
7759
+ */
7760
+
7761
+ declare class MfaChallengesRepository extends BaseRepository {
7762
+ /** Park a fresh challenge. The caller has already written the pending key. */
7763
+ create(data: NewMfaChallenge): Promise<MfaChallenge>;
7764
+ /**
7765
+ * The challenge this secret names, whatever state it is in.
7766
+ *
7767
+ * Reads the primary rather than the replica: `verify` decides whether a key
7768
+ * becomes usable, and a replica lagging behind the 202 that created the row
7769
+ * would refuse a challenge the caller was handed a moment ago.
7770
+ */
7771
+ findByHash(challengeHash: string): Promise<MfaChallenge | null>;
7772
+ /**
7773
+ * The live challenge already outstanding for this key, if there is one.
7774
+ *
7775
+ * What makes a retried registration resume rather than collide: a native
7776
+ * client reusing its keyId, or an OAuth state replayed from the back button,
7777
+ * gets the challenge it was already given instead of a 409 about a key it
7778
+ * does own.
7779
+ */
7780
+ findLiveByKeyId(userId: number, keyId: string): Promise<MfaChallenge | null>;
7781
+ /**
7782
+ * Spend the challenge, or answer false because somebody else already did.
7783
+ *
7784
+ * A conditional UPDATE and not a read followed by a write: two verifies
7785
+ * arriving together would both pass a read, and a challenge is single use.
7786
+ * Every condition the caller checked is restated here, so the row that is
7787
+ * marked is a row that was still spendable at the moment it was marked.
7788
+ */
7789
+ markVerified(id: number, keyEpoch: number): Promise<boolean>;
7790
+ /**
7791
+ * Count a wrong proof, and say whether that was the last one allowed.
7792
+ *
7793
+ * The increment is on the row the presented secret actually hashed to, which
7794
+ * is the whole reason lookup is by hash: there is no id for a caller to name,
7795
+ * so no counter but their own can be moved.
7796
+ */
7797
+ countFailure(id: number): Promise<boolean>;
7798
+ /**
7799
+ * Point an inactive key at the challenge that gates it.
7800
+ *
7801
+ * A second statement rather than a column on the insert, because the
7802
+ * challenge references the key: the key row has to exist before the
7803
+ * challenge can, so the back-reference is written once both do. Both run
7804
+ * inside the registering route's transaction.
7805
+ */
7806
+ markKeyPending(keyId: string, challengeId: number): Promise<void>;
7807
+ /**
7808
+ * Give a live challenge a fresh secret, leaving everything else alone.
7809
+ *
7810
+ * What a retried registration gets. The row keeps its id, its expiry and its
7811
+ * spent attempts, so retrying is not a way to reset the counter or buy
7812
+ * another ten minutes; only the secret is new, because the old one was never
7813
+ * stored and cannot be handed out twice.
7814
+ */
7815
+ resecret(id: number, challengeHash: string): Promise<boolean>;
7816
+ /**
7817
+ * Delete the pending key a challenge was gating, which takes the challenge
7818
+ * with it through the cascade.
7819
+ *
7820
+ * Scoped to `pending_mfa_challenge_id`, so a key that has since been
7821
+ * activated is never deleted by a late failure or a sweep.
7822
+ */
7823
+ dropPendingKey(keyId: string, challengeId: number): Promise<void>;
7824
+ /**
7825
+ * Mark a key active and no longer pending — what a verified challenge buys.
7826
+ *
7827
+ * Conditioned on the challenge id, so this can only activate the key that
7828
+ * challenge was minted for.
7829
+ */
7830
+ activateKey(keyId: string, challengeId: number): Promise<void>;
7831
+ /**
7832
+ * Clear out challenges nobody finished, and the keys they were gating.
7833
+ *
7834
+ * Counted from the read rather than from either delete, because the two
7835
+ * overlap: deleting a pending key cascades onto its challenge, so the second
7836
+ * statement finds only the challenges whose key had already gone some other
7837
+ * way — and a count taken there would report a sweep that did nothing.
7838
+ *
7839
+ * A spent challenge is kept for the same span as an expired one, so a replay
7840
+ * inside the window meets "already verified" from a row rather than
7841
+ * "unknown" from an absence. The 401 is the same either way; the record is
7842
+ * not.
7843
+ *
7844
+ * @returns how many challenge rows are gone
7845
+ */
7846
+ sweepFinished(cutoff: Date): Promise<number>;
7847
+ }
7848
+ declare const mfaChallengesRepository: MfaChallengesRepository;
7849
+
7794
7850
  /**
7795
7851
  * MFA Enrolment Repository
7796
7852
  *
@@ -9088,9 +9144,9 @@ declare class AccountDeletionRequestsRepository extends BaseRepository {
9088
9144
  cancelledAt: Date | null;
9089
9145
  completedAt: Date | null;
9090
9146
  userPublicId: string;
9091
- requestedAt: Date;
9092
9147
  requestedBy: "admin" | "self";
9093
9148
  purgeStrategy: "anonymize" | "hard-delete" | null;
9149
+ requestedAt: Date;
9094
9150
  }>;
9095
9151
  /**
9096
9152
  * 요청 취소 (복구)
@@ -9661,6 +9717,31 @@ declare function verifyKeyFingerprint(publicKeyB64: string, expectedFingerprint:
9661
9717
  */
9662
9718
  declare function assertKeyMatchesAlgorithm(publicKeyB64: string, algorithm: KeyAlgorithmType): void;
9663
9719
 
9720
+ /**
9721
+ * Whether a key may sign a renewal — the one clause that differs from `authenticate`.
9722
+ *
9723
+ * Bound, and within its grace. Early renewal is allowed, because a browser that
9724
+ * asks a minute before its key runs out should not have to wait for the failure
9725
+ * first; late renewal is allowed for the grace window, so a laptop closed over a
9726
+ * long weekend comes back to one prompt instead of a sign-in. Past it there is
9727
+ * nothing to continue, and an unbound key was never in this ceremony at all.
9728
+ */
9729
+ declare function admitsForRenewal(key: UserPublicKey): boolean;
9730
+ /**
9731
+ * Renewal authentication middleware
9732
+ *
9733
+ * Auto-skips the global `auth` middleware, the way `optionalAuth` does: a route
9734
+ * that carries this one must not also be asked for a live key.
9735
+ *
9736
+ * @example
9737
+ * ```typescript
9738
+ * export const sessionRenewOptions = route.post('/_auth/session/renew/options')
9739
+ * .use([authenticateForRenewal])
9740
+ * .handler(async (c) => startSessionRenewService({ expiredKeyId: getAuth(c).keyId }));
9741
+ * ```
9742
+ */
9743
+ declare const authenticateForRenewal: _spfn_core_route.NamedMiddleware<"authForRenewal">;
9744
+
9664
9745
  /**
9665
9746
  * @spfn/auth - Permission Middleware
9666
9747
  *
@@ -10008,6 +10089,7 @@ declare function getUser(c: Context | {
10008
10089
  status: "active" | "inactive" | "suspended" | "pending_deletion" | "deleted";
10009
10090
  publicId: string;
10010
10091
  passwordChangeRequired: boolean;
10092
+ sessionBinding: "none" | "passkey";
10011
10093
  id: number;
10012
10094
  username: string | null;
10013
10095
  passwordHash: string | null;
@@ -10017,6 +10099,7 @@ declare function getUser(c: Context | {
10017
10099
  emailVerifiedAt: Date | null;
10018
10100
  phoneVerifiedAt: Date | null;
10019
10101
  keyEpoch: number;
10102
+ sessionBindingChangedAt: Date | null;
10020
10103
  lastLoginAt: Date | null;
10021
10104
  deletedAt: Date | null;
10022
10105
  deletedBy: string | null;
@@ -11011,4 +11094,4 @@ declare const authJobRouter: _spfn_core_job.JobRouter<{
11011
11094
  */
11012
11095
  declare const createAuthDeletionJobRouter: typeof createAuthJobRouter;
11013
11096
 
11014
- export { AUTHORIZE_PATH, type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthCsrfConfig, type AuthDeletionConfig, type AuthDeviceAuthConfig, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthMetadataEntity, AuthMetadataRepository, type AuthorizationServerConfig, type AuthorizationServerOptions, type BeforeRegisterContext, COOKIE_NAMES, CSRF_HEADER, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, type CreateRevokeAllLinkOptions, type CsrfMode, DEFAULT_ACCESS_TOKEN_TTL_MS, DEFAULT_CODE_TTL_MS, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, DEFAULT_DEVICE_AUTH_INTERVAL_MS, DEFAULT_DEVICE_AUTH_TTL_MS, DEFAULT_REFRESH_TOKEN_TTL_MS, DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON, DEVICE_AUTH_STATUSES, type DecryptedOAuthToken, type DeviceAuthStatus, type DeviceAuthorization, DeviceAuthorizationsRepository, type EmailNormalizationResult, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, type KeyRevokeAllToken, KeyRevokeAllTokensRepository, KeysRepository, type LiveRevokeAllLink, MAX_UNGRANTED_CLIENTS_PER_IP, MFA_CONFIRM_ATTEMPT_LIMIT, type MfaConfig, MfaEnrolmentRepository, type MfaRecoveryCode, MfaRecoveryCodesRepository, type MfaTotp, MfaTotpRepository, MfaVerification, MfaVerificationMethod, MfaVerificationsRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewDeviceAuthorization, type NewInvitation, type NewKeyRevokeAllToken, type NewMfaRecoveryCode, type NewMfaTotp, type NewOAuth2AuthorizationCode, type NewOAuth2Client, type NewOAuth2Grant, type NewOAuth2Token, type NewOpsToken, NewPasskey, type NewPasswordResetToken, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewSignupLinkToken, type NewUser, type NewUserPermission, type NewUserProfile, type NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, type NewWebAuthnChallenge, OAUTH2_ACCESS_TOKEN_PREFIX, OAUTH2_REFRESH_TOKEN_PREFIX, OAUTH2_TOKEN_KINDS, type OAuth2AuthorizationCode, OAuth2AuthorizationCodesRepository, type OAuth2Client, OAuth2ClientsRepository, type OAuth2Grant, type OAuth2GrantWithClient, OAuth2GrantsRepository, type OAuth2RegisterRefusal, type OAuth2RegisterRequest, type OAuth2RegisterResult, type OAuth2RegisteredClient, type OAuth2Token, type OAuth2TokenKind, type OAuth2TokenRefusal, type OAuth2TokenRequest, type OAuth2TokenResponse, type OAuth2TokenResult, OAuth2TokensRepository, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, OPS_TOKEN_PREFIX, type OpsOrUserConfig, type OpsToken, OpsTokensRepository, Passkey, type PasskeyConfig, type PasskeyUserVerification, PasskeysRepository, type PasswordResetToken, PasswordResetTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RedirectUriRefusal, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type RevokeAllLink, type RevokeAllLinkDescription, type RevokedKey, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, STALE_CLIENT_MAX_AGE_MS, SUPPORTED_GRANT_TYPES, SUPPORTED_RESPONSE_TYPES, type SessionPayload, type SignupLinkToken, SignupLinkTokensRepository, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenEncryptionKey, type TokenPayload, USER_CODE_ALPHABET, USER_CODE_LENGTH, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, type UserPublicKey, type UserSocialAccount, UsersRepository, V2_PREFIX, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOAuth2AccessToken, type VerifiedOpsToken, type VerifyIdTokenParams, WEBAUTHN_CHALLENGE_KINDS, type WebAuthnChallenge, type WebAuthnChallengeKind, WebAuthnChallengesRepository, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, appleProvider, assertAuthorizationServerIssuer, assertCanAssignRole, assertKeyMatchesAlgorithm, assertPasskeyConfig, authJobRouter, authLogger, authMetadata, authMetadataRepository, authSchema, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureAuthorizationServer, configureDeletion, configureDeviceAuth, configureOAuthTokenCipher, consumeRevokeAllLink, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthJobRouter, createAuthLifecycle, createInvitation, createOAuthState, createRevokeAllLink, createRevokeAllTokenPurgeJob, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, deriveCsrfToken, describeRevokeAllLink, deviceAuthorizations, deviceAuthorizationsRepository, encryptToken, exchangeCodeForTokens, expireOldInvitations, formatUserCode, generateAccessToken, generateAuthorizationCode, generateDeviceCode, generateOAuthNonce, generateRefreshToken, generateToken, generateUserCode, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getAuthorizationServerConfig, getCsrfExemptPaths, getCsrfMode, getDeletionConfig, getDeviceAuthConfig, getDummyPasswordHash, getEncryptionKeyring, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getMfaConfig, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPasskeyConfig, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashDeviceCode, hashOAuth2Secret, hashPassword, initOneTimeTokenManager, initializeAuth, invitationsRepository, isAccessTokenShaped, isEncrypted, isGoogleOAuthEnabled, isLoopbackHostname, isOpsToken, isPkceS256ChallengeShaped, isPkceVerifierShaped, issueOpsTokenService, kakaoProvider, keyRevokeAllTokens, keyRevokeAllTokensRepository, keysRepository, linkMailJob, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, matchesCsrfToken, matchesRegisteredRedirectUri, mfaEnrolmentRepository, mfaRecoveryCodes, mfaRecoveryCodesRepository, mfaTotp, mfaTotpRepository, mfaVerificationsRepository, naverProvider, normalizeEmail, normalizeOptionalEmail, normalizeResource, normalizeStoredEmails, normalizeUserCode, oauth2AuthorizationCodes, oauth2AuthorizationCodesRepository, oauth2Clients, oauth2ClientsRepository, oauth2Grants, oauth2GrantsRepository, oauth2TokenService, oauth2Tokens, oauth2TokensRepository, oneTimeTokenAuth, opsOrUser, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, passkeysRepository, passwordResetTokens, passwordResetTokensRepository, permissions, permissionsRepository, pkceChallengeFor, purgeRevokeAllTokensService, purgeStaleOAuth2ClientsService, purgeUserService, redirectHostOf, refreshAccessToken, refuseRedirectUriRegistration, registerOAuth2ClientService, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, resolveIssuerSource, revokeOAuth2TokenService, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, sameOAuth2Hash, sameResource, secondsUntil, setRolePermissions, signupLinkTokens, signupLinkTokensRepository, socialAccountsRepository, sweepDuePurges, toEpochSeconds, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userPublicKeys, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyAccessToken, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken, webauthnChallenges, webauthnChallengesRepository };
11097
+ export { AUTHORIZE_PATH, type AccountDeletionPurgeUser, type AccountDeletionRequest, AccountDeletionRequestedBy, AccountDeletionRequestsRepository, type AuthConfig, AuthContext, type AuthCsrfConfig, type AuthDeletionConfig, type AuthDeviceAuthConfig, type AuthLifecycleConfig, type AuthLifecycleOptions, type AuthMetadataEntity, AuthMetadataRepository, type AuthorizationServerConfig, type AuthorizationServerOptions, type BeforeRegisterContext, COOKIE_NAMES, CSRF_HEADER, type CancelAccountDeletionParams, type CancelAccountDeletionResult, type CreateOAuthStateParams, type CreateRevokeAllLinkOptions, type CsrfMode, DEFAULT_ACCESS_TOKEN_TTL_MS, DEFAULT_CODE_TTL_MS, DEFAULT_DELETION_ALLOW_SELF_IMMEDIATE, DEFAULT_DELETION_GRACE_PERIOD_DAYS, DEFAULT_DELETION_PURGE_CRON, DEFAULT_DELETION_PURGE_STRATEGY, DEFAULT_DELETION_SEND_NOTIFICATIONS, DEFAULT_DEVICE_AUTH_INTERVAL_MS, DEFAULT_DEVICE_AUTH_TTL_MS, DEFAULT_REFRESH_TOKEN_TTL_MS, DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON, DEVICE_AUTH_STATUSES, type DecryptedOAuthToken, type DeviceAuthStatus, type DeviceAuthorization, DeviceAuthorizationsRepository, type EmailNormalizationResult, EnvironmentKeyringTokenCipher, type GoogleTokenResponse, type GoogleUserInfo, type Invitation, InvitationStatus, InvitationsRepository, type IssuedOpsToken, KeyAlgorithmType, type KeyRevokeAllToken, KeyRevokeAllTokensRepository, KeysRepository, type LiveRevokeAllLink, MAX_UNGRANTED_CLIENTS_PER_IP, MFA_CONFIRM_ATTEMPT_LIMIT, MfaChallenge, MfaChallengesRepository, type MfaConfig, MfaEnrolmentRepository, type MfaRecoveryCode, MfaRecoveryCodesRepository, type MfaTotp, MfaTotpRepository, MfaVerification, MfaVerificationMethod, MfaVerificationsRepository, type NewAccountDeletionRequest, type NewAuthMetadataEntity, type NewDeviceAuthorization, type NewInvitation, type NewKeyRevokeAllToken, NewMfaChallenge, type NewMfaRecoveryCode, type NewMfaTotp, type NewOAuth2AuthorizationCode, type NewOAuth2Client, type NewOAuth2Grant, type NewOAuth2Token, type NewOpsToken, NewPasskey, type NewPasswordResetToken, type NewPermission, type NewPermissionEntity, type NewRole, type NewRoleEntity, type NewRolePermission, type NewSignupLinkToken, type NewUser, type NewUserPermission, type NewUserProfile, NewUserPublicKey, type NewUserSocialAccount, type NewVerificationCode, type NewWebAuthnChallenge, OAUTH2_ACCESS_TOKEN_PREFIX, OAUTH2_REFRESH_TOKEN_PREFIX, OAUTH2_TOKEN_KINDS, type OAuth2AuthorizationCode, OAuth2AuthorizationCodesRepository, type OAuth2Client, OAuth2ClientsRepository, type OAuth2Grant, type OAuth2GrantWithClient, OAuth2GrantsRepository, type OAuth2RegisterRefusal, type OAuth2RegisterRequest, type OAuth2RegisterResult, type OAuth2RegisteredClient, type OAuth2Token, type OAuth2TokenKind, type OAuth2TokenRefusal, type OAuth2TokenRequest, type OAuth2TokenResponse, type OAuth2TokenResult, OAuth2TokensRepository, OAuthProvider, type OAuthState, type OAuthTokenCipher, type OAuthTokenContext, type OAuthTokenType, OPS_TOKEN_PREFIX, type OpsOrUserConfig, type OpsToken, OpsTokensRepository, Passkey, type PasskeyConfig, type PasskeyUserVerification, PasskeysRepository, type PasswordResetToken, PasswordResetTokensRepository, type PendingDeletionInfo, type Permission, type PermissionEntity, PermissionsRepository, PurgeStrategy, type PurgeUserResult, type RedirectUriRefusal, type RegisterChannel, type RequestAccountDeletionParams, type RequestAccountDeletionResult, type RevokeAllLink, type RevokeAllLinkDescription, type RevokedKey, type Role, type RoleEntity, type RoleGuardOptions, type RolePermission, RolePermissionsRepository, RolesRepository, STALE_CLIENT_MAX_AGE_MS, SUPPORTED_GRANT_TYPES, SUPPORTED_RESPONSE_TYPES, type SessionPayload, type SignupLinkToken, SignupLinkTokensRepository, SocialAccountsRepository, SocialProvider, type SweepDuePurgesResult, type TokenEncryptionKey, type TokenPayload, USER_CODE_ALPHABET, USER_CODE_LENGTH, type UpdateProfileParams, type User, type UserPermission, UserPermissionsRepository, type UserProfile, UserProfilesRepository, UserPublicKey, type UserSocialAccount, UsersRepository, V2_PREFIX, type VerificationCode, VerificationCodesRepository, VerificationPurpose, type VerifiedOAuth2AccessToken, type VerifiedOpsToken, type VerifyIdTokenParams, WEBAUTHN_CHALLENGE_KINDS, type WebAuthnChallenge, type WebAuthnChallengeKind, WebAuthnChallengesRepository, acceptInvitation, accountDeletionRequests, accountDeletionRequestsRepository, addPermissionToRole, admitsForRenewal, appleProvider, assertAuthorizationServerIssuer, assertCanAssignRole, assertKeyMatchesAlgorithm, assertPasskeyConfig, authJobRouter, authLogger, authMetadata, authMetadataRepository, authSchema, authenticateForRenewal, cancelAccountDeletionService, cancelInvitation, checkUsernameAvailableService, configureAuth, configureAuthorizationServer, configureDeletion, configureDeviceAuth, configureOAuthTokenCipher, consumeRevokeAllLink, createAuthDeletionJobRouter, createAuthDeletionPurgeJob, createAuthJobRouter, createAuthLifecycle, createInvitation, createOAuthState, createRevokeAllLink, createRevokeAllTokenPurgeJob, createRole, decodeToken, decryptToken, deleteInvitation, deleteRole, deriveCsrfToken, describeRevokeAllLink, deviceAuthorizations, deviceAuthorizationsRepository, encryptToken, exchangeCodeForTokens, expireOldInvitations, formatUserCode, generateAccessToken, generateAuthorizationCode, generateDeviceCode, generateOAuthNonce, generateRefreshToken, generateToken, generateUserCode, getAllRoles, getAuth, getAuthConfig, getAuthSessionService, getAuthorizationServerConfig, getBoundKeyRenewGraceMs, getBoundKeyTtlMs, getConcurrentUseWindowMs, getCsrfExemptPaths, getCsrfMode, getDeletionConfig, getDeviceAuthConfig, getDummyPasswordHash, getEncryptionKeyring, getGoogleAuthUrl, getGoogleOAuthConfig, getGoogleUserInfo, getInvitationByToken, getInvitationWithDetails, getKeyId, getLocale, getMfaConfig, getOneTimeTokenManager, getOpsToken, getOptionalAuth, getPasskeyConfig, getPendingDeletionInfo, getRole, getRoleByName, getRolePermissions, getSessionRenewPath, getSessionTtl, getUser, getUserByEmailService, getUserByIdService, getUserByPhoneService, getUserId, getUserPermissions, getUserProfileService, getUserRole, githubProvider, googleProvider, hasAllPermissions, hasAnyPermission, hasAnyRole, hasPermission, hasRole, hashDeviceCode, hashOAuth2Secret, hashPassword, initOneTimeTokenManager, initializeAuth, invitationsRepository, isAccessTokenShaped, isEncrypted, isGoogleOAuthEnabled, isLoopbackHostname, isOpsToken, isPkceS256ChallengeShaped, isPkceVerifierShaped, issueOpsTokenService, kakaoProvider, keyRevokeAllTokens, keyRevokeAllTokensRepository, keysRepository, linkMailJob, listInvitations, listOpsTokensService, matchOAuthCsrfCookies, matchesCsrfToken, matchesRegisteredRedirectUri, mfaChallengesRepository, mfaEnrolmentRepository, mfaRecoveryCodes, mfaRecoveryCodesRepository, mfaTotp, mfaTotpRepository, mfaVerificationsRepository, naverProvider, normalizeEmail, normalizeOptionalEmail, normalizeResource, normalizeStoredEmails, normalizeUserCode, oauth2AuthorizationCodes, oauth2AuthorizationCodesRepository, oauth2Clients, oauth2ClientsRepository, oauth2Grants, oauth2GrantsRepository, oauth2TokenService, oauth2Tokens, oauth2TokensRepository, oneTimeTokenAuth, opsOrUser, opsTokenAuth, opsTokens, opsTokensRepository, parseDuration, passkeysRepository, passwordResetTokens, passwordResetTokensRepository, permissions, permissionsRepository, pkceChallengeFor, purgeRevokeAllTokensService, purgeStaleOAuth2ClientsService, purgeUserService, redirectHostOf, refreshAccessToken, refuseRedirectUriRegistration, registerOAuth2ClientService, removePermissionFromRole, requestAccountDeletionService, requireAnyPermission, requireOpsScope, requirePermissions, requireRole, resendInvitation, resolveIssuerSource, revokeOAuth2TokenService, revokeOpsTokenService, roleGuard, rolePermissions, rolePermissionsRepository, roles, rolesRepository, runBeforeRegister, sameOAuth2Hash, sameResource, secondsUntil, setRolePermissions, signupLinkTokens, signupLinkTokensRepository, socialAccountsRepository, sweepDuePurges, toEpochSeconds, updateLastLoginService, updateLocaleService, updateRole, updateUserProfileService, updateUserService, updateUsernameService, userInvitations, userPermissions, userPermissionsRepository, userProfiles, userProfilesRepository, userSocialAccounts, users, usersRepository, validateInvitation, validatePasswordStrength, verificationCodes, verificationCodesRepository, verifyAccessToken, verifyClientToken, verifyIdToken, verifyKeyFingerprint, verifyOAuthState, verifyOpsTokenService, verifyPassword, verifyToken, webauthnChallenges, webauthnChallengesRepository };