@supabase/server 1.0.1-rc.57 → 1.1.0-rc.58

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/README.md CHANGED
@@ -400,19 +400,20 @@ export default {
400
400
 
401
401
  Automatically available in Supabase Edge Functions:
402
402
 
403
- | Variable | Format | Description |
404
- | --------------------------- | ------------------------------------------------------------- | ------------------------------------- |
405
- | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your project URL |
406
- | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_...","web":"sb_publishable_..."}` | Publishable API keys (named) |
407
- | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_...","web":"sb_secret_..."}` | Secret API keys (named) |
408
- | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification |
403
+ | Variable | Format | Description |
404
+ | --------------------------- | ------------------------------------------------------------- | -------------------------------------------- |
405
+ | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your project URL |
406
+ | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_...","web":"sb_publishable_..."}` | Publishable API keys (named) |
407
+ | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_...","web":"sb_secret_..."}` | Secret API keys (named) |
408
+ | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | Inline JSON Web Key Set for JWT verification |
409
409
 
410
410
  Also supported (for local dev, self-hosted, or other runtimes):
411
411
 
412
- | Variable | Format | Description |
413
- | -------------------------- | -------------------- | ---------------------- |
414
- | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key |
415
- | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key |
412
+ | Variable | Format | Description |
413
+ | -------------------------- | -------------------- | --------------------------------------------------------- |
414
+ | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key |
415
+ | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key |
416
+ | `SUPABASE_JWKS_URL` | `https://...` | Remote JWKS endpoint (used when `SUPABASE_JWKS` is unset) |
416
417
 
417
418
  When both singular and plural forms are set, plural takes priority.
418
419
 
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_create_supabase_context = require('../../create-supabase-context-B-2NDJhL.cjs');
2
+ const require_create_supabase_context = require('../../create-supabase-context-Bmuv6M-5.cjs');
3
3
  let h3 = require("h3");
4
4
 
5
5
  //#region src/adapters/h3/middleware.ts
@@ -1,4 +1,4 @@
1
- import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-u7fYLtzC.cjs";
1
+ import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-Bjy1j07i.cjs";
2
2
  import { Middleware } from "h3";
3
3
 
4
4
  //#region src/adapters/h3/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-B2yXZjmG.mjs";
1
+ import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-DP9l5Cvf.mjs";
2
2
  import { Middleware } from "h3";
3
3
 
4
4
  //#region src/adapters/h3/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createSupabaseContext } from "../../create-supabase-context-BBZtr3D2.mjs";
1
+ import { t as createSupabaseContext } from "../../create-supabase-context-BeZJlUBy.mjs";
2
2
  import { HTTPError, defineMiddleware } from "h3";
3
3
 
4
4
  //#region src/adapters/h3/middleware.ts
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_create_supabase_context = require('../../create-supabase-context-B-2NDJhL.cjs');
2
+ const require_create_supabase_context = require('../../create-supabase-context-Bmuv6M-5.cjs');
3
3
  let hono_http_exception = require("hono/http-exception");
4
4
  let hono_factory = require("hono/factory");
5
5
 
@@ -1,4 +1,4 @@
1
- import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-u7fYLtzC.cjs";
1
+ import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-Bjy1j07i.cjs";
2
2
  import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/adapters/hono/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-B2yXZjmG.mjs";
1
+ import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-DP9l5Cvf.mjs";
2
2
  import { MiddlewareHandler } from "hono";
3
3
 
4
4
  //#region src/adapters/hono/middleware.d.ts
@@ -1,4 +1,4 @@
1
- import { t as createSupabaseContext } from "../../create-supabase-context-BBZtr3D2.mjs";
1
+ import { t as createSupabaseContext } from "../../create-supabase-context-BeZJlUBy.mjs";
2
2
  import { HTTPException } from "hono/http-exception";
3
3
  import { createMiddleware } from "hono/factory";
4
4
 
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_verify_auth = require('../verify-auth-BKZK83Y8.cjs');
2
+ const require_verify_auth = require('../verify-auth-z-iM98dR.cjs');
3
3
 
4
4
  exports.createAdminClient = require_verify_auth.createAdminClient;
5
5
  exports.createContextClient = require_verify_auth.createContextClient;
@@ -1,4 +1,4 @@
1
- import { a as AuthResult, c as CreateContextClientOptions, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, o as ClientAuth, s as CreateAdminClientOptions } from "../types-u7fYLtzC.cjs";
1
+ import { a as AuthResult, c as CreateContextClientOptions, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, o as ClientAuth, s as CreateAdminClientOptions } from "../types-Bjy1j07i.cjs";
2
2
  import { i as EnvError, t as AuthError } from "../errors-CZFEYnV_.cjs";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
 
@@ -7,8 +7,9 @@ import { SupabaseClient } from "@supabase/supabase-js";
7
7
  * Resolves Supabase environment configuration from runtime environment variables.
8
8
  *
9
9
  * Reads `SUPABASE_URL`, keys (`SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`),
10
- * and `SUPABASE_JWKS`. Works across Deno, Node.js, and Bun. For Cloudflare Workers,
11
- * use `overrides` or enable node-compat.
10
+ * and the JWKS source (`SUPABASE_JWKS` for inline keys, or `SUPABASE_JWKS_URL`
11
+ * for a remote endpoint). Works across Deno, Node.js, and Bun. For Cloudflare
12
+ * Workers, use `overrides` or enable node-compat.
12
13
  *
13
14
  * @param overrides - Partial values that take precedence over env vars.
14
15
  * @returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
@@ -1,4 +1,4 @@
1
- import { a as AuthResult, c as CreateContextClientOptions, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, o as ClientAuth, s as CreateAdminClientOptions } from "../types-B2yXZjmG.mjs";
1
+ import { a as AuthResult, c as CreateContextClientOptions, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, o as ClientAuth, s as CreateAdminClientOptions } from "../types-DP9l5Cvf.mjs";
2
2
  import { i as EnvError, t as AuthError } from "../errors-0dbzn5gA.mjs";
3
3
  import { SupabaseClient } from "@supabase/supabase-js";
4
4
 
@@ -7,8 +7,9 @@ import { SupabaseClient } from "@supabase/supabase-js";
7
7
  * Resolves Supabase environment configuration from runtime environment variables.
8
8
  *
9
9
  * Reads `SUPABASE_URL`, keys (`SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`),
10
- * and `SUPABASE_JWKS`. Works across Deno, Node.js, and Bun. For Cloudflare Workers,
11
- * use `overrides` or enable node-compat.
10
+ * and the JWKS source (`SUPABASE_JWKS` for inline keys, or `SUPABASE_JWKS_URL`
11
+ * for a remote endpoint). Works across Deno, Node.js, and Bun. For Cloudflare
12
+ * Workers, use `overrides` or enable node-compat.
12
13
  *
13
14
  * @param overrides - Partial values that take precedence over env vars.
14
15
  * @returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
@@ -1,3 +1,3 @@
1
- import { a as createAdminClient, i as createContextClient, n as verifyCredentials, o as resolveEnv, r as extractCredentials, t as verifyAuth } from "../verify-auth-CZQd36s0.mjs";
1
+ import { a as createAdminClient, i as createContextClient, n as verifyCredentials, o as resolveEnv, r as extractCredentials, t as verifyAuth } from "../verify-auth-CZ3mB47e.mjs";
2
2
 
3
3
  export { createAdminClient, createContextClient, extractCredentials, resolveEnv, verifyAuth, verifyCredentials };
@@ -1,4 +1,4 @@
1
- import { a as createAdminClient, f as Errors, i as createContextClient, l as CreateSupabaseClientError, s as AuthError, t as verifyAuth, u as EnvError } from "./verify-auth-CZQd36s0.mjs";
1
+ import { a as createAdminClient, f as Errors, i as createContextClient, l as CreateSupabaseClientError, s as AuthError, t as verifyAuth, u as EnvError } from "./verify-auth-CZ3mB47e.mjs";
2
2
 
3
3
  //#region src/create-supabase-context.ts
4
4
  /**
@@ -1,4 +1,4 @@
1
- const require_verify_auth = require('./verify-auth-BKZK83Y8.cjs');
1
+ const require_verify_auth = require('./verify-auth-z-iM98dR.cjs');
2
2
 
3
3
  //#region src/create-supabase-context.ts
4
4
  /**
package/dist/index.cjs CHANGED
@@ -1,6 +1,6 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
2
- const require_verify_auth = require('./verify-auth-BKZK83Y8.cjs');
3
- const require_create_supabase_context = require('./create-supabase-context-B-2NDJhL.cjs');
2
+ const require_verify_auth = require('./verify-auth-z-iM98dR.cjs');
3
+ const require_create_supabase_context = require('./create-supabase-context-Bmuv6M-5.cjs');
4
4
  let _supabase_supabase_js_cors = require("@supabase/supabase-js/cors");
5
5
 
6
6
  //#region src/cors.ts
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AuthResult, c as CreateContextClientOptions, d as SupabaseContext, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, m as WithSupabaseConfig, n as AllowWithKey, o as ClientAuth, p as UserClaims, r as AuthMode, s as CreateAdminClientOptions, t as Allow, u as JWTClaims } from "./types-u7fYLtzC.cjs";
1
+ import { a as AuthResult, c as CreateContextClientOptions, d as SupabaseContext, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, m as WithSupabaseConfig, n as AllowWithKey, o as ClientAuth, p as UserClaims, r as AuthMode, s as CreateAdminClientOptions, t as Allow, u as JWTClaims } from "./types-Bjy1j07i.cjs";
2
2
  import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-CZFEYnV_.cjs";
3
3
 
4
4
  //#region src/with-supabase.d.ts
package/dist/index.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as AuthResult, c as CreateContextClientOptions, d as SupabaseContext, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, m as WithSupabaseConfig, n as AllowWithKey, o as ClientAuth, p as UserClaims, r as AuthMode, s as CreateAdminClientOptions, t as Allow, u as JWTClaims } from "./types-B2yXZjmG.mjs";
1
+ import { a as AuthResult, c as CreateContextClientOptions, d as SupabaseContext, f as SupabaseEnv, i as AuthModeWithKey, l as Credentials, m as WithSupabaseConfig, n as AllowWithKey, o as ClientAuth, p as UserClaims, r as AuthMode, s as CreateAdminClientOptions, t as Allow, u as JWTClaims } from "./types-DP9l5Cvf.mjs";
2
2
  import { a as EnvGenericError, c as MissingDefaultPublishableKeyError, d as MissingSecretKeyError, f as MissingSupabaseURLError, i as EnvError, l as MissingDefaultSecretKeyError, n as AuthGenericError, o as Errors, r as CreateSupabaseClientError, s as InvalidCredentialsError, t as AuthError, u as MissingPublishableKeyError } from "./errors-0dbzn5gA.mjs";
3
3
 
4
4
  //#region src/with-supabase.d.ts
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { _ as MissingSecretKeyError, c as AuthGenericError, d as EnvGenericError, f as Errors, g as MissingPublishableKeyError, h as MissingDefaultSecretKeyError, l as CreateSupabaseClientError, m as MissingDefaultPublishableKeyError, p as InvalidCredentialsError, s as AuthError, u as EnvError, v as MissingSupabaseURLError } from "./verify-auth-CZQd36s0.mjs";
2
- import { t as createSupabaseContext } from "./create-supabase-context-BBZtr3D2.mjs";
1
+ import { _ as MissingSecretKeyError, c as AuthGenericError, d as EnvGenericError, f as Errors, g as MissingPublishableKeyError, h as MissingDefaultSecretKeyError, l as CreateSupabaseClientError, m as MissingDefaultPublishableKeyError, p as InvalidCredentialsError, s as AuthError, u as EnvError, v as MissingSupabaseURLError } from "./verify-auth-CZ3mB47e.mjs";
2
+ import { t as createSupabaseContext } from "./create-supabase-context-BeZJlUBy.mjs";
3
3
  import { corsHeaders } from "@supabase/supabase-js/cors";
4
4
 
5
5
  //#region src/cors.ts
@@ -73,11 +73,22 @@ interface SupabaseEnv {
73
73
  */
74
74
  secretKeys: Record<string, string>;
75
75
  /**
76
- * JSON Web Key Set used for JWT verification. Sourced from `SUPABASE_JWKS`.
77
- * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
76
+ * JWKS source used for JWT verification.
77
+ *
78
+ * Sourced from one of (in priority order):
79
+ * - `SUPABASE_JWKS` — inline JSON. Resolves to a {@link JsonWebKeySet}.
80
+ * - `SUPABASE_JWKS_URL` — remote endpoint. Resolves to a {@link URL}; keys
81
+ * are fetched lazily and cached in memory (cooldown / max-age handled by
82
+ * `jose`). `https://` is always accepted; plain `http://` is accepted
83
+ * only for loopback hosts (`localhost`, `127.0.0.0/8`, `::1`) to support
84
+ * the Supabase CLI. Any other `http://` URL is rejected to prevent MITM
85
+ * swap-in of a forged signing key.
86
+ *
78
87
  * `null` when no JWKS is configured (JWT verification will be unavailable).
88
+ * Each env var is authoritative when set: a malformed value resolves to
89
+ * `null` rather than falling through to the other variable.
79
90
  */
80
- jwks: JsonWebKeySet | null;
91
+ jwks: JsonWebKeySet | URL | null;
81
92
  }
