@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/CHANGELOG.md +10 -0
- package/README.md +33 -0
- package/dist/cli.js +682 -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.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/
|
|
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,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
|
|
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
|
|
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
|
|
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
|
|
12238
|
-
import { dirname as
|
|
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
|
|
12289
|
-
import { dirname as
|
|
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(
|
|
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
|
|
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
|
|
12720
|
-
import { dirname as
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
13436
|
+
importUrl = pathToFileURL2(bundledPath).href;
|
|
12877
13437
|
} catch (e) {
|
|
12878
13438
|
if (e.message !== "esbuild-not-available")
|
|
12879
13439
|
throw e;
|
|
12880
|
-
importUrl =
|
|
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,
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 :
|
|
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
|
|
13400
|
-
import { dirname as
|
|
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 =
|
|
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(
|
|
13428
|
-
const stagingRoot = await mkdtemp(join9(
|
|
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(
|
|
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:
|
|
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:
|
|
13543
|
-
stateDir:
|
|
13544
|
-
dataDir: paths.dataDir ?
|
|
13545
|
-
storageDir:
|
|
13546
|
-
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 (
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
13799
|
-
const normalizedChild =
|
|
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 =
|
|
13804
|
-
const normalizedRight =
|
|
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 =
|
|
14427
|
+
options.projectDir = resolve6(next());
|
|
13854
14428
|
else if (argument === "--state-dir")
|
|
13855
|
-
options.stateDir =
|
|
14429
|
+
options.stateDir = resolve6(next());
|
|
13856
14430
|
else if (argument === "--data-dir")
|
|
13857
|
-
options.dataDir =
|
|
14431
|
+
options.dataDir = resolve6(next());
|
|
13858
14432
|
else if (argument === "--storage-dir")
|
|
13859
|
-
options.storageDir =
|
|
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 =
|
|
14451
|
+
options.replicationTlsCertFile = resolve6(next());
|
|
13878
14452
|
else if (argument === "--replication-tls-key")
|
|
13879
|
-
options.replicationTlsKeyFile =
|
|
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 =
|
|
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(
|
|
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(
|
|
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 ${
|
|
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
|
}
|