@supabase/server 0.1.1-rc.24 → 0.1.1-rc.26

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.
@@ -0,0 +1,227 @@
1
+ import { SupabaseClient } from "@supabase/supabase-js";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Authentication mode that determines what credentials a request must provide.
6
+ *
7
+ * - `"always"` — No credentials required. Every request is accepted.
8
+ * - `"public"` — Requires a valid publishable key in the `apikey` header.
9
+ * - `"secret"` — Requires a valid secret key in the `apikey` header (timing-safe comparison).
10
+ * - `"user"` — Requires a valid JWT in the `Authorization: Bearer <token>` header.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // Single mode
15
+ * withSupabase({ allow: 'user' }, handler)
16
+ *
17
+ * // Multiple modes — the first match wins
18
+ * withSupabase({ allow: ['user', 'public'] }, handler)
19
+ * ```
20
+ */
21
+ type Allow = 'always' | 'public' | 'secret' | 'user';
22
+ /**
23
+ * Extended auth mode that supports targeting a specific named key.
24
+ *
25
+ * Use the colon syntax (`"public:web_app"`) to require a specific named key
26
+ * from the `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS` JSON object.
27
+ * Use `"public:*"` or `"secret:*"` to accept any key in the set.
28
+ *
29
+ * @example
30
+ * ```ts
31
+ * // Accept only the "mobile" publishable key
32
+ * withSupabase({ allow: 'public:mobile' }, handler)
33
+ *
34
+ * // Accept any secret key
35
+ * withSupabase({ allow: 'secret:*' }, handler)
36
+ *
37
+ * // Mix named keys with other modes
38
+ * withSupabase({ allow: ['user', 'public:web_app'] }, handler)
39
+ * ```
40
+ */
41
+ type AllowWithKey = Allow | `public:${string}` | `secret:${string}`;
42
+ /**
43
+ * Resolved Supabase environment configuration.
44
+ *
45
+ * Holds the project URL, API keys, and JWKS needed by every other primitive.
46
+ * Typically resolved automatically from environment variables by {@link resolveEnv},
47
+ * but can be passed explicitly via the `env` option.
48
+ *
49
+ * @see {@link resolveEnv} for how each field maps to environment variables.
50
+ */
51
+ interface SupabaseEnv {
52
+ /** Supabase project URL (e.g. `"https://<ref>.supabase.co"`). Sourced from `SUPABASE_URL`. */
53
+ url: string;
54
+ /**
55
+ * Named publishable (anon) keys. Sourced from `SUPABASE_PUBLISHABLE_KEYS` (JSON object)
56
+ * or `SUPABASE_PUBLISHABLE_KEY` (single key, stored as `{ default: "<value>" }`).
57
+ */
58
+ publishableKeys: Record<string, string>;
59
+ /**
60
+ * Named secret (service-role) keys. Sourced from `SUPABASE_SECRET_KEYS` (JSON object)
61
+ * or `SUPABASE_SECRET_KEY` (single key, stored as `{ default: "<value>" }`).
62
+ */
63
+ secretKeys: Record<string, string>;
64
+ /**
65
+ * JSON Web Key Set used for JWT verification. Sourced from `SUPABASE_JWKS`.
66
+ * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
67
+ * `null` when no JWKS is configured (JWT verification will be unavailable).
68
+ */
69
+ jwks: JsonWebKeySet | null;
70
+ }
71
+ /**
72
+ * A JSON Web Key Set as defined by RFC 7517.
73
+ *
74
+ * @see https://datatracker.ietf.org/doc/html/rfc7517
75
+ */
76
+ interface JsonWebKeySet {
77
+ /** Array of JSON Web Keys. */
78
+ keys: JsonWebKey[];
79
+ }
80
+ /**
81
+ * Raw credentials extracted from an incoming HTTP request.
82
+ *
83
+ * Produced by {@link extractCredentials} from the `Authorization` and `apikey` headers.
84
+ *
85
+ * @see {@link extractCredentials}
86
+ */
87
+ interface Credentials {
88
+ /** Bearer token from the `Authorization: Bearer <token>` header, or `null` if absent. */
89
+ token: string | null;
90
+ /** API key from the `apikey` header, or `null` if absent. */
91
+ apikey: string | null;
92
+ }
93
+ /**
94
+ * Result of credential verification.
95
+ *
96
+ * Contains the resolved auth mode, the verified token (for `"user"` mode),
97
+ * decoded JWT claims, and the matched key name (for `"public"` / `"secret"` modes).
98
+ *
99
+ * @see {@link verifyCredentials}
100
+ * @see {@link verifyAuth}
101
+ */
102
+ interface AuthResult {
103
+ /** The auth mode that was successfully matched. */
104
+ authType: Allow;
105
+ /** The verified JWT, or `null` for non-user auth modes. */
106
+ token: string | null;
107
+ /** Normalized user identity derived from the JWT, or `null` when no JWT is present. */
108
+ userClaims: UserClaims | null;
109
+ /** Raw JWT payload, or `null` when no JWT is present. */
110
+ claims: JWTClaims | null;
111
+ /** Name of the matched key (e.g. `"default"`, `"mobile"`), or `null` for `"user"` / `"always"` modes. */
112
+ keyName?: string | null;
113
+ }
114
+ /**
115
+ * Standard JWT claims as defined by RFC 7519, extended with Supabase-specific fields.
116
+ *
117
+ * This is the raw JWT payload — use {@link UserClaims} for a normalized, camelCase view.
118
+ *
119
+ * @see https://datatracker.ietf.org/doc/html/rfc7519#section-4.1
120
+ */
121
+ interface JWTClaims {
122
+ /** Subject — the user's unique ID. */
123
+ sub: string;
124
+ /** Issuer — typically your Supabase project URL. */
125
+ iss?: string;
126
+ /** Audience — who the token is intended for. */
127
+ aud?: string | string[];
128
+ /** Expiration time (seconds since epoch). */
129
+ exp?: number;
130
+ /** Issued at (seconds since epoch). */
131
+ iat?: number;
132
+ /** Supabase role (e.g. `"authenticated"`, `"anon"`). */
133
+ role?: string;
134
+ /** User's email address from Supabase Auth. */
135
+ email?: string;
136
+ /** Application-level metadata set via Supabase Auth admin APIs. */
137
+ app_metadata?: Record<string, unknown>;
138
+ /** User-editable metadata set via Supabase Auth. */
139
+ user_metadata?: Record<string, unknown>;
140
+ /** Additional custom claims. */
141
+ [key: string]: unknown;
142
+ }
143
+ /**
144
+ * Normalized, camelCase view of the authenticated user's identity.
145
+ *
146
+ * Derived from {@link JWTClaims}. For the full Supabase `User` object
147
+ * (including email confirmation status, providers, etc.), call
148
+ * `supabase.auth.getUser()` using the context client.
149
+ */
150
+ interface UserClaims {
151
+ /** User's unique ID (same as `JWTClaims.sub`). */
152
+ id: string;
153
+ /** Supabase role (e.g. `"authenticated"`). */
154
+ role?: string;
155
+ /** User's email address. */
156
+ email?: string;
157
+ /** Application-level metadata (e.g. roles, permissions). */
158
+ appMetadata?: Record<string, unknown>;
159
+ /** User-editable profile metadata (e.g. display name, avatar). */
160
+ userMetadata?: Record<string, unknown>;
161
+ }
162
+ /**
163
+ * Configuration for {@link withSupabase} and {@link createSupabaseContext}.
164
+ *
165
+ * Controls which auth modes are accepted, environment overrides, and CORS behavior.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * // Require authenticated users, auto-CORS enabled (default)
170
+ * const config: WithSupabaseConfig = { allow: 'user' }
171
+ *
172
+ * // Accept users or service-to-service calls, custom CORS headers
173
+ * const config: WithSupabaseConfig = {
174
+ * allow: ['user', 'secret'],
175
+ * cors: { 'Access-Control-Allow-Origin': 'https://myapp.com' },
176
+ * }
177
+ *
178
+ * // No auth required, CORS disabled
179
+ * const config: WithSupabaseConfig = { allow: 'always', cors: false }
180
+ * ```
181
+ */
182
+ interface WithSupabaseConfig {
183
+ /**
184
+ * Auth mode(s) to accept. Modes are tried in order — the first match wins.
185
+ *
186
+ * @defaultValue `"user"`
187
+ */
188
+ allow?: AllowWithKey | AllowWithKey[];
189
+ /**
190
+ * Override auto-detected environment variables. Useful for testing
191
+ * or when running in environments without standard env var support.
192
+ */
193
+ env?: Partial<SupabaseEnv>;
194
+ /**
195
+ * CORS configuration for the `withSupabase` wrapper.
196
+ *
197
+ * - `true` (default) — uses `@supabase/supabase-js` default CORS headers.
198
+ * - `false` — disables CORS handling entirely.
199
+ * - `Record<string, string>` — custom CORS headers.
200
+ *
201
+ * @remarks Only applies to the top-level {@link withSupabase} wrapper.
202
+ * The Hono adapter handles CORS separately via Hono's own middleware.
203
+ *
204
+ * @defaultValue `true`
205
+ */
206
+ cors?: boolean | Record<string, string>;
207
+ }
208
+ /**
209
+ * The Supabase context created for each authenticated request.
210
+ *
211
+ * Contains pre-configured Supabase clients and the caller's identity.
212
+ * Identical regardless of which layer or adapter produced it.
213
+ */
214
+ interface SupabaseContext<Database = unknown> {
215
+ /** Supabase client scoped to the caller's identity. RLS policies apply. */
216
+ supabase: SupabaseClient<Database>;
217
+ /** Admin Supabase client that bypasses Row-Level Security. */
218
+ supabaseAdmin: SupabaseClient<Database>;
219
+ /** JWT-derived identity. For the full Supabase User object, call `supabase.auth.getUser()`. */
220
+ userClaims: UserClaims | null;
221
+ /** Raw JWT payload. `null` for non-user auth modes. */
222
+ claims: JWTClaims | null;
223
+ /** The auth mode that was used for this request. */
224
+ authType: Allow;
225
+ }
226
+ //#endregion
227
+ export { JWTClaims as a, UserClaims as c, Credentials as i, WithSupabaseConfig as l, AllowWithKey as n, SupabaseContext as o, AuthResult as r, SupabaseEnv as s, Allow as t };
@@ -2,6 +2,26 @@ import { createClient } from "@supabase/supabase-js";
2
2
  import { createLocalJWKSet, jwtVerify } from "jose";
