@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.
- package/dist/adapters/hono/index.cjs +27 -1
- package/dist/adapters/hono/index.d.cts +27 -1
- package/dist/adapters/hono/index.d.mts +27 -1
- package/dist/adapters/hono/index.mjs +27 -1
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.d.cts +194 -2
- package/dist/core/index.d.mts +194 -2
- package/dist/core/index.mjs +1 -1
- package/dist/{create-supabase-context-BrSIe29v.mjs → create-supabase-context-CmWaH3s6.mjs} +21 -1
- package/dist/{create-supabase-context-DNWor6i_.cjs → create-supabase-context-DcVorGKG.cjs} +21 -1
- package/dist/errors-5ivL23qo.d.mts +78 -0
- package/dist/errors-BmSsOAvx.d.cts +78 -0
- package/dist/index.cjs +45 -8
- package/dist/index.d.cts +46 -3
- package/dist/index.d.mts +46 -3
- package/dist/index.mjs +46 -3
- package/dist/types-ClmJ8pi8.d.mts +227 -0
- package/dist/types-CnKoFCMX.d.cts +227 -0
- package/dist/{verify-auth-DxUT0XoT.mjs → verify-auth-2S7zFfR-.mjs} +217 -0
- package/dist/{verify-auth-6a1UPrFz.cjs → verify-auth-DvRVnjdq.cjs} +217 -0
- package/dist/wrappers/index.cjs +19 -0
- package/dist/wrappers/index.d.cts +19 -0
- package/dist/wrappers/index.d.mts +19 -0
- package/dist/wrappers/index.mjs +19 -0
- package/package.json +1 -1
- package/dist/create-admin-client-BQEHqeFW.d.mts +0 -60
- package/dist/create-admin-client-CnuiHPpV.d.cts +0 -60
- package/dist/types-C9J9JcPD.d.cts +0 -59
- package/dist/types-Ceetds5F.d.mts +0 -59
|
@@ -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
|
}
|