@exposify/supabase 0.1.0 → 0.3.1
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/LICENSE +21 -0
- package/README.md +122 -23
- package/dist/cli.js +412 -75
- package/dist/index.cjs +34 -25
- package/dist/index.d.cts +40 -19
- package/dist/index.d.ts +40 -19
- package/dist/index.js +32 -23
- package/package.json +12 -3
- package/sql/{agentify_meta.sql → exposify_meta.sql} +12 -4
package/dist/index.cjs
CHANGED
|
@@ -21,11 +21,11 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
MANIFEST_VERSION: () => MANIFEST_VERSION,
|
|
24
|
-
|
|
24
|
+
MissingExposifyMetaError: () => MissingExposifyMetaError,
|
|
25
25
|
aiReady: () => aiReady,
|
|
26
26
|
buildManifest: () => buildManifest,
|
|
27
27
|
columnsFromOpenApi: () => columnsFromOpenApi,
|
|
28
|
-
|
|
28
|
+
fetchExposifyMeta: () => fetchExposifyMeta,
|
|
29
29
|
fetchOpenApi: () => fetchOpenApi,
|
|
30
30
|
introspectTables: () => introspectTables,
|
|
31
31
|
loadManifest: () => loadManifest,
|
|
@@ -103,7 +103,7 @@ function warnOnce(message) {
|
|
|
103
103
|
if (warned.has(message)) return;
|
|
104
104
|
warned.add(message);
|
|
105
105
|
try {
|
|
106
|
-
console.warn(`[
|
|
106
|
+
console.warn(`[exposify-supabase] ${message}`);
|
|
107
107
|
} catch {
|
|
108
108
|
}
|
|
109
109
|
}
|
|
@@ -129,6 +129,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
|
|
|
129
129
|
if (value) headers.set(name, value);
|
|
130
130
|
}
|
|
131
131
|
if (!headers.has("apikey")) headers.set("apikey", anonKey);
|
|
132
|
+
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
|
|
132
133
|
return headers;
|
|
133
134
|
}
|
|
134
135
|
function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
@@ -161,42 +162,49 @@ function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
|
161
162
|
}
|
|
162
163
|
|
|
163
164
|
// src/fetch-openapi.ts
|
|
165
|
+
var WRONG_KEY_TIER_RE = /service_role|secret api key/i;
|
|
164
166
|
async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
165
167
|
const res = await fetchImpl(postgrestUrl, {
|
|
166
168
|
headers: { ...Object.fromEntries(headers), accept: "application/openapi+json" }
|
|
167
169
|
});
|
|
168
170
|
if (!res.ok) {
|
|
169
|
-
|
|
171
|
+
const body = await res.text().catch(() => "");
|
|
172
|
+
if (res.status === 401 && WRONG_KEY_TIER_RE.test(body)) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`PostgREST OpenAPI fetch failed: HTTP 401 -- Supabase rejected this request because it wasn't made with the service role key (response: "${body.trim()}"). Schema introspection always needs SUPABASE_SERVICE_ROLE_KEY, never the anon key -- see this package's README, "Why introspection needs the service role key". In a deployed function this is auto-injected automatically; for local dev/testing pass { serviceRoleKey } explicitly (or --key for the CLI).`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
throw new Error(`PostgREST OpenAPI fetch failed: HTTP ${res.status}${body ? `: ${body}` : ""}`);
|
|
170
178
|
}
|
|
171
179
|
return await res.json();
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
// src/meta.ts
|
|
175
|
-
var
|
|
183
|
+
var MissingExposifyMetaError = class extends Error {
|
|
176
184
|
constructor(cause) {
|
|
177
185
|
super(cause);
|
|
178
|
-
this.name = "
|
|
186
|
+
this.name = "MissingExposifyMetaError";
|
|
179
187
|
}
|
|
180
188
|
};
|
|
181
|
-
async function
|
|
182
|
-
const res = await fetchImpl(`${postgrestUrl}/rpc/
|
|
189
|
+
async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
190
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
|
|
183
191
|
method: "POST",
|
|
184
192
|
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
185
193
|
body: "{}"
|
|
186
194
|
});
|
|
187
195
|
if (res.status === 404) {
|
|
188
|
-
throw new
|
|
189
|
-
"
|
|
196
|
+
throw new MissingExposifyMetaError(
|
|
197
|
+
"exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
190
198
|
);
|
|
191
199
|
}
|
|
192
200
|
if (!res.ok) {
|
|
193
201
|
const body = await res.text().catch(() => "");
|
|
194
202
|
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
195
|
-
throw new
|
|
196
|
-
"
|
|
203
|
+
throw new MissingExposifyMetaError(
|
|
204
|
+
"exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
|
|
197
205
|
);
|
|
198
206
|
}
|
|
199
|
-
throw new Error(`
|
|
207
|
+
throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
200
208
|
}
|
|
201
209
|
return await res.json();
|
|
202
210
|
}
|
|
@@ -227,7 +235,7 @@ function columnsFromOpenApi(doc, table) {
|
|
|
227
235
|
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
228
236
|
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
229
237
|
// so a column with no real comment ends up `undefined`, not the
|
|
230
|
-
// boilerplate text.
|
|
238
|
+
// boilerplate text. exposify_meta()'s copy wins when both exist anyway
|
|
231
239
|
// -- see introspect.ts's merge.
|
|
232
240
|
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
233
241
|
}));
|
|
@@ -262,7 +270,7 @@ function introspectTables(openapi, metaRows) {
|
|
|
262
270
|
if (!fromOpenApi) continue;
|
|
263
271
|
columns.push({
|
|
264
272
|
...fromOpenApi,
|
|
265
|
-
//
|
|
273
|
+
// exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
266
274
|
// "Column comments → param descriptions" sourced from the migration).
|
|
267
275
|
description: row.column_comment ?? fromOpenApi.description
|
|
268
276
|
});
|
|
@@ -305,7 +313,7 @@ async function resolveTableSchemas(deps) {
|
|
|
305
313
|
try {
|
|
306
314
|
const [openapi, metaRows] = await Promise.all([
|
|
307
315
|
fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
|
|
308
|
-
|
|
316
|
+
fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
|
|
309
317
|
]);
|
|
310
318
|
return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
|
|
311
319
|
} catch (err) {
|
|
@@ -315,7 +323,7 @@ async function resolveTableSchemas(deps) {
|
|
|
315
323
|
if (manifest) {
|
|
316
324
|
warnings.push({
|
|
317
325
|
rung: "manifest",
|
|
318
|
-
reason: "runtime introspection failed; serving tools from the bundled
|
|
326
|
+
reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
|
|
319
327
|
});
|
|
320
328
|
return { tables: manifest.tables, mode: "manifest", warnings };
|
|
321
329
|
}
|
|
@@ -602,9 +610,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
|
|
|
602
610
|
function registerDiagnosticsResource(server, warnings) {
|
|
603
611
|
if (warnings.length === 0) return;
|
|
604
612
|
server.registerResource(
|
|
605
|
-
"
|
|
606
|
-
"
|
|
607
|
-
{ title: "
|
|
613
|
+
"exposify-diagnostics",
|
|
614
|
+
"exposify://diagnostics",
|
|
615
|
+
{ title: "exposify-supabase diagnostics", mimeType: "application/json" },
|
|
608
616
|
async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
|
|
609
617
|
);
|
|
610
618
|
}
|
|
@@ -637,6 +645,7 @@ function aiReady(options = {}) {
|
|
|
637
645
|
const env = denoEnv();
|
|
638
646
|
const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
|
|
639
647
|
const anonKey = options.anonKey ?? env?.get("SUPABASE_ANON_KEY") ?? "";
|
|
648
|
+
const serviceRoleKey = options.serviceRoleKey ?? env?.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
|
640
649
|
let kbPromise = null;
|
|
641
650
|
const getKb = () => {
|
|
642
651
|
kbPromise ??= (async () => {
|
|
@@ -654,12 +663,12 @@ function aiReady(options = {}) {
|
|
|
654
663
|
const getLadder = (postgrestUrl) => {
|
|
655
664
|
ladderPromise ??= resolveTableSchemas({
|
|
656
665
|
postgrestUrl,
|
|
657
|
-
headers: buildPostgrestHeaders(new Headers(),
|
|
666
|
+
headers: buildPostgrestHeaders(new Headers(), serviceRoleKey),
|
|
658
667
|
fetchImpl,
|
|
659
668
|
manifest: options.manifest
|
|
660
669
|
}).then((ladder) => {
|
|
661
670
|
for (const w of ladder.warnings) {
|
|
662
|
-
console.error(`[
|
|
671
|
+
console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
|
|
663
672
|
}
|
|
664
673
|
return ladder;
|
|
665
674
|
});
|
|
@@ -687,7 +696,7 @@ function aiReady(options = {}) {
|
|
|
687
696
|
return { status: res.status, bodyText: await res.text() };
|
|
688
697
|
};
|
|
689
698
|
return buildSupabaseMcpServer({
|
|
690
|
-
appName: "
|
|
699
|
+
appName: "exposify-supabase",
|
|
691
700
|
appVersion: "0.0.0",
|
|
692
701
|
tables: ladder.tables,
|
|
693
702
|
cfg,
|
|
@@ -706,11 +715,11 @@ function aiReady(options = {}) {
|
|
|
706
715
|
// Annotate the CommonJS export names for ESM import in node:
|
|
707
716
|
0 && (module.exports = {
|
|
708
717
|
MANIFEST_VERSION,
|
|
709
|
-
|
|
718
|
+
MissingExposifyMetaError,
|
|
710
719
|
aiReady,
|
|
711
720
|
buildManifest,
|
|
712
721
|
columnsFromOpenApi,
|
|
713
|
-
|
|
722
|
+
fetchExposifyMeta,
|
|
714
723
|
fetchOpenApi,
|
|
715
724
|
introspectTables,
|
|
716
725
|
loadManifest,
|
package/dist/index.d.cts
CHANGED
|
@@ -7,18 +7,18 @@ interface ColumnSchema {
|
|
|
7
7
|
/** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
|
|
8
8
|
format?: string;
|
|
9
9
|
required: boolean;
|
|
10
|
-
/** From `
|
|
10
|
+
/** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
|
|
11
11
|
description?: string;
|
|
12
12
|
isPrimaryKey?: boolean;
|
|
13
13
|
}
|
|
14
14
|
interface TableSchema {
|
|
15
15
|
name: string;
|
|
16
16
|
rlsEnabled: boolean;
|
|
17
|
-
/** From `
|
|
17
|
+
/** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
|
|
18
18
|
description?: string;
|
|
19
19
|
columns: ColumnSchema[];
|
|
20
20
|
}
|
|
21
|
-
/** One row of `
|
|
21
|
+
/** One row of `exposify_meta()` -- the migration returns one row per column. */
|
|
22
22
|
interface MetaRow {
|
|
23
23
|
table_name: string;
|
|
24
24
|
rls_enabled: boolean;
|
|
@@ -60,9 +60,21 @@ interface AgentifySupabaseOptions {
|
|
|
60
60
|
/** Overrides for local/CI use -- production reads these from the Deno env automatically. */
|
|
61
61
|
supabaseUrl?: string;
|
|
62
62
|
anonKey?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Used ONLY for schema introspection (fetching PostgREST's OpenAPI document
|
|
65
|
+
* + calling `exposify_meta()`) -- never for per-request data access, which
|
|
66
|
+
* always uses the caller's forwarded JWT (or the anon key as its fallback).
|
|
67
|
+
* Required because Supabase's gateway rejects the OpenAPI-discovery route
|
|
68
|
+
* for anon/publishable-tier keys outright (`Invalid API key` / `Only the
|
|
69
|
+
* service_role API key can be used for this endpoint`), regardless of key
|
|
70
|
+
* format -- discovered via a real deployment, not documented anywhere at
|
|
71
|
+
* design time. Auto-injected as `SUPABASE_SERVICE_ROLE_KEY` in every
|
|
72
|
+
* deployed edge function; this option exists for local dev/testing only.
|
|
73
|
+
*/
|
|
74
|
+
serviceRoleKey?: string;
|
|
63
75
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
64
76
|
fetchImpl?: typeof fetch;
|
|
65
|
-
/** Bundled fallback manifest (from `
|
|
77
|
+
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
|
66
78
|
manifest?: unknown;
|
|
67
79
|
rateLimit?: number;
|
|
68
80
|
rateWindowMs?: number;
|
|
@@ -98,7 +110,7 @@ interface PostgrestColumnProperty {
|
|
|
98
110
|
/**
|
|
99
111
|
* Extracts column schemas for one table from a PostgREST OpenAPI document.
|
|
100
112
|
* Returns `[]` for a table PostgREST never exposed (not in `definitions` --
|
|
101
|
-
* e.g. it exists at the catalog level per `
|
|
113
|
+
* e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
|
|
102
114
|
* role lacks USAGE/SELECT grants on it), which the caller treats the same as
|
|
103
115
|
* "RLS-less": never shown to the agent.
|
|
104
116
|
*/
|
|
@@ -113,27 +125,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
|
|
|
113
125
|
declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
|
|
114
126
|
|
|
115
127
|
declare const MANIFEST_VERSION = 1;
|
|
116
|
-
interface
|
|
128
|
+
interface ExposifyManifest {
|
|
117
129
|
version: typeof MANIFEST_VERSION;
|
|
118
130
|
generatedAt: string;
|
|
119
131
|
tables: TableSchema[];
|
|
120
132
|
}
|
|
121
|
-
/** Built by `npx
|
|
122
|
-
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string):
|
|
133
|
+
/** Built by `npx exposify-supabase generate` against a local/linked project. */
|
|
134
|
+
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
|
|
123
135
|
/**
|
|
124
136
|
* Defensive load: a manifest is bundled JSON shipped alongside the function
|
|
125
137
|
* and can go stale relative to the deployed schema, or simply be malformed
|
|
126
138
|
* (hand-edited, wrong version). Returns `null` -- never throws -- so the
|
|
127
139
|
* degradation ladder can fall through to the next rung instead of crashing.
|
|
128
140
|
*/
|
|
129
|
-
declare function loadManifest(raw: unknown):
|
|
141
|
+
declare function loadManifest(raw: unknown): ExposifyManifest | null;
|
|
130
142
|
|
|
131
143
|
/**
|
|
132
|
-
* Merges PostgREST's OpenAPI document (column types) with `
|
|
144
|
+
* Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
|
|
133
145
|
* rows (RLS status + comments) into the DB tool surface.
|
|
134
146
|
*
|
|
135
147
|
* DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
|
|
136
|
-
* appears here if `
|
|
148
|
+
* appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
|
|
137
149
|
* PostgREST actually exposes it in its OpenAPI `definitions` (the role
|
|
138
150
|
* calling this has grants on it) -- a table failing either check never
|
|
139
151
|
* appears, silently, exactly like a table PostgREST doesn't know about at
|
|
@@ -142,25 +154,34 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
|
|
|
142
154
|
*/
|
|
143
155
|
declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
|
|
144
156
|
|
|
145
|
-
declare class
|
|
157
|
+
declare class MissingExposifyMetaError extends Error {
|
|
146
158
|
constructor(cause: string);
|
|
147
159
|
}
|
|
148
160
|
/**
|
|
149
|
-
* Calls the `
|
|
150
|
-
* endpoint. Requires the migration from `sql/
|
|
161
|
+
* Calls the `exposify_meta()` Postgres function via PostgREST's RPC
|
|
162
|
+
* endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
|
|
151
163
|
* applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
|
|
152
164
|
* found in schema cache) both mean "migration missing" and are translated
|
|
153
165
|
* into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
|
|
154
|
-
* produces a self-diagnosing message naming the exact fix"
|
|
166
|
+
* produces a self-diagnosing message naming the exact fix" -- function
|
|
167
|
+
* renamed to `exposify_meta()` post-spec, see the decisions journal).
|
|
155
168
|
*/
|
|
156
|
-
declare function
|
|
169
|
+
declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
|
|
157
170
|
|
|
158
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* PostgREST serves its OpenAPI document at the API root when asked for it
|
|
173
|
+
* explicitly. This is the one call in the whole SDK most likely to fail for
|
|
174
|
+
* a specific, fixable reason (the wrong key tier -- see decisions journal
|
|
175
|
+
* §9), so unlike a generic fetch wrapper this one reads the body on failure
|
|
176
|
+
* and turns that one known failure mode into a self-diagnosing message
|
|
177
|
+
* naming the exact fix, the same pattern `meta.ts` already uses for a
|
|
178
|
+
* missing `exposify_meta()` migration.
|
|
179
|
+
*/
|
|
159
180
|
declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
|
|
160
181
|
|
|
161
182
|
/**
|
|
162
183
|
* Builds the `Deno.serve`-compatible MCP handler. Everything expensive
|
|
163
|
-
* (OpenAPI fetch, `
|
|
184
|
+
* (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
|
|
164
185
|
* lazily on first request and is cached in-memory for the life of the edge
|
|
165
186
|
* instance -- "cold start" in the spec's sense, not literally module load,
|
|
166
187
|
* since env vars and the incoming JWT aren't available before the first
|
|
@@ -168,4 +189,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
|
|
|
168
189
|
*/
|
|
169
190
|
declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
|
|
170
191
|
|
|
171
|
-
export { type
|
|
192
|
+
export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
|
package/dist/index.d.ts
CHANGED
|
@@ -7,18 +7,18 @@ interface ColumnSchema {
|
|
|
7
7
|
/** OpenAPI `format`, e.g. "uuid", "timestamp with time zone", "bigint". */
|
|
8
8
|
format?: string;
|
|
9
9
|
required: boolean;
|
|
10
|
-
/** From `
|
|
10
|
+
/** From `exposify_meta()`'s `column_comment` (a `COMMENT ON COLUMN`), not OpenAPI. */
|
|
11
11
|
description?: string;
|
|
12
12
|
isPrimaryKey?: boolean;
|
|
13
13
|
}
|
|
14
14
|
interface TableSchema {
|
|
15
15
|
name: string;
|
|
16
16
|
rlsEnabled: boolean;
|
|
17
|
-
/** From `
|
|
17
|
+
/** From `exposify_meta()`'s `table_comment` (a `COMMENT ON TABLE`). */
|
|
18
18
|
description?: string;
|
|
19
19
|
columns: ColumnSchema[];
|
|
20
20
|
}
|
|
21
|
-
/** One row of `
|
|
21
|
+
/** One row of `exposify_meta()` -- the migration returns one row per column. */
|
|
22
22
|
interface MetaRow {
|
|
23
23
|
table_name: string;
|
|
24
24
|
rls_enabled: boolean;
|
|
@@ -60,9 +60,21 @@ interface AgentifySupabaseOptions {
|
|
|
60
60
|
/** Overrides for local/CI use -- production reads these from the Deno env automatically. */
|
|
61
61
|
supabaseUrl?: string;
|
|
62
62
|
anonKey?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Used ONLY for schema introspection (fetching PostgREST's OpenAPI document
|
|
65
|
+
* + calling `exposify_meta()`) -- never for per-request data access, which
|
|
66
|
+
* always uses the caller's forwarded JWT (or the anon key as its fallback).
|
|
67
|
+
* Required because Supabase's gateway rejects the OpenAPI-discovery route
|
|
68
|
+
* for anon/publishable-tier keys outright (`Invalid API key` / `Only the
|
|
69
|
+
* service_role API key can be used for this endpoint`), regardless of key
|
|
70
|
+
* format -- discovered via a real deployment, not documented anywhere at
|
|
71
|
+
* design time. Auto-injected as `SUPABASE_SERVICE_ROLE_KEY` in every
|
|
72
|
+
* deployed edge function; this option exists for local dev/testing only.
|
|
73
|
+
*/
|
|
74
|
+
serviceRoleKey?: string;
|
|
63
75
|
/** Injectable for tests; defaults to global `fetch`. */
|
|
64
76
|
fetchImpl?: typeof fetch;
|
|
65
|
-
/** Bundled fallback manifest (from `
|
|
77
|
+
/** Bundled fallback manifest (from `exposify-supabase generate`), used when runtime introspection fails. */
|
|
66
78
|
manifest?: unknown;
|
|
67
79
|
rateLimit?: number;
|
|
68
80
|
rateWindowMs?: number;
|
|
@@ -98,7 +110,7 @@ interface PostgrestColumnProperty {
|
|
|
98
110
|
/**
|
|
99
111
|
* Extracts column schemas for one table from a PostgREST OpenAPI document.
|
|
100
112
|
* Returns `[]` for a table PostgREST never exposed (not in `definitions` --
|
|
101
|
-
* e.g. it exists at the catalog level per `
|
|
113
|
+
* e.g. it exists at the catalog level per `exposify_meta()` but PostgREST's
|
|
102
114
|
* role lacks USAGE/SELECT grants on it), which the caller treats the same as
|
|
103
115
|
* "RLS-less": never shown to the agent.
|
|
104
116
|
*/
|
|
@@ -113,27 +125,27 @@ declare function columnsFromOpenApi(doc: PostgrestOpenApiDoc, table: string): Co
|
|
|
113
125
|
declare function rpcArgSchema(doc: PostgrestOpenApiDoc, fn: string): JsonSchemaObject | null;
|
|
114
126
|
|
|
115
127
|
declare const MANIFEST_VERSION = 1;
|
|
116
|
-
interface
|
|
128
|
+
interface ExposifyManifest {
|
|
117
129
|
version: typeof MANIFEST_VERSION;
|
|
118
130
|
generatedAt: string;
|
|
119
131
|
tables: TableSchema[];
|
|
120
132
|
}
|
|
121
|
-
/** Built by `npx
|
|
122
|
-
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string):
|
|
133
|
+
/** Built by `npx exposify-supabase generate` against a local/linked project. */
|
|
134
|
+
declare function buildManifest(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[], generatedAt: string): ExposifyManifest;
|
|
123
135
|
/**
|
|
124
136
|
* Defensive load: a manifest is bundled JSON shipped alongside the function
|
|
125
137
|
* and can go stale relative to the deployed schema, or simply be malformed
|
|
126
138
|
* (hand-edited, wrong version). Returns `null` -- never throws -- so the
|
|
127
139
|
* degradation ladder can fall through to the next rung instead of crashing.
|
|
128
140
|
*/
|
|
129
|
-
declare function loadManifest(raw: unknown):
|
|
141
|
+
declare function loadManifest(raw: unknown): ExposifyManifest | null;
|
|
130
142
|
|
|
131
143
|
/**
|
|
132
|
-
* Merges PostgREST's OpenAPI document (column types) with `
|
|
144
|
+
* Merges PostgREST's OpenAPI document (column types) with `exposify_meta()`
|
|
133
145
|
* rows (RLS status + comments) into the DB tool surface.
|
|
134
146
|
*
|
|
135
147
|
* DB exposure policy (spec, locked): RLS-gated, reads auto. A table only
|
|
136
|
-
* appears here if `
|
|
148
|
+
* appears here if `exposify_meta()` reports `rls_enabled: true` for it AND
|
|
137
149
|
* PostgREST actually exposes it in its OpenAPI `definitions` (the role
|
|
138
150
|
* calling this has grants on it) -- a table failing either check never
|
|
139
151
|
* appears, silently, exactly like a table PostgREST doesn't know about at
|
|
@@ -142,25 +154,34 @@ declare function loadManifest(raw: unknown): AgentifyManifest | null;
|
|
|
142
154
|
*/
|
|
143
155
|
declare function introspectTables(openapi: PostgrestOpenApiDoc, metaRows: MetaRow[]): TableSchema[];
|
|
144
156
|
|
|
145
|
-
declare class
|
|
157
|
+
declare class MissingExposifyMetaError extends Error {
|
|
146
158
|
constructor(cause: string);
|
|
147
159
|
}
|
|
148
160
|
/**
|
|
149
|
-
* Calls the `
|
|
150
|
-
* endpoint. Requires the migration from `sql/
|
|
161
|
+
* Calls the `exposify_meta()` Postgres function via PostgREST's RPC
|
|
162
|
+
* endpoint. Requires the migration from `sql/exposify_meta.sql` to have been
|
|
151
163
|
* applied; a 404 (unknown route) or PostgREST's `PGRST202` (function not
|
|
152
164
|
* found in schema cache) both mean "migration missing" and are translated
|
|
153
165
|
* into a self-diagnosing error (spec: "a missing `agentify_meta()` migration
|
|
154
|
-
* produces a self-diagnosing message naming the exact fix"
|
|
166
|
+
* produces a self-diagnosing message naming the exact fix" -- function
|
|
167
|
+
* renamed to `exposify_meta()` post-spec, see the decisions journal).
|
|
155
168
|
*/
|
|
156
|
-
declare function
|
|
169
|
+
declare function fetchExposifyMeta(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<MetaRow[]>;
|
|
157
170
|
|
|
158
|
-
/**
|
|
171
|
+
/**
|
|
172
|
+
* PostgREST serves its OpenAPI document at the API root when asked for it
|
|
173
|
+
* explicitly. This is the one call in the whole SDK most likely to fail for
|
|
174
|
+
* a specific, fixable reason (the wrong key tier -- see decisions journal
|
|
175
|
+
* §9), so unlike a generic fetch wrapper this one reads the body on failure
|
|
176
|
+
* and turns that one known failure mode into a self-diagnosing message
|
|
177
|
+
* naming the exact fix, the same pattern `meta.ts` already uses for a
|
|
178
|
+
* missing `exposify_meta()` migration.
|
|
179
|
+
*/
|
|
159
180
|
declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl: typeof fetch): Promise<PostgrestOpenApiDoc>;
|
|
160
181
|
|
|
161
182
|
/**
|
|
162
183
|
* Builds the `Deno.serve`-compatible MCP handler. Everything expensive
|
|
163
|
-
* (OpenAPI fetch, `
|
|
184
|
+
* (OpenAPI fetch, `exposify_meta()` call, doc loading/chunking) happens
|
|
164
185
|
* lazily on first request and is cached in-memory for the life of the edge
|
|
165
186
|
* instance -- "cold start" in the spec's sense, not literally module load,
|
|
166
187
|
* since env vars and the incoming JWT aren't available before the first
|
|
@@ -168,4 +189,4 @@ declare function fetchOpenApi(postgrestUrl: string, headers: Headers, fetchImpl:
|
|
|
168
189
|
*/
|
|
169
190
|
declare function aiReady(options?: AgentifySupabaseOptions): WebMcpHandler;
|
|
170
191
|
|
|
171
|
-
export { type
|
|
192
|
+
export { type AgentifySupabaseOptions, type ExposifyManifest, MANIFEST_VERSION, MissingExposifyMetaError, type PostgrestOpenApiDoc, aiReady, buildManifest, columnsFromOpenApi, fetchExposifyMeta, fetchOpenApi, introspectTables, loadManifest, rpcArgSchema };
|
package/dist/index.js
CHANGED
|
@@ -70,7 +70,7 @@ function warnOnce(message) {
|
|
|
70
70
|
if (warned.has(message)) return;
|
|
71
71
|
warned.add(message);
|
|
72
72
|
try {
|
|
73
|
-
console.warn(`[
|
|
73
|
+
console.warn(`[exposify-supabase] ${message}`);
|
|
74
74
|
} catch {
|
|
75
75
|
}
|
|
76
76
|
}
|
|
@@ -96,6 +96,7 @@ function buildPostgrestHeaders(incoming, anonKey) {
|
|
|
96
96
|
if (value) headers.set(name, value);
|
|
97
97
|
}
|
|
98
98
|
if (!headers.has("apikey")) headers.set("apikey", anonKey);
|
|
99
|
+
if (!headers.has("authorization")) headers.set("authorization", `Bearer ${anonKey}`);
|
|
99
100
|
return headers;
|
|
100
101
|
}
|
|
101
102
|
function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
@@ -128,42 +129,49 @@ function createPostgrestClient(postgrestUrl, headers, fetchImpl) {
|
|
|
128
129
|
}
|
|
129
130
|
|
|
130
131
|
// src/fetch-openapi.ts
|
|
132
|
+
var WRONG_KEY_TIER_RE = /service_role|secret api key/i;
|
|
131
133
|
async function fetchOpenApi(postgrestUrl, headers, fetchImpl) {
|
|
132
134
|
const res = await fetchImpl(postgrestUrl, {
|
|
133
135
|
headers: { ...Object.fromEntries(headers), accept: "application/openapi+json" }
|
|
134
136
|
});
|
|
135
137
|
if (!res.ok) {
|
|
136
|
-
|
|
138
|
+
const body = await res.text().catch(() => "");
|
|
139
|
+
if (res.status === 401 && WRONG_KEY_TIER_RE.test(body)) {
|
|
140
|
+
throw new Error(
|
|
141
|
+
`PostgREST OpenAPI fetch failed: HTTP 401 -- Supabase rejected this request because it wasn't made with the service role key (response: "${body.trim()}"). Schema introspection always needs SUPABASE_SERVICE_ROLE_KEY, never the anon key -- see this package's README, "Why introspection needs the service role key". In a deployed function this is auto-injected automatically; for local dev/testing pass { serviceRoleKey } explicitly (or --key for the CLI).`
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
throw new Error(`PostgREST OpenAPI fetch failed: HTTP ${res.status}${body ? `: ${body}` : ""}`);
|
|
137
145
|
}
|
|
138
146
|
return await res.json();
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
// src/meta.ts
|
|
142
|
-
var
|
|
150
|
+
var MissingExposifyMetaError = class extends Error {
|
|
143
151
|
constructor(cause) {
|
|
144
152
|
super(cause);
|
|
145
|
-
this.name = "
|
|
153
|
+
this.name = "MissingExposifyMetaError";
|
|
146
154
|
}
|
|
147
155
|
};
|
|
148
|
-
async function
|
|
149
|
-
const res = await fetchImpl(`${postgrestUrl}/rpc/
|
|
156
|
+
async function fetchExposifyMeta(postgrestUrl, headers, fetchImpl) {
|
|
157
|
+
const res = await fetchImpl(`${postgrestUrl}/rpc/exposify_meta`, {
|
|
150
158
|
method: "POST",
|
|
151
159
|
headers: { ...Object.fromEntries(headers), "content-type": "application/json" },
|
|
152
160
|
body: "{}"
|
|
153
161
|
});
|
|
154
162
|
if (res.status === 404) {
|
|
155
|
-
throw new
|
|
156
|
-
"
|
|
163
|
+
throw new MissingExposifyMetaError(
|
|
164
|
+
"exposify_meta() is not reachable at /rpc/exposify_meta (404). Run `npx exposify-supabase init` (or apply sql/exposify_meta.sql directly) and `supabase db push`, then redeploy the function."
|
|
157
165
|
);
|
|
158
166
|
}
|
|
159
167
|
if (!res.ok) {
|
|
160
168
|
const body = await res.text().catch(() => "");
|
|
161
169
|
if (/PGRST202|function .* does not exist/i.test(body)) {
|
|
162
|
-
throw new
|
|
163
|
-
"
|
|
170
|
+
throw new MissingExposifyMetaError(
|
|
171
|
+
"exposify_meta() is missing from the database (PostgREST error PGRST202). Apply the migration in sql/exposify_meta.sql via `supabase db push`, then redeploy the function."
|
|
164
172
|
);
|
|
165
173
|
}
|
|
166
|
-
throw new Error(`
|
|
174
|
+
throw new Error(`exposify_meta() call failed: HTTP ${res.status} ${body}`.trim());
|
|
167
175
|
}
|
|
168
176
|
return await res.json();
|
|
169
177
|
}
|
|
@@ -194,7 +202,7 @@ function columnsFromOpenApi(doc, table) {
|
|
|
194
202
|
// ON COLUMN` into `description`, then appends its own "Note:\nThis is a
|
|
195
203
|
// Primary/Foreign Key...<pk/|fk/>" boilerplate) -- strip that boilerplate
|
|
196
204
|
// so a column with no real comment ends up `undefined`, not the
|
|
197
|
-
// boilerplate text.
|
|
205
|
+
// boilerplate text. exposify_meta()'s copy wins when both exist anyway
|
|
198
206
|
// -- see introspect.ts's merge.
|
|
199
207
|
description: prop.description?.replace(/\n*Note:[\s\S]*$/, "").trim() || void 0
|
|
200
208
|
}));
|
|
@@ -229,7 +237,7 @@ function introspectTables(openapi, metaRows) {
|
|
|
229
237
|
if (!fromOpenApi) continue;
|
|
230
238
|
columns.push({
|
|
231
239
|
...fromOpenApi,
|
|
232
|
-
//
|
|
240
|
+
// exposify_meta()'s comment wins over OpenAPI's mirrored copy (spec:
|
|
233
241
|
// "Column comments → param descriptions" sourced from the migration).
|
|
234
242
|
description: row.column_comment ?? fromOpenApi.description
|
|
235
243
|
});
|
|
@@ -272,7 +280,7 @@ async function resolveTableSchemas(deps) {
|
|
|
272
280
|
try {
|
|
273
281
|
const [openapi, metaRows] = await Promise.all([
|
|
274
282
|
fetchOpenApi(deps.postgrestUrl, deps.headers, deps.fetchImpl),
|
|
275
|
-
|
|
283
|
+
fetchExposifyMeta(deps.postgrestUrl, deps.headers, deps.fetchImpl)
|
|
276
284
|
]);
|
|
277
285
|
return { tables: introspectTables(openapi, metaRows), mode: "runtime", warnings, openapi };
|
|
278
286
|
} catch (err) {
|
|
@@ -282,7 +290,7 @@ async function resolveTableSchemas(deps) {
|
|
|
282
290
|
if (manifest) {
|
|
283
291
|
warnings.push({
|
|
284
292
|
rung: "manifest",
|
|
285
|
-
reason: "runtime introspection failed; serving tools from the bundled
|
|
293
|
+
reason: "runtime introspection failed; serving tools from the bundled exposify.manifest.json instead"
|
|
286
294
|
});
|
|
287
295
|
return { tables: manifest.tables, mode: "manifest", warnings };
|
|
288
296
|
}
|
|
@@ -572,9 +580,9 @@ ${h.chunk.text}`).join("\n\n---\n\n") : "No matching documentation found.";
|
|
|
572
580
|
function registerDiagnosticsResource(server, warnings) {
|
|
573
581
|
if (warnings.length === 0) return;
|
|
574
582
|
server.registerResource(
|
|
575
|
-
"
|
|
576
|
-
"
|
|
577
|
-
{ title: "
|
|
583
|
+
"exposify-diagnostics",
|
|
584
|
+
"exposify://diagnostics",
|
|
585
|
+
{ title: "exposify-supabase diagnostics", mimeType: "application/json" },
|
|
578
586
|
async (uri) => ({ contents: [{ uri: uri.href, text: JSON.stringify(warnings, null, 2) }] })
|
|
579
587
|
);
|
|
580
588
|
}
|
|
@@ -607,6 +615,7 @@ function aiReady(options = {}) {
|
|
|
607
615
|
const env = denoEnv();
|
|
608
616
|
const supabaseUrl = options.supabaseUrl ?? env?.get("SUPABASE_URL");
|
|
609
617
|
const anonKey = options.anonKey ?? env?.get("SUPABASE_ANON_KEY") ?? "";
|
|
618
|
+
const serviceRoleKey = options.serviceRoleKey ?? env?.get("SUPABASE_SERVICE_ROLE_KEY") ?? "";
|
|
610
619
|
let kbPromise = null;
|
|
611
620
|
const getKb = () => {
|
|
612
621
|
kbPromise ??= (async () => {
|
|
@@ -624,12 +633,12 @@ function aiReady(options = {}) {
|
|
|
624
633
|
const getLadder = (postgrestUrl) => {
|
|
625
634
|
ladderPromise ??= resolveTableSchemas({
|
|
626
635
|
postgrestUrl,
|
|
627
|
-
headers: buildPostgrestHeaders(new Headers(),
|
|
636
|
+
headers: buildPostgrestHeaders(new Headers(), serviceRoleKey),
|
|
628
637
|
fetchImpl,
|
|
629
638
|
manifest: options.manifest
|
|
630
639
|
}).then((ladder) => {
|
|
631
640
|
for (const w of ladder.warnings) {
|
|
632
|
-
console.error(`[
|
|
641
|
+
console.error(`[exposify-supabase] degradation ladder: ${w.rung}: ${w.reason}`);
|
|
633
642
|
}
|
|
634
643
|
return ladder;
|
|
635
644
|
});
|
|
@@ -657,7 +666,7 @@ function aiReady(options = {}) {
|
|
|
657
666
|
return { status: res.status, bodyText: await res.text() };
|
|
658
667
|
};
|
|
659
668
|
return buildSupabaseMcpServer({
|
|
660
|
-
appName: "
|
|
669
|
+
appName: "exposify-supabase",
|
|
661
670
|
appVersion: "0.0.0",
|
|
662
671
|
tables: ladder.tables,
|
|
663
672
|
cfg,
|
|
@@ -675,11 +684,11 @@ function aiReady(options = {}) {
|
|
|
675
684
|
}
|
|
676
685
|
export {
|
|
677
686
|
MANIFEST_VERSION,
|
|
678
|
-
|
|
687
|
+
MissingExposifyMetaError,
|
|
679
688
|
aiReady,
|
|
680
689
|
buildManifest,
|
|
681
690
|
columnsFromOpenApi,
|
|
682
|
-
|
|
691
|
+
fetchExposifyMeta,
|
|
683
692
|
fetchOpenApi,
|
|
684
693
|
introspectTables,
|
|
685
694
|
loadManifest,
|