@supabase/server 1.1.0-rc.66 → 1.2.0-rc.69
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 +27 -5
- package/dist/adapters/nestjs/index.cjs +118 -0
- package/dist/adapters/nestjs/index.d.cts +82 -0
- package/dist/adapters/nestjs/index.d.mts +82 -0
- package/dist/adapters/nestjs/index.mjs +116 -0
- package/docs/adapters/nestjs.md +204 -0
- package/package.json +22 -2
package/README.md
CHANGED
|
@@ -266,11 +266,12 @@ Adapters wrap `withSupabase` for a specific framework's middleware contract. The
|
|
|
266
266
|
|
|
267
267
|
> **Adapters are a community-driven initiative.** They're developed, maintained, and evolved by contributors — including responding to upstream framework changes. See [`src/adapters/README.md`](src/adapters/README.md) for the contribution requirements (tests, types, docs, build wiring) if you'd like to add or help maintain one.
|
|
268
268
|
|
|
269
|
-
| Framework | Import | Framework version
|
|
270
|
-
| --------- | ---------------------------------- |
|
|
271
|
-
| Hono | `@supabase/server/adapters/hono` | `^4.0.0`
|
|
272
|
-
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0`
|
|
273
|
-
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0`
|
|
269
|
+
| Framework | Import | Framework version | Docs |
|
|
270
|
+
| --------- | ---------------------------------- | ---------------------- | -------------------------------------------------- |
|
|
271
|
+
| Hono | `@supabase/server/adapters/hono` | `^4.0.0` | [docs/adapters/hono.md](docs/adapters/hono.md) |
|
|
272
|
+
| H3 / Nuxt | `@supabase/server/adapters/h3` | `^2.0.0` | [docs/adapters/h3.md](docs/adapters/h3.md) |
|
|
273
|
+
| Elysia | `@supabase/server/adapters/elysia` | `^1.4.0` | [docs/adapters/elysia.md](docs/adapters/elysia.md) |
|
|
274
|
+
| NestJS | `@supabase/server/adapters/nestjs` | `^10.0.0 \|\| ^11.0.0` | [docs/adapters/nestjs.md](docs/adapters/nestjs.md) |
|
|
274
275
|
|
|
275
276
|
See the per-adapter docs above for setup, per-route auth, CORS, error handling, and other patterns.
|
|
276
277
|
|
|
@@ -316,6 +317,25 @@ app.listen(3000)
|
|
|
316
317
|
|
|
317
318
|
The adapter does not handle CORS — use `@elysiajs/cors` for that.
|
|
318
319
|
|
|
320
|
+
### NestJS
|
|
321
|
+
|
|
322
|
+
```ts
|
|
323
|
+
import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
324
|
+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
325
|
+
import type { SupabaseContext } from '@supabase/server'
|
|
326
|
+
|
|
327
|
+
@Controller('games')
|
|
328
|
+
@UseGuards(withSupabase({ auth: 'user' }))
|
|
329
|
+
export class GamesController {
|
|
330
|
+
@Get()
|
|
331
|
+
list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
332
|
+
return ctx.supabase.from('favorite_games').select()
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
See [docs/adapters/nestjs.md](docs/adapters/nestjs.md) for per-route auth, exception filters, CORS, and more.
|
|
338
|
+
|
|
319
339
|
## Primitives
|
|
320
340
|
|
|
321
341
|
For when you need more control than `withSupabase` provides — multiple routes with different auth, custom response headers, or building your own wrapper.
|
|
@@ -465,6 +485,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
|
|
|
465
485
|
| `@supabase/server/adapters/hono` | `withSupabase` (Hono middleware) |
|
|
466
486
|
| `@supabase/server/adapters/h3` | `withSupabase` (H3 / Nuxt middleware) |
|
|
467
487
|
| `@supabase/server/adapters/elysia` | `withSupabase` (Elysia plugin) |
|
|
488
|
+
| `@supabase/server/adapters/nestjs` | `withSupabase` (NestJS guard), `SupabaseCtx` (param decorator) |
|
|
468
489
|
|
|
469
490
|
## Documentation
|
|
470
491
|
|
|
@@ -476,6 +497,7 @@ No. `@supabase/ssr` handles cookie-based session management for frameworks like
|
|
|
476
497
|
| How do I use this with Hono? | [`docs/adapters/hono.md`](docs/adapters/hono.md) |
|
|
477
498
|
| How do I use this with H3 / Nuxt? | [`docs/adapters/h3.md`](docs/adapters/h3.md) |
|
|
478
499
|
| How do I use this with Elysia? | [`docs/adapters/elysia.md`](docs/adapters/elysia.md) |
|
|
500
|
+
| How do I use this with NestJS? | [`docs/adapters/nestjs.md`](docs/adapters/nestjs.md) |
|
|
479
501
|
| How do I use low-level primitives for custom flows? | [`docs/core-primitives.md`](docs/core-primitives.md) |
|
|
480
502
|
| How do environment variables work across runtimes? | [`docs/environment-variables.md`](docs/environment-variables.md) |
|
|
481
503
|
| How do I handle errors? What codes exist? | [`docs/error-handling.md`](docs/error-handling.md) |
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_create_supabase_context = require('../../create-supabase-context-CKv-4AIg.cjs');
|
|
3
|
+
let _nestjs_common = require("@nestjs/common");
|
|
4
|
+
|
|
5
|
+
//#region src/adapters/nestjs/middleware.ts
|
|
6
|
+
function toWebRequest(req) {
|
|
7
|
+
const headers = new Headers();
|
|
8
|
+
for (const [name, value] of Object.entries(req.headers ?? {})) {
|
|
9
|
+
if (name.startsWith(":")) continue;
|
|
10
|
+
if (Array.isArray(value)) headers.set(name, value.join(", "));
|
|
11
|
+
else if (value != null) headers.set(name, String(value));
|
|
12
|
+
}
|
|
13
|
+
return new Request(`http://nestjs.local${req.url ?? "/"}`, { headers });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* NestJS guard that creates a {@link SupabaseContext} and stores it on the
|
|
17
|
+
* underlying request as `request.supabaseContext`.
|
|
18
|
+
*
|
|
19
|
+
* **HTTP-only.** The guard reads HTTP headers via `switchToHttp()` and throws
|
|
20
|
+
* if applied to an RPC or WebSocket handler — those transports must
|
|
21
|
+
* authenticate via context-specific mechanisms.
|
|
22
|
+
*
|
|
23
|
+
* Always runs, even if a previous guard already set the context. This matches
|
|
24
|
+
* Nest's guard order (global → controller → handler), so handler-level guards
|
|
25
|
+
* can tighten what a global guard set rather than being skipped.
|
|
26
|
+
*
|
|
27
|
+
* Throws `HttpException` on auth failure — the original `AuthError` is
|
|
28
|
+
* available via `cause`.
|
|
29
|
+
*
|
|
30
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded —
|
|
31
|
+
* use NestJS's built-in CORS (`app.enableCors()`).
|
|
32
|
+
* @returns A guard class that can be passed to `@UseGuards(...)`.
|
|
33
|
+
*
|
|
34
|
+
* @example App-wide auth via `app.useGlobalGuards()`
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { NestFactory } from '@nestjs/core'
|
|
37
|
+
* import { withSupabase } from '@supabase/server/adapters/nestjs'
|
|
38
|
+
*
|
|
39
|
+
* const app = await NestFactory.create(AppModule)
|
|
40
|
+
* app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
|
|
41
|
+
* await app.listen(3000)
|
|
42
|
+
* ```
|
|
43
|
+
*
|
|
44
|
+
* @example Per-route auth via `@UseGuards(...)`
|
|
45
|
+
* ```ts
|
|
46
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
47
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
48
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
49
|
+
*
|
|
50
|
+
* @Controller('games')
|
|
51
|
+
* export class GamesController {
|
|
52
|
+
* @Get()
|
|
53
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
54
|
+
* async list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
55
|
+
* const { data } = await ctx.supabase.from('favorite_games').select()
|
|
56
|
+
* return data
|
|
57
|
+
* }
|
|
58
|
+
* }
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
function withSupabase(config) {
|
|
62
|
+
@((0, _nestjs_common.Injectable)()) class SupabaseAuthGuard {
|
|
63
|
+
async canActivate(executionContext) {
|
|
64
|
+
const contextType = executionContext.getType();
|
|
65
|
+
if (contextType !== "http") throw new _nestjs_common.HttpException({
|
|
66
|
+
message: `withSupabase guard only supports HTTP contexts (got '${contextType}'). Authenticate non-HTTP transports separately.`,
|
|
67
|
+
code: "unsupported_context"
|
|
68
|
+
}, 500);
|
|
69
|
+
const req = executionContext.switchToHttp().getRequest();
|
|
70
|
+
const { data: ctx, error } = await require_create_supabase_context.createSupabaseContext(toWebRequest(req), config);
|
|
71
|
+
if (error) throw new _nestjs_common.HttpException({
|
|
72
|
+
message: error.message,
|
|
73
|
+
code: error.code
|
|
74
|
+
}, error.status, { cause: error });
|
|
75
|
+
req.supabaseContext = ctx;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return SupabaseAuthGuard;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/adapters/nestjs/decorator.ts
|
|
84
|
+
/**
|
|
85
|
+
* NestJS param decorator that returns the {@link SupabaseContext} attached
|
|
86
|
+
* by `withSupabase()`. Pass a key (e.g. `'supabase'`, `'userClaims'`) to pull
|
|
87
|
+
* a single field, or no argument to receive the whole context.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```ts
|
|
91
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
92
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
93
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
94
|
+
*
|
|
95
|
+
* @Controller('games')
|
|
96
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
97
|
+
* export class GamesController {
|
|
98
|
+
* @Get()
|
|
99
|
+
* list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
100
|
+
* return ctx.supabase.from('favorite_games').select()
|
|
101
|
+
* }
|
|
102
|
+
*
|
|
103
|
+
* @Get('me')
|
|
104
|
+
* me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
|
|
105
|
+
* return user
|
|
106
|
+
* }
|
|
107
|
+
* }
|
|
108
|
+
* ```
|
|
109
|
+
*/
|
|
110
|
+
const SupabaseCtx = (0, _nestjs_common.createParamDecorator)((data, ctx) => {
|
|
111
|
+
const supabaseContext = ctx.switchToHttp().getRequest().supabaseContext;
|
|
112
|
+
if (data) return supabaseContext?.[data];
|
|
113
|
+
return supabaseContext;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
//#endregion
|
|
117
|
+
exports.SupabaseCtx = SupabaseCtx;
|
|
118
|
+
exports.withSupabase = withSupabase;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-CwKZOVIv.cjs";
|
|
2
|
+
import { CanActivate, PipeTransform, Type } from "@nestjs/common";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/nestjs/middleware.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* NestJS guard that creates a {@link SupabaseContext} and stores it on the
|
|
7
|
+
* underlying request as `request.supabaseContext`.
|
|
8
|
+
*
|
|
9
|
+
* **HTTP-only.** The guard reads HTTP headers via `switchToHttp()` and throws
|
|
10
|
+
* if applied to an RPC or WebSocket handler — those transports must
|
|
11
|
+
* authenticate via context-specific mechanisms.
|
|
12
|
+
*
|
|
13
|
+
* Always runs, even if a previous guard already set the context. This matches
|
|
14
|
+
* Nest's guard order (global → controller → handler), so handler-level guards
|
|
15
|
+
* can tighten what a global guard set rather than being skipped.
|
|
16
|
+
*
|
|
17
|
+
* Throws `HttpException` on auth failure — the original `AuthError` is
|
|
18
|
+
* available via `cause`.
|
|
19
|
+
*
|
|
20
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded —
|
|
21
|
+
* use NestJS's built-in CORS (`app.enableCors()`).
|
|
22
|
+
* @returns A guard class that can be passed to `@UseGuards(...)`.
|
|
23
|
+
*
|
|
24
|
+
* @example App-wide auth via `app.useGlobalGuards()`
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { NestFactory } from '@nestjs/core'
|
|
27
|
+
* import { withSupabase } from '@supabase/server/adapters/nestjs'
|
|
28
|
+
*
|
|
29
|
+
* const app = await NestFactory.create(AppModule)
|
|
30
|
+
* app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
|
|
31
|
+
* await app.listen(3000)
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Per-route auth via `@UseGuards(...)`
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
37
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
38
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
39
|
+
*
|
|
40
|
+
* @Controller('games')
|
|
41
|
+
* export class GamesController {
|
|
42
|
+
* @Get()
|
|
43
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
44
|
+
* async list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
45
|
+
* const { data } = await ctx.supabase.from('favorite_games').select()
|
|
46
|
+
* return data
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Type<CanActivate>;
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/adapters/nestjs/decorator.d.ts
|
|
54
|
+
/**
|
|
55
|
+
* NestJS param decorator that returns the {@link SupabaseContext} attached
|
|
56
|
+
* by `withSupabase()`. Pass a key (e.g. `'supabase'`, `'userClaims'`) to pull
|
|
57
|
+
* a single field, or no argument to receive the whole context.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
62
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
63
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
64
|
+
*
|
|
65
|
+
* @Controller('games')
|
|
66
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
67
|
+
* export class GamesController {
|
|
68
|
+
* @Get()
|
|
69
|
+
* list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
70
|
+
* return ctx.supabase.from('favorite_games').select()
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* @Get('me')
|
|
74
|
+
* me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
|
|
75
|
+
* return user
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare const SupabaseCtx: (data?: keyof SupabaseContext, ...pipes: (Type<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { SupabaseCtx, withSupabase };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { d as SupabaseContext, m as WithSupabaseConfig } from "../../types-DbvLfq25.mjs";
|
|
2
|
+
import { CanActivate, PipeTransform, Type } from "@nestjs/common";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/nestjs/middleware.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* NestJS guard that creates a {@link SupabaseContext} and stores it on the
|
|
7
|
+
* underlying request as `request.supabaseContext`.
|
|
8
|
+
*
|
|
9
|
+
* **HTTP-only.** The guard reads HTTP headers via `switchToHttp()` and throws
|
|
10
|
+
* if applied to an RPC or WebSocket handler — those transports must
|
|
11
|
+
* authenticate via context-specific mechanisms.
|
|
12
|
+
*
|
|
13
|
+
* Always runs, even if a previous guard already set the context. This matches
|
|
14
|
+
* Nest's guard order (global → controller → handler), so handler-level guards
|
|
15
|
+
* can tighten what a global guard set rather than being skipped.
|
|
16
|
+
*
|
|
17
|
+
* Throws `HttpException` on auth failure — the original `AuthError` is
|
|
18
|
+
* available via `cause`.
|
|
19
|
+
*
|
|
20
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded —
|
|
21
|
+
* use NestJS's built-in CORS (`app.enableCors()`).
|
|
22
|
+
* @returns A guard class that can be passed to `@UseGuards(...)`.
|
|
23
|
+
*
|
|
24
|
+
* @example App-wide auth via `app.useGlobalGuards()`
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { NestFactory } from '@nestjs/core'
|
|
27
|
+
* import { withSupabase } from '@supabase/server/adapters/nestjs'
|
|
28
|
+
*
|
|
29
|
+
* const app = await NestFactory.create(AppModule)
|
|
30
|
+
* app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
|
|
31
|
+
* await app.listen(3000)
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example Per-route auth via `@UseGuards(...)`
|
|
35
|
+
* ```ts
|
|
36
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
37
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
38
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
39
|
+
*
|
|
40
|
+
* @Controller('games')
|
|
41
|
+
* export class GamesController {
|
|
42
|
+
* @Get()
|
|
43
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
44
|
+
* async list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
45
|
+
* const { data } = await ctx.supabase.from('favorite_games').select()
|
|
46
|
+
* return data
|
|
47
|
+
* }
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare function withSupabase(config?: Omit<WithSupabaseConfig, 'cors'>): Type<CanActivate>;
|
|
52
|
+
//#endregion
|
|
53
|
+
//#region src/adapters/nestjs/decorator.d.ts
|
|
54
|
+
/**
|
|
55
|
+
* NestJS param decorator that returns the {@link SupabaseContext} attached
|
|
56
|
+
* by `withSupabase()`. Pass a key (e.g. `'supabase'`, `'userClaims'`) to pull
|
|
57
|
+
* a single field, or no argument to receive the whole context.
|
|
58
|
+
*
|
|
59
|
+
* @example
|
|
60
|
+
* ```ts
|
|
61
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
62
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
63
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
64
|
+
*
|
|
65
|
+
* @Controller('games')
|
|
66
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
67
|
+
* export class GamesController {
|
|
68
|
+
* @Get()
|
|
69
|
+
* list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
70
|
+
* return ctx.supabase.from('favorite_games').select()
|
|
71
|
+
* }
|
|
72
|
+
*
|
|
73
|
+
* @Get('me')
|
|
74
|
+
* me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
|
|
75
|
+
* return user
|
|
76
|
+
* }
|
|
77
|
+
* }
|
|
78
|
+
* ```
|
|
79
|
+
*/
|
|
80
|
+
declare const SupabaseCtx: (data?: keyof SupabaseContext, ...pipes: (Type<PipeTransform> | PipeTransform)[]) => ParameterDecorator;
|
|
81
|
+
//#endregion
|
|
82
|
+
export { SupabaseCtx, withSupabase };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { t as createSupabaseContext } from "../../create-supabase-context-BxSEJN8a.mjs";
|
|
2
|
+
import { HttpException, Injectable, createParamDecorator } from "@nestjs/common";
|
|
3
|
+
|
|
4
|
+
//#region src/adapters/nestjs/middleware.ts
|
|
5
|
+
function toWebRequest(req) {
|
|
6
|
+
const headers = new Headers();
|
|
7
|
+
for (const [name, value] of Object.entries(req.headers ?? {})) {
|
|
8
|
+
if (name.startsWith(":")) continue;
|
|
9
|
+
if (Array.isArray(value)) headers.set(name, value.join(", "));
|
|
10
|
+
else if (value != null) headers.set(name, String(value));
|
|
11
|
+
}
|
|
12
|
+
return new Request(`http://nestjs.local${req.url ?? "/"}`, { headers });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* NestJS guard that creates a {@link SupabaseContext} and stores it on the
|
|
16
|
+
* underlying request as `request.supabaseContext`.
|
|
17
|
+
*
|
|
18
|
+
* **HTTP-only.** The guard reads HTTP headers via `switchToHttp()` and throws
|
|
19
|
+
* if applied to an RPC or WebSocket handler — those transports must
|
|
20
|
+
* authenticate via context-specific mechanisms.
|
|
21
|
+
*
|
|
22
|
+
* Always runs, even if a previous guard already set the context. This matches
|
|
23
|
+
* Nest's guard order (global → controller → handler), so handler-level guards
|
|
24
|
+
* can tighten what a global guard set rather than being skipped.
|
|
25
|
+
*
|
|
26
|
+
* Throws `HttpException` on auth failure — the original `AuthError` is
|
|
27
|
+
* available via `cause`.
|
|
28
|
+
*
|
|
29
|
+
* @param config - Auth modes and optional environment overrides. CORS is excluded —
|
|
30
|
+
* use NestJS's built-in CORS (`app.enableCors()`).
|
|
31
|
+
* @returns A guard class that can be passed to `@UseGuards(...)`.
|
|
32
|
+
*
|
|
33
|
+
* @example App-wide auth via `app.useGlobalGuards()`
|
|
34
|
+
* ```ts
|
|
35
|
+
* import { NestFactory } from '@nestjs/core'
|
|
36
|
+
* import { withSupabase } from '@supabase/server/adapters/nestjs'
|
|
37
|
+
*
|
|
38
|
+
* const app = await NestFactory.create(AppModule)
|
|
39
|
+
* app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
|
|
40
|
+
* await app.listen(3000)
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
43
|
+
* @example Per-route auth via `@UseGuards(...)`
|
|
44
|
+
* ```ts
|
|
45
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
46
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
47
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
48
|
+
*
|
|
49
|
+
* @Controller('games')
|
|
50
|
+
* export class GamesController {
|
|
51
|
+
* @Get()
|
|
52
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
53
|
+
* async list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
54
|
+
* const { data } = await ctx.supabase.from('favorite_games').select()
|
|
55
|
+
* return data
|
|
56
|
+
* }
|
|
57
|
+
* }
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
function withSupabase(config) {
|
|
61
|
+
@Injectable() class SupabaseAuthGuard {
|
|
62
|
+
async canActivate(executionContext) {
|
|
63
|
+
const contextType = executionContext.getType();
|
|
64
|
+
if (contextType !== "http") throw new HttpException({
|
|
65
|
+
message: `withSupabase guard only supports HTTP contexts (got '${contextType}'). Authenticate non-HTTP transports separately.`,
|
|
66
|
+
code: "unsupported_context"
|
|
67
|
+
}, 500);
|
|
68
|
+
const req = executionContext.switchToHttp().getRequest();
|
|
69
|
+
const { data: ctx, error } = await createSupabaseContext(toWebRequest(req), config);
|
|
70
|
+
if (error) throw new HttpException({
|
|
71
|
+
message: error.message,
|
|
72
|
+
code: error.code
|
|
73
|
+
}, error.status, { cause: error });
|
|
74
|
+
req.supabaseContext = ctx;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return SupabaseAuthGuard;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
//#endregion
|
|
82
|
+
//#region src/adapters/nestjs/decorator.ts
|
|
83
|
+
/**
|
|
84
|
+
* NestJS param decorator that returns the {@link SupabaseContext} attached
|
|
85
|
+
* by `withSupabase()`. Pass a key (e.g. `'supabase'`, `'userClaims'`) to pull
|
|
86
|
+
* a single field, or no argument to receive the whole context.
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
91
|
+
* import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
92
|
+
* import type { SupabaseContext } from '@supabase/server'
|
|
93
|
+
*
|
|
94
|
+
* @Controller('games')
|
|
95
|
+
* @UseGuards(withSupabase({ auth: 'user' }))
|
|
96
|
+
* export class GamesController {
|
|
97
|
+
* @Get()
|
|
98
|
+
* list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
99
|
+
* return ctx.supabase.from('favorite_games').select()
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* @Get('me')
|
|
103
|
+
* me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
|
|
104
|
+
* return user
|
|
105
|
+
* }
|
|
106
|
+
* }
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
const SupabaseCtx = createParamDecorator((data, ctx) => {
|
|
110
|
+
const supabaseContext = ctx.switchToHttp().getRequest().supabaseContext;
|
|
111
|
+
if (data) return supabaseContext?.[data];
|
|
112
|
+
return supabaseContext;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
export { SupabaseCtx, withSupabase };
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# NestJS Adapter
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Install NestJS as a peer dependency:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @nestjs/common @nestjs/core
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The adapter exports `withSupabase` (a guard factory) and `SupabaseCtx` (a param decorator). Together they replace the `c.var.supabaseContext` / `event.context.supabaseContext` patterns from the Hono and H3 adapters.
|
|
12
|
+
|
|
13
|
+
`withSupabase(config)` returns a `CanActivate` guard class. The guard reads the underlying request (Express or Fastify), verifies credentials with `@supabase/server/core`, and attaches the resulting `SupabaseContext` to `request.supabaseContext`. From any handler you can pull it out with `@SupabaseCtx()`.
|
|
14
|
+
|
|
15
|
+
## Basic controller with auth
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
// games.controller.ts
|
|
19
|
+
import { Controller, Get, UseGuards } from '@nestjs/common'
|
|
20
|
+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
21
|
+
import type { SupabaseContext } from '@supabase/server'
|
|
22
|
+
|
|
23
|
+
@Controller('games')
|
|
24
|
+
@UseGuards(withSupabase({ auth: 'user' }))
|
|
25
|
+
export class GamesController {
|
|
26
|
+
@Get()
|
|
27
|
+
async list(@SupabaseCtx() ctx: SupabaseContext) {
|
|
28
|
+
const { data } = await ctx.supabase.from('favorite_games').select()
|
|
29
|
+
return data
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
@Get('me')
|
|
33
|
+
me(@SupabaseCtx('userClaims') user: SupabaseContext['userClaims']) {
|
|
34
|
+
return user
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`@SupabaseCtx()` returns the entire `SupabaseContext` (`supabase`, `supabaseAdmin`, `userClaims`, `jwtClaims`, `authMode`, `authKeyName`). Pass a key (`@SupabaseCtx('supabase')`) to extract a single field.
|
|
40
|
+
|
|
41
|
+
### Typing your database
|
|
42
|
+
|
|
43
|
+
The guard does not thread a `Database` generic, so `@SupabaseCtx()` resolves to `SupabaseContext<unknown>` by default. To get typed table access, annotate the parameter at the handler:
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import type { SupabaseContext } from '@supabase/server'
|
|
47
|
+
import type { Database } from './database.types'
|
|
48
|
+
|
|
49
|
+
@Get()
|
|
50
|
+
async list(@SupabaseCtx() ctx: SupabaseContext<Database>) {
|
|
51
|
+
const { data } = await ctx.supabase.from('favorite_games').select()
|
|
52
|
+
return data
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Per-route auth
|
|
57
|
+
|
|
58
|
+
Apply different auth modes per controller or per handler — the closest `@UseGuards()` wins:
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
import { Controller, Get, Post, UseGuards } from '@nestjs/common'
|
|
62
|
+
import { withSupabase, SupabaseCtx } from '@supabase/server/adapters/nestjs'
|
|
63
|
+
import type { SupabaseContext } from '@supabase/server'
|
|
64
|
+
|
|
65
|
+
@Controller()
|
|
66
|
+
export class AppController {
|
|
67
|
+
// Public — no guard
|
|
68
|
+
@Get('health')
|
|
69
|
+
health() {
|
|
70
|
+
return { status: 'ok' }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// User-authenticated route
|
|
74
|
+
@Get('todos')
|
|
75
|
+
@UseGuards(withSupabase({ auth: 'user' }))
|
|
76
|
+
async todos(@SupabaseCtx() ctx: SupabaseContext) {
|
|
77
|
+
const { data } = await ctx.supabase.from('todos').select()
|
|
78
|
+
return data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Secret-key-protected admin route
|
|
82
|
+
@Post('admin/sync')
|
|
83
|
+
@UseGuards(withSupabase({ auth: 'secret' }))
|
|
84
|
+
async sync(@SupabaseCtx() ctx: SupabaseContext) {
|
|
85
|
+
const { data } = await ctx.supabaseAdmin
|
|
86
|
+
.from('audit_log')
|
|
87
|
+
.insert({ action: 'sync' })
|
|
88
|
+
return data
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Dual auth — users or services
|
|
92
|
+
@Get('reports')
|
|
93
|
+
@UseGuards(withSupabase({ auth: ['user', 'secret'] }))
|
|
94
|
+
reports(@SupabaseCtx('authMode') authMode: SupabaseContext['authMode']) {
|
|
95
|
+
return { authMode }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## App-wide guard
|
|
101
|
+
|
|
102
|
+
Apply the guard globally with `app.useGlobalGuards()`:
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
// main.ts
|
|
106
|
+
import { NestFactory } from '@nestjs/core'
|
|
107
|
+
import { withSupabase } from '@supabase/server/adapters/nestjs'
|
|
108
|
+
import { AppModule } from './app.module'
|
|
109
|
+
|
|
110
|
+
async function bootstrap() {
|
|
111
|
+
const app = await NestFactory.create(AppModule)
|
|
112
|
+
app.useGlobalGuards(new (withSupabase({ auth: 'user' }))())
|
|
113
|
+
await app.listen(3000)
|
|
114
|
+
}
|
|
115
|
+
bootstrap()
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Multiple guards
|
|
119
|
+
|
|
120
|
+
`withSupabase` always runs, even if a previous guard already set `request.supabaseContext`. NestJS executes guards in order (global → controller → handler), so a handler-level guard naturally tightens what a global guard set: the later guard re-authenticates with its own config and either rejects the request or overwrites the context. The innermost guard wins.
|
|
121
|
+
|
|
122
|
+
If you need different auth per route, prefer per-route `@UseGuards(...)` without a global guard.
|
|
123
|
+
|
|
124
|
+
## CORS
|
|
125
|
+
|
|
126
|
+
The NestJS adapter does not handle CORS. Use NestJS's built-in CORS:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
// main.ts
|
|
130
|
+
import { NestFactory } from '@nestjs/core'
|
|
131
|
+
import { AppModule } from './app.module'
|
|
132
|
+
|
|
133
|
+
async function bootstrap() {
|
|
134
|
+
const app = await NestFactory.create(AppModule)
|
|
135
|
+
app.enableCors({ origin: 'https://myapp.com' })
|
|
136
|
+
await app.listen(3000)
|
|
137
|
+
}
|
|
138
|
+
bootstrap()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The `cors` option is excluded from `WithSupabaseConfig` for this adapter.
|
|
142
|
+
|
|
143
|
+
## Error handling
|
|
144
|
+
|
|
145
|
+
When auth fails, the adapter throws a NestJS `HttpException`. The original `AuthError` is available via `cause`. Add an exception filter to format the response:
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
// supabase-auth.filter.ts
|
|
149
|
+
import {
|
|
150
|
+
ArgumentsHost,
|
|
151
|
+
Catch,
|
|
152
|
+
ExceptionFilter,
|
|
153
|
+
HttpException,
|
|
154
|
+
} from '@nestjs/common'
|
|
155
|
+
import { AuthError } from '@supabase/server'
|
|
156
|
+
import type { Response } from 'express'
|
|
157
|
+
|
|
158
|
+
@Catch(HttpException)
|
|
159
|
+
export class SupabaseAuthFilter implements ExceptionFilter {
|
|
160
|
+
catch(exception: HttpException, host: ArgumentsHost) {
|
|
161
|
+
const cause = exception.cause
|
|
162
|
+
if (!(cause instanceof AuthError)) throw exception
|
|
163
|
+
|
|
164
|
+
const res = host.switchToHttp().getResponse<Response>()
|
|
165
|
+
res.status(cause.status).json({
|
|
166
|
+
error: cause.message,
|
|
167
|
+
code: cause.code,
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Register it globally:
|
|
174
|
+
|
|
175
|
+
```ts
|
|
176
|
+
// main.ts
|
|
177
|
+
app.useGlobalFilters(new SupabaseAuthFilter())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## Environment overrides
|
|
181
|
+
|
|
182
|
+
Pass `env` to override auto-detected environment variables:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
@UseGuards(
|
|
186
|
+
withSupabase({
|
|
187
|
+
auth: 'user',
|
|
188
|
+
env: { url: 'http://localhost:54321' },
|
|
189
|
+
}),
|
|
190
|
+
)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Supabase client options
|
|
194
|
+
|
|
195
|
+
Forward options to the underlying `createClient()` calls:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
@UseGuards(
|
|
199
|
+
withSupabase({
|
|
200
|
+
auth: 'user',
|
|
201
|
+
supabaseOptions: { db: { schema: 'api' } },
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@supabase/server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0-rc.69",
|
|
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",
|
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
"import": "./dist/adapters/elysia/index.mjs",
|
|
45
45
|
"require": "./dist/adapters/elysia/index.cjs"
|
|
46
46
|
},
|
|
47
|
+
"./adapters/nestjs": {
|
|
48
|
+
"types": "./dist/adapters/nestjs/index.d.mts",
|
|
49
|
+
"import": "./dist/adapters/nestjs/index.mjs",
|
|
50
|
+
"require": "./dist/adapters/nestjs/index.cjs"
|
|
51
|
+
},
|
|
47
52
|
"./package.json": "./package.json"
|
|
48
53
|
},
|
|
49
54
|
"main": "./dist/index.cjs",
|
|
@@ -68,19 +73,23 @@
|
|
|
68
73
|
"prepare": "simple-git-hooks",
|
|
69
74
|
"test": "vitest run",
|
|
70
75
|
"test:watch": "vitest",
|
|
71
|
-
"typecheck": "tsc --noEmit"
|
|
76
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p src/adapters/nestjs"
|
|
72
77
|
},
|
|
73
78
|
"simple-git-hooks": {
|
|
74
79
|
"pre-commit": "pnpm pretty-quick --staged",
|
|
75
80
|
"commit-msg": "pnpm commitlint --edit \"$1\""
|
|
76
81
|
},
|
|
77
82
|
"peerDependencies": {
|
|
83
|
+
"@nestjs/common": "^10.0.0 || ^11.0.0",
|
|
78
84
|
"@supabase/supabase-js": "^2.0.0",
|
|
79
85
|
"h3": "^2.0.0",
|
|
80
86
|
"hono": "^4.0.0",
|
|
81
87
|
"elysia": "^1.4.0"
|
|
82
88
|
},
|
|
83
89
|
"peerDependenciesMeta": {
|
|
90
|
+
"@nestjs/common": {
|
|
91
|
+
"optional": true
|
|
92
|
+
},
|
|
84
93
|
"h3": {
|
|
85
94
|
"optional": true
|
|
86
95
|
},
|
|
@@ -94,18 +103,29 @@
|
|
|
94
103
|
"devDependencies": {
|
|
95
104
|
"@commitlint/cli": "^20.4.2",
|
|
96
105
|
"@commitlint/config-conventional": "^20.4.2",
|
|
106
|
+
"@nestjs/common": "^11.1.19",
|
|
107
|
+
"@nestjs/core": "^11.1.19",
|
|
108
|
+
"@nestjs/platform-express": "^11.1.19",
|
|
109
|
+
"@nestjs/platform-fastify": "^11.1.19",
|
|
110
|
+
"@nestjs/testing": "^11.1.19",
|
|
97
111
|
"@supabase/supabase-js": "^2.105.4",
|
|
112
|
+
"@swc/core": "^1.15.33",
|
|
113
|
+
"@types/supertest": "^7.2.0",
|
|
98
114
|
"eslint": "^10.0.2",
|
|
99
115
|
"elysia": "^1.4.0",
|
|
100
116
|
"h3": "2.0.1-rc.20",
|
|
101
117
|
"hono": "^4.12.5",
|
|
102
118
|
"prettier": "3.8.1",
|
|
103
119
|
"pretty-quick": "^4.2.2",
|
|
120
|
+
"reflect-metadata": "^0.2.2",
|
|
121
|
+
"rxjs": "^7.8.2",
|
|
104
122
|
"simple-git-hooks": "^2.13.1",
|
|
123
|
+
"supertest": "^7.2.2",
|
|
105
124
|
"tsdown": "^0.20.3",
|
|
106
125
|
"typedoc": "^0.28.19",
|
|
107
126
|
"typescript": "^5.9.3",
|
|
108
127
|
"typescript-eslint": "^8.56.1",
|
|
128
|
+
"unplugin-swc": "^1.5.9",
|
|
109
129
|
"vitest": "^4.0.18"
|
|
110
130
|
},
|
|
111
131
|
"dependencies": {
|