Skip to main content

Errors

WIP

The AuthError class, the Answer readers that turn a missing row into null, how an error reaches the wire, and every code with its status

duck-auth throws one class, AuthError. Each carries a code, the HTTP status that code maps to, and a typed meta.

AuthError

import { AuthError } from '@gentleduck/auth/core'

try {
  await auth.flows.signIn({ providerId: 'password', input })
} catch (err) {
  if (err instanceof AuthError && err.code === 'AUTH_RATE_LIMITED') {
    err.status // 429
    err.meta.retryAfter // seconds, typed from the code
  }
  throw err
}

err.message is the bare code. Anything a code needs to say rides on meta, typed per code, so toThrow(/detail/) in a test never matches; assert on err.meta instead.

asAuthError(err, fallbackCode) from @gentleduck/auth/core/errors passes an AuthError through and wraps anything else under the fallback, with the original on cause.

Answers

Calls that look something up answer an Answer: a promise that throws when awaited, with readers that take the failure as a value.

const identity = await auth.identities.getByEmail(email) // throws AUTH_IDENTITY_NOT_FOUND
const maybe = await auth.identities.getByEmail(email).orNull() // null when there is no row
const named = await auth.identities.getByEmail(email).orDefault(guest) // guest when there is no row
const { data, error } = await auth.identities.getByEmail(email).wrap() // never throws

orNull and orDefault absorb only the codes that mean "no row", and rethrow everything else, so a database that is down never reads as a free email address:

AUTH_CREDENTIAL_NOT_FOUND, AUTH_IDEMPOTENCY_MISS, AUTH_IDENTITY_NOT_FOUND, AUTH_IMPERSONATE_WINDOW_CLOSED, AUTH_JWT_INVALID, AUTH_JWT_KEY_UNKNOWN, AUTH_MEMBERSHIP_NOT_FOUND, AUTH_ORG_NOT_FOUND, AUTH_SESSION_EXPIRED, AUTH_SESSION_REVOKED.

The set is exported as ABSENT. A few codes stay out of it on purpose: AUTH_SESSION_IDENTITY_ERASED, AUTH_ANOMALY_DENIED and AUTH_TENANT_SCOPE_VIOLATION are refusals or integrity failures, and must not quietly become an anonymous request.

orNull(promise) does the same for a plain promise, such as a store call:

import { orNull } from '@gentleduck/auth/core'

const findIdentityByEmail = (email: string) => orNull(stores.identities.find({ email }))

On the wire

errorToHttp(err) from @gentleduck/auth/server/generic is what every server adapter answers with:

errorToHttp(new AuthError('AUTH_RATE_LIMITED', { retryAfter: 30 }))
// { status: 429, body: { ok: false, error: { code: 'AUTH_RATE_LIMITED', status: 429, retryAfter: 30 } } }
  • An AuthError answers its status, with its meta spread into error. Meta keys that look like secrets are scrubbed.
  • Anything else answers 500 with detail: 'internal error', and is logged rather than described.
  • A page reads error.code from this body, so the browser sees the server's code.

Codes

CodeStatusRaised when
AUTH_UNAUTHENTICATED401No session where one is required, or its identity is gone.
AUTH_SESSION_EXPIRED401The idle or absolute deadline passed. meta.expiredAt.
AUTH_SESSION_REVOKED401No session, or one that was revoked. meta.reason.
AUTH_SESSION_IDENTITY_ERASED401A live session whose identity was erased.
AUTH_STEP_UP_REQUIRED401A stronger or fresher session is needed. meta.challenge.
AUTH_ANOMALY_DENIED403The anomaly score crossed the deny threshold. meta.score.
AUTH_MFA_REQUIRED401The operation needs an MFA method enrolled or confirmed. meta.methods.
AUTH_INVALID_CREDENTIALS401A wrong password, code or token, or a password too weak to set.
AUTH_PASSKEY_MISMATCH401A passkey or WebAuthn registration or assertion that does not verify.
AUTH_RATE_LIMITED429The limiter refused the attempt. meta.retryAfter in seconds.
AUTH_PROVIDER_FAILED400An unknown provider id, or a provider refusing its input. meta.providerId.
AUTH_PROVIDER_UNSUPPORTED400The provider exists but does not sign anyone in this way.
AUTH_PROVIDER_NOT_REGISTERED500A facet was used without its provider, such as auth.mfa with no mfaProvider().
AUTH_PROVIDER_TAKEN409Another identity already holds that provider login.
AUTH_OAUTH_STATE_MISMATCH400The OAuth callback's state does not match the one issued.
AUTH_OAUTH_NONCE_REPLAY400An OpenID nonce was presented twice.
AUTH_CSRF403The CSRF check failed.
AUTH_DPOP_INVALID401A DPoP proof failed. meta.reason.
AUTH_JWT_INVALID401A JWT session token does not verify. meta.reason.
AUTH_JWT_KEY_UNKNOWN401A JWT names a key id the transport does not hold. meta.kid.
AUTH_SIGNUP_INCOMPLETE409A staged sign-up has stages left. meta.missing.
AUTH_SIGNUP_TOKEN_INVALID400A sign-up flow token is unknown, spent or expired.
AUTH_RECOVERY_TOKEN_INVALID400A reset, verification or deletion token is unknown or spent.
AUTH_RECOVERY_TOKEN_EXPIRED410That token's TTL passed.
AUTH_RECOVERY_REQUIRES_MFA401A reset for an MFA account needs a stepped-up session. meta.methods.
AUTH_STALE_WRITE409An optimistic-concurrency write lost the race.
AUTH_GRACE_EXPIRED410A restore after the soft-delete window.
AUTH_EMAIL_TAKEN409The email belongs to another identity.
AUTH_USERNAME_TAKEN409The username belongs to another identity.
AUTH_ALREADY_EXISTS409A store refused a duplicate row.
AUTH_IMPERSONATE_FORBIDDEN403authorize refused, or the impersonation request was invalid. meta.reason.
AUTH_IMPERSONATE_EXPIRED401releaseImpersonation on a session that is not impersonating.
AUTH_IMPERSONATE_WINDOW_CLOSED401An impersonation session past its window. meta.closedAt.
AUTH_APIKEY_INVALID401An API key that does not verify.
AUTH_APIKEY_REVOKED401A revoked API key.
AUTH_APIKEY_SCOPE_INSUFFICIENT403The key lacks a required scope. meta.required, meta.missing.
AUTH_IDENTITY_NOT_FOUND404No such identity, or a soft-deleted one.
AUTH_CREDENTIAL_NOT_FOUND404No such credential row.
AUTH_ORG_NOT_FOUND404No such organization.
AUTH_MEMBERSHIP_NOT_FOUND404No such membership.
AUTH_IDEMPOTENCY_MISS404No stored answer for that idempotency key.
AUTH_TENANT_SCOPE_VIOLATION500A store answered a row from another tenant.
AUTH_INVALID_PARAMETERS400A value a parser or store refused.
AUTH_NOT_ENOUGH_PARAMETERS500A store call missing what it needs.
AUTH_MISCONFIGURED500A config problem, found at boot or on first use. meta.detail.
AUTH_ADAPTER_FAILED500The store's driver failed; the driver error is on cause.
AUTH_ADAPTER_UNAVAILABLE503The store could not be reached.

The registry is exported as AUTH_ERRORS from @gentleduck/auth/core/errors, keyed by code.