@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.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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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"
|