82
93
  /**
83
94
  * A JSON Web Key Set as defined by RFC 7517.
@@ -73,11 +73,22 @@ interface SupabaseEnv {
73
73
  */
74
74
  secretKeys: Record<string, string>;
75
75
  /**
76
- * JSON Web Key Set used for JWT verification. Sourced from `SUPABASE_JWKS`.
77
- * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
76
+ * JWKS source used for JWT verification.
77
+ *
78
+ * Sourced from one of (in priority order):
79
+ * - `SUPABASE_JWKS` — inline JSON. Resolves to a {@link JsonWebKeySet}.
80
+ * - `SUPABASE_JWKS_URL` — remote endpoint. Resolves to a {@link URL}; keys
81
+ * are fetched lazily and cached in memory (cooldown / max-age handled by
82
+ * `jose`). `https://` is always accepted; plain `http://` is accepted
83
+ * only for loopback hosts (`localhost`, `127.0.0.0/8`, `::1`) to support
84
+ * the Supabase CLI. Any other `http://` URL is rejected to prevent MITM
85
+ * swap-in of a forged signing key.
86
+ *
78
87
  * `null` when no JWKS is configured (JWT verification will be unavailable).
88
+ * Each env var is authoritative when set: a malformed value resolves to
89
+ * `null` rather than falling through to the other variable.
79
90
  */
