@supacloud/lite 0.10.0 → 0.12.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.10.0",
12
+ version: "0.12.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,6 +59,7 @@ var package_default = {
59
59
  },
60
60
  dependencies: {
61
61
  "@electric-sql/pglite": "0.5.8",
62
+ "@supacloud/db": "^0.2.0",
62
63
  tar: "^7.5.22"
63
64
  },
64
65
  devDependencies: {
@@ -2525,8 +2526,99 @@ function resetCapturedHandler() {
2525
2526
  captured.handler = undefined;
2526
2527
  }
2527
2528
 
2528
- // src/runtime/functions/pgredis.ts
2529
+ // src/runtime/functions/edge-runtime-shim.ts
2529
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";
2530
2622
  var CACHE_NAMESPACE = "supacloud-edge-runtime";
2531
2623
  var CACHE_TABLE = "public.supacloud_pgredis_kv";
2532
2624
  var MAX_KEY_CHARACTERS = 512;
@@ -2619,7 +2711,7 @@ class PgredisCache {
2619
2711
  });
2620
2712
  }
2621
2713
  }
2622
- var cacheContexts = new AsyncLocalStorage2;
2714
+ var cacheContexts = new AsyncLocalStorage4;
2623
2715
  var cacheFacade = Object.freeze({
2624
2716
  get: async (key) => activeCache().get(key),
2625
2717
  set: async (key, cacheValue, ttlMs) => activeCache().set(key, cacheValue, ttlMs),
@@ -2693,6 +2785,23 @@ async function upsertWithoutTtl(query, key, serializedValue) {
2693
2785
  }
2694
2786
 
2695
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
+ }
2696
2805
  function isLoadedFunction(value) {
2697
2806
  return typeof value === "object" && value !== null && "handler" in value;
2698
2807
  }
@@ -2705,9 +2814,7 @@ function isFrameworkRouterHandler(handler) {
2705
2814
  const candidate = handler;
2706
2815
  if (candidate.__supacloud?.routeAware === true)
2707
2816
  return true;
2708
- if (typeof candidate.handle === "function")
2709
- return true;
2710
- return Array.isArray(candidate.routes) && typeof candidate.fetch === "function";
2817
+ return Array.isArray(candidate.routes) && (typeof candidate.handle === "function" || typeof candidate.fetch === "function");
2711
2818
  }
2712
2819
  function toFunctionLocalUrl(requestUrl) {
2713
2820
  const url = new URL(requestUrl);
@@ -2748,23 +2855,68 @@ class FunctionsHandler {
2748
2855
  return json2(404, { error: `function "${name}" not found` });
2749
2856
  }
2750
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
+ }
2751
2871
  const routeAware = entry.framework !== undefined && entry.framework !== "fetch" || isFrameworkRouterHandler(entry.handler);
2752
- const target = routeAware ? new Request(toFunctionLocalUrl(req.url), req) : req;
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);
2753
2879
  try {
2754
- const invoke = () => this.invoke(entry.handler, target, ctx);
2755
- 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();
2756
2886
  if (!(res instanceof Response)) {
2757
2887
  return json2(500, { error: `function "${name}" did not return a Response` });
2758
2888
  }
2759
- return res;
2889
+ const maxResponse = limits?.maxResponseBodyBytes;
2890
+ return maxResponse !== undefined ? withResponseLimit(name, res, maxResponse) : res;
2760
2891
  } catch (e) {
2761
2892
  const message = e instanceof Error ? e.message : String(e);
2762
2893
  return json2(500, { error: message });
2763
2894
  }
2764
2895
  }
