@exposify/supabase 0.3.8 → 0.3.9
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 +49 -55
- package/dist/cli.js +32 -49
- package/dist/index.cjs +26 -6
- package/dist/index.d.cts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +24 -7
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -159,26 +159,25 @@ re-runs safe:
|
|
|
159
159
|
|
|
160
160
|
### Using the hosted broker
|
|
161
161
|
|
|
162
|
-
If you use the Exposify hosted broker (the dashboard's "Hosted broker" card),
|
|
163
162
|
`init` asks first:
|
|
164
163
|
|
|
165
164
|
```
|
|
166
|
-
|
|
165
|
+
Let agents log in as your users through the Exposify hosted broker (they get a long-lived connection instead of an hourly Supabase JWT)? [y/N]
|
|
167
166
|
```
|
|
168
167
|
|
|
169
|
-
Or skip the question with `--broker-url <url
|
|
170
|
-
|
|
171
|
-
non-interactively. Saying yes does three
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
`authRequired: true` to `aiReady()`.
|
|
176
|
-
`
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
2. **Sets `verify_jwt = false` for `mcp` in `supabase/config.toml`.**
|
|
181
|
-
|
|
168
|
+
Or skip the question with `--broker` (`--broker-url <url>`, or
|
|
169
|
+
`EXPOSIFY_BROKER_URL`, also points it at a different broker, for local
|
|
170
|
+
development), which also works non-interactively. Saying yes does three
|
|
171
|
+
things:
|
|
172
|
+
|
|
173
|
+
1. **Wires the broker into the function.** Adds `auth: hostedBroker()` and
|
|
174
|
+
`authRequired: true` to `aiReady()`. `hostedBroker()` reads the
|
|
175
|
+
`EXPOSIFY_KEY` secret, which is never written into the file. This goes in
|
|
176
|
+
the managed block, so an existing `index.ts` gets it too, unless its block
|
|
177
|
+
was hand-edited. In that case `init` prints the block for you to merge,
|
|
178
|
+
and leaves `verify_jwt` alone until you have.
|
|
179
|
+
2. **Sets `verify_jwt = false` for `mcp` in `supabase/config.toml`.** A
|
|
180
|
+
broker token isn't a Supabase JWT, so with the gateway's check on, every request
|
|
182
181
|
fails with `{"code":"UNAUTHORIZED_ASYMMETRIC_JWT","message":"Invalid JWT"}`
|
|
183
182
|
before the function runs. The setting goes in config.toml and not just
|
|
184
183
|
on the deploy command line, because a later plain
|
|
@@ -187,13 +186,14 @@ non-interactively. Saying yes does three things:
|
|
|
187
186
|
that have no valid broker token. Otherwise those requests would run as
|
|
188
187
|
`anon`.
|
|
189
188
|
3. **Deploys with `--no-verify-jwt`**, and adds
|
|
190
|
-
`supabase secrets set
|
|
191
|
-
`init` never sees the
|
|
189
|
+
`supabase secrets set EXPOSIFY_KEY=...` to the printed steps.
|
|
190
|
+
`init` never sees the key, so you set that secret yourself.
|
|
192
191
|
Until you do, every request gets a 401.
|
|
193
192
|
|
|
194
|
-
MCP clients then
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
MCP clients then click Connect and sign in through the broker (or send a
|
|
194
|
+
pasted `Authorization: Bearer extb_...`). Don't give the client the JWT that
|
|
195
|
+
`POST /resolve` returns. It's the short-lived Supabase token the function
|
|
196
|
+
swaps in on each request, and it stops working when it expires.
|
|
197
197
|
|
|
198
198
|
## 2. Decide what's exposed
|
|
199
199
|
|
|
@@ -254,7 +254,7 @@ Other options you can pass to `aiReady()`:
|
|
|
254
254
|
| Option | Default | What it does |
|
|
255
255
|
|---|---|---|
|
|
256
256
|
| `docs` | `["docs/**/*.md"]` | Glob(s), relative to the function directory, bundled into the knowledge base |
|
|
257
|
-
| `docsAuth` | `"public"` | `"required"` hides `search_docs` until the caller sends *any* `Authorization` header (presence-only, not validated) |
|
|
257
|
+
| `docsAuth` | `"public"` (`"required"` with the hosted broker) | `"required"` hides `search_docs` until the caller sends *any* `Authorization` header (presence-only, not validated) |
|
|
258
258
|
| `maxRows` | `100` | Hard cap applied to every `query_<table>` call |
|
|
259
259
|
| `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 |
|
|
260
260
|
| `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. |
|
|
@@ -306,7 +306,8 @@ unless you're editing `sql/exposify_meta.sql` directly).
|
|
|
306
306
|
## 4. Connect an MCP client
|
|
307
307
|
|
|
308
308
|
Point any MCP client at the function's URL with a real user's JWT as the
|
|
309
|
-
bearer token (or set up OAuth, below, so the client logs
|
|
309
|
+
bearer token (or set up OAuth or the hosted broker, below, so the client logs
|
|
310
|
+
in by itself) — for
|
|
310
311
|
example, with the Claude Code CLI:
|
|
311
312
|
|
|
312
313
|
```bash
|
|
@@ -342,7 +343,7 @@ exits 1 if anything failed, so you can run it in CI.
|
|
|
342
343
|
(`initialize`, then `tools/list`) and reports the tool count. If the
|
|
343
344
|
function registered `exposify://diagnostics`, which it only does when
|
|
344
345
|
degraded, doctor prints that too. With the broker, it also checks that the
|
|
345
|
-
`
|
|
346
|
+
`EXPOSIFY_KEY` secret exists (`supabase secrets list`).
|
|
346
347
|
|
|
347
348
|
**How the function check authenticates:** it uses `--token` if you pass one.
|
|
348
349
|
Otherwise it sends no token. If the gateway rejects that (`verify_jwt` on),
|
|
@@ -453,29 +454,39 @@ will likely set to the proxy's address, so every client could share one
|
|
|
453
454
|
60-requests/minute bucket. Rate-limit at the proxy and raise `rateLimit` on
|
|
454
455
|
the function. This setup is untested against a real deployment.
|
|
455
456
|
|
|
456
|
-
## Long-lived tokens for clients that can't refresh (
|
|
457
|
+
## Long-lived tokens for clients that can't refresh (the hosted broker)
|
|
457
458
|
|
|
458
459
|
Supabase user JWTs expire (default 1h) and their refresh tokens rotate; a plain
|
|
459
|
-
MCP client can hold a bearer token but can't refresh it.
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
MCP client can hold a bearer token but can't refresh it. Exposify's hosted
|
|
461
|
+
broker holds each user's session, refreshes it, and swaps the agent's
|
|
462
|
+
long-lived connection token (`extb_...`) for a live Supabase JWT on every
|
|
463
|
+
call ([`docs/hosted-broker.md`](../../docs/hosted-broker.md)). `init` sets it
|
|
464
|
+
up (see "Using the hosted broker" above); by hand:
|
|
465
|
+
|
|
466
|
+
```sh
|
|
467
|
+
supabase secrets set EXPOSIFY_KEY=exk_... # the project's secret key from the dashboard
|
|
468
|
+
```
|
|
463
469
|
|
|
464
470
|
```ts
|
|
465
471
|
// supabase/functions/mcp/index.ts
|
|
466
|
-
import { aiReady } from "npm:@exposify/supabase";
|
|
467
|
-
import { broker } from "./broker.ts"; // createTokenBroker(...) over a shared store
|
|
472
|
+
import { aiReady, hostedBroker } from "npm:@exposify/supabase";
|
|
468
473
|
|
|
469
|
-
Deno.serve(aiReady({ auth:
|
|
474
|
+
Deno.serve(aiReady({ auth: hostedBroker(), authRequired: true, writes: ["todos.*"] }));
|
|
470
475
|
```
|
|
471
476
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
477
|
+
With the `EXPOSIFY_KEY` secret set, `aiReady()` uses the broker even without
|
|
478
|
+
`auth`, and `authRequired` defaults to on; `hostedBroker()` just says so in
|
|
479
|
+
the code, and never throws (a missing secret refuses every call with one
|
|
480
|
+
warning).
|
|
481
|
+
|
|
482
|
+
- `auth` takes any resolver (`(incoming: Headers) => Headers | Promise<Headers>`).
|
|
483
|
+
It receives the request's original headers (the `extb_...` token) and its
|
|
484
|
+
result **replaces** them for PostgREST and `functions` calls, so RLS applies
|
|
485
|
+
as that user and the connection token is never forwarded. `apikey` and a
|
|
475
486
|
missing `authorization` still fall back to the anon key.
|
|
476
|
-
- `docsAuth: "required"` checks the raw incoming headers, so a
|
|
477
|
-
counts as authenticated.
|
|
478
|
-
-
|
|
487
|
+
- `docsAuth: "required"` checks the raw incoming headers, so a connection
|
|
488
|
+
token counts as authenticated.
|
|
489
|
+
- The resolver returns empty headers for an unknown or revoked token or a
|
|
479
490
|
failed refresh. By default the request then runs as the `anon` role (RLS
|
|
480
491
|
decides). Set `authRequired: true` to answer `401` instead. Recommended.
|
|
481
492
|
- A resolver that throws yields a 503.
|
|
@@ -483,24 +494,7 @@ Deno.serve(aiReady({ auth: broker.resolver, authRequired: true, writes: ["todos.
|
|
|
483
494
|
(`[functions.mcp] verify_jwt = false` in `supabase/config.toml`, or
|
|
484
495
|
`supabase functions deploy mcp --no-verify-jwt`). Otherwise Supabase
|
|
485
496
|
rejects the `extb_...` token as `Invalid JWT` before your code runs. With
|
|
486
|
-
the check off, `authRequired: true` stops being optional.
|
|
487
|
-
- For the platform-hosted broker, use `createHostedAuthResolver` in place of
|
|
488
|
-
`broker.resolver`. `init` can set this up for you; see "Using the hosted
|
|
489
|
-
broker" above.
|
|
490
|
-
|
|
491
|
-
**Deployment constraints** (not verified end to end, see HANDOVER):
|
|
492
|
-
|
|
493
|
-
- The broker's OAuth server (`listenTokenBroker`) is a Node HTTP server and
|
|
494
|
-
can't run in an Edge Function; run it as a separate service.
|
|
495
|
-
- The edge function builds its own `createTokenBroker(...)` over the **same
|
|
496
|
-
persistent `TokenStore`** (Redis/Postgres, see the token-broker README's
|
|
497
|
-
`ioredis` example) and the same `TOKEN_BROKER_ENCRYPTION_KEY`. The in-memory
|
|
498
|
-
store is per-process and won't work.
|
|
499
|
-
- The broker service does proactive refresh; short-lived edge instances rely on
|
|
500
|
-
the resolver's lazy refresh when `expiresAt` has passed.
|
|
501
|
-
- `@exposify/token-broker` imports `node:crypto` (`scryptSync`, AES-256-GCM,
|
|
502
|
-
RS256). Whether that works under Supabase's Deno runtime has **not** been
|
|
503
|
-
tested; try `supabase functions serve` before depending on it.
|
|
497
|
+
the check off, `authRequired: true` stops being optional. `init` does both.
|
|
504
498
|
|
|
505
499
|
## Knowledge base
|
|
506
500
|
|
package/dist/cli.js
CHANGED
|
@@ -400,7 +400,7 @@ async function discoverConnection(deps) {
|
|
|
400
400
|
}
|
|
401
401
|
|
|
402
402
|
// bin/function-file.ts
|
|
403
|
-
var
|
|
403
|
+
var KEY_SECRET = "EXPOSIFY_KEY";
|
|
404
404
|
var EMPTY_CONFIG = { writes: [], rpc: [], anonymousSignIn: false, broker: null, oauth: false };
|
|
405
405
|
function bypassesGatewayJwt(config) {
|
|
406
406
|
return config.broker !== null || config.oauth;
|
|
@@ -426,18 +426,12 @@ function buildManagedBlock(config) {
|
|
|
426
426
|
`// everything down to exposify:end when you re-run it. Add your own code below.`
|
|
427
427
|
];
|
|
428
428
|
if (config.anonymousSignIn) lines.push(...ANONYMOUS_SIGN_IN_WARNING);
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
);
|
|
432
|
-
if (config.broker) lines.push(`import { createHostedAuthResolver } from "npm:@exposify/token-broker";`);
|
|
429
|
+
const imports = ["aiReady", ...config.oauth ? ["supabaseAuth"] : [], ...config.broker ? ["hostedBroker"] : []];
|
|
430
|
+
lines.push(`import { ${imports.join(", ")} } from "npm:@exposify/supabase";`);
|
|
433
431
|
lines.push("", "const generated = {");
|
|
434
432
|
if (config.broker) {
|
|
435
433
|
lines.push(
|
|
436
|
-
|
|
437
|
-
` brokerUrl: ${JSON.stringify(config.broker.brokerUrl)},`,
|
|
438
|
-
` projectId: ${JSON.stringify(config.broker.projectId)},`,
|
|
439
|
-
` licenseKey: Deno.env.get(${JSON.stringify(LICENSE_KEY_SECRET)}) ?? "",`,
|
|
440
|
-
" }),",
|
|
434
|
+
config.broker.brokerUrl ? ` auth: hostedBroker({ brokerUrl: ${JSON.stringify(config.broker.brokerUrl)} }),` : " auth: hostedBroker(),",
|
|
441
435
|
" authRequired: true,"
|
|
442
436
|
);
|
|
443
437
|
}
|
|
@@ -487,13 +481,19 @@ function readStringField(text, key) {
|
|
|
487
481
|
}
|
|
488
482
|
}
|
|
489
483
|
function readConfig(text) {
|
|
490
|
-
const
|
|
491
|
-
|
|
484
|
+
const inline = /\bhostedBroker\(\{\s*brokerUrl:\s*("(?:[^"\\]|\\.)*")\s*\}\)/.exec(text);
|
|
485
|
+
let brokerUrl = null;
|
|
486
|
+
try {
|
|
487
|
+
brokerUrl = inline ? JSON.parse(inline[1]) : readStringField(text, "brokerUrl");
|
|
488
|
+
} catch {
|
|
489
|
+
brokerUrl = null;
|
|
490
|
+
}
|
|
492
491
|
return {
|
|
493
492
|
writes: readStringArray(text, "writes") ?? [],
|
|
494
493
|
rpc: readStringArray(text, "rpc") ?? [],
|
|
495
494
|
anonymousSignIn: text.includes(ANONYMOUS_MARKER),
|
|
496
|
-
|
|
495
|
+
// hostedBroker() today; createHostedAuthResolver(...) in files from before the one-key setup.
|
|
496
|
+
broker: /\b(hostedBroker|createHostedAuthResolver)\s*\(/.test(text) ? brokerUrl ? { brokerUrl } : {} : null,
|
|
497
497
|
oauth: /^\s*oauth:\s*supabaseAuth\(\s*\)\s*,?\s*$/m.test(text)
|
|
498
498
|
};
|
|
499
499
|
}
|
|
@@ -522,7 +522,7 @@ function replaceManagedBlock(text, config) {
|
|
|
522
522
|
}
|
|
523
523
|
function configsEqual(a, b) {
|
|
524
524
|
const sameSet = (x, y) => x.length === y.length && [...x].sort().join("\0") === [...y].sort().join("\0");
|
|
525
|
-
return sameSet(a.writes, b.writes) && sameSet(a.rpc, b.rpc) && a.anonymousSignIn === b.anonymousSignIn && a.broker
|
|
525
|
+
return sameSet(a.writes, b.writes) && sameSet(a.rpc, b.rpc) && a.anonymousSignIn === b.anonymousSignIn && a.broker === null === (b.broker === null) && a.broker?.brokerUrl === b.broker?.brokerUrl && a.oauth === b.oauth;
|
|
526
526
|
}
|
|
527
527
|
|
|
528
528
|
// bin/doctor.ts
|
|
@@ -532,7 +532,7 @@ var MIGRATIONS_DIR = "supabase/migrations";
|
|
|
532
532
|
var CONFIG_TOML = "supabase/config.toml";
|
|
533
533
|
var DIAGNOSTICS_URI = "exposify://diagnostics";
|
|
534
534
|
function detectAuthMode(functionSource) {
|
|
535
|
-
if (/\
|
|
535
|
+
if (/\b(hostedBroker|createHostedAuthResolver)\s*\(/.test(functionSource)) return "broker";
|
|
536
536
|
return /\boauth:\s*supabaseAuth\s*\(/.test(functionSource) ? "oauth" : "jwt";
|
|
537
537
|
}
|
|
538
538
|
function bypassesGatewayJwt2(mode) {
|
|
@@ -824,18 +824,18 @@ function probeChecks(url, outcome, authMode, local) {
|
|
|
824
824
|
}
|
|
825
825
|
function licenseSecretCheck(deps) {
|
|
826
826
|
if (!deps.runSupabase || !deps.projectRef) {
|
|
827
|
-
return { status: "skip", message: `${
|
|
827
|
+
return { status: "skip", message: `${KEY_SECRET} secret not checked (needs the Supabase CLI and a linked project)` };
|
|
828
828
|
}
|
|
829
829
|
let names = null;
|
|
830
830
|
try {
|
|
831
831
|
names = parseSecretNames(deps.runSupabase(["secrets", "list", "--project-ref", deps.projectRef, "-o", "json"]));
|
|
832
832
|
} catch {
|
|
833
833
|
}
|
|
834
|
-
if (names === null) return { status: "skip", message: `${
|
|
835
|
-
return names.includes(
|
|
834
|
+
if (names === null) return { status: "skip", message: `${KEY_SECRET} secret not checked (\`supabase secrets list\` failed)` };
|
|
835
|
+
return names.includes(KEY_SECRET) ? { status: "pass", message: `${KEY_SECRET} secret is set` } : {
|
|
836
836
|
status: "fail",
|
|
837
|
-
message: `${
|
|
838
|
-
fix: `supabase secrets set ${
|
|
837
|
+
message: `${KEY_SECRET} secret is missing; every request will 401`,
|
|
838
|
+
fix: `supabase secrets set ${KEY_SECRET}=<your secret key from the Exposify dashboard (exk_...)>`
|
|
839
839
|
};
|
|
840
840
|
}
|
|
841
841
|
var SYMBOL = { pass: "\u2713", warn: "!", fail: "\u2717", skip: "-" };
|
|
@@ -887,7 +887,7 @@ function describeConfigChange(from, to) {
|
|
|
887
887
|
}
|
|
888
888
|
if (authLabel(from) !== authLabel(to)) {
|
|
889
889
|
parts.push(`auth: ${authLabel(from)} -> ${authLabel(to)}`);
|
|
890
|
-
} else if (from.broker?.brokerUrl !== to.broker?.brokerUrl
|
|
890
|
+
} else if (from.broker?.brokerUrl !== to.broker?.brokerUrl) {
|
|
891
891
|
parts.push("hosted broker settings");
|
|
892
892
|
}
|
|
893
893
|
return parts.join("; ");
|
|
@@ -1121,14 +1121,14 @@ function deployArgs(auth) {
|
|
|
1121
1121
|
}
|
|
1122
1122
|
function clientStep(auth) {
|
|
1123
1123
|
if (auth.oauth) return "Add the function URL to an MCP client; it opens a Supabase Auth login on connect.";
|
|
1124
|
-
const credential = auth.broker ? "a
|
|
1124
|
+
const credential = auth.broker ? "a connection token (extb_..., from the hosted broker)" : "a user JWT";
|
|
1125
1125
|
return `Paste the function URL + ${credential} into an MCP client.`;
|
|
1126
1126
|
}
|
|
1127
1127
|
function printManualCommands(steps, auth) {
|
|
1128
1128
|
console.log("\nRun these to finish setup:");
|
|
1129
1129
|
const lines = [];
|
|
1130
1130
|
if (steps.push) lines.push("supabase db push");
|
|
1131
|
-
if (auth.broker) lines.push(`supabase secrets set ${
|
|
1131
|
+
if (auth.broker) lines.push(`supabase secrets set ${KEY_SECRET}=<your secret key from the Exposify dashboard (exk_...)>`);
|
|
1132
1132
|
if (auth.oauth) lines.push(OAUTH_DASHBOARD_STEP);
|
|
1133
1133
|
if (steps.deploy) lines.push(`supabase ${deployArgs(auth).join(" ")}`);
|
|
1134
1134
|
lines.forEach((cmd, i) => console.log(` ${i + 1}. ${cmd}`));
|
|
@@ -1138,7 +1138,7 @@ function printLicenseKeyReminder(auth) {
|
|
|
1138
1138
|
if (!auth.broker) return;
|
|
1139
1139
|
console.log(
|
|
1140
1140
|
`
|
|
1141
|
-
If you haven't already: supabase secrets set ${
|
|
1141
|
+
If you haven't already: supabase secrets set ${KEY_SECRET}=<your secret key (exk_...)> -- until it's set, every request is rejected with 401.`
|
|
1142
1142
|
);
|
|
1143
1143
|
}
|
|
1144
1144
|
function runSteps(steps, auth) {
|
|
@@ -1335,19 +1335,16 @@ async function warnIfNotSupabaseProject() {
|
|
|
1335
1335
|
}
|
|
1336
1336
|
function brokerFromFlags() {
|
|
1337
1337
|
const flagUrl = argValue("--broker-url") ?? process.env.EXPOSIFY_BROKER_URL;
|
|
1338
|
-
|
|
1339
|
-
if (!flagUrl
|
|
1340
|
-
if (!flagUrl || !flagProject) {
|
|
1341
|
-
throw new Error("the hosted broker needs both --broker-url and --broker-project (or EXPOSIFY_BROKER_URL/EXPOSIFY_PROJECT_ID).");
|
|
1342
|
-
}
|
|
1338
|
+
if (!flagUrl && !process.argv.includes("--broker")) return void 0;
|
|
1339
|
+
if (!flagUrl) return {};
|
|
1343
1340
|
const problem = brokerUrlProblem(flagUrl);
|
|
1344
1341
|
if (problem) throw new Error(problem);
|
|
1345
|
-
return { brokerUrl: normalizeBrokerUrl(flagUrl)
|
|
1342
|
+
return { brokerUrl: normalizeBrokerUrl(flagUrl) };
|
|
1346
1343
|
}
|
|
1347
1344
|
function authFromFlags() {
|
|
1348
1345
|
const broker = brokerFromFlags();
|
|
1349
1346
|
const oauth = process.argv.includes("--oauth") || process.env.EXPOSIFY_OAUTH === "1";
|
|
1350
|
-
if (broker && oauth) throw new Error("--oauth can't be combined with
|
|
1347
|
+
if (broker && oauth) throw new Error("--oauth can't be combined with --broker; pick one.");
|
|
1351
1348
|
if (broker) return { broker, oauth: false };
|
|
1352
1349
|
if (oauth) return { broker: null, oauth: true };
|
|
1353
1350
|
return void 0;
|
|
@@ -1365,25 +1362,11 @@ async function askAuth(rl, current) {
|
|
|
1365
1362
|
async function askBroker(rl, current) {
|
|
1366
1363
|
const use = await promptYesNo(
|
|
1367
1364
|
rl,
|
|
1368
|
-
"\
|
|
1365
|
+
"\nLet agents log in as your users through the Exposify hosted broker (they get a long-lived connection instead of an hourly Supabase JWT)?",
|
|
1369
1366
|
current !== null
|
|
1370
1367
|
);
|
|
1371
1368
|
if (!use) return null;
|
|
1372
|
-
|
|
1373
|
-
for (; ; ) {
|
|
1374
|
-
const hint = current ? ` [${current.brokerUrl}]` : "";
|
|
1375
|
-
const answer = await promptText(rl, ` Broker URL (from the dashboard's Hosted broker card)${hint}: `);
|
|
1376
|
-
brokerUrl = normalizeBrokerUrl(answer || current?.brokerUrl || "");
|
|
1377
|
-
const problem = brokerUrlProblem(brokerUrl);
|
|
1378
|
-
if (!problem) break;
|
|
1379
|
-
console.log(` ${problem}`);
|
|
1380
|
-
}
|
|
1381
|
-
let projectId = "";
|
|
1382
|
-
while (!projectId) {
|
|
1383
|
-
const hint = current ? ` [${current.projectId}]` : "";
|
|
1384
|
-
projectId = await promptText(rl, ` Project ID${hint}: `) || current?.projectId || "";
|
|
1385
|
-
}
|
|
1386
|
-
return { brokerUrl, projectId };
|
|
1369
|
+
return current?.brokerUrl ? { brokerUrl: current.brokerUrl } : {};
|
|
1387
1370
|
}
|
|
1388
1371
|
async function readLocalState() {
|
|
1389
1372
|
const migrations = await listFilesOrEmpty(MIGRATIONS_DIR2);
|
|
@@ -1604,13 +1587,13 @@ async function main() {
|
|
|
1604
1587
|
if (cmd === "generate") return cmdGenerate();
|
|
1605
1588
|
if (cmd === "doctor") return cmdDoctor();
|
|
1606
1589
|
console.log(
|
|
1607
|
-
"Usage: exposify-supabase <init|generate|doctor> [--url <url>] [--key <service-role-key>] [--local|--linked] [--broker
|
|
1590
|
+
"Usage: exposify-supabase <init|generate|doctor> [--url <url>] [--key <service-role-key>] [--local|--linked] [--broker [--broker-url <url>] | --oauth] [--token <token>] [--check]"
|
|
1608
1591
|
);
|
|
1609
1592
|
console.log(" init Scaffold the exposify_meta() migration + supabase/functions/mcp/index.ts, then (when the");
|
|
1610
1593
|
console.log(" project is reachable) walk through anonymous sign-in, per-table writes, RPC exposure, and");
|
|
1611
1594
|
console.log(" documentation. Safe to re-run: answers default to what's already configured, only the");
|
|
1612
1595
|
console.log(" exposify:begin/end block of index.ts is ever rewritten, and a run that changes nothing");
|
|
1613
|
-
console.log(" says so and stops. Offers the hosted broker (or --broker
|
|
1596
|
+
console.log(" says so and stops. Offers the hosted broker (or --broker; it reads the EXPOSIFY_KEY secret), or");
|
|
1614
1597
|
console.log(" Supabase Auth sign-in (--oauth): adds oauth: supabaseAuth(), so MCP clients open a login");
|
|
1615
1598
|
console.log(" on connect. Either way it sets verify_jwt = false for mcp and deploys --no-verify-jwt.");
|
|
1616
1599
|
console.log(" --check: change nothing, exit 1 if a run would change a file.");
|
package/dist/index.cjs
CHANGED
|
@@ -25,8 +25,11 @@ __export(index_exports, {
|
|
|
25
25
|
aiReady: () => aiReady,
|
|
26
26
|
buildManifest: () => buildManifest,
|
|
27
27
|
columnsFromOpenApi: () => columnsFromOpenApi,
|
|
28
|
+
completeMcpLogin: () => import_core6.completeMcpLogin,
|
|
29
|
+
createHostedAuthResolver: () => import_core6.createHostedAuthResolver,
|
|
28
30
|
fetchExposifyMeta: () => fetchExposifyMeta,
|
|
29
31
|
fetchOpenApi: () => fetchOpenApi,
|
|
32
|
+
hostedBroker: () => import_core6.hostedBroker,
|
|
30
33
|
introspectTables: () => introspectTables,
|
|
31
34
|
loadManifest: () => loadManifest,
|
|
32
35
|
rpcArgSchema: () => rpcArgSchema,
|
|
@@ -108,7 +111,7 @@ function warnOnce(message) {
|
|
|
108
111
|
} catch {
|
|
109
112
|
}
|
|
110
113
|
}
|
|
111
|
-
function resolveSupabaseConfig(options = {}) {
|
|
114
|
+
function resolveSupabaseConfig(options = {}, hostedBroker2 = false) {
|
|
112
115
|
if (options.oauth) {
|
|
113
116
|
if (options.docsAuth === "public") {
|
|
114
117
|
throw new Error('docsAuth: "public" cannot be combined with oauth (the whole MCP endpoint requires a token)');
|
|
@@ -122,7 +125,7 @@ function resolveSupabaseConfig(options = {}) {
|
|
|
122
125
|
rpc: options.rpc ?? [],
|
|
123
126
|
functions: options.functions ?? {},
|
|
124
127
|
docs: options.docs ?? ["docs/**/*.md"],
|
|
125
|
-
docsAuth: options.docsAuth ?? (options.oauth ? "required" : "public"),
|
|
128
|
+
docsAuth: options.docsAuth ?? (options.oauth || hostedBroker2 ? "required" : "public"),
|
|
126
129
|
maxRows: options.maxRows ?? 100,
|
|
127
130
|
rateLimit: options.rateLimit,
|
|
128
131
|
rateWindowMs: options.rateWindowMs
|
|
@@ -735,11 +738,13 @@ function logColdStart(opts) {
|
|
|
735
738
|
}
|
|
736
739
|
|
|
737
740
|
// src/index.ts
|
|
741
|
+
var import_core6 = require("@exposify/core");
|
|
738
742
|
function denoEnv() {
|
|
739
743
|
return globalThis.Deno?.env;
|
|
740
744
|
}
|
|
741
745
|
function aiReady(options = {}) {
|
|
742
|
-
const
|
|
746
|
+
const brokerOn = (0, import_core5.isHostedResolver)(options.auth) || !options.auth && !options.oauth && Boolean((0, import_core5.readEnv)("EXPOSIFY_KEY"));
|
|
747
|
+
const cfg = resolveSupabaseConfig(options, brokerOn);
|
|
743
748
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
744
749
|
const env = denoEnv();
|
|
745
750
|
const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
|
|
@@ -749,7 +754,7 @@ function aiReady(options = {}) {
|
|
|
749
754
|
const logReady = (0, import_core5.startupLogEnabled)(options.startupLog);
|
|
750
755
|
const functionName = functionNameFromMainModule();
|
|
751
756
|
const endpoint = gate?.metadata.resource ?? (supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : "this function");
|
|
752
|
-
const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
|
|
757
|
+
const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : brokerOn ? "hosted broker (EXPOSIFY_KEY)" : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
|
|
753
758
|
if (!supabaseUrl) {
|
|
754
759
|
console.error(
|
|
755
760
|
(0, import_core5.formatFailed)(LOG_PREFIX, "SUPABASE_URL is not set", "deployed functions get it automatically; for local dev pass { supabaseUrl }")
|
|
@@ -851,9 +856,21 @@ function aiReady(options = {}) {
|
|
|
851
856
|
return result.ok ? core(req) : result.response;
|
|
852
857
|
};
|
|
853
858
|
}
|
|
854
|
-
const
|
|
859
|
+
const envAuth = options.auth || options.oauth ? void 0 : (0, import_core5.hostedResolverFromEnv)();
|
|
860
|
+
const auth = options.auth ?? envAuth;
|
|
861
|
+
const authRequired = options.authRequired ?? (envAuth !== void 0 || (0, import_core5.isHostedResolver)(auth));
|
|
855
862
|
if (!auth) return core;
|
|
863
|
+
const brokerGate = authRequired ? (0, import_core5.createBrokerGate)(auth) : null;
|
|
864
|
+
const functionUrl = supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : null;
|
|
856
865
|
return async (req) => {
|
|
866
|
+
if (brokerGate) {
|
|
867
|
+
const own = (0, import_core5.publicRequestUrl)(req.url, req.headers);
|
|
868
|
+
const isPrm = own.endsWith(PRM_SUFFIX);
|
|
869
|
+
const resource = brokerGate.resourceFor(functionUrl ?? (isPrm ? own.slice(0, -PRM_SUFFIX.length) : own));
|
|
870
|
+
if (isPrm) return brokerGate.metadataResponse(req.method, resource);
|
|
871
|
+
const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
|
|
872
|
+
if (!result.ok) return result.response;
|
|
873
|
+
}
|
|
857
874
|
let resolved;
|
|
858
875
|
try {
|
|
859
876
|
resolved = await auth(req.headers, {});
|
|
@@ -861,7 +878,7 @@ function aiReady(options = {}) {
|
|
|
861
878
|
console.error("exposify-supabase: auth resolver failed", err);
|
|
862
879
|
return Response.json({ error: "internal MCP server error" }, { status: 503 });
|
|
863
880
|
}
|
|
864
|
-
if (
|
|
881
|
+
if (authRequired && !resolved.get("authorization")) {
|
|
865
882
|
return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
866
883
|
}
|
|
867
884
|
resolvedAuth.set(req, resolved);
|
|
@@ -875,8 +892,11 @@ function aiReady(options = {}) {
|
|
|
875
892
|
aiReady,
|
|
876
893
|
buildManifest,
|
|
877
894
|
columnsFromOpenApi,
|
|
895
|
+
completeMcpLogin,
|
|
896
|
+
createHostedAuthResolver,
|
|
878
897
|
fetchExposifyMeta,
|
|
879
898
|
fetchOpenApi,
|
|
899
|
+
hostedBroker,
|
|
880
900
|
introspectTables,
|
|
881
901
|
loadManifest,
|
|
882
902
|
rpcArgSchema,
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OAuthOptions, AuthResolver, WebMcpHandler } from '@exposify/core';
|
|
2
|
+
export { HostedAuthResolverConfig, completeMcpLogin, createHostedAuthResolver, hostedBroker } from '@exposify/core';
|
|
2
3
|
|
|
3
4
|
type ColumnType = "string" | "number" | "integer" | "boolean" | "object" | "array";
|
|
4
5
|
interface ColumnSchema {
|
|
@@ -109,8 +110,8 @@ interface AgentifySupabaseOptions {
|
|
|
109
110
|
serviceRoleKey?: string;
|
|
110
111
|
/**
|
|
111
112
|
* Resolves the caller's incoming headers into the headers used for
|
|
112
|
-
* PostgREST and edge-function calls, e.g. `
|
|
113
|
-
*
|
|
113
|
+
* PostgREST and edge-function calls, e.g. `createHostedAuthResolver` (Exposify's
|
|
114
|
+
* hosted broker) to swap a long-lived `extb_...` token for a live
|
|
114
115
|
* Supabase JWT. Only `authorization`/`apikey` from the result are used; a
|
|
115
116
|
* missing `authorization` falls back to the anon key (unless
|
|
116
117
|
* `authRequired`). Absent: the caller's own `authorization`/`apikey` are
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { OAuthOptions, AuthResolver, WebMcpHandler } from '@exposify/core';
|
|
2
|
+
export { HostedAuthResolverConfig, completeMcpLogin, createHostedAuthResolver, hostedBroker } from '@exposify/core';
|
|
2
3
|
|
|
3
4
|
type ColumnType = "string" | "number" | "integer" | "boolean" | "object" | "array";
|
|
4
5
|
interface ColumnSchema {
|
|
@@ -109,8 +110,8 @@ interface AgentifySupabaseOptions {
|
|
|
109
110
|
serviceRoleKey?: string;
|
|
110
111
|
/**
|
|
111
112
|
* Resolves the caller's incoming headers into the headers used for
|
|
112
|
-
* PostgREST and edge-function calls, e.g. `
|
|
113
|
-
*
|
|
113
|
+
* PostgREST and edge-function calls, e.g. `createHostedAuthResolver` (Exposify's
|
|
114
|
+
* hosted broker) to swap a long-lived `extb_...` token for a live
|
|
114
115
|
* Supabase JWT. Only `authorization`/`apikey` from the result are used; a
|
|
115
116
|
* missing `authorization` falls back to the anon key (unless
|
|
116
117
|
* `authRequired`). Absent: the caller's own `authorization`/`apikey` are
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createWebMcpHandler, formatFailed as formatFailed2, formatWarning as formatWarning2, startupLogEnabled } from "@exposify/core";
|
|
2
|
+
import { createBrokerGate, createWebMcpHandler, formatFailed as formatFailed2, publicRequestUrl, formatWarning as formatWarning2, hostedResolverFromEnv, isHostedResolver, readEnv, startupLogEnabled } from "@exposify/core";
|
|
3
3
|
|
|
4
4
|
// src/kb.ts
|
|
5
5
|
import { KnowledgeBase } from "@exposify/core";
|
|
@@ -74,7 +74,7 @@ function warnOnce(message) {
|
|
|
74
74
|
} catch {
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
-
function resolveSupabaseConfig(options = {}) {
|
|
77
|
+
function resolveSupabaseConfig(options = {}, hostedBroker2 = false) {
|
|
78
78
|
if (options.oauth) {
|
|
79
79
|
if (options.docsAuth === "public") {
|
|
80
80
|
throw new Error('docsAuth: "public" cannot be combined with oauth (the whole MCP endpoint requires a token)');
|
|
@@ -88,7 +88,7 @@ function resolveSupabaseConfig(options = {}) {
|
|
|
88
88
|
rpc: options.rpc ?? [],
|
|
89
89
|
functions: options.functions ?? {},
|
|
90
90
|
docs: options.docs ?? ["docs/**/*.md"],
|
|
91
|
-
docsAuth: options.docsAuth ?? (options.oauth ? "required" : "public"),
|
|
91
|
+
docsAuth: options.docsAuth ?? (options.oauth || hostedBroker2 ? "required" : "public"),
|
|
92
92
|
maxRows: options.maxRows ?? 100,
|
|
93
93
|
rateLimit: options.rateLimit,
|
|
94
94
|
rateWindowMs: options.rateWindowMs
|
|
@@ -704,11 +704,13 @@ function logColdStart(opts) {
|
|
|
704
704
|
}
|
|
705
705
|
|
|
706
706
|
// src/index.ts
|
|
707
|
+
import { completeMcpLogin, createHostedAuthResolver, hostedBroker } from "@exposify/core";
|
|
707
708
|
function denoEnv() {
|
|
708
709
|
return globalThis.Deno?.env;
|
|
709
710
|
}
|
|
710
711
|
function aiReady(options = {}) {
|
|
711
|
-
const
|
|
712
|
+
const brokerOn = isHostedResolver(options.auth) || !options.auth && !options.oauth && Boolean(readEnv("EXPOSIFY_KEY"));
|
|
713
|
+
const cfg = resolveSupabaseConfig(options, brokerOn);
|
|
712
714
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
713
715
|
const env = denoEnv();
|
|
714
716
|
const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
|
|
@@ -718,7 +720,7 @@ function aiReady(options = {}) {
|
|
|
718
720
|
const logReady = startupLogEnabled(options.startupLog);
|
|
719
721
|
const functionName = functionNameFromMainModule();
|
|
720
722
|
const endpoint = gate?.metadata.resource ?? (supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : "this function");
|
|
721
|
-
const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
|
|
723
|
+
const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : brokerOn ? "hosted broker (EXPOSIFY_KEY)" : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
|
|
722
724
|
if (!supabaseUrl) {
|
|
723
725
|
console.error(
|
|
724
726
|
formatFailed2(LOG_PREFIX, "SUPABASE_URL is not set", "deployed functions get it automatically; for local dev pass { supabaseUrl }")
|
|
@@ -820,9 +822,21 @@ function aiReady(options = {}) {
|
|
|
820
822
|
return result.ok ? core(req) : result.response;
|
|
821
823
|
};
|
|
822
824
|
}
|
|
823
|
-
const
|
|
825
|
+
const envAuth = options.auth || options.oauth ? void 0 : hostedResolverFromEnv();
|
|
826
|
+
const auth = options.auth ?? envAuth;
|
|
827
|
+
const authRequired = options.authRequired ?? (envAuth !== void 0 || isHostedResolver(auth));
|
|
824
828
|
if (!auth) return core;
|
|
829
|
+
const brokerGate = authRequired ? createBrokerGate(auth) : null;
|
|
830
|
+
const functionUrl = supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : null;
|
|
825
831
|
return async (req) => {
|
|
832
|
+
if (brokerGate) {
|
|
833
|
+
const own = publicRequestUrl(req.url, req.headers);
|
|
834
|
+
const isPrm = own.endsWith(PRM_SUFFIX);
|
|
835
|
+
const resource = brokerGate.resourceFor(functionUrl ?? (isPrm ? own.slice(0, -PRM_SUFFIX.length) : own));
|
|
836
|
+
if (isPrm) return brokerGate.metadataResponse(req.method, resource);
|
|
837
|
+
const result = await brokerGate.check(req.headers, resource, `${resource}${PRM_SUFFIX}`);
|
|
838
|
+
if (!result.ok) return result.response;
|
|
839
|
+
}
|
|
826
840
|
let resolved;
|
|
827
841
|
try {
|
|
828
842
|
resolved = await auth(req.headers, {});
|
|
@@ -830,7 +844,7 @@ function aiReady(options = {}) {
|
|
|
830
844
|
console.error("exposify-supabase: auth resolver failed", err);
|
|
831
845
|
return Response.json({ error: "internal MCP server error" }, { status: 503 });
|
|
832
846
|
}
|
|
833
|
-
if (
|
|
847
|
+
if (authRequired && !resolved.get("authorization")) {
|
|
834
848
|
return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
835
849
|
}
|
|
836
850
|
resolvedAuth.set(req, resolved);
|
|
@@ -843,8 +857,11 @@ export {
|
|
|
843
857
|
aiReady,
|
|
844
858
|
buildManifest,
|
|
845
859
|
columnsFromOpenApi,
|
|
860
|
+
completeMcpLogin,
|
|
861
|
+
createHostedAuthResolver,
|
|
846
862
|
fetchExposifyMeta,
|
|
847
863
|
fetchOpenApi,
|
|
864
|
+
hostedBroker,
|
|
848
865
|
introspectTables,
|
|
849
866
|
loadManifest,
|
|
850
867
|
rpcArgSchema,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/supabase",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"prepack": "yarn build"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@exposify/core": "0.3.
|
|
30
|
+
"@exposify/core": "0.3.13",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
32
32
|
"zod": "^3.24.0"
|
|
33
33
|
},
|