@exposify/supabase 0.1.0 → 0.3.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.
- package/LICENSE +21 -0
- package/README.md +122 -23
- package/dist/cli.js +412 -75
- package/dist/index.cjs +34 -25
- package/dist/index.d.cts +40 -19
- package/dist/index.d.ts +40 -19
- package/dist/index.js +32 -23
- package/package.json +12 -3
- package/sql/{agentify_meta.sql → exposify_meta.sql} +12 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ali
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@ root.
|
|
|
15
15
|
|
|
16
16
|
- A Supabase project (local via the [Supabase CLI](https://supabase.com/docs/guides/cli), or hosted).
|
|
17
17
|
- The Supabase CLI (`supabase` on your `PATH`) for `db push` / `functions deploy`.
|
|
18
|
-
- Node or Yarn to run `npx
|
|
18
|
+
- Node or Yarn to run `npx exposify-supabase` (the CLI is a regular npm
|
|
19
19
|
package; the edge function itself runs on Deno and needs no local install).
|
|
20
20
|
|
|
21
21
|
## 1. Scaffold
|
|
@@ -23,13 +23,16 @@ root.
|
|
|
23
23
|
From your Supabase project's root (the directory containing `supabase/`):
|
|
24
24
|
|
|
25
25
|
```bash
|
|
26
|
-
npx
|
|
26
|
+
npx exposify-supabase init
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
This
|
|
29
|
+
This is safe to re-run — it never overwrites an existing function file, and
|
|
30
|
+
never scaffolds a second copy of the migration if one's already there
|
|
31
|
+
(checked by filename, not content: migrations are meant to be immutable
|
|
32
|
+
once written, so a re-run skips rather than rewrites). It writes:
|
|
30
33
|
|
|
31
|
-
- `supabase/migrations/<timestamp>
|
|
32
|
-
adds `
|
|
34
|
+
- `supabase/migrations/<timestamp>_exposify_meta.sql` — a SQL migration that
|
|
35
|
+
adds `exposify_meta()`, a `SECURITY DEFINER` function returning, per table,
|
|
33
36
|
whether RLS is enabled and any `COMMENT ON TABLE`/`COMMENT ON COLUMN` text
|
|
34
37
|
you've written. It only ever returns catalog metadata (table/column names,
|
|
35
38
|
flags, comments) — never row data — which is what makes it safe to grant
|
|
@@ -39,8 +42,13 @@ This writes two things and never overwrites an existing function file:
|
|
|
39
42
|
- `supabase/functions/mcp/docs/` — an empty directory for optional bundled
|
|
40
43
|
docs (see "Knowledge base" below).
|
|
41
44
|
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
If you run it somewhere that doesn't look like a Supabase project root (no
|
|
46
|
+
`supabase/config.toml`), it warns but still proceeds — worth double-checking
|
|
47
|
+
you're in the directory you think you are before continuing.
|
|
48
|
+
|
|
49
|
+
`init` is interactive from there (skips straight to the old fixed behavior
|
|
50
|
+
below if stdin isn't a real terminal — CI, a script, piped input). If the
|
|
51
|
+
Supabase CLI is on your `PATH`, it asks:
|
|
44
52
|
|
|
45
53
|
```
|
|
46
54
|
Apply the migration and deploy the function to whatever Supabase project is
|
|
@@ -49,8 +57,8 @@ currently linked, now? [y/N]
|
|
|
49
57
|
|
|
50
58
|
Answering **y** runs `supabase db push` then `supabase functions deploy mcp`
|
|
51
59
|
for you, streaming their output. The default is **no** — pressing Enter, the
|
|
52
|
-
Supabase CLI not being installed, or running `init` non-interactively
|
|
53
|
-
|
|
60
|
+
Supabase CLI not being installed, or running `init` non-interactively all
|
|
61
|
+
skip straight to printing the commands instead:
|
|
54
62
|
|
|
55
63
|
```
|
|
56
64
|
Run these to finish setup:
|
|
@@ -61,14 +69,51 @@ Run these to finish setup:
|
|
|
61
69
|
|
|
62
70
|
That's deliberate: `db push`/`functions deploy` act on whatever project is
|
|
63
71
|
*currently linked*, which may not be a throwaway one, so nothing gets pushed
|
|
64
|
-
without an explicit yes.
|
|
65
|
-
|
|
66
|
-
|
|
72
|
+
without an explicit yes.
|
|
73
|
+
|
|
74
|
+
### The setup wizard
|
|
75
|
+
|
|
76
|
+
If `init` can reach the project you're setting up — either because it was
|
|
77
|
+
already deployed (a re-run), or because you say yes above and the deploy
|
|
78
|
+
succeeds — it walks through a short interactive wizard instead of leaving
|
|
79
|
+
you to hand-edit `writes`/`rpc`/comments yourself:
|
|
80
|
+
|
|
81
|
+
1. **Anonymous sign-in.** `init` tries to auto-detect this via the project's
|
|
82
|
+
public `/auth/v1/settings` endpoint and asks you to confirm; if it can't
|
|
83
|
+
detect it, it just asks. If enabled, it adds a warning comment to the
|
|
84
|
+
generated `index.ts`: an anonymous Supabase Auth session gets
|
|
85
|
+
`role: authenticated` (not `role: anon`), so it passes any RLS policy
|
|
86
|
+
scoped to `authenticated` unless that policy explicitly excludes
|
|
87
|
+
`auth.jwt()->>'is_anonymous' = 'true'`. Worth knowing before granting
|
|
88
|
+
`writes` on anything an anonymous session could reach.
|
|
89
|
+
2. **Per-table writes.** For every RLS-enabled table, asks which of
|
|
90
|
+
insert/update/delete to grant (`i`, `u`, `d`, comma-separated, or Enter
|
|
91
|
+
for none) — the resulting `writes` array is written for you instead of
|
|
92
|
+
left as a commented-out example.
|
|
93
|
+
3. **RPC functions.** Lists Postgres functions PostgREST exposes and lets
|
|
94
|
+
you pick which to expose as tools.
|
|
95
|
+
4. **Documentation.** For every table or column `exposify_meta()` reports
|
|
96
|
+
has no comment, offers to write one right there — the answers become a
|
|
97
|
+
`COMMENT ON TABLE`/`COMMENT ON COLUMN` migration
|
|
98
|
+
(`<timestamp>_exposify_comments.sql`) that gets applied alongside the
|
|
99
|
+
rest. This is what gives `query_<table>`/`insert_<table>`/etc. their tool
|
|
100
|
+
and parameter descriptions (see "Column/table descriptions" below) —
|
|
101
|
+
worth doing even for tables you're not granting writes on, since it's
|
|
102
|
+
the only thing that makes read tools self-explanatory to an agent.
|
|
103
|
+
|
|
104
|
+
If `supabase/functions/mcp/index.ts` already exists, none of this touches
|
|
105
|
+
it — your selections are printed as a config snippet to merge in by hand
|
|
106
|
+
instead, and the documentation migration still gets written either way.
|
|
107
|
+
Connects using `SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` (env vars or
|
|
108
|
+
`--url`/`--key` flags — **the service role key, not the anon key**; see the
|
|
109
|
+
note on introspection below) — without those, `init` can't reach anything to
|
|
110
|
+
introspect and falls back to the fixed scaffold-and-ask-to-deploy flow above,
|
|
111
|
+
same as before this wizard existed.
|
|
67
112
|
|
|
68
113
|
## 2. Decide what's exposed
|
|
69
114
|
|
|
70
115
|
**Every RLS-enabled table is automatically readable.** A table only shows up
|
|
71
|
-
at all if `
|
|
116
|
+
at all if `exposify_meta()` reports RLS is turned on for it *and* PostgREST
|
|
72
117
|
actually exposes it (the calling role has grants) — an RLS-less table is
|
|
73
118
|
invisible, full stop, with no override. For each exposed table you get a
|
|
74
119
|
`query_<table>` tool: typed filters (`eq`/`neq`/`gt`/`lt`/`like`), a `select`
|
|
@@ -122,7 +167,34 @@ Other options you can pass to `aiReady()`:
|
|
|
122
167
|
| `docsAuth` | `"public"` | `"required"` hides `search_docs` until the caller sends *any* `Authorization` header (presence-only, not validated) |
|
|
123
168
|
| `maxRows` | `100` | Hard cap applied to every `query_<table>` call |
|
|
124
169
|
| `rateLimit` / `rateWindowMs` | `60` / `60000` | In-memory, per-instance, best-effort rate limit — **not** distributed across edge instances; real limits belong to Supabase's own gateway |
|
|
125
|
-
| `supabaseUrl` / `anonKey` | read from `SUPABASE_URL` / `SUPABASE_ANON_KEY` env vars | Override for local dev/testing; in a deployed function
|
|
170
|
+
| `supabaseUrl` / `anonKey` / `serviceRoleKey` | read from `SUPABASE_URL` / `SUPABASE_ANON_KEY` / `SUPABASE_SERVICE_ROLE_KEY` env vars | Override for local dev/testing; in a deployed function all three env vars are automatic. `serviceRoleKey` is used only for schema introspection — see below. |
|
|
171
|
+
|
|
172
|
+
### Why introspection needs the service role key
|
|
173
|
+
|
|
174
|
+
Every actual data call (`query_<table>`, `insert_<table>`, `rpc_<fn>`, ...)
|
|
175
|
+
uses the caller's own forwarded JWT, or the anon key as its fallback for an
|
|
176
|
+
unauthenticated caller — **that part is unaffected by any of this** and is
|
|
177
|
+
still what makes RLS the real authorization layer.
|
|
178
|
+
|
|
179
|
+
Schema introspection is different: fetching PostgREST's OpenAPI document (the
|
|
180
|
+
one-time-per-instance call that discovers which tables/columns exist) has to
|
|
181
|
+
use `SUPABASE_SERVICE_ROLE_KEY`. Supabase's gateway rejects that specific
|
|
182
|
+
request outright for an anon or publishable-tier key —
|
|
183
|
+
`{"message":"Invalid API key","hint":"Only the service_role API key can be
|
|
184
|
+
used for this endpoint."}` — regardless of RLS policies or table grants, and
|
|
185
|
+
regardless of whether you're using a legacy JWT-format anon key or a newer
|
|
186
|
+
`sb_publishable_...` key. This isn't documented anywhere in Supabase's own
|
|
187
|
+
docs as far as we've found; it was discovered by deploying this SDK against
|
|
188
|
+
a real project. The service role key never leaves the function and is never
|
|
189
|
+
used for anything except this schema-shape discovery — it doesn't touch row
|
|
190
|
+
data, and RLS still fully governs every actual tool call.
|
|
191
|
+
|
|
192
|
+
If you deploy with `supabase functions deploy` this just works — Supabase
|
|
193
|
+
auto-injects `SUPABASE_SERVICE_ROLE_KEY` into every edge function already.
|
|
194
|
+
The only place you'd ever need to supply it yourself is local dev/testing
|
|
195
|
+
(the `serviceRoleKey` option above) or the CLI's `init`/`generate` (see
|
|
196
|
+
their own sections), which run outside the deployed function and need it
|
|
197
|
+
passed explicitly.
|
|
126
198
|
|
|
127
199
|
## 3. Apply the migration and deploy
|
|
128
200
|
|
|
@@ -136,7 +208,7 @@ supabase functions deploy mcp
|
|
|
136
208
|
|
|
137
209
|
Only `functions deploy` is needed for a config-only change to `index.ts`;
|
|
138
210
|
`db push` is only needed again if the migration itself changed (it hasn't,
|
|
139
|
-
unless you're editing `sql/
|
|
211
|
+
unless you're editing `sql/exposify_meta.sql` directly).
|
|
140
212
|
|
|
141
213
|
(Local dev: `supabase start` then `supabase functions serve mcp` instead of
|
|
142
214
|
`deploy`.)
|
|
@@ -169,9 +241,9 @@ them via the `search_docs` tool.
|
|
|
169
241
|
Every request tries, in order:
|
|
170
242
|
|
|
171
243
|
1. **Runtime introspection** — fetch PostgREST's OpenAPI document and call
|
|
172
|
-
`
|
|
244
|
+
`exposify_meta()`. This is the normal path; the result is cached in
|
|
173
245
|
memory for the life of the edge instance, not refetched per call.
|
|
174
|
-
2. **Bundled manifest** — if step 1 fails, and a `
|
|
246
|
+
2. **Bundled manifest** — if step 1 fails, and a `exposify.manifest.json`
|
|
175
247
|
was generated ahead of time (see below), tools are built from that
|
|
176
248
|
instead.
|
|
177
249
|
3. **Functions + knowledge base only** — if neither works, the function
|
|
@@ -188,31 +260,38 @@ project (local `supabase start` or linked):
|
|
|
188
260
|
```bash
|
|
189
261
|
SUPABASE_URL=http://localhost:54321 \
|
|
190
262
|
SUPABASE_SERVICE_ROLE_KEY=<service-role-key> \
|
|
191
|
-
npx
|
|
263
|
+
npx exposify-supabase generate
|
|
192
264
|
```
|
|
193
265
|
|
|
194
|
-
|
|
266
|
+
Uses the **service role key**, deliberately the same key runtime
|
|
267
|
+
introspection itself uses (see "Why introspection needs the service role
|
|
268
|
+
key" above, and "Fallback ladder" below) — the anon key can't fetch
|
|
269
|
+
PostgREST's OpenAPI document at all, so `generate` needs the same
|
|
270
|
+
privileged key runtime introspection does. Nothing from the service role
|
|
271
|
+
key ends up in the generated manifest — it's just table/column metadata.
|
|
272
|
+
|
|
273
|
+
This writes `supabase/functions/mcp/exposify.manifest.json`. Load it in
|
|
195
274
|
`aiReady({ manifest: ... })` — since the function runs on Deno, import it
|
|
196
275
|
with a JSON import:
|
|
197
276
|
|
|
198
277
|
```ts
|
|
199
|
-
import manifest from "./
|
|
278
|
+
import manifest from "./exposify.manifest.json" with { type: "json" };
|
|
200
279
|
|
|
201
280
|
Deno.serve(aiReady({ manifest, /* ...writes/rpc/functions... */ }));
|
|
202
281
|
```
|
|
203
282
|
|
|
204
283
|
## Troubleshooting
|
|
205
284
|
|
|
206
|
-
**"
|
|
285
|
+
**"exposify_meta() is not reachable" / "PGRST202" errors in function logs.**
|
|
207
286
|
The migration hasn't been applied to this database. Run `supabase db push`
|
|
208
|
-
(or apply `sql/
|
|
287
|
+
(or apply `sql/exposify_meta.sql` from this package directly), then
|
|
209
288
|
redeploy the function — the error message names this exact fix.
|
|
210
289
|
|
|
211
290
|
**A table I expected doesn't show up as a tool.** Check, in order: is RLS
|
|
212
291
|
actually enabled on it (`alter table ... enable row level security;`)? Does
|
|
213
292
|
the calling role (`anon`/`authenticated`) have grants PostgREST can see? If
|
|
214
293
|
both are true and it's still missing, confirm the migration is applied —
|
|
215
|
-
`
|
|
294
|
+
`exposify_meta()` is what reports the RLS flag in the first place.
|
|
216
295
|
|
|
217
296
|
**An `rpc_<fn>` tool takes a single opaque `args` object instead of named
|
|
218
297
|
parameters.** PostgREST's OpenAPI document didn't have a recognizable
|
|
@@ -226,3 +305,23 @@ entries are `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` /
|
|
|
226
305
|
`"<table>.*"` — a malformed entry is dropped (with a console warning), not
|
|
227
306
|
an error, so a typo silently disables just that grant rather than crashing
|
|
228
307
|
the function.
|
|
308
|
+
|
|
309
|
+
**`init` just scaffolds and asks to deploy — it never asks about writes/RPCs/
|
|
310
|
+
documentation.** The wizard only runs when `init` can actually reach the
|
|
311
|
+
project (`SUPABASE_URL`/`SUPABASE_SERVICE_ROLE_KEY` env vars, or `--url`/`--key`
|
|
312
|
+
flags — the service role key, not the anon key, for the same reason described
|
|
313
|
+
above — pointing at a project where the migration is already applied). On a
|
|
314
|
+
brand-new project's first `init`, that's not true yet — say yes to the
|
|
315
|
+
deploy prompt (or run `supabase db push` yourself) and the wizard picks up
|
|
316
|
+
automatically once the migration is live, in the same run if you said yes,
|
|
317
|
+
or on your next `exposify-supabase init` otherwise.
|
|
318
|
+
|
|
319
|
+
**`PostgREST OpenAPI fetch failed: HTTP 401`, and the error body (if you
|
|
320
|
+
curl it directly) says `"Only the service_role API key can be used for this
|
|
321
|
+
endpoint"`.** This is expected if `SUPABASE_SERVICE_ROLE_KEY` isn't
|
|
322
|
+
reaching the function — see "Why introspection needs the service role key"
|
|
323
|
+
above. On a deployed function this should be automatic; if you're still
|
|
324
|
+
seeing this after a `supabase functions deploy`, check `supabase secrets
|
|
325
|
+
list` for a stale explicit `SUPABASE_SERVICE_ROLE_KEY` override, and confirm
|
|
326
|
+
you're not accidentally passing an anon/publishable key as `serviceRoleKey`
|
|
327
|
+
in local dev.
|