2765
- invoke(handler, req, ctx) {
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) {
2766
2918
  if (typeof handler === "function") {
2767
- return Promise.resolve(handler(req, { auth: ctx, env: this.env }));
2919
+ return Promise.resolve(handler(req, { auth: ctx, env }));
2768
2920
  }
2769
2921
  if (typeof handler.handle === "function") {
2770
2922
  return Promise.resolve(handler.handle.call(handler, req));
@@ -2775,6 +2927,54 @@ class FunctionsHandler {
2775
2927
  throw new Error("function handler must be a function or an object with handle()/fetch()");
2776
2928
  }
2777
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" } : {} });
2977
+ }
2778
2978
  function json2(status, body) {
2779
2979
  return new Response(JSON.stringify(body), {
2780
2980
  status,
@@ -10022,9 +10222,9 @@ class RetentionService {
10022
10222
  }
10023
10223
 
10024
10224
  // src/runtime/security.ts
10025
- 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]);
10026
10226
  function isNetworkExposed(host) {
10027
- return !LOOPBACK_HOSTS.has(host);
10227
+ return !LOOPBACK_HOSTS2.has(host);
10028
10228
  }
10029
10229
  function assertSecretsSafe(input) {
10030
10230
  const { host, jwtSecret, vaultKeyDerived, warn } = input;
@@ -11958,6 +12158,393 @@ async function closeResources(...resources) {
11958
12158
  throw failed.reason;
11959
12159
  }
11960
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 TRIGGERS_SQL = `
12208
+ SELECT n.nspname AS schema,
12209
+ c.relname AS table,
12210
+ t.tgname AS name,
12211
+ t.tgenabled <> 'D' AS enabled
12212
+ FROM pg_trigger t
12213
+ JOIN pg_class c ON c.oid = t.tgrelid
12214
+ JOIN pg_namespace n ON n.oid = c.relnamespace
12215
+ WHERE NOT t.tgisinternal
12216
+ AND n.nspname = ANY($1)
12217
+ ORDER BY n.nspname, c.relname, t.tgname
12218
+ `;
12219
+ var GRANTS_SQL = `
12220
+ SELECT table_schema AS object_schema,
12221
+ table_name AS object_name,
12222
+ privilege_type AS privilege,
12223
+ grantee
12224
+ FROM information_schema.role_table_grants
12225
+ WHERE table_schema = ANY($1)
12226
+ UNION ALL
12227
+ SELECT routine_schema AS object_schema,
12228
+ routine_name AS object_name,
12229
+ privilege_type AS privilege,
12230
+ grantee
12231
+ FROM information_schema.routine_privileges
12232
+ WHERE routine_schema = ANY($1)
12233
+ `;
12234
+ var POLCMD_MAP = {
12235
+ r: "select",
12236
+ a: "insert",
12237
+ w: "update",
12238
+ d: "delete",
12239
+ "*": "all"
12240
+ };
12241
+ function extractSearchPath(config) {
12242
+ if (!config)
12243
+ return null;
12244
+ const entry = config.find((item) => item.startsWith("search_path="));
12245
+ if (!entry)
12246
+ return null;
12247
+ return entry.slice("search_path=".length);
12248
+ }
12249
+ async function readCatalog(executor, schemas = ["public"]) {
12250
+ const params = [schemas];
12251
+ const [tableRows, policyRows, functionRows, triggerRows, grantRows] = await Promise.all([
12252
+ executor.query(TABLES_SQL, params),
12253
+ executor.query(POLICIES_SQL, params),
12254
+ executor.query(FUNCTIONS_SQL, params),
12255
+ executor.query(TRIGGERS_SQL, params),
12256
+ executor.query(GRANTS_SQL, params)
12257
+ ]);
12258
+ return {
12259
+ tables: tableRows.map((row) => ({
12260
+ schema: row.schema,
12261
+ name: row.name,
12262
+ rlsEnabled: row.rls_enabled,
12263
+ rlsForced: row.rls_forced
12264
+ })),
12265
+ policies: policyRows.map((row) => ({
12266
+ schema: row.schema,
12267
+ table: row.table,
12268
+ name: row.name,
12269
+ command: POLCMD_MAP[row.command] ?? "all",
12270
+ roles: row.roles ?? [],
12271
+ usingExpr: row.using_expr ?? undefined,
12272
+ checkExpr: row.check_expr ?? undefined
12273
+ })),
12274
+ functions: functionRows.map((row) => ({
12275
+ schema: row.schema,
12276
+ name: row.name,
12277
+ security: row.security_definer ? "definer" : "invoker",
12278
+ searchPath: extractSearchPath(row.config),
12279
+ language: row.language
12280
+ })),
12281
+ triggers: triggerRows.map((row) => ({
12282
+ schema: row.schema,
12283
+ table: row.table,
12284
+ name: row.name,
12285
+ enabled: row.enabled
12286
+ })),
12287
+ grants: grantRows.map((row) => ({
12288
+ objectSchema: row.object_schema,
12289
+ objectName: row.object_name,
12290
+ privilege: row.privilege,
12291
+ grantee: row.grantee
12292
+ }))
12293
+ };
12294
+ }
12295
+ function splitQualifiedName(name) {
12296
+ const dot = name.indexOf(".");
12297
+ if (dot === -1)
12298
+ return ["public", name];
12299
+ return [name.slice(0, dot), name.slice(dot + 1)];
12300
+ }
12301
+ function isFixedSearchPath(searchPath) {
12302
+ if (searchPath === null)
12303
+ return false;
12304
+ const parts = searchPath.split(",").map((part) => part.trim().replace(/^"|"$/g, ""));
12305
+ return parts.every((part) => part !== "" && part.toLowerCase() !== "pg_temp");
12306
+ }
12307
+ function reconcileModule(module, catalog) {
12308
+ const issues = [];
12309
+ const ownedTables = new Set(module.tables);
12310
+ const push = (severity, code, object, message2) => issues.push({ severity, code, object, message: message2 });
12311
+ for (const policy of module.policies) {
12312
+ const [schema, table] = splitQualifiedName(policy.table);
12313
+ const found = catalog.policies.some((cp) => cp.schema === schema && cp.table === table && cp.name === policy.name);
12314
+ if (!found) {
12315
+ 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`);
12316
+ }
12317
+ }
12318
+ const declaredPolicyKeys = new Set(module.policies.map((p) => `${p.table}::${p.name}`));
12319
+ for (const cp of catalog.policies) {
12320
+ const qualified = `${cp.schema}.${cp.table}`;
12321
+ if (ownedTables.has(qualified) && !declaredPolicyKeys.has(`${qualified}::${cp.name}`)) {
12322
+ 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`);
12323
+ }
12324
+ }
12325
+ for (const fn of module.functions) {
12326
+ const [schema, name] = splitQualifiedName(fn.name);
12327
+ const cf = catalog.functions.find((f) => f.schema === schema && f.name === name);
12328
+ if (!cf) {
12329
+ push("error", "missing-function", fn.name, `\u58F0\u660E\u7684\u51FD\u6570 ${fn.name} \u5728 catalog \u4E2D\u4E0D\u5B58\u5728`);
12330
+ continue;
12331
+ }
12332
+ if (cf.security !== fn.security) {
12333
+ push("warn", "security-mismatch", fn.name, `\u51FD\u6570 ${fn.name} \u58F0\u660E\u4E3A security ${fn.security}\uFF0Ccatalog \u5B9E\u9645\u4E3A ${cf.security}`);
12334
+ }
12335
+ const effectiveDefiner = fn.security === "definer" || cf.security === "definer";
12336
+ if (effectiveDefiner && !isFixedSearchPath(cf.searchPath)) {
12337
+ 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`);
12338
+ }
12339
+ }
12340
+ for (const trigger of module.triggers) {
12341
+ const [schema, table] = splitQualifiedName(trigger.table);
12342
+ const found = catalog.triggers.some((ct) => ct.schema === schema && ct.table === table && ct.name === trigger.name);
12343
+ if (!found) {
12344
+ push("error", "missing-trigger", `${trigger.table}.${trigger.name}`, `\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${trigger.name} \u5728\u8868 ${trigger.table} \u7684 catalog \u4E2D\u4E0D\u5B58\u5728`);
12345
+ }
12346
+ }
12347
+ const declaredTriggerKeys = new Set(module.triggers.map((t) => `${t.table}::${t.name}`));
12348
+ for (const ct of catalog.triggers) {
12349
+ const qualified = `${ct.schema}.${ct.table}`;
12350
+ if (ownedTables.has(qualified) && !declaredTriggerKeys.has(`${qualified}::${ct.name}`)) {
12351
+ push("warn", "undeclared-trigger", `${qualified}.${ct.name}`, `\u5F52\u5C5E\u8868 ${qualified} \u4E0A\u5B58\u5728\u672A\u58F0\u660E\u7684\u89E6\u53D1\u5668 ${ct.name}${ct.enabled ? "" : "\uFF08\u5DF2\u7981\u7528\uFF09"}\uFF0C\u53EF\u80FD\u53D1\u751F\u6F02\u79FB`);
12352
+ }
12353
+ }
12354
+ for (const table of module.tables) {
12355
+ const [schema, name] = splitQualifiedName(table);
12356
+ const ct = catalog.tables.find((t) => t.schema === schema && t.name === name);
12357
+ if (ct && !ct.rlsEnabled) {
12358
+ push("error", "rls-disabled", table, `\u5F52\u5C5E\u8868 ${table} \u672A\u5F00\u542F\u884C\u7EA7\u5B89\u5168\uFF08relrowsecurity = false\uFF09`);
12359
+ }
12360
+ }
12361
+ for (const grant of catalog.grants) {
12362
+ const qualified = `${grant.objectSchema}.${grant.objectName}`;
12363
+ if (ownedTables.has(qualified) && grant.grantee.toUpperCase() === "PUBLIC") {
12364
+ push("error", "wildcard-grant", qualified, `\u5F52\u5C5E\u8868 ${qualified} \u5B58\u5728\u6388\u4E88 PUBLIC \u7684 ${grant.privilege} \u6743\u9650`);
12365
+ }
12366
+ }
12367
+ for (const grant of module.grants) {
12368
+ const [schema, name] = splitQualifiedName(grant.object);
12369
+ const found = catalog.grants.some((cg) => cg.objectSchema === schema && cg.objectName === name && cg.privilege.toLowerCase() === grant.privilege.toLowerCase() && cg.grantee.toLowerCase() === grant.role.toLowerCase());
12370
+ if (!found) {
12371
+ 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`);
12372
+ }
12373
+ }
12374
+ return {
12375
+ module: module.name,
12376
+ issues,
12377
+ ok: !issues.some((issue) => issue.severity === "error")
12378
+ };
12379
+ }
12380
+ var SECURITY_DEFINER_RE = /\bsecurity\s+definer\b/i;
12381
+ var SET_SEARCH_PATH_RE = /\bset\s+search_path\b/i;
12382
+ var GRANT_TO_PUBLIC_RE = /\bgrant\b[^;]*\bto\s+public\b/i;
12383
+ var DROP_WITHOUT_IF_EXISTS_RE = /\bdrop\s+(?:table|column)\s+(?!if\s+exists\b)/i;
12384
+ var ENABLE_RLS_RE = /\benable\s+row\s+level\s+security\b/i;
12385
+ var CREATE_POLICY_RE = /\bcreate\s+policy\b/i;
12386
+ var DROP_POLICY_IF_EXISTS_RE = /\bdrop\s+policy\s+if\s+exists\b/i;
12387
+ function lineOf(sql, index) {
12388
+ let line = 1;
12389
+ for (let i = 0;i < index; i += 1) {
12390
+ if (sql.charCodeAt(i) === 10)
12391
+ line += 1;
12392
+ }
12393
+ return line;
12394
+ }
12395
+ function lintSql(sql, file) {
12396
+ const issues = [];
12397
+ const definer = SECURITY_DEFINER_RE.exec(sql);
12398
+ if (definer && !SET_SEARCH_PATH_RE.test(sql)) {
12399
+ issues.push({
12400
+ severity: "error",
12401
+ code: "definer-no-search-path",
12402
+ message: "security definer \u51FD\u6570\u5FC5\u987B\u663E\u5F0F set search_path\uFF0C\u907F\u514D search_path \u52AB\u6301",
12403
+ file,
12404
+ line: lineOf(sql, definer.index)
12405
+ });
12406
+ }
12407
+ const grantPublic = GRANT_TO_PUBLIC_RE.exec(sql);
12408
+ if (grantPublic) {
12409
+ issues.push({
12410
+ severity: "error",
12411
+ code: "grant-to-public",
12412
+ message: "\u7981\u6B62\u5C06\u6743\u9650\u6388\u4E88 PUBLIC \u89D2\u8272",
12413
+ file,
12414
+ line: lineOf(sql, grantPublic.index)
12415
+ });
12416
+ }
12417
+ const drop = DROP_WITHOUT_IF_EXISTS_RE.exec(sql);
12418
+ if (drop) {
12419
+ issues.push({
12420
+ severity: "warn",
12421
+ code: "drop-without-if-exists",
12422
+ message: "drop table/column \u5EFA\u8BAE\u4F7F\u7528 if exists\uFF0C\u4FDD\u8BC1\u8FC1\u79FB\u53EF\u91CD\u5165",
12423
+ file,
12424
+ line: lineOf(sql, drop.index)
12425
+ });
12426
+ }
12427
+ const createPolicy = CREATE_POLICY_RE.exec(sql);
12428
+ if (createPolicy) {
12429
+ const dropPolicy = DROP_POLICY_IF_EXISTS_RE.exec(sql);
12430
+ if (!dropPolicy || dropPolicy.index > createPolicy.index) {
12431
+ issues.push({
12432
+ severity: "warn",
12433
+ code: "non-idempotent-policy",
12434
+ message: "create policy \u524D\u7F3A\u5C11 drop policy if exists\uFF0C\u7B56\u7565\u4E0D\u53EF\u91CD\u590D\u6267\u884C",
12435
+ file,
12436
+ line: lineOf(sql, createPolicy.index)
12437
+ });
12438
+ }
12439
+ }
12440
+ return issues;
12441
+ }
12442
+ async function lintModule(module, readFile2) {
12443
+ const issues = [];
12444
+ const sources = new Set;
12445
+ for (const decl of [
12446
+ ...module.policies,
12447
+ ...module.functions,
12448
+ ...module.triggers,
12449
+ ...module.grants
12450
+ ]) {
12451
+ sources.add(decl.source);
12452
+ }
12453
+ const contents = new Map;
12454
+ await Promise.all([...sources].map(async (path) => {
12455
+ contents.set(path, await readFile2(path));
12456
+ }));
12457
+ for (const [file, sql] of contents) {
12458
+ issues.push(...lintSql(sql, file));
12459
+ }
12460
+ if (module.policies.length > 0) {
12461
+ const anyEnable = module.policies.some((policy) => ENABLE_RLS_RE.test(contents.get(policy.source) ?? ""));
12462
+ if (!anyEnable) {
12463
+ issues.push({
12464
+ severity: "warn",
12465
+ code: "missing-rls-enable",
12466
+ 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`,
12467
+ file: module.policies[0].source
12468
+ });
12469
+ }
12470
+ }
12471
+ for (const policy of module.policies) {
12472
+ if (!policy.tests || policy.tests.length === 0) {
12473
+ issues.push({
12474
+ severity: "warn",
12475
+ code: "policy-without-test",
12476
+ message: `\u7B56\u7565 ${policy.name} \u672A\u58F0\u660E\u6D4B\u8BD5\u6587\u4EF6`,
12477
+ file: policy.source
12478
+ });
12479
+ }
12480
+ }
12481
+ for (const fn of module.functions) {
12482
+ if (!fn.tests || fn.tests.length === 0) {
12483
+ issues.push({
12484
+ severity: "warn",
12485
+ code: "policy-without-test",
12486
+ message: `\u51FD\u6570 ${fn.name} \u672A\u58F0\u660E\u6D4B\u8BD5\u6587\u4EF6`,
12487
+ file: fn.source
12488
+ });
12489
+ }
12490
+ }
12491
+ return issues;
12492
+ }
12493
+
12494
+ // src/runtime/node/db-check.ts
12495
+ async function loadDatabaseModules(moduleFile) {
12496
+ const absolute = resolve3(moduleFile);
12497
+ try {
12498
+ await stat(absolute);
12499
+ } catch {
12500
+ throw new Error(`database module manifest not found: ${absolute}`);
12501
+ }
12502
+ const mod = await import(pathToFileURL(absolute).href);
12503
+ const candidate = mod.default ?? mod.modules;
12504
+ const list = Array.isArray(candidate) ? candidate : candidate ? [candidate] : [];
12505
+ if (list.length === 0) {
12506
+ throw new Error(`database module manifest ${absolute} must export a default module or module array`);
12507
+ }
12508
+ for (const entry of list) {
12509
+ if (!entry || typeof entry.name !== "string") {
12510
+ throw new Error(`database module manifest ${absolute} contains an entry without a name`);
12511
+ }
12512
+ }
12513
+ return list;
12514
+ }
12515
+ async function checkDatabaseModules(options) {
12516
+ const modules = await loadDatabaseModules(options.moduleFile);
12517
+ const baseDir = dirname3(resolve3(options.moduleFile));
12518
+ const readSource = (path) => readFile2(resolve3(baseDir, path), "utf8");
12519
+ const catalog = await readCatalog(options.executor, [options.schema ?? "public"]);
12520
+ const reports = [];
12521
+ for (const module of modules) {
12522
+ const lintIssues = await lintModule(module, readSource);
12523
+ const reconcile = reconcileModule(module, catalog);
12524
+ reports.push({ module: module.name, lintIssues, reconcile });
12525
+ }
12526
+ const ok = reports.every((report) => report.reconcile.ok && !report.lintIssues.some((issue) => issue.severity === "error"));
12527
+ return { ok, reports };
12528
+ }
12529
+ function formatDatabaseModuleCheck(result) {
12530
+ const lines = [];
12531
+ for (const report of result.reports) {
12532
+ lines.push(`module ${report.module}:`);
12533
+ for (const issue of report.lintIssues) {
12534
+ lines.push(` [lint ${issue.severity}] ${issue.code}: ${issue.message} (${issue.file})`);
12535
+ }
12536
+ for (const issue of report.reconcile.issues) {
12537
+ lines.push(` [catalog ${issue.severity}] ${issue.code}: ${issue.message} (${issue.object})`);
12538
+ }
12539
+ if (report.lintIssues.length === 0 && report.reconcile.issues.length === 0) {
12540
+ lines.push(" ok");
12541
+ }
12542
+ }
12543
+ return `${lines.join(`
12544
+ `)}
12545
+ `;
12546
+ }
12547
+
11961
12548
  // src/runtime/node/native/readiness.ts
11962
12549
  function liteCapabilities(engine, replicationProfile) {
11963
12550
  if (engine === "pglite") {
@@ -12189,7 +12776,7 @@ function sameStrings(left, right) {
12189
12776
  }
12190
12777
 
12191
12778
  // src/runtime/node/project.ts
12192
- import { readdir, readFile as readFile2 } from "fs/promises";
12779
+ import { readdir, readFile as readFile3 } from "fs/promises";
12193
12780
  import { join as join3 } from "path";
12194
12781
  async function loadSupabaseProject(projectDir, seed = {}) {
12195
12782
  const migrationsDir = join3(projectDir, "supabase", "migrations");
@@ -12204,7 +12791,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12204
12791
  for (const entry of entries.sort()) {
12205
12792
  if (!entry.endsWith(".sql"))
12206
12793
  continue;
12207
- const sql = await readFile2(join3(migrationsDir, entry), "utf8");
12794
+ const sql = await readFile3(join3(migrationsDir, entry), "utf8");
12208
12795
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
12209
12796
  }
12210
12797
  let seedSql;
@@ -12216,7 +12803,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12216
12803
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
12217
12804
  for (const relativePath of matches) {
12218
12805
  try {
12219
- parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
12806
+ parts.push(await readFile3(join3(supabaseDir, relativePath), "utf8"));
12220
12807
  } catch (error) {
12221
12808
  if (!isNotFound(error))
12222
12809
  throw error;
@@ -12234,8 +12821,8 @@ function isNotFound(error) {
12234
12821
  }
12235
12822
 
12236
12823
  // src/project-runtime.ts
12237
- import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
12238
- import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve3 } from "path";
12824
+ import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
12825
+ import { dirname as dirname6, isAbsolute, join as join8, parse, relative, resolve as resolve4 } from "path";
12239
12826
 
12240
12827
  // src/runtime/node/bun-server.ts
12241
12828
  async function serveBun(backend, opts = {}) {
@@ -12285,8 +12872,8 @@ async function serveBun(backend, opts = {}) {
12285
12872
  }
12286
12873
 
12287
12874
  // src/runtime/node/fs-driver.ts
12288
- import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
12289
- import { dirname as dirname3, join as join4, normalize, sep } from "path";
12875
+ import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
12876
+ import { dirname as dirname4, join as join4, normalize, sep } from "path";
12290
12877
 
12291
12878
  class FsStorageDriver {
12292
12879
  root;
@@ -12302,7 +12889,7 @@ class FsStorageDriver {
12302
12889
  }
12303
12890
  async put(key, data) {
12304
12891
  const path = this.resolve(key);
12305
- await mkdir3(dirname3(path), { recursive: true });
12892
+ await mkdir3(dirname4(path), { recursive: true });
12306
12893
  const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
12307
12894
  try {
12308
12895
  await writeFile3(temporaryPath, data);
@@ -12314,7 +12901,7 @@ class FsStorageDriver {
12314
12901
  }
12315
12902
  async get(key) {
12316
12903
  try {
12317
- return new Uint8Array(await readFile3(this.resolve(key)));
12904
+ return new Uint8Array(await readFile4(this.resolve(key)));
12318
12905
  } catch (e) {
12319
12906
  if (e.code === "ENOENT")
12320
12907
  return null;
@@ -12710,19 +13297,40 @@ function readFunctions(root) {
12710
13297
  console.warn(` warning: [functions.${name}] framework "${framework}" is not one of fetch/elysia/hono; falling back to fetch`);
12711
13298
  }
12712
13299
  }
13300
+ const timeoutMs = getInt(t, "timeout_ms");
13301
+ if (timeoutMs !== undefined && timeoutMs > 0)
13302
+ opts.timeoutMs = timeoutMs;
13303
+ const maxRequestBodyBytes = getInt(t, "max_request_body_bytes");
13304
+ if (maxRequestBodyBytes !== undefined && maxRequestBodyBytes > 0)
13305
+ opts.maxRequestBodyBytes = maxRequestBodyBytes;
13306
+ const maxResponseBodyBytes = getInt(t, "max_response_body_bytes");
13307
+ if (maxResponseBodyBytes !== undefined && maxResponseBodyBytes > 0)
13308
+ opts.maxResponseBodyBytes = maxResponseBodyBytes;
13309
+ const waitUntilTimeoutMs = getInt(t, "wait_until_timeout_ms");
13310
+ if (waitUntilTimeoutMs !== undefined && waitUntilTimeoutMs > 0)
13311
+ opts.waitUntilTimeoutMs = waitUntilTimeoutMs;
13312
+ const outboundHosts = getStringArray(t, "outbound_hosts");
13313
+ if (outboundHosts !== undefined)
13314
+ opts.outboundHosts = outboundHosts;
13315
+ const secrets = getStringArray(t, "secrets");
13316
+ if (secrets !== undefined)
13317
+ opts.secrets = secrets;
13318
+ const background = getBool(t, "background");
13319
+ if (background !== undefined)
13320
+ opts.background = background;
12713
13321
  out[name] = opts;
12714
13322
  }
12715
13323
  return out;
12716
13324
  }
12717
13325
 
12718
13326
  // src/runtime/node/load-functions.ts
12719
- import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
12720
- import { dirname as dirname4, join as join7 } from "path";
12721
- import { pathToFileURL } from "url";
13327
+ import { readdir as readdir2, readFile as readFile6, realpath, rm as rm4, stat as stat2 } from "fs/promises";
13328
+ import { dirname as dirname5, join as join7 } from "path";
13329
+ import { pathToFileURL as pathToFileURL2 } from "url";
12722
13330
 
12723
13331
  // src/runtime/node/bundle-function.ts
12724
13332
  import { createHash as createHash3 } from "crypto";
12725
- import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
13333
+ import { mkdir as mkdir4, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
12726
13334
  import { existsSync as existsSync3 } from "fs";
12727
13335
  import { tmpdir as tmpdir3 } from "os";
12728
13336
  import { join as join6 } from "path";
@@ -12738,7 +13346,7 @@ async function fetchModule(url) {
12738
13346
  const key = createHash3("sha256").update(url).digest("hex");
12739
13347
  const cached = join6(HTTP_CACHE, key);
12740
13348
  if (existsSync3(cached))
12741
- return readFile4(cached, "utf8");
13349
+ return readFile5(cached, "utf8");
12742
13350
  const res = await fetch(url, { redirect: "follow" });
12743
13351
  if (!res.ok)
12744
13352
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
@@ -12797,7 +13405,7 @@ async function bundleFunction(entryPath, name) {
12797
13405
  async function loadFunctionEnv(projectDir) {
12798
13406
  let text;
12799
13407
  try {
12800
- text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
13408
+ text = await readFile6(join7(projectDir, "supabase", "functions", ".env"), "utf8");
12801
13409
  } catch {
12802
13410
  return {};
12803
13411
  }
@@ -12859,12 +13467,12 @@ async function loadFunctionsUnlocked(projectDir, options) {
12859
13467
  if (options[name]?.enabled === false)
12860
13468
  continue;
12861
13469
  const dir = join7(root, name);
12862
- if (!(await stat(dir)).isDirectory())
13470
+ if (!(await stat2(dir)).isDirectory())
12863
13471
  continue;
12864
13472
  const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
12865
13473
  for (const path of candidates) {
12866
13474
  try {
12867
- await stat(path);
13475
+ await stat2(path);
12868
13476
  } catch {
12869
13477
  continue;
12870
13478
  }
@@ -12873,11 +13481,11 @@ async function loadFunctionsUnlocked(projectDir, options) {
12873
13481
  let importUrl;
12874
13482
  try {
12875
13483
  bundledPath = await realpath(await bundleFunction(path, `${name}-${crypto.randomUUID()}`));
12876
- importUrl = pathToFileURL(bundledPath).href;
13484
+ importUrl = pathToFileURL2(bundledPath).href;
12877
13485
  } catch (e) {
12878
13486
  if (e.message !== "esbuild-not-available")
12879
13487
  throw e;
12880
- importUrl = pathToFileURL(path).href;
13488
+ importUrl = pathToFileURL2(path).href;
12881
13489
  }
12882
13490
  resetCapturedHandler();
12883
13491
  const mod = await import(importUrl);
@@ -12885,9 +13493,23 @@ async function loadFunctionsUnlocked(projectDir, options) {
12885
13493
  const defaultExport = mod.default;
12886
13494
  const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && (typeof defaultExport.handle === "function" || typeof defaultExport.fetch === "function") ? defaultExport : denoHandler ? (req) => denoHandler(req) : undefined;
12887
13495
  if (handler) {
13496
+ const opts = options[name];
13497
+ const limits = opts && (opts.timeoutMs !== undefined || opts.maxRequestBodyBytes !== undefined || opts.maxResponseBodyBytes !== undefined || opts.waitUntilTimeoutMs !== undefined) ? {
13498
+ ...opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {},
13499
+ ...opts.maxRequestBodyBytes !== undefined ? { maxRequestBodyBytes: opts.maxRequestBodyBytes } : {},
13500
+ ...opts.maxResponseBodyBytes !== undefined ? { maxResponseBodyBytes: opts.maxResponseBodyBytes } : {},
13501
+ ...opts.waitUntilTimeoutMs !== undefined ? { waitUntilTimeoutMs: opts.waitUntilTimeoutMs } : {}
13502
+ } : undefined;
13503
+ const capabilities = opts && (opts.outboundHosts !== undefined || opts.secrets !== undefined || opts.background !== undefined) ? {
13504
+ ...opts.outboundHosts !== undefined ? { outboundHosts: opts.outboundHosts } : {},
13505
+ ...opts.secrets !== undefined ? { secrets: opts.secrets } : {},
13506
+ ...opts.background !== undefined ? { background: opts.background } : {}
13507
+ } : undefined;
12888
13508
  functions.set(name, {
12889
13509
  handler,
12890
- framework: resolveFramework(name, options[name]?.framework)
13510
+ framework: resolveFramework(name, opts?.framework),
13511
+ ...limits ? { limits } : {},
13512
+ ...capabilities ? { capabilities } : {}
12891
13513
  });
12892
13514
  } else {
12893
13515
  console.warn(` warning: function "${name}" has no default function, handle/fetch object, or Deno.serve() handler, skipped`);
@@ -12901,7 +13523,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
12901
13523
  }
12902
13524
  } finally {
12903
13525
  if (bundledPath)
12904
- await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
13526
+ await rm4(dirname5(bundledPath), { recursive: true, force: true }).catch(() => {});
12905
13527
  }
12906
13528
  break;
12907
13529
  }
@@ -13007,7 +13629,7 @@ class S3StorageDriver {
13007
13629
  var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
13008
13630
  var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
13009
13631
  function resolveProjectPaths(options = {}) {
13010
- const projectDir = resolve3(options.projectDir ?? process.cwd());
13632
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
13011
13633
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
13012
13634
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
13013
13635
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -13022,7 +13644,7 @@ function resolveProjectPaths(options = {}) {
13022
13644
  };
13023
13645
  }
13024
13646
  async function assertResetPathsSafe(paths) {
13025
- const stateDir = resolve3(paths.stateDir);
13647
+ const stateDir = resolve4(paths.stateDir);
13026
13648
  if (stateDir === parse(stateDir).root)
13027
13649
  throw new Error("refusing to use the filesystem root as the state directory");
13028
13650
  const stateInfo = await requiredResetEntry(stateDir);
@@ -13030,7 +13652,7 @@ async function assertResetPathsSafe(paths) {
13030
13652
  throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
13031
13653
  }
13032
13654
  const canonicalStateDir = await realpath2(stateDir);
13033
- const secretsFile = resolve3(paths.secretsFile);
13655
+ const secretsFile = resolve4(paths.secretsFile);
13034
13656
  if (secretsFile !== join8(stateDir, "secrets.json")) {
13035
13657
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
13036
13658
  }
@@ -13044,7 +13666,7 @@ async function assertResetPathsSafe(paths) {
13044
13666
  ["storage", paths.storageDir]
13045
13667
  ];
13046
13668
  for (const [label, targetPath] of targets) {
13047
- const target2 = resolve3(targetPath);
13669
+ const target2 = resolve4(targetPath);
13048
13670
  const relativePath = relative(stateDir, target2);
13049
13671
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
13050
13672
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
@@ -13064,7 +13686,7 @@ async function requiredResetEntry(path) {
13064
13686
  async function assertResetSecretsValid(path) {
13065
13687
  let serializedSecrets;
13066
13688
  try {
13067
- serializedSecrets = await readFile6(path, "utf8");
13689
+ serializedSecrets = await readFile7(path, "utf8");
13068
13690
  } catch (error) {
13069
13691
  if (error.code === "ENOENT")
13070
13692
  throw new Error(RESET_INITIALIZATION_ERROR);
@@ -13087,13 +13709,13 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
13087
13709
  if (error.code !== "ENOENT")
13088
13710
  throw error;
13089
13711
  }
13090
- const parent = dirname5(current);
13712
+ const parent = dirname6(current);
13091
13713
  if (parent === current)
13092
13714
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
13093
13715
  current = parent;
13094
13716
  }
13095
13717
  const existingAncestor = await nearestExistingAncestor(target2);
13096
- const canonicalTarget = resolve3(await realpath2(existingAncestor), relative(existingAncestor, target2));
13718
+ const canonicalTarget = resolve4(await realpath2(existingAncestor), relative(existingAncestor, target2));
13097
13719
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
13098
13720
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
13099
13721
  throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
@@ -13109,7 +13731,7 @@ async function nearestExistingAncestor(target2) {
13109
13731
  if (error.code !== "ENOENT")
13110
13732
  throw error;
13111
13733
  }
13112
- const parent = dirname5(current);
13734
+ const parent = dirname6(current);
13113
13735
  if (parent === current)
13114
13736
  throw new Error(`unable to resolve an existing ancestor for ${target2}`);
13115
13737
  current = parent;
@@ -13124,7 +13746,7 @@ async function ensureProjectSecrets(paths) {
13124
13746
  await chmod(paths.stateDir, 448);
13125
13747
  let stored;
13126
13748
  try {
13127
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13749
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13128
13750
  } catch (error) {
13129
13751
  if (error.code !== "ENOENT")
13130
13752
  throw error;
@@ -13142,7 +13764,7 @@ async function ensureProjectSecrets(paths) {
13142
13764
  } catch (error2) {
13143
13765
  if (error2.code !== "EEXIST")
13144
13766
  throw error2;
13145
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13767
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13146
13768
  } finally {
13147
13769
  await unlink2(temporaryFile).catch((error2) => {
13148
13770
  if (error2.code !== "ENOENT")
@@ -13342,7 +13964,7 @@ async function startProjectServer(options = {}) {
13342
13964
  }
13343
13965
  async function loadWebhooks(projectDir) {
13344
13966
  try {
13345
- const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
13967
+ const parsed = JSON.parse(await readFile7(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
13346
13968
  return Array.isArray(parsed) ? parsed : [];
13347
13969
  } catch (error) {
13348
13970
  if (error.code === "ENOENT")
@@ -13351,7 +13973,7 @@ async function loadWebhooks(projectDir) {
13351
13973
  }
13352
13974
  }
13353
13975
  function resolvePath(projectDir, path) {
13354
- return isAbsolute(path) ? path : resolve3(projectDir, path);
13976
+ return isAbsolute(path) ? path : resolve4(projectDir, path);
13355
13977
  }
13356
13978
  function randomHex(bytes) {
13357
13979
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -13396,8 +14018,8 @@ async function findEphemeralPort(host = "127.0.0.1") {
13396
14018
  }
13397
14019
 
13398
14020
  // src/snapshot.ts
13399
- 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";
13400
- import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
14021
+ 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";
14022
+ import { dirname as dirname7, join as join9, parse as parse2, relative as relative2, resolve as resolve5, sep as sep2 } from "path";
13401
14023
  import { create as createTar, extract as extractTar2 } from "tar";
13402
14024
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
13403
14025
  var SNAPSHOT_VERSION = 1;
@@ -13421,11 +14043,11 @@ async function createSnapshot(options) {
13421
14043
  postgresMajor: await readPostgresMajor(paths.dataDir)
13422
14044
  } : {}
13423
14045
  };
13424
- const output = resolve4(options.output);
14046
+ const output = resolve5(options.output);
13425
14047
  if (await existingInfo(output))
13426
14048
  throw new Error(`snapshot output already exists: ${output}`);
13427
- await mkdir6(dirname6(output), { recursive: true });
13428
- const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
14049
+ await mkdir6(dirname7(output), { recursive: true });
14050
+ const stagingRoot = await mkdtemp(join9(dirname7(output), ".supacloud-lite-snapshot-"));
13429
14051
  try {
13430
14052
  await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
13431
14053
  `);
@@ -13454,7 +14076,7 @@ async function restoreSnapshot(options) {
13454
14076
  const paths = normalizePaths(options.paths);
13455
14077
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
13456
14078
  await assertNoDataDirectoryLock(paths);
13457
- const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
14079
+ const stagingRoot = await mkdtemp(join9(dirname7(paths.stateDir), ".supacloud-lite-restore-"));
13458
14080
  const payloadRoot = join9(stagingRoot, "payload");
13459
14081
  const rollbackId = crypto.randomUUID();
13460
14082
  const rollbackPaths = [];
@@ -13462,7 +14084,7 @@ async function restoreSnapshot(options) {
13462
14084
  await mkdir6(payloadRoot, { recursive: true });
13463
14085
  await extractTar2({
13464
14086
  cwd: payloadRoot,
13465
- file: resolve4(options.input),
14087
+ file: resolve5(options.input),
13466
14088
  preserveOwner: false,
13467
14089
  preservePaths: false,
13468
14090
  strict: true,
@@ -13539,11 +14161,11 @@ async function restoreSnapshot(options) {
13539
14161
  function normalizePaths(paths) {
13540
14162
  return {
13541
14163
  ...paths,
13542
- projectDir: resolve4(paths.projectDir),
13543
- stateDir: resolve4(paths.stateDir),
13544
- dataDir: paths.dataDir ? resolve4(paths.dataDir) : undefined,
13545
- storageDir: resolve4(paths.storageDir),
13546
- secretsFile: resolve4(paths.secretsFile)
14164
+ projectDir: resolve5(paths.projectDir),
14165
+ stateDir: resolve5(paths.stateDir),
14166
+ dataDir: paths.dataDir ? resolve5(paths.dataDir) : undefined,
14167
+ storageDir: resolve5(paths.storageDir),
14168
+ secretsFile: resolve5(paths.secretsFile)
13547
14169
  };
13548
14170
  }
13549
14171
  async function assertSnapshotPaths(paths, options = {}) {
@@ -13573,7 +14195,7 @@ async function assertSnapshotPaths(paths, options = {}) {
13573
14195
  async function assertDirectoryOrMissing(path) {
13574
14196
  if (!path)
13575
14197
  return;
13576
- if (resolve4(path) === parse2(resolve4(path)).root)
14198
+ if (resolve5(path) === parse2(resolve5(path)).root)
13577
14199
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
13578
14200
  try {
13579
14201
  const info = await lstat2(path);
@@ -13627,13 +14249,13 @@ async function stageDirectory(root, destination) {
13627
14249
  await walk(root, destination);
13628
14250
  }
13629
14251
  async function stageFile(source, target2) {
13630
- await mkdir6(dirname6(target2), { recursive: true });
14252
+ await mkdir6(dirname7(target2), { recursive: true });
13631
14253
  await copyFile(source, target2);
13632
14254
  }
13633
14255
  async function readManifest(payloadRoot) {
13634
14256
  let parsed;
13635
14257
  try {
13636
- parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
14258
+ parsed = JSON.parse(await readFile8(join9(payloadRoot, "manifest.json"), "utf8"));
13637
14259
  } catch (error) {
13638
14260
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
13639
14261
  }
@@ -13665,7 +14287,7 @@ async function readPostgresMajor(dataDir) {
13665
14287
  if (!dataDir)
13666
14288
  return;
13667
14289
  try {
13668
- return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
14290
+ return (await readFile8(join9(dataDir, "PG_VERSION"), "utf8")).trim();
13669
14291
  } catch (error) {
13670
14292
  if (error.code === "ENOENT")
13671
14293
  return;
@@ -13725,12 +14347,12 @@ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
13725
14347
  throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
13726
14348
  await rm5(target2, { recursive: true, force: true });
13727
14349
  } else {
13728
- swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
14350
+ swap.rollbackPath = join9(dirname7(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
13729
14351
  await rename2(target2, swap.rollbackPath);
13730
14352
  }
13731
14353
  }
13732
14354
  try {
13733
- await mkdir6(dirname6(target2), { recursive: true });
14355
+ await mkdir6(dirname7(target2), { recursive: true });
13734
14356
  await rename2(source, target2);
13735
14357
  swaps.push(swap);
13736
14358
  } catch (error) {
@@ -13764,7 +14386,7 @@ async function copyEntry(source, target2) {
13764
14386
  for (const entry of await readdir3(source))
13765
14387
  await copyEntry(join9(source, entry), join9(target2, entry));
13766
14388
  } else if (info.isFile()) {
13767
- await mkdir6(dirname6(target2), { recursive: true });
14389
+ await mkdir6(dirname7(target2), { recursive: true });
13768
14390
  await Bun.write(target2, Bun.file(source));
13769
14391
  } else
13770
14392
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
@@ -13795,13 +14417,13 @@ async function assertNoSymlinks(root) {
13795
14417
  }
13796
14418
  }
13797
14419
  function isWithin(parent, child) {
13798
- const normalizedParent = resolve4(parent);
13799
- const normalizedChild = resolve4(child);
14420
+ const normalizedParent = resolve5(parent);
14421
+ const normalizedChild = resolve5(child);
13800
14422
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
13801
14423
  }
13802
14424
  function pathsOverlap(left, right) {
13803
- const normalizedLeft = resolve4(left);
13804
- const normalizedRight = resolve4(right);
14425
+ const normalizedLeft = resolve5(left);
14426
+ const normalizedRight = resolve5(right);
13805
14427
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
13806
14428
  }
13807
14429
 
@@ -13850,13 +14472,13 @@ function parseArgs(argv) {
13850
14472
  else if (argument === "--site-url")
13851
14473
  options.siteUrl = next();
13852
14474
  else if (argument === "--project-dir" || argument === "--dir")
13853
- options.projectDir = resolve5(next());
14475
+ options.projectDir = resolve6(next());
13854
14476
  else if (argument === "--state-dir")
13855
- options.stateDir = resolve5(next());
14477
+ options.stateDir = resolve6(next());
13856
14478
  else if (argument === "--data-dir")
13857
- options.dataDir = resolve5(next());
14479
+ options.dataDir = resolve6(next());
13858
14480
  else if (argument === "--storage-dir")
13859
- options.storageDir = resolve5(next());
14481
+ options.storageDir = resolve6(next());
13860
14482
  else if (argument === "--storage-backend")
13861
14483
  options.storageBackend = next();
13862
14484
  else if (argument === "--s3-prefix")
@@ -13874,15 +14496,17 @@ function parseArgs(argv) {
13874
14496
  else if (argument === "--powersync-tables")
13875
14497
  options.powersyncPublicationTables = commaSeparated2(next());
13876
14498
  else if (argument === "--replication-tls-cert")
13877
- options.replicationTlsCertFile = resolve5(next());
14499
+ options.replicationTlsCertFile = resolve6(next());
13878
14500
  else if (argument === "--replication-tls-key")
13879
- options.replicationTlsKeyFile = resolve5(next());
14501
+ options.replicationTlsKeyFile = resolve6(next());
13880
14502
  else if (argument === "--memory")
13881
14503
  options.memory = true;
13882
14504
  else if (argument === "--output" || argument === "-o")
13883
- options.output = resolve5(next());
14505
+ options.output = resolve6(next());
13884
14506
  else if (argument === "--file" || argument === "-f")
13885
14507
  options.diffFile = next();
14508
+ else if (argument === "--module-file")
14509
+ options.moduleFile = resolve6(next());
13886
14510
  else if (argument === "--service-role")
13887
14511
  options.serviceRole = true;
13888
14512
  else if (argument === "--force")
@@ -13950,7 +14574,7 @@ ${privilegedKey}
13950
14574
  try {
13951
14575
  const source = await generateTypes(project2.backend.db, "public");
13952
14576
  if (options.output) {
13953
- await mkdir7(dirname7(options.output), { recursive: true });
14577
+ await mkdir7(dirname8(options.output), { recursive: true });
13954
14578
  await writeFile7(options.output, source);
13955
14579
  await writeStandardOutput(`Wrote ${options.output}
13956
14580
  `);
@@ -14069,7 +14693,7 @@ async function runDbCommand(options) {
14069
14693
  }
14070
14694
  return;
14071
14695
  }
14072
- const project = await loadSupabaseProject(resolve5(options.projectDir ?? process.cwd()));
14696
+ const project = await loadSupabaseProject(resolve6(options.projectDir ?? process.cwd()));
14073
14697
  if (subcommand === "diff") {
14074
14698
  const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
14075
14699
  dataDir: paths.dataDir,
@@ -14124,6 +14748,29 @@ async function runDbCommand(options) {
14124
14748
  `);
14125
14749
  return;
14126
14750
  }
14751
+ if (subcommand === "check") {
14752
+ const moduleFile = options.moduleFile ?? join10(paths.projectDir, "supabase", "db", "modules.ts");
14753
+ const project2 = await createProjectBackend({
14754
+ ...options,
14755
+ applyMigrations: false,
14756
+ includeFunctions: false,
14757
+ includeWebhooks: false,
14758
+ startRuntimeServices: false,
14759
+ log: quietLog
14760
+ });
14761
+ try {
14762
+ const executor = {
14763
+ query: async (sql, params) => (await project2.backend.db.query(sql, params)).rows
14764
+ };
14765
+ const result = await checkDatabaseModules({ moduleFile, executor });
14766
+ await writeStandardOutput(formatDatabaseModuleCheck(result));
14767
+ if (!result.ok)
14768
+ throw new Error("db check found error-level issues");
14769
+ } finally {
14770
+ await project2.backend.close();
14771
+ }
14772
+ return;
14773
+ }
14127
14774
  throw new Error(`unknown db subcommand: ${subcommand ?? "(none)"}`);
14128
14775
  }
14129
14776
  async function runSnapshotCommand(options) {
@@ -14152,7 +14799,7 @@ async function runSnapshotCommand(options) {
14152
14799
  const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
14153
14800
  const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
14154
14801
  await writeStandardOutput([
14155
- `Snapshot restored from ${resolve5(input)}`,
14802
+ `Snapshot restored from ${resolve6(input)}`,
14156
14803
  ...rollbackLines,
14157
14804
  ...reconnectLine
14158
14805
  ].join(`
@@ -14249,6 +14896,7 @@ Commands:
14249
14896
  db reset reset initialized database/storage and re-run migrations
14250
14897
  db diff print schema changes outside migrations
14251
14898
  db pull [name] write live schema changes as an applied migration
14899
+ db check reconcile database module manifests (@supacloud/db) against the live catalog
14252
14900
  snapshot create create a compressed database/storage/secrets snapshot
14253
14901
  snapshot restore <f> restore a snapshot into an empty target
14254
14902
  upgrade snapshot first, then apply pending migrations
@@ -14281,6 +14929,7 @@ Options:
14281
14929
  --json emit machine-readable doctor output
14282
14930
  -o, --output <p> output file for gen types
14283
14931
  -f, --file <name> migration suffix for db diff
14932
+ --module-file <p> database module manifest for db check (default supabase/db/modules.ts)
14284
14933
  --force replace non-empty restore targets and retain rollback copies
14285
14934
  `);
14286
14935
  }