@supacloud/lite 0.9.2 → 0.11.0

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/cli.js CHANGED
@@ -5,11 +5,11 @@ var __require = import.meta.require;
5
5
  // src/cli.ts
6
6
  import { existsSync as existsSync4 } from "fs";
7
7
  import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
8
- import { dirname as dirname7, join as join10, resolve as resolve5 } from "path";
8
+ import { dirname as dirname8, join as join10, resolve as resolve6 } from "path";
9
9
  // package.json
10
10
  var package_default = {
11
11
  name: "@supacloud/lite",
12
- version: "0.9.2",
12
+ version: "0.11.0",
13
13
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
14
14
  type: "module",
15
15
  license: "Apache-2.0",
@@ -59,11 +59,13 @@ var package_default = {
59
59
  },
60
60
  dependencies: {
61
61
  "@electric-sql/pglite": "0.5.8",
62
+ "@supacloud/db": "^0.1.0",
62
63
  tar: "^7.5.22"
63
64
  },
64
65
  devDependencies: {
65
66
  "@supabase/supabase-js": "^2.112.4",
66
67
  "@types/bun": "^1.4.0",
68
+ elysia: "^1.4.30",
67
69
  typescript: "^7.0.2"
68
70
  },
69
71
  engines: {
@@ -2524,8 +2526,99 @@ function resetCapturedHandler() {
2524
2526
  captured.handler = undefined;
2525
2527
  }
2526
2528
 
2527
- // src/runtime/functions/pgredis.ts
2529
+ // src/runtime/functions/edge-runtime-shim.ts
2528
2530
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2531
+ var scopeStore = new AsyncLocalStorage2;
2532
+ var NOT_ENABLED_MESSAGE = "EdgeRuntime.waitUntil is not enabled by the Function capability policy";
2533
+ var installed;
2534
+ function installEdgeRuntimeShim() {
2535
+ if (installed)
2536
+ return;
2537
+ const runtime = {
2538
+ waitUntil(promise) {
2539
+ const scope = scopeStore.getStore();
2540
+ if (scope && !scope.allowed) {
2541
+ throw new Error(NOT_ENABLED_MESSAGE);
2542
+ }
2543
+ const task = Promise.resolve(promise).catch((error) => {
2544
+ console.error("[EdgeRuntime.waitUntil] background task failed", error);
2545
+ });
2546
+ scope?.tasks.push(task);
2547
+ }
2548
+ };
2549
+ installed = runtime;
2550
+ globalThis.EdgeRuntime = runtime;
2551
+ }
2552
+ function runWithBackgroundTasks(options, fn) {
2553
+ installEdgeRuntimeShim();
2554
+ const scope = { allowed: options.allowed, tasks: [] };
2555
+ return scopeStore.run(scope, async () => {
2556
+ try {
2557
+ return await fn();
2558
+ } finally {
2559
+ flushBackgroundTasks(scope.tasks, options.timeoutMs);
2560
+ }
2561
+ });
2562
+ }
2563
+ async function flushBackgroundTasks(tasks, timeoutMs) {
2564
+ if (tasks.length === 0)
2565
+ return;
2566
+ const drain = (async () => {
2567
+ while (tasks.length > 0) {
2568
+ const batch = tasks.splice(0);
2569
+ await Promise.allSettled(batch);
2570
+ }
2571
+ })();
2572
+ if (timeoutMs === undefined) {
2573
+ await drain;
2574
+ return;
2575
+ }
2576
+ const timedOut = await Promise.race([
2577
+ drain.then(() => false),
2578
+ new Promise((resolve) => setTimeout(() => resolve(true), timeoutMs))
2579
+ ]);
2580
+ if (timedOut) {
2581
+ console.warn(`[EdgeRuntime.waitUntil] background tasks did not settle within ${timeoutMs}ms; no longer waiting`);
2582
+ }
2583
+ }
2584
+
2585
+ // src/runtime/functions/fetch-policy.ts
2586
+ import { AsyncLocalStorage as AsyncLocalStorage3 } from "async_hooks";
2587
+ var policyStore = new AsyncLocalStorage3;
2588
+ var LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "::1"]);
2589
+ var installed2 = false;
2590
+ function installFetchPolicyShim() {
2591
+ if (installed2)
2592
+ return;
2593
+ installed2 = true;
2594
+ const original = globalThis.fetch;
2595
+ globalThis.fetch = async (input, init) => {
2596
+ const allowed = policyStore.getStore();
2597
+ if (!allowed)
2598
+ return original(input, init);
2599
+ const host = hostOf(input);
2600
+ if (host !== undefined && !LOOPBACK_HOSTS.has(host) && !allowed.has(host)) {
2601
+ throw new Error(`outbound host not allowed: ${host}`);
2602
+ }
2603
+ return original(input, init);
2604
+ };
2605
+ }
2606
+ function runWithFetchPolicy(allowedHosts, fn) {
2607
+ installFetchPolicyShim();
2608
+ return policyStore.run(new Set(allowedHosts), fn);
2609
+ }
2610
+ function hostOf(input) {
2611
+ try {
2612
+ const raw = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
2613
+ const host = new URL(raw).hostname;
2614
+ return host.startsWith("[") && host.endsWith("]") ? host.slice(1, -1) : host;
2615
+ } catch {
2616
+ return;
2617
+ }
2618
+ }
2619
+
2620
+ // src/runtime/functions/pgredis.ts
2621
+ import { AsyncLocalStorage as AsyncLocalStorage4 } from "async_hooks";
2529
2622
  var CACHE_NAMESPACE = "supacloud-edge-runtime";
2530
2623
  var CACHE_TABLE = "public.supacloud_pgredis_kv";
2531
2624
  var MAX_KEY_CHARACTERS = 512;
@@ -2618,7 +2711,7 @@ class PgredisCache {
2618
2711
  });
2619
2712
  }
2620
2713
  }
