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

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.
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
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';
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-BvRw8b8t.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-BvRw8b8t.js';
3
3
  import * as drizzle_orm_pg_core from 'drizzle-orm/pg-core';
4
4
  import { l as SocialProvider, K as KeyAlgorithmType, d as InvitationStatus, c as AccountDeletionRequestedBy, i as PurgeStrategy } from './types-CTdoTOxM.js';
5
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';
@@ -7896,6 +7896,12 @@ declare const mfaEnrolmentRepository: MfaEnrolmentRepository;
7896
7896
  * most of all, since the poll that spends it is the one call that registers a
7897
7897
  * key. Judging the clock only in the read would let that key be registered after
7898
7898
  * the code was dead.
7899
+ *
7900
+ * The three transitions that answer a record — approve, deny, and the global
7901
+ * revocation's sweep — also wake any long poll parked on it, after commit. That
7902
+ * is done here rather than in each caller because the sweep has four callers
7903
+ * across the revocation paths, and a caller that forgot would leave a waiting
7904
+ * device on its slower recheck instead of telling it no.
7899
7905
  */
7900
7906
 
7901
7907
  declare class DeviceAuthorizationsRepository extends BaseRepository {
@@ -7930,6 +7936,16 @@ declare class DeviceAuthorizationsRepository extends BaseRepository {
7930
7936
  * Read replica.
7931
7937
  */
7932
7938
  findByDeviceCodeHash(deviceCodeHash: string): Promise<DeviceAuthorization | null>;
7939
+ /**
7940
+ * `findByDeviceCodeHash`, read from the primary even outside a transaction.
7941
+ *
7942
+ * For the long poll's wait, which runs before any transaction opens: it is
7943
+ * woken right after an answer commits, and a replica that has not caught up
7944
+ * yet would show it the record still pending and send it back to sleep.
7945
+ *
7946
+ * Write primary.
7947
+ */
7948
+ findByDeviceCodeHashOnPrimary(deviceCodeHash: string): Promise<DeviceAuthorization | null>;
7933
7949
  /**
7934
7950
  * Bind the approving user and move the record to `approved`, but only from
7935
7951
  * `pending`.
@@ -7986,6 +8002,8 @@ declare class DeviceAuthorizationsRepository extends BaseRepository {
7986
8002
  * @returns the spent row, or null if it was not approved (any more), or expired
7987
8003
  */
7988
8004
  consumeApproved(deviceCodeHash: string): Promise<DeviceAuthorization | null>;
8005
+ /** Wake the polls parked on a record this call moved, and hand the row back. */
8006
+ private answered;
7989
8007
  }
7990
8008
  declare const deviceAuthorizationsRepository: DeviceAuthorizationsRepository;
7991
8009
 
@@ -10590,6 +10608,8 @@ interface AuthDeviceAuthConfig {
10590
10608
  ttlMs: number;
10591
10609
  /** How long the waiting device should wait between polls, in milliseconds. */
10592
10610
  intervalMs: number;
10611
+ /** The longest a poll that asks to wait (`waitMillis`) is held open, in milliseconds. */
10612
+ maxWaitMs: number;
10593
10613
  }
10594
10614
  /**
10595
10615
  * Ten minutes. Long enough to walk to the other device and type the code, short
@@ -10598,12 +10618,19 @@ interface AuthDeviceAuthConfig {
10598
10618
  declare const DEFAULT_DEVICE_AUTH_TTL_MS: number;
10599
10619
  /** Five seconds. The interval the server asks for; the rate limit is what enforces it. */
10600
10620
  declare const DEFAULT_DEVICE_AUTH_INTERVAL_MS: number;
10621
+ /**
10622
+ * Twenty seconds. Load balancers commonly close a request that has sent nothing
10623
+ * for 30 seconds (Google Cloud's backend service default among them), and a long
10624
+ * poll cut off there reaches the device as a network error rather than as a
10625
+ * pending answer. Twenty leaves room under that for the request's own work.
10626
+ */
10627
+ declare const DEFAULT_DEVICE_AUTH_MAX_WAIT_MS: number;
10601
10628
  /**
10602
10629
  * Set the resolved device-auth config. Called synchronously from
10603
10630
  * `createAuthLifecycle()` for the same reason `configureDeletion` is: it must
10604
10631
  * take effect before any handler that reads `getDeviceAuthConfig()` can run.
10605
10632
  *
10606
- * Both knobs are whole millisecond counts, and one that is not is refused here
10633
+ * Every knob is a whole millisecond count, and one that is not is refused here
10607
10634
  * rather than served. `intervalMs` is the reason the check exists: it leaves as
10608
10635
  * `intervalMillis` in the start and poll answers, which `DeviceAuthPollResponseSchema`
10609
10636
  * declares an integer and the mobile contract exports as one, so a fractional
@@ -10911,6 +10938,12 @@ interface AuthLifecycleOptions extends AuthInitOptions {
10911
10938
  * @default 5000
10912
10939
  */
10913
10940
  intervalMs?: number;
10941
+ /**
10942
+ * The longest a poll that sends `waitMillis` is held open, in milliseconds.
10943
+ * Keep it under the idle timeout of every proxy in front of the server.
10944
+ * @default 20000
10945
+ */
10946
+ maxWaitMs?: number;
10914
10947
  };
