@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/CHANGELOG.md +22 -0
- package/README.md +33 -0
- package/dist/cli.js +730 -81
- package/dist/index.js +250 -15
- package/dist/runtime/functions/edge-runtime-shim.d.ts +15 -0
- package/dist/runtime/functions/edge-runtime-shim.d.ts.map +1 -0
- package/dist/runtime/functions/fetch-policy.d.ts +7 -0
- package/dist/runtime/functions/fetch-policy.d.ts.map +1 -0
- package/dist/runtime/functions/handler.d.ts +49 -2
- package/dist/runtime/functions/handler.d.ts.map +1 -1
- package/dist/runtime/index.d.ts +2 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/node/load-config.d.ts +14 -0
- package/dist/runtime/node/load-config.d.ts.map +1 -1
- package/dist/runtime/node/load-functions.d.ts +14 -0
- package/dist/runtime/node/load-functions.d.ts.map +1 -1
- package/package.json +2 -1
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
|
|
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.
|
|
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/
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2755
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
10225
|
+
var LOOPBACK_HOSTS2 = new Set(["127.0.0.1", "localhost", "::1", "", undefined]);
|
|
10026
10226
|
function isNetworkExposed(host) {
|
|
10027
|
-
return !
|
|
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
|
|
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
|
|
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
|
|
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
|
|
12238
|
-
import { dirname as
|
|
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
|
|
12289
|
-
import { dirname as
|
|
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(
|
|
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
|
|
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
|
|
12720
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
13484
|
+
importUrl = pathToFileURL2(bundledPath).href;
|
|
12877
13485
|
} catch (e) {
|
|
12878
13486
|
if (e.message !== "esbuild-not-available")
|
|
12879
13487
|
throw e;
|
|
12880
|
-
importUrl =
|
|
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,
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
13400
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
13428
|
-
const stagingRoot = await mkdtemp(join9(
|
|
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(
|
|
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:
|
|
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:
|
|
13543
|
-
stateDir:
|
|
13544
|
-
dataDir: paths.dataDir ?
|
|
13545
|
-
storageDir:
|
|
13546
|
-
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 (
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
13799
|
-
const normalizedChild =
|
|
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 =
|
|
13804
|
-
const normalizedRight =
|
|
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 =
|
|
14475
|
+
options.projectDir = resolve6(next());
|
|
13854
14476
|
else if (argument === "--state-dir")
|
|
13855
|
-
options.stateDir =
|
|
14477
|
+
options.stateDir = resolve6(next());
|
|
13856
14478
|
else if (argument === "--data-dir")
|
|
13857
|
-
options.dataDir =
|
|
14479
|
+
options.dataDir = resolve6(next());
|
|
13858
14480
|
else if (argument === "--storage-dir")
|
|
13859
|
-
options.storageDir =
|
|
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 =
|
|
14499
|
+
options.replicationTlsCertFile = resolve6(next());
|
|
13878
14500
|
else if (argument === "--replication-tls-key")
|
|
13879
|
-
options.replicationTlsKeyFile =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 ${
|
|
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
|
}
|