@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 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
- Use the Exposify hosted broker for auth (MCP clients get a long-lived extb_... token instead of a Supabase JWT)? [y/N]
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> --broker-project <id>`
170
- (or `EXPOSIFY_BROKER_URL`/`EXPOSIFY_PROJECT_ID`), which also works
171
- non-interactively. Saying yes does three things:
172
-
173
- 1. **Wires the resolver into the function.** Adds
174
- `auth: createHostedAuthResolver({ brokerUrl, projectId, licenseKey })` and
175
- `authRequired: true` to `aiReady()`. The license key is read from the
176
- `EXPOSIFY_LICENSE_KEY` secret and is never written into the file. This
177
- goes in the managed block, so an existing `index.ts` gets it too, unless
178
- its block was hand-edited. In that case `init` prints the block for you to
179
- merge, and leaves `verify_jwt` alone until you have.
180
- 2. **Sets `verify_jwt = false` for `mcp` in `supabase/config.toml`.** An
181
- `extb_...` token isn't a JWT, so with the gateway's check on, every request
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 EXPOSIFY_LICENSE_KEY=...` to the printed steps.
191
- `init` never sees the license key, so you set that secret yourself.
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 send `Authorization: Bearer extb_...`. Don't give the client
195
- the JWT that `POST /resolve` returns. It's the short-lived Supabase token the
196
- function swaps in on each request, and it stops working when it expires.
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 in by itself) — for
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
- `EXPOSIFY_LICENSE_KEY` secret exists (`supabase secrets list`).
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 (token broker)
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. Pass an `auth`
460
- resolver (any `(incoming: Headers) => Headers | Promise<Headers>`, such as
461
- `broker.resolver` from [`@exposify/token-broker`](../token-broker/README.md))
462
- and the client sends a long-lived broker token instead:
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: broker.resolver, authRequired: true, writes: ["todos.*"] }));
474
+ Deno.serve(aiReady({ auth: hostedBroker(), authRequired: true, writes: ["todos.*"] }));
470
475
  ```
471
476
 
472
- - The resolver receives the request's original headers (the `extb_...` token)
473
- and its result **replaces** them for PostgREST and `functions` calls, so RLS
474
- applies as that user and the broker token is never forwarded. `apikey` and a
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 broker token
477
- counts as authenticated.
478
- - A broker resolver returns empty headers for an unknown/revoked token or a
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 LICENSE_KEY_SECRET = "EXPOSIFY_LICENSE_KEY";
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
- lines.push(
430
- config.oauth ? `import { aiReady, supabaseAuth } from "npm:@exposify/supabase";` : `import { aiReady } from "npm:@exposify/supabase";`
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
- " auth: createHostedAuthResolver({",
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 brokerUrl = readStringField(text, "brokerUrl");
491
- const projectId = readStringField(text, "projectId");
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
- broker: /\bcreateHostedAuthResolver\s*\(/.test(text) && brokerUrl && projectId ? { brokerUrl, projectId } : null,
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?.brokerUrl === b.broker?.brokerUrl && a.broker?.projectId === b.broker?.projectId && a.oauth === b.oauth;
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 (/\bcreateHostedAuthResolver\s*\(/.test(functionSource)) return "broker";
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: `${LICENSE_KEY_SECRET} secret not checked (needs the Supabase CLI and a linked project)` };
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: `${LICENSE_KEY_SECRET} secret not checked (\`supabase secrets list\` failed)` };
835
- return names.includes(LICENSE_KEY_SECRET) ? { status: "pass", message: `${LICENSE_KEY_SECRET} secret is set` } : {
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: `${LICENSE_KEY_SECRET} secret is missing; every request will 401`,
838
- fix: `supabase secrets set ${LICENSE_KEY_SECRET}=<your license key (expfy_...)>`
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 || from.broker?.projectId !== to.broker?.projectId) {
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 broker token (extb_..., minted by the hosted broker)" : "a user JWT";
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 ${LICENSE_KEY_SECRET}=<your license key (expfy_...)>`);
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 ${LICENSE_KEY_SECRET}=<your license key (expfy_...)> -- until it's set, every request is rejected with 401.`
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
- const flagProject = argValue("--broker-project") ?? process.env.EXPOSIFY_PROJECT_ID;
1339
- if (!flagUrl && !flagProject) return void 0;
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), projectId: flagProject.trim() };
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 the hosted broker flags; pick one.");
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
- "\nUse the Exposify hosted broker for auth (MCP clients get a long-lived extb_... token instead of a Supabase JWT)?",
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
- let brokerUrl = "";
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-url <url> --broker-project <id> | --oauth] [--token <token>] [--check]"
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-url/--broker-project), or");
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 cfg = resolveSupabaseConfig(options);
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 auth = options.auth;
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 (options.authRequired && !resolved.get("authorization")) {
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. `broker.resolver` from
113
- * `@exposify/token-broker` to swap a long-lived `extb_...` token for a live
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. `broker.resolver` from
113
- * `@exposify/token-broker` to swap a long-lived `extb_...` token for a live
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 cfg = resolveSupabaseConfig(options);
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 auth = options.auth;
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 (options.authRequired && !resolved.get("authorization")) {
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.8",
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.12",
30
+ "@exposify/core": "0.3.13",
31
31
  "@modelcontextprotocol/sdk": "^1.12.0",
32
32
  "zod": "^3.24.0"
33
33
  },