@supabase/server 0.1.1 → 0.1.2
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 +73 -6
- package/SKILL.md +415 -0
- package/dist/adapters/hono/index.cjs +1 -1
- package/dist/adapters/hono/index.d.cts +1 -1
- package/dist/adapters/hono/index.d.mts +1 -1
- package/dist/adapters/hono/index.mjs +1 -1
- package/dist/core/index.d.cts +1 -1
- package/dist/core/index.d.mts +1 -1
- package/dist/{create-supabase-context-DDIAxA8h.cjs → create-supabase-context--VqMJpDu.cjs} +4 -2
- package/dist/{create-supabase-context-Bmwyha9p.mjs → create-supabase-context-B3Uzt_3I.mjs} +4 -2
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{types-BmWSIuH7.d.mts → types-CbC-wBUe.d.mts} +4 -2
- package/dist/{types-X7xYi2LN.d.cts → types-DxTr0Qum.d.cts} +4 -2
- package/docs/api-reference.md +322 -0
- package/docs/auth-modes.md +203 -0
- package/docs/core-primitives.md +240 -0
- package/docs/environment-variables.md +189 -0
- package/docs/error-handling.md +191 -0
- package/docs/getting-started.md +149 -0
- package/docs/hono-adapter.md +185 -0
- package/docs/security.md +82 -0
- package/docs/ssr-frameworks.md +330 -0
- package/docs/typescript-generics.md +143 -0
- package/package.json +4 -7
- package/dist/wrappers/index.cjs +0 -45
- package/dist/wrappers/index.d.cts +0 -23
- package/dist/wrappers/index.d.mts +0 -23
- package/dist/wrappers/index.mjs +0 -43
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
# SSR Frameworks
|
|
2
|
+
|
|
3
|
+
## When you need this
|
|
4
|
+
|
|
5
|
+
In SSR frameworks like Next.js, Nuxt, SvelteKit, and Remix, the user's JWT doesn't arrive in an `Authorization` header — it's stored in session cookies managed by `@supabase/ssr`. The high-level wrappers (`withSupabase`, `createSupabaseContext`) expect a standard `Request` with auth headers, so they don't work directly in SSR contexts.
|
|
6
|
+
|
|
7
|
+
Instead, use the [core primitives](core-primitives.md) to build a lightweight adapter for your framework. The pattern is the same everywhere — only the cookie-reading part changes.
|
|
8
|
+
|
|
9
|
+
## The pattern
|
|
10
|
+
|
|
11
|
+
Every SSR adapter follows these steps:
|
|
12
|
+
|
|
13
|
+
1. **Extract the access token from cookies** (framework-specific)
|
|
14
|
+
2. **Bridge environment variables** to the `SupabaseEnv` shape
|
|
15
|
+
3. **Resolve JWKS** for JWT verification
|
|
16
|
+
4. **Call `verifyCredentials`** with the extracted token
|
|
17
|
+
5. **Create clients** with `createContextClient` + `createAdminClient`
|
|
18
|
+
6. **Return a `SupabaseContext`**
|
|
19
|
+
|
|
20
|
+
## Reading Supabase session cookies
|
|
21
|
+
|
|
22
|
+
`@supabase/ssr` stores the session in cookies using a chunked, base64-encoded format:
|
|
23
|
+
|
|
24
|
+
- **Cookie name:** `sb-<project-ref>-auth-token` (the project ref is extracted from your Supabase URL)
|
|
25
|
+
- **Chunking:** if the session is too large for a single cookie, it's split into `sb-<ref>-auth-token.0`, `.1`, `.2`, etc.
|
|
26
|
+
- **Base64 encoding:** the cookie value may be prefixed with `base64-`, indicating base64url encoding
|
|
27
|
+
|
|
28
|
+
To extract the access token:
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
const BASE64_PREFIX = 'base64-'
|
|
32
|
+
|
|
33
|
+
function getAccessTokenFromCookies(
|
|
34
|
+
getCookie: (name: string) => string | undefined,
|
|
35
|
+
supabaseUrl: string,
|
|
36
|
+
): string | null {
|
|
37
|
+
// Extract project ref from URL: "https://abc123.supabase.co" → "abc123"
|
|
38
|
+
const ref = new URL(supabaseUrl).hostname.split('.')[0]
|
|
39
|
+
const storageKey = `sb-${ref}-auth-token`
|
|
40
|
+
|
|
41
|
+
// Try single cookie first, then chunked
|
|
42
|
+
let raw = getCookie(storageKey) ?? null
|
|
43
|
+
|
|
44
|
+
if (!raw) {
|
|
45
|
+
const chunks: string[] = []
|
|
46
|
+
for (let i = 0; ; i++) {
|
|
47
|
+
const chunk = getCookie(`${storageKey}.${i}`)
|
|
48
|
+
if (!chunk) break
|
|
49
|
+
chunks.push(chunk)
|
|
50
|
+
}
|
|
51
|
+
if (chunks.length > 0) raw = chunks.join('')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!raw) return null
|
|
55
|
+
|
|
56
|
+
// Decode base64url if needed
|
|
57
|
+
let decoded = raw
|
|
58
|
+
if (decoded.startsWith(BASE64_PREFIX)) {
|
|
59
|
+
try {
|
|
60
|
+
const base64 = decoded
|
|
61
|
+
.substring(BASE64_PREFIX.length)
|
|
62
|
+
.replace(/-/g, '+')
|
|
63
|
+
.replace(/_/g, '/')
|
|
64
|
+
decoded = atob(base64)
|
|
65
|
+
} catch {
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Parse the session JSON and extract access_token
|
|
71
|
+
try {
|
|
72
|
+
const session = JSON.parse(decoded)
|
|
73
|
+
return session.access_token ?? null
|
|
74
|
+
} catch {
|
|
75
|
+
return null
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The `getCookie` parameter is a function that reads a cookie by name — its implementation depends on your framework (e.g., `cookies().get(name)?.value` in Next.js, `event.cookies.get(name)` in SvelteKit).
|
|
81
|
+
|
|
82
|
+
## Environment variable bridging
|
|
83
|
+
|
|
84
|
+
SSR frameworks often use their own naming conventions for environment variables. Map them to a `Partial<SupabaseEnv>` that the core primitives expect:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import type { SupabaseEnv } from '@supabase/server'
|
|
88
|
+
|
|
89
|
+
function resolveEnvFromFramework(): Partial<SupabaseEnv> {
|
|
90
|
+
// Example: Next.js uses NEXT_PUBLIC_* for client-exposed vars
|
|
91
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
92
|
+
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
93
|
+
const secretKey = process.env.SUPABASE_SECRET_KEY
|
|
94
|
+
|
|
95
|
+
return {
|
|
96
|
+
url: url ?? undefined,
|
|
97
|
+
publishableKeys: publishableKey ? { default: publishableKey } : {},
|
|
98
|
+
secretKeys: secretKey ? { default: secretKey } : {},
|
|
99
|
+
// JWKS: either set SUPABASE_JWKS env var, or fetch it (see below)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## JWKS resolution
|
|
105
|
+
|
|
106
|
+
JWT verification requires a JWKS (JSON Web Key Set). Two options:
|
|
107
|
+
|
|
108
|
+
**Option 1: Set the `SUPABASE_JWKS` environment variable.** This is auto-available on the Supabase platform and in local CLI. If set, the core primitives pick it up automatically — no extra code needed.
|
|
109
|
+
|
|
110
|
+
**Option 2: Fetch from the well-known endpoint and cache.** Useful when deploying to environments where `SUPABASE_JWKS` isn't set:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import type { SupabaseEnv } from '@supabase/server'
|
|
114
|
+
|
|
115
|
+
let cachedJwks: SupabaseEnv['jwks'] = null
|
|
116
|
+
|
|
117
|
+
async function getJwks(supabaseUrl: string): Promise<SupabaseEnv['jwks']> {
|
|
118
|
+
if (cachedJwks) return cachedJwks
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const res = await fetch(`${supabaseUrl}/auth/v1/.well-known/jwks.json`)
|
|
122
|
+
if (!res.ok) return null
|
|
123
|
+
cachedJwks = await res.json()
|
|
124
|
+
return cachedJwks
|
|
125
|
+
} catch {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The cache lives in module scope, so it persists across requests for the lifetime of the server process. For serverless environments (e.g., Vercel), the cache is per-invocation — consider using an external cache or always setting `SUPABASE_JWKS`.
|
|
132
|
+
|
|
133
|
+
## Complete example: Next.js adapter
|
|
134
|
+
|
|
135
|
+
A full adapter for Next.js App Router — works in Server Components, Server Actions, and Route Handlers:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
// lib/supabase/context.ts
|
|
139
|
+
import { cookies } from 'next/headers'
|
|
140
|
+
import {
|
|
141
|
+
verifyCredentials,
|
|
142
|
+
createContextClient,
|
|
143
|
+
createAdminClient,
|
|
144
|
+
} from '@supabase/server/core'
|
|
145
|
+
import type {
|
|
146
|
+
AllowWithKey,
|
|
147
|
+
SupabaseContext,
|
|
148
|
+
SupabaseEnv,
|
|
149
|
+
} from '@supabase/server'
|
|
150
|
+
|
|
151
|
+
const BASE64_PREFIX = 'base64-'
|
|
152
|
+
|
|
153
|
+
function getAccessTokenFromCookies(
|
|
154
|
+
cookieStore: Awaited<ReturnType<typeof cookies>>,
|
|
155
|
+
url: string,
|
|
156
|
+
): string | null {
|
|
157
|
+
const ref = new URL(url).hostname.split('.')[0]
|
|
158
|
+
const storageKey = `sb-${ref}-auth-token`
|
|
159
|
+
|
|
160
|
+
let raw = cookieStore.get(storageKey)?.value ?? null
|
|
161
|
+
|
|
162
|
+
if (!raw) {
|
|
163
|
+
const chunks: string[] = []
|
|
164
|
+
for (let i = 0; ; i++) {
|
|
165
|
+
const chunk = cookieStore.get(`${storageKey}.${i}`)?.value
|
|
166
|
+
if (!chunk) break
|
|
167
|
+
chunks.push(chunk)
|
|
168
|
+
}
|
|
169
|
+
if (chunks.length > 0) raw = chunks.join('')
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (!raw) return null
|
|
173
|
+
|
|
174
|
+
let decoded = raw
|
|
175
|
+
if (decoded.startsWith(BASE64_PREFIX)) {
|
|
176
|
+
try {
|
|
177
|
+
const base64 = decoded
|
|
178
|
+
.substring(BASE64_PREFIX.length)
|
|
179
|
+
.replace(/-/g, '+')
|
|
180
|
+
.replace(/_/g, '/')
|
|
181
|
+
decoded = atob(base64)
|
|
182
|
+
} catch {
|
|
183
|
+
return null
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const session = JSON.parse(decoded)
|
|
189
|
+
return session.access_token ?? null
|
|
190
|
+
} catch {
|
|
191
|
+
return null
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resolveNextEnv(): Partial<SupabaseEnv> {
|
|
196
|
+
const url = process.env.NEXT_PUBLIC_SUPABASE_URL
|
|
197
|
+
const publishableKey = process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY
|
|
198
|
+
const secretKey = process.env.SUPABASE_SECRET_KEY
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
url: url ?? undefined,
|
|
202
|
+
publishableKeys: publishableKey ? { default: publishableKey } : {},
|
|
203
|
+
secretKeys: secretKey ? { default: secretKey } : {},
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let cachedJwks: SupabaseEnv['jwks'] = null
|
|
208
|
+
|
|
209
|
+
async function getJwks(supabaseUrl: string): Promise<SupabaseEnv['jwks']> {
|
|
210
|
+
if (cachedJwks) return cachedJwks
|
|
211
|
+
try {
|
|
212
|
+
const res = await fetch(`${supabaseUrl}/auth/v1/.well-known/jwks.json`)
|
|
213
|
+
if (!res.ok) return null
|
|
214
|
+
cachedJwks = await res.json()
|
|
215
|
+
return cachedJwks
|
|
216
|
+
} catch {
|
|
217
|
+
return null
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function createSupabaseContext(
|
|
222
|
+
options: { allow?: AllowWithKey | AllowWithKey[] } = { allow: 'user' },
|
|
223
|
+
): Promise<
|
|
224
|
+
{ data: SupabaseContext; error: null } | { data: null; error: Error }
|
|
225
|
+
> {
|
|
226
|
+
const nextEnv = resolveNextEnv()
|
|
227
|
+
|
|
228
|
+
if (!nextEnv.url) {
|
|
229
|
+
return { data: null, error: new Error('Missing SUPABASE_URL') }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const cookieStore = await cookies()
|
|
233
|
+
const token = getAccessTokenFromCookies(cookieStore, nextEnv.url)
|
|
234
|
+
|
|
235
|
+
const jwks = await getJwks(nextEnv.url)
|
|
236
|
+
const env: Partial<SupabaseEnv> = { ...nextEnv, jwks }
|
|
237
|
+
|
|
238
|
+
const { data: auth, error } = await verifyCredentials(
|
|
239
|
+
{ token, apikey: null },
|
|
240
|
+
{ allow: options.allow ?? 'user', env },
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if (error) {
|
|
244
|
+
return { data: null, error }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const supabase = createContextClient({
|
|
248
|
+
auth: { token: auth!.token },
|
|
249
|
+
env,
|
|
250
|
+
})
|
|
251
|
+
const supabaseAdmin = createAdminClient({ env })
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
data: {
|
|
255
|
+
supabase,
|
|
256
|
+
supabaseAdmin,
|
|
257
|
+
userClaims: auth!.userClaims,
|
|
258
|
+
claims: auth!.claims,
|
|
259
|
+
authType: auth!.authType,
|
|
260
|
+
},
|
|
261
|
+
error: null,
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Usage
|
|
267
|
+
|
|
268
|
+
### In a Server Component
|
|
269
|
+
|
|
270
|
+
```tsx
|
|
271
|
+
// app/page.tsx
|
|
272
|
+
import { createSupabaseContext } from '@/lib/supabase/context'
|
|
273
|
+
import { redirect } from 'next/navigation'
|
|
274
|
+
|
|
275
|
+
export default async function Home() {
|
|
276
|
+
const { data: ctx, error } = await createSupabaseContext()
|
|
277
|
+
|
|
278
|
+
if (error) {
|
|
279
|
+
redirect('/auth/login')
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const { data: todos } = await ctx!.supabase.from('todos').select()
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<ul>
|
|
286
|
+
{todos?.map((t) => (
|
|
287
|
+
<li key={t.id}>{t.title}</li>
|
|
288
|
+
))}
|
|
289
|
+
</ul>
|
|
290
|
+
)
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### In a Route Handler
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
// app/api/todos/route.ts
|
|
298
|
+
import { createSupabaseContext } from '@/lib/supabase/context'
|
|
299
|
+
|
|
300
|
+
export async function GET() {
|
|
301
|
+
const { data: ctx, error } = await createSupabaseContext()
|
|
302
|
+
|
|
303
|
+
if (error) {
|
|
304
|
+
return Response.json({ message: error.message }, { status: 401 })
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const { data } = await ctx!.supabase.from('todos').select()
|
|
308
|
+
return Response.json(data)
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
### With different auth modes
|
|
313
|
+
|
|
314
|
+
```ts
|
|
315
|
+
// Public endpoint — no auth required
|
|
316
|
+
const { data: ctx } = await createSupabaseContext({ allow: 'always' })
|
|
317
|
+
|
|
318
|
+
// Accept either user JWT or skip auth
|
|
319
|
+
const { data: ctx } = await createSupabaseContext({ allow: ['user', 'always'] })
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
## Adapting for other frameworks
|
|
323
|
+
|
|
324
|
+
The adapter above is Next.js-specific only in how it reads cookies (`await cookies()` from `next/headers`). To adapt for another framework, replace the cookie-reading logic:
|
|
325
|
+
|
|
326
|
+
- **SvelteKit:** `event.cookies.get(name)` in `+page.server.ts` or `+server.ts`
|
|
327
|
+
- **Nuxt:** `useCookie(name)` in server routes, or `getCookie(event, name)` from `h3`
|
|
328
|
+
- **Remix:** `request.headers.get('cookie')` then parse with a cookie library
|
|
329
|
+
|
|
330
|
+
Everything else — env bridging, JWKS fetching, `verifyCredentials`, client creation — stays the same.
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# TypeScript Generics
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
All client-creating functions accept a `Database` generic parameter. When you pass your generated database types, every `.from('table').select()` call is fully typed — column names, return types, insert shapes, and RPC signatures.
|
|
6
|
+
|
|
7
|
+
## Generating types
|
|
8
|
+
|
|
9
|
+
Use the Supabase CLI to generate TypeScript types from your database schema:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx supabase gen types typescript --project-id your-project-ref > src/database.types.ts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
This produces a `Database` type that describes your schema.
|
|
16
|
+
|
|
17
|
+
## Using with withSupabase
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { withSupabase } from '@supabase/server'
|
|
21
|
+
import type { Database } from './database.types.ts'
|
|
22
|
+
|
|
23
|
+
export default {
|
|
24
|
+
fetch: withSupabase<Database>({ allow: 'user' }, async (_req, ctx) => {
|
|
25
|
+
// ctx.supabase is SupabaseClient<Database>
|
|
26
|
+
// Fully typed: column names, return type, etc.
|
|
27
|
+
const { data } = await ctx.supabase
|
|
28
|
+
.from('todos')
|
|
29
|
+
.select('id, title, completed')
|
|
30
|
+
// data is { id: number; title: string; completed: boolean }[] | null
|
|
31
|
+
return Response.json(data)
|
|
32
|
+
}),
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Using with createSupabaseContext
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { createSupabaseContext } from '@supabase/server'
|
|
40
|
+
import type { Database } from './database.types.ts'
|
|
41
|
+
|
|
42
|
+
const { data: ctx, error } = await createSupabaseContext<Database>(request, {
|
|
43
|
+
allow: 'user',
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
if (error) {
|
|
47
|
+
throw error
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ctx.supabase and ctx.supabaseAdmin are both SupabaseClient<Database>
|
|
51
|
+
const { data } = await ctx!.supabase.from('profiles').select('id, email')
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Using with core primitives
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import {
|
|
58
|
+
verifyAuth,
|
|
59
|
+
createContextClient,
|
|
60
|
+
createAdminClient,
|
|
61
|
+
} from '@supabase/server/core'
|
|
62
|
+
import type { Database } from './database.types.ts'
|
|
63
|
+
|
|
64
|
+
const { data: auth } = await verifyAuth(request, { allow: 'user' })
|
|
65
|
+
|
|
66
|
+
const supabase = createContextClient<Database>({
|
|
67
|
+
auth: { token: auth!.token },
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const supabaseAdmin = createAdminClient<Database>()
|
|
71
|
+
|
|
72
|
+
// Both clients are fully typed
|
|
73
|
+
const { data: todos } = await supabase.from('todos').select()
|
|
74
|
+
const { data: users } = await supabaseAdmin.from('profiles').select()
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Using with the Hono adapter
|
|
78
|
+
|
|
79
|
+
The Hono context variable is typed as `SupabaseContext` (without the generic). To get typed clients, assert the type when destructuring:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { Hono } from 'hono'
|
|
83
|
+
import { withSupabase } from '@supabase/server/adapters/hono'
|
|
84
|
+
import type { SupabaseContext } from '@supabase/server'
|
|
85
|
+
import type { Database } from './database.types.ts'
|
|
86
|
+
|
|
87
|
+
const app = new Hono()
|
|
88
|
+
|
|
89
|
+
app.use('*', withSupabase({ allow: 'user' }))
|
|
90
|
+
|
|
91
|
+
app.get('/todos', async (c) => {
|
|
92
|
+
const { supabase } = c.var.supabaseContext as SupabaseContext<Database>
|
|
93
|
+
const { data } = await supabase.from('todos').select('id, title')
|
|
94
|
+
return c.json(data)
|
|
95
|
+
})
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Custom schema
|
|
99
|
+
|
|
100
|
+
If your tables are in a schema other than `public`, pass it via `supabaseOptions`:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { withSupabase } from '@supabase/server'
|
|
104
|
+
import type { Database } from './database.types.ts'
|
|
105
|
+
|
|
106
|
+
export default {
|
|
107
|
+
fetch: withSupabase<Database>(
|
|
108
|
+
{
|
|
109
|
+
allow: 'user',
|
|
110
|
+
supabaseOptions: { db: { schema: 'api' } },
|
|
111
|
+
},
|
|
112
|
+
async (_req, ctx) => {
|
|
113
|
+
// Queries target the 'api' schema
|
|
114
|
+
const { data } = await ctx.supabase.from('todos').select()
|
|
115
|
+
return Response.json(data)
|
|
116
|
+
},
|
|
117
|
+
),
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Forwarding other Supabase client options
|
|
122
|
+
|
|
123
|
+
`supabaseOptions` accepts the same options as `createClient()` from `@supabase/supabase-js`, with two exceptions:
|
|
124
|
+
|
|
125
|
+
- `accessToken` is stripped — token injection is managed by the SDK from verified credentials
|
|
126
|
+
- Auth settings (`persistSession`, `autoRefreshToken`, `detectSessionInUrl`) are force-set to server-safe values
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
withSupabase<Database>(
|
|
130
|
+
{
|
|
131
|
+
allow: 'user',
|
|
132
|
+
supabaseOptions: {
|
|
133
|
+
db: { schema: 'api' },
|
|
134
|
+
global: {
|
|
135
|
+
headers: { 'x-custom-header': 'value' },
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
handler,
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Note: `Authorization` and `apikey` headers in `supabaseOptions.global.headers` are sanitized (removed) to prevent overriding the verified credentials.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/server",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
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",
|
|
@@ -29,11 +29,6 @@
|
|
|
29
29
|
"import": "./dist/core/index.mjs",
|
|
30
30
|
"require": "./dist/core/index.cjs"
|
|
31
31
|
},
|
|
32
|
-
"./wrappers": {
|
|
33
|
-
"types": "./dist/wrappers/index.d.mts",
|
|
34
|
-
"import": "./dist/wrappers/index.mjs",
|
|
35
|
-
"require": "./dist/wrappers/index.cjs"
|
|
36
|
-
},
|
|
37
32
|
"./adapters/hono": {
|
|
38
33
|
"types": "./dist/adapters/hono/index.d.mts",
|
|
39
34
|
"import": "./dist/adapters/hono/index.mjs",
|
|
@@ -45,7 +40,9 @@
|
|
|
45
40
|
"types": "./dist/index.d.cts",
|
|
46
41
|
"sideEffects": false,
|
|
47
42
|
"files": [
|
|
48
|
-
"dist"
|
|
43
|
+
"dist",
|
|
44
|
+
"docs",
|
|
45
|
+
"SKILL.md"
|
|
49
46
|
],
|
|
50
47
|
"engines": {
|
|
51
48
|
"node": ">=20"
|
package/dist/wrappers/index.cjs
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
-
|
|
3
|
-
//#region src/wrappers/webhook.ts
|
|
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
|
-
*/
|
|
24
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
25
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
26
|
-
name: "HMAC",
|
|
27
|
-
hash: "SHA-256"
|
|
28
|
-
}, false, ["sign"]);
|
|
29
|
-
const expected = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
30
|
-
const expectedHex = Array.from(new Uint8Array(expected)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
31
|
-
const compareKey = await crypto.subtle.importKey("raw", crypto.getRandomValues(new Uint8Array(32)), {
|
|
32
|
-
name: "HMAC",
|
|
33
|
-
hash: "SHA-256"
|
|
34
|
-
}, false, ["sign"]);
|
|
35
|
-
const [sigA, sigB] = await Promise.all([crypto.subtle.sign("HMAC", compareKey, encoder.encode(expectedHex)), crypto.subtle.sign("HMAC", compareKey, encoder.encode(signature))]);
|
|
36
|
-
const viewA = new Uint8Array(sigA);
|
|
37
|
-
const viewB = new Uint8Array(sigB);
|
|
38
|
-
if (viewA.length !== viewB.length) return false;
|
|
39
|
-
let result = 0;
|
|
40
|
-
for (let i = 0; i < viewA.length; i++) result |= viewA[i] ^ viewB[i];
|
|
41
|
-
return result === 0;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
//#endregion
|
|
45
|
-
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
*/
|
|
21
|
-
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
22
|
-
//#endregion
|
|
23
|
-
export { verifyWebhookSignature };
|
|
@@ -1,23 +0,0 @@
|
|
|
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
|
-
*/
|
|
21
|
-
declare function verifyWebhookSignature(payload: string, signature: string, secret: string): Promise<boolean>;
|
|
22
|
-
//#endregion
|
|
23
|
-
export { verifyWebhookSignature };
|
package/dist/wrappers/index.mjs
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
//#region src/wrappers/webhook.ts
|
|
2
|
-
const encoder = new TextEncoder();
|
|
3
|
-
/**
|
|
4
|
-
* Verifies a webhook signature using HMAC-SHA256 with timing-safe comparison.
|
|
5
|
-
*
|
|
6
|
-
* @param payload - The raw request body as a string.
|
|
7
|
-
* @param signature - The hex-encoded signature from the webhook header.
|
|
8
|
-
* @param secret - The shared secret used to sign webhooks.
|
|
9
|
-
* @returns `true` if the signature is valid, `false` otherwise.
|
|
10
|
-
*
|
|
11
|
-
* @example
|
|
12
|
-
* ```ts
|
|
13
|
-
* const payload = await req.text()
|
|
14
|
-
* const signature = req.headers.get('x-webhook-signature') ?? ''
|
|
15
|
-
*
|
|
16
|
-
* const isValid = await verifyWebhookSignature(payload, signature, secret)
|
|
17
|
-
* if (!isValid) {
|
|
18
|
-
* return Response.json({ error: 'Invalid signature' }, { status: 401 })
|
|
19
|
-
* }
|
|
20
|
-
* ```
|
|
21
|
-
*/
|
|
22
|
-
async function verifyWebhookSignature(payload, signature, secret) {
|
|
23
|
-
const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
|
|
24
|
-
name: "HMAC",
|
|
25
|
-
hash: "SHA-256"
|
|
26
|
-
}, false, ["sign"]);
|
|
27
|
-
const expected = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
|
|
28
|
-
const expectedHex = Array.from(new Uint8Array(expected)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
29
|
-
const compareKey = await crypto.subtle.importKey("raw", crypto.getRandomValues(new Uint8Array(32)), {
|
|
30
|
-
name: "HMAC",
|
|
31
|
-
hash: "SHA-256"
|
|
32
|
-
}, false, ["sign"]);
|
|
33
|
-
const [sigA, sigB] = await Promise.all([crypto.subtle.sign("HMAC", compareKey, encoder.encode(expectedHex)), crypto.subtle.sign("HMAC", compareKey, encoder.encode(signature))]);
|
|
34
|
-
const viewA = new Uint8Array(sigA);
|
|
35
|
-
const viewB = new Uint8Array(sigB);
|
|
36
|
-
if (viewA.length !== viewB.length) return false;
|
|
37
|
-
let result = 0;
|
|
38
|
-
for (let i = 0; i < viewA.length; i++) result |= viewA[i] ^ viewB[i];
|
|
39
|
-
return result === 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
//#endregion
|
|
43
|
-
export { verifyWebhookSignature };
|