11import { buildCorsHeaders , addCorsHeaders } from './cors.js'
22import { createSupabaseContext } from './create-supabase-context.js'
33import type { SupabaseContext , WithSupabaseConfig } from './types.js'
4+ import type { Entry } from '@supabase/web-middleware'
5+
6+ type AnyEntry = Entry < string , object , unknown >
7+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
8+ type AnyHandler = ( req : Request , ctx : any ) => Promise < Response >
9+
10+ /**
11+ * Accumulate the ctx contributions of a plugin tuple — same logic as
12+ * `pipeline`'s internal `Accumulate`, seeded from `object` (no `BaseContext`
13+ * or `_runtime` in the visible ctx type; see implementation note below).
14+ */
15+ type PluginsCtx < Plugins extends readonly AnyEntry [ ] > =
16+ Plugins extends readonly [
17+ Entry < infer Key extends string , object , infer Contribution > ,
18+ ...infer Rest ,
19+ ]
20+ ? Rest extends readonly AnyEntry [ ]
21+ ? { [ P in Key ] : Contribution } & PluginsCtx < Rest >
22+ : { [ P in Key ] : Contribution }
23+ : object
424
525/**
626 * Wraps a request handler with Supabase auth, client creation, and CORS handling.
@@ -17,6 +37,7 @@ import type { SupabaseContext, WithSupabaseConfig } from './types.js'
1737 * ```ts
1838 * import { withSupabase } from '@supabase/server'
1939 *
40+ * // Without plugins — existing API, unchanged.
2041 * export default {
2142 * fetch: withSupabase({ auth: 'user' }, async (req, ctx) => {
2243 * const { data } = await ctx.supabase.rpc('get_my_profile')
@@ -28,6 +49,55 @@ import type { SupabaseContext, WithSupabaseConfig } from './types.js'
2849export function withSupabase < Database = unknown > (
2950 config : WithSupabaseConfig ,
3051 handler : ( req : Request , ctx : SupabaseContext < Database > ) => Promise < Response > ,
52+ ) : ( req : Request ) => Promise < Response >
53+
54+ /**
55+ * Variant that accepts a `plugins` array — each `withFoo(config)` call returns
56+ * an `Entry` from `@supabase/web-middleware`. Plugins run **after** the Supabase
57+ * context is established; they receive `ctx.supabase`, `ctx.userClaims`, etc.
58+ * already present and contribute their own typed keys on top.
59+ *
60+ * @example
61+ * ```ts
62+ * import { withSupabase } from '@supabase/server'
63+ * import { withGuestbook } from '@supabase/plugin-guestbook/server'
64+ * import { withRateLimit } from '@supabase/plugin-rate-limit/server'
65+ *
66+ * export default {
67+ * fetch: withSupabase(
68+ * { auth: 'user', plugins: [withRateLimit({ rpm: 100 }), withGuestbook()] },
69+ * async (req, ctx) => {
70+ * ctx.supabase // from @supabase /server
71+ * ctx.rateLimit // from withRateLimit
72+ * ctx.guestbook // from withGuestbook
73+ * return Response.json(await ctx.guestbook.list())
74+ * },
75+ * ),
76+ * }
77+ * ```
78+ *
79+ * **Type note.** `PluginsCtx<Plugins>` accumulates the key contributions of the
80+ * plugins array. Plugins that declare `In` prerequisites on Supabase-provided
81+ * keys (`supabase`, `userClaims`, …) satisfy those at runtime (the Supabase
82+ * context is merged before plugins run) but not at the type level — a full
83+ * implementation would widen the prerequisite-validation seed to include
84+ * `SupabaseContext`. Ordering and collision checks within the plugins array work
85+ * normally via `web-middleware`'s runtime chain.
86+ */
87+ export function withSupabase <
88+ Database = unknown ,
89+ const Plugins extends readonly AnyEntry [ ] = readonly AnyEntry [ ] ,
90+ > (
91+ config : WithSupabaseConfig & { plugins : Plugins } ,
92+ handler : (
93+ req : Request ,
94+ ctx : SupabaseContext < Database > & PluginsCtx < Plugins > ,
95+ ) => Promise < Response > ,
96+ ) : ( req : Request ) => Promise < Response >
97+
98+ export function withSupabase < Database = unknown > (
99+ config : WithSupabaseConfig & { plugins ?: readonly AnyEntry [ ] } ,
100+ handler : AnyHandler ,
31101) : ( req : Request ) => Promise < Response > {
32102 return async ( req : Request ) => {
33103 if ( config . cors !== false && req . method === 'OPTIONS' ) {
@@ -51,7 +121,29 @@ export function withSupabase<Database = unknown>(
51121 )
52122 }
53123
54- const response = await handler ( req , ctx )
124+ let response : Response
125+ if ( config . plugins ?. length ) {
126+ // Compose plugins around the handler — same fold as pipeline's reduceRight,
127+ // but without calling pipeline() so we supply the seeded ctx ourselves.
128+ const composed = (
129+ config . plugins as readonly AnyEntry [ ]
130+ ) . reduceRight < AnyHandler > ( ( h , entry ) => entry ( h ) , handler )
131+ // Seed _runtime so web-middleware entries recognise this as an upstream
132+ // context (isContext() checks for _runtime.getEnv). Falls through to
133+ // process.env; a full implementation would bridge to SupabaseEnv.
134+ const g = globalThis as {
135+ process ?: { env ?: Record < string , string | undefined > }
136+ }
137+ response = await composed ( req , {
138+ ...ctx ,
139+ _runtime : {
140+ name : 'unknown' as const ,
141+ getEnv : ( key : string ) : string | undefined => g . process ?. env ?. [ key ] ,
142+ } ,
143+ } )
144+ } else {
145+ response = await handler ( req , ctx as object )
146+ }
55147
56148 if ( config . cors !== false ) {
57149 return addCorsHeaders ( response , config . cors )
0 commit comments