10915
10948
  /**
10916
10949
  * OAuth 2.1 authorization server for MCP clients
@@ -11094,4 +11127,4 @@ declare const authJobRouter: _spfn_core_job.JobRouter<{
11094
11127
  */
11095
11128
  declare const createAuthDeletionJobRouter: typeof createAuthJobRouter;
11096
11129
 
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 };
11130
+ 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_MAX_WAIT_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 };
package/dist/server.js CHANGED
@@ -8505,6 +8505,77 @@ var init_mfa_enrolment_repository = __esm({
8505
8505
  }
8506
8506
  });
8507
8507
 
8508
+ // src/server/lib/device-auth-waiters.ts
8509
+ import { onAfterCommit } from "@spfn/core/db";
8510
+ function announceDeviceAuthAnswered(ids) {
8511
+ if (ids.length === 0) {
8512
+ return;
8513
+ }
8514
+ onAfterCommit(() => {
8515
+ for (const id26 of ids) {
8516
+ wake(id26);
8517
+ }
8518
+ });
8519
+ }
8520
+ function wake(id26) {
8521
+ const parked = waiters.get(id26);
8522
+ if (!parked) {
8523
+ return;
8524
+ }
8525
+ waiters.delete(id26);
8526
+ for (const resolve of parked) {
8527
+ resolve();
8528
+ }
8529
+ }
8530
+ function waitForDeviceAuthAnswer(id26, timeoutMs, signal) {
8531
+ if (signal?.aborted) {
8532
+ return Promise.resolve();
8533
+ }
8534
+ return new Promise((resolve) => {
8535
+ const parked = waiters.get(id26) ?? /* @__PURE__ */ new Set();
8536
+ const done = () => {
8537
+ clearTimeout(timer);
8538
+ signal?.removeEventListener("abort", done);
8539
+ unpark(id26, parked, done);
8540
+ resolve();
8541
+ };
8542
+ const timer = setTimeout(done, timeoutMs);
8543
+ signal?.addEventListener("abort", done, { once: true });
8544
+ parked.add(done);
8545
+ waiters.set(id26, parked);
8546
+ });
8547
+ }
8548
+ function unpark(id26, parked, resolver) {
8549
+ parked.delete(resolver);
8550
+ if (parked.size === 0 && waiters.get(id26) === parked) {
8551
+ waiters.delete(id26);
8552
+ }
8553
+ }
8554
+ function waitingOnDeviceAuth(id26) {
8555
+ return waiting.get(id26) ?? 0;
8556
+ }
8557
+ async function holdDeviceAuthWait(id26, wait) {
8558
+ waiting.set(id26, waitingOnDeviceAuth(id26) + 1);
8559
+ try {
8560
+ return await wait();
8561
+ } finally {
8562
+ const left = waitingOnDeviceAuth(id26) - 1;
8563
+ if (left > 0) {
8564
+ waiting.set(id26, left);
8565
+ } else {
8566
+ waiting.delete(id26);
8567
+ }
8568
+ }
8569
+ }
8570
+ var waiters, waiting;
8571
+ var init_device_auth_waiters = __esm({
8572
+ "src/server/lib/device-auth-waiters.ts"() {
8573
+ "use strict";
8574
+ waiters = /* @__PURE__ */ new Map();
8575
+ waiting = /* @__PURE__ */ new Map();
8576
+ }
8577
+ });
8578
+
8508
8579
  // src/server/repositories/device-authorizations.repository.ts
