@exposify/supabase 0.3.7 → 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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // src/index.ts
2
- import { createWebMcpHandler } 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,13 +74,21 @@ function warnOnce(message) {
74
74
  } catch {
75
75
  }
76
76
  }
77
- function resolveSupabaseConfig(options = {}) {
77
+ function resolveSupabaseConfig(options = {}, hostedBroker2 = false) {
78
+ if (options.oauth) {
79
+ if (options.docsAuth === "public") {
80
+ throw new Error('docsAuth: "public" cannot be combined with oauth (the whole MCP endpoint requires a token)');
81
+ }
82
+ if (options.auth || options.authRequired) {
83
+ throw new Error("oauth cannot be combined with auth/authRequired (oauth already requires a valid token on every request)");
84
+ }
85
+ }
78
86
  return {
79
87
  writes: parseWrites(options.writes),
80
88
  rpc: options.rpc ?? [],
81
89
  functions: options.functions ?? {},
82
90
  docs: options.docs ?? ["docs/**/*.md"],
83
- docsAuth: options.docsAuth ?? "public",
91
+ docsAuth: options.docsAuth ?? (options.oauth || hostedBroker2 ? "required" : "public"),
84
92
  maxRows: options.maxRows ?? 100,
85
93
  rateLimit: options.rateLimit,
86
94
  rateWindowMs: options.rateWindowMs
@@ -605,24 +613,141 @@ async function toResult(run) {
605
613
  }
606
614
  }
607
615
 
616
+ // src/oauth.ts
617
+ import { createOAuthGate, resolveOAuthOptions } from "@exposify/core";
618
+ var PRM_SUFFIX = "/.well-known/oauth-protected-resource";
619
+ function supabaseAuth(overrides = {}) {
620
+ return (supabaseUrl) => {
621
+ const base = supabaseUrl.replace(/\/+$/, "");
622
+ return {
623
+ issuer: `${base}/auth/v1`,
624
+ jwksUri: `${base}/auth/v1/.well-known/jwks.json`,
625
+ audience: "authenticated",
626
+ ...overrides
627
+ };
628
+ };
629
+ }
630
+ function functionNameFromMainModule() {
631
+ const mainModule = globalThis.Deno?.mainModule;
632
+ if (!mainModule) return void 0;
633
+ try {
634
+ const parts = new URL(mainModule).pathname.split("/").filter(Boolean);
635
+ return parts.length >= 2 ? parts[parts.length - 2] : void 0;
636
+ } catch {
637
+ return void 0;
638
+ }
639
+ }
640
+ function createSupabaseOAuthGate(oauth, supabaseUrl, fetchImpl) {
641
+ if (typeof oauth === "function" && !supabaseUrl) {
642
+ throw new Error("oauth: SUPABASE_URL is not set. In a deployed function this is automatic; for local dev pass { supabaseUrl }.");
643
+ }
644
+ const opts = typeof oauth === "function" ? oauth(supabaseUrl) : oauth;
645
+ let resource = opts.resource;
646
+ if (!resource) {
647
+ const name = functionNameFromMainModule();
648
+ if (!supabaseUrl || !name) {
649
+ throw new Error("oauth.resource could not be derived (needs SUPABASE_URL and Deno.mainModule); pass oauth.resource explicitly.");
650
+ }
651
+ resource = `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${name}`;
652
+ }
653
+ const { metadataUrl, ...oauthOptions } = opts;
654
+ const cfg = resolveOAuthOptions({ ...oauthOptions, resource });
655
+ if (metadataUrl !== void 0) {
656
+ try {
657
+ new URL(metadataUrl);
658
+ } catch {
659
+ throw new Error(`oauth.metadataUrl must be an absolute URL, got ${JSON.stringify(metadataUrl)}`);
660
+ }
661
+ }
662
+ return createOAuthGate(cfg, { metadataUrl: metadataUrl ?? `${cfg.resource}${PRM_SUFFIX}`, fetchImpl });
663
+ }
664
+
665
+ // src/startup.ts
666
+ import { formatFailed, formatReady, formatWarning } from "@exposify/core";
667
+ var LOG_PREFIX = "[exposify-supabase]";
668
+ var INTROSPECTION_FIX = "check that SUPABASE_SERVICE_ROLE_KEY is available to the function and the exposify_meta migration is applied (`supabase db push`); the reason is logged above. To survive this, bundle a manifest with `exposify-supabase generate`";
669
+ function supabaseToolsPhrase(ladder, cfg, hasDocs) {
670
+ const query = ladder.tables.length;
671
+ let write = 0;
672
+ for (const table of ladder.tables) write += cfg.writes.get(table.name)?.size ?? 0;
673
+ const rpc = cfg.rpc.length;
674
+ const fn = Object.keys(cfg.functions).length;
675
+ const parts = [`${query} query`];
676
+ if (write) parts.push(`${write} write`);
677
+ if (rpc) parts.push(`${rpc} rpc`);
678
+ if (fn) parts.push(`${fn} function${fn === 1 ? "" : "s"}`);
679
+ if (hasDocs) parts.push("search_docs");
680
+ const total = query + write + rpc + fn + (hasDocs ? 1 : 0);
681
+ return `${total} tool${total === 1 ? "" : "s"} (${parts.join(", ")})`;
682
+ }
683
+ function logColdStart(opts) {
684
+ const { ladder, cfg, docs, endpoint, auth, logReady } = opts;
685
+ if (ladder.mode === "functions-only") {
686
+ console.error(
687
+ formatFailed(
688
+ LOG_PREFIX,
689
+ "couldn't read the database schema and no bundled manifest exists, so only registered functions and docs are exposed",
690
+ INTROSPECTION_FIX
691
+ )
692
+ );
693
+ return;
694
+ }
695
+ if (logReady) {
696
+ const details = ladder.mode === "manifest" ? ["from the bundled manifest"] : [];
697
+ console.log(formatReady(LOG_PREFIX, { endpoint, tools: supabaseToolsPhrase(ladder, cfg, docs > 0), details, docs, auth }));
698
+ }
699
+ if (ladder.mode === "manifest") {
700
+ console.warn(
701
+ formatWarning(LOG_PREFIX, "live schema introspection failed; tools come from the bundled manifest and may be out of date", INTROSPECTION_FIX)
702
+ );
703
+ }
704
+ }
705
+
608
706
  // src/index.ts
707
+ import { completeMcpLogin, createHostedAuthResolver, hostedBroker } from "@exposify/core";
609
708
  function denoEnv() {
610
709
  return globalThis.Deno?.env;
611
710
  }
612
711
  function aiReady(options = {}) {
613
- const cfg = resolveSupabaseConfig(options);
712
+ const brokerOn = isHostedResolver(options.auth) || !options.auth && !options.oauth && Boolean(readEnv("EXPOSIFY_KEY"));
713
+ const cfg = resolveSupabaseConfig(options, brokerOn);
614
714
  const fetchImpl = options.fetchImpl ?? fetch;
615
715
  const env = denoEnv();
616
716
  const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
617
717
  const anonKey = options.anonKey ?? env?.get("SUPABASE_ANON_KEY") ?? "";
618
718
  const serviceRoleKey = options.serviceRoleKey ?? env?.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
719
+ const gate = options.oauth ? createSupabaseOAuthGate(options.oauth, supabaseUrl, fetchImpl) : null;
720
+ const logReady = startupLogEnabled(options.startupLog);
721
+ const functionName = functionNameFromMainModule();
722
+ const endpoint = gate?.metadata.resource ?? (supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : "this function");
723
+ const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : brokerOn ? "hosted broker (EXPOSIFY_KEY)" : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
724
+ if (!supabaseUrl) {
725
+ console.error(
726
+ formatFailed2(LOG_PREFIX, "SUPABASE_URL is not set", "deployed functions get it automatically; for local dev pass { supabaseUrl }")
727
+ );
728
+ } else if (gate && logReady) {
729
+ void gate.probe().then((problem) => {
730
+ if (!problem) return;
731
+ console.warn(
732
+ formatWarning2(
733
+ LOG_PREFIX,
734
+ `can't verify access tokens from oauth.issuer ${String(gate.metadata.authorization_servers[0])}: ${problem}`,
735
+ "check the issuer (supabaseAuth() needs the project's OAuth server enabled and asymmetric JWT signing keys); until then MCP clients get 503"
736
+ )
737
+ );
738
+ });
739
+ }
740
+ let coldStartLogged = false;
619
741
  let kbPromise = null;
742
+ let docCount = 0;
620
743
  const getKb = () => {
621
744
  kbPromise ??= (async () => {
622
745
  const mainModule = globalThis.Deno?.mainModule;
623
746
  if (!mainModule) return KnowledgeBase.build([]);
624
747
  try {
625
- return KnowledgeBase.build(await loadDocsFromDeno(cfg.docs, new URL(mainModule)));
748
+ const docs = await loadDocsFromDeno(cfg.docs, new URL(mainModule));
749
+ docCount = docs.length;
750
+ return KnowledgeBase.build(docs);
626
751
  } catch {
627
752
  return KnowledgeBase.build([]);
628
753
  }
@@ -657,6 +782,14 @@ function aiReady(options = {}) {
657
782
  const incoming = req.headers;
658
783
  const headers = buildPostgrestHeaders(resolvedAuth.get(req) ?? incoming, anonKey);
659
784
  const ladder = await getLadder(postgrestUrl);
785
+ const kb = await getKb();
786
+ if (!coldStartLogged) {
787
+ coldStartLogged = true;
788
+ try {
789
+ logColdStart({ ladder, cfg, docs: docCount, endpoint, auth: authLabel, logReady });
790
+ } catch {
791
+ }
792
+ }
660
793
  const client = createPostgrestClient(postgrestUrl, headers, fetchImpl);
661
794
  const invokeFunction = async (name, body) => {
662
795
  const res = await fetchImpl(`${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${name}`, {
@@ -673,7 +806,7 @@ function aiReady(options = {}) {
673
806
  cfg,
674
807
  client,
675
808
  openapi: ladder.openapi,
676
- kb: await getKb(),
809
+ kb,
677
810
  incoming,
678
811
  invokeFunction,
679
812
  warnings: ladder.warnings
@@ -682,17 +815,36 @@ function aiReady(options = {}) {
682
815
  },
683
816
  { rateLimit: cfg.rateLimit, rateWindowMs: cfg.rateWindowMs }
684
817
  );
685
- const auth = options.auth;
818
+ if (gate) {
819
+ return async (req) => {
820
+ if (new URL(req.url).pathname.endsWith(PRM_SUFFIX)) return gate.metadataResponse(req.method);
821
+ const result = await gate.check(req.headers);
822
+ return result.ok ? core(req) : result.response;
823
+ };
824
+ }
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));
686
828
  if (!auth) return core;
829
+ const brokerGate = authRequired ? createBrokerGate(auth) : null;
830
+ const functionUrl = supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : null;
687
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
+ }
688
840
  let resolved;
689
841
  try {
690
- resolved = await auth(req.headers);
842
+ resolved = await auth(req.headers, {});
691
843
  } catch (err) {
692
844
  console.error("exposify-supabase: auth resolver failed", err);
693
845
  return Response.json({ error: "internal MCP server error" }, { status: 503 });
694
846
  }
695
- if (options.authRequired && !resolved.get("authorization")) {
847
+ if (authRequired && !resolved.get("authorization")) {
696
848
  return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
697
849
  }
698
850
  resolvedAuth.set(req, resolved);
@@ -705,9 +857,13 @@ export {
705
857
  aiReady,
706
858
  buildManifest,
707
859
  columnsFromOpenApi,
860
+ completeMcpLogin,
861
+ createHostedAuthResolver,
708
862
  fetchExposifyMeta,
709
863
  fetchOpenApi,
864
+ hostedBroker,
710
865
  introspectTables,
711
866
  loadManifest,
712
- rpcArgSchema
867
+ rpcArgSchema,
868
+ supabaseAuth
713
869
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exposify/supabase",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",
@@ -27,12 +27,13 @@
27
27
  "prepack": "yarn build"
28
28
  },
29
29
  "dependencies": {
30
- "@exposify/core": "0.3.11",
30
+ "@exposify/core": "0.3.13",
31
31
  "@modelcontextprotocol/sdk": "^1.12.0",
32
32
  "zod": "^3.24.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^22.0.0",
36
+ "jose": "^6.1.3",
36
37
  "tsup": "^8.3.0",
37
38
  "typescript": "^5.6.0",
38
39
  "vitest": "^3.0.0"