@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.cjs CHANGED
@@ -25,14 +25,18 @@ __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
- rpcArgSchema: () => rpcArgSchema
35
+ rpcArgSchema: () => rpcArgSchema,
36
+ supabaseAuth: () => supabaseAuth
33
37
  });
34
38
  module.exports = __toCommonJS(index_exports);
35
- var import_core3 = require("@exposify/core");
39
+ var import_core5 = require("@exposify/core");
36
40
 
37
41
  // src/kb.ts
38
42
  var import_core = require("@exposify/core");
@@ -107,13 +111,21 @@ function warnOnce(message) {
107
111
  } catch {
108
112
  }
109
113
  }
110
- function resolveSupabaseConfig(options = {}) {
114
+ function resolveSupabaseConfig(options = {}, hostedBroker2 = false) {
115
+ if (options.oauth) {
116
+ if (options.docsAuth === "public") {
117
+ throw new Error('docsAuth: "public" cannot be combined with oauth (the whole MCP endpoint requires a token)');
118
+ }
119
+ if (options.auth || options.authRequired) {
120
+ throw new Error("oauth cannot be combined with auth/authRequired (oauth already requires a valid token on every request)");
121
+ }
122
+ }
111
123
  return {
112
124
  writes: parseWrites(options.writes),
113
125
  rpc: options.rpc ?? [],
114
126
  functions: options.functions ?? {},
115
127
  docs: options.docs ?? ["docs/**/*.md"],
116
- docsAuth: options.docsAuth ?? "public",
128
+ docsAuth: options.docsAuth ?? (options.oauth || hostedBroker2 ? "required" : "public"),
117
129
  maxRows: options.maxRows ?? 100,
118
130
  rateLimit: options.rateLimit,
119
131
  rateWindowMs: options.rateWindowMs
@@ -635,24 +647,141 @@ async function toResult(run) {
635
647
  }
636
648
  }
637
649
 
650
+ // src/oauth.ts
651
+ var import_core3 = require("@exposify/core");
652
+ var PRM_SUFFIX = "/.well-known/oauth-protected-resource";
653
+ function supabaseAuth(overrides = {}) {
654
+ return (supabaseUrl) => {
655
+ const base = supabaseUrl.replace(/\/+$/, "");
656
+ return {
657
+ issuer: `${base}/auth/v1`,
658
+ jwksUri: `${base}/auth/v1/.well-known/jwks.json`,
659
+ audience: "authenticated",
660
+ ...overrides
661
+ };
662
+ };
663
+ }
664
+ function functionNameFromMainModule() {
665
+ const mainModule = globalThis.Deno?.mainModule;
666
+ if (!mainModule) return void 0;
667
+ try {
668
+ const parts = new URL(mainModule).pathname.split("/").filter(Boolean);
669
+ return parts.length >= 2 ? parts[parts.length - 2] : void 0;
670
+ } catch {
671
+ return void 0;
672
+ }
673
+ }
674
+ function createSupabaseOAuthGate(oauth, supabaseUrl, fetchImpl) {
675
+ if (typeof oauth === "function" && !supabaseUrl) {
676
+ throw new Error("oauth: SUPABASE_URL is not set. In a deployed function this is automatic; for local dev pass { supabaseUrl }.");
677
+ }
678
+ const opts = typeof oauth === "function" ? oauth(supabaseUrl) : oauth;
679
+ let resource = opts.resource;
680
+ if (!resource) {
681
+ const name = functionNameFromMainModule();
682
+ if (!supabaseUrl || !name) {
683
+ throw new Error("oauth.resource could not be derived (needs SUPABASE_URL and Deno.mainModule); pass oauth.resource explicitly.");
684
+ }
685
+ resource = `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${name}`;
686
+ }
687
+ const { metadataUrl, ...oauthOptions } = opts;
688
+ const cfg = (0, import_core3.resolveOAuthOptions)({ ...oauthOptions, resource });
689
+ if (metadataUrl !== void 0) {
690
+ try {
691
+ new URL(metadataUrl);
692
+ } catch {
693
+ throw new Error(`oauth.metadataUrl must be an absolute URL, got ${JSON.stringify(metadataUrl)}`);
694
+ }
695
+ }
696
+ return (0, import_core3.createOAuthGate)(cfg, { metadataUrl: metadataUrl ?? `${cfg.resource}${PRM_SUFFIX}`, fetchImpl });
697
+ }
698
+
699
+ // src/startup.ts
700
+ var import_core4 = require("@exposify/core");
701
+ var LOG_PREFIX = "[exposify-supabase]";
702
+ 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`";
703
+ function supabaseToolsPhrase(ladder, cfg, hasDocs) {
704
+ const query = ladder.tables.length;
705
+ let write = 0;
706
+ for (const table of ladder.tables) write += cfg.writes.get(table.name)?.size ?? 0;
707
+ const rpc = cfg.rpc.length;
708
+ const fn = Object.keys(cfg.functions).length;
709
+ const parts = [`${query} query`];
710
+ if (write) parts.push(`${write} write`);
711
+ if (rpc) parts.push(`${rpc} rpc`);
712
+ if (fn) parts.push(`${fn} function${fn === 1 ? "" : "s"}`);
713
+ if (hasDocs) parts.push("search_docs");
714
+ const total = query + write + rpc + fn + (hasDocs ? 1 : 0);
715
+ return `${total} tool${total === 1 ? "" : "s"} (${parts.join(", ")})`;
716
+ }
717
+ function logColdStart(opts) {
718
+ const { ladder, cfg, docs, endpoint, auth, logReady } = opts;
719
+ if (ladder.mode === "functions-only") {
720
+ console.error(
721
+ (0, import_core4.formatFailed)(
722
+ LOG_PREFIX,
723
+ "couldn't read the database schema and no bundled manifest exists, so only registered functions and docs are exposed",
724
+ INTROSPECTION_FIX
725
+ )
726
+ );
727
+ return;
728
+ }
729
+ if (logReady) {
730
+ const details = ladder.mode === "manifest" ? ["from the bundled manifest"] : [];
731
+ console.log((0, import_core4.formatReady)(LOG_PREFIX, { endpoint, tools: supabaseToolsPhrase(ladder, cfg, docs > 0), details, docs, auth }));
732
+ }
733
+ if (ladder.mode === "manifest") {
734
+ console.warn(
735
+ (0, import_core4.formatWarning)(LOG_PREFIX, "live schema introspection failed; tools come from the bundled manifest and may be out of date", INTROSPECTION_FIX)
736
+ );
737
+ }
738
+ }
739
+
638
740
  // src/index.ts
741
+ var import_core6 = require("@exposify/core");
639
742
  function denoEnv() {
640
743
  return globalThis.Deno?.env;
641
744
  }
642
745
  function aiReady(options = {}) {
643
- 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);
644
748
  const fetchImpl = options.fetchImpl ?? fetch;
645
749
  const env = denoEnv();
646
750
  const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
647
751
  const anonKey = options.anonKey ?? env?.get("SUPABASE_ANON_KEY") ?? "";
648
752
  const serviceRoleKey = options.serviceRoleKey ?? env?.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
753
+ const gate = options.oauth ? createSupabaseOAuthGate(options.oauth, supabaseUrl, fetchImpl) : null;
754
+ const logReady = (0, import_core5.startupLogEnabled)(options.startupLog);
755
+ const functionName = functionNameFromMainModule();
756
+ const endpoint = gate?.metadata.resource ?? (supabaseUrl && functionName ? `${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${functionName}` : "this function");
757
+ const authLabel = gate ? `oauth (${String(gate.metadata.authorization_servers[0])})` : brokerOn ? "hosted broker (EXPOSIFY_KEY)" : options.auth ? "custom resolver" : "passthrough (caller's JWT)";
758
+ if (!supabaseUrl) {
759
+ console.error(
760
+ (0, import_core5.formatFailed)(LOG_PREFIX, "SUPABASE_URL is not set", "deployed functions get it automatically; for local dev pass { supabaseUrl }")
761
+ );
762
+ } else if (gate && logReady) {
763
+ void gate.probe().then((problem) => {
764
+ if (!problem) return;
765
+ console.warn(
766
+ (0, import_core5.formatWarning)(
767
+ LOG_PREFIX,
768
+ `can't verify access tokens from oauth.issuer ${String(gate.metadata.authorization_servers[0])}: ${problem}`,
769
+ "check the issuer (supabaseAuth() needs the project's OAuth server enabled and asymmetric JWT signing keys); until then MCP clients get 503"
770
+ )
771
+ );
772
+ });
773
+ }
774
+ let coldStartLogged = false;
649
775
  let kbPromise = null;