2621
- var cacheContexts = new AsyncLocalStorage2;
2714
+ var cacheContexts = new AsyncLocalStorage4;
2622
2715
  var cacheFacade = Object.freeze({
2623
2716
  get: async (key) => activeCache().get(key),
2624
2717
  set: async (key, cacheValue, ttlMs) => activeCache().set(key, cacheValue, ttlMs),
@@ -2692,6 +2785,51 @@ async function upsertWithoutTtl(query, key, serializedValue) {
2692
2785
  }
2693
2786
 
2694
2787
  // src/runtime/functions/handler.ts
2788
+ var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
2789
+ var UNSAFE_VERIFIED_JWT_SUBJECT = /[\u0000-\u001F\u007F-\u009F\u0100-\u{10FFFF}]/u;
2790
+ function verifiedSubject(value) {
2791
+ if (typeof value !== "string" || value.length === 0 || value.length > 1024)
2792
+ return null;
2793
+ if (value.trim() !== value || UNSAFE_VERIFIED_JWT_SUBJECT.test(value))
2794
+ return null;
2795
+ return value;
2796
+ }
2797
+ function withVerifiedJwtSubject(request, ctx) {
2798
+ const trustedRequest = request.clone();
2799
+ trustedRequest.headers.delete(VERIFIED_JWT_SUBJECT_HEADER);
2800
+ const subject = verifiedSubject(ctx.claims?.sub);
2801
+ if (subject !== null)
2802
+ trustedRequest.headers.set(VERIFIED_JWT_SUBJECT_HEADER, subject);
2803
+ return trustedRequest;
2804
+ }
2805
+ function isLoadedFunction(value) {
2806
+ return typeof value === "object" && value !== null && "handler" in value;
2807
+ }
2808
+ function normalizeEntry(value) {
2809
+ return isLoadedFunction(value) ? value : { handler: value };
2810
+ }
2811
+ function isFrameworkRouterHandler(handler) {
2812
+ if (!handler || typeof handler !== "object")
2813
+ return false;
2814
+ const candidate = handler;
2815
+ if (candidate.__supacloud?.routeAware === true)
2816
+ return true;
2817
+ return Array.isArray(candidate.routes) && (typeof candidate.handle === "function" || typeof candidate.fetch === "function");
2818
+ }
2819
+ function toFunctionLocalUrl(requestUrl) {
2820
+ const url = new URL(requestUrl);
2821
+ const publicRoute = url.pathname.match(/^\/functions\/v1\/[^/]+(\/.*)?$/);
2822
+ if (publicRoute) {
2823
+ url.pathname = publicRoute[1] || "/";
2824
+ return url.toString();
2825
+ }
2826
+ const internalRoute = url.pathname.match(/^\/[^/]+(\/.*)?$/);
2827
+ if (internalRoute) {
2828
+ url.pathname = internalRoute[1] || "/";
2829
+ }
2830
+ return url.toString();
2831
+ }
2832
+
2695
2833
  class FunctionsHandler {
2696
2834
  functions;
2697
2835
  env;
@@ -2712,22 +2850,130 @@ class FunctionsHandler {
2712
2850
  if (!name) {
2713
2851
  return json2(404, { error: "function name required: /functions/v1/<name>" });
2714
2852
  }
2715
- const fn = this.functions.get(name);
2716
- if (!fn) {
2853
+ const value = this.functions.get(name);
2854
+ if (!value) {
2717
2855
  return json2(404, { error: `function "${name}" not found` });
2718
2856
  }
2857
+ const entry = normalizeEntry(value);
2858
+ const limits = entry.limits;
2859
+ const capabilities = entry.capabilities;
2860
+ const maxBody = limits?.maxRequestBodyBytes;
2861
+ let request = req;
2862
+ if (maxBody !== undefined) {
2863
+ const contentLength = req.headers.get("content-length");
2864
+ if (contentLength !== null && Number(contentLength) > maxBody) {
2865
+ return json2(413, { error: `function "${name}" request body exceeded ${maxBody} bytes` });
2866
+ }
2867
+ if (contentLength === null && req.body) {
2868
+ request = withCountedBody(request, maxBody);
2869
+ }
2870
+ }
2871
+ const routeAware = entry.framework !== undefined && entry.framework !== "fetch" || isFrameworkRouterHandler(entry.handler);
2872
+ request = withVerifiedJwtSubject(request, ctx);
2873
+ if (routeAware)
2874
+ request = new Request(toFunctionLocalUrl(request.url), request);
2875
+ const timeoutMs = limits?.timeoutMs;
2876
+ const abort = timeoutMs !== undefined ? new AbortController : undefined;
2877
+ if (abort)
2878
+ request = withSignal(request, abort.signal);
2719
2879
  try {
2720
- const invoke = () => Promise.resolve(fn(req, { auth: ctx, env: this.env }));
2721
- const res = await runWithDenoEnv(this.env, () => runWithPgredisCache(this.pgredis, invoke));
2880
+ const env = capabilities?.secrets ? filterSecretsEnv(this.env, capabilities.secrets) : this.env;
2881
+ const invoke = () => this.invoke(entry.handler, request, ctx, env);
2882
+ const inner = () => runWithDenoEnv(env, () => runWithPgredisCache(this.pgredis, invoke));
2883
+ const run = capabilities?.outboundHosts ? () => runWithFetchPolicy(capabilities.outboundHosts, inner) : inner;
2884
+ const invokeWithBackground = () => runWithBackgroundTasks({ allowed: capabilities?.background !== false, timeoutMs: limits?.waitUntilTimeoutMs }, run);
2885
+ const res = timeoutMs !== undefined ? await this.withTimeout(name, timeoutMs, invokeWithBackground, abort) : await invokeWithBackground();
2722
2886
  if (!(res instanceof Response)) {
2723
2887
  return json2(500, { error: `function "${name}" did not return a Response` });
2724
2888
  }
2725
- return res;
2889
+ const maxResponse = limits?.maxResponseBodyBytes;
2890
+ return maxResponse !== undefined ? withResponseLimit(name, res, maxResponse) : res;
2726
2891
  } catch (e) {
2727
2892
  const message = e instanceof Error ? e.message : String(e);
2728
2893
  return json2(500, { error: message });
2729
2894
  }
2730
2895
  }
2896
+ async withTimeout(name, timeoutMs, run, abort) {
2897
+ let timer;
2898
+ const pending = run();
2899
+ try {
2900
+ const winner = await Promise.race([
2901
+ pending.then((res) => ({ timedOut: false, res })),
2902
+ new Promise((resolve) => {
2903
+ timer = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
2904
+ })
2905
+ ]);
2906
+ if (winner.timedOut) {
2907
+ abort.abort();
2908
+ pending.then(() => {}, () => {});
2909
+ return json2(504, { error: `function "${name}" timed out after ${timeoutMs}ms` });
2910
+ }
2911
+ return winner.res;
2912
+ } finally {
2913
+ if (timer)
2914
+ clearTimeout(timer);
2915
+ }
2916
+ }
2917
+ invoke(handler, req, ctx, env) {
2918
+ if (typeof handler === "function") {
2919
+ return Promise.resolve(handler(req, { auth: ctx, env }));
2920
+ }
2921
+ if (typeof handler.handle === "function") {
2922
+ return Promise.resolve(handler.handle.call(handler, req));
2923
+ }
2924
+ if (typeof handler.fetch === "function") {
2925
+ return Promise.resolve(handler.fetch.call(handler, req));
2926
+ }
2927
+ throw new Error("function handler must be a function or an object with handle()/fetch()");
2928
+ }
2929
+ }
2930
+ var BASE_ENV_KEYS = ["SUPABASE_URL", "SUPABASE_ANON_KEY", "SUPABASE_SERVICE_ROLE_KEY"];
2931
+ function filterSecretsEnv(env, secrets) {
2932
+ const out = {};
2933
+ for (const key of BASE_ENV_KEYS)
2934
+ out[key] = env[key];
2935
+ for (const key of secrets) {
2936
+ if (env[key] !== undefined)
2937
+ out[key] = env[key];
2938
+ }
2939
+ return out;
2940
+ }
2941
+ function withCountedBody(req, limit) {
2942
+ let seen = 0;
2943
+ const counter = new TransformStream({
2944
+ transform(chunk, controller) {
2945
+ seen += chunk.byteLength;
2946
+ if (seen > limit) {
2947
+ controller.error(new Error(`request body exceeded ${limit} bytes`));
2948
+ return;
2949
+ }
2950
+ controller.enqueue(chunk);
2951
+ }
2952
+ });
2953
+ return new Request(req, { body: req.body.pipeThrough(counter), duplex: "half" });
2954
+ }
2955
+ function withResponseLimit(name, res, limit) {
2956
+ const contentLength = res.headers.get("content-length");
2957
+ if (contentLength !== null && Number(contentLength) > limit) {
2958
+ return json2(502, { error: `function "${name}" response exceeded ${limit} bytes` });
2959
+ }
2960
+ if (!res.body)
2961
+ return res;
2962
+ let seen = 0;
2963
+ const counter = new TransformStream({
2964
+ transform(chunk, controller) {
2965
+ seen += chunk.byteLength;
2966
+ if (seen > limit) {
2967
+ controller.error(new Error(`function "${name}" response exceeded ${limit} bytes`));
2968
+ return;
2969
+ }
2970
+ controller.enqueue(chunk);
2971
+ }
2972
+ });
2973
+ return new Response(res.body.pipeThrough(counter), res);
2974
+ }
2975
+ function withSignal(req, signal) {
2976
+ return new Request(req, { signal, ...req.body ? { duplex: "half" } : {} });
2731
2977
  }
2732
2978
  function json2(status, body) {
2733
2979
  return new Response(JSON.stringify(body), {
@@ -9976,9 +10222,9 @@ class RetentionService {
9976
10222
  }
9977
10223
 
9978
10224
  // src/runtime/security.ts
9979
- var LOOPBACK_HOSTS = new Set(["127.0.0.1", "localhost", "::1", "", undefined]);
10225
+ var LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "::1", "", undefined]);
9980
10226
  function isNetworkExposed(host) {
9981
- return !LOOPBACK_HOSTS.has(host);
10227
+ return !LOOPBACK_HOSTS2.has(host);
9982
10228
  }
9983
10229
  function assertSecretsSafe(input) {
9984
10230
  const { host, jwtSecret, vaultKeyDerived, warn } = input;
@@ -11912,6 +12158,345 @@ async function closeResources(...resources) {
11912
12158
  throw failed.reason;
11913
12159
  }
11914
12160
 
12161
+ // src/runtime/node/db-check.ts
12162
+ import { readFile as readFile2, stat } from "fs/promises";
12163
+ import { dirname as dirname3, resolve as resolve3 } from "path";
12164
+ import { pathToFileURL } from "url";
12165
+
12166
+ // node_modules/@supacloud/db/dist/index.js
12167
+ var TABLES_SQL = `
12168
+ SELECT n.nspname AS schema,
12169
+ c.relname AS name,
12170
+ c.relrowsecurity AS rls_enabled,
12171
+ c.relforcerowsecurity AS rls_forced
12172
+ FROM pg_class c
12173
+ JOIN pg_namespace n ON n.oid = c.relnamespace
12174
+ WHERE c.relkind = 'r'
12175
+ AND n.nspname = ANY($1)
12176
+ ORDER BY n.nspname, c.relname
12177
+ `;
12178
+ var POLICIES_SQL = `
12179
+ SELECT n.nspname AS schema,
12180
+ c.relname AS table,
12181
+ p.polname AS name,
12182
+ p.polcmd AS command,
12183
+ ARRAY(
12184
+ SELECT CASE WHEN r = 0 THEN 'PUBLIC' ELSE pg_get_userbyid(r) END
12185
+ FROM unnest(p.polroles) AS r
12186
+ ) AS roles,
12187
+ pg_get_expr(p.polqual, p.polrelid) AS using_expr,
12188
+ pg_get_expr(p.polwithcheck, p.polrelid) AS check_expr
12189
+ FROM pg_policy p
12190
+ JOIN pg_class c ON c.oid = p.polrelid
12191
+ JOIN pg_namespace n ON n.oid = c.relnamespace
12192
+ WHERE n.nspname = ANY($1)
12193
+ ORDER BY n.nspname, c.relname, p.polname
12194
+ `;
12195
+ var FUNCTIONS_SQL = `
12196
+ SELECT n.nspname AS schema,
12197
+ p.proname AS name,
12198
+ p.prosecdef AS security_definer,
12199
+ p.proconfig AS config,
12200
+ l.lanname AS language
12201
+ FROM pg_proc p
12202
+ JOIN pg_namespace n ON n.oid = p.pronamespace
12203
+ JOIN pg_language l ON l.oid = p.prolang
12204
+ WHERE n.nspname = ANY($1)
12205
+ ORDER BY n.nspname, p.proname
12206
+ `;
12207
+ var GRANTS_SQL = `
12208
+ SELECT table_schema AS object_schema,
12209
+ table_name AS object_name,
12210
+ privilege_type AS privilege,
12211
+ grantee
12212
+ FROM information_schema.role_table_grants
12213
+ WHERE table_schema = ANY($1)
12214
+ UNION ALL
12215
+ SELECT routine_schema AS object_schema,
12216
+ routine_name AS object_name,
12217
+ privilege_type AS privilege,
12218
+ grantee
12219
+ FROM information_schema.routine_privileges
12220
+ WHERE routine_schema = ANY($1)
12221
+ `;
12222
+ var POLCMD_MAP = {
12223
+ r: "select",
12224
+ a: "insert",
12225
+ w: "update",
12226
+ d: "delete",
12227
+ "*": "all"
12228
+ };
12229
+ function extractSearchPath(config) {
12230
+ if (!config)
12231
+ return null;
12232
+ const entry = config.find((item) => item.startsWith("search_path="));
12233
+ if (!entry)
12234
+ return null;
12235
+ return entry.slice("search_path=".length);
12236
+ }
12237
+ async function readCatalog(executor, schemas = ["public"]) {
12238
+ const params = [schemas];
12239
+ const [tableRows, policyRows, functionRows, grantRows] = await Promise.all([
12240
+ executor.query(TABLES_SQL, params),
12241
+ executor.query(POLICIES_SQL, params),
12242
+ executor.query(FUNCTIONS_SQL, params),
12243
+ executor.query(GRANTS_SQL, params)
12244
+ ]);
12245
+ return {
12246
+ tables: tableRows.map((row) => ({
12247
+ schema: row.schema,
12248
+ name: row.name,
12249
+ rlsEnabled: row.rls_enabled,
12250
+ rlsForced: row.rls_forced
12251
+ })),
12252
+ policies: policyRows.map((row) => ({
12253
+ schema: row.schema,
12254
+ table: row.table,
12255
+ name: row.name,
12256
+ command: POLCMD_MAP[row.command] ?? "all",
12257
+ roles: row.roles ?? [],
12258
+ usingExpr: row.using_expr ?? undefined,
12259
+ checkExpr: row.check_expr ?? undefined
12260
+ })),
12261
+ functions: functionRows.map((row) => ({
12262
+ schema: row.schema,
12263
+ name: row.name,
12264
+ security: row.security_definer ? "definer" : "invoker",
12265
+ searchPath: extractSearchPath(row.config),
12266
+ language: row.language
12267
+ })),
12268
+ grants: grantRows.map((row) => ({
12269
+ objectSchema: row.object_schema,
12270
+ objectName: row.object_name,
12271
+ privilege: row.privilege,
12272
+ grantee: row.grantee
12273
+ }))
12274
+ };
12275
+ }
12276
+ function splitQualifiedName(name) {
12277
+ const dot = name.indexOf(".");
12278
+ if (dot === -1)
12279
+ return ["public", name];
12280
+ return [name.slice(0, dot), name.slice(dot + 1)];
12281
+ }
12282
+ function isFixedSearchPath(searchPath) {
12283
+ if (searchPath === null)
12284
+ return false;
12285
+ const parts = searchPath.split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
12286
+ return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp");
12287
+ }
12288
+ function reconcileModule(module, catalog) {
12289
+ const issues = [];
12290
+ const ownedTables = new Set(module.tables);
12291
+ const push = (severity, code, object, message2) => issues.push({ severity, code, object, message: message2 });
12292
+ for (const policy of module.policies) {
12293
+ const [schema, table] = splitQualifiedName(policy.table);
12294
+ const found = catalog.policies.some((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
12295
+ if (!found) {
12296
+ push("error", "missing-policy", `${policy.table}.${policy.name}`, `\u58F0\u660E\u7684\u7B56\u7565 ${policy.name} \u5728\u8868 ${policy.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
12297
+ }
12298
+ }
12299
+ const declaredPolicyKeys = new Set(module.policies.map((p) => `${p.table}::${p.name}`));
12300
+ for (const cp of catalog.policies) {
12301
+ const qualified = `${cp.schema}.${cp.table}`;
12302
+ if (ownedTables.has(qualified) && !declaredPolicyKeys.has(`${qualified}::${cp.name}`)) {
12303
+ push("warn", "undeclared-policy", `${qualified}.${cp.name}`, `\u5F52\u5C5E\u8868 ${qualified} \u4E0A\u5B58\u5728\u672A\u58F0\u660E\u7684\u7B56\u7565 ${cp.name}\uFF0C\u53EF\u80FD\u53D1\u751F\u6F02\u79FB`);
12304
+ }
12305
+ }
12306
+ for (const fn of module.functions) {
12307
+ const [schema, name] = splitQualifiedName(fn.name);
12308
+ const cf = catalog.functions.find((f) => f.schema === schema && f.name === name);
12309
+ if (!cf) {
12310
+ push("error", "missing-function", fn.name, `\u58F0\u660E\u7684\u51FD\u6570 ${fn.name} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
12311
+ continue;
12312
+ }
12313
+ if (cf.security !== fn.security) {
12314
+ push("warn", "security-mismatch", fn.name, `\u51FD\u6570 ${fn.name} \u58F0\u660E\u4E3A security ${fn.security}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${cf.security}`);
12315
+ }
12316
+ const effectiveDefiner = fn.security === "definer" || cf.security === "definer";
12317
+ if (effectiveDefiner && !isFixedSearchPath(cf.searchPath)) {
12318
+ push("error", "definer-without-search-path", fn.name, `security definer \u51FD\u6570 ${fn.name} \u672A\u8BBE\u7F6E\u56FA\u5B9A search_path\uFF08\u5F53\u524D: ${cf.searchPath ?? "\u672A\u8BBE\u7F6E"}\uFF09`);
12319
+ }
12320
+ }
12321
+ for (const table of module.tables) {
12322
+ const [schema, name] = splitQualifiedName(table);
12323
+ const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
12324
+ if (ct && !ct.rlsEnabled) {
12325
+ push("error", "rls-disabled", table, `\u5F52\u5C5E\u8868 ${table} \u672A\u5F00\u542F\u884C\u7EA7\u5B89\u5168\uFF08relrowsecurity = false\uFF09`);
12326
+ }
12327
+ }
12328
+ for (const grant of catalog.grants) {
12329
+ const qualified = `${grant.objectSchema}.${grant.objectName}`;
12330
+ if (ownedTables.has(qualified) && grant.grantee.toUpperCase() === "PUBLIC") {
12331
+ push("error", "wildcard-grant", qualified, `\u5F52\u5C5E\u8868 ${qualified} \u5B58\u5728\u6388\u4E88 PUBLIC \u7684 ${grant.privilege} \u6743\u9650`);
12332
+ }
12333
+ }
12334
+ for (const grant of module.grants) {
12335
+ const [schema, name] = splitQualifiedName(grant.object);
12336
+ const found = catalog.grants.some((cg) => cg.objectSchema === schema && cg.objectName === name && cg.privilege.toLowerCase() === grant.privilege.toLowerCase() && cg.grantee.toLowerCase() === grant.role.toLowerCase());
12337
+ if (!found) {
12338
+ push("warn", "grant-drift", grant.object, `\u58F0\u660E\u7684\u6388\u6743 ${grant.privilege} ON ${grant.object} TO ${grant.role} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
12339
+ }
12340
+ }
12341
+ return {
12342
+ module: module.name,
12343
+ issues,
12344
+ ok: !issues.some((issue) => issue.severity === "error")
12345
+ };
12346
+ }
12347
+ var SECURITY_DEFINER_RE = /\bsecurity\s+definer\b/i;
12348
+ var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
12349
+ var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
12350
+ var DROP_WITHOUT_IF_EXISTS_RE = /\bdrop\s+(?:table|column)\s+(?!if\s+exists\b)/i;
12351
+ var ENABLE_RLS_RE = /\benable\s+row\s+level\s+security\b/i;
12352
+ function lineOf(sql, index) {
12353
+ let line = 1;
12354
+ for (let i = 0;i < index; i += 1) {
12355
+ if (sql.charCodeAt(i) === 10)
12356
+ line += 1;
12357
+ }
12358
+ return line;
12359
+ }
12360
+ function lintSql(sql, file) {
12361
+ const issues = [];
12362
+ const definer = SECURITY_DEFINER_RE.exec(sql);
12363
+ if (definer && !SET_SEARCH_PATH_RE.test(sql)) {
12364
+ issues.push({
12365
+ severity: "error",
12366
+ code: "definer-no-search-path",
12367
+ message: "security definer \u51FD\u6570\u5FC5\u987B\u663E\u5F0F set search_path\uFF0C\u907F\u514D search_path \u52AB\u6301",
12368
+ file,
12369
+ line: lineOf(sql, definer.index)
12370
+ });
12371
+ }
12372
+ const grantPublic = GRANT_TO_PUBLIC_RE.exec(sql);
12373
+ if (grantPublic) {
12374
+ issues.push({
12375
+ severity: "error",
12376
+ code: "grant-to-public",
12377
+ message: "\u7981\u6B62\u5C06\u6743\u9650\u6388\u4E88 PUBLIC \u89D2\u8272",
12378
+ file,
12379
+ line: lineOf(sql, grantPublic.index)
12380
+ });
12381
+ }
12382
+ const drop = DROP_WITHOUT_IF_EXISTS_RE.exec(sql);
12383
+ if (drop) {
12384
+ issues.push({
12385
+ severity: "warn",
12386
+ code: "drop-without-if-exists",
12387
+ message: "drop table/column \u5EFA\u8BAE\u4F7F\u7528 if exists\uFF0C\u4FDD\u8BC1\u8FC1\u79FB\u53EF\u91CD\u5165",
12388
+ file,
12389
+ line: lineOf(sql, drop.index)
12390
+ });
12391
+ }
12392
+ return issues;
12393
+ }
12394
+ async function lintModule(module, readFile2) {
12395
+ const issues = [];
12396
+ const sources = new Set;
12397
+ for (const decl of [
12398
+ ...module.policies,
12399
+ ...module.functions,
12400
+ ...module.triggers,
12401
+ ...module.grants
12402
+ ]) {
12403
+ sources.add(decl.source);
12404
+ }
12405
+ const contents = new Map;
12406
+ await Promise.all([...sources].map(async (path) => {
12407
+ contents.set(path, await readFile2(path));
12408
+ }));
12409
+ for (const [file, sql] of contents) {
12410
+ issues.push(...lintSql(sql, file));
12411
+ }
12412
+ if (module.policies.length > 0) {
12413
+ const anyEnable = module.policies.some((policy) => ENABLE_RLS_RE.test(contents.get(policy.source) ?? ""));
12414
+ if (!anyEnable) {
12415
+ issues.push({
12416
+ severity: "warn",
12417
+ code: "missing-rls-enable",
12418
+ message: `\u6A21\u5757 ${module.name} \u58F0\u660E\u4E86 ${module.policies.length} \u6761\u7B56\u7565\uFF0C\u4F46\u6240\u6709\u7B56\u7565\u6E90\u6587\u4EF6\u90FD\u6CA1\u6709 enable row level security`,
12419
+ file: module.policies[0].source
12420
+ });
12421
+ }
12422
+ }
12423
+ for (const policy of module.policies) {
12424
+ if (!policy.tests || policy.tests.length === 0) {
12425
+ issues.push({
12426
+ severity: "warn",
12427
+ code: "policy-without-test",
12428
+ message: `\u7B56\u7565 ${policy.name} \u672A\u58F0\u660E\u6D4B\u8BD5\u6587\u4EF6`,
12429
+ file: policy.source
12430
+ });
12431
+ }
12432
+ }
12433
+ for (const fn of module.functions) {
12434
+ if (!fn.tests || fn.tests.length === 0) {
12435
+ issues.push({
12436
+ severity: "warn",
12437
+ code: "policy-without-test",
12438
+ message: `\u51FD\u6570 ${fn.name} \u672A\u58F0\u660E\u6D4B\u8BD5\u6587\u4EF6`,
12439
+ file: fn.source
12440
+ });
12441
+ }
12442
+ }
12443
+ return issues;
12444
+ }
12445
+
12446
+ // src/runtime/node/db-check.ts
12447
+ async function loadDatabaseModules(moduleFile) {
12448
+ const absolute = resolve3(moduleFile);
12449
+ try {
12450
+ await stat(absolute);
12451
+ } catch {
12452
+ throw new Error(`database module manifest not found: ${absolute}`);
12453
+ }
12454
+ const mod = await import(pathToFileURL(absolute).href);
12455
+ const candidate = mod.default ?? mod.modules;
12456
+ const list = Array.isArray(candidate) ? candidate : candidate ? [candidate] : [];
12457
+ if (list.length === 0) {
12458
+ throw new Error(`database module manifest ${absolute} must export a default module or module array`);
12459
+ }
12460
+ for (const entry of list) {
12461
+ if (!entry || typeof entry.name !== "string") {
12462
+ throw new Error(`database module manifest ${absolute} contains an entry without a name`);
12463
+ }
12464
+ }
12465
+ return list;
12466
+ }
12467
+ async function checkDatabaseModules(options) {
12468
+ const modules = await loadDatabaseModules(options.moduleFile);
12469
+ const baseDir = dirname3(resolve3(options.moduleFile));
12470
+ const readSource = (path) => readFile2(resolve3(baseDir, path), "utf8");
12471
+ const catalog = await readCatalog(options.executor, [options.schema ?? "public"]);
12472
+ const reports = [];
12473
+ for (const module of modules) {
12474
+ const lintIssues = await lintModule(module, readSource);
12475
+ const reconcile = reconcileModule(module, catalog);
12476
+ reports.push({ module: module.name, lintIssues, reconcile });
12477
+ }
12478
+ const ok = reports.every((report) => report.reconcile.ok && !report.lintIssues.some((issue) => issue.severity === "error"));
12479
+ return { ok, reports };
12480
+ }
12481
+ function formatDatabaseModuleCheck(result) {
12482
+ const lines = [];
12483
+ for (const report of result.reports) {
12484
+ lines.push(`module ${report.module}:`);
12485
+ for (const issue of report.lintIssues) {
12486
+ lines.push(` [lint ${issue.severity}] ${issue.code}: ${issue.message} (${issue.file})`);
12487
+ }
12488
+ for (const issue of report.reconcile.issues) {
12489
+ lines.push(` [catalog ${issue.severity}] ${issue.code}: ${issue.message} (${issue.object})`);
12490
+ }
12491
+ if (report.lintIssues.length === 0 && report.reconcile.issues.length === 0) {
12492
+ lines.push(" ok");
12493
+ }
12494
+ }
12495
+ return `${lines.join(`
12496
+ `)}
12497
+ `;
12498
+ }
12499
+
11915
12500
  // src/runtime/node/native/readiness.ts
11916
12501
  function liteCapabilities(engine, replicationProfile) {
11917
12502
  if (engine === "pglite") {
@@ -12143,7 +12728,7 @@ function sameStrings(left, right) {
12143
12728
  }
12144
12729
 
12145
12730
  // src/runtime/node/project.ts
12146
- import { readdir, readFile as readFile2 } from "fs/promises";
12731
+ import { readdir, readFile as readFile3 } from "fs/promises";
12147
12732
  import { join as join3 } from "path";
12148
12733
  async function loadSupabaseProject(projectDir, seed = {}) {
12149
12734
  const migrationsDir = join3(projectDir, "supabase", "migrations");
@@ -12158,7 +12743,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12158
12743
  for (const entry of entries.sort()) {
12159
12744
  if (!entry.endsWith(".sql"))
12160
12745
  continue;
12161
- const sql = await readFile2(join3(migrationsDir, entry), "utf8");
12746
+ const sql = await readFile3(join3(migrationsDir, entry), "utf8");
12162
12747
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
12163
12748
  }
12164
12749
  let seedSql;
@@ -12170,7 +12755,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12170
12755
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
12171
12756
  for (const relativePath of matches) {
12172
12757
  try {
12173
- parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
12758
+ parts.push(await readFile3(join3(supabaseDir, relativePath), "utf8"));
12174
12759
  } catch (error) {
12175
12760
  if (!isNotFound(error))
12176
12761
  throw error;
@@ -12188,8 +12773,8 @@ function isNotFound(error) {
12188
12773
  }
12189
12774
 
12190
12775
  // src/project-runtime.ts
12191
- import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
12192
- import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve3 } from "path";
12776
+ import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
12777
+ import { dirname as dirname6, isAbsolute, join as join8, parse, relative, resolve as resolve4 } from "path";
12193
12778
 
12194
12779
  // src/runtime/node/bun-server.ts
12195
12780
  async function serveBun(backend, opts = {}) {
@@ -12239,8 +12824,8 @@ async function serveBun(backend, opts = {}) {
12239
12824
  }
12240
12825
 
12241
12826
  // src/runtime/node/fs-driver.ts
12242
- import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
12243
- import { dirname as dirname3, join as join4, normalize, sep } from "path";
12827
+ import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
12828
+ import { dirname as dirname4, join as join4, normalize, sep } from "path";
12244
12829
 
12245
12830
  class FsStorageDriver {
12246
12831
  root;
@@ -12256,7 +12841,7 @@ class FsStorageDriver {
12256
12841
  }
12257
12842
  async put(key, data) {
12258
12843
  const path = this.resolve(key);
12259
- await mkdir3(dirname3(path), { recursive: true });
12844
+ await mkdir3(dirname4(path), { recursive: true });
12260
12845
  const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
12261
12846
  try {
12262
12847
  await writeFile3(temporaryPath, data);
@@ -12268,7 +12853,7 @@ class FsStorageDriver {
12268
12853
  }
12269
12854
  async get(key) {
12270
12855
  try {
12271
- return new Uint8Array(await readFile3(this.resolve(key)));
12856
+ return new Uint8Array(await readFile4(this.resolve(key)));
12272
12857
  } catch (e) {
12273
12858
  if (e.code === "ENOENT")
12274
12859
  return null;
@@ -12656,19 +13241,48 @@ function readFunctions(root) {
12656
13241
  const entrypoint = getString(t, "entrypoint");
12657
13242
  if (entrypoint !== undefined)
12658
13243
  opts.entrypoint = entrypoint;
13244
+ const framework = getString(t, "framework");
13245
+ if (framework !== undefined) {
13246
+ if (framework === "fetch" || framework === "elysia" || framework === "hono") {
13247
+ opts.framework = framework;
13248
+ } else {
13249
+ console.warn(` warning: [functions.${name}] framework "${framework}" is not one of fetch/elysia/hono; falling back to fetch`);
13250
+ }
13251
+ }
13252
+ const timeoutMs = getInt(t, "timeout_ms");
13253
+ if (timeoutMs !== undefined && timeoutMs > 0)
13254
+ opts.timeoutMs = timeoutMs;
13255
+ const maxRequestBodyBytes = getInt(t, "max_request_body_bytes");
13256
+ if (maxRequestBodyBytes !== undefined && maxRequestBodyBytes > 0)
13257
+ opts.maxRequestBodyBytes = maxRequestBodyBytes;
13258
+ const maxResponseBodyBytes = getInt(t, "max_response_body_bytes");
13259
+ if (maxResponseBodyBytes !== undefined && maxResponseBodyBytes > 0)
13260
+ opts.maxResponseBodyBytes = maxResponseBodyBytes;
13261
+ const waitUntilTimeoutMs = getInt(t, "wait_until_timeout_ms");
13262
+ if (waitUntilTimeoutMs !== undefined && waitUntilTimeoutMs > 0)
13263
+ opts.waitUntilTimeoutMs = waitUntilTimeoutMs;
13264
+ const outboundHosts = getStringArray(t, "outbound_hosts");
13265
+ if (outboundHosts !== undefined)
13266
+ opts.outboundHosts = outboundHosts;
13267
+ const secrets = getStringArray(t, "secrets");
13268
+ if (secrets !== undefined)
13269
+ opts.secrets = secrets;
13270
+ const background = getBool(t, "background");
13271
+ if (background !== undefined)
13272
+ opts.background = background;
12659
13273
  out[name] = opts;
12660
13274
  }
12661
13275
  return out;
12662
13276
  }
12663
13277
 
12664
13278
  // src/runtime/node/load-functions.ts
12665
- import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
12666
- import { dirname as dirname4, join as join7 } from "path";
12667
- import { pathToFileURL } from "url";
13279
+ import { readdir as readdir2, readFile as readFile6, realpath, rm as rm4, stat as stat2 } from "fs/promises";
13280
+ import { dirname as dirname5, join as join7 } from "path";
13281
+ import { pathToFileURL as pathToFileURL2 } from "url";
12668
13282
 
12669
13283
  // src/runtime/node/bundle-function.ts
12670
13284
  import { createHash as createHash3 } from "crypto";
12671
- import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
13285
+ import { mkdir as mkdir4, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
12672
13286
  import { existsSync as existsSync3 } from "fs";
12673
13287
  import { tmpdir as tmpdir3 } from "os";
12674
13288
  import { join as join6 } from "path";
@@ -12684,7 +13298,7 @@ async function fetchModule(url) {
12684
13298
  const key = createHash3("sha256").update(url).digest("hex");
12685
13299
  const cached = join6(HTTP_CACHE, key);
12686
13300
  if (existsSync3(cached))
12687
- return readFile4(cached, "utf8");
13301
+ return readFile5(cached, "utf8");
12688
13302
  const res = await fetch(url, { redirect: "follow" });
12689
13303
  if (!res.ok)
12690
13304
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
@@ -12743,7 +13357,7 @@ async function bundleFunction(entryPath, name) {
12743
13357
  async function loadFunctionEnv(projectDir) {
12744
13358
  let text;
12745
13359
  try {
12746
- text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
13360
+ text = await readFile6(join7(projectDir, "supabase", "functions", ".env"), "utf8");
12747
13361
  } catch {
12748
13362
  return {};
12749
13363
  }
@@ -12766,6 +13380,15 @@ async function loadFunctionEnv(projectDir) {
12766
13380
  }
12767
13381
  return env;
12768
13382
  }
13383
+ var FUNCTION_FRAMEWORKS = new Set(["fetch", "elysia", "hono"]);
13384
+ function resolveFramework(name, value) {
13385
+ if (value === undefined)
13386
+ return;
13387
+ if (FUNCTION_FRAMEWORKS.has(value))
13388
+ return value;
13389
+ console.warn(` warning: function "${name}" has unsupported framework "${value}", expected one of fetch/elysia/hono; falling back to fetch`);
13390
+ return;
13391
+ }
12769
13392
  var loadQueue = Promise.resolve();
12770
13393
  async function loadFunctions2(projectDir, options = {}) {
12771
13394
  let releaseQueue;
@@ -12796,12 +13419,12 @@ async function loadFunctionsUnlocked(projectDir, options) {
12796
13419
  if (options[name]?.enabled === false)
12797
13420
  continue;
12798
13421
  const dir = join7(root, name);
12799
- if (!(await stat(dir)).isDirectory())
13422
+ if (!(await stat2(dir)).isDirectory())
12800
13423
  continue;
12801
13424
  const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
12802
13425
  for (const path of candidates) {
12803
13426
  try {
12804
- await stat(path);
13427
+ await stat2(path);
12805
13428
  } catch {
12806
13429
  continue;
12807
13430
  }
@@ -12810,21 +13433,38 @@ async function loadFunctionsUnlocked(projectDir, options) {
12810
13433
  let importUrl;
12811
13434
  try {
12812
13435
  bundledPath = await realpath(await bundleFunction(path, `${name}-${crypto.randomUUID()}`));
12813
- importUrl = pathToFileURL(bundledPath).href;
13436
+ importUrl = pathToFileURL2(bundledPath).href;
12814
13437
  } catch (e) {
12815
13438
  if (e.message !== "esbuild-not-available")
12816
13439
  throw e;
12817
- importUrl = pathToFileURL(path).href;
13440
+ importUrl = pathToFileURL2(path).href;
12818
13441
  }
12819
13442
  resetCapturedHandler();
12820
13443
  const mod = await import(importUrl);
12821
13444
  const denoHandler = takeCapturedHandler();
12822
13445
  const defaultExport = mod.default;
12823
- const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && typeof defaultExport.fetch === "function" ? defaultExport.fetch.bind(defaultExport) : denoHandler ? (req) => denoHandler(req) : undefined;
13446
+ const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && (typeof defaultExport.handle === "function" || typeof defaultExport.fetch === "function") ? defaultExport : denoHandler ? (req) => denoHandler(req) : undefined;
12824
13447
  if (handler) {
12825
- functions.set(name, handler);
13448
+ const opts = options[name];
13449
+ const limits = opts && (opts.timeoutMs !== undefined || opts.maxRequestBodyBytes !== undefined || opts.maxResponseBodyBytes !== undefined || opts.waitUntilTimeoutMs !== undefined) ? {
13450
+ ...opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {},
13451
+ ...opts.maxRequestBodyBytes !== undefined ? { maxRequestBodyBytes: opts.maxRequestBodyBytes } : {},
13452
+ ...opts.maxResponseBodyBytes !== undefined ? { maxResponseBodyBytes: opts.maxResponseBodyBytes } : {},
13453
+ ...opts.waitUntilTimeoutMs !== undefined ? { waitUntilTimeoutMs: opts.waitUntilTimeoutMs } : {}
13454
+ } : undefined;
13455
+ const capabilities = opts && (opts.outboundHosts !== undefined || opts.secrets !== undefined || opts.background !== undefined) ? {
13456
+ ...opts.outboundHosts !== undefined ? { outboundHosts: opts.outboundHosts } : {},
13457
+ ...opts.secrets !== undefined ? { secrets: opts.secrets } : {},
13458
+ ...opts.background !== undefined ? { background: opts.background } : {}
13459
+ } : undefined;
13460
+ functions.set(name, {
13461
+ handler,
13462
+ framework: resolveFramework(name, opts?.framework),
13463
+ ...limits ? { limits } : {},
13464
+ ...capabilities ? { capabilities } : {}
13465
+ });
12826
13466
  } else {
12827
- console.warn(` warning: function "${name}" has no default function, fetch object, or Deno.serve() handler, skipped`);
13467
+ console.warn(` warning: function "${name}" has no default function, handle/fetch object, or Deno.serve() handler, skipped`);
12828
13468
  }
12829
13469
  } catch (e) {
12830
13470
  const msg = e instanceof Error ? e.message : String(e);
@@ -12835,7 +13475,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
12835
13475
  }
12836
13476
  } finally {
12837
13477
  if (bundledPath)
12838
- await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
13478
+ await rm4(dirname5(bundledPath), { recursive: true, force: true }).catch(() => {});
12839
13479
  }
12840
13480
  break;
12841
13481
  }
@@ -12941,7 +13581,7 @@ class S3StorageDriver {
12941
13581
  var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
12942
13582
  var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
12943
13583
  function resolveProjectPaths(options = {}) {
12944
- const projectDir = resolve3(options.projectDir ?? process.cwd());
13584
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
12945
13585
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
12946
13586
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
12947
13587
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -12956,7 +13596,7 @@ function resolveProjectPaths(options = {}) {
12956
13596
  };
12957
13597
  }
12958
13598
  async function assertResetPathsSafe(paths) {
12959
- const stateDir = resolve3(paths.stateDir);
13599
+ const stateDir = resolve4(paths.stateDir);
12960
13600
  if (stateDir === parse(stateDir).root)
12961
13601
  throw new Error("refusing to use the filesystem root as the state directory");
12962
13602
  const stateInfo = await requiredResetEntry(stateDir);
@@ -12964,7 +13604,7 @@ async function assertResetPathsSafe(paths) {
12964
13604
  throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
12965
13605
  }
12966
13606
  const canonicalStateDir = await realpath2(stateDir);
12967
- const secretsFile = resolve3(paths.secretsFile);
13607
+ const secretsFile = resolve4(paths.secretsFile);
12968
13608
  if (secretsFile !== join8(stateDir, "secrets.json")) {
12969
13609
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
12970
13610
  }
@@ -12978,7 +13618,7 @@ async function assertResetPathsSafe(paths) {
12978
13618
  ["storage", paths.storageDir]
12979
13619
  ];
12980
13620
  for (const [label, targetPath] of targets) {
12981
- const target2 = resolve3(targetPath);
13621
+ const target2 = resolve4(targetPath);
12982
13622
  const relativePath = relative(stateDir, target2);
12983
13623
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
12984
13624
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
@@ -12998,7 +13638,7 @@ async function requiredResetEntry(path) {
12998
13638
  async function assertResetSecretsValid(path) {
12999
13639
  let serializedSecrets;
13000
13640
  try {
13001
- serializedSecrets = await readFile6(path, "utf8");
13641
+ serializedSecrets = await readFile7(path, "utf8");
13002
13642
  } catch (error) {
13003
13643
  if (error.code === "ENOENT")
13004
13644
  throw new Error(RESET_INITIALIZATION_ERROR);
@@ -13021,13 +13661,13 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
13021
13661
  if (error.code !== "ENOENT")
13022
13662
  throw error;
13023
13663
  }
13024
- const parent = dirname5(current);
13664
+ const parent = dirname6(current);
13025
13665
  if (parent === current)
13026
13666
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
13027
13667
  current = parent;
13028
13668
  }
13029
13669
  const existingAncestor = await nearestExistingAncestor(target2);
13030
- const canonicalTarget = resolve3(await realpath2(existingAncestor), relative(existingAncestor, target2));
13670
+ const canonicalTarget = resolve4(await realpath2(existingAncestor), relative(existingAncestor, target2));
13031
13671
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
13032
13672
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
13033
13673
  throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
@@ -13043,7 +13683,7 @@ async function nearestExistingAncestor(target2) {
13043
13683
  if (error.code !== "ENOENT")
13044
13684
  throw error;
13045
13685
  }
13046
- const parent = dirname5(current);
13686
+ const parent = dirname6(current);
13047
13687
  if (parent === current)
13048
13688
  throw new Error(`unable to resolve an existing ancestor for ${target2}`);
13049
13689
  current = parent;
@@ -13058,7 +13698,7 @@ async function ensureProjectSecrets(paths) {
13058
13698
  await chmod(paths.stateDir, 448);
13059
13699
  let stored;
13060
13700
  try {
13061
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13701
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13062
13702
  } catch (error) {
13063
13703
  if (error.code !== "ENOENT")
13064
13704
  throw error;
@@ -13076,7 +13716,7 @@ async function ensureProjectSecrets(paths) {
13076
13716
  } catch (error2) {
13077
13717
  if (error2.code !== "EEXIST")
13078
13718
  throw error2;
13079
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13719
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13080
13720
  } finally {
13081
13721
  await unlink2(temporaryFile).catch((error2) => {
13082
13722
  if (error2.code !== "ENOENT")
@@ -13276,7 +13916,7 @@ async function startProjectServer(options = {}) {
13276
13916
  }
13277
13917
  async function loadWebhooks(projectDir) {
13278
13918
  try {
13279
- const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
13919
+ const parsed = JSON.parse(await readFile7(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
13280
13920
  return Array.isArray(parsed) ? parsed : [];
13281
13921
  } catch (error) {
13282
13922
  if (error.code === "ENOENT")
@@ -13285,7 +13925,7 @@ async function loadWebhooks(projectDir) {
13285
13925
  }
13286
13926
  }
13287
13927
  function resolvePath(projectDir, path) {
13288
- return isAbsolute(path) ? path : resolve3(projectDir, path);
13928
+ return isAbsolute(path) ? path : resolve4(projectDir, path);
13289
13929
  }
13290
13930
  function randomHex(bytes) {
13291
13931
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -13330,8 +13970,8 @@ async function findEphemeralPort(host = "127.0.0.1") {
13330
13970
  }
13331
13971
 
13332
13972
  // src/snapshot.ts
13333
- import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
13334
- import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
13973
+ import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile8, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
13974
+ import { dirname as dirname7, join as join9, parse as parse2, relative as relative2, resolve as resolve5, sep as sep2 } from "path";
13335
13975
  import { create as createTar, extract as extractTar2 } from "tar";
13336
13976
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
13337
13977
  var SNAPSHOT_VERSION = 1;
@@ -13355,11 +13995,11 @@ async function createSnapshot(options) {
13355
13995
  postgresMajor: await readPostgresMajor(paths.dataDir)
13356
13996
  } : {}
13357
13997
  };
13358
- const output = resolve4(options.output);
13998
+ const output = resolve5(options.output);
13359
13999
  if (await existingInfo(output))
13360
14000
  throw new Error(`snapshot output already exists: ${output}`);
13361
- await mkdir6(dirname6(output), { recursive: true });
13362
- const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
14001
+ await mkdir6(dirname7(output), { recursive: true });
14002
+ const stagingRoot = await mkdtemp(join9(dirname7(output), ".supacloud-lite-snapshot-"));
13363
14003
  try {
13364
14004
  await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
13365
14005
  `);
@@ -13388,7 +14028,7 @@ async function restoreSnapshot(options) {
13388
14028
  const paths = normalizePaths(options.paths);
13389
14029
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
13390
14030
  await assertNoDataDirectoryLock(paths);
13391
- const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
14031
+ const stagingRoot = await mkdtemp(join9(dirname7(paths.stateDir), ".supacloud-lite-restore-"));
13392
14032
  const payloadRoot = join9(stagingRoot, "payload");
13393
14033
  const rollbackId = crypto.randomUUID();
13394
14034
  const rollbackPaths = [];
@@ -13396,7 +14036,7 @@ async function restoreSnapshot(options) {
13396
14036
  await mkdir6(payloadRoot, { recursive: true });
13397
14037
  await extractTar2({
13398
14038
  cwd: payloadRoot,
13399
- file: resolve4(options.input),
14039
+ file: resolve5(options.input),
13400
14040
  preserveOwner: false,
13401
14041
  preservePaths: false,
13402
14042
  strict: true,
@@ -13473,11 +14113,11 @@ async function restoreSnapshot(options) {
13473
14113
  function normalizePaths(paths) {
13474
14114
  return {
13475
14115
  ...paths,
13476
- projectDir: resolve4(paths.projectDir),
13477
- stateDir: resolve4(paths.stateDir),
13478
- dataDir: paths.dataDir ? resolve4(paths.dataDir) : undefined,
13479
- storageDir: resolve4(paths.storageDir),
13480
- secretsFile: resolve4(paths.secretsFile)
14116
+ projectDir: resolve5(paths.projectDir),
14117
+ stateDir: resolve5(paths.stateDir),
14118
+ dataDir: paths.dataDir ? resolve5(paths.dataDir) : undefined,
14119
+ storageDir: resolve5(paths.storageDir),
14120
+ secretsFile: resolve5(paths.secretsFile)
13481
14121
  };
13482
14122
  }
13483
14123
  async function assertSnapshotPaths(paths, options = {}) {
@@ -13507,7 +14147,7 @@ async function assertSnapshotPaths(paths, options = {}) {
13507
14147
  async function assertDirectoryOrMissing(path) {
13508
14148
  if (!path)
13509
14149
  return;
13510
- if (resolve4(path) === parse2(resolve4(path)).root)
14150
+ if (resolve5(path) === parse2(resolve5(path)).root)
13511
14151
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
13512
14152
  try {
13513
14153
  const info = await lstat2(path);
@@ -13561,13 +14201,13 @@ async function stageDirectory(root, destination) {
13561
14201
  await walk(root, destination);
13562
14202
  }
13563
14203
  async function stageFile(source, target2) {
13564
- await mkdir6(dirname6(target2), { recursive: true });
14204
+ await mkdir6(dirname7(target2), { recursive: true });
13565
14205
  await copyFile(source, target2);
13566
14206
  }
13567
14207
  async function readManifest(payloadRoot) {
13568
14208
  let parsed;
13569
14209
  try {
13570
- parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
14210
+ parsed = JSON.parse(await readFile8(join9(payloadRoot, "manifest.json"), "utf8"));
13571
14211
  } catch (error) {
13572
14212
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
13573
14213
  }
@@ -13599,7 +14239,7 @@ async function readPostgresMajor(dataDir) {
13599
14239
  if (!dataDir)
13600
14240
  return;
13601
14241
  try {
13602
- return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
14242
+ return (await readFile8(join9(dataDir, "PG_VERSION"), "utf8")).trim();
13603
14243
  } catch (error) {
13604
14244
  if (error.code === "ENOENT")
13605
14245
  return;
@@ -13659,12 +14299,12 @@ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
13659
14299
  throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
13660
14300
  await rm5(target2, { recursive: true, force: true });
13661
14301
  } else {
13662
- swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
14302
+ swap.rollbackPath = join9(dirname7(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
13663
14303
  await rename2(target2, swap.rollbackPath);
13664
14304
  }
13665
14305
  }
13666
14306
  try {
13667
- await mkdir6(dirname6(target2), { recursive: true });
14307
+ await mkdir6(dirname7(target2), { recursive: true });
13668
14308
  await rename2(source, target2);
13669
14309
  swaps.push(swap);
13670
14310
  } catch (error) {
@@ -13698,7 +14338,7 @@ async function copyEntry(source, target2) {
13698
14338
  for (const entry of await readdir3(source))
13699
14339
  await copyEntry(join9(source, entry), join9(target2, entry));
13700
14340
  } else if (info.isFile()) {
13701
- await mkdir6(dirname6(target2), { recursive: true });
14341
+ await mkdir6(dirname7(target2), { recursive: true });
13702
14342
  await Bun.write(target2, Bun.file(source));
13703
14343
  } else
13704
14344
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
@@ -13729,13 +14369,13 @@ async function assertNoSymlinks(root) {
13729
14369
  }
13730
14370
  }
13731
14371
  function isWithin(parent, child) {
13732
- const normalizedParent = resolve4(parent);
13733
- const normalizedChild = resolve4(child);
14372
+ const normalizedParent = resolve5(parent);
14373
+ const normalizedChild = resolve5(child);
13734
14374
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
13735
14375
  }
13736
14376
  function pathsOverlap(left, right) {
13737
- const normalizedLeft = resolve4(left);
13738
- const normalizedRight = resolve4(right);
14377
+ const normalizedLeft = resolve5(left);
14378
+ const normalizedRight = resolve5(right);
13739
14379
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
13740
14380
  }
13741
14381
 
@@ -13784,13 +14424,13 @@ function parseArgs(argv) {
13784
14424
  else if (argument === "--site-url")
13785
14425
  options.siteUrl = next();
13786
14426
  else if (argument === "--project-dir" || argument === "--dir")
13787
- options.projectDir = resolve5(next());
14427
+ options.projectDir = resolve6(next());
13788
14428
  else if (argument === "--state-dir")
13789
- options.stateDir = resolve5(next());
14429
+ options.stateDir = resolve6(next());
13790
14430
  else if (argument === "--data-dir")
13791
- options.dataDir = resolve5(next());
14431
+ options.dataDir = resolve6(next());
13792
14432
  else if (argument === "--storage-dir")
13793
- options.storageDir = resolve5(next());
14433
+ options.storageDir = resolve6(next());
13794
14434
  else if (argument === "--storage-backend")
13795
14435
  options.storageBackend = next();
13796
14436
  else if (argument === "--s3-prefix")
@@ -13808,15 +14448,17 @@ function parseArgs(argv) {
13808
14448
  else if (argument === "--powersync-tables")
13809
14449
  options.powersyncPublicationTables = commaSeparated2(next());
13810
14450
  else if (argument === "--replication-tls-cert")
13811
- options.replicationTlsCertFile = resolve5(next());
14451
+ options.replicationTlsCertFile = resolve6(next());
13812
14452
  else if (argument === "--replication-tls-key")
13813
- options.replicationTlsKeyFile = resolve5(next());
14453
+ options.replicationTlsKeyFile = resolve6(next());
13814
14454
  else if (argument === "--memory")
13815
14455
  options.memory = true;
13816
14456
  else if (argument === "--output" || argument === "-o")
13817
- options.output = resolve5(next());
14457
+ options.output = resolve6(next());
13818
14458
  else if (argument === "--file" || argument === "-f")
13819
14459
  options.diffFile = next();
14460
+ else if (argument === "--module-file")
14461
+ options.moduleFile = resolve6(next());
13820
14462
  else if (argument === "--service-role")
13821
14463
  options.serviceRole = true;
13822
14464
  else if (argument === "--force")
@@ -13884,7 +14526,7 @@ ${privilegedKey}
13884
14526
  try {
13885
14527
  const source = await generateTypes(project2.backend.db, "public");
13886
14528
  if (options.output) {
13887
- await mkdir7(dirname7(options.output), { recursive: true });
14529
+ await mkdir7(dirname8(options.output), { recursive: true });
13888
14530
  await writeFile7(options.output, source);
13889
14531
  await writeStandardOutput(`Wrote ${options.output}
13890
14532
  `);
@@ -14003,7 +14645,7 @@ async function runDbCommand(options) {
14003
14645
  }
14004
14646
  return;
14005
14647
  }
14006
- const project = await loadSupabaseProject(resolve5(options.projectDir ?? process.cwd()));
14648
+ const project = await loadSupabaseProject(resolve6(options.projectDir ?? process.cwd()));
14007
14649
  if (subcommand === "diff") {
14008
14650
  const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
14009
14651
  dataDir: paths.dataDir,
@@ -14058,6 +14700,29 @@ async function runDbCommand(options) {
14058
14700
  `);
14059
14701
  return;
14060
14702
  }
14703
+ if (subcommand === "check") {
14704
+ const moduleFile = options.moduleFile ?? join10(paths.projectDir, "supabase", "db", "modules.ts");
14705
+ const project2 = await createProjectBackend({
14706
+ ...options,
14707
+ applyMigrations: false,
14708
+ includeFunctions: false,
14709
+ includeWebhooks: false,
14710
+ startRuntimeServices: false,
14711
+ log: quietLog
14712
+ });
14713
+ try {
14714
+ const executor = {
14715
+ query: async (sql, params) => (await project2.backend.db.query(sql, params)).rows
14716
+ };
14717
+ const result = await checkDatabaseModules({ moduleFile, executor });
14718
+ await writeStandardOutput(formatDatabaseModuleCheck(result));
14719
+ if (!result.ok)
14720
+ throw new Error("db check found error-level issues");
14721
+ } finally {
14722
+ await project2.backend.close();
14723
+ }
14724
+ return;
14725
+ }
14061
14726
  throw new Error(`unknown db subcommand: ${subcommand ?? "(none)"}`);
14062
14727
  }
14063
14728
  async function runSnapshotCommand(options) {
@@ -14086,7 +14751,7 @@ async function runSnapshotCommand(options) {
14086
14751
  const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
14087
14752
  const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
14088
14753
  await writeStandardOutput([
14089
- `Snapshot restored from ${resolve5(input)}`,
14754
+ `Snapshot restored from ${resolve6(input)}`,
14090
14755
  ...rollbackLines,
14091
14756
  ...reconnectLine
14092
14757
  ].join(`
@@ -14183,6 +14848,7 @@ Commands:
14183
14848
  db reset reset initialized database/storage and re-run migrations
14184
14849
  db diff print schema changes outside migrations
14185
14850
  db pull [name] write live schema changes as an applied migration
14851
+ db check reconcile database module manifests (@supacloud/db) against the live catalog
14186
14852
  snapshot create create a compressed database/storage/secrets snapshot
14187
14853
  snapshot restore <f> restore a snapshot into an empty target
14188
14854
  upgrade snapshot first, then apply pending migrations
@@ -14215,6 +14881,7 @@ Options:
14215
14881
  --json emit machine-readable doctor output
14216
14882
  -o, --output <p> output file for gen types
14217
14883
  -f, --file <name> migration suffix for db diff
14884
+ --module-file <p> database module manifest for db check (default supabase/db/modules.ts)
14218
14885
  --force replace non-empty restore targets and retain rollback copies
14219
14886
  `);
14220
14887
  }