3
3
 
4
4
  //#region src/errors.ts
5
+ /**
6
+ * Thrown when a required environment variable is missing or malformed.
7
+ *
8
+ * Has a fixed `status` of `500` since environment errors are server-side
9
+ * configuration issues, not client errors.
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { EnvError } from '@supabase/server'
14
+ *
15
+ * try {
16
+ * const client = createAdminClient()
17
+ * } catch (e) {
18
+ * if (e instanceof EnvError) {
19
+ * console.error(`Config issue [${e.code}]: ${e.message}`)
20
+ * // → "Config issue [MISSING_SUPABASE_URL]: SUPABASE_URL is required but not set"
21
+ * }
22
+ * }
23
+ * ```
24
+ */
5
25
  var EnvError = class extends Error {
6
26
  constructor(message, code = "ENV_ERROR") {
7
27
  super(message);
@@ -10,6 +30,26 @@ var EnvError = class extends Error {
10
30
  this.code = code;
11
31
  }
12
32
  };
33
+ /**
34
+ * Thrown when authentication or authorization fails.
35
+ *
36
+ * Carries an HTTP `status` code suitable for returning directly in a response
37
+ * (typically `401` for invalid credentials, `500` for server-side auth failures).
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * import { AuthError, createSupabaseContext } from '@supabase/server'
42
+ *
43
+ * const { data: ctx, error } = await createSupabaseContext(request, { allow: 'user' })
44
+ * if (error) {
45
+ * // error is an AuthError
46
+ * return Response.json(
47
+ * { error: error.message, code: error.code },
48
+ * { status: error.status },
49
+ * )
50
+ * }
51
+ * ```
52
+ */
13
53
  var AuthError = class extends Error {
14
54
  constructor(message, code = "AUTH_ERROR", status = 401) {
15
55
  super(message);
@@ -21,10 +61,20 @@ var AuthError = class extends Error {
21
61
 
22
62
  //#endregion
23
63
  //#region src/core/resolve-env.ts
64
+ /**
65
+ * Reads an environment variable from the current runtime (Deno, Node.js, or Bun).
66
+ * Cloudflare Workers require node-compat or passing values via `overrides`.
67
+ * @internal
68
+ */
24
69
  function getEnvVar(name) {
25
70
  if (typeof Deno !== "undefined" && Deno.env?.get) return Deno.env.get(name);
26
71
  if (typeof process !== "undefined" && process.env) return process.env[name];
27
72
  }
73
+ /**
74
+ * Parses a JSON string into a `Record<string, string>` key map.
75
+ * Returns an empty object if the input is missing, malformed, or not a plain object.
76
+ * @internal
77
+ */
28
78
  function parseKeys(raw) {
29
79
  if (!raw) return {};
30
80
  try {
@@ -35,6 +85,12 @@ function parseKeys(raw) {
35
85
  return {};
36
86
  }
37
87
  }
88
+ /**
89
+ * Resolves API keys from environment variables. Checks the plural form first
90
+ * (`SUPABASE_PUBLISHABLE_KEYS` as JSON), then falls back to the singular form
91
+ * (`SUPABASE_PUBLISHABLE_KEY` stored as `{ default: "<value>" }`).
92
+ * @internal
93
+ */
38
94
  function resolveKeys(singularVar, pluralVar) {
39
95
  const plural = getEnvVar(pluralVar);
40
96
  if (plural) return parseKeys(plural);
@@ -42,6 +98,12 @@ function resolveKeys(singularVar, pluralVar) {
42
98
  if (singular) return { default: singular };
43
99
  return {};
44
100
  }
101
+ /**
102
+ * Parses a JWKS JSON string into a {@link JsonWebKeySet}.
103
+ * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
104
+ * Returns `null` if the input is missing or malformed.
105
+ * @internal
106
+ */
45
107
  function parseJwks(raw) {
46
108
  if (!raw) return null;
47
109
  try {
@@ -53,6 +115,25 @@ function parseJwks(raw) {
53
115
  return null;
54
116
  }
55
117
  }
118
+ /**
119
+ * Resolves Supabase environment configuration from runtime environment variables.
120
+ *
121
+ * Reads `SUPABASE_URL`, keys (`SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`),
122
+ * and `SUPABASE_JWKS`. Works across Deno, Node.js, and Bun. For Cloudflare Workers,
123
+ * use `overrides` or enable node-compat.
124
+ *
125
+ * @param overrides - Partial values that take precedence over env vars.
126
+ * @returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
127
+ *
128
+ * @example
129
+ * ```ts
130
+ * const { data: env, error } = resolveEnv()
131
+ * if (error) throw error
132
+ *
133
+ * // Override for tests
134
+ * const { data: env } = resolveEnv({ url: 'http://localhost:54321' })
135
+ * ```
136
+ */
56
137
  function resolveEnv(overrides) {
57
138
  const url = overrides?.url ?? getEnvVar("SUPABASE_URL");
58
139
  if (!url) return {
@@ -72,6 +153,23 @@ function resolveEnv(overrides) {
72
153
 
73
154
  //#endregion
74
155
  //#region src/core/create-admin-client.ts
156
+ /**
157
+ * Creates an admin Supabase client that bypasses Row-Level Security.
158
+ *
159
+ * Uses a secret key for authentication, giving full access to all data.
160
+ * Session persistence is disabled (stateless, one client per request).
161
+ *
162
+ * @param env - Optional environment overrides (passed through to {@link resolveEnv}).
163
+ * @param keyName - Name of the secret key to use. Falls back to `"default"`, then first available.
164
+ * @returns A configured {@link SupabaseClient} with admin (service-role) privileges.
165
+ * @throws {@link EnvError} If `SUPABASE_URL` is missing or the specified secret key is not found.
166
+ *
167
+ * @example
168
+ * ```ts
169
+ * const supabaseAdmin = createAdminClient()
170
+ * const { data } = await supabaseAdmin.from('audit_log').insert({ action: 'user_login' })
171
+ * ```
172
+ */
75
173
  function createAdminClient(env, keyName) {
76
174
  const { data: resolved, error } = resolveEnv(env);
77
175
  if (error) throw error;
@@ -88,6 +186,26 @@ function createAdminClient(env, keyName) {
88
186
 
89
187
  //#endregion
90
188
  //#region src/core/create-context-client.ts
189
+ /**
190
+ * Creates a Supabase client scoped to the caller's context.
191
+ *
192
+ * Configured with a publishable key and (optionally) the caller's JWT,
193
+ * so Row-Level Security policies apply. Session persistence is disabled
194
+ * (stateless, one client per request).
195
+ *
196
+ * @param token - The caller's JWT, or `null` for anonymous access.
197
+ * @param env - Optional environment overrides (passed through to {@link resolveEnv}).
198
+ * @param keyName - Name of the publishable key to use. Falls back to `"default"`, then first available.
199
+ * @returns A configured {@link SupabaseClient} with RLS enforced.
200
+ * @throws {@link EnvError} If `SUPABASE_URL` is missing or the specified publishable key is not found.
201
+ *
202
+ * @example
203
+ * ```ts
204
+ * const { data: auth } = await verifyAuth(request, { allow: 'user' })
205
+ * const supabase = createContextClient(auth.token)
206
+ * const { data } = await supabase.rpc('get_my_items')
207
+ * ```
208
+ */
91
209
  function createContextClient(token, env, keyName) {
92
210
  const { data: resolved, error } = resolveEnv(env);
93
211
  if (error) throw error;
@@ -107,6 +225,28 @@ function createContextClient(token, env, keyName) {
107
225
 
108
226
  //#endregion
109
227
  //#region src/core/extract-credentials.ts
228
+ /**
229
+ * Extracts authentication credentials from an incoming HTTP request.
230
+ *
231
+ * Reads two headers:
232
+ * - `Authorization: Bearer <token>` → extracted as `token`
233
+ * - `apikey: <key>` → extracted as `apikey`
234
+ *
235
+ * This is a pure extraction step — no validation or verification is performed.
236
+ * Pass the result to {@link verifyCredentials} to validate against allowed auth modes.
237
+ *
238
+ * @param request - The incoming HTTP request.
239
+ * @returns The extracted {@link Credentials}. Fields are `null` when the corresponding header is absent.
240
+ *
241
+ * @example
242
+ * ```ts
243
+ * import { extractCredentials } from '@supabase/server/core'
244
+ *
245
+ * const creds = extractCredentials(request)
246
+ * console.log(creds.token) // "eyJhbGci..." or null
247
+ * console.log(creds.apikey) // "sb-abc123-publishable-..." or null
248
+ * ```
249
+ */
110
250
  function extractCredentials(request) {
111
251
  const authHeader = request.headers.get("authorization");
112
252
  return {
@@ -118,6 +258,12 @@ function extractCredentials(request) {
118
258
  //#endregion
119
259
  //#region src/core/utils/timing-safe-equal.ts
120
260
  const encoder = new TextEncoder();
261
+ /**
262
+ * Compares two strings in constant time to prevent timing attacks.
263
+ * Uses the double-HMAC technique with a random ephemeral key.
264
+ *
265
+ * @internal
266
+ */
121
267
  async function timingSafeEqual(a, b) {
122
268
  const key = crypto.getRandomValues(new Uint8Array(32));
123
269
  const cryptoKey = await crypto.subtle.importKey("raw", key, {
@@ -135,6 +281,18 @@ async function timingSafeEqual(a, b) {
135
281
 
136
282
  //#endregion
137
283
  //#region src/core/verify-credentials.ts
284
+ /**
285
+ * Parses an {@link AllowWithKey} string into its base mode and optional key name.
286
+ *
287
+ * @example
288
+ * ```
289
+ * parseAllowMode('user') → { base: 'user', keyName: null }
290
+ * parseAllowMode('public:web') → { base: 'public', keyName: 'web' }
291
+ * parseAllowMode('secret:*') → { base: 'secret', keyName: '*' }
292
+ * ```
293
+ *
294
+ * @internal
295
+ */
138
296
  function parseAllowMode(mode) {
139
297
  if (mode === "always" || mode === "public" || mode === "secret" || mode === "user") return {
140
298
  base: mode,
@@ -152,6 +310,10 @@ function parseAllowMode(mode) {
152
310
  keyName
153
311
  };
154
312
  }
313
+ /**
314
+ * Converts raw {@link JWTClaims} (snake_case) to a normalized {@link UserClaims} (camelCase).
315
+ * @internal
316
+ */
155
317
  function claimsToUserClaims(claims) {
156
318
  return {
157
319
  id: claims.sub,
@@ -161,6 +323,11 @@ function claimsToUserClaims(claims) {
161
323
  userMetadata: claims.user_metadata
162
324
  };
163
325
  }
326
+ /**
327
+ * Attempts to authenticate credentials against a single auth mode.
328
+ * Returns the {@link AuthResult} on success, or `null` if the mode doesn't match.
329
+ * @internal
330
+ */
164
331
  async function tryMode(mode, credentials, env) {
165
332
  const { base, keyName } = parseAllowMode(mode);
166
333
  switch (base) {
@@ -240,6 +407,27 @@ async function tryMode(mode, credentials, env) {
240
407
  default: return null;
241
408
  }
242
409
  }
410
+ /**
411
+ * Verifies pre-extracted credentials against one or more allowed auth modes.
412
+ *
413
+ * Tries each mode in order — first match wins. Use {@link verifyAuth} to extract
414
+ * and verify in a single call.
415
+ *
416
+ * @param credentials - The credentials to verify (from {@link extractCredentials}).
417
+ * @param options - Allowed auth modes and optional env overrides.
418
+ * @returns `{ data: AuthResult, error: null }` on success, `{ data: null, error: AuthError }` on failure.
419
+ *
420
+ * @example
421
+ * ```ts
422
+ * const credentials = extractCredentials(request)
423
+ * const { data: auth, error } = await verifyCredentials(credentials, {
424
+ * allow: ['user', 'public'],
425
+ * })
426
+ * if (error) {
427
+ * return Response.json({ error: error.message }, { status: error.status })
428
+ * }
429
+ * ```
430
+ */
243
431
  async function verifyCredentials(credentials, options) {
244
432
  const { data: env, error: envError } = resolveEnv(options.env);
245
433
  if (envError) return {
@@ -262,6 +450,35 @@ async function verifyCredentials(credentials, options) {
262
450
 
263
451
  //#endregion
264
452
  //#region src/core/verify-auth.ts
453
+ /**
454
+ * Extracts credentials from a request and verifies them in a single step.
455
+ *
456
+ * This is a convenience function that combines {@link extractCredentials} and
457
+ * {@link verifyCredentials}. Use it when you want the full auth flow without
458
+ * needing to inspect the raw credentials.
459
+ *
460
+ * @param request - The incoming HTTP request.
461
+ * @param options - Auth modes to accept and optional environment overrides.
462
+ *
463
+ * @returns A result tuple: `{ data, error }`.
464
+ * - On success: `{ data: AuthResult, error: null }`
465
+ * - On failure: `{ data: null, error: AuthError }`
466
+ *
467
+ * @example
468
+ * ```ts
469
+ * import { verifyAuth } from '@supabase/server/core'
470
+ *
471
+ * const { data: auth, error } = await verifyAuth(request, {
472
+ * allow: 'user',
473
+ * })
474
+ *
475
+ * if (error) {
476
+ * return Response.json({ error: error.message }, { status: error.status })
477
+ * }
478
+ *
479
+ * console.log(auth.userClaims!.id) // "d0f1a2b3-..."
480
+ * ```
481
+ */
265
482
  async function verifyAuth(request, options) {
266
483
  return verifyCredentials(extractCredentials(request), options);
267
484
  }