@molecule/api-external-auth-supabase 1.0.0 → 1.0.1

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.
Files changed (2) hide show
  1. package/README.md +265 -0
  2. package/package.json +5 -4
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.0",
3
+ "version": "1.0.1",
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,10 +32,10 @@
31
32
  "@supabase/supabase-js": "2.111.0"
32
33
  },
33
34
  "peerDependencies": {
34
- "@molecule/api-external-auth": "^1.0.0"
35
+ "@molecule/api-external-auth": "^1.0.1"
35
36
  },
36
37
  "devDependencies": {
37
- "@molecule/api-external-auth": "1.0.0",
38
+ "@molecule/api-external-auth": "1.0.1",
38
39
  "@types/node": "26.1.2",
39
40
  "typescript": "6.0.3",
40
41
  "vitest": "4.1.10"