@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/README.md +283 -72
- package/dist/cli.js +1144 -279
- package/dist/index.cjs +172 -12
- package/dist/index.d.cts +54 -4
- package/dist/index.d.ts +54 -4
- package/dist/index.js +166 -10
- package/package.json +3 -2
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
|
|
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
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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. `
|
|
78
|
-
*
|
|
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. `
|
|
78
|
-
*
|
|
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 };
|