Skip to content

Commit 9e402e1

Browse files
mandariniclaude
andcommitted
feat(middleware): add postgres and claims middleware entrypoints
Graduate withPostgres and withClaims out of plugin-examples into @supabase/server/middleware/*, so the PRFAQ's built-in middleware ship from the package instead of example-local code (SDK-1163 item 5). - withPostgres reads claims from ctx.jwtClaims (already populated by withSupabase), so `middleware: [withPostgres()]` works with no separate withClaims. Keeps the RLS role-clamp and tx-local request.jwt.claims injection. pg is an optional peer dep (Node/Deno only, not Workers). - withClaims ships for the standalone agnostic pipeline() case (demo-only: no signature verification). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a1d9563 commit 9e402e1

8 files changed

Lines changed: 535 additions & 27 deletions

File tree

jsr.json

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,17 +8,12 @@
88
"./adapters/hono": "./src/adapters/hono/index.ts",
99
"./adapters/h3": "./src/adapters/h3/index.ts",
1010
"./adapters/elysia": "./src/adapters/elysia/index.ts",
11-
"./adapters/nestjs": "./src/adapters/nestjs/index.ts"
11+
"./adapters/nestjs": "./src/adapters/nestjs/index.ts",
12+
"./middleware/postgres": "./src/middleware/postgres/index.ts",
13+
"./middleware/claims": "./src/middleware/claims/index.ts"
1214
},
1315
"publish": {
14-
"include": [
15-
"src/**/*.ts",
16-
"README.md",
17-
"LICENSE"
18-
],
19-
"exclude": [
20-
"src/**/*.test.ts",
21-
"src/**/*.spec.ts"
22-
]
16+
"include": ["src/**/*.ts", "README.md", "LICENSE"],
17+
"exclude": ["src/**/*.test.ts", "src/**/*.spec.ts"]
2318
}
2419
}

package.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,26 @@
8989
"default": "./dist/adapters/nestjs/index.cjs"
9090
}
9191
},
92+
"./middleware/postgres": {
93+
"import": {
94+
"types": "./dist/middleware/postgres/index.d.mts",
95+
"default": "./dist/middleware/postgres/index.mjs"
96+
},
97+
"require": {
98+
"types": "./dist/middleware/postgres/index.d.cts",
99+
"default": "./dist/middleware/postgres/index.cjs"
100+
}
101+
},
102+
"./middleware/claims": {
103+
"import": {
104+
"types": "./dist/middleware/claims/index.d.mts",
105+
"default": "./dist/middleware/claims/index.mjs"
106+
},
107+
"require": {
108+
"types": "./dist/middleware/claims/index.d.cts",
109+
"default": "./dist/middleware/claims/index.cjs"
110+
}
111+
},
92112
"./package.json": "./package.json"
93113
},
94114
"main": "./dist/index.cjs",
@@ -125,7 +145,8 @@
125145
"@supabase/supabase-js": "^2.0.0",
126146
"h3": "^2.0.0",
127147
"hono": "^4.0.0",
128-
"elysia": "^1.4.0"
148+
"elysia": "^1.4.0",
149+
"pg": "^8.0.0"
129150
},
130151
"peerDependenciesMeta": {
131152
"@nestjs/common": {
@@ -139,6 +160,9 @@
139160
},
140161
"elysia": {
141162
"optional": true
163+
},
164+
"pg": {
165+
"optional": true
142166
}
143167
},
144168
"devDependencies": {
@@ -151,6 +175,7 @@
151175
"@nestjs/testing": "^11.1.19",
152176
"@supabase/supabase-js": "^2.105.4",
153177
"@swc/core": "^1.15.33",
178+
"@types/pg": "^8.11.0",
154179
"@types/supertest": "^7.2.0",
155180
"eslint": "^10.0.2",
156181
"elysia": "^1.4.0",

pnpm-lock.yaml

Lines changed: 121 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { withClaims } from './index.js'
4+
5+
function base64url(obj: unknown): string {
6+
return Buffer.from(JSON.stringify(obj))
7+
.toString('base64')
8+
.replace(/\+/g, '-')
9+
.replace(/\//g, '_')
10+
.replace(/=+$/, '')
11+
}
12+
13+
function tokenFor(claims: Record<string, unknown>): string {
14+
return `header.${base64url(claims)}.sig`
15+
}
16+
17+
const runtime = { name: 'node' as const, getEnv: () => undefined }
18+
19+
describe('withClaims', () => {
20+
it('decodes the Bearer token payload into ctx.jwtClaims', async () => {
21+
let seen: unknown
22+
const handler = withClaims(async (_req, ctx) => {
23+
seen = ctx.jwtClaims
24+
return Response.json({ ok: true })
25+
})
26+
27+
await handler(
28+
new Request('http://localhost', {
29+
headers: {
30+
Authorization: `Bearer ${tokenFor({ sub: 'u1', role: 'authenticated' })}`,
31+
},
32+
}),
33+
{ _runtime: runtime },
34+
)
35+
36+
expect(seen).toEqual({ sub: 'u1', role: 'authenticated' })
37+
})
38+
39+
it('contributes null when no Authorization header is present', async () => {
40+
let seen: unknown = 'unset'
41+
const handler = withClaims(async (_req, ctx) => {
42+
seen = ctx.jwtClaims
43+
return Response.json({ ok: true })
44+
})
45+
46+
await handler(new Request('http://localhost'), { _runtime: runtime })
47+
48+
expect(seen).toBeNull()
49+
})
50+
51+
it('contributes null for a malformed token', async () => {
52+
let seen: unknown = 'unset'
53+
const handler = withClaims(async (_req, ctx) => {
54+
seen = ctx.jwtClaims
55+
return Response.json({ ok: true })
56+
})
57+
58+
await handler(
59+
new Request('http://localhost', {
60+
headers: { Authorization: 'Bearer not-a-jwt' },
61+
}),
62+
{ _runtime: runtime },
63+
)
64+
65+
expect(seen).toBeNull()
66+
})
67+
})

0 commit comments

Comments
 (0)