@exposify/supabase 0.3.4 → 0.3.6
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 +41 -0
- package/dist/index.cjs +19 -2
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +19 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -227,6 +227,47 @@ Ask the agent to query or mutate data. **It should succeed for that user's
|
|
|
227
227
|
own rows and be refused everyone else's** — that's the definition of done:
|
|
228
228
|
RLS enforcement, not anything in this SDK, is what makes that true.
|
|
229
229
|
|
|
230
|
+
## Long-lived tokens for clients that can't refresh (token broker)
|
|
231
|
+
|
|
232
|
+
Supabase user JWTs expire (default 1h) and their refresh tokens rotate; a plain
|
|
233
|
+
MCP client can hold a bearer token but can't refresh it. Pass an `auth`
|
|
234
|
+
resolver (any `(incoming: Headers) => Headers | Promise<Headers>`, such as
|
|
235
|
+
`broker.resolver` from [`@exposify/token-broker`](../token-broker/README.md))
|
|
236
|
+
and the client sends a long-lived broker token instead:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// supabase/functions/mcp/index.ts
|
|
240
|
+
import { aiReady } from "npm:@exposify/supabase";
|
|
241
|
+
import { broker } from "./broker.ts"; // createTokenBroker(...) over a shared store
|
|
242
|
+
|
|
243
|
+
Deno.serve(aiReady({ auth: broker.resolver, authRequired: true, writes: ["todos.*"] }));
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- The resolver receives the request's original headers (the `extb_...` token)
|
|
247
|
+
and its result **replaces** them for PostgREST and `functions` calls, so RLS
|
|
248
|
+
applies as that user and the broker token is never forwarded. `apikey` and a
|
|
249
|
+
missing `authorization` still fall back to the anon key.
|
|
250
|
+
- `docsAuth: "required"` checks the raw incoming headers, so a broker token
|
|
251
|
+
counts as authenticated.
|
|
252
|
+
- A broker resolver returns empty headers for an unknown/revoked token or a
|
|
253
|
+
failed refresh. By default the request then runs as the `anon` role (RLS
|
|
254
|
+
decides). Set `authRequired: true` to answer `401` instead. Recommended.
|
|
255
|
+
- A resolver that throws yields a 503.
|
|
256
|
+
|
|
257
|
+
**Deployment constraints** (not verified end to end, see HANDOVER):
|
|
258
|
+
|
|
259
|
+
- The broker's OAuth server (`listenTokenBroker`) is a Node HTTP server and
|
|
260
|
+
can't run in an Edge Function; run it as a separate service.
|
|
261
|
+
- The edge function builds its own `createTokenBroker(...)` over the **same
|
|
262
|
+
persistent `TokenStore`** (Redis/Postgres, see the token-broker README's
|
|
263
|
+
`ioredis` example) and the same `TOKEN_BROKER_ENCRYPTION_KEY`. The in-memory
|
|
264
|
+
store is per-process and won't work.
|
|
265
|
+
- The broker service does proactive refresh; short-lived edge instances rely on
|
|
266
|
+
the resolver's lazy refresh when `expiresAt` has passed.
|
|
267
|
+
- `@exposify/token-broker` imports `node:crypto` (`scryptSync`, AES-256-GCM,
|
|
268
|
+
RS256). Whether that works under Supabase's Deno runtime has **not** been
|
|
269
|
+
tested; try `supabase functions serve` before depending on it.
|
|
270
|
+
|
|
230
271
|
## Knowledge base
|
|
231
272
|
|
|
232
273
|
Any markdown files matching the `docs` glob (default `docs/**/*.md`,
|
package/dist/index.cjs
CHANGED
|
@@ -674,7 +674,8 @@ function aiReady(options = {}) {
|
|
|
674
674
|
});
|
|
675
675
|
return ladderPromise;
|
|
676
676
|
};
|
|
677
|
-
|
|
677
|
+
const resolvedAuth = /* @__PURE__ */ new WeakMap();
|
|
678
|
+
const core = (0, import_core3.createWebMcpHandler)(
|
|
678
679
|
{
|
|
679
680
|
getServer: async (req) => {
|
|
680
681
|
if (!supabaseUrl) {
|
|
@@ -684,7 +685,7 @@ function aiReady(options = {}) {
|
|
|
684
685
|
}
|
|
685
686
|
const postgrestUrl = `${supabaseUrl.replace(/\/+$/, "")}/rest/v1`;
|
|
686
687
|
const incoming = req.headers;
|
|
687
|
-
const headers = buildPostgrestHeaders(incoming, anonKey);
|
|
688
|
+
const headers = buildPostgrestHeaders(resolvedAuth.get(req) ?? incoming, anonKey);
|
|
688
689
|
const ladder = await getLadder(postgrestUrl);
|
|
689
690
|
const client = createPostgrestClient(postgrestUrl, headers, fetchImpl);
|
|
690
691
|
const invokeFunction = async (name, body) => {
|
|
@@ -711,6 +712,22 @@ function aiReady(options = {}) {
|
|
|
711
712
|
},
|
|
712
713
|
{ rateLimit: cfg.rateLimit, rateWindowMs: cfg.rateWindowMs }
|
|
713
714
|
);
|
|
715
|
+
const auth = options.auth;
|
|
716
|
+
if (!auth) return core;
|
|
717
|
+
return async (req) => {
|
|
718
|
+
let resolved;
|
|
719
|
+
try {
|
|
720
|
+
resolved = await auth(req.headers);
|
|
721
|
+
} catch (err) {
|
|
722
|
+
console.error("exposify-supabase: auth resolver failed", err);
|
|
723
|
+
return Response.json({ error: "internal MCP server error" }, { status: 503 });
|
|
724
|
+
}
|
|
725
|
+
if (options.authRequired && !resolved.get("authorization")) {
|
|
726
|
+
return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
727
|
+
}
|
|
728
|
+
resolvedAuth.set(req, resolved);
|
|
729
|
+
return core(req);
|
|
730
|
+
};
|
|
714
731
|
}
|
|
715
732
|
// Annotate the CommonJS export names for ESM import in node:
|
|
716
733
|
0 && (module.exports = {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebMcpHandler } from '@exposify/core';
|
|
1
|
+
import { AuthResolver, WebMcpHandler } from '@exposify/core';
|
|
2
2
|
|
|
3
3
|
type ColumnType = "string" | "number" | "integer" | "boolean" | "object" | "array";
|
|
4
4
|
interface ColumnSchema {
|
|
@@ -72,6 +72,22 @@ interface AgentifySupabaseOptions {
|
|
|
72
72
|
* deployed edge function; this option exists for local dev/testing only.
|
|
73
73
|
*/
|
|
74
74
|
serviceRoleKey?: string;
|
|
75
|
+
/**
|
|
76
|
+
* 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
|
|
79
|
+
* Supabase JWT. Only `authorization`/`apikey` from the result are used; a
|
|
80
|
+
* missing `authorization` falls back to the anon key (unless
|
|
81
|
+
* `authRequired`). Absent: the caller's own `authorization`/`apikey` are
|
|
82
|
+
* forwarded as-is.
|
|
83
|
+
*/
|
|
84
|
+
auth?: AuthResolver;
|
|
85
|
+
/**
|
|
86
|
+
* With `auth` set: respond 401 when the resolver returns no `authorization`
|
|
87
|
+
* (unknown/revoked broker token, failed refresh) instead of silently
|
|
88
|
+
* running the request as the `anon` role. Default false.
|
|
89
|
+
*/
|
|
90
|
+
authRequired?: boolean;
|
|
75
91
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
76
92
|
fetchImpl?: typeof fetch;
|
|
77
93
|
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WebMcpHandler } from '@exposify/core';
|
|
1
|
+
import { AuthResolver, WebMcpHandler } from '@exposify/core';
|
|
2
2
|
|
|
3
3
|
type ColumnType = "string" | "number" | "integer" | "boolean" | "object" | "array";
|
|
4
4
|
interface ColumnSchema {
|
|
@@ -72,6 +72,22 @@ interface AgentifySupabaseOptions {
|
|
|
72
72
|
* deployed edge function; this option exists for local dev/testing only.
|
|
73
73
|
*/
|
|
74
74
|
serviceRoleKey?: string;
|
|
75
|
+
/**
|
|
76
|
+
* 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
|
|
79
|
+
* Supabase JWT. Only `authorization`/`apikey` from the result are used; a
|
|
80
|
+
* missing `authorization` falls back to the anon key (unless
|
|
81
|
+
* `authRequired`). Absent: the caller's own `authorization`/`apikey` are
|
|
82
|
+
* forwarded as-is.
|
|
83
|
+
*/
|
|
84
|
+
auth?: AuthResolver;
|
|
85
|
+
/**
|
|
86
|
+
* With `auth` set: respond 401 when the resolver returns no `authorization`
|
|
87
|
+
* (unknown/revoked broker token, failed refresh) instead of silently
|
|
88
|
+
* running the request as the `anon` role. Default false.
|
|
89
|
+
*/
|
|
90
|
+
authRequired?: boolean;
|
|
75
91
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
76
92
|
fetchImpl?: typeof fetch;
|
|
77
93
|
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
package/dist/index.js
CHANGED
|
@@ -644,7 +644,8 @@ function aiReady(options = {}) {
|
|
|
644
644
|
});
|
|
645
645
|
return ladderPromise;
|
|
646
646
|
};
|
|
647
|
-
|
|
647
|
+
const resolvedAuth = /* @__PURE__ */ new WeakMap();
|
|
648
|
+
const core = createWebMcpHandler(
|
|
648
649
|
{
|
|
649
650
|
getServer: async (req) => {
|
|
650
651
|
if (!supabaseUrl) {
|
|
@@ -654,7 +655,7 @@ function aiReady(options = {}) {
|
|
|
654
655
|
}
|
|
655
656
|
const postgrestUrl = `${supabaseUrl.replace(/\/+$/, "")}/rest/v1`;
|
|
656
657
|
const incoming = req.headers;
|
|
657
|
-
const headers = buildPostgrestHeaders(incoming, anonKey);
|
|
658
|
+
const headers = buildPostgrestHeaders(resolvedAuth.get(req) ?? incoming, anonKey);
|
|
658
659
|
const ladder = await getLadder(postgrestUrl);
|
|
659
660
|
const client = createPostgrestClient(postgrestUrl, headers, fetchImpl);
|
|
660
661
|
const invokeFunction = async (name, body) => {
|
|
@@ -681,6 +682,22 @@ function aiReady(options = {}) {
|
|
|
681
682
|
},
|
|
682
683
|
{ rateLimit: cfg.rateLimit, rateWindowMs: cfg.rateWindowMs }
|
|
683
684
|
);
|
|
685
|
+
const auth = options.auth;
|
|
686
|
+
if (!auth) return core;
|
|
687
|
+
return async (req) => {
|
|
688
|
+
let resolved;
|
|
689
|
+
try {
|
|
690
|
+
resolved = await auth(req.headers);
|
|
691
|
+
} catch (err) {
|
|
692
|
+
console.error("exposify-supabase: auth resolver failed", err);
|
|
693
|
+
return Response.json({ error: "internal MCP server error" }, { status: 503 });
|
|
694
|
+
}
|
|
695
|
+
if (options.authRequired && !resolved.get("authorization")) {
|
|
696
|
+
return Response.json({ error: "unauthorized" }, { status: 401, headers: { "www-authenticate": "Bearer" } });
|
|
697
|
+
}
|
|
698
|
+
resolvedAuth.set(req, resolved);
|
|
699
|
+
return core(req);
|
|
700
|
+
};
|
|
684
701
|
}
|
|
685
702
|
export {
|
|
686
703
|
MANIFEST_VERSION,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@exposify/supabase",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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.
|
|
30
|
+
"@exposify/core": "0.3.10",
|
|
31
31
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
32
32
|
"zod": "^3.24.0"
|
|
33
33
|
},
|