@molecule/api-external-auth-supabase 1.0.0 → 1.0.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 +265 -0
- package/package.json +7 -6
package/README.md
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
AUTO-GENERATED — DO NOT EDIT THIS FILE.
|
|
3
|
+
Generated by `mlcl sync-docs` from the package's src/index.ts JSDoc + mlcl/registry.json.
|
|
4
|
+
Edits here are overwritten on the next commit (molecule's pre-commit hook regenerates).
|
|
5
|
+
To change this document, edit the module-level JSDoc in src/index.ts.
|
|
6
|
+
Generated: 2026-08-04T01:48:11.951Z
|
|
7
|
+
-->
|
|
8
|
+
|
|
9
|
+
# @molecule/api-external-auth-supabase
|
|
10
|
+
|
|
11
|
+
> **Auto-generated, AI-first package reference** for the [molecule.dev](https://molecule.dev) ecosystem.
|
|
12
|
+
> It is written to be read by coding agents as much as by people, and is generated from this
|
|
13
|
+
> package's source — edit `src/index.ts` JSDoc, not this file.
|
|
14
|
+
|
|
15
|
+
External authentication provider using Supabase — first-class support for
|
|
16
|
+
apps imported into molecule.dev that were built on Supabase (Lovable and
|
|
17
|
+
similar).
|
|
18
|
+
|
|
19
|
+
Implements the `@molecule/api-external-auth` contract: the exported
|
|
20
|
+
`provider` verifies a user's Supabase JWT server-side via the anon client's
|
|
21
|
+
`auth.getUser(token)`. Two tiers:
|
|
22
|
+
|
|
23
|
+
- **No-secret tier** (works out of the box): token verification and
|
|
24
|
+
RLS-constrained PostgREST access via `getAnonClient()` — both using only
|
|
25
|
+
the PUBLIC anon key (`SUPABASE_ANON_KEY` / `VITE_SUPABASE_ANON_KEY` /
|
|
26
|
+
`VITE_SUPABASE_PUBLISHABLE_KEY`). No secret required.
|
|
27
|
+
- **Connected tier**: once the user supplies `SUPABASE_SERVICE_ROLE_KEY`,
|
|
28
|
+
`getServiceClient()` returns an admin client that bypasses Row Level
|
|
29
|
+
Security. Gate every admin path with `hasServiceRole()`.
|
|
30
|
+
|
|
31
|
+
Settings come from `configureSupabase()` or, lazily at first client
|
|
32
|
+
creation, from env: `SUPABASE_URL` ?? `VITE_SUPABASE_URL`,
|
|
33
|
+
`SUPABASE_ANON_KEY` ?? `VITE_SUPABASE_ANON_KEY` ??
|
|
34
|
+
`VITE_SUPABASE_PUBLISHABLE_KEY`, `SUPABASE_SERVICE_ROLE_KEY`.
|
|
35
|
+
`resetSupabase()` clears cached clients + settings (test hook).
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
// bonds.ts — wire the provider at startup:
|
|
41
|
+
import { setProvider } from '@molecule/api-external-auth'
|
|
42
|
+
import { provider } from '@molecule/api-external-auth-supabase'
|
|
43
|
+
|
|
44
|
+
setProvider(provider)
|
|
45
|
+
|
|
46
|
+
// Then server routes verify the token the frontend already sends:
|
|
47
|
+
import { verifyUserToken } from '@molecule/api-external-auth'
|
|
48
|
+
|
|
49
|
+
app.get('/api/me', async (req, res) => {
|
|
50
|
+
const token = req.headers.authorization?.replace(/^Bearer /, '') ?? ''
|
|
51
|
+
const user = await verifyUserToken(token)
|
|
52
|
+
if (!user) {
|
|
53
|
+
res.status(401).json({ error: 'Invalid or expired session.' })
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
res.json({ userId: user.userId, email: user.email })
|
|
57
|
+
})
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
// Admin operations need the service-role key — ALWAYS gate on hasServiceRole():
|
|
62
|
+
import { getServiceClient, hasServiceRole } from '@molecule/api-external-auth-supabase'
|
|
63
|
+
|
|
64
|
+
if (hasServiceRole()) {
|
|
65
|
+
const admin = getServiceClient()
|
|
66
|
+
await admin.from('profiles').update({ suspended: true }).eq('id', targetId)
|
|
67
|
+
} else {
|
|
68
|
+
// Degrade: store the flag in the provisioned DATABASE_URL Postgres,
|
|
69
|
+
// or ask the user to connect SUPABASE_SERVICE_ROLE_KEY in the
|
|
70
|
+
// Environment panel.
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Type
|
|
75
|
+
|
|
76
|
+
`provider`
|
|
77
|
+
|
|
78
|
+
## Installation
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
npm install @molecule/api-external-auth-supabase @molecule/api-external-auth @supabase/supabase-js
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## API
|
|
85
|
+
|
|
86
|
+
### Interfaces
|
|
87
|
+
|
|
88
|
+
#### `SupabaseSettings`
|
|
89
|
+
|
|
90
|
+
Connection settings for the Supabase integration.
|
|
91
|
+
|
|
92
|
+
Every field is optional — anything not set explicitly via
|
|
93
|
+
{@link configureSupabase} falls back to environment variables, read lazily
|
|
94
|
+
at first client creation (never at module load):
|
|
95
|
+
|
|
96
|
+
- `url`: `SUPABASE_URL` ?? `VITE_SUPABASE_URL`
|
|
97
|
+
- `anonKey`: `SUPABASE_ANON_KEY` ?? `VITE_SUPABASE_ANON_KEY` ??
|
|
98
|
+
`VITE_SUPABASE_PUBLISHABLE_KEY`
|
|
99
|
+
- `serviceRoleKey`: `SUPABASE_SERVICE_ROLE_KEY`
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
interface SupabaseSettings {
|
|
103
|
+
/**
|
|
104
|
+
* The Supabase project URL (e.g. `https://abcd1234.supabase.co`).
|
|
105
|
+
*/
|
|
106
|
+
url?: string
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The PUBLIC anon (publishable) key. Safe to expose in browsers — it grants
|
|
110
|
+
* only what Row Level Security allows. Sufficient for verifying user JWTs.
|
|
111
|
+
*/
|
|
112
|
+
anonKey?: string
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The SECRET service-role key. Bypasses Row Level Security — server-only,
|
|
116
|
+
* never exposed to clients. Not provisioned in molecule sandboxes by
|
|
117
|
+
* default; check {@link hasServiceRole} before relying on it.
|
|
118
|
+
*/
|
|
119
|
+
serviceRoleKey?: string
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Functions
|
|
124
|
+
|
|
125
|
+
#### `configureSupabase(settings)`
|
|
126
|
+
|
|
127
|
+
Explicitly sets (part of) the Supabase connection settings, overriding the
|
|
128
|
+
corresponding environment-variable fallbacks.
|
|
129
|
+
|
|
130
|
+
Merges with any previously configured settings, so it can be called with
|
|
131
|
+
just the piece that changed (e.g. only `serviceRoleKey` once the user
|
|
132
|
+
connects it). Invalidates the cached client singletons so the next
|
|
133
|
+
`getAnonClient()` / `getServiceClient()` call reflects the new settings.
|
|
134
|
+
Use `resetSupabase()` for a full clean slate.
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
function configureSupabase(settings: SupabaseSettings): void
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
#### `getAnonClient()`
|
|
141
|
+
|
|
142
|
+
Returns the anon-key Supabase client (lazy singleton). Created on first
|
|
143
|
+
call from the resolved settings — explicit `configureSupabase()` values
|
|
144
|
+
first, env fallbacks second (`SUPABASE_URL` ?? `VITE_SUPABASE_URL`, and
|
|
145
|
+
`SUPABASE_ANON_KEY` ?? `VITE_SUPABASE_ANON_KEY` ??
|
|
146
|
+
`VITE_SUPABASE_PUBLISHABLE_KEY`).
|
|
147
|
+
|
|
148
|
+
The anon key is PUBLIC — this client requires no secret and is constrained
|
|
149
|
+
by Row Level Security. Use it for `verifyUserToken()` and for PostgREST
|
|
150
|
+
reads/writes that RLS already permits.
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
function getAnonClient(): SupabaseClient<any, 'public', 'public', any, any>
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
#### `getServiceClient()`
|
|
157
|
+
|
|
158
|
+
Returns the service-role (admin) Supabase client (lazy singleton). Bypasses
|
|
159
|
+
Row Level Security — server-only, for admin operations the anon client
|
|
160
|
+
cannot perform.
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
function getServiceClient(): SupabaseClient<any, 'public', 'public', any, any>
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### `hasServiceRole()`
|
|
167
|
+
|
|
168
|
+
Whether a service-role key is available (explicitly configured or via
|
|
169
|
+
`SUPABASE_SERVICE_ROLE_KEY`). ALWAYS check this before calling
|
|
170
|
+
`getServiceClient()` — molecule sandboxes do NOT provision the key by
|
|
171
|
+
default, so admin paths must degrade gracefully or ask the user to
|
|
172
|
+
connect it.
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
function hasServiceRole(): boolean
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `resetSupabase()`
|
|
179
|
+
|
|
180
|
+
Test hook: clears the cached clients AND any settings set via
|
|
181
|
+
`configureSupabase()`, returning the module to its pristine state. The next
|
|
182
|
+
client creation re-reads the environment from scratch.
|
|
183
|
+
|
|
184
|
+
```typescript
|
|
185
|
+
function resetSupabase(): void
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Constants
|
|
189
|
+
|
|
190
|
+
#### `provider`
|
|
191
|
+
|
|
192
|
+
External authentication provider backed by Supabase. Wire it at startup:
|
|
193
|
+
`setProvider(provider)` from `@molecule/api-external-auth`.
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const provider: ExternalAuthProvider
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Core Interface
|
|
200
|
+
|
|
201
|
+
Implements `@molecule/api-external-auth` interface.
|
|
202
|
+
|
|
203
|
+
## Bond Wiring
|
|
204
|
+
|
|
205
|
+
Setup function to register this provider with the core interface:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
import { setProvider } from '@molecule/api-external-auth'
|
|
209
|
+
import { provider } from '@molecule/api-external-auth-supabase'
|
|
210
|
+
|
|
211
|
+
export function setupExternalAuthSupabase(): void {
|
|
212
|
+
setProvider(provider)
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Injection Notes
|
|
217
|
+
|
|
218
|
+
### Requirements
|
|
219
|
+
|
|
220
|
+
Peer dependencies:
|
|
221
|
+
|
|
222
|
+
- `@molecule/api-external-auth` ^1.0.1
|
|
223
|
+
|
|
224
|
+
### Runtime Dependencies
|
|
225
|
+
|
|
226
|
+
- `@molecule/api-external-auth`
|
|
227
|
+
- `@supabase/supabase-js`
|
|
228
|
+
|
|
229
|
+
- The anon key is PUBLIC (it ships in the browser bundle of every Supabase
|
|
230
|
+
app) — `provider.verifyUserToken()` and `getAnonClient()` need NO secret.
|
|
231
|
+
Do not treat a missing service-role key as "Supabase is unusable".
|
|
232
|
+
- `SUPABASE_SERVICE_ROLE_KEY` is NOT provisioned in molecule sandboxes and
|
|
233
|
+
never will be by default. Check `hasServiceRole()` and degrade gracefully
|
|
234
|
+
or ask the user to connect it in the Environment panel — never assume it
|
|
235
|
+
exists. Migrations/DDL against the user's hosted Supabase database cannot
|
|
236
|
+
run from the sandbox.
|
|
237
|
+
- New server-side tables belong in the provisioned `DATABASE_URL` Postgres,
|
|
238
|
+
NOT in the user's hosted Supabase — unless the user has connected
|
|
239
|
+
Supabase credentials that allow it.
|
|
240
|
+
- This package is SERVER-ONLY. It throws immediately if bundled into
|
|
241
|
+
browser/client code — import it only from server code, and never polyfill
|
|
242
|
+
`Buffer`/`process` to silence the guard.
|
|
243
|
+
|
|
244
|
+
## E2E Tests
|
|
245
|
+
|
|
246
|
+
Integration checklist — drive the real UI (live preview, no mocks), adapt
|
|
247
|
+
each item to this app's actual screens/flows, and check every box off one
|
|
248
|
+
by one. A box you can't check is an integration bug to fix — not a skip:
|
|
249
|
+
|
|
250
|
+
- [ ] The provider bond matching the app's auth platform is wired with
|
|
251
|
+
`setProvider()` at startup — no "No external-auth provider bonded" errors
|
|
252
|
+
appear in server logs when hitting protected routes.
|
|
253
|
+
- [ ] Log in through the app's existing auth UI, then hit a protected API
|
|
254
|
+
route: `verifyUserToken()` accepts the live session token and the route
|
|
255
|
+
returns that user's data.
|
|
256
|
+
- [ ] The counterparty is the auth platform itself: obtain a REAL user token
|
|
257
|
+
by signing up / logging in through the app's own UI (`interact_preview`)
|
|
258
|
+
and exercise `verifyUserToken()` with the token the frontend sends —
|
|
259
|
+
never fabricate, hand-mint, or replay a made-up token as a "valid" case.
|
|
260
|
+
- [ ] A garbage or expired Bearer token gets a clean 401 from protected
|
|
261
|
+
routes (`verifyUserToken()` → `null`) — never a 500 or a crash.
|
|
262
|
+
- [ ] A request with no Authorization header is rejected as
|
|
263
|
+
unauthenticated (401), not treated as a server error.
|
|
264
|
+
- [ ] Server-side records created for the logged-in user are keyed on the
|
|
265
|
+
verified `userId`, and another user's token never reads them back.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@molecule/api-external-auth-supabase",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "External authentication provider using Supabase for molecule.dev — verifies Supabase user JWTs server-side with the public anon key",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
}
|
|
18
18
|
},
|
|
19
19
|
"files": [
|
|
20
|
-
"dist"
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md"
|
|
21
22
|
],
|
|
22
23
|
"keywords": [
|
|
23
24
|
"molecule",
|
|
@@ -31,20 +32,20 @@
|
|
|
31
32
|
"@supabase/supabase-js": "2.111.0"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@molecule/api-external-auth": "^1.0.
|
|
35
|
+
"@molecule/api-external-auth": "^1.0.1"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@molecule/api-external-auth": "1.0.
|
|
38
|
+
"@molecule/api-external-auth": "1.0.2",
|
|
38
39
|
"@types/node": "26.1.2",
|
|
39
40
|
"typescript": "6.0.3",
|
|
40
|
-
"vitest": "4.1.
|
|
41
|
+
"vitest": "4.1.11"
|
|
41
42
|
},
|
|
42
43
|
"repository": {
|
|
43
44
|
"type": "git",
|
|
44
45
|
"url": "https://github.com/molecule-dev/molecule.git",
|
|
45
46
|
"directory": "packages/api/bonds/external-auth/supabase"
|
|
46
47
|
},
|
|
47
|
-
"homepage": "https://
|
|
48
|
+
"homepage": "https://www.molecule.dev/packages/api-external-auth-supabase",
|
|
48
49
|
"bugs": "https://github.com/molecule-dev/molecule/issues",
|
|
49
50
|
"publishConfig": {
|
|
50
51
|
"access": "public"
|