80
- jwks: JsonWebKeySet | null;
91
+ jwks: JsonWebKeySet | URL | null;
81
92
  }
82
93
  /**
83
94
  * A JSON Web Key Set as defined by RFC 7517.
@@ -1,5 +1,5 @@
1
1
  import { createClient } from "@supabase/supabase-js";
2
- import { createLocalJWKSet, jwtVerify } from "jose";
2
+ import { createLocalJWKSet, createRemoteJWKSet, jwtVerify } from "jose";
3
3
 
4
4
  //#region src/errors.ts
5
5
  /**
@@ -141,9 +141,10 @@ function resolveKeys(singularVar, pluralVar) {
141
141
  return {};
142
142
  }
143
143
  /**
144
- * Parses a JWKS JSON string into a {@link JsonWebKeySet}.
145
- * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
146
- * Returns `null` if the input is missing or malformed.
144
+ * Parses an inline JWKS JSON string. Accepts `{ keys: [...] }` or a bare
145
+ * array `[...]` (wrapped as `{ keys: [...] }`). Returns `null` for missing
146
+ * or malformed input.
147
+ *
147
148
  * @internal
148
149
  */
149
150
  function parseJwks(raw) {
@@ -158,11 +159,63 @@ function parseJwks(raw) {
158
159
  }
159
160
  }
