1+ import { defineMiddleware } from '@supabase/middleware'
2+ import type { Middleware } from '@supabase/middleware'
3+
14import { resourceMetadataResponse } from './responses.js'
25import { getResourceMetadataUrl , inferFunctionName } from './url.js'
36
7+ /** Shape contributed at `ctx.oauthProtectedResource`. */
8+ export interface OAuthProtectedResourceContribution {
9+ /** Absolute URL of this resource's OAuth Protected Resource Metadata document (RFC 9728). */
10+ resourceMetadataUrl : string
11+ }
12+
413/**
514 * Wraps a request handler with OAuth 2.1 Protected Resource behavior (RFC 9728)
615 * for Supabase Edge Functions.
@@ -9,11 +18,13 @@ import { getResourceMetadataUrl, inferFunctionName } from './url.js'
918 * (with permissive CORS, including the `OPTIONS` preflight, so browser-based clients can read it)
1019 * - Enriches a `401` from the inner handler with `WWW-Authenticate: Bearer resource_metadata="..."`,
1120 * unless the handler already set a `WWW-Authenticate` header (its value wins)
12- * - Returns `404` for any other path (Edge Functions are single-endpoint - the inner handler owns `/{fn}` only)
21+ * - Passes any other path through to the inner handler unchanged (composition,
22+ * not routing, decides what happens to it)
1323 *
14- * The returned handler's optional second parameter is the host's platform
15- * argument (a Workers `env`, a Deno `ServeHandlerInfo`) and is forwarded to
16- * the inner handler unchanged — required for `withSupabase` to capture it.
24+ * Contributes `ctx.oauthProtectedResource` (the resolved metadata URL) to the
25+ * downstream context. When nested under `withSupabase`, this key is present at
26+ * runtime but not yet reflected in the handler's `SupabaseContext` type — see
27+ * `withSupabase`'s type note.
1728 *
1829 * @category Middleware
1930 *
@@ -32,61 +43,75 @@ import { getResourceMetadataUrl, inferFunctionName } from './url.js'
3243 * )
3344 * ```
3445 */
35- export function withOAuthProtectedResource (
36- handler : ( req : Request , platformArg ?: unknown ) => Promise < Response > ,
37- ) : ( req : Request , platformArg ?: unknown ) => Promise < Response > {
38- return async ( req : Request , platformArg ?: unknown ) : Promise < Response > => {
39- const url = new URL ( req . url )
40- const fn = inferFunctionName ( req )
41- if ( ! fn ) return new Response ( 'Not Found' , { status : 404 } )
42- const basePath = `/${ fn } `
46+ export const withOAuthProtectedResource : Middleware <
47+ 'oauthProtectedResource' ,
48+ undefined ,
49+ Record < never , never > ,
50+ OAuthProtectedResourceContribution
51+ > = defineMiddleware <
52+ 'oauthProtectedResource' ,
53+ undefined ,
54+ Record < never , never > ,
55+ OAuthProtectedResourceContribution
56+ > ( {
57+ key : 'oauthProtectedResource' ,
58+ run : ( ) =>
59+ async function * ( req ) {
60+ const url = new URL ( req . url )
61+ const fn = inferFunctionName ( req )
62+ const metadataPath = fn ? `/${ fn } /oauth-protected-resource` : undefined
4363
44- // RFC 9728 — OAuth Protected Resource Metadata
45- if (
46- req . method === 'GET' &&
47- url . pathname === `${ basePath } /oauth-protected-resource`
48- ) {
49- return resourceMetadataResponse ( req )
50- }
64+ // RFC 9728 — OAuth Protected Resource Metadata
65+ if (
66+ metadataPath &&
67+ req . method === 'GET' &&
68+ url . pathname === metadataPath
69+ ) {
70+ return resourceMetadataResponse ( req )
71+ }
5172
52- // CORS preflight for the metadata route — browser-based clients (e.g.
53- // MCP Inspector) fetch the discovery document cross-origin.
54- if (
55- req . method === 'OPTIONS' &&
56- url . pathname === `${ basePath } /oauth-protected-resource`
57- ) {
58- return new Response ( null , {
59- status : 204 ,
60- headers : {
61- 'Access-Control-Allow-Origin' : '*' ,
62- 'Access-Control-Allow-Methods' : 'GET, OPTIONS' ,
63- 'Access-Control-Allow-Headers' : 'content-type, mcp-protocol-version' ,
64- } ,
65- } )
66- }
73+ // CORS preflight for the metadata route — browser-based clients (e.g.
74+ // MCP Inspector) fetch the discovery document cross-origin.
75+ if (
76+ metadataPath &&
77+ req . method === 'OPTIONS' &&
78+ url . pathname === metadataPath
79+ ) {
80+ return new Response ( null , {
81+ status : 204 ,
82+ headers : {
83+ 'Access-Control-Allow-Origin' : '*' ,
84+ 'Access-Control-Allow-Methods' : 'GET, OPTIONS' ,
85+ 'Access-Control-Allow-Headers' :
86+ 'content-type, mcp-protocol-version' ,
87+ } ,
88+ } )
89+ }
6790
68- if ( url . pathname !== basePath ) {
69- return new Response ( 'Not Found' , { status : 404 } )
70- }
91+ const resourceMetadataUrl = getResourceMetadataUrl ( req )
92+ const response = yield {
93+ oauthProtectedResource : { resourceMetadataUrl } ,
94+ }
7195
72- const response = await handler ( req , platformArg )
96+ // Enrich a 401 with WWW-Authenticate so clients can discover the auth
97+ // server — unless the handler already set one (its value wins, e.g. an
98+ // RFC 6750 error or a custom resource_metadata override).
99+ if (
100+ response . status === 401 &&
101+ ! response . headers . has ( 'WWW-Authenticate' )
102+ ) {
103+ const headers = new Headers ( response . headers )
104+ headers . set (
105+ 'WWW-Authenticate' ,
106+ `Bearer resource_metadata="${ resourceMetadataUrl } "` ,
107+ )
108+ return new Response ( response . body , {
109+ status : 401 ,
110+ statusText : response . statusText ,
111+ headers,
112+ } )
113+ }
73114
74- // Enrich a 401 with WWW-Authenticate so clients can discover the auth
75- // server — unless the handler already set one (its value wins, e.g. an
76- // RFC 6750 error or a custom resource_metadata override).
77- if ( response . status === 401 && ! response . headers . has ( 'WWW-Authenticate' ) ) {
78- const headers = new Headers ( response . headers )
79- headers . set (
80- 'WWW-Authenticate' ,
81- `Bearer resource_metadata="${ getResourceMetadataUrl ( req ) } "` ,
82- )
83- return new Response ( response . body , {
84- status : 401 ,
85- statusText : response . statusText ,
86- headers,
87- } )
88- }
89-
90- return response
91- }
92- }
115+ return response
116+ } ,
117+ } )
0 commit comments