@supabase/server 0.1.4 → 0.2.0-rc.45
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 +146 -50
- package/dist/adapters/h3/index.cjs +59 -0
- package/dist/adapters/h3/index.d.cts +51 -0
- package/dist/adapters/h3/index.d.mts +51 -0
- package/dist/adapters/h3/index.mjs +58 -0
- package/dist/adapters/hono/index.cjs +1 -1
- package/dist/adapters/hono/index.d.cts +4 -4
- package/dist/adapters/hono/index.d.mts +4 -4
- package/dist/adapters/hono/index.mjs +1 -1
- package/dist/core/index.cjs +1 -1
- package/dist/core/index.d.cts +7 -4
- package/dist/core/index.d.mts +7 -4
- package/dist/core/index.mjs +1 -1
- package/dist/{create-supabase-context--VqMJpDu.cjs → create-supabase-context-C_8SbO5w.cjs} +1 -1
- package/dist/{create-supabase-context-B3Uzt_3I.mjs → create-supabase-context-DXD5rxi1.mjs} +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.cts +2 -2
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/{types-CbC-wBUe.d.mts → types-DKe8uOwI.d.mts} +6 -1
- package/dist/{types-DxTr0Qum.d.cts → types-DqhOaSlC.d.cts} +6 -1
- package/dist/{verify-auth-DrgvEuKo.cjs → verify-auth-C4zqDlfj.cjs} +18 -5
- package/dist/{verify-auth-Bt2uGltH.mjs → verify-auth-CxFZy9rl.mjs} +18 -5
- package/docs/api-reference.md +11 -11
- package/docs/auth-modes.md +3 -1
- package/docs/error-handling.md +5 -5
- package/docs/security.md +3 -1
- package/package.json +12 -2
- package/skills/supabase-server/SKILL.md +5 -0
- /package/dist/{errors-CAH-RRA3.d.mts → errors-Dyj5Cjt6.d.cts} +0 -0
- /package/dist/{errors-O2ugIMec.d.cts → errors-m42mkqhD.d.mts} +0 -0
package/README.md
CHANGED
|
@@ -4,32 +4,38 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@supabase/server)
|
|
5
5
|
[](https://pkg.pr.new/~/supabase/server)
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
> **Beta:** This package is under active development. APIs and documentation may change. If you find a bug or have a feature request, please [open an issue](https://github.com/supabase/server/issues) or [submit a PR](https://github.com/supabase/server/blob/main/CONTRIBUTING.md).
|
|
8
|
+
|
|
9
|
+
`@supabase/server` gives you batteries included access to the
|
|
10
|
+
[supabase-js SDK](https://github.com/supabase/supabase-js), including client
|
|
11
|
+
creation and authentication automatically scoped to the inbound requests to your
|
|
12
|
+
Edge Functions and APIs.
|
|
8
13
|
|
|
9
14
|
```ts
|
|
10
15
|
import { withSupabase } from '@supabase/server'
|
|
11
16
|
|
|
12
17
|
export default {
|
|
13
18
|
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
// RLS-scoped — this user only sees their own favorites
|
|
20
|
+
const { data: myGames } = await ctx.supabase.from('favorite_games').select()
|
|
21
|
+
return Response.json(myGames)
|
|
16
22
|
}),
|
|
17
23
|
}
|
|
18
24
|
```
|
|
19
25
|
|
|
20
|
-
One import. One line of config. Auth is validated, clients are
|
|
26
|
+
One import. One line of config. Auth is validated, clients are ready, CORS is handled. Your handler only runs on successful auth.
|
|
21
27
|
|
|
22
28
|
## Installation
|
|
23
29
|
|
|
24
30
|
```bash
|
|
31
|
+
# Deno / Supabase Edge Functions (no install — import directly)
|
|
32
|
+
import { withSupabase } from "npm:@supabase/server";
|
|
33
|
+
|
|
25
34
|
# npm
|
|
26
35
|
npm install @supabase/server
|
|
27
36
|
|
|
28
37
|
# pnpm
|
|
29
38
|
pnpm add @supabase/server
|
|
30
|
-
|
|
31
|
-
# Deno / Supabase Edge Functions (no install — import directly)
|
|
32
|
-
import { withSupabase } from "npm:@supabase/server";
|
|
33
39
|
```
|
|
34
40
|
|
|
35
41
|
### AI coding skills
|
|
@@ -42,19 +48,24 @@ npx skills add supabase/server
|
|
|
42
48
|
|
|
43
49
|
## Quick Start
|
|
44
50
|
|
|
51
|
+
Imagine you're building an app where users track their favorite games. They sign in and manage their own list. An admin dashboard curates featured titles. A cron job refreshes the "popular this week" rankings. Here's how each piece looks:
|
|
52
|
+
|
|
45
53
|
### Authenticated endpoint
|
|
46
54
|
|
|
47
55
|
```ts
|
|
56
|
+
// A signed-in user fetches their favorite games.
|
|
48
57
|
export default {
|
|
49
58
|
fetch: withSupabase({ allow: 'user' }, async (_req, ctx) => {
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
59
|
+
const { supabase, supabaseAdmin, userClaims, claims, authType } = ctx
|
|
60
|
+
// supabase — RLS-scoped to the authenticated user
|
|
61
|
+
// supabaseAdmin — bypasses RLS (service role)
|
|
62
|
+
// userClaims — user identity from JWT (id, email, role)
|
|
63
|
+
// claims — full JWT claims
|
|
64
|
+
// authType — which auth mode matched
|
|
65
|
+
|
|
66
|
+
// RLS-scoped — this user only sees their own favorites
|
|
67
|
+
const { data: myGames } = await supabase.from('favorite_games').select()
|
|
68
|
+
return Response.json(myGames)
|
|
58
69
|
}),
|
|
59
70
|
}
|
|
60
71
|
```
|
|
@@ -62,6 +73,8 @@ export default {
|
|
|
62
73
|
### Public endpoint (no auth)
|
|
63
74
|
|
|
64
75
|
```ts
|
|
76
|
+
// The frontend hits this before showing the login screen.
|
|
77
|
+
// allow: 'always' means no credentials required.
|
|
65
78
|
export default {
|
|
66
79
|
fetch: withSupabase({ allow: 'always' }, async (_req, _ctx) => {
|
|
67
80
|
return Response.json({ status: 'ok' })
|
|
@@ -72,10 +85,14 @@ export default {
|
|
|
72
85
|
### API key protected
|
|
73
86
|
|
|
74
87
|
```ts
|
|
88
|
+
// An admin dashboard fetches the list of featured games to curate.
|
|
89
|
+
// Secret key auth (not a user JWT) — supabaseAdmin bypasses RLS.
|
|
75
90
|
export default {
|
|
76
91
|
fetch: withSupabase({ allow: 'secret' }, async (_req, ctx) => {
|
|
77
|
-
const { data } = await ctx.supabaseAdmin
|
|
78
|
-
|
|
92
|
+
const { data: featuredGames } = await ctx.supabaseAdmin
|
|
93
|
+
.from('featured_games')
|
|
94
|
+
.select()
|
|
95
|
+
return Response.json(featuredGames)
|
|
79
96
|
}),
|
|
80
97
|
}
|
|
81
98
|
```
|
|
@@ -83,14 +100,25 @@ export default {
|
|
|
83
100
|
### Dual auth (user or service)
|
|
84
101
|
|
|
85
102
|
```ts
|
|
103
|
+
// Users view their own play stats from the app (JWT).
|
|
104
|
+
// A backend service pulls stats for any user (secret key + user_id in body).
|
|
86
105
|
export default {
|
|
87
106
|
fetch: withSupabase({ allow: ['user', 'secret'] }, async (req, ctx) => {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
const callerIsUser = ctx.authType === 'user'
|
|
108
|
+
|
|
109
|
+
if (callerIsUser) {
|
|
110
|
+
// RLS-scoped — the database enforces "own stats only"
|
|
111
|
+
const { data: myStats } = await ctx.supabase.from('play_stats').select()
|
|
112
|
+
return Response.json(myStats)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Service path — bypass RLS to pull stats for any user
|
|
116
|
+
const { user_id } = await req.json()
|
|
117
|
+
const { data: playStats } = await ctx.supabaseAdmin
|
|
118
|
+
.from('play_stats')
|
|
91
119
|
.select()
|
|
92
|
-
.eq('user_id',
|
|
93
|
-
return Response.json(
|
|
120
|
+
.eq('user_id', user_id)
|
|
121
|
+
return Response.json(playStats)
|
|
94
122
|
}),
|
|
95
123
|
}
|
|
96
124
|
```
|
|
@@ -98,28 +126,35 @@ export default {
|
|
|
98
126
|
### Server-to-server
|
|
99
127
|
|
|
100
128
|
```ts
|
|
101
|
-
//
|
|
129
|
+
// A cron job refreshes the "popular this week" list every hour.
|
|
130
|
+
// Named key ("cron") so it can be rotated without touching other services.
|
|
102
131
|
export default {
|
|
103
|
-
fetch: withSupabase({ allow: 'secret:
|
|
104
|
-
const
|
|
105
|
-
const { data } = await ctx.supabaseAdmin
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
132
|
+
fetch: withSupabase({ allow: 'secret:cron' }, async (_req, ctx) => {
|
|
133
|
+
const oneWeekAgo = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000)
|
|
134
|
+
const { data: popularThisWeek } = await ctx.supabaseAdmin.rpc(
|
|
135
|
+
'get_most_favorited_since',
|
|
136
|
+
{ since: oneWeekAgo.toISOString(), limit_count: 10 },
|
|
137
|
+
)
|
|
138
|
+
await ctx.supabaseAdmin
|
|
139
|
+
.from('featured_games')
|
|
140
|
+
.upsert(
|
|
141
|
+
popularThisWeek.map((g) => ({ game_id: g.id, reason: 'popular' })),
|
|
142
|
+
)
|
|
143
|
+
return Response.json({ popularThisWeek })
|
|
109
144
|
}),
|
|
110
145
|
}
|
|
111
146
|
```
|
|
112
147
|
|
|
113
|
-
The
|
|
148
|
+
The cron job sends the named secret key in the `apikey` header:
|
|
114
149
|
|
|
115
150
|
```ts
|
|
116
|
-
|
|
151
|
+
const refreshEndpoint =
|
|
152
|
+
'https://<project>.supabase.co/functions/v1/refresh-popular'
|
|
153
|
+
const cronKey = 'sb_secret_...' // the "cron" named secret key
|
|
154
|
+
|
|
155
|
+
await fetch(refreshEndpoint, {
|
|
117
156
|
method: 'POST',
|
|
118
|
-
headers: {
|
|
119
|
-
'Content-Type': 'application/json',
|
|
120
|
-
apikey: 'sb_secret_...', // the "automations" secret key
|
|
121
|
-
},
|
|
122
|
-
body: JSON.stringify({ taskName: 'cleanup' }),
|
|
157
|
+
headers: { apikey: cronKey },
|
|
123
158
|
})
|
|
124
159
|
```
|
|
125
160
|
|
|
@@ -132,7 +167,7 @@ await fetch('https://<project>.supabase.co/functions/v1/my-function', {
|
|
|
132
167
|
| `"secret"` | Valid secret key | Server-to-server, internal calls |
|
|
133
168
|
| `"always"` | None | Open endpoints, wrappers that handle their own auth |
|
|
134
169
|
|
|
135
|
-
Array syntax (`allow: ["user", "secret"]`) accepts multiple auth methods — first match wins.
|
|
170
|
+
Array syntax (`allow: ["user", "secret"]`) accepts multiple auth methods — first match wins. An absent credential falls through to the next mode; a present-but-invalid JWT rejects the request (no silent downgrade). See [`docs/auth-modes.md`](docs/auth-modes.md).
|
|
136
171
|
|
|
137
172
|
Named key validation: `allow: "public:web_app"` or `allow: "secret:automations"` validates against a specific named key in `SUPABASE_PUBLISHABLE_KEYS` or `SUPABASE_SECRET_KEYS`.
|
|
138
173
|
|
|
@@ -154,6 +189,7 @@ interface SupabaseContext {
|
|
|
154
189
|
userClaims: UserClaims | null // JWT-derived identity (for full User, call supabase.auth.getUser())
|
|
155
190
|
claims: JWTClaims | null // Present when auth is JWT
|
|
156
191
|
authType: Allow // Which auth mode matched
|
|
192
|
+
authKeyName?: string | null // Auth key name of the API key that was used for this request
|
|
157
193
|
}
|
|
158
194
|
```
|
|
159
195
|
|
|
@@ -201,12 +237,14 @@ import { withSupabase } from '@supabase/server/adapters/hono'
|
|
|
201
237
|
|
|
202
238
|
const app = new Hono()
|
|
203
239
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
const {
|
|
207
|
-
|
|
240
|
+
// Protected — withSupabase middleware validates the JWT before the handler runs
|
|
241
|
+
app.get('/games', withSupabase({ allow: 'user' }), async (c) => {
|
|
242
|
+
const { supabase } = c.var.supabaseContext
|
|
243
|
+
const { data: myGames } = await supabase.from('favorite_games').select()
|
|
244
|
+
return c.json(myGames)
|
|
208
245
|
})
|
|
209
246
|
|
|
247
|
+
// Public — no middleware means no auth
|
|
210
248
|
app.get('/health', (c) => c.json({ status: 'ok' }))
|
|
211
249
|
|
|
212
250
|
export default { fetch: app.fetch }
|
|
@@ -214,6 +252,56 @@ export default { fetch: app.fetch }
|
|
|
214
252
|
|
|
215
253
|
The adapter does not handle CORS — use `hono/cors` for that. Per-route auth works naturally by applying the middleware to specific routes.
|
|
216
254
|
|
|
255
|
+
### H3 / Nuxt
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { H3 } from 'h3'
|
|
259
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
260
|
+
|
|
261
|
+
const app = new H3()
|
|
262
|
+
|
|
263
|
+
// Protected — withSupabase validates the JWT before the handler runs
|
|
264
|
+
app.use(withSupabase({ allow: 'user' }))
|
|
265
|
+
|
|
266
|
+
app.get('/games', async (event) => {
|
|
267
|
+
const { supabase } = event.context.supabaseContext
|
|
268
|
+
const { data: myGames } = await supabase.from('favorite_games').select()
|
|
269
|
+
return myGames
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Public — no middleware means no auth
|
|
273
|
+
app.get('/health', () => ({ status: 'ok' }))
|
|
274
|
+
|
|
275
|
+
export default { fetch: app.fetch }
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
For **Nuxt**, use `defineHandler` for file routes:
|
|
279
|
+
|
|
280
|
+
```ts
|
|
281
|
+
// server/api/games.get.ts
|
|
282
|
+
import { defineHandler } from 'h3'
|
|
283
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
284
|
+
|
|
285
|
+
export default defineHandler({
|
|
286
|
+
middleware: [withSupabase({ allow: 'user' })],
|
|
287
|
+
handler: async (event) => {
|
|
288
|
+
const { supabase } = event.context.supabaseContext
|
|
289
|
+
return supabase.from('favorite_games').select()
|
|
290
|
+
},
|
|
291
|
+
})
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
For app-wide auth, register it as a server middleware:
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// server/middleware/supabase.ts
|
|
298
|
+
import { withSupabase } from '@supabase/server/adapters/h3'
|
|
299
|
+
|
|
300
|
+
export default withSupabase({ allow: 'user' })
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The adapter does not handle CORS — use H3's CORS utilities for that.
|
|
304
|
+
|
|
217
305
|
## Primitives
|
|
218
306
|
|
|
219
307
|
For when you need more control than `withSupabase` provides — multiple routes with different auth, custom response headers, or building your own wrapper.
|
|
@@ -253,9 +341,9 @@ const { data: auth, error } = await verifyCredentials(credentials, {
|
|
|
253
341
|
### createContextClient / createAdminClient
|
|
254
342
|
|
|
255
343
|
```ts
|
|
256
|
-
const
|
|
257
|
-
const
|
|
258
|
-
const
|
|
344
|
+
const userScopedClient = createContextClient(auth.token) // RLS applies as this user
|
|
345
|
+
const anonClient = createContextClient() // RLS applies as anon role
|
|
346
|
+
const adminClient = createAdminClient() // bypasses RLS entirely
|
|
259
347
|
```
|
|
260
348
|
|
|
261
349
|
### createSupabaseContext
|
|
@@ -278,6 +366,8 @@ const { data: env, error } = resolveEnv({
|
|
|
278
366
|
|
|
279
367
|
### Example: custom multi-route handler
|
|
280
368
|
|
|
369
|
+
The same games API and health check from the Hono example, built from primitives instead of a framework:
|
|
370
|
+
|
|
281
371
|
```ts
|
|
282
372
|
import { verifyAuth, createContextClient } from '@supabase/server/core'
|
|
283
373
|
|
|
@@ -285,11 +375,13 @@ export default {
|
|
|
285
375
|
fetch: async (req) => {
|
|
286
376
|
const url = new URL(req.url)
|
|
287
377
|
|
|
378
|
+
// Public — no auth needed
|
|
288
379
|
if (url.pathname === '/health') {
|
|
289
380
|
return Response.json({ status: 'ok' })
|
|
290
381
|
}
|
|
291
382
|
|
|
292
|
-
|
|
383
|
+
// Protected — verify the JWT, then create a user-scoped client
|
|
384
|
+
if (url.pathname === '/games') {
|
|
293
385
|
const { data: auth, error } = await verifyAuth(req, { allow: 'user' })
|
|
294
386
|
if (error)
|
|
295
387
|
return Response.json(
|
|
@@ -297,9 +389,11 @@ export default {
|
|
|
297
389
|
{ status: error.status },
|
|
298
390
|
)
|
|
299
391
|
|
|
300
|
-
const
|
|
301
|
-
const { data } = await
|
|
302
|
-
|
|
392
|
+
const userScopedClient = createContextClient(auth.token)
|
|
393
|
+
const { data: myGames } = await userScopedClient
|
|
394
|
+
.from('favorite_games')
|
|
395
|
+
.select()
|
|
396
|
+
return Response.json(myGames)
|
|
303
397
|
}
|
|
304
398
|
|
|
305
399
|
return new Response('Not found', { status: 404 })
|
|
@@ -333,9 +427,10 @@ For other environments, pass overrides via the `env` config option or `resolveEn
|
|
|
333
427
|
|
|
334
428
|
- **Supabase Edge Functions** — environment variables are auto-injected. Zero config.
|
|
335
429
|
- **Deno / Bun** — works out of the box with the `export default { fetch }` pattern.
|
|
336
|
-
- **Node.js** — use the [Hono adapter](#hono) or [core primitives](#primitives) with your framework of choice.
|
|
430
|
+
- **Node.js** — use the [Hono adapter](#hono), [H3 adapter](#h3--nuxt), or [core primitives](#primitives) with your framework of choice.
|
|
337
431
|
- **Cloudflare Workers** — enable `nodejs_compat` in `wrangler.toml` or pass env overrides via the `env` config option.
|
|
338
|
-
- **
|
|
432
|
+
- **Nuxt** — use the [H3 adapter](#h3--nuxt) directly as a server middleware.
|
|
433
|
+
- **Next.js / SvelteKit / Remix** — use core primitives to build a cookie-based auth adapter. See [`docs/ssr-frameworks.md`](docs/ssr-frameworks.md).
|
|
339
434
|
|
|
340
435
|
## Exports
|
|
341
436
|
|
|
@@ -344,6 +439,7 @@ For other environments, pass overrides via the `env` config option or `resolveEn
|
|
|
344
439
|
| `@supabase/server` | `withSupabase`, `createSupabaseContext` |
|
|
345
440
|
| `@supabase/server/core` | `verifyAuth`, `verifyCredentials`, `extractCredentials`, `createContextClient`, `createAdminClient`, `resolveEnv` |
|
|
346
441
|
| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
|
|
442
|
+
| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
|
|
347
443
|
|
|
348
444
|
## Documentation
|
|
349
445
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_create_supabase_context = require('../../create-supabase-context-C_8SbO5w.cjs');
|
|
3
|
+
let h3 = require("h3");
|
|
4
|
+
|
|
5
|
+
//#region src/adapters/h3/middleware.ts
|
|
6
|
+
/**
|
|
7
|
+
* H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
|
|
8
|
+
*
|
|
9
|
+
* Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
|
|
10
|
+
* Throws an `HTTPError` on auth failure.
|
|
11
|
+
*
|
|
12
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
|
|
13
|
+
* @returns An H3 middleware.
|
|
14
|
+
*
|
|
15
|
+
* @example App-wide auth via `app.use()`
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { H3 } from 'h3'
|
|
18
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
19
|
+
*
|
|
20
|
+
* const app = new H3()
|
|
21
|
+
* app.use(withSupabase({ allow: 'user' }))
|
|
22
|
+
*
|
|
23
|
+
* app.get('/games', async (event) => {
|
|
24
|
+
* const { supabase } = event.context.supabaseContext
|
|
25
|
+
* return supabase.from('favorite_games').select()
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* export default { fetch: app.fetch }
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example Per-route auth via `defineHandler`
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { defineHandler } from 'h3'
|
|
34
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
35
|
+
*
|
|
36
|
+
* export default defineHandler({
|
|
37
|
+
* middleware: [withSupabase({ allow: 'user' })],
|
|
38
|
+
* handler: async (event) => {
|
|
39
|
+
* const { supabase } = event.context.supabaseContext
|
|
40
|
+
* return supabase.from('favorite_games').select()
|
|
41
|
+
* },
|
|
42
|
+
* })
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
function withSupabase(config) {
|
|
46
|
+
return (0, h3.defineMiddleware)(async (event, next) => {
|
|
47
|
+
if (event.context.supabaseContext) return next();
|
|
48
|
+
const { data: ctx, error } = await require_create_supabase_context.createSupabaseContext(event.req, config);
|
|
49
|
+
if (error) throw new h3.HTTPError(error.message, {
|
|
50
|
+
status: error.status,
|
|
51
|
+
cause: error
|
|
52
|
+
});
|
|
53
|
+
event.context.supabaseContext = ctx;
|
|
54
|
+
return next();
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
//#endregion
|
|
59
|
+
exports.withSupabase = withSupabase;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DqhOaSlC.cjs";
|
|
2
|
+
import { Middleware } from "h3";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/h3/middleware.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
|
|
7
|
+
*
|
|
8
|
+
* Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
|
|
9
|
+
* Throws an `HTTPError` on auth failure.
|
|
10
|
+
*
|
|
11
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
|
|
12
|
+
* @returns An H3 middleware.
|
|
13
|
+
*
|
|
14
|
+
* @example App-wide auth via `app.use()`
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { H3 } from 'h3'
|
|
17
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
18
|
+
*
|
|
19
|
+
* const app = new H3()
|
|
20
|
+
* app.use(withSupabase({ allow: 'user' }))
|
|
21
|
+
*
|
|
22
|
+
* app.get('/games', async (event) => {
|
|
23
|
+
* const { supabase } = event.context.supabaseContext
|
|
24
|
+
* return supabase.from('favorite_games').select()
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* export default { fetch: app.fetch }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example Per-route auth via `defineHandler`
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { defineHandler } from 'h3'
|
|
33
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
34
|
+
*
|
|
35
|
+
* export default defineHandler({
|
|
36
|
+
* middleware: [withSupabase({ allow: 'user' })],
|
|
37
|
+
* handler: async (event) => {
|
|
38
|
+
* const { supabase } = event.context.supabaseContext
|
|
39
|
+
* return supabase.from('favorite_games').select()
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware;
|
|
45
|
+
declare module 'h3' {
|
|
46
|
+
interface H3EventContext {
|
|
47
|
+
supabaseContext: SupabaseContext;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { withSupabase };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DKe8uOwI.mjs";
|
|
2
|
+
import { Middleware } from "h3";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/h3/middleware.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
|
|
7
|
+
*
|
|
8
|
+
* Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
|
|
9
|
+
* Throws an `HTTPError` on auth failure.
|
|
10
|
+
*
|
|
11
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
|
|
12
|
+
* @returns An H3 middleware.
|
|
13
|
+
*
|
|
14
|
+
* @example App-wide auth via `app.use()`
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { H3 } from 'h3'
|
|
17
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
18
|
+
*
|
|
19
|
+
* const app = new H3()
|
|
20
|
+
* app.use(withSupabase({ allow: 'user' }))
|
|
21
|
+
*
|
|
22
|
+
* app.get('/games', async (event) => {
|
|
23
|
+
* const { supabase } = event.context.supabaseContext
|
|
24
|
+
* return supabase.from('favorite_games').select()
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* export default { fetch: app.fetch }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example Per-route auth via `defineHandler`
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { defineHandler } from 'h3'
|
|
33
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
34
|
+
*
|
|
35
|
+
* export default defineHandler({
|
|
36
|
+
* middleware: [withSupabase({ allow: 'user' })],
|
|
37
|
+
* handler: async (event) => {
|
|
38
|
+
* const { supabase } = event.context.supabaseContext
|
|
39
|
+
* return supabase.from('favorite_games').select()
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Middleware;
|
|
45
|
+
declare module 'h3' {
|
|
46
|
+
interface H3EventContext {
|
|
47
|
+
supabaseContext: SupabaseContext;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { withSupabase };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { t as createSupabaseContext } from "../../create-supabase-context-DXD5rxi1.mjs";
|
|
2
|
+
import { HTTPError, defineMiddleware } from "h3";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/h3/middleware.ts
|
|
5
|
+
/**
|
|
6
|
+
* H3 middleware that creates a {@link SupabaseContext} and stores it in `event.context.supabaseContext`.
|
|
7
|
+
*
|
|
8
|
+
* Skips if a previous middleware already set the context, enabling chained middleware via `app.use()`.
|
|
9
|
+
* Throws an `HTTPError` on auth failure.
|
|
10
|
+
*
|
|
11
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded — use H3's CORS utilities.
|
|
12
|
+
* @returns An H3 middleware.
|
|
13
|
+
*
|
|
14
|
+
* @example App-wide auth via `app.use()`
|
|
15
|
+
* ```ts
|
|
16
|
+
* import { H3 } from 'h3'
|
|
17
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
18
|
+
*
|
|
19
|
+
* const app = new H3()
|
|
20
|
+
* app.use(withSupabase({ allow: 'user' }))
|
|
21
|
+
*
|
|
22
|
+
* app.get('/games', async (event) => {
|
|
23
|
+
* const { supabase } = event.context.supabaseContext
|
|
24
|
+
* return supabase.from('favorite_games').select()
|
|
25
|
+
* })
|
|
26
|
+
*
|
|
27
|
+
* export default { fetch: app.fetch }
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example Per-route auth via `defineHandler`
|
|
31
|
+
* ```ts
|
|
32
|
+
* import { defineHandler } from 'h3'
|
|
33
|
+
* import { withSupabase } from '@supabase/server/adapters/h3'
|
|
34
|
+
*
|
|
35
|
+
* export default defineHandler({
|
|
36
|
+
* middleware: [withSupabase({ allow: 'user' })],
|
|
37
|
+
* handler: async (event) => {
|
|
38
|
+
* const { supabase } = event.context.supabaseContext
|
|
39
|
+
* return supabase.from('favorite_games').select()
|
|
40
|
+
* },
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
function withSupabase(config) {
|
|
45
|
+
return defineMiddleware(async (event, next) => {
|
|
46
|
+
if (event.context.supabaseContext) return next();
|
|
47
|
+
const { data: ctx, error } = await createSupabaseContext(event.req, config);
|
|
48
|
+
if (error) throw new HTTPError(error.message, {
|
|
49
|
+
status: error.status,
|
|
50
|
+
cause: error
|
|
51
|
+
});
|
|
52
|
+
event.context.supabaseContext = ctx;
|
|
53
|
+
return next();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
//#endregion
|
|
58
|
+
export { withSupabase };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_create_supabase_context = require('../../create-supabase-context
|
|
2
|
+
const require_create_supabase_context = require('../../create-supabase-context-C_8SbO5w.cjs');
|
|
3
3
|
let hono_http_exception = require("hono/http-exception");
|
|
4
4
|
let hono_factory = require("hono/factory");
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-
|
|
2
|
-
import
|
|
1
|
+
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DqhOaSlC.cjs";
|
|
2
|
+
import { MiddlewareHandler } from "hono";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/hono/middleware.d.ts
|
|
5
5
|
/**
|
|
@@ -28,10 +28,10 @@ import * as hono_types0 from "hono/types";
|
|
|
28
28
|
* export default { fetch: app.fetch }
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>):
|
|
31
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): MiddlewareHandler<{
|
|
32
32
|
Variables: {
|
|
33
33
|
supabaseContext: SupabaseContext;
|
|
34
34
|
};
|
|
35
|
-
}
|
|
35
|
+
}>;
|
|
36
36
|
//#endregion
|
|
37
37
|
export { withSupabase };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-
|
|
2
|
-
import
|
|
1
|
+
import { f as WithSupabaseConfig, l as SupabaseContext } from "../../types-DKe8uOwI.mjs";
|
|
2
|
+
import { MiddlewareHandler } from "hono";
|
|
3
3
|
|
|
4
4
|
//#region src/adapters/hono/middleware.d.ts
|
|
5
5
|
/**
|
|
@@ -28,10 +28,10 @@ import * as hono_types0 from "hono/types";
|
|
|
28
28
|
* export default { fetch: app.fetch }
|
|
29
29
|
* ```
|
|
30
30
|
*/
|
|
31
|
-
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>):
|
|
31
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): MiddlewareHandler<{
|
|
32
32
|
Variables: {
|
|
33
33
|
supabaseContext: SupabaseContext;
|
|
34
34
|
};
|
|
35
|
-
}
|
|
35
|
+
}>;
|
|
36
36
|
//#endregion
|
|
37
37
|
export { withSupabase };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as createSupabaseContext } from "../../create-supabase-context-
|
|
1
|
+
import { t as createSupabaseContext } from "../../create-supabase-context-DXD5rxi1.mjs";
|
|
2
2
|
import { HTTPException } from "hono/http-exception";
|
|
3
3
|
import { createMiddleware } from "hono/factory";
|
|
4
4
|
|
package/dist/core/index.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
const require_verify_auth = require('../verify-auth-
|
|
2
|
+
const require_verify_auth = require('../verify-auth-C4zqDlfj.cjs');
|
|
3
3
|
|
|
4
4
|
exports.createAdminClient = require_verify_auth.createAdminClient;
|
|
5
5
|
exports.createContextClient = require_verify_auth.createContextClient;
|
package/dist/core/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-
|
|
2
|
-
import { i as EnvError, t as AuthError } from "../errors-
|
|
1
|
+
import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-DqhOaSlC.cjs";
|
|
2
|
+
import { i as EnvError, t as AuthError } from "../errors-Dyj5Cjt6.cjs";
|
|
3
3
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
4
4
|
|
|
5
5
|
//#region src/core/resolve-env.d.ts
|
|
@@ -72,8 +72,11 @@ interface VerifyCredentialsOptions {
|
|
|
72
72
|
/**
|
|
73
73
|
* Verifies pre-extracted credentials against one or more allowed auth modes.
|
|
74
74
|
*
|
|
75
|
-
* Tries each mode in order — first match wins.
|
|
76
|
-
*
|
|
75
|
+
* Tries each mode in order — first match wins. A mode is only tried when its
|
|
76
|
+
* credential is present; a JWT that is present but fails verification
|
|
77
|
+
* short-circuits the chain with `InvalidCredentialsError` instead of falling
|
|
78
|
+
* through to the next mode. Use {@link verifyAuth} to extract and verify in a
|
|
79
|
+
* single call.
|
|
77
80
|
*
|
|
78
81
|
* @param credentials - The credentials to verify (from {@link extractCredentials}).
|
|
79
82
|
* @param options - Allowed auth modes and optional env overrides.
|
package/dist/core/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-
|
|
2
|
-
import { i as EnvError, t as AuthError } from "../errors-
|
|
1
|
+
import { a as CreateAdminClientOptions, i as ClientAuth, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, u as SupabaseEnv } from "../types-DKe8uOwI.mjs";
|
|
2
|
+
import { i as EnvError, t as AuthError } from "../errors-m42mkqhD.mjs";
|
|
3
3
|
import { SupabaseClient } from "@supabase/supabase-js";
|
|
4
4
|
|
|
5
5
|
//#region src/core/resolve-env.d.ts
|
|
@@ -72,8 +72,11 @@ interface VerifyCredentialsOptions {
|
|
|
72
72
|
/**
|
|
73
73
|
* Verifies pre-extracted credentials against one or more allowed auth modes.
|
|
74
74
|
*
|
|
75
|
-
* Tries each mode in order — first match wins.
|
|
76
|
-
*
|
|
75
|
+
* Tries each mode in order — first match wins. A mode is only tried when its
|
|
76
|
+
* credential is present; a JWT that is present but fails verification
|
|
77
|
+
* short-circuits the chain with `InvalidCredentialsError` instead of falling
|
|
78
|
+
* through to the next mode. Use {@link verifyAuth} to extract and verify in a
|
|
79
|
+
* single call.
|
|
77
80
|
*
|
|
78
81
|
* @param credentials - The credentials to verify (from {@link extractCredentials}).
|
|
79
82
|
* @param options - Allowed auth modes and optional env overrides.
|
package/dist/core/index.mjs
CHANGED
|
@@ -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-
|
|
1
|
+
import { a as createAdminClient, i as createContextClient, n as verifyCredentials, o as resolveEnv, r as extractCredentials, t as verifyAuth } from "../verify-auth-CxFZy9rl.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-
|
|
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-CxFZy9rl.mjs";
|
|
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-
|
|
3
|
-
const require_create_supabase_context = require('./create-supabase-context
|
|
2
|
+
const require_verify_auth = require('./verify-auth-C4zqDlfj.cjs');
|
|
3
|
+
const require_create_supabase_context = require('./create-supabase-context-C_8SbO5w.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,5 +1,5 @@
|
|
|
1
|
-
import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-
|
|
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-
|
|
1
|
+
import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-DqhOaSlC.cjs";
|
|
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-Dyj5Cjt6.cjs";
|
|
3
3
|
|
|
4
4
|
//#region src/with-supabase.d.ts
|
|
5
5
|
/**
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-
|
|
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-
|
|
1
|
+
import { a as CreateAdminClientOptions, c as JWTClaims, d as UserClaims, f as WithSupabaseConfig, i as ClientAuth, l as SupabaseContext, n as AllowWithKey, o as CreateContextClientOptions, r as AuthResult, s as Credentials, t as Allow, u as SupabaseEnv } from "./types-DKe8uOwI.mjs";
|
|
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-m42mkqhD.mjs";
|
|
3
3
|
|
|
4
4
|
//#region src/with-supabase.d.ts
|
|
5
5
|
/**
|
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-
|
|
2
|
-
import { t as createSupabaseContext } from "./create-supabase-context-
|
|
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-CxFZy9rl.mjs";
|
|
2
|
+
import { t as createSupabaseContext } from "./create-supabase-context-DXD5rxi1.mjs";
|
|
3
3
|
import { corsHeaders } from "@supabase/supabase-js/cors";
|
|
4
4
|
|
|
5
5
|
//#region src/cors.ts
|
|
@@ -14,7 +14,10 @@ import { SupabaseClient, SupabaseClientOptions } from "@supabase/supabase-js";
|
|
|
14
14
|
* // Single mode
|
|
15
15
|
* withSupabase({ allow: 'user' }, handler)
|
|
16
16
|
*
|
|
17
|
-
* // Multiple modes — the first match wins
|
|
17
|
+
* // Multiple modes — the first match wins.
|
|
18
|
+
* // A mode is tried only when its credential is present; a JWT that is
|
|
19
|
+
* // present but fails verification rejects immediately rather than falling
|
|
20
|
+
* // through to the next mode.
|
|
18
21
|
* withSupabase({ allow: ['user', 'public'] }, handler)
|
|
19
22
|
* ```
|
|
20
23
|
*/
|
|
@@ -182,6 +185,8 @@ interface UserClaims {
|
|
|
182
185
|
interface WithSupabaseConfig {
|
|
183
186
|
/**
|
|
184
187
|
* Auth mode(s) to accept. Modes are tried in order — the first match wins.
|
|
188
|
+
* A mode falls through only when its credential is absent; a present-but-invalid
|
|
189
|
+
* JWT short-circuits the chain with `InvalidCredentialsError`.
|
|
185
190
|
*
|
|
186
191
|
* @defaultValue `"user"`
|
|
187
192
|
*/
|
|
@@ -14,7 +14,10 @@ import { SupabaseClient, SupabaseClientOptions } from "@supabase/supabase-js";
|
|
|
14
14
|
* // Single mode
|
|
15
15
|
* withSupabase({ allow: 'user' }, handler)
|
|
16
16
|
*
|
|
17
|
-
* // Multiple modes — the first match wins
|
|
17
|
+
* // Multiple modes — the first match wins.
|
|
18
|
+
* // A mode is tried only when its credential is present; a JWT that is
|
|
19
|
+
* // present but fails verification rejects immediately rather than falling
|
|
20
|
+
* // through to the next mode.
|
|
18
21
|
* withSupabase({ allow: ['user', 'public'] }, handler)
|
|
19
22
|
* ```
|
|
20
23
|
*/
|
|
@@ -182,6 +185,8 @@ interface UserClaims {
|
|
|
182
185
|
interface WithSupabaseConfig {
|
|
183
186
|
/**
|
|
184
187
|
* Auth mode(s) to accept. Modes are tried in order — the first match wins.
|
|
188
|
+
* A mode falls through only when its credential is absent; a present-but-invalid
|
|
189
|
+
* JWT short-circuits the chain with `InvalidCredentialsError`.
|
|
185
190
|
*
|
|
186
191
|
* @defaultValue `"user"`
|
|
187
192
|
*/
|
|
@@ -388,9 +388,15 @@ function claimsToUserClaims(claims) {
|
|
|
388
388
|
userMetadata: claims.user_metadata
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
|
+
const INVALID = Symbol("invalid");
|
|
391
392
|
/**
|
|
392
393
|
* Attempts to authenticate credentials against a single auth mode.
|
|
393
|
-
*
|
|
394
|
+
*
|
|
395
|
+
* Returns:
|
|
396
|
+
* - `AuthResult` on success.
|
|
397
|
+
* - `null` if the mode doesn't apply (no relevant credential present — safe to try the next mode).
|
|
398
|
+
* - `INVALID` if a credential was present but failed verification (must reject immediately).
|
|
399
|
+
*
|
|
394
400
|
* @internal
|
|
395
401
|
*/
|
|
396
402
|
async function tryMode(mode, credentials, env) {
|
|
@@ -457,7 +463,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
457
463
|
try {
|
|
458
464
|
const jwkSet = (0, jose.createLocalJWKSet)(env.jwks);
|
|
459
465
|
const { payload } = await (0, jose.jwtVerify)(credentials.token, jwkSet);
|
|
460
|
-
if (typeof payload.sub !== "string") return
|
|
466
|
+
if (typeof payload.sub !== "string") return INVALID;
|
|
461
467
|
const claims = payload;
|
|
462
468
|
return {
|
|
463
469
|
authType: "user",
|
|
@@ -467,7 +473,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
467
473
|
keyName: null
|
|
468
474
|
};
|
|
469
475
|
} catch {
|
|
470
|
-
return
|
|
476
|
+
return INVALID;
|
|
471
477
|
}
|
|
472
478
|
default: return null;
|
|
473
479
|
}
|
|
@@ -475,8 +481,11 @@ async function tryMode(mode, credentials, env) {
|
|
|
475
481
|
/**
|
|
476
482
|
* Verifies pre-extracted credentials against one or more allowed auth modes.
|
|
477
483
|
*
|
|
478
|
-
* Tries each mode in order — first match wins.
|
|
479
|
-
*
|
|
484
|
+
* Tries each mode in order — first match wins. A mode is only tried when its
|
|
485
|
+
* credential is present; a JWT that is present but fails verification
|
|
486
|
+
* short-circuits the chain with `InvalidCredentialsError` instead of falling
|
|
487
|
+
* through to the next mode. Use {@link verifyAuth} to extract and verify in a
|
|
488
|
+
* single call.
|
|
480
489
|
*
|
|
481
490
|
* @param credentials - The credentials to verify (from {@link extractCredentials}).
|
|
482
491
|
* @param options - Allowed auth modes and optional env overrides.
|
|
@@ -502,6 +511,10 @@ async function verifyCredentials(credentials, options) {
|
|
|
502
511
|
const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
|
|
503
512
|
for (const mode of modes) {
|
|
504
513
|
const result = await tryMode(mode, credentials, env);
|
|
514
|
+
if (result === INVALID) return {
|
|
515
|
+
data: null,
|
|
516
|
+
error: Errors[InvalidCredentialsError]()
|
|
517
|
+
};
|
|
505
518
|
if (result) return {
|
|
506
519
|
data: result,
|
|
507
520
|
error: null
|
|
@@ -388,9 +388,15 @@ function claimsToUserClaims(claims) {
|
|
|
388
388
|
userMetadata: claims.user_metadata
|
|
389
389
|
};
|
|
390
390
|
}
|
|
391
|
+
const INVALID = Symbol("invalid");
|
|
391
392
|
/**
|
|
392
393
|
* Attempts to authenticate credentials against a single auth mode.
|
|
393
|
-
*
|
|
394
|
+
*
|
|
395
|
+
* Returns:
|
|
396
|
+
* - `AuthResult` on success.
|
|
397
|
+
* - `null` if the mode doesn't apply (no relevant credential present — safe to try the next mode).
|
|
398
|
+
* - `INVALID` if a credential was present but failed verification (must reject immediately).
|
|
399
|
+
*
|
|
394
400
|
* @internal
|
|
395
401
|
*/
|
|
396
402
|
async function tryMode(mode, credentials, env) {
|
|
@@ -457,7 +463,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
457
463
|
try {
|
|
458
464
|
const jwkSet = createLocalJWKSet(env.jwks);
|
|
459
465
|
const { payload } = await jwtVerify(credentials.token, jwkSet);
|
|
460
|
-
if (typeof payload.sub !== "string") return
|
|
466
|
+
if (typeof payload.sub !== "string") return INVALID;
|
|
461
467
|
const claims = payload;
|
|
462
468
|
return {
|
|
463
469
|
authType: "user",
|
|
@@ -467,7 +473,7 @@ async function tryMode(mode, credentials, env) {
|
|
|
467
473
|
keyName: null
|
|
468
474
|
};
|
|
469
475
|
} catch {
|
|
470
|
-
return
|
|
476
|
+
return INVALID;
|
|
471
477
|
}
|
|
472
478
|
default: return null;
|
|
473
479
|
}
|
|
@@ -475,8 +481,11 @@ async function tryMode(mode, credentials, env) {
|
|
|
475
481
|
/**
|
|
476
482
|
* Verifies pre-extracted credentials against one or more allowed auth modes.
|
|
477
483
|
*
|
|
478
|
-
* Tries each mode in order — first match wins.
|
|
479
|
-
*
|
|
484
|
+
* Tries each mode in order — first match wins. A mode is only tried when its
|
|
485
|
+
* credential is present; a JWT that is present but fails verification
|
|
486
|
+
* short-circuits the chain with `InvalidCredentialsError` instead of falling
|
|
487
|
+
* through to the next mode. Use {@link verifyAuth} to extract and verify in a
|
|
488
|
+
* single call.
|
|
480
489
|
*
|
|
481
490
|
* @param credentials - The credentials to verify (from {@link extractCredentials}).
|
|
482
491
|
* @param options - Allowed auth modes and optional env overrides.
|
|
@@ -502,6 +511,10 @@ async function verifyCredentials(credentials, options) {
|
|
|
502
511
|
const modes = Array.isArray(options.allow) ? options.allow : [options.allow];
|
|
503
512
|
for (const mode of modes) {
|
|
504
513
|
const result = await tryMode(mode, credentials, env);
|
|
514
|
+
if (result === INVALID) return {
|
|
515
|
+
data: null,
|
|
516
|
+
error: Errors[InvalidCredentialsError]()
|
|
517
|
+
};
|
|
505
518
|
if (result) return {
|
|
506
519
|
data: result,
|
|
507
520
|
error: null
|
package/docs/api-reference.md
CHANGED
|
@@ -291,17 +291,17 @@ class AuthError extends Error {
|
|
|
291
291
|
|
|
292
292
|
## Error Code Constants
|
|
293
293
|
|
|
294
|
-
| Constant | Value | Class | Meaning
|
|
295
|
-
| ----------------------------------- | ----------------------------------- | ----------- |
|
|
296
|
-
| `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error
|
|
297
|
-
| `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set
|
|
298
|
-
| `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found
|
|
299
|
-
| `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key
|
|
300
|
-
| `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found
|
|
301
|
-
| `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key
|
|
302
|
-
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error
|
|
303
|
-
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched
|
|
304
|
-
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth
|
|
294
|
+
| Constant | Value | Class | Meaning |
|
|
295
|
+
| ----------------------------------- | ----------------------------------- | ----------- | ------------------------------------------------- |
|
|
296
|
+
| `EnvGenericError` | `'ENV_ERROR'` | `EnvError` | Generic environment error |
|
|
297
|
+
| `MissingSupabaseURLError` | `'MISSING_SUPABASE_URL'` | `EnvError` | `SUPABASE_URL` not set |
|
|
298
|
+
| `MissingPublishableKeyError` | `'MISSING_PUBLISHABLE_KEY'` | `EnvError` | Named publishable key not found |
|
|
299
|
+
| `MissingDefaultPublishableKeyError` | `'MISSING_DEFAULT_PUBLISHABLE_KEY'` | `EnvError` | No default publishable key |
|
|
300
|
+
| `MissingSecretKeyError` | `'MISSING_SECRET_KEY'` | `EnvError` | Named secret key not found |
|
|
301
|
+
| `MissingDefaultSecretKeyError` | `'MISSING_DEFAULT_SECRET_KEY'` | `EnvError` | No default secret key |
|
|
302
|
+
| `AuthGenericError` | `'AUTH_ERROR'` | `AuthError` | Generic auth error |
|
|
303
|
+
| `InvalidCredentialsError` | `'INVALID_CREDENTIALS'` | `AuthError` | No credential matched, or JWT failed verification |
|
|
304
|
+
| `CreateSupabaseClientError` | `'CREATE_SUPABASE_CLIENT_ERROR'` | `AuthError` | Client creation failed after auth |
|
|
305
305
|
|
|
306
306
|
---
|
|
307
307
|
|
package/docs/auth-modes.md
CHANGED
|
@@ -147,6 +147,8 @@ export default {
|
|
|
147
147
|
|
|
148
148
|
A request with a valid JWT matches `'user'`. A request with a valid secret key matches `'secret'`. A request with neither is rejected.
|
|
149
149
|
|
|
150
|
+
**Fallthrough vs rejection.** A mode is only "tried" when its credential is actually present. A request with no `Authorization` header moves on to the next mode. But if a JWT _is_ present and fails verification (malformed, expired, wrong signature, or missing a `sub` claim), the request is rejected immediately with `InvalidCredentialsError` — it will not silently fall through to `'public'`, `'secret'`, or `'always'`. The same rule applies on the API-key side: `'public'` and `'secret'` fall through only when no `apikey` header is sent. This prevents a bad credential from being downgraded to a less-privileged auth mode.
|
|
151
|
+
|
|
150
152
|
## Named key syntax
|
|
151
153
|
|
|
152
154
|
When your project has multiple API keys (e.g., separate keys for web, mobile, and internal services), use the colon syntax to validate against a specific named key.
|
|
@@ -198,6 +200,6 @@ withSupabase({ allow: ['user', 'public:web'] }, async (_req, ctx) => {
|
|
|
198
200
|
|
|
199
201
|
1. `extractCredentials(request)` reads `Authorization: Bearer <token>` and `apikey` from headers
|
|
200
202
|
2. Each mode in `allow` is tried in order against the extracted credentials
|
|
201
|
-
3. First match wins — returns an `AuthResult` with `authType`, `token`, `userClaims`, `claims`, and `keyName`
|
|
203
|
+
3. First match wins — returns an `AuthResult` with `authType`, `token`, `userClaims`, `claims`, and `keyName`. A mode falls through to the next only when its credential is absent; a credential that is present but invalid terminates the chain with `InvalidCredentialsError`.
|
|
202
204
|
4. The auth result is used to create scoped clients (`supabase` with the user's token, `supabaseAdmin` with the secret key)
|
|
203
205
|
5. Everything is bundled into a `SupabaseContext` and passed to your handler
|
package/docs/error-handling.md
CHANGED
|
@@ -21,11 +21,11 @@ Thrown when a required environment variable is missing or malformed. Always `sta
|
|
|
21
21
|
|
|
22
22
|
Thrown when authentication or authorization fails. Status is `401` for invalid credentials, `500` for server-side auth failures.
|
|
23
23
|
|
|
24
|
-
| Code | Status | Meaning
|
|
25
|
-
| ------------------------------ | ------ |
|
|
26
|
-
| `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode |
|
|
27
|
-
| `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed
|
|
28
|
-
| `AUTH_ERROR` | 401 | Generic authentication error
|
|
24
|
+
| Code | Status | Meaning |
|
|
25
|
+
| ------------------------------ | ------ | ----------------------------------------------------------------------------------------- |
|
|
26
|
+
| `INVALID_CREDENTIALS` | 401 | No credential matched any allowed auth mode, or a JWT was present but failed verification |
|
|
27
|
+
| `CREATE_SUPABASE_CLIENT_ERROR` | 500 | Auth succeeded but client creation failed |
|
|
28
|
+
| `AUTH_ERROR` | 401 | Generic authentication error |
|
|
29
29
|
|
|
30
30
|
## How errors surface in each layer
|
|
31
31
|
|
package/docs/security.md
CHANGED
|
@@ -56,10 +56,12 @@ JWT verification in `user` mode works as follows:
|
|
|
56
56
|
2. The token is verified against the JWKS from the `SUPABASE_JWKS` environment variable
|
|
57
57
|
3. Verification uses `jose`'s `jwtVerify` with a **local** key set — there are no network calls to a JWKS endpoint
|
|
58
58
|
4. The token must contain a `sub` (subject) claim to be considered valid
|
|
59
|
-
5. On success, the decoded claims are available as `ctx.
|
|
59
|
+
5. On success, the decoded claims are available as `ctx.userClaims` and `ctx.claims`
|
|
60
60
|
|
|
61
61
|
If JWKS is not configured (`SUPABASE_JWKS` is missing or malformed), `user` mode is unavailable and will always reject requests.
|
|
62
62
|
|
|
63
|
+
**No silent downgrade.** When `user` is combined with other modes (e.g. `allow: ['user', 'public']`), a JWT that is present but fails verification rejects the request with `InvalidCredentialsError` — it does not fall through to the next mode. This prevents a bad token paired with a valid `apikey` (or with `'always'`) from being silently downgraded to a less-privileged auth mode. Requests that simply omit the `Authorization` header still fall through as expected.
|
|
64
|
+
|
|
63
65
|
## CORS handling
|
|
64
66
|
|
|
65
67
|
`withSupabase` handles CORS automatically:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-rc.45",
|
|
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",
|
|
@@ -34,6 +34,11 @@
|
|
|
34
34
|
"import": "./dist/adapters/hono/index.mjs",
|
|
35
35
|
"require": "./dist/adapters/hono/index.cjs"
|
|
36
36
|
},
|
|
37
|
+
"./adapters/h3": {
|
|
38
|
+
"types": "./dist/adapters/h3/index.d.mts",
|
|
39
|
+
"import": "./dist/adapters/h3/index.mjs",
|
|
40
|
+
"require": "./dist/adapters/h3/index.cjs"
|
|
41
|
+
},
|
|
37
42
|
"./package.json": "./package.json"
|
|
38
43
|
},
|
|
39
44
|
"main": "./dist/index.cjs",
|
|
@@ -69,9 +74,13 @@
|
|
|
69
74
|
},
|
|
70
75
|
"peerDependencies": {
|
|
71
76
|
"@supabase/supabase-js": "^2.0.0",
|
|
72
|
-
"hono": "^4.0.0"
|
|
77
|
+
"hono": "^4.0.0",
|
|
78
|
+
"h3": "^2.0.0"
|
|
73
79
|
},
|
|
74
80
|
"peerDependenciesMeta": {
|
|
81
|
+
"h3": {
|
|
82
|
+
"optional": true
|
|
83
|
+
},
|
|
75
84
|
"hono": {
|
|
76
85
|
"optional": true
|
|
77
86
|
}
|
|
@@ -81,6 +90,7 @@
|
|
|
81
90
|
"@commitlint/config-conventional": "^20.4.2",
|
|
82
91
|
"@supabase/supabase-js": "^2.98.0",
|
|
83
92
|
"eslint": "^10.0.2",
|
|
93
|
+
"h3": "2.0.1-rc.20",
|
|
84
94
|
"hono": "^4.12.5",
|
|
85
95
|
"prettier": "3.8.1",
|
|
86
96
|
"pretty-quick": "^4.2.2",
|
|
@@ -5,6 +5,8 @@ description: Use when writing server-side code with Supabase — Edge Functions,
|
|
|
5
5
|
|
|
6
6
|
# @supabase/server
|
|
7
7
|
|
|
8
|
+
> **Beta:** This package is under active development. APIs and documentation may change. If you find a bug or have a feature request, please [open an issue](https://github.com/supabase/server/issues) or [submit a PR](https://github.com/supabase/server/blob/main/CONTRIBUTING.md).
|
|
9
|
+
|
|
8
10
|
> **This is a brand new package.** There is no information available online yet — no blog posts, no Stack Overflow answers, no tutorials. Do not search the web for usage examples. Rely exclusively on the documentation files listed below and the source code in this repository.
|
|
9
11
|
|
|
10
12
|
> **Do not use legacy Supabase keys.** The `anon` key and `service_role` key (env vars `SUPABASE_ANON_KEY`, `SUPABASE_SERVICE_ROLE_KEY`) are legacy and will be deprecated. Do not use them unless the user explicitly asks. Always use the new API keys:
|
|
@@ -22,6 +24,7 @@ Server-side utilities for Supabase. Handles auth, client creation, and context i
|
|
|
22
24
|
|
|
23
25
|
- Wraps fetch handlers with credential verification, CORS, and pre-configured Supabase clients
|
|
24
26
|
- Supports 4 auth modes: `user` (JWT), `public` (publishable key), `secret` (secret key), `always` (none)
|
|
27
|
+
- Array syntax (`allow: ['user', 'secret']`) is first-match-wins. A present-but-invalid JWT rejects with `InvalidCredentialsError` — it does not silently downgrade to the next mode.
|
|
25
28
|
- Provides composable core primitives for custom auth flows and framework integration
|
|
26
29
|
- Includes a Hono adapter for per-route auth
|
|
27
30
|
|
|
@@ -197,6 +200,8 @@ Use `allow: 'secret'` to accept any secret key, or `allow: 'secret:name'` to req
|
|
|
197
200
|
|
|
198
201
|
**Never use `allow: 'always'` for endpoints that read or write user data without verifying who the caller is.**
|
|
199
202
|
|
|
203
|
+
**On `allow: ['user', 'always']`.** A stale or malformed JWT on such an endpoint is rejected with `InvalidCredentialsError` — it is not silently downgraded to anonymous. Callers that might hold a cached/expired token should either omit the `Authorization` header entirely or refresh before calling. If the goal is "anonymous unless a valid user is signed in," this is the correct behavior; if the goal is truly "accept anything," use `allow: 'always'` on its own.
|
|
204
|
+
|
|
200
205
|
## Edge Function recipes
|
|
201
206
|
|
|
202
207
|
### Function-to-function calls
|
|
File without changes
|
|
File without changes
|