160
161
  /**
162
+ * Returns true if the hostname is a loopback address — `localhost`,
163
+ * `*.localhost`, `127.0.0.0/8`, or `::1`. Browsers treat these as secure
164
+ * contexts because traffic never leaves the machine.
165
+ *
166
+ * @internal
167
+ */
168
+ function isLoopbackHost(hostname) {
169
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
170
+ if (hostname === "[::1]") return true;
171
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) return true;
172
+ return false;
173
+ }
174
+ /**
175
+ * Parses a JWKS endpoint URL. `https://` is always accepted. Plain `http://`
176
+ * is accepted only for loopback hosts (`localhost`, `127.0.0.0/8`, `::1`) so
177
+ * the Supabase CLI flow works against `http://localhost:54321`. For any
178
+ * other host, http is rejected: a MITM on the JWKS fetch could swap in an
179
+ * attacker-controlled key and forge JWTs that pass verification. Returns
180
+ * `null` for missing or malformed input.
181
+ *
182
+ * @internal
183
+ */
184
+ function parseJwksUrl(raw) {
185
+ if (!raw) return null;
186
+ const trimmed = raw.trim();
187
+ try {
188
+ const url = new URL(trimmed);
189
+ if (url.protocol === "https:") return url;
190
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return url;
191
+ return null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+ /**
197
+ * Resolves the JWKS source from `SUPABASE_JWKS` (inline JSON) or
198
+ * `SUPABASE_JWKS_URL` (https endpoint). `SUPABASE_JWKS` wins when set;
199
+ * `SUPABASE_JWKS_URL` is only consulted if `SUPABASE_JWKS` is absent. Each
200
+ * variable is treated as authoritative — if set but malformed, the result is
201
+ * `null` and the other variable is *not* consulted as a fallback.
202
+ *
203
+ * @internal
204
+ */
205
+ function resolveJwks() {
206
+ const rawJwks = getEnvVar("SUPABASE_JWKS");
207
+ if (rawJwks && rawJwks.trim()) return parseJwks(rawJwks);
208
+ const rawJwksUrl = getEnvVar("SUPABASE_JWKS_URL");
209
+ if (rawJwksUrl && rawJwksUrl.trim()) return parseJwksUrl(rawJwksUrl);
210
+ return null;
211
+ }
212
+ /**
161
213
  * Resolves Supabase environment configuration from runtime environment variables.
162
214
  *
163
215
  * Reads `SUPABASE_URL`, keys (`SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`),
164
- * and `SUPABASE_JWKS`. Works across Deno, Node.js, and Bun. For Cloudflare Workers,
165
- * use `overrides` or enable node-compat.
216
+ * and the JWKS source (`SUPABASE_JWKS` for inline keys, or `SUPABASE_JWKS_URL`
217
+ * for a remote endpoint). Works across Deno, Node.js, and Bun. For Cloudflare
218
+ * Workers, use `overrides` or enable node-compat.
166
219
  *
167
220
  * @param overrides - Partial values that take precedence over env vars.
168
221
  * @returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
@@ -187,7 +240,7 @@ function resolveEnv(overrides) {
187
240
  url,
188
241
  publishableKeys: overrides?.publishableKeys ?? resolveKeys("SUPABASE_PUBLISHABLE_KEY", "SUPABASE_PUBLISHABLE_KEYS"),
189
242
  secretKeys: overrides?.secretKeys ?? resolveKeys("SUPABASE_SECRET_KEY", "SUPABASE_SECRET_KEYS"),
190
- jwks: overrides?.jwks ?? parseJwks(getEnvVar("SUPABASE_JWKS"))
243
+ jwks: overrides?.jwks ?? resolveJwks()
191
244
  },
192
245
  error: null
193
246
  };
@@ -420,6 +473,28 @@ function jwtClaimsToUserClaims(jwtClaims) {
420
473
  };
421
474
  }
422
475
  const INVALID = Symbol("invalid");
476
+ let remoteJwksResolver = void 0;
477
+ /**
478
+ * Returns a key resolver for the given JWKS source.
479
+ *
480
+ * For a {@link URL}, the underlying `createRemoteJWKSet` resolver is cached
481
+ * across requests so `jose`'s built-in cooldown / max-age caching is
482
+ * preserved. Local JWKS objects are wrapped on every call — they're trivially
483
+ * cheap and the object identity may change across requests.
484
+ *
485
+ * @internal
486
+ */
487
+ function getJwksResolver(jwks) {
488
+ if (jwks instanceof URL) {
489
+ const url = jwks.toString();
490
+ if (remoteJwksResolver?.url !== url) remoteJwksResolver = {
491
+ url,
492
+ resolver: createRemoteJWKSet(jwks)
493
+ };
494
+ return remoteJwksResolver.resolver;
495
+ }
496
+ return createLocalJWKSet(jwks);
497
+ }
423
498
  /**
424
499
  * Attempts to authenticate credentials against a single auth mode.
425
500
  *
@@ -492,7 +567,7 @@ async function tryMode(mode, credentials, env) {
492
567
  if (!credentials.token) return null;
493
568
  if (!env.jwks) return null;
494
569
  try {
495
- const jwkSet = createLocalJWKSet(env.jwks);
570
+ const jwkSet = getJwksResolver(env.jwks);
496
571
  const { payload } = await jwtVerify(credentials.token, jwkSet);
497
572
  if (typeof payload.sub !== "string") return INVALID;
498
573
  const jwtClaims = payload;
@@ -141,9 +141,10 @@ function resolveKeys(singularVar, pluralVar) {
141
141
  return {};
142
142
  }
143
143
  /**
144
- * Parses a JWKS JSON string into a {@link JsonWebKeySet}.
145
- * Accepts both `{ keys: [...] }` and bare `[...]` array formats.
146
- * Returns `null` if the input is missing or malformed.
144
+ * Parses an inline JWKS JSON string. Accepts `{ keys: [...] }` or a bare
145
+ * array `[...]` (wrapped as `{ keys: [...] }`). Returns `null` for missing
146
+ * or malformed input.
147
+ *
147
148
  * @internal
148
149
  */
149
150
  function parseJwks(raw) {
@@ -158,11 +159,63 @@ function parseJwks(raw) {
158
159
  }
159
160
  }
160
161
  /**
162
+ * Returns true if the hostname is a loopback address — `localhost`,
163
+ * `*.localhost`, `127.0.0.0/8`, or `::1`. Browsers treat these as secure
164
+ * contexts because traffic never leaves the machine.
165
+ *
166
+ * @internal
167
+ */
168
+ function isLoopbackHost(hostname) {
169
+ if (hostname === "localhost" || hostname.endsWith(".localhost")) return true;
170
+ if (hostname === "[::1]") return true;
171
+ if (/^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(hostname)) return true;
172
+ return false;
173
+ }
174
+ /**
175
+ * Parses a JWKS endpoint URL. `https://` is always accepted. Plain `http://`
176
+ * is accepted only for loopback hosts (`localhost`, `127.0.0.0/8`, `::1`) so
177
+ * the Supabase CLI flow works against `http://localhost:54321`. For any
178
+ * other host, http is rejected: a MITM on the JWKS fetch could swap in an
179
+ * attacker-controlled key and forge JWTs that pass verification. Returns
180
+ * `null` for missing or malformed input.
181
+ *
182
+ * @internal
183
+ */
184
+ function parseJwksUrl(raw) {
185
+ if (!raw) return null;
186
+ const trimmed = raw.trim();
187
+ try {
188
+ const url = new URL(trimmed);
189
+ if (url.protocol === "https:") return url;
190
+ if (url.protocol === "http:" && isLoopbackHost(url.hostname)) return url;
191
+ return null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+ /**
197
+ * Resolves the JWKS source from `SUPABASE_JWKS` (inline JSON) or
198
+ * `SUPABASE_JWKS_URL` (https endpoint). `SUPABASE_JWKS` wins when set;
199
+ * `SUPABASE_JWKS_URL` is only consulted if `SUPABASE_JWKS` is absent. Each
200
+ * variable is treated as authoritative — if set but malformed, the result is
201
+ * `null` and the other variable is *not* consulted as a fallback.
202
+ *
203
+ * @internal
204
+ */
205
+ function resolveJwks() {
206
+ const rawJwks = getEnvVar("SUPABASE_JWKS");
207
+ if (rawJwks && rawJwks.trim()) return parseJwks(rawJwks);
208
+ const rawJwksUrl = getEnvVar("SUPABASE_JWKS_URL");
209
+ if (rawJwksUrl && rawJwksUrl.trim()) return parseJwksUrl(rawJwksUrl);
210
+ return null;
211
+ }
212
+ /**
161
213
  * Resolves Supabase environment configuration from runtime environment variables.
162
214
  *
163
215
  * Reads `SUPABASE_URL`, keys (`SUPABASE_PUBLISHABLE_KEYS` / `SUPABASE_SECRET_KEYS`),
164
- * and `SUPABASE_JWKS`. Works across Deno, Node.js, and Bun. For Cloudflare Workers,
165
- * use `overrides` or enable node-compat.
216
+ * and the JWKS source (`SUPABASE_JWKS` for inline keys, or `SUPABASE_JWKS_URL`
217
+ * for a remote endpoint). Works across Deno, Node.js, and Bun. For Cloudflare
218
+ * Workers, use `overrides` or enable node-compat.
166
219
  *
167
220
  * @param overrides - Partial values that take precedence over env vars.
168
221
  * @returns `{ data: SupabaseEnv, error: null }` on success, `{ data: null, error: EnvError }` on failure.
@@ -187,7 +240,7 @@ function resolveEnv(overrides) {
187
240
  url,
188
241
  publishableKeys: overrides?.publishableKeys ?? resolveKeys("SUPABASE_PUBLISHABLE_KEY", "SUPABASE_PUBLISHABLE_KEYS"),
189
242
  secretKeys: overrides?.secretKeys ?? resolveKeys("SUPABASE_SECRET_KEY", "SUPABASE_SECRET_KEYS"),
190
- jwks: overrides?.jwks ?? parseJwks(getEnvVar("SUPABASE_JWKS"))
243
+ jwks: overrides?.jwks ?? resolveJwks()
191
244
  },
192
245
  error: null
193
246
  };
@@ -420,6 +473,28 @@ function jwtClaimsToUserClaims(jwtClaims) {
420
473
  };
421
474
  }
422
475
  const INVALID = Symbol("invalid");
476
+ let remoteJwksResolver = void 0;
477
+ /**
478
+ * Returns a key resolver for the given JWKS source.
479
+ *
480
+ * For a {@link URL}, the underlying `createRemoteJWKSet` resolver is cached
481
+ * across requests so `jose`'s built-in cooldown / max-age caching is
482
+ * preserved. Local JWKS objects are wrapped on every call — they're trivially
483
+ * cheap and the object identity may change across requests.
484
+ *
485
+ * @internal
486
+ */
487
+ function getJwksResolver(jwks) {
488
+ if (jwks instanceof URL) {
489
+ const url = jwks.toString();
490
+ if (remoteJwksResolver?.url !== url) remoteJwksResolver = {
491
+ url,
492
+ resolver: (0, jose.createRemoteJWKSet)(jwks)
493
+ };
494
+ return remoteJwksResolver.resolver;
495
+ }
496
+ return (0, jose.createLocalJWKSet)(jwks);
497
+ }
423
498
  /**
424
499
  * Attempts to authenticate credentials against a single auth mode.
425
500
  *
@@ -492,7 +567,7 @@ async function tryMode(mode, credentials, env) {
492
567
  if (!credentials.token) return null;
493
568
  if (!env.jwks) return null;
494
569
  try {
495
- const jwkSet = (0, jose.createLocalJWKSet)(env.jwks);
570
+ const jwkSet = getJwksResolver(env.jwks);
496
571
  const { payload } = await (0, jose.jwtVerify)(credentials.token, jwkSet);
497
572
  if (typeof payload.sub !== "string") return INVALID;
498
573
  const jwtClaims = payload;
@@ -2,25 +2,25 @@
2
2
 
3
3
  On Supabase Platform and Local Development (CLI), all variables are auto-provisioned — no configuration needed
4
4
 
5
- | Variable | Format | Description | Available in |
6
- | --------------------------- | ---------------------------------- | ------------------------------------- | --------------------------------- |
7
- | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your Supabase project URL | All |
8
- | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_..."}` | Named publishable keys as JSON object | All |
9
- | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object | All |
10
- | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | JSON Web Key Set for JWT verification | All |
11
- | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key (fallback) | Self-hosted, if manually exported |
12
- | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key (fallback) | Self-hosted, if manually exported |
5
+ | Variable | Format | Description | Available in |
6
+ | --------------------------- | ---------------------------------- | -------------------------------------------- | --------------------------------- |
7
+ | `SUPABASE_URL` | `https://<ref>.supabase.co` | Your Supabase project URL | All |
8
+ | `SUPABASE_PUBLISHABLE_KEYS` | `{"default":"sb_publishable_..."}` | Named publishable keys as JSON object | All |
9
+ | `SUPABASE_SECRET_KEYS` | `{"default":"sb_secret_..."}` | Named secret keys as JSON object | All |
10
+ | `SUPABASE_JWKS` | `{"keys":[...]}` or `[...]` | Inline JSON Web Key Set for JWT verification | All |
11
+ | `SUPABASE_PUBLISHABLE_KEY` | `sb_publishable_...` | Single publishable key (fallback) | Self-hosted, if manually exported |
12
+ | `SUPABASE_SECRET_KEY` | `sb_secret_...` | Single secret key (fallback) | Self-hosted, if manually exported |
13
13
 
14
14
  ## Non-Supabase environments (Node.js, Bun, Cloudflare, self-hosted)
15
15
 
16
16
  Set these based on which auth modes your app uses:
17
17
 
18
- | Variable | Required when |
19
- | -------------------------- | ----------------------------------------- |
20
- | `SUPABASE_URL` | Always |
21
- | `SUPABASE_SECRET_KEY` | `auth: 'secret'` or using `supabaseAdmin` |
22
- | `SUPABASE_PUBLISHABLE_KEY` | `auth: 'publishable'` |
23
- | `SUPABASE_JWKS` | `auth: 'user'` (JWT verification) |
18
+ | Variable | Required when |
19
+ | -------------------------------------- | ----------------------------------------- |
20
+ | `SUPABASE_URL` | Always |
21
+ | `SUPABASE_SECRET_KEY` | `auth: 'secret'` or using `supabaseAdmin` |
22
+ | `SUPABASE_PUBLISHABLE_KEY` | `auth: 'publishable'` |
23
+ | `SUPABASE_JWKS` or `SUPABASE_JWKS_URL` | `auth: 'user'` (JWT verification) |
24
24
 
25
25
  ### Minimal `.env` example
26
26
 
@@ -75,19 +75,34 @@ The singular form is a convenience for the common case where you only have one k
75
75
 
76
76
  When both singular and plural forms are set, the plural form takes priority.
77
77
 
78
- ## JWKS format
78
+ ## JWKS source
79
79
 
80
- `SUPABASE_JWKS` accepts two formats:
80
+ JWT verification (`auth: 'user'`) needs a JWKS. There are two ways to provide one:
81
81
 
82
82
  ```
83
- # Standard JWKS format
83
+ # Inline JSON — standard JWKS format
84
84
  SUPABASE_JWKS={"keys":[{"kty":"RSA","n":"...","e":"AQAB"}]}
85
85
 
86
- # Bare array (convenience)
86
+ # Inline JSON — bare array (convenience, wrapped as { keys: [...] })
87
87
  SUPABASE_JWKS=[{"kty":"RSA","n":"...","e":"AQAB"}]
88
+
89
+ # Remote JWKS endpoint — keys are fetched on demand and cached in memory.
90
+ # HTTPS is required for any non-loopback host; plain http:// is rejected
91
+ # (a MITM on the JWKS fetch could swap in an attacker-controlled key and
92
+ # forge JWTs that verify). http:// is allowed for loopback hosts only —
93
+ # `localhost`, `127.0.0.0/8`, `::1` — to support the local Supabase CLI.
94
+ SUPABASE_JWKS_URL=https://<ref>.supabase.co/auth/v1/.well-known/jwks.json
95
+
96
+ # Local development against `supabase start`:
97
+ SUPABASE_JWKS_URL=http://localhost:54321/auth/v1/.well-known/jwks.json
88
98
  ```
89
99
 
90
- When `SUPABASE_JWKS` is not set, JWT verification (`auth: 'user'`) is unavailable.
100
+ ### Resolution order
101
+
102
+ 1. `SUPABASE_JWKS` — when set, treated as authoritative inline JSON.
103
+ 2. `SUPABASE_JWKS_URL` — only checked when `SUPABASE_JWKS` is unset or empty.
104
+ Must be `https://`, except loopback hosts may use `http://`.
105
+ 3. Otherwise — `null`. JWT verification (`auth: 'user'`) is unavailable.
91
106
 
92
107
  ## Runtime-specific behavior
93
108
 
@@ -176,7 +191,8 @@ interface SupabaseEnv {
176
191
  url: string
177
192
  publishableKeys: Record<string, string>
178
193
  secretKeys: Record<string, string>
179
- jwks: JsonWebKeySet | null
194
+ // `URL` when SUPABASE_JWKS is a remote endpoint, `JsonWebKeySet` for inline keys
195
+ jwks: JsonWebKeySet | URL | null
180
196
  }
181
197
  ```
182
198
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@supabase/server",
3
- "version": "1.0.1-rc.57",
3
+ "version": "1.1.0-rc.58",
4
4
  "description": "Server-side utilities for Supabase. Handles auth, client creation, and context injection so you write business logic, not boilerplate.",
5
5
  "keywords": [
6
6
  "edge",