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