* fix: reject invalid JWTs immediately instead of falling through to next auth mode Introduce an INVALID sentinel so tryMode can distinguish "credential present but failed" from "credential absent." The main loop now short- circuits on INVALID instead of silently trying the next allowed mode. Also covers the case where a JWT verifies cryptographically but has no sub claim (or sub isn't a string) -- previously returned null (fallthrough), now returns INVALID (reject). BREAKING CHANGE: when multiple auth modes are allowed, a present-but-invalid JWT is now rejected with InvalidCredentialsError instead of falling through to the next mode. Clients that previously relied on silent fallthrough (e.g., stale token + valid apikey) must now either omit the Authorization header or refresh the token. * docs: clarify invalid-JWT no-fallthrough semantics Align documentation with the behavior introduced in the fix!: commit on this branch. Make clear across user-facing docs, TSDoc, and SKILL that: - A mode is "tried" only when its credential is actually present, so a request with no Authorization header still falls through. - A JWT that is present but fails verification (malformed, expired, wrong signature, missing sub) rejects with InvalidCredentialsError — it does not silently fall through to another allowed mode. Touches README, docs/auth-modes, docs/security, docs/error-handling, docs/api-reference, skills/supabase-server/SKILL, and TSDoc on the Allow type, WithSupabaseConfig.allow, and verifyCredentials. --------- Co-authored-by: Tomas Pozo <tomaspozogarzon@gmail.com>
7.9 KiB
API Reference
Complete reference for every export, organized by entry point.
@supabase/server
withSupabase
function withSupabase<Database = unknown>(
config: WithSupabaseConfig,
handler: (req: Request, ctx: SupabaseContext<Database>) => Promise<Response>,
): (req: Request) => Promise<Response>
Wraps a fetch handler with auth, CORS, and client creation. Returns a (req: Request) => Promise<Response> function suitable for export default { fetch }.
- Handles
OPTIONSpreflight when CORS is enabled - Verifies credentials per
config.allow - Returns JSON error response on auth failure
- Adds CORS headers to all responses
createSupabaseContext
function createSupabaseContext<Database = unknown>(
request: Request,
options?: WithSupabaseConfig,
): Promise<
| { data: SupabaseContext<Database>; error: null }
| { data: null; error: AuthError }
>
Creates a SupabaseContext from a request. Returns a result tuple. The cors option is ignored.
Defaults to allow: 'user' when options is omitted.
@supabase/server/core
verifyAuth
function verifyAuth(
request: Request,
options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
Extracts credentials from a request and verifies them. Convenience wrapper over extractCredentials + verifyCredentials.
verifyCredentials
function verifyCredentials(
credentials: Credentials,
options: { allow: AllowWithKey | AllowWithKey[]; env?: Partial<SupabaseEnv> },
): Promise<{ data: AuthResult; error: null } | { data: null; error: AuthError }>
Verifies pre-extracted credentials against allowed auth modes. Tries each mode in order — first match wins.
extractCredentials
function extractCredentials(request: Request): Credentials
Reads Authorization: Bearer <token> and apikey headers from a request. Pure extraction, no validation. Synchronous.
resolveEnv
function resolveEnv(
overrides?: Partial<SupabaseEnv>,
): { data: SupabaseEnv; error: null } | { data: null; error: EnvError }
Resolves Supabase environment configuration from runtime variables. SUPABASE_URL is the only hard requirement.
createContextClient
function createContextClient<Database = unknown>(
options?: CreateContextClientOptions,
): SupabaseClient<Database>
Creates a user-scoped Supabase client. RLS applies. Throws EnvError if URL or publishable key is missing.
Configured with:
- Publishable key (named or default) as
apikeyheader - User's JWT as
Authorization: Bearerheader (whenauth.tokenis provided) persistSession: false,autoRefreshToken: false,detectSessionInUrl: false
createAdminClient
function createAdminClient<Database = unknown>(
options?: CreateAdminClientOptions,
): SupabaseClient<Database>
Creates an admin Supabase client that bypasses RLS. Throws EnvError if URL or secret key is missing.
@supabase/server/adapters/hono
withSupabase (Hono)
function withSupabase(
config?: Omit<WithSupabaseConfig, 'cors'>,
): MiddlewareHandler
Hono middleware. Sets c.var.supabaseContext on the Hono context. Throws HTTPException on auth failure with cause: AuthError.
Skips if c.var.supabaseContext is already set (enables route-level overrides).
Defaults to allow: 'user' when config is omitted.
Types
Allow
type Allow = 'always' | 'public' | 'secret' | 'user'
AllowWithKey
type AllowWithKey = Allow | `public:${string}` | `secret:${string}`
Extended auth mode with named key support. Examples: 'public:web', 'secret:*', 'secret:internal'.
SupabaseContext<Database>
interface SupabaseContext<Database = unknown> {
supabase: SupabaseClient<Database>
supabaseAdmin: SupabaseClient<Database>
userClaims: UserClaims | null
claims: JWTClaims | null
authType: Allow
}
WithSupabaseConfig
interface WithSupabaseConfig {
allow?: AllowWithKey | AllowWithKey[] // default: 'user'
env?: Partial<SupabaseEnv>
cors?: boolean | Record<string, string> // default: true
supabaseOptions?: SupabaseClientOptions<string>
}
SupabaseEnv
interface SupabaseEnv {
url: string
publishableKeys: Record<string, string>
secretKeys: Record<string, string>
jwks: JsonWebKeySet | null
}
Credentials
interface Credentials {
token: string | null
apikey: string | null
}
AuthResult
interface AuthResult {
authType: Allow
token: string | null
userClaims: UserClaims | null
claims: JWTClaims | null
keyName?: string | null
}
JWTClaims
interface JWTClaims {
sub: string
iss?: string
aud?: string | string[]
exp?: number
iat?: number
role?: string
email?: string
app_metadata?: Record<string, unknown>
user_metadata?: Record<string, unknown>
[key: string]: unknown
}
UserClaims
interface UserClaims {
id: string
role?: string
email?: string
appMetadata?: Record<string, unknown>
userMetadata?: Record<string, unknown>
}
ClientAuth
interface ClientAuth {
token?: string | null
keyName?: string | null
}
CreateContextClientOptions
interface CreateContextClientOptions {
auth?: ClientAuth
env?: Partial<SupabaseEnv>
supabaseOptions?: SupabaseClientOptions<string>
}
CreateAdminClientOptions
interface CreateAdminClientOptions {
auth?: Pick<ClientAuth, 'keyName'>
env?: Partial<SupabaseEnv>
supabaseOptions?: SupabaseClientOptions<string>
}
JsonWebKeySet
interface JsonWebKeySet {
keys: JsonWebKey[]
}
Error Classes
EnvError
class EnvError extends Error {
readonly status: 500
readonly code: string
}
AuthError
class AuthError extends Error {
readonly status: number // 401 or 500
readonly code: string
}
Error Code Constants
| Constant | Value | Class | Meaning |
|---|---|---|---|
EnvGenericError |
'ENV_ERROR' |
EnvError |
Generic environment error |
MissingSupabaseURLError |
'MISSING_SUPABASE_URL' |
EnvError |
SUPABASE_URL not set |
MissingPublishableKeyError |
'MISSING_PUBLISHABLE_KEY' |
EnvError |
Named publishable key not found |
MissingDefaultPublishableKeyError |
'MISSING_DEFAULT_PUBLISHABLE_KEY' |
EnvError |
No default publishable key |
MissingSecretKeyError |
'MISSING_SECRET_KEY' |
EnvError |
Named secret key not found |
MissingDefaultSecretKeyError |
'MISSING_DEFAULT_SECRET_KEY' |
EnvError |
No default secret key |
AuthGenericError |
'AUTH_ERROR' |
AuthError |
Generic auth error |
InvalidCredentialsError |
'INVALID_CREDENTIALS' |
AuthError |
No credential matched, or JWT failed verification |
CreateSupabaseClientError |
'CREATE_SUPABASE_CLIENT_ERROR' |
AuthError |
Client creation failed after auth |
Errors Factory Map
const Errors: {
[MissingSupabaseURLError]: () => EnvError
[MissingPublishableKeyError]: (name: string) => EnvError
[MissingDefaultPublishableKeyError]: () => EnvError
[MissingSecretKeyError]: (name: string) => EnvError
[MissingDefaultSecretKeyError]: () => EnvError
[InvalidCredentialsError]: () => AuthError
[CreateSupabaseClientError]: () => AuthError
}
Keyed by error code constant. Each entry returns a pre-configured error instance.