@supabase/server 0.1.0-alpha.1 → 0.1.1-rc.25
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 +2 -2
- package/dist/core/index.d.mts +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/create-admin-client-Cp7FxI6O.d.cts +271 -0
- package/dist/create-admin-client-ZTnl1zMe.d.mts +271 -0
- 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/index.cjs +45 -2
- package/dist/index.d.cts +48 -5
- package/dist/index.d.mts +48 -5
- package/dist/index.mjs +45 -2
- 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-BZ_3qcxI.d.cts +0 -60
- package/dist/create-admin-client-CSX-Q_Fv.d.mts +0 -60
- package/dist/types-BLM5-qA8.d.mts +0 -59
- package/dist/types-DNh3Z1O1.d.cts +0 -59
|
@@ -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
|
}
|
|
@@ -2,6 +2,26 @@ let _supabase_supabase_js = require("@supabase/supabase-js");
|
|
|
2
2
|
let jose = require("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
|
}
|
package/dist/wrappers/index.cjs
CHANGED
|
@@ -2,6 +2,25 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
|
2
2
|
|
|
3
3
|
//#region src/wrappers/webhook.ts
|
|
4
4
|
const encoder = new TextEncoder();
|
|
5
|
+
/**
|
|
6
|
+
* Verifies a webhook signature using HMAC-SHA256 with timing-safe comparison.
|
|
7
|
+
*
|
|
8
|
+
* @param payload - The raw request body as a string.
|
|
9
|
+
* @param signature - The hex-encoded signature from the webhook header.
|
|
10
|
+
* @param secret - The shared secret used to sign webhooks.
|
|
11
|
+
* @returns `true` if the signature is valid, `false` otherwise.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* const payload = await req.text()
|
|
16
|
+
* const signature = req.headers.get('x-webhook-signature') ?? ''
|
|
17
|
+
*
|
|
18
|
+
* const isValid = await verifyWebhookSignature(payload, signature, secret)
|
|
19
|
+
* if (!isValid) {
|
|
20
|
+
* return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
5
24
|
async function verifyWebhookSignature(payload, signature, secret) {
|
|
6
25
|
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
7
26
|
name: "HMAC",
|
|
@@ -1,4 +1,23 @@
|
|
|
1
1
|
//#region src/wrappers/webhook.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Verifies a webhook signature using HMAC-SHA256 with timing-safe comparison.
|
|
4
|
+
*
|
|
5
|
+
* @param payload - The raw request body as a string.
|
|
6
|
+
* @param signature - The hex-encoded signature from the webhook header.
|
|
7
|
+
* @param secret - The shared secret used to sign webhooks.
|
|
8
|
+
* @returns `true` if the signature is valid, `false` otherwise.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* const payload = await req.text()
|
|
13
|
+
* const signature = req.headers.get('x-webhook-signature') ?? ''
|
|
14
|
+
*
|
|
15
|
+
* const isValid = await verifyWebhookSignature(payload, signature, secret)
|
|
16
|
+
* if (!isValid) {
|
|
17
|
+
* return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
2
21
|
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
3
22
|
//#endregion
|
|
4
23
|
export { verifyWebhookSignature };
|