776
+ let docCount = 0;
650
777
  const getKb = () => {
651
778
  kbPromise ??= (async () => {
652
779
  const mainModule = globalThis.Deno?.mainModule;
653
780
  if (!mainModule) return import_core.KnowledgeBase.build([]);
654
781
  try {
655
- return import_core.KnowledgeBase.build(await loadDocsFromDeno(cfg.docs, new URL(mainModule)));
782
+ const docs = await loadDocsFromDeno(cfg.docs, new URL(mainModule));
783
+ docCount = docs.length;
784
+ return import_core.KnowledgeBase.build(docs);
656
785
  } catch {
657
786
  return import_core.KnowledgeBase.build([]);
658
787
  }
@@ -675,7 +804,7 @@ function aiReady(options = {}) {
675
804
  return ladderPromise;
676
805
  };
677
806
  const resolvedAuth = /* @__PURE__ */ new WeakMap();
678
- const core = (0, import_core3.createWebMcpHandler)(
807
+ const core = (0, import_core5.createWebMcpHandler)(
679
808
  {
680
809
  getServer: async (req) => {
681
810
  if (!supabaseUrl) {
@@ -687,6 +816,14 @@ function aiReady(options = {}) {
687
816
  const incoming = req.headers;
688
817
  const headers = buildPostgrestHeaders(resolvedAuth.get(req) ?? incoming, anonKey);
689
818
  const ladder = await getLadder(postgrestUrl);
819
+ const kb = await getKb();
820
+ if (!coldStartLogged) {
821
+ coldStartLogged = true;
822
+ try {
823
+ logColdStart({ ladder, cfg, docs: docCount, endpoint, auth: authLabel, logReady });
824
+ } catch {
825
+ }
826
+ }
690
827
  const client = createPostgrestClient(postgrestUrl, headers, fetchImpl);
691
828
  const invokeFunction = async (name, body) => {
692
829
  const res = await fetchImpl(`${supabaseUrl.replace(/\/+$/, "")}/functions/v1/${name}`, {
@@ -703,7 +840,7 @@ function aiReady(options = {}) {
703
840
  cfg,
704
841
  client,
705
842
  openapi: ladder.openapi,
706
- kb: await getKb(),
843
+ kb,
707
844
  incoming,
708
845
  invokeFunction,
709
846
  warnings: ladder.warnings
@@ -712,17 +849,36 @@ function aiReady(options = {}) {
712
849
  },
713
850
  { rateLimit: cfg.rateLimit, rateWindowMs: cfg.rateWindowMs }
714
851
  );
715
- const auth = options.auth;
852
+ if (gate) {
853
+ return async (req) => {
854
+ if (new URL(req.url).pathname.endsWith(PRM_SUFFIX)) return gate.metadataResponse(req.method);
855
+ const result = await gate.check(req.headers);
856
+ return result.ok ? core(req) : result.response;
857
+ };
858
+ }
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));
716
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;
717
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
+ }
718
874
  let resolved;
719
875
  try {
720
- resolved = await auth(req.headers);
876
+ resolved = await auth(req.headers, {});
721
877
  } catch (err) {
722
878
  console.error("exposify-supabase: auth resolver failed", err);
723
879
  return Response.json({ error: "internal MCP server error" }, { status: 503 });
724
880
  }
725
- if (options.authRequired && !resolved.get("authorization")) {
881
+ if (authRequired && !resolved.get("authorization")) {
726
882
  return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
727
883
  }
728
884
  resolvedAuth.set(req, resolved);
@@ -736,9 +892,13 @@ function aiReady(options = {}) {
736
892
  aiReady,
737
893
  buildManifest,
738
894
  columnsFromOpenApi,
895
+ completeMcpLogin,
896
+ createHostedAuthResolver,
739
897
  fetchExposifyMeta,
740
898
  fetchOpenApi,
899
+ hostedBroker,
741
900
  introspectTables,
742
901
  loadManifest,
743
- rpcArgSchema
902
+ rpcArgSchema,
903
+ supabaseAuth
744
904
  });
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { AuthResolver, WebMcpHandler } from '@exposify/core';
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 {
@@ -45,6 +46,23 @@ interface JsonSchemaObject {
45
46
  properties?: Record<string, JsonSchemaProperty>;
46
47
  required?: string[];
47
48
  }
49
+ /**
50
+ * `OAuthOptions` with `resource` optional: it defaults to this function's own
51
+ * URL, `${SUPABASE_URL}/functions/v1/<function name>`.
52
+ */
53
+ type SupabaseOAuthOptions = Omit<OAuthOptions, "resource"> & {
54
+ resource?: string;
55
+ /**
56
+ * Where the `401` challenge says the Protected Resource Metadata lives.
57
+ * Default: `${resource}/.well-known/oauth-protected-resource`, under the
58
+ * function's own URL. Set it when a proxy you control serves the function
59
+ * on your own domain and forwards the RFC 9728 root-level well-known URI
60
+ * (e.g. `https://mcp.acme.com/.well-known/oauth-protected-resource/mcp`)
61
+ * to the function; the function answers any path ending in
62
+ * `/.well-known/oauth-protected-resource`.
63
+ */
64
+ metadataUrl?: string;
65
+ };
48
66
  interface AgentifySupabaseOptions {
49
67
  /** `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` / `"<table>.*"`. */
50
68
  writes?: string[];
@@ -54,7 +72,25 @@ interface AgentifySupabaseOptions {
54
72
  functions?: Record<string, FunctionToolConfig>;
55
73
  /** Doc globs bundled in the function directory. Default: `["docs/**\/*.md"]`. */
56
74
  docs?: string[];
75
+ /** Default `"public"`, or `"required"` when `oauth` is set (which can't be combined with `"public"`). */
57
76
  docsAuth?: "public" | "required";
77
+ /**
78
+ * Require an OAuth access token on the whole endpoint, so MCP clients can
79
+ * "click Connect and log in": a tokenless request gets a `401` pointing at
80
+ * Protected Resource Metadata served under this function's own path. Use
81
+ * `supabaseAuth()` for Supabase Auth's OAuth 2.1 server, or pass any
82
+ * spec-compliant AS (a function form receives the resolved `SUPABASE_URL`).
83
+ * The validated token is forwarded to PostgREST as-is, so RLS runs as the
84
+ * signed-in user. Can't be combined with `auth`/`authRequired`; deploy with
85
+ * `verify_jwt = false` so the gateway lets the first tokenless request
86
+ * through to answer it.
87
+ */
88
+ oauth?: SupabaseOAuthOptions | ((supabaseUrl: string) => SupabaseOAuthOptions);
89
+ /**
90
+ * The cold-start "MCP ready" line and the oauth issuer check. Default: on,
91
+ * except under `NODE_ENV=test`. Failures are logged regardless.
92
+ */
93
+ startupLog?: boolean;
58
94
  /** Row cap applied to every `query_<table>` call regardless of caller-supplied `limit`. Default 100. */
59
95
  maxRows?: number;
60
96
  /** Overrides for local/CI use -- production reads these from the Deno env automatically. */
@@ -74,8 +110,8 @@ interface AgentifySupabaseOptions {
74
110
  serviceRoleKey?: string;
75
111
  /**
76
112
  * Resolves the caller's incoming headers into the headers used for
77
- * PostgREST and edge-function calls, e.g. `broker.resolver` from
78
- * `@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
79
115
  * Supabase JWT. Only `authorization`/`apikey` from the result are used; a
80
116
  * missing `authorization` falls back to the anon key (unless
81
117
  * `authRequired`). Absent: the caller's own `authorization`/`apikey` are
@@ -96,6 +132,20 @@ interface AgentifySupabaseOptions {
96
132
  rateWindowMs?: number;
97
133
  }
98
134
 
135
+ /**
136
+ * Supabase Auth's OAuth 2.1 server as the authorization server. Per
137
+ * Supabase's docs its access tokens are ordinary project JWTs: `iss` is
138
+ * `${SUPABASE_URL}/auth/v1`, keys are at `/auth/v1/.well-known/jwks.json`,
139
+ * and `aud` is always `"authenticated"` -- it can't bind tokens to this
140
+ * function's URL, hence the `audience` override (any of the project's user
141
+ * JWTs is accepted, which is exactly what RLS already trusts). Needs, in the
142
+ * project: the OAuth 2.1 server enabled (beta as of 2026-10) with dynamic
143
+ * client registration on, an authorization path pointing at a consent page
144
+ * the app hosts, and asymmetric JWT signing keys (legacy HS256 projects have
145
+ * no JWKS to verify against).
146
+ */
147
+ declare function supabaseAuth(overrides?: Partial<SupabaseOAuthOptions>): (supabaseUrl: string) => SupabaseOAuthOptions;
148
+
99
149
  /** Subset of PostgREST's generated OpenAPI (Swagger 2.0) document that we read. */
100
150
  interface PostgrestOpenApiDoc {
101
151
  definitions?: Record<string, PostgrestTableDefinition>;
@@ -205,4 +255,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
205
255
  */
206
256
  declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
207
257
 
208
- export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
258
+ export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, type SupabaseOAuthOptions, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema, supabaseAuth };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { AuthResolver, WebMcpHandler } from '@exposify/core';
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 {
@@ -45,6 +46,23 @@ interface JsonSchemaObject {
45
46
  properties?: Record<string, JsonSchemaProperty>;
46
47
  required?: string[];
47
48
  }
49
+ /**
50
+ * `OAuthOptions` with `resource` optional: it defaults to this function's own
51
+ * URL, `${SUPABASE_URL}/functions/v1/<function name>`.
52
+ */
53
+ type SupabaseOAuthOptions = Omit<OAuthOptions, "resource"> & {
54
+ resource?: string;
55
+ /**
56
+ * Where the `401` challenge says the Protected Resource Metadata lives.
57
+ * Default: `${resource}/.well-known/oauth-protected-resource`, under the
58
+ * function's own URL. Set it when a proxy you control serves the function
59
+ * on your own domain and forwards the RFC 9728 root-level well-known URI
60
+ * (e.g. `https://mcp.acme.com/.well-known/oauth-protected-resource/mcp`)
61
+ * to the function; the function answers any path ending in
62
+ * `/.well-known/oauth-protected-resource`.
63
+ */
64
+ metadataUrl?: string;
65
+ };
48
66
  interface AgentifySupabaseOptions {
49
67
  /** `"<table>.insert"` / `"<table>.update"` / `"<table>.delete"` / `"<table>.*"`. */
50
68
  writes?: string[];
@@ -54,7 +72,25 @@ interface AgentifySupabaseOptions {
54
72
  functions?: Record<string, FunctionToolConfig>;
55
73
  /** Doc globs bundled in the function directory. Default: `["docs/**\/*.md"]`. */
56
74
  docs?: string[];
75
+ /** Default `"public"`, or `"required"` when `oauth` is set (which can't be combined with `"public"`). */
57
76
  docsAuth?: "public" | "required";
77
+ /**
78
+ * Require an OAuth access token on the whole endpoint, so MCP clients can
79
+ * "click Connect and log in": a tokenless request gets a `401` pointing at
80
+ * Protected Resource Metadata served under this function's own path. Use
81
+ * `supabaseAuth()` for Supabase Auth's OAuth 2.1 server, or pass any
82
+ * spec-compliant AS (a function form receives the resolved `SUPABASE_URL`).
83
+ * The validated token is forwarded to PostgREST as-is, so RLS runs as the
84
+ * signed-in user. Can't be combined with `auth`/`authRequired`; deploy with
85
+ * `verify_jwt = false` so the gateway lets the first tokenless request
86
+ * through to answer it.
87
+ */
88
+ oauth?: SupabaseOAuthOptions | ((supabaseUrl: string) => SupabaseOAuthOptions);
89
+ /**
90
+ * The cold-start "MCP ready" line and the oauth issuer check. Default: on,
91
+ * except under `NODE_ENV=test`. Failures are logged regardless.
92
+ */
93
+ startupLog?: boolean;
58
94
  /** Row cap applied to every `query_<table>` call regardless of caller-supplied `limit`. Default 100. */
59
95
  maxRows?: number;
60
96
  /** Overrides for local/CI use -- production reads these from the Deno env automatically. */
@@ -74,8 +110,8 @@ interface AgentifySupabaseOptions {
74
110
  serviceRoleKey?: string;
75
111
  /**
76
112
  * Resolves the caller's incoming headers into the headers used for
77
- * PostgREST and edge-function calls, e.g. `broker.resolver` from
78
- * `@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
79
115
  * Supabase JWT. Only `authorization`/`apikey` from the result are used; a
80
116
  * missing `authorization` falls back to the anon key (unless
81
117
  * `authRequired`). Absent: the caller's own `authorization`/`apikey` are
@@ -96,6 +132,20 @@ interface AgentifySupabaseOptions {
96
132
  rateWindowMs?: number;
97
133
  }
98
134
 
135
+ /**
136
+ * Supabase Auth's OAuth 2.1 server as the authorization server. Per
137
+ * Supabase's docs its access tokens are ordinary project JWTs: `iss` is
138
+ * `${SUPABASE_URL}/auth/v1`, keys are at `/auth/v1/.well-known/jwks.json`,
139
+ * and `aud` is always `"authenticated"` -- it can't bind tokens to this
140
+ * function's URL, hence the `audience` override (any of the project's user
141
+ * JWTs is accepted, which is exactly what RLS already trusts). Needs, in the
142
+ * project: the OAuth 2.1 server enabled (beta as of 2026-10) with dynamic
143
+ * client registration on, an authorization path pointing at a consent page
144
+ * the app hosts, and asymmetric JWT signing keys (legacy HS256 projects have
145
+ * no JWKS to verify against).
146
+ */
147
+ declare function supabaseAuth(overrides?: Partial<SupabaseOAuthOptions>): (supabaseUrl: string) => SupabaseOAuthOptions;
148
+
99
149
  /** Subset of PostgREST's generated OpenAPI (Swagger 2.0) document that we read. */
100
150
  interface PostgrestOpenApiDoc {
101
151
  definitions?: Record<string, PostgrestTableDefinition>;
@@ -205,4 +255,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
205
255
  */
206
256
  declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
207
257
 
208
- export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
258
+ export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, type SupabaseOAuthOptions, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema, supabaseAuth };