@supacloud/lite 0.10.0 → 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.10.0",
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,6 +59,7 @@ 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: {
@@ -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,345 @@ 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 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
+
11961
12500
  // src/runtime/node/native/readiness.ts
11962
12501
  function liteCapabilities(engine, replicationProfile) {
11963
12502
  if (engine === "pglite") {
@@ -12189,7 +12728,7 @@ function sameStrings(left, right) {
12189
12728
  }
12190
12729
 
12191
12730
  // src/runtime/node/project.ts
12192
- import { readdir, readFile as readFile2 } from "fs/promises";
12731
+ import { readdir, readFile as readFile3 } from "fs/promises";
12193
12732
  import { join as join3 } from "path";
12194
12733
  async function loadSupabaseProject(projectDir, seed = {}) {
12195
12734
  const migrationsDir = join3(projectDir, "supabase", "migrations");
@@ -12204,7 +12743,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12204
12743
  for (const entry of entries.sort()) {
12205
12744
  if (!entry.endsWith(".sql"))
12206
12745
  continue;
12207
- const sql = await readFile2(join3(migrationsDir, entry), "utf8");
12746
+ const sql = await readFile3(join3(migrationsDir, entry), "utf8");
12208
12747
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
12209
12748
  }
12210
12749
  let seedSql;
@@ -12216,7 +12755,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
12216
12755
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
12217
12756
  for (const relativePath of matches) {
12218
12757
  try {
12219
- parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
12758
+ parts.push(await readFile3(join3(supabaseDir, relativePath), "utf8"));
12220
12759
  } catch (error) {
12221
12760
  if (!isNotFound(error))
12222
12761
  throw error;
@@ -12234,8 +12773,8 @@ function isNotFound(error) {
12234
12773
  }
12235
12774
 
12236
12775
  // 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";
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";
12239
12778
 
12240
12779
  // src/runtime/node/bun-server.ts
12241
12780
  async function serveBun(backend, opts = {}) {
@@ -12285,8 +12824,8 @@ async function serveBun(backend, opts = {}) {
12285
12824
  }
12286
12825
 
12287
12826
  // 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";
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";
12290
12829
 
12291
12830
  class FsStorageDriver {
12292
12831
  root;
@@ -12302,7 +12841,7 @@ class FsStorageDriver {
12302
12841
  }
12303
12842
  async put(key, data) {
12304
12843
  const path = this.resolve(key);
12305
- await mkdir3(dirname3(path), { recursive: true });
12844
+ await mkdir3(dirname4(path), { recursive: true });
12306
12845
  const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
12307
12846
  try {
12308
12847
  await writeFile3(temporaryPath, data);
@@ -12314,7 +12853,7 @@ class FsStorageDriver {
12314
12853
  }
12315
12854
  async get(key) {
12316
12855
  try {
12317
- return new Uint8Array(await readFile3(this.resolve(key)));
12856
+ return new Uint8Array(await readFile4(this.resolve(key)));
12318
12857
  } catch (e) {
12319
12858
  if (e.code === "ENOENT")
12320
12859
  return null;
@@ -12710,19 +13249,40 @@ function readFunctions(root) {
12710
13249
  console.warn(` warning: [functions.${name}] framework "${framework}" is not one of fetch/elysia/hono; falling back to fetch`);
12711
13250
  }
12712
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;
12713
13273
  out[name] = opts;
12714
13274
  }
12715
13275
  return out;
12716
13276
  }
12717
13277
 
12718
13278
  // 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";
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";
12722
13282
 
12723
13283
  // src/runtime/node/bundle-function.ts
12724
13284
  import { createHash as createHash3 } from "crypto";
12725
- 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";
12726
13286
  import { existsSync as existsSync3 } from "fs";
12727
13287
  import { tmpdir as tmpdir3 } from "os";
12728
13288
  import { join as join6 } from "path";
@@ -12738,7 +13298,7 @@ async function fetchModule(url) {
12738
13298
  const key = createHash3("sha256").update(url).digest("hex");
12739
13299
  const cached = join6(HTTP_CACHE, key);
12740
13300
  if (existsSync3(cached))
12741
- return readFile4(cached, "utf8");
13301
+ return readFile5(cached, "utf8");
12742
13302
  const res = await fetch(url, { redirect: "follow" });
12743
13303
  if (!res.ok)
12744
13304
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
@@ -12797,7 +13357,7 @@ async function bundleFunction(entryPath, name) {
12797
13357
  async function loadFunctionEnv(projectDir) {
12798
13358
  let text;
12799
13359
  try {
12800
- text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
13360
+ text = await readFile6(join7(projectDir, "supabase", "functions", ".env"), "utf8");
12801
13361
  } catch {
12802
13362
  return {};
12803
13363
  }
@@ -12859,12 +13419,12 @@ async function loadFunctionsUnlocked(projectDir, options) {
12859
13419
  if (options[name]?.enabled === false)
12860
13420
  continue;
12861
13421
  const dir = join7(root, name);
12862
- if (!(await stat(dir)).isDirectory())
13422
+ if (!(await stat2(dir)).isDirectory())
12863
13423
  continue;
12864
13424
  const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
12865
13425
  for (const path of candidates) {
12866
13426
  try {
12867
- await stat(path);
13427
+ await stat2(path);
12868
13428
  } catch {
12869
13429
  continue;
12870
13430
  }
@@ -12873,11 +13433,11 @@ async function loadFunctionsUnlocked(projectDir, options) {
12873
13433
  let importUrl;
12874
13434
  try {
12875
13435
  bundledPath = await realpath(await bundleFunction(path, `${name}-${crypto.randomUUID()}`));
12876
- importUrl = pathToFileURL(bundledPath).href;
13436
+ importUrl = pathToFileURL2(bundledPath).href;
12877
13437
  } catch (e) {
12878
13438
  if (e.message !== "esbuild-not-available")
12879
13439
  throw e;
12880
- importUrl = pathToFileURL(path).href;
13440
+ importUrl = pathToFileURL2(path).href;
12881
13441
  }
12882
13442
  resetCapturedHandler();
12883
13443
  const mod = await import(importUrl);
@@ -12885,9 +13445,23 @@ async function loadFunctionsUnlocked(projectDir, options) {
12885
13445
  const defaultExport = mod.default;
12886
13446
  const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && (typeof defaultExport.handle === "function" || typeof defaultExport.fetch === "function") ? defaultExport : denoHandler ? (req) => denoHandler(req) : undefined;
12887
13447
  if (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;
12888
13460
  functions.set(name, {
12889
13461
  handler,
12890
- framework: resolveFramework(name, options[name]?.framework)
13462
+ framework: resolveFramework(name, opts?.framework),
13463
+ ...limits ? { limits } : {},
13464
+ ...capabilities ? { capabilities } : {}
12891
13465
  });
12892
13466
  } else {
12893
13467
  console.warn(` warning: function "${name}" has no default function, handle/fetch object, or Deno.serve() handler, skipped`);
@@ -12901,7 +13475,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
12901
13475
  }
12902
13476
  } finally {
12903
13477
  if (bundledPath)
12904
- await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
13478
+ await rm4(dirname5(bundledPath), { recursive: true, force: true }).catch(() => {});
12905
13479
  }
12906
13480
  break;
12907
13481
  }
@@ -13007,7 +13581,7 @@ class S3StorageDriver {
13007
13581
  var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
13008
13582
  var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
13009
13583
  function resolveProjectPaths(options = {}) {
13010
- const projectDir = resolve3(options.projectDir ?? process.cwd());
13584
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
13011
13585
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
13012
13586
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
13013
13587
  const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -13022,7 +13596,7 @@ function resolveProjectPaths(options = {}) {
13022
13596
  };
13023
13597
  }
13024
13598
  async function assertResetPathsSafe(paths) {
13025
- const stateDir = resolve3(paths.stateDir);
13599
+ const stateDir = resolve4(paths.stateDir);
13026
13600
  if (stateDir === parse(stateDir).root)
13027
13601
  throw new Error("refusing to use the filesystem root as the state directory");
13028
13602
  const stateInfo = await requiredResetEntry(stateDir);
@@ -13030,7 +13604,7 @@ async function assertResetPathsSafe(paths) {
13030
13604
  throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
13031
13605
  }
13032
13606
  const canonicalStateDir = await realpath2(stateDir);
13033
- const secretsFile = resolve3(paths.secretsFile);
13607
+ const secretsFile = resolve4(paths.secretsFile);
13034
13608
  if (secretsFile !== join8(stateDir, "secrets.json")) {
13035
13609
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
13036
13610
  }
@@ -13044,7 +13618,7 @@ async function assertResetPathsSafe(paths) {
13044
13618
  ["storage", paths.storageDir]
13045
13619
  ];
13046
13620
  for (const [label, targetPath] of targets) {
13047
- const target2 = resolve3(targetPath);
13621
+ const target2 = resolve4(targetPath);
13048
13622
  const relativePath = relative(stateDir, target2);
13049
13623
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
13050
13624
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
@@ -13064,7 +13638,7 @@ async function requiredResetEntry(path) {
13064
13638
  async function assertResetSecretsValid(path) {
13065
13639
  let serializedSecrets;
13066
13640
  try {
13067
- serializedSecrets = await readFile6(path, "utf8");
13641
+ serializedSecrets = await readFile7(path, "utf8");
13068
13642
  } catch (error) {
13069
13643
  if (error.code === "ENOENT")
13070
13644
  throw new Error(RESET_INITIALIZATION_ERROR);
@@ -13087,13 +13661,13 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
13087
13661
  if (error.code !== "ENOENT")
13088
13662
  throw error;
13089
13663
  }
13090
- const parent = dirname5(current);
13664
+ const parent = dirname6(current);
13091
13665
  if (parent === current)
13092
13666
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
13093
13667
  current = parent;
13094
13668
  }
13095
13669
  const existingAncestor = await nearestExistingAncestor(target2);
13096
- const canonicalTarget = resolve3(await realpath2(existingAncestor), relative(existingAncestor, target2));
13670
+ const canonicalTarget = resolve4(await realpath2(existingAncestor), relative(existingAncestor, target2));
13097
13671
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
13098
13672
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
13099
13673
  throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
@@ -13109,7 +13683,7 @@ async function nearestExistingAncestor(target2) {
13109
13683
  if (error.code !== "ENOENT")
13110
13684
  throw error;
13111
13685
  }
13112
- const parent = dirname5(current);
13686
+ const parent = dirname6(current);
13113
13687
  if (parent === current)
13114
13688
  throw new Error(`unable to resolve an existing ancestor for ${target2}`);
13115
13689
  current = parent;
@@ -13124,7 +13698,7 @@ async function ensureProjectSecrets(paths) {
13124
13698
  await chmod(paths.stateDir, 448);
13125
13699
  let stored;
13126
13700
  try {
13127
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13701
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13128
13702
  } catch (error) {
13129
13703
  if (error.code !== "ENOENT")
13130
13704
  throw error;
@@ -13142,7 +13716,7 @@ async function ensureProjectSecrets(paths) {
13142
13716
  } catch (error2) {
13143
13717
  if (error2.code !== "EEXIST")
13144
13718
  throw error2;
13145
- stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
13719
+ stored = validateSecrets(JSON.parse(await readFile7(paths.secretsFile, "utf8")));
13146
13720
  } finally {
13147
13721
  await unlink2(temporaryFile).catch((error2) => {
13148
13722
  if (error2.code !== "ENOENT")
@@ -13342,7 +13916,7 @@ async function startProjectServer(options = {}) {
13342
13916
  }
13343
13917
  async function loadWebhooks(projectDir) {
13344
13918
  try {
13345
- 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"));
13346
13920
  return Array.isArray(parsed) ? parsed : [];
13347
13921
  } catch (error) {
13348
13922
  if (error.code === "ENOENT")
@@ -13351,7 +13925,7 @@ async function loadWebhooks(projectDir) {
13351
13925
  }
13352
13926
  }
13353
13927
  function resolvePath(projectDir, path) {
13354
- return isAbsolute(path) ? path : resolve3(projectDir, path);
13928
+ return isAbsolute(path) ? path : resolve4(projectDir, path);
13355
13929
  }
13356
13930
  function randomHex(bytes) {
13357
13931
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -13396,8 +13970,8 @@ async function findEphemeralPort(host = "127.0.0.1") {
13396
13970
  }
13397
13971
 
13398
13972
  // 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";
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";
13401
13975
  import { create as createTar, extract as extractTar2 } from "tar";
13402
13976
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
13403
13977
  var SNAPSHOT_VERSION = 1;
@@ -13421,11 +13995,11 @@ async function createSnapshot(options) {
13421
13995
  postgresMajor: await readPostgresMajor(paths.dataDir)
13422
13996
  } : {}
13423
13997
  };
13424
- const output = resolve4(options.output);
13998
+ const output = resolve5(options.output);
13425
13999
  if (await existingInfo(output))
13426
14000
  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-"));
14001
+ await mkdir6(dirname7(output), { recursive: true });
14002
+ const stagingRoot = await mkdtemp(join9(dirname7(output), ".supacloud-lite-snapshot-"));
13429
14003
  try {
13430
14004
  await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
13431
14005
  `);
@@ -13454,7 +14028,7 @@ async function restoreSnapshot(options) {
13454
14028
  const paths = normalizePaths(options.paths);
13455
14029
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
13456
14030
  await assertNoDataDirectoryLock(paths);
13457
- const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
14031
+ const stagingRoot = await mkdtemp(join9(dirname7(paths.stateDir), ".supacloud-lite-restore-"));
13458
14032
  const payloadRoot = join9(stagingRoot, "payload");
13459
14033
  const rollbackId = crypto.randomUUID();
13460
14034
  const rollbackPaths = [];
@@ -13462,7 +14036,7 @@ async function restoreSnapshot(options) {
13462
14036
  await mkdir6(payloadRoot, { recursive: true });
13463
14037
  await extractTar2({
13464
14038
  cwd: payloadRoot,
13465
- file: resolve4(options.input),
14039
+ file: resolve5(options.input),
13466
14040
  preserveOwner: false,
13467
14041
  preservePaths: false,
13468
14042
  strict: true,
@@ -13539,11 +14113,11 @@ async function restoreSnapshot(options) {
13539
14113
  function normalizePaths(paths) {
13540
14114
  return {
13541
14115
  ...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)
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)
13547
14121
  };
13548
14122
  }
13549
14123
  async function assertSnapshotPaths(paths, options = {}) {
@@ -13573,7 +14147,7 @@ async function assertSnapshotPaths(paths, options = {}) {
13573
14147
  async function assertDirectoryOrMissing(path) {
13574
14148
  if (!path)
13575
14149
  return;
13576
- if (resolve4(path) === parse2(resolve4(path)).root)
14150
+ if (resolve5(path) === parse2(resolve5(path)).root)
13577
14151
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
13578
14152
  try {
13579
14153
  const info = await lstat2(path);
@@ -13627,13 +14201,13 @@ async function stageDirectory(root, destination) {
13627
14201
  await walk(root, destination);
13628
14202
  }
13629
14203
  async function stageFile(source, target2) {
13630
- await mkdir6(dirname6(target2), { recursive: true });
14204
+ await mkdir6(dirname7(target2), { recursive: true });
13631
14205
  await copyFile(source, target2);
13632
14206
  }
13633
14207
  async function readManifest(payloadRoot) {
13634
14208
  let parsed;
13635
14209
  try {
13636
- parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
14210
+ parsed = JSON.parse(await readFile8(join9(payloadRoot, "manifest.json"), "utf8"));
13637
14211
  } catch (error) {
13638
14212
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
13639
14213
  }
@@ -13665,7 +14239,7 @@ async function readPostgresMajor(dataDir) {
13665
14239
  if (!dataDir)
13666
14240
  return;
13667
14241
  try {
13668
- return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
14242
+ return (await readFile8(join9(dataDir, "PG_VERSION"), "utf8")).trim();
13669
14243
  } catch (error) {
13670
14244
  if (error.code === "ENOENT")
13671
14245
  return;
@@ -13725,12 +14299,12 @@ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
13725
14299
  throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
13726
14300
  await rm5(target2, { recursive: true, force: true });
13727
14301
  } else {
13728
- 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}`);
13729
14303
  await rename2(target2, swap.rollbackPath);
13730
14304
  }
13731
14305
  }
13732
14306
  try {
13733
- await mkdir6(dirname6(target2), { recursive: true });
14307
+ await mkdir6(dirname7(target2), { recursive: true });
13734
14308
  await rename2(source, target2);
13735
14309
  swaps.push(swap);
13736
14310
  } catch (error) {
@@ -13764,7 +14338,7 @@ async function copyEntry(source, target2) {
13764
14338
  for (const entry of await readdir3(source))
13765
14339
  await copyEntry(join9(source, entry), join9(target2, entry));
13766
14340
  } else if (info.isFile()) {
13767
- await mkdir6(dirname6(target2), { recursive: true });
14341
+ await mkdir6(dirname7(target2), { recursive: true });
13768
14342
  await Bun.write(target2, Bun.file(source));
13769
14343
  } else
13770
14344
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
@@ -13795,13 +14369,13 @@ async function assertNoSymlinks(root) {
13795
14369
  }
13796
14370
  }
13797
14371
  function isWithin(parent, child) {
13798
- const normalizedParent = resolve4(parent);
13799
- const normalizedChild = resolve4(child);
14372
+ const normalizedParent = resolve5(parent);
14373
+ const normalizedChild = resolve5(child);
13800
14374
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
13801
14375
  }
13802
14376
  function pathsOverlap(left, right) {
13803
- const normalizedLeft = resolve4(left);
13804
- const normalizedRight = resolve4(right);
14377
+ const normalizedLeft = resolve5(left);
14378
+ const normalizedRight = resolve5(right);
13805
14379
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
13806
14380
  }
13807
14381
 
@@ -13850,13 +14424,13 @@ function parseArgs(argv) {
13850
14424
  else if (argument === "--site-url")
13851
14425
  options.siteUrl = next();
13852
14426
  else if (argument === "--project-dir" || argument === "--dir")
13853
- options.projectDir = resolve5(next());
14427
+ options.projectDir = resolve6(next());
13854
14428
  else if (argument === "--state-dir")
13855
- options.stateDir = resolve5(next());
14429
+ options.stateDir = resolve6(next());
13856
14430
  else if (argument === "--data-dir")
13857
- options.dataDir = resolve5(next());
14431
+ options.dataDir = resolve6(next());
13858
14432
  else if (argument === "--storage-dir")
13859
- options.storageDir = resolve5(next());
14433
+ options.storageDir = resolve6(next());
13860
14434
  else if (argument === "--storage-backend")
13861
14435
  options.storageBackend = next();
13862
14436
  else if (argument === "--s3-prefix")
@@ -13874,15 +14448,17 @@ function parseArgs(argv) {
13874
14448
  else if (argument === "--powersync-tables")
13875
14449
  options.powersyncPublicationTables = commaSeparated2(next());
13876
14450
  else if (argument === "--replication-tls-cert")
13877
- options.replicationTlsCertFile = resolve5(next());
14451
+ options.replicationTlsCertFile = resolve6(next());
13878
14452
  else if (argument === "--replication-tls-key")
13879
- options.replicationTlsKeyFile = resolve5(next());
14453
+ options.replicationTlsKeyFile = resolve6(next());
13880
14454
  else if (argument === "--memory")
13881
14455
  options.memory = true;
13882
14456
  else if (argument === "--output" || argument === "-o")
13883
- options.output = resolve5(next());
14457
+ options.output = resolve6(next());
13884
14458
  else if (argument === "--file" || argument === "-f")
13885
14459
  options.diffFile = next();
14460
+ else if (argument === "--module-file")
14461
+ options.moduleFile = resolve6(next());
13886
14462
  else if (argument === "--service-role")
13887
14463
  options.serviceRole = true;
13888
14464
  else if (argument === "--force")
@@ -13950,7 +14526,7 @@ ${privilegedKey}
13950
14526
  try {
13951
14527
  const source = await generateTypes(project2.backend.db, "public");
13952
14528
  if (options.output) {
13953
- await mkdir7(dirname7(options.output), { recursive: true });
14529
+ await mkdir7(dirname8(options.output), { recursive: true });
13954
14530
  await writeFile7(options.output, source);
13955
14531
  await writeStandardOutput(`Wrote ${options.output}
13956
14532
  `);
@@ -14069,7 +14645,7 @@ async function runDbCommand(options) {
14069
14645
  }
14070
14646
  return;
14071
14647
  }
14072
- const project = await loadSupabaseProject(resolve5(options.projectDir ?? process.cwd()));
14648
+ const project = await loadSupabaseProject(resolve6(options.projectDir ?? process.cwd()));
14073
14649
  if (subcommand === "diff") {
14074
14650
  const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
14075
14651
  dataDir: paths.dataDir,
@@ -14124,6 +14700,29 @@ async function runDbCommand(options) {
14124
14700
  `);
14125
14701
  return;
14126
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
+ }
14127
14726
  throw new Error(`unknown db subcommand: ${subcommand ?? "(none)"}`);
14128
14727
  }
14129
14728
  async function runSnapshotCommand(options) {
@@ -14152,7 +14751,7 @@ async function runSnapshotCommand(options) {
14152
14751
  const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
14153
14752
  const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
14154
14753
  await writeStandardOutput([
14155
- `Snapshot restored from ${resolve5(input)}`,
14754
+ `Snapshot restored from ${resolve6(input)}`,
14156
14755
  ...rollbackLines,
14157
14756
  ...reconnectLine
14158
14757
  ].join(`
@@ -14249,6 +14848,7 @@ Commands:
14249
14848
  db reset reset initialized database/storage and re-run migrations
14250
14849
  db diff print schema changes outside migrations
14251
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
14252
14852
  snapshot create create a compressed database/storage/secrets snapshot
14253
14853
  snapshot restore <f> restore a snapshot into an empty target
14254
14854
  upgrade snapshot first, then apply pending migrations
@@ -14281,6 +14881,7 @@ Options:
14281
14881
  --json emit machine-readable doctor output
14282
14882
  -o, --output <p> output file for gen types
14283
14883
  -f, --file <name> migration suffix for db diff
14884
+ --module-file <p> database module manifest for db check (default supabase/db/modules.ts)
14284
14885
  --force replace non-empty restore targets and retain rollback copies
14285
14886
  `);
14286
14887
  }