8509
8580
  import { BaseRepository as BaseRepository14 } from "@spfn/core/db";
8510
8581
  import { eq as eq14, and as and14, gt as gt7, inArray as inArray2, sql as sql9 } from "drizzle-orm";
@@ -8513,6 +8584,7 @@ var init_device_authorizations_repository = __esm({
8513
8584
  "src/server/repositories/device-authorizations.repository.ts"() {
8514
8585
  "use strict";
8515
8586
  init_device_authorizations();
8587
+ init_device_auth_waiters();
8516
8588
  notExpired = () => gt7(deviceAuthorizations.expiresAt, sql9`now()`);
8517
8589
  DeviceAuthorizationsRepository = class extends BaseRepository14 {
8518
8590
  /**
@@ -8555,6 +8627,19 @@ var init_device_authorizations_repository = __esm({
8555
8627
  const result = await this.readDb.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
8556
8628
  return result[0] ?? null;
8557
8629
  }
8630
+ /**
8631
+ * `findByDeviceCodeHash`, read from the primary even outside a transaction.
8632
+ *
8633
+ * For the long poll's wait, which runs before any transaction opens: it is
8634
+ * woken right after an answer commits, and a replica that has not caught up
8635
+ * yet would show it the record still pending and send it back to sleep.
8636
+ *
8637
+ * Write primary.
8638
+ */
8639
+ async findByDeviceCodeHashOnPrimary(deviceCodeHash) {
8640
+ const result = await this.db.select().from(deviceAuthorizations).where(eq14(deviceAuthorizations.deviceCodeHash, deviceCodeHash)).limit(1);
8641
+ return result[0] ?? null;
8642
+ }
8558
8643
  /**
8559
8644
  * Bind the approving user and move the record to `approved`, but only from
8560
8645
  * `pending`.
@@ -8572,7 +8657,7 @@ var init_device_authorizations_repository = __esm({
8572
8657
  notExpired()
8573
8658
  )
8574
8659
  ).returning();
8575
- return result[0] ?? null;
8660
+ return this.answered(result[0] ?? null);
8576
8661
  }
8577
8662
  /**
8578
8663
  * Move the record to `denied`, but only from `pending`.
@@ -8589,7 +8674,7 @@ var init_device_authorizations_repository = __esm({
8589
8674
  notExpired()
8590
8675
  )
8591
8676
  ).returning();
8592
- return result[0] ?? null;
8677
+ return this.answered(result[0] ?? null);
8593
8678
  }
8594
8679
  /**
8595
8680
  * Refuse every authorization a user still has in flight.
@@ -8617,12 +8702,14 @@ var init_device_authorizations_repository = __esm({
8617
8702
  * @returns the rows this call refused
8618
8703
  */
8619
8704
  async denyAllActiveByUserId(userId) {
8620
- return await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8705
+ const denied = await this.db.update(deviceAuthorizations).set({ status: "denied" }).where(
8621
8706
  and14(
8622
8707
  eq14(deviceAuthorizations.userId, userId),
8623
8708
  inArray2(deviceAuthorizations.status, ["pending", "approved"])
8624
8709
  )
8625
8710
  ).returning();
8711
+ announceDeviceAuthAnswered(denied.map((record) => record.id));
8712
+ return denied;
8626
8713
  }
8627
8714
  /**
8628
8715
  * Spend an approved record, but only from `approved`, and address it by the
@@ -8645,6 +8732,13 @@ var init_device_authorizations_repository = __esm({
8645
8732
  ).returning();
8646
8733
  return result[0] ?? null;
8647
8734
  }
8735
+ /** Wake the polls parked on a record this call moved, and hand the row back. */
8736
+ answered(record) {
8737
+ if (record) {
8738
+ announceDeviceAuthAnswered([record.id]);
8739
+ }
8740
+ return record;
8741
+ }
8648
8742
  };
8649
8743
  deviceAuthorizationsRepository = new DeviceAuthorizationsRepository();
8650
8744
  }
@@ -11587,7 +11681,7 @@ import { InvalidKeyFingerprintError, KeyIdAlreadyRegisteredError } from "@spfn/a
11587
11681
  import { ValidationError as ValidationError3 } from "@spfn/core/errors";
11588
11682
 
11589
11683
  // src/server/services/device-registration.service.ts
11590
- import { onAfterCommit } from "@spfn/core/db";
11684
+ import { onAfterCommit as onAfterCommit2 } from "@spfn/core/db";
11591
11685
 
11592
11686
  // src/server/events/index.ts
11593
11687
  init_esm();
@@ -11739,7 +11833,7 @@ init_repositories();
11739
11833
  var DEVICE_EVENT_FINGERPRINT_PREFIX_LENGTH = 12;
11740
11834
  async function emitDeviceRegistered(row, channel) {
11741
11835
  const mfaEnrolled = await mfaEnrolmentRepository.isEnrolled(row.userId);
11742
- onAfterCommit(() => authDeviceRegisteredEvent.emit({
11836
+ onAfterCommit2(() => authDeviceRegisteredEvent.emit({
11743
11837
  userId: String(row.userId),
11744
11838
  keyId: row.keyId,
11745
11839
  algorithm: row.algorithm,
@@ -11757,7 +11851,7 @@ async function emitDeviceRegistered(row, channel) {
11757
11851
  // src/server/services/mfa.service.ts
11758
11852
  init_logger();
11759
11853
  init_config();
11760
- import { onAfterCommit as onAfterCommit2, runInTransaction as runInTransaction2 } from "@spfn/core/db";
11854
+ import { onAfterCommit as onAfterCommit3, runInTransaction as runInTransaction2 } from "@spfn/core/db";
11761
11855
  import { ValidationError as ValidationError2 } from "@spfn/core/errors";
11762
11856
  import {
11763
11857
  MfaAlreadyEnrolledError,
@@ -12230,7 +12324,7 @@ async function releaseDeferredAnnouncements(row, key) {
12230
12324
  return;
12231
12325
  }
12232
12326
  const mfaEnrolled = await mfaEnrolledForUser(row.userId);
12233
- onAfterCommit2(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
12327
+ onAfterCommit3(() => authLoginEvent.emit({ ...row.loginEvent, userId: String(row.userId), mfaEnrolled }));
12234
12328
  }
12235
12329
  async function verifyMfaChallengeService(params) {
12236
12330
  const challengeHash = hashCredential(params.challenge);
@@ -12324,7 +12418,7 @@ async function countConfirmFailure(userId) {
12324
12418
  function readSecret(secretEnc, userId) {
12325
12419
  const { value, needsRotation } = decryptMfaSecret(secretEnc, userId);
12326
12420
  if (needsRotation) {
12327
- onAfterCommit2(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
12421
+ onAfterCommit3(() => mfaTotpRepository.updateSecret(userId, encryptMfaSecret(value, userId)));
12328
12422
  }
12329
12423
  return value;
12330
12424
  }
@@ -12633,7 +12727,7 @@ init_key_policy();
12633
12727
  // src/server/services/account-deletion.service.ts
12634
12728
  init_repositories();
12635
12729
  import { ValidationError as ValidationError4, NotFoundError as NotFoundError2 } from "@spfn/core/errors";
12636
- import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit3 } from "@spfn/core/db";
12730
+ import { runInTransaction as runInTransaction3, onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
12637
12731
  import { sendEmail as sendEmail3 } from "@spfn/notification/server";
12638
12732
  import {
12639
12733
  InvalidCredentialsError,
@@ -12790,13 +12884,13 @@ async function requestAccountDeletionService(userId, params) {
12790
12884
  await keysRepository.revokeAllActiveByUserId(user.id, "Account deletion requested");
12791
12885
  await deviceAuthorizationsRepository.denyAllActiveByUserId(user.id);
12792
12886
  await revokeAllOAuth2GrantsForUser(user.id);
12793
- onAfterCommit3(() => authDeletionRequestedEvent.emit({
12887
+ onAfterCommit4(() => authDeletionRequestedEvent.emit({
12794
12888
  userId: String(user.id),
12795
12889
  userPublicId: user.publicId,
12796
12890
  purgeScheduledAt: purgeScheduledAt.toISOString(),
12797
12891
  requestedBy
12798
12892
  }));
12799
- onAfterCommit3(() => notifyDeletionRequested(user, purgeScheduledAt));
12893
+ onAfterCommit4(() => notifyDeletionRequested(user, purgeScheduledAt));
12800
12894
  if (gracePeriodDays === 0) {
12801
12895
  await purgePendingRequest(request);
12802
12896
  }
@@ -12828,11 +12922,11 @@ async function cancelAccountDeletionService(params) {
12828
12922
  throw new DeletionNotRequestedError();
12829
12923
  }
12830
12924
  await usersRepository.reactivateFromPendingDeletion(user.id);
12831
- onAfterCommit3(() => authDeletionCancelledEvent.emit({
12925
+ onAfterCommit4(() => authDeletionCancelledEvent.emit({
12832
12926
  userId: String(user.id),
12833
12927
  userPublicId: user.publicId
12834
12928
  }));
12835
- onAfterCommit3(() => notifyDeletionCancelled(user));
12929
+ onAfterCommit4(() => notifyDeletionCancelled(user));
12836
12930
  return { userId: String(user.id) };
12837
12931
  }
12838
12932
  async function anonymizeUser(user) {
@@ -12919,9 +13013,9 @@ async function purgePendingRequest(request) {
12919
13013
  }
12920
13014
  const { email, publicId } = purgedUser;
12921
13015
  if (email) {
12922
- onAfterCommit3(() => notifyPurgeFinal(email));
13016
+ onAfterCommit4(() => notifyPurgeFinal(email));
12923
13017
  }
12924
- onAfterCommit3(() => authDeletionCompletedEvent.emit({
13018
+ onAfterCommit4(() => authDeletionCompletedEvent.emit({
12925
13019
  userPublicId: publicId,
12926
13020
  purgeStrategy
12927
13021
  }));
@@ -13275,7 +13369,7 @@ init_repositories();
13275
13369
  import { env as env11 } from "@spfn/auth/config";
13276
13370
  import { PasswordResetLinkError, PasswordResetSessionError } from "@spfn/auth/errors";
13277
13371
  import { ValidationError as ValidationError6 } from "@spfn/core/errors";
13278
- import { onAfterCommit as onAfterCommit4 } from "@spfn/core/db";
13372
+ import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
13279
13373
  init_key_policy();
13280
13374
  async function activeUserOf(userId) {
13281
13375
  const user = await usersRepository.findByIdOnPrimary(userId);
@@ -13376,7 +13470,7 @@ async function completePasswordResetService(params) {
13376
13470
  throw new PasswordResetSessionError();
13377
13471
  }
13378
13472
  const registered = await replaceCredentials(row, user, params);
13379
- onAfterCommit4(() => authPasswordResetEvent.emit({
13473
+ onAfterCommit5(() => authPasswordResetEvent.emit({
13380
13474
  userId: String(user.id),
13381
13475
  email: row.email
13382
13476
  }));
@@ -13468,21 +13562,26 @@ import {
13468
13562
  DeviceAuthDeniedError,
13469
13563
  InvalidKeyFingerprintError as InvalidKeyFingerprintError2
13470
13564
  } from "@spfn/auth/errors";
13471
- import { onAfterCommit as onAfterCommit5 } from "@spfn/core/db";
13565
+ import { onAfterCommit as onAfterCommit6 } from "@spfn/core/db";
13566
+ import { getShutdownManager } from "@spfn/core/server";
13472
13567
 
13473
13568
  // src/server/lib/device-auth-config.ts
13474
13569
  var DEFAULT_DEVICE_AUTH_TTL_MS = 10 * 60 * 1e3;
13475
13570
  var DEFAULT_DEVICE_AUTH_INTERVAL_MS = 5 * 1e3;
13571
+ var DEFAULT_DEVICE_AUTH_MAX_WAIT_MS = 20 * 1e3;
13476
13572
  var config2 = {
13477
13573
  ttlMs: DEFAULT_DEVICE_AUTH_TTL_MS,
13478
- intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS
13574
+ intervalMs: DEFAULT_DEVICE_AUTH_INTERVAL_MS,
13575
+ maxWaitMs: DEFAULT_DEVICE_AUTH_MAX_WAIT_MS
13479
13576
  };
13480
13577
  function configureDeviceAuth(options) {
13481
13578
  const ttlMs = options?.ttlMs ?? DEFAULT_DEVICE_AUTH_TTL_MS;
13482
13579
  const intervalMs = options?.intervalMs ?? DEFAULT_DEVICE_AUTH_INTERVAL_MS;
13580
+ const maxWaitMs = options?.maxWaitMs ?? DEFAULT_DEVICE_AUTH_MAX_WAIT_MS;
13483
13581
  assertWholeMillis("ttlMs", ttlMs);
13484
13582
  assertWholeMillis("intervalMs", intervalMs);
13485
- config2 = { ttlMs, intervalMs };
13583
+ assertWholeMillis("maxWaitMs", maxWaitMs);
13584
+ config2 = { ttlMs, intervalMs, maxWaitMs };
13486
13585
  }
13487
13586
  function assertWholeMillis(name, value) {
13488
13587
  if (!Number.isInteger(value) || value <= 0) {
@@ -13495,6 +13594,9 @@ function getDeviceAuthConfig() {
13495
13594
  return config2;
13496
13595
  }
13497
13596
 
13597
+ // src/server/services/device-auth.service.ts
13598
+ init_device_auth_waiters();
13599
+
13498
13600
  // src/server/lib/device-code.ts
13499
13601
  import { createHash, randomBytes, randomInt } from "crypto";
13500
13602
  var USER_CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
@@ -13524,6 +13626,8 @@ function hashDeviceCode(deviceCode) {
13524
13626
  // src/server/services/device-auth.service.ts
13525
13627
  init_key_policy();
13526
13628
  var USER_CODE_ATTEMPTS = 3;
13629
+ var WAIT_RECHECK_MS = 1e3;
13630
+ var MAX_WAITERS_PER_RECORD = 3;
13527
13631
  function assertActionable(record) {
13528
13632
  if (!record || record.status === "consumed") {
13529
13633
  throw new DeviceAuthNotFoundError();
@@ -13623,6 +13727,37 @@ async function denyDeviceAuthService(params) {
13623
13727
  );
13624
13728
  }
13625
13729
  }
13730
+ async function waitForDeviceAuthAnswerService(params) {
13731
+ const requested = Math.min(params.waitMillis, getDeviceAuthConfig().maxWaitMs);
13732
+ const deviceCodeHash = hashDeviceCode(params.deviceCode);
13733
+ const record = requested > 0 ? await readWaitable(deviceCodeHash) : null;
13734
+ if (!record || waitingOnDeviceAuth(record.id) >= MAX_WAITERS_PER_RECORD) {
13735
+ return 0;
13736
+ }
13737
+ const startedAt = Date.now();
13738
+ const deadline = Math.min(startedAt + requested, record.expiresAt.getTime());
13739
+ await holdDeviceAuthWait(record.id, () => waitUntil(record.id, deviceCodeHash, deadline, params.signal));
13740
+ return Date.now() - startedAt;
13741
+ }
13742
+ async function waitUntil(id26, deviceCodeHash, deadline, signal) {
13743
+ while (!signal?.aborted && !getShutdownManager().isShuttingDown()) {
13744
+ const remaining = deadline - Date.now();
13745
+ if (remaining <= 0) {
13746
+ return;
13747
+ }
13748
+ await waitForDeviceAuthAnswer(id26, Math.min(remaining, WAIT_RECHECK_MS), signal);
13749
+ if (!await readWaitable(deviceCodeHash)) {
13750
+ return;
13751
+ }
13752
+ }
13753
+ }
13754
+ async function readWaitable(deviceCodeHash) {
13755
+ const record = await deviceAuthorizationsRepository.findByDeviceCodeHashOnPrimary(deviceCodeHash).catch(() => null);
13756
+ return isWaitable(record) ? record : null;
13757
+ }
13758
+ function isWaitable(record) {
13759
+ return record?.status === "pending" && record.expiresAt.getTime() > Date.now();
13760
+ }
13626
13761
  async function pollDeviceAuthService(params) {
13627
13762
  const deviceCodeHash = hashDeviceCode(params.deviceCode);
13628
13763
  const record = assertActionable(
@@ -13632,7 +13767,10 @@ async function pollDeviceAuthService(params) {
13632
13767
  throw new DeviceAuthDeniedError();
13633
13768
  }
13634
13769
  if (record.status === "pending") {
13635
- return { status: "pending", intervalMillis: getDeviceAuthConfig().intervalMs };
13770
+ return {
13771
+ status: "pending",
13772
+ intervalMillis: Math.max(0, getDeviceAuthConfig().intervalMs - (params.waitedMillis ?? 0))
13773
+ };
13636
13774
  }
13637
13775
  const consumed = await deviceAuthorizationsRepository.consumeApproved(deviceCodeHash);
13638
13776
  if (!consumed) {
@@ -13688,7 +13826,7 @@ async function completeDeviceLogin(record, provenance) {
13688
13826
  ...loginBindingFields(registeredBinding(registered))
13689
13827
  };
13690
13828
  const mfaEnrolled = await mfaEnrolledForUser(user.id);
13691
- onAfterCommit5(() => authLoginEvent.emit({
13829
+ onAfterCommit6(() => authLoginEvent.emit({
13692
13830
  userId: String(user.id),
13693
13831
  provider: "device",
13694
13832
  email: result.email,
@@ -13701,7 +13839,7 @@ async function completeDeviceLogin(record, provenance) {
13701
13839
  // src/server/services/passkey.service.ts
13702
13840
  init_logger();
13703
13841
  init_config();
13704
- import { onAfterCommit as onAfterCommit6, runInTransaction as runInTransaction4 } from "@spfn/core/db";
13842
+ import { onAfterCommit as onAfterCommit7, runInTransaction as runInTransaction4 } from "@spfn/core/db";
13705
13843
  import { ValidationError as ValidationError8 } from "@spfn/core/errors";
13706
13844
  import {
13707
13845
  AccountDisabledError as AccountDisabledError3,
@@ -13819,7 +13957,7 @@ async function finishPasskeyEnrollmentService(params) {
13819
13957
  aaguid: verified.aaguid,
13820
13958
  label: params.label ?? null
13821
13959
  });
13822
- onAfterCommit6(() => passkeyEnrolledEvent.emit({
13960
+ onAfterCommit7(() => passkeyEnrolledEvent.emit({
13823
13961
  userId: String(params.userId),
13824
13962
  passkeyId: String(row.id),
13825
13963
  label: row.label ?? void 0
@@ -13919,7 +14057,7 @@ async function startSession(user, params) {
13919
14057
  ...loginBindingFields(registeredBinding(registered))
13920
14058
  };
13921
14059
  const mfaEnrolled = await mfaEnrolledForUser(user.id);
13922
- onAfterCommit6(() => authLoginEvent.emit({
14060
+ onAfterCommit7(() => authLoginEvent.emit({
13923
14061
  userId: String(user.id),
13924
14062
  provider: "passkey",
13925
14063
  email: result.email,
@@ -14002,7 +14140,7 @@ async function revokePasskeyService(params) {
14002
14140
  if (!revoked) {
14003
14141
  throw new PasskeyNotFoundError2();
14004
14142
  }
14005
- onAfterCommit6(() => passkeyRevokedEvent.emit({
14143
+ onAfterCommit7(() => passkeyRevokedEvent.emit({
14006
14144
  userId: String(params.userId),
14007
14145
  passkeyId: String(revoked.id),
14008
14146
  reason: "user"
@@ -15830,7 +15968,7 @@ async function oauthUnlinkNotifyService(provider, notification) {
15830
15968
  }
15831
15969
 
15832
15970
  // src/server/services/oauth-native.service.ts
15833
- import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit7 } from "@spfn/core/db";
15971
+ import { runInTransaction as runInTransaction5, onAfterCommit as onAfterCommit8 } from "@spfn/core/db";
15834
15972
  import {
15835
15973
  InvalidKeyFingerprintError as InvalidKeyFingerprintError3,
15836
15974
  NativeSignInUnsupportedError as NativeSignInUnsupportedError5,
@@ -15903,7 +16041,7 @@ async function persistNativeLogin(identity, params) {
15903
16041
  }
15904
16042
  await updateLastLoginService(userId);
15905
16043
  const mfaEnrolled = isNewUser ? false : await mfaEnrolledForUser(userId);
15906
- onAfterCommit7(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
16044
+ onAfterCommit8(() => isNewUser ? authRegisterEvent.emit(eventPayload) : authLoginEvent.emit({ ...eventPayload, mfaEnrolled }));
15907
16045
  return { mfaRequired: false, userId: String(userId), keyId: params.keyId, isNewUser };
15908
16046
  }, { context: "auth:oauth-native" });
15909
16047
  }
@@ -16937,6 +17075,31 @@ function attestedClientIp(c) {
16937
17075
  return provenance.webProxy ? provenance.ip ?? null : null;
16938
17076
  }
16939
17077
 
17078
+ // src/server/middleware/device-auth-long-poll.ts
17079
+ var DEVICE_AUTH_WAITED_MILLIS = "deviceAuthWaitedMillis";
17080
+ function deviceAuthLongPoll() {
17081
+ return async (c, next) => {
17082
+ const target = waitTarget(await c.req.json().catch(() => null));
17083
+ if (!target) {
17084
+ return next();
17085
+ }
17086
+ const signal = c.req.raw.signal;
17087
+ const waitedMillis = await waitForDeviceAuthAnswerService({ ...target, signal });
17088
+ if (signal.aborted) {
17089
+ return c.body(null, 204);
17090
+ }
17091
+ c.set(DEVICE_AUTH_WAITED_MILLIS, waitedMillis);
17092
+ return next();
17093
+ };
17094
+ }
17095
+ function waitTarget(body) {
17096
+ const { deviceCode, waitMillis } = body ?? {};
17097
+ if (typeof deviceCode !== "string" || !Number.isInteger(waitMillis) || waitMillis <= 0) {
17098
+ return null;
17099
+ }
17100
+ return { deviceCode, waitMillis };
17101
+ }
17102
+
16940
17103
  // src/server/routes/auth/index.ts
16941
17104
  var sendVerificationCode = route.post("/_auth/codes").input({
16942
17105
  body: Type.Object({
@@ -17095,11 +17258,23 @@ var startDeviceAuth = route.post("/_auth/device/start").input({
17095
17258
  });
17096
17259
  var pollDeviceAuth = route.post("/_auth/device/poll").input({
17097
17260
  body: Type.Object({
17098
- deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" })
17261
+ deviceCode: Type.String({ description: "Device code returned by /_auth/device/start" }),
17262
+ waitMillis: Type.Optional(Type.Integer({
17263
+ minimum: 0,
17264
+ description: "Longest to hold the request while nobody has answered; capped by the server"
17265
+ }))
17099
17266
  })
17100
- }).use([rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }), Transactional()]).skip(["auth"]).handler(async (c) => {
17267
+ }).use([
17268
+ rateLimitPolicy("auth-device-poll", { limit: 30, windowMs: 6e4 }),
17269
+ deviceAuthLongPoll(),
17270
+ Transactional()
17271
+ ]).skip(["auth"]).handler(async (c) => {
17101
17272
  const { body } = await c.data();
17102
- return await pollDeviceAuthService({ ...body, ...deviceProvenance(c.raw) });
17273
+ return await pollDeviceAuthService({
17274
+ deviceCode: body.deviceCode,
17275
+ ...deviceProvenance(c.raw),
17276
+ waitedMillis: Number(c.raw.get(DEVICE_AUTH_WAITED_MILLIS) ?? 0)
17277
+ });
17103
17278
  });
17104
17279
  var getDeviceAuthInfo = route.post("/_auth/device/info").input({
17105
17280
  body: Type.Object({
@@ -18176,7 +18351,7 @@ var CORE_PREREQUISITE_OPERATIONS = [
18176
18351
 
18177
18352
  // src/server/client-proof/contract-bundle.ts
18178
18353
  init_wire_headers();
18179
- var CONTRACT_VERSION = "0.13.0";
18354
+ var CONTRACT_VERSION = "0.13.1";
18180
18355
  var CONTRACT_SUPPORTED_RANGE = ">=0.13.0 <0.14.0";
18181
18356
  function required(name, type) {
18182
18357
  return { name, type, optional: false };
@@ -18510,7 +18685,8 @@ var CONTRACT_TYPES = [
18510
18685
  {
18511
18686
  name: "PollDeviceAuthRequest",
18512
18687
  fields: [
18513
- required("deviceCode", "string")
18688
+ required("deviceCode", "string"),
18689
+ optional("waitMillis", "integer")
18514
18690
  ]
18515
18691
  },
18516
18692
  /**
@@ -21479,6 +21655,7 @@ export {
21479
21655
  DEFAULT_DELETION_PURGE_STRATEGY,
21480
21656
  DEFAULT_DELETION_SEND_NOTIFICATIONS,
21481
21657
  DEFAULT_DEVICE_AUTH_INTERVAL_MS,
21658
+ DEFAULT_DEVICE_AUTH_MAX_WAIT_MS,
21482
21659
  DEFAULT_DEVICE_AUTH_TTL_MS,
21483
21660
  DEFAULT_REFRESH_TOKEN_TTL_MS,
21484
21661
  DEFAULT_REVOKE_ALL_TOKEN_PURGE_CRON,