@supacloud/lite 0.13.0 → 0.14.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 +7 -0
- package/README.md +33 -4
- package/dist/cli.js +1028 -264
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +850 -99
- package/dist/project-runtime.d.ts +14 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/auth/handler.d.ts.map +1 -1
- package/dist/runtime/auth/inbox.d.ts.map +1 -1
- package/dist/runtime/auth/oauth.d.ts.map +1 -1
- package/dist/runtime/auth/rate-limit.d.ts.map +1 -1
- package/dist/runtime/auth/sms-inbox.d.ts.map +1 -1
- package/dist/runtime/cron/service.d.ts.map +1 -1
- package/dist/runtime/db/bootstrap.d.ts +1 -1
- package/dist/runtime/db/bootstrap.d.ts.map +1 -1
- package/dist/runtime/db/database.d.ts +4 -0
- package/dist/runtime/db/database.d.ts.map +1 -1
- package/dist/runtime/db/engine.d.ts.map +1 -1
- package/dist/runtime/db/sql-compat.d.ts +1 -1
- package/dist/runtime/db/sql-compat.d.ts.map +1 -1
- package/dist/runtime/functions/handler.d.ts +3 -1
- package/dist/runtime/functions/handler.d.ts.map +1 -1
- package/dist/runtime/functions/pgredis.d.ts.map +1 -1
- package/dist/runtime/functions/profile.d.ts +5 -0
- package/dist/runtime/functions/profile.d.ts.map +1 -0
- package/dist/runtime/graphql.d.ts +31 -0
- package/dist/runtime/graphql.d.ts.map +1 -0
- package/dist/runtime/identity.d.ts +27 -0
- package/dist/runtime/identity.d.ts.map +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/log-buffer.d.ts.map +1 -1
- package/dist/runtime/net/service.d.ts.map +1 -1
- package/dist/runtime/node/bundle-function.d.ts.map +1 -1
- package/dist/runtime/node/fs-driver.d.ts.map +1 -1
- package/dist/runtime/node/load-config.d.ts +13 -1
- package/dist/runtime/node/load-config.d.ts.map +1 -1
- package/dist/runtime/node/load-functions.d.ts +2 -1
- package/dist/runtime/node/load-functions.d.ts.map +1 -1
- package/dist/runtime/node/native/engine.d.ts +3 -0
- package/dist/runtime/node/native/engine.d.ts.map +1 -1
- package/dist/runtime/node/native/readiness.d.ts +14 -0
- package/dist/runtime/node/native/readiness.d.ts.map +1 -1
- package/dist/runtime/node/native/wire.d.ts.map +1 -1
- package/dist/runtime/node/project.d.ts +7 -0
- package/dist/runtime/node/project.d.ts.map +1 -1
- package/dist/runtime/realtime/engine.d.ts +4 -1
- package/dist/runtime/realtime/engine.d.ts.map +1 -1
- package/dist/runtime/rest/build.d.ts.map +1 -1
- package/dist/runtime/rest/handler.d.ts.map +1 -1
- package/dist/runtime/retention/service.d.ts.map +1 -1
- package/dist/runtime/storage/driver.d.ts.map +1 -1
- package/dist/runtime/storage/handler.d.ts.map +1 -1
- package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -1
- package/dist/runtime/storage/s3-driver.d.ts +1 -1
- package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +4 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/runtime/validation.d.ts +4 -0
- package/dist/runtime/validation.d.ts.map +1 -0
- package/dist/runtime/webhooks/service.d.ts.map +1 -1
- package/dist/snapshot.d.ts +1 -0
- package/dist/snapshot.d.ts.map +1 -1
- package/package.json +8 -2
package/dist/cli.js
CHANGED
|
@@ -9,7 +9,7 @@ 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.14.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",
|
|
@@ -48,6 +48,8 @@ var package_default = {
|
|
|
48
48
|
start: "bun run src/cli.ts start",
|
|
49
49
|
test: "bun test --timeout 20000",
|
|
50
50
|
"test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts test/qauth-workflow-matrix.test.ts test/maker-checker-workflow.test.ts test/commands-artifacts.test.ts --timeout 180000",
|
|
51
|
+
"test:graphql": "SUPACLOUD_LITE_TEST_GRAPHQL=1 bun test test/graphql-native.test.ts --timeout 180000",
|
|
52
|
+
benchmark: "bun run scripts/benchmark.ts",
|
|
51
53
|
parity: "bun run parity/harness.ts",
|
|
52
54
|
"parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
|
|
53
55
|
"check:native": "bun run test:native && bun run parity:native",
|
|
@@ -59,13 +61,17 @@ var package_default = {
|
|
|
59
61
|
},
|
|
60
62
|
dependencies: {
|
|
61
63
|
"@electric-sql/pglite": "0.5.8",
|
|
62
|
-
"@supacloud/db": "^0.
|
|
64
|
+
"@supacloud/db": "^0.6.0",
|
|
63
65
|
tar: "^7.5.22"
|
|
64
66
|
},
|
|
65
67
|
devDependencies: {
|
|
68
|
+
"@supacloud/app": "^0.11.0",
|
|
69
|
+
"@supacloud/compiler": "^0.13.1",
|
|
70
|
+
"@supacloud/elysia": "^0.10.0",
|
|
66
71
|
"@supabase/supabase-js": "^2.115.0",
|
|
67
72
|
"@types/bun": "^1.4.0",
|
|
68
73
|
elysia: "^1.4.30",
|
|
74
|
+
jose: "^6.2.11",
|
|
69
75
|
typescript: "^7.0.2"
|
|
70
76
|
},
|
|
71
77
|
engines: {
|
|
@@ -2784,6 +2790,38 @@ async function upsertWithoutTtl(query, key, serializedValue) {
|
|
|
2784
2790
|
on conflict (namespace, key) do update set value = excluded.value, expires_at = null, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue]);
|
|
2785
2791
|
}
|
|
2786
2792
|
|
|
2793
|
+
// src/runtime/functions/profile.ts
|
|
2794
|
+
function runtimeMode(value) {
|
|
2795
|
+
if (value === undefined)
|
|
2796
|
+
return "development";
|
|
2797
|
+
if (value !== "development" && value !== "strict")
|
|
2798
|
+
throw new Error("Lite runtime mode must be development or strict");
|
|
2799
|
+
return value;
|
|
2800
|
+
}
|
|
2801
|
+
function strictFunction(entry) {
|
|
2802
|
+
const limits = {
|
|
2803
|
+
timeoutMs: 900000,
|
|
2804
|
+
maxRequestBodyBytes: 30 * 1024 * 1024,
|
|
2805
|
+
maxResponseBodyBytes: 30 * 1024 * 1024,
|
|
2806
|
+
waitUntilTimeoutMs: 300000,
|
|
2807
|
+
...entry.limits
|
|
2808
|
+
};
|
|
2809
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
2810
|
+
const maximum = name.includes("Body") ? 30 * 1024 * 1024 : name === "waitUntilTimeoutMs" ? 300000 : 900000;
|
|
2811
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
2812
|
+
throw new Error(`invalid strict function limit: ${name}`);
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
if (entry.framework !== undefined && !["fetch", "elysia", "hono"].includes(entry.framework)) {
|
|
2816
|
+
throw new Error("invalid strict function framework");
|
|
2817
|
+
}
|
|
2818
|
+
return {
|
|
2819
|
+
...entry,
|
|
2820
|
+
limits,
|
|
2821
|
+
capabilities: { background: false, ...entry.capabilities }
|
|
2822
|
+
};
|
|
2823
|
+
}
|
|
2824
|
+
|
|
2787
2825
|
// src/runtime/functions/handler.ts
|
|
2788
2826
|
var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
2789
2827
|
var UNSAFE_VERIFIED_JWT_SUBJECT = /[\u0000-\u001F\u007F-\u009F\u0100-\u{10FFFF}]/u;
|
|
@@ -2834,12 +2872,19 @@ class FunctionsHandler {
|
|
|
2834
2872
|
functions;
|
|
2835
2873
|
env;
|
|
2836
2874
|
pgredis;
|
|
2837
|
-
|
|
2875
|
+
mode;
|
|
2876
|
+
constructor(functions, env, pgredis, mode = "development") {
|
|
2838
2877
|
this.functions = functions;
|
|
2839
2878
|
this.env = env;
|
|
2840
2879
|
this.pgredis = pgredis;
|
|
2880
|
+
this.mode = mode;
|
|
2881
|
+
if (mode === "strict")
|
|
2882
|
+
for (const entry of functions.values())
|
|
2883
|
+
strictFunction(normalizeEntry(entry));
|
|
2841
2884
|
}
|
|
2842
2885
|
register(name, fn) {
|
|
2886
|
+
if (this.mode === "strict")
|
|
2887
|
+
strictFunction(normalizeEntry(fn));
|
|
2843
2888
|
this.functions.set(name, fn);
|
|
2844
2889
|
}
|
|
2845
2890
|
list() {
|
|
@@ -2854,7 +2899,7 @@ class FunctionsHandler {
|
|
|
2854
2899
|
if (!value) {
|
|
2855
2900
|
return json2(404, { error: `function "${name}" not found` });
|
|
2856
2901
|
}
|
|
2857
|
-
const entry = normalizeEntry(value);
|
|
2902
|
+
const entry = this.mode === "strict" ? strictFunction(normalizeEntry(value)) : normalizeEntry(value);
|
|
2858
2903
|
const limits = entry.limits;
|
|
2859
2904
|
const capabilities = entry.capabilities;
|
|
2860
2905
|
const maxBody = limits?.maxRequestBodyBytes;
|
|
@@ -3224,14 +3269,17 @@ alter role anon set search_path to "$user", public, extensions;
|
|
|
3224
3269
|
alter role authenticated set search_path to "$user", public, extensions;
|
|
3225
3270
|
alter role service_role set search_path to "$user", public, extensions;
|
|
3226
3271
|
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3272
|
+
-- Initialize defaults once. Restart/maintenance must not undo application ACLs.
|
|
3273
|
+
do $$
|
|
3274
|
+
begin
|
|
3275
|
+
if to_regclass('supabase_migrations.schema_migrations') is null then
|
|
3276
|
+
grant usage on schema public to anon, authenticated, service_role;
|
|
3277
|
+
alter default privileges in schema public
|
|
3278
|
+
grant all on tables to anon, authenticated, service_role;
|
|
3279
|
+
alter default privileges in schema public
|
|
3280
|
+
grant all on sequences to anon, authenticated, service_role;
|
|
3281
|
+
end if;
|
|
3282
|
+
end $$;
|
|
3235
3283
|
|
|
3236
3284
|
-- PostgreSQL already grants EXECUTE on new functions to PUBLIC. Keep that
|
|
3237
3285
|
-- default so project migrations can revoke PUBLIC and grant only selected
|
|
@@ -5691,7 +5739,9 @@ grant execute on function vault.create_secret(text,text,text,uuid), vault.update
|
|
|
5691
5739
|
`;
|
|
5692
5740
|
|
|
5693
5741
|
// src/runtime/db/sql-compat.ts
|
|
5694
|
-
function rewriteMigrationSql(sql) {
|
|
5742
|
+
function rewriteMigrationSql(sql, strict = false) {
|
|
5743
|
+
if (strict)
|
|
5744
|
+
return sql;
|
|
5695
5745
|
const out = [];
|
|
5696
5746
|
const n = sql.length;
|
|
5697
5747
|
let i = 0;
|
|
@@ -5765,6 +5815,8 @@ function rewriteStatement(stmt) {
|
|
|
5765
5815
|
const trivia = LEADING_TRIVIA.exec(stmt)?.[0] ?? "";
|
|
5766
5816
|
const rest = stmt.slice(trivia.length);
|
|
5767
5817
|
if (/^(?:create|drop)\s+extension\b/i.test(rest)) {
|
|
5818
|
+
if (extensionName(rest) === "pg_graphql")
|
|
5819
|
+
return stmt;
|
|
5768
5820
|
const semi = rest.lastIndexOf(";");
|
|
5769
5821
|
const bare = (semi !== -1 && rest.slice(semi + 1).trim() === "" ? rest.slice(0, semi) : rest).trim();
|
|
5770
5822
|
if (!bare)
|
|
@@ -5778,6 +5830,26 @@ function rewriteStatement(stmt) {
|
|
|
5778
5830
|
}
|
|
5779
5831
|
return stmt;
|
|
5780
5832
|
}
|
|
5833
|
+
function extensionName(statement) {
|
|
5834
|
+
let rest = statement;
|
|
5835
|
+
const tokens = [];
|
|
5836
|
+
while (tokens.length < 6) {
|
|
5837
|
+
rest = rest.slice(LEADING_TRIVIA.exec(rest)?.[0].length ?? 0);
|
|
5838
|
+
const token = /^(?:"((?:""|[^"])*)"|([A-Za-z_][A-Za-z_0-9]*))/.exec(rest);
|
|
5839
|
+
if (!token)
|
|
5840
|
+
break;
|
|
5841
|
+
const quoted = token[1];
|
|
5842
|
+
const bare = token[2];
|
|
5843
|
+
if (quoted !== undefined)
|
|
5844
|
+
tokens.push(quoted.replaceAll('""', '"'));
|
|
5845
|
+
else if (bare !== undefined)
|
|
5846
|
+
tokens.push(bare.toLowerCase());
|
|
5847
|
+
else
|
|
5848
|
+
throw new Error("invalid migration SQL token");
|
|
5849
|
+
rest = rest.slice(token[0].length);
|
|
5850
|
+
}
|
|
5851
|
+
return tokens[2] === "if" ? tokens[tokens[3] === "not" ? 5 : 4] : tokens[2];
|
|
5852
|
+
}
|
|
5781
5853
|
function pickTag(text, base) {
|
|
5782
5854
|
let tag = `$${base}$`;
|
|
5783
5855
|
let k = 0;
|
|
@@ -6051,19 +6123,51 @@ async function removePreparedBundles(cleanup) {
|
|
|
6051
6123
|
}
|
|
6052
6124
|
}
|
|
6053
6125
|
|
|
6126
|
+
// src/runtime/db/engine.ts
|
|
6127
|
+
class Mutex {
|
|
6128
|
+
tail = Promise.resolve();
|
|
6129
|
+
async lock() {
|
|
6130
|
+
let release;
|
|
6131
|
+
const next = new Promise((r) => release = r);
|
|
6132
|
+
const prev = this.tail;
|
|
6133
|
+
this.tail = this.tail.then(() => next);
|
|
6134
|
+
await prev;
|
|
6135
|
+
return release;
|
|
6136
|
+
}
|
|
6137
|
+
async run(fn) {
|
|
6138
|
+
const release = await this.lock();
|
|
6139
|
+
try {
|
|
6140
|
+
return await fn();
|
|
6141
|
+
} finally {
|
|
6142
|
+
release();
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6147
|
+
// src/runtime/validation.ts
|
|
6148
|
+
function isRecord(value) {
|
|
6149
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6150
|
+
}
|
|
6151
|
+
function errorProperty(error, key) {
|
|
6152
|
+
return isRecord(error) ? error[key] : undefined;
|
|
6153
|
+
}
|
|
6154
|
+
|
|
6054
6155
|
// src/runtime/db/database.ts
|
|
6055
6156
|
var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
|
|
6056
6157
|
|
|
6057
6158
|
class Database {
|
|
6058
6159
|
engine;
|
|
6160
|
+
strict;
|
|
6161
|
+
migrationMutex = new Mutex;
|
|
6059
6162
|
schemaCache = new Map;
|
|
6060
6163
|
fnCache = new Map;
|
|
6061
6164
|
cdcListeners = new Set;
|
|
6062
6165
|
cdcStarting = null;
|
|
6063
6166
|
cdcStopping = null;
|
|
6064
6167
|
cdcUnsubscribe = null;
|
|
6065
|
-
constructor(engine) {
|
|
6168
|
+
constructor(engine, strict = false) {
|
|
6066
6169
|
this.engine = engine;
|
|
6170
|
+
this.strict = strict;
|
|
6067
6171
|
}
|
|
6068
6172
|
static async create(dataDirOrEngine, opts) {
|
|
6069
6173
|
const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
|
|
@@ -6082,7 +6186,7 @@ class Database {
|
|
|
6082
6186
|
await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
|
|
6083
6187
|
}
|
|
6084
6188
|
}
|
|
6085
|
-
return new Database(engine);
|
|
6189
|
+
return new Database(engine, opts?.strict);
|
|
6086
6190
|
} catch (error) {
|
|
6087
6191
|
try {
|
|
6088
6192
|
await engine.close();
|
|
@@ -6109,16 +6213,48 @@ class Database {
|
|
|
6109
6213
|
});
|
|
6110
6214
|
}
|
|
6111
6215
|
async runMigrations(migrations, seedSql) {
|
|
6216
|
+
return this.migrationMutex.run(() => this.applyMigrations(migrations, seedSql));
|
|
6217
|
+
}
|
|
6218
|
+
async applyMigrations(migrations, seedSql) {
|
|
6112
6219
|
const applied = [];
|
|
6113
6220
|
const sorted = [...migrations].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
6221
|
+
const versions = new Set;
|
|
6222
|
+
const history = await this.engine.query("select version, statements from supabase_migrations.schema_migrations");
|
|
6223
|
+
const recorded = new Map;
|
|
6224
|
+
for (const row of history.rows) {
|
|
6225
|
+
if (!isRecord(row) || typeof row["version"] !== "string")
|
|
6226
|
+
throw new Error("invalid migration history record");
|
|
6227
|
+
const statements = row["statements"];
|
|
6228
|
+
if (statements !== null && (!Array.isArray(statements) || !statements.every((sql) => typeof sql === "string"))) {
|
|
6229
|
+
throw new Error(`invalid migration history SQL: ${row["version"]}`);
|
|
6230
|
+
}
|
|
6231
|
+
recorded.set(row["version"], statements);
|
|
6232
|
+
}
|
|
6233
|
+
for (const migration of sorted) {
|
|
6234
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
6235
|
+
if (versions.has(version))
|
|
6236
|
+
throw new Error(`duplicate migration version: ${version}`);
|
|
6237
|
+
versions.add(version);
|
|
6238
|
+
}
|
|
6239
|
+
for (const migration of sorted) {
|
|
6240
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
6241
|
+
if (recorded.has(version)) {
|
|
6242
|
+
const statements = recorded.get(version);
|
|
6243
|
+
if (!statements?.length)
|
|
6244
|
+
throw new Error(`migration history has no verifiable SQL: ${version}`);
|
|
6245
|
+
if (statements.join(`
|
|
6246
|
+
`) !== migration.sql) {
|
|
6247
|
+
throw new Error(`applied migration content mismatch: ${version}; add a forward migration instead`);
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
}
|
|
6114
6251
|
for (const m of sorted) {
|
|
6115
6252
|
const version = m.name.match(/^(\d+)/)?.[1] ?? m.name;
|
|
6116
|
-
|
|
6117
|
-
if (seen.rows.length > 0)
|
|
6253
|
+
if (recorded.has(version))
|
|
6118
6254
|
continue;
|
|
6119
6255
|
const applyMigration = () => this.engine.transaction(async (tx) => {
|
|
6120
6256
|
await tx.exec(DEFAULT_SEARCH_PATH_SQL);
|
|
6121
|
-
await tx.exec(rewriteMigrationSql(m.sql));
|
|
6257
|
+
await tx.exec(rewriteMigrationSql(m.sql, this.strict));
|
|
6122
6258
|
await tx.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
|
|
6123
6259
|
values ($1, $2, $3)`, [version, m.name, [m.sql]]);
|
|
6124
6260
|
});
|
|
@@ -6140,7 +6276,7 @@ class Database {
|
|
|
6140
6276
|
if (seen.rows.length === 0) {
|
|
6141
6277
|
await this.engine.transaction(async (tx) => {
|
|
6142
6278
|
await tx.exec(DEFAULT_SEARCH_PATH_SQL);
|
|
6143
|
-
await tx.exec(rewriteMigrationSql(seedSql));
|
|
6279
|
+
await tx.exec(rewriteMigrationSql(seedSql, this.strict));
|
|
6144
6280
|
await tx.query(`insert into supabase_migrations.seed_files (path, hash) values ('supabase/seed.sql', $1)
|
|
6145
6281
|
on conflict (path) do update set hash = excluded.hash, applied_at = now()`, [hash]);
|
|
6146
6282
|
});
|
|
@@ -6437,19 +6573,23 @@ async function sha256Hex2(text) {
|
|
|
6437
6573
|
class RealtimeEngine {
|
|
6438
6574
|
db;
|
|
6439
6575
|
jwtSecret;
|
|
6576
|
+
resolveExternalToken;
|
|
6440
6577
|
connections = new Set;
|
|
6441
6578
|
bindingCounter = 1;
|
|
6442
6579
|
phxRefCounter = 1;
|
|
6443
6580
|
presence = new Map;
|
|
6444
6581
|
stopCdc = null;
|
|
6445
6582
|
stopDbBroadcast = null;
|
|
6446
|
-
constructor(db, jwtSecret) {
|
|
6583
|
+
constructor(db, jwtSecret, resolveExternalToken) {
|
|
6447
6584
|
this.db = db;
|
|
6448
6585
|
this.jwtSecret = jwtSecret;
|
|
6586
|
+
this.resolveExternalToken = resolveExternalToken;
|
|
6449
6587
|
}
|
|
6450
6588
|
async contextFromToken(token) {
|
|
6451
6589
|
if (!token || !this.jwtSecret)
|
|
6452
6590
|
return { role: "anon", claims: null };
|
|
6591
|
+
if (this.resolveExternalToken)
|
|
6592
|
+
return this.resolveExternalToken(token);
|
|
6453
6593
|
const claims = await verifyJwt(token, this.jwtSecret);
|
|
6454
6594
|
if (!claims)
|
|
6455
6595
|
return { role: "anon", claims: null };
|
|
@@ -6509,8 +6649,11 @@ class RealtimeEngine {
|
|
|
6509
6649
|
const stopDbBroadcast = this.stopDbBroadcast;
|
|
6510
6650
|
this.stopCdc = null;
|
|
6511
6651
|
this.stopDbBroadcast = null;
|
|
6512
|
-
for (const conn of this.connections)
|
|
6652
|
+
for (const conn of this.connections) {
|
|
6653
|
+
for (const topic of conn.channels.keys())
|
|
6654
|
+
this.leaveChannel(conn, topic);
|
|
6513
6655
|
conn.socket.close(1001, "server shutting down");
|
|
6656
|
+
}
|
|
6514
6657
|
this.connections.clear();
|
|
6515
6658
|
const [cdcStop, broadcastStop] = await Promise.allSettled([
|
|
6516
6659
|
Promise.resolve().then(() => stopCdc?.()),
|
|
@@ -6588,13 +6731,29 @@ class RealtimeEngine {
|
|
|
6588
6731
|
break;
|
|
6589
6732
|
case "access_token":
|
|
6590
6733
|
{
|
|
6591
|
-
const
|
|
6592
|
-
const
|
|
6593
|
-
|
|
6594
|
-
|
|
6595
|
-
|
|
6596
|
-
|
|
6597
|
-
|
|
6734
|
+
const selected = conn.channels.get(msg.topic);
|
|
6735
|
+
const channels = selected ? [selected] : [...conn.channels.values()];
|
|
6736
|
+
try {
|
|
6737
|
+
const token = msg.payload?.access_token;
|
|
6738
|
+
if (token !== undefined && typeof token !== "string")
|
|
6739
|
+
throw new Error("Invalid Realtime access token");
|
|
6740
|
+
const ctx = await this.contextFromToken(token);
|
|
6741
|
+
for (const channel of channels) {
|
|
6742
|
+
if (this.resolveExternalToken && channel.private) {
|
|
6743
|
+
const access = await this.authorizePrivate(channel.topic.replace(/^realtime:/, ""), ctx);
|
|
6744
|
+
if (!access.read)
|
|
6745
|
+
throw new Error("Realtime authentication denied");
|
|
6746
|
+
channel.canBroadcast = access.write;
|
|
6747
|
+
}
|
|
6748
|
+
channel.ctx = ctx;
|
|
6749
|
+
this.scheduleExternalExpiry(conn, channel);
|
|
6750
|
+
}
|
|
6751
|
+
} catch (error) {
|
|
6752
|
+
if (this.resolveExternalToken)
|
|
6753
|
+
for (const channel of channels)
|
|
6754
|
+
this.leaveChannel(conn, channel.topic);
|
|
6755
|
+
throw error;
|
|
6756
|
+
}
|
|
6598
6757
|
}
|
|
6599
6758
|
break;
|
|
6600
6759
|
default:
|
|
@@ -6606,7 +6765,10 @@ class RealtimeEngine {
|
|
|
6606
6765
|
}
|
|
6607
6766
|
async handleJoin(conn, msg) {
|
|
6608
6767
|
const config = msg.payload?.config ?? {};
|
|
6609
|
-
const
|
|
6768
|
+
const token = msg.payload?.access_token;
|
|
6769
|
+
if (token !== undefined && typeof token !== "string")
|
|
6770
|
+
throw new Error("Invalid Realtime access token");
|
|
6771
|
+
const ctx = await this.contextFromToken(token);
|
|
6610
6772
|
const isPrivate = config.private === true;
|
|
6611
6773
|
let canBroadcast = true;
|
|
6612
6774
|
if (isPrivate && !this.db.engine.minimalBootstrap) {
|
|
@@ -6658,7 +6820,10 @@ class RealtimeEngine {
|
|
|
6658
6820
|
private: isPrivate,
|
|
6659
6821
|
canBroadcast
|
|
6660
6822
|
};
|
|
6823
|
+
if (this.resolveExternalToken)
|
|
6824
|
+
this.leaveChannel(conn, msg.topic);
|
|
6661
6825
|
conn.channels.set(msg.topic, channel);
|
|
6826
|
+
this.scheduleExternalExpiry(conn, channel);
|
|
6662
6827
|
this.reply(conn, msg, "ok", {
|
|
6663
6828
|
postgres_changes: bindings.map((b) => ({
|
|
6664
6829
|
id: b.id,
|
|
@@ -6693,6 +6858,8 @@ class RealtimeEngine {
|
|
|
6693
6858
|
const channel = conn.channels.get(topic);
|
|
6694
6859
|
if (!channel)
|
|
6695
6860
|
return;
|
|
6861
|
+
if (channel.expiryTimer)
|
|
6862
|
+
clearTimeout(channel.expiryTimer);
|
|
6696
6863
|
conn.channels.delete(topic);
|
|
6697
6864
|
const state = this.presence.get(topic);
|
|
6698
6865
|
if (state?.has(channel.presenceKey)) {
|
|
@@ -6708,6 +6875,20 @@ class RealtimeEngine {
|
|
|
6708
6875
|
});
|
|
6709
6876
|
}
|
|
6710
6877
|
}
|
|
6878
|
+
scheduleExternalExpiry(conn, channel) {
|
|
6879
|
+
if (channel.expiryTimer)
|
|
6880
|
+
clearTimeout(channel.expiryTimer);
|
|
6881
|
+
const expires = channel.ctx.claims?.exp;
|
|
6882
|
+
if (!this.resolveExternalToken || typeof expires !== "number")
|
|
6883
|
+
return;
|
|
6884
|
+
channel.expiryTimer = setTimeout(() => {
|
|
6885
|
+
if (expires * 1000 > Date.now())
|
|
6886
|
+
return this.scheduleExternalExpiry(conn, channel);
|
|
6887
|
+
this.leaveChannel(conn, channel.topic);
|
|
6888
|
+
this.send(conn, { topic: channel.topic, event: "phx_error", payload: { reason: "token expired" }, ref: null });
|
|
6889
|
+
}, Math.max(0, Math.min(expires * 1000 - Date.now(), 2147483647)));
|
|
6890
|
+
channel.expiryTimer.unref?.();
|
|
6891
|
+
}
|
|
6711
6892
|
handleBinary(conn, bytes) {
|
|
6712
6893
|
if (bytes.length < 7 || bytes[0] !== 3)
|
|
6713
6894
|
return;
|
|
@@ -10245,6 +10426,168 @@ function assertSecretsSafe(input) {
|
|
|
10245
10426
|
warn("\u26A0 Vault key derived from the JWT secret - set an independent vaultKey before exposing this server.");
|
|
10246
10427
|
}
|
|
10247
10428
|
}
|
|
10429
|
+
|
|
10430
|
+
// src/runtime/graphql.ts
|
|
10431
|
+
async function inspectGraphql(engine) {
|
|
10432
|
+
const result = await engine.query(`
|
|
10433
|
+
select e.extversion, exists (
|
|
10434
|
+
select 1 from pg_proc p
|
|
10435
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
10436
|
+
join pg_depend d on d.classid = 'pg_proc'::regclass and d.objid = p.oid
|
|
10437
|
+
and d.refclassid = 'pg_extension'::regclass and d.refobjid = e.oid and d.deptype = 'e'
|
|
10438
|
+
where n.nspname = 'graphql' and p.proname = 'resolve' and not p.prosecdef
|
|
10439
|
+
and p.oid = to_regprocedure('graphql.resolve(text,jsonb,text,jsonb)')
|
|
10440
|
+
) as resolver from pg_extension e where e.extname = 'pg_graphql'
|
|
10441
|
+
`);
|
|
10442
|
+
const row = result.rows[0];
|
|
10443
|
+
if (row === undefined) {
|
|
10444
|
+
return { status: "unsupported", extension: "pg_graphql", reason: "PG_GRAPHQL_NOT_INSTALLED" };
|
|
10445
|
+
}
|
|
10446
|
+
if (!isRecord(row) || typeof row["extversion"] !== "string" || !row["extversion"] || typeof row["resolver"] !== "boolean" || result.rows.length !== 1) {
|
|
10447
|
+
return { status: "unsupported", extension: "pg_graphql", reason: "PG_GRAPHQL_INVALID_CATALOG" };
|
|
10448
|
+
}
|
|
10449
|
+
const version = row["extversion"];
|
|
10450
|
+
if (row["resolver"]) {
|
|
10451
|
+
try {
|
|
10452
|
+
const probe = await engine.query(`select graphql.resolve('{ __typename }', '{}'::jsonb, null, '{}'::jsonb) as result`);
|
|
10453
|
+
const probeRow = probe.rows[0];
|
|
10454
|
+
if (probe.rows.length !== 1 || !isRecord(probeRow) || !isGraphqlEnvelope(probeRow["result"])) {
|
|
10455
|
+
throw new Error("Invalid GraphQL probe response");
|
|
10456
|
+
}
|
|
10457
|
+
} catch {
|
|
10458
|
+
return { status: "unsupported", extension: "pg_graphql", version, reason: "PG_GRAPHQL_RESOLVER_FAILED" };
|
|
10459
|
+
}
|
|
10460
|
+
}
|
|
10461
|
+
return row["resolver"] ? { status: "supported", extension: "pg_graphql", version } : {
|
|
10462
|
+
status: "unsupported",
|
|
10463
|
+
extension: "pg_graphql",
|
|
10464
|
+
reason: "PG_GRAPHQL_RESOLVER_UNAVAILABLE"
|
|
10465
|
+
};
|
|
10466
|
+
}
|
|
10467
|
+
|
|
10468
|
+
class GraphqlHandler {
|
|
10469
|
+
db;
|
|
10470
|
+
capability;
|
|
10471
|
+
schemas;
|
|
10472
|
+
maxBytes;
|
|
10473
|
+
timeoutMs;
|
|
10474
|
+
constructor(db, capability, options = {}, schemas = ["public"]) {
|
|
10475
|
+
this.db = db;
|
|
10476
|
+
this.capability = capability;
|
|
10477
|
+
this.schemas = schemas;
|
|
10478
|
+
this.maxBytes = positive(options.maxRequestBodyBytes ?? 1024 * 1024);
|
|
10479
|
+
this.timeoutMs = positive(options.statementTimeoutMs ?? 30000);
|
|
10480
|
+
}
|
|
10481
|
+
async handle(request, context) {
|
|
10482
|
+
if (this.capability.status !== "supported") {
|
|
10483
|
+
return response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10484
|
+
errors: [{
|
|
10485
|
+
message: "GraphQL requires the real pg_graphql extension and graphql.resolve.",
|
|
10486
|
+
extensions: { code: this.capability.reason ?? "GRAPHQL_DISABLED" }
|
|
10487
|
+
}]
|
|
10488
|
+
});
|
|
10489
|
+
}
|
|
10490
|
+
if (request.method !== "POST")
|
|
10491
|
+
return response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" });
|
|
10492
|
+
try {
|
|
10493
|
+
if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
10494
|
+
throw new ApiError(415, { message: "GraphQL requests require application/json" });
|
|
10495
|
+
}
|
|
10496
|
+
const body = JSON.parse(await boundedBody(request, this.maxBytes));
|
|
10497
|
+
if (!isObject(body) || typeof body.query !== "string" || !body.query.trim() || body.variables != null && !isObject(body.variables) || body.extensions != null && !isObject(body.extensions) || body.operationName != null && typeof body.operationName !== "string") {
|
|
10498
|
+
throw new ApiError(400, { message: "Invalid GraphQL request" });
|
|
10499
|
+
}
|
|
10500
|
+
const result = await this.db.withContext(context, async (query) => {
|
|
10501
|
+
await query(`select set_config('statement_timeout', $1, true)`, [String(this.timeoutMs)]);
|
|
10502
|
+
await query(`select set_config('search_path', $1, true)`, [
|
|
10503
|
+
this.schemas.map((schema) => `"${schema.replaceAll('"', '""')}"`).join(",")
|
|
10504
|
+
]);
|
|
10505
|
+
return query("select graphql.resolve($1::text, $2::jsonb, $3::text, $4::jsonb) as result", [body.query, JSON.stringify(body.variables ?? {}), body.operationName ?? null, JSON.stringify(body.extensions ?? {})]);
|
|
10506
|
+
});
|
|
10507
|
+
const row = result.rows[0];
|
|
10508
|
+
if (result.rows.length !== 1 || !isRecord(row) || !isGraphqlEnvelope(row["result"])) {
|
|
10509
|
+
throw new Error("Invalid GraphQL resolver response");
|
|
10510
|
+
}
|
|
10511
|
+
return response(200, row["result"]);
|
|
10512
|
+
} catch (error) {
|
|
10513
|
+
if (error instanceof ApiError)
|
|
10514
|
+
return response(error.status, { errors: [{ message: error.message }] });
|
|
10515
|
+
if (error instanceof SyntaxError)
|
|
10516
|
+
return response(400, { errors: [{ message: "Invalid JSON request" }] });
|
|
10517
|
+
const code = errorProperty(error, "errno") ?? errorProperty(error, "code");
|
|
10518
|
+
const status = code === "42501" ? 403 : code === "57014" ? 504 : 500;
|
|
10519
|
+
return response(status, { errors: [{
|
|
10520
|
+
message: "GraphQL execution failed",
|
|
10521
|
+
extensions: { code: status === 403 ? "GRAPHQL_ACCESS_DENIED" : status === 504 ? "GRAPHQL_TIMEOUT" : "GRAPHQL_EXECUTION_FAILED" }
|
|
10522
|
+
}] });
|
|
10523
|
+
}
|
|
10524
|
+
}
|
|
10525
|
+
}
|
|
10526
|
+
async function boundedBody(request, limit) {
|
|
10527
|
+
if (Number(request.headers.get("content-length")) > limit)
|
|
10528
|
+
throw new ApiError(413, { message: "GraphQL request too large" });
|
|
10529
|
+
const reader = request.body?.getReader();
|
|
10530
|
+
if (!reader)
|
|
10531
|
+
throw new ApiError(400, { message: "GraphQL request body required" });
|
|
10532
|
+
const chunks = [];
|
|
10533
|
+
let size = 0;
|
|
10534
|
+
try {
|
|
10535
|
+
while (true) {
|
|
10536
|
+
const chunk = await reader.read();
|
|
10537
|
+
if (chunk.done)
|
|
10538
|
+
break;
|
|
10539
|
+
size += chunk.value.byteLength;
|
|
10540
|
+
if (size > limit) {
|
|
10541
|
+
await reader.cancel().catch(() => {});
|
|
10542
|
+
throw new ApiError(413, { message: "GraphQL request too large" });
|
|
10543
|
+
}
|
|
10544
|
+
chunks.push(chunk.value);
|
|
10545
|
+
}
|
|
10546
|
+
} finally {
|
|
10547
|
+
reader.releaseLock();
|
|
10548
|
+
}
|
|
10549
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
10550
|
+
}
|
|
10551
|
+
function isObject(value) {
|
|
10552
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10553
|
+
}
|
|
10554
|
+
function isGraphqlEnvelope(value) {
|
|
10555
|
+
if (!isRecord(value) || !("data" in value) && !("errors" in value))
|
|
10556
|
+
return false;
|
|
10557
|
+
if ("data" in value && value["data"] !== null && !isRecord(value["data"]))
|
|
10558
|
+
return false;
|
|
10559
|
+
if ("extensions" in value && !isRecord(value["extensions"]))
|
|
10560
|
+
return false;
|
|
10561
|
+
if ("errors" in value) {
|
|
10562
|
+
const errors = value["errors"];
|
|
10563
|
+
if (!Array.isArray(errors) || errors.length === 0 || !errors.every((error) => isRecord(error) && typeof error["message"] === "string"))
|
|
10564
|
+
return false;
|
|
10565
|
+
}
|
|
10566
|
+
return true;
|
|
10567
|
+
}
|
|
10568
|
+
function positive(value) {
|
|
10569
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2147483647)
|
|
10570
|
+
throw new Error("GraphQL limits must be positive bounded integers");
|
|
10571
|
+
return value;
|
|
10572
|
+
}
|
|
10573
|
+
function response(status, body, headers = {}) {
|
|
10574
|
+
return Response.json(body, { status, headers: { "cache-control": "no-store", ...headers } });
|
|
10575
|
+
}
|
|
10576
|
+
|
|
10577
|
+
// src/runtime/identity.ts
|
|
10578
|
+
function validateExternalIdentityClaims(value) {
|
|
10579
|
+
if (!isRecord(value) || value["role"] !== "authenticated" || typeof value["sub"] !== "string" || !value["sub"] || typeof value["exp"] !== "number" || !Number.isSafeInteger(value["exp"]) || value["exp"] * 1000 <= Date.now()) {
|
|
10580
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10581
|
+
}
|
|
10582
|
+
for (const key of ["iss", "aud", "email"]) {
|
|
10583
|
+
if (key in value && typeof value[key] !== "string")
|
|
10584
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10585
|
+
}
|
|
10586
|
+
if ("iat" in value && (typeof value["iat"] !== "number" || !Number.isSafeInteger(value["iat"])) || "aal" in value && value["aal"] !== "aal1" && value["aal"] !== "aal2" || "amr" in value && (!Array.isArray(value["amr"]) || !value["amr"].every((entry) => isRecord(entry) && typeof entry["method"] === "string" && typeof entry["timestamp"] === "number" && Number.isFinite(entry["timestamp"])))) {
|
|
10587
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10588
|
+
}
|
|
10589
|
+
return value;
|
|
10590
|
+
}
|
|
10248
10591
|
// src/runtime/gen-types.ts
|
|
10249
10592
|
var JSON_TYPE = "Json";
|
|
10250
10593
|
async function generateTypes(db, schema = "public") {
|
|
@@ -10657,6 +11000,7 @@ var CORS_HEADERS = {
|
|
|
10657
11000
|
"access-control-max-age": "86400"
|
|
10658
11001
|
};
|
|
10659
11002
|
async function createBackend(config = {}) {
|
|
11003
|
+
const mode = runtimeMode(config.runtimeMode);
|
|
10660
11004
|
const jwtSecret = config.jwtSecret ?? randomSecret();
|
|
10661
11005
|
const apiUrl = config.apiUrl ?? "http://localhost:54321";
|
|
10662
11006
|
const siteUrl = config.siteUrl ?? apiUrl;
|
|
@@ -10671,7 +11015,7 @@ async function createBackend(config = {}) {
|
|
|
10671
11015
|
const vaultKey = config.vaultKey ?? `supacloud-lite-vault:${jwtSecret}`;
|
|
10672
11016
|
assertSecretsSafe({ host: config.host, jwtSecret, vaultKeyDerived, warn: log });
|
|
10673
11017
|
const engine = config.engine;
|
|
10674
|
-
const db = await Database.create(engine ?? config.dataDir, { vaultKey });
|
|
11018
|
+
const db = await Database.create(engine ?? config.dataDir, { vaultKey, strict: mode === "strict" });
|
|
10675
11019
|
const cleanup = [];
|
|
10676
11020
|
const failStartup = async (e) => {
|
|
10677
11021
|
for (const fn of cleanup.reverse())
|
|
@@ -10688,6 +11032,11 @@ async function createBackend(config = {}) {
|
|
|
10688
11032
|
} catch (e) {
|
|
10689
11033
|
await failStartup(e);
|
|
10690
11034
|
}
|
|
11035
|
+
const graphqlCapability = config.graphql?.enabled === false ? { status: "disabled", extension: "pg_graphql" } : await inspectGraphql(db.engine).catch(failStartup);
|
|
11036
|
+
if (config.graphql?.enabled === true && graphqlCapability.status !== "supported") {
|
|
11037
|
+
await failStartup(new Error(`${graphqlCapability.reason}: GraphQL requires an installed pg_graphql extension with graphql.resolve; PGlite and stock native binaries do not bundle it`));
|
|
11038
|
+
}
|
|
11039
|
+
const graphql = await Promise.resolve().then(() => new GraphqlHandler(db, graphqlCapability, config.graphql, config.dbSchemas)).catch(failStartup);
|
|
10691
11040
|
const pgredis = await PgredisCache.create(db).catch(failStartup);
|
|
10692
11041
|
const { anonKey, serviceRoleKey } = await mintProjectApiKeys(jwtSecret);
|
|
10693
11042
|
const exposedSchemas = [...new Set(config.dbSchemas ?? ["public", "pgmq_public"])];
|
|
@@ -10728,7 +11077,13 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10728
11077
|
rateLimiter: new RateLimiter(config.authRateLimits)
|
|
10729
11078
|
});
|
|
10730
11079
|
cleanup.push(() => auth.stop());
|
|
10731
|
-
const realtime = new RealtimeEngine(db, jwtSecret)
|
|
11080
|
+
const realtime = new RealtimeEngine(db, jwtSecret, config.externalIdentity ? async (token) => {
|
|
11081
|
+
const request = new Request(`${apiUrl}/realtime/v1`, { headers: { authorization: `Bearer ${token}` } });
|
|
11082
|
+
const context = await resolveContext(request, new URL(request.url));
|
|
11083
|
+
if (context instanceof Response)
|
|
11084
|
+
throw new Error("Realtime authentication denied");
|
|
11085
|
+
return context;
|
|
11086
|
+
} : undefined);
|
|
10732
11087
|
const webhooks = new WebhooksService(db, config.webhookFetch, (d) => log(`[webhook] ${d.event.type} ${d.event.schema}.${d.event.table} -> ${d.webhook.url} ${d.ok ? d.status : "FAILED " + (d.error ?? "")}`), isNetworkExposed(config.host));
|
|
10733
11088
|
const cron = new CronService(db);
|
|
10734
11089
|
const retention = new RetentionService(db, config.retention);
|
|
@@ -10763,7 +11118,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10763
11118
|
} catch (error) {
|
|
10764
11119
|
await failStartup(error);
|
|
10765
11120
|
}
|
|
10766
|
-
const functions = new FunctionsHandler(fnMap, fnEnv, pgredis);
|
|
11121
|
+
const functions = await Promise.resolve().then(() => new FunctionsHandler(fnMap, fnEnv, pgredis, mode)).catch(failStartup);
|
|
10767
11122
|
async function resolveContext(req, url) {
|
|
10768
11123
|
const authz = req.headers.get("authorization");
|
|
10769
11124
|
const bearer = authz?.toLowerCase().startsWith("bearer ") ? authz.slice(7) : null;
|
|
@@ -10774,7 +11129,15 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10774
11129
|
headers: { "content-type": "application/json" }
|
|
10775
11130
|
}));
|
|
10776
11131
|
}
|
|
10777
|
-
|
|
11132
|
+
let claims = await verifyJwt(token, jwtSecret);
|
|
11133
|
+
if (config.externalIdentity && token !== anonKey && token !== serviceRoleKey) {
|
|
11134
|
+
try {
|
|
11135
|
+
claims = validateExternalIdentityClaims(await config.externalIdentity(req));
|
|
11136
|
+
} catch (error) {
|
|
11137
|
+
const status = error instanceof ApiError && [401, 403, 503].includes(error.status) ? error.status : 503;
|
|
11138
|
+
return withCors(Response.json({ message: status === 503 ? "Identity verification unavailable" : "External access denied" }, { status }));
|
|
11139
|
+
}
|
|
11140
|
+
}
|
|
10778
11141
|
if (!claims) {
|
|
10779
11142
|
return withCors(new Response(JSON.stringify({ message: "Invalid API key" }), {
|
|
10780
11143
|
status: 401,
|
|
@@ -10816,7 +11179,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10816
11179
|
return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
|
|
10817
11180
|
}
|
|
10818
11181
|
}
|
|
10819
|
-
if (config.authEnabled === false && path.startsWith("/auth/v1")) {
|
|
11182
|
+
if ((config.authEnabled === false || config.externalIdentity) && path.startsWith("/auth/v1")) {
|
|
10820
11183
|
return withCors(new Response(JSON.stringify({ message: "Auth service is disabled" }), {
|
|
10821
11184
|
status: 404,
|
|
10822
11185
|
headers: { "content-type": "application/json" }
|
|
@@ -10848,6 +11211,8 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10848
11211
|
return ctx;
|
|
10849
11212
|
if (path.startsWith("/rest/v1"))
|
|
10850
11213
|
return withCors(await rest.handle(req, ctx, url));
|
|
11214
|
+
if (path === "/graphql/v1" || path === "/graphql/v1/")
|
|
11215
|
+
return withCors(await graphql.handle(req, ctx));
|
|
10851
11216
|
if (path.startsWith("/functions/v1"))
|
|
10852
11217
|
return withCors(await functions.handle(req, ctx, url));
|
|
10853
11218
|
if (path.startsWith("/storage/v1"))
|
|
@@ -10892,6 +11257,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10892
11257
|
return loggedFetch(request);
|
|
10893
11258
|
}, { preconnect: fetch.preconnect });
|
|
10894
11259
|
return {
|
|
11260
|
+
graphql: graphqlCapability,
|
|
10895
11261
|
fetch: publicFetch,
|
|
10896
11262
|
db,
|
|
10897
11263
|
realtime,
|
|
@@ -10906,7 +11272,16 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10906
11272
|
logs,
|
|
10907
11273
|
inbox,
|
|
10908
11274
|
smsInbox,
|
|
10909
|
-
migrate: (migrations, seedSql) =>
|
|
11275
|
+
migrate: async (migrations, seedSql) => {
|
|
11276
|
+
const applied = await db.runMigrations(migrations, seedSql);
|
|
11277
|
+
if (config.graphql?.enabled !== false) {
|
|
11278
|
+
const capability = await inspectGraphql(db.engine);
|
|
11279
|
+
delete graphqlCapability.version;
|
|
11280
|
+
delete graphqlCapability.reason;
|
|
11281
|
+
Object.assign(graphqlCapability, capability);
|
|
11282
|
+
}
|
|
11283
|
+
return applied;
|
|
11284
|
+
},
|
|
10910
11285
|
close: () => {
|
|
10911
11286
|
closePromise ??= (async () => {
|
|
10912
11287
|
let firstError;
|
|
@@ -11233,12 +11608,19 @@ class PgWireClient {
|
|
|
11233
11608
|
onNotification = null;
|
|
11234
11609
|
static async connect(opts) {
|
|
11235
11610
|
const client = new PgWireClient;
|
|
11236
|
-
|
|
11611
|
+
try {
|
|
11612
|
+
await client.open(opts);
|
|
11613
|
+
} catch (error) {
|
|
11614
|
+
client.socket.destroy();
|
|
11615
|
+
throw error;
|
|
11616
|
+
}
|
|
11237
11617
|
return client;
|
|
11238
11618
|
}
|
|
11239
11619
|
open(opts) {
|
|
11240
11620
|
return new Promise((resolve3, reject) => {
|
|
11621
|
+
let startupComplete = false;
|
|
11241
11622
|
this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
|
|
11623
|
+
this.socket.setTimeout(1e4, () => this.socket.destroy(new Error("PostgreSQL handshake timed out")));
|
|
11242
11624
|
this.socket.on("error", (e) => {
|
|
11243
11625
|
if (this.pending)
|
|
11244
11626
|
this.pending.reject(e);
|
|
@@ -11247,6 +11629,8 @@ class PgWireClient {
|
|
|
11247
11629
|
this.socket.on("close", () => {
|
|
11248
11630
|
this.closed = true;
|
|
11249
11631
|
this.pending?.reject(new Error("connection closed"));
|
|
11632
|
+
if (!startupComplete)
|
|
11633
|
+
reject(new Error("connection closed during PostgreSQL handshake"));
|
|
11250
11634
|
});
|
|
11251
11635
|
this.socket.on("connect", () => {
|
|
11252
11636
|
const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
|
|
@@ -11330,6 +11714,8 @@ class PgWireClient {
|
|
|
11330
11714
|
reject(new PgWireError(parseErrorFields(payload)));
|
|
11331
11715
|
return;
|
|
11332
11716
|
} else if (type === 90) {
|
|
11717
|
+
startupComplete = true;
|
|
11718
|
+
this.socket.setTimeout(0);
|
|
11333
11719
|
this.socket.off("data", startupHandler);
|
|
11334
11720
|
this.socket.on("data", (c) => {
|
|
11335
11721
|
this.buffer = Buffer.concat([this.buffer, c]);
|
|
@@ -11607,27 +11993,6 @@ function parsePgArray(text) {
|
|
|
11607
11993
|
return out;
|
|
11608
11994
|
}
|
|
11609
11995
|
|
|
11610
|
-
// src/runtime/db/engine.ts
|
|
11611
|
-
class Mutex {
|
|
11612
|
-
tail = Promise.resolve();
|
|
11613
|
-
async lock() {
|
|
11614
|
-
let release;
|
|
11615
|
-
const next = new Promise((r) => release = r);
|
|
11616
|
-
const prev = this.tail;
|
|
11617
|
-
this.tail = this.tail.then(() => next);
|
|
11618
|
-
await prev;
|
|
11619
|
-
return release;
|
|
11620
|
-
}
|
|
11621
|
-
async run(fn) {
|
|
11622
|
-
const release = await this.lock();
|
|
11623
|
-
try {
|
|
11624
|
-
return await fn();
|
|
11625
|
-
} finally {
|
|
11626
|
-
release();
|
|
11627
|
-
}
|
|
11628
|
-
}
|
|
11629
|
-
}
|
|
11630
|
-
|
|
11631
11996
|
// src/runtime/node/native/wire-engine.ts
|
|
11632
11997
|
async function buildWireEngine(options) {
|
|
11633
11998
|
const queryClient = await options.connect();
|
|
@@ -11666,9 +12031,9 @@ async function buildWireEngine(options) {
|
|
|
11666
12031
|
return queryMutex.run(async () => {
|
|
11667
12032
|
await queryClient.exec("begin");
|
|
11668
12033
|
try {
|
|
11669
|
-
const
|
|
12034
|
+
const response2 = await callback(transactionClient);
|
|
11670
12035
|
await queryClient.exec("commit");
|
|
11671
|
-
return
|
|
12036
|
+
return response2;
|
|
11672
12037
|
} catch (error) {
|
|
11673
12038
|
await queryClient.exec("rollback").catch(() => {});
|
|
11674
12039
|
throw error;
|
|
@@ -11740,6 +12105,14 @@ function toPgArrayLiteral(array) {
|
|
|
11740
12105
|
var DEFAULT_PG_VERSION = "17.7.0";
|
|
11741
12106
|
var POSTGRES_MIRROR_URL_ERROR = "SUPACLOUD_LITE_POSTGRES_MIRROR must be an absolute HTTPS URL or a loopback HTTP URL";
|
|
11742
12107
|
var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
|
|
12108
|
+
function nativePostgresMajor(installDir = process.env.SUPACLOUD_LITE_POSTGRES_DIR) {
|
|
12109
|
+
if (!installDir)
|
|
12110
|
+
return NATIVE_POSTGRES_MAJOR;
|
|
12111
|
+
const version = execFileSync(join(installDir, "bin", "postgres"), ["--version"], { encoding: "utf8", timeout: 1e4 }).match(/PostgreSQL\)\s+(\d+)/)?.[1];
|
|
12112
|
+
if (!version)
|
|
12113
|
+
throw new Error("unable to identify the PostgreSQL installation major version");
|
|
12114
|
+
return version;
|
|
12115
|
+
}
|
|
11743
12116
|
var GLIBC_DYNAMIC_LOADERS = {
|
|
11744
12117
|
x64: [
|
|
11745
12118
|
"/lib64/ld-linux-x86-64.so.2",
|
|
@@ -11889,10 +12262,10 @@ async function fetchRelease(url) {
|
|
|
11889
12262
|
let lastError;
|
|
11890
12263
|
for (let attempt = 1;attempt <= 3; attempt++) {
|
|
11891
12264
|
try {
|
|
11892
|
-
const
|
|
11893
|
-
if (
|
|
11894
|
-
return
|
|
11895
|
-
lastError = new Error(`failed to download ${url}: HTTP ${
|
|
12265
|
+
const response2 = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
12266
|
+
if (response2.ok || response2.status < 500)
|
|
12267
|
+
return response2;
|
|
12268
|
+
lastError = new Error(`failed to download ${url}: HTTP ${response2.status}`);
|
|
11896
12269
|
} catch (error) {
|
|
11897
12270
|
lastError = error;
|
|
11898
12271
|
}
|
|
@@ -11918,8 +12291,16 @@ async function createNativeEngine(opts) {
|
|
|
11918
12291
|
let postgres;
|
|
11919
12292
|
let removeExitHandler;
|
|
11920
12293
|
try {
|
|
11921
|
-
const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
|
|
12294
|
+
const installDir = opts.installDir ?? process.env.SUPACLOUD_LITE_POSTGRES_DIR ?? await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
|
|
11922
12295
|
const bin = (name) => join(installDir, "bin", name);
|
|
12296
|
+
if (!existsSync2(bin("postgres")) || !existsSync2(bin("initdb"))) {
|
|
12297
|
+
throw new Error("PostgreSQL installation requires bin/postgres and bin/initdb");
|
|
12298
|
+
}
|
|
12299
|
+
const serverVersion = nativePostgresMajor(installDir);
|
|
12300
|
+
const storedMajor = existsSync2(join(opts.dataDir, "PG_VERSION")) ? readFileSync(join(opts.dataDir, "PG_VERSION"), "utf8").trim() : undefined;
|
|
12301
|
+
if (!serverVersion || storedMajor && storedMajor !== serverVersion) {
|
|
12302
|
+
throw new Error("PostgreSQL installation major version does not match the data directory; use a logical migration");
|
|
12303
|
+
}
|
|
11923
12304
|
if (!existsSync2(join(opts.dataDir, "PG_VERSION"))) {
|
|
11924
12305
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
11925
12306
|
try {
|
|
@@ -12047,6 +12428,7 @@ async function computeDbDiff(opts) {
|
|
|
12047
12428
|
let operationFailed = false;
|
|
12048
12429
|
try {
|
|
12049
12430
|
shadow = await createBackend({
|
|
12431
|
+
runtimeMode: opts.runtimeMode,
|
|
12050
12432
|
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
12051
12433
|
migrations: opts.migrations,
|
|
12052
12434
|
startRuntimeServices: false
|
|
@@ -12054,6 +12436,7 @@ async function computeDbDiff(opts) {
|
|
|
12054
12436
|
const liveEngine = unclaimedLiveEngine;
|
|
12055
12437
|
unclaimedLiveEngine = undefined;
|
|
12056
12438
|
live = await createBackend({
|
|
12439
|
+
runtimeMode: opts.runtimeMode,
|
|
12057
12440
|
engine: liveEngine,
|
|
12058
12441
|
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
12059
12442
|
migrations: opts.migrations,
|
|
@@ -12077,11 +12460,11 @@ async function computeDbDiff(opts) {
|
|
|
12077
12460
|
function shadowNativeDataDir() {
|
|
12078
12461
|
return join2(mkdtempSync2(join2(tmpdir2(), "supacloud-lite-shadow-")), "pg");
|
|
12079
12462
|
}
|
|
12080
|
-
async function createTemporaryNativeEngine() {
|
|
12463
|
+
async function createTemporaryNativeEngine(installDir) {
|
|
12081
12464
|
const dataDir = shadowNativeDataDir();
|
|
12082
12465
|
let engine;
|
|
12083
12466
|
try {
|
|
12084
|
-
engine = await createNativeEngine({ dataDir });
|
|
12467
|
+
engine = await createNativeEngine({ dataDir, installDir });
|
|
12085
12468
|
} catch (error) {
|
|
12086
12469
|
try {
|
|
12087
12470
|
await rm(dirname2(dataDir), { recursive: true, force: true });
|
|
@@ -12109,6 +12492,7 @@ async function pullSchema(opts) {
|
|
|
12109
12492
|
let operationFailed = false;
|
|
12110
12493
|
try {
|
|
12111
12494
|
shadow = await createBackend({
|
|
12495
|
+
runtimeMode: opts.runtimeMode,
|
|
12112
12496
|
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
12113
12497
|
migrations: opts.migrations,
|
|
12114
12498
|
startRuntimeServices: false
|
|
@@ -12116,6 +12500,7 @@ async function pullSchema(opts) {
|
|
|
12116
12500
|
const liveEngine = unclaimedLiveEngine;
|
|
12117
12501
|
unclaimedLiveEngine = undefined;
|
|
12118
12502
|
live = await createBackend({
|
|
12503
|
+
runtimeMode: opts.runtimeMode,
|
|
12119
12504
|
engine: liveEngine,
|
|
12120
12505
|
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
12121
12506
|
migrations: opts.migrations,
|
|
@@ -12164,6 +12549,7 @@ import { dirname as dirname3, resolve as resolve3 } from "path";
|
|
|
12164
12549
|
import { pathToFileURL } from "url";
|
|
12165
12550
|
|
|
12166
12551
|
// node_modules/@supacloud/db/dist/index.js
|
|
12552
|
+
import { createHash as createHash3 } from "crypto";
|
|
12167
12553
|
var TABLES_SQL = `
|
|
12168
12554
|
SELECT n.nspname AS schema,
|
|
12169
12555
|
c.relname AS name,
|
|
@@ -12490,6 +12876,150 @@ async function lintModule(module, readFile2) {
|
|
|
12490
12876
|
}
|
|
12491
12877
|
return issues;
|
|
12492
12878
|
}
|
|
12879
|
+
var TOKEN = /__[A-Z][A-Z0-9_]*__/g;
|
|
12880
|
+
var RESERVED_TOKEN = /__SC_BINDING_[A-Z0-9_]+__/;
|
|
12881
|
+
var HASH = /^[a-f0-9]{64}$/;
|
|
12882
|
+
function migrationBindingSha256(value) {
|
|
12883
|
+
return createHash3("sha256").update(value).digest("hex");
|
|
12884
|
+
}
|
|
12885
|
+
function record(value) {
|
|
12886
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
12887
|
+
throw new Error("Invalid migration binding object");
|
|
12888
|
+
}
|
|
12889
|
+
return value;
|
|
12890
|
+
}
|
|
12891
|
+
function keys(row, expected) {
|
|
12892
|
+
if (Object.keys(row).sort().join(",") !== expected.sort().join(",")) {
|
|
12893
|
+
throw new Error("Unexpected migration binding fields");
|
|
12894
|
+
}
|
|
12895
|
+
}
|
|
12896
|
+
function validTarget(value) {
|
|
12897
|
+
const target2 = record(value);
|
|
12898
|
+
keys(target2, ["environment", "projectRef"]);
|
|
12899
|
+
if (typeof target2.environment !== "string" || !/^[a-z][a-z0-9-]{0,62}$/.test(target2.environment) || typeof target2.projectRef !== "string" || !/^[a-z0-9][a-z0-9_-]{0,62}$/.test(target2.projectRef)) {
|
|
12900
|
+
throw new Error("Invalid migration binding target");
|
|
12901
|
+
}
|
|
12902
|
+
return target2;
|
|
12903
|
+
}
|
|
12904
|
+
function validFile(file) {
|
|
12905
|
+
return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
|
|
12906
|
+
}
|
|
12907
|
+
function parseMigrationBindingManifest(value) {
|
|
12908
|
+
const manifest = record(value);
|
|
12909
|
+
keys(manifest, ["schema", "targets", "templates"]);
|
|
12910
|
+
if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
|
|
12911
|
+
throw new Error("Invalid migration binding manifest");
|
|
12912
|
+
}
|
|
12913
|
+
const targets = manifest.targets.map(validTarget);
|
|
12914
|
+
if (new Set(targets.map((target2) => target2.environment)).size !== targets.length) {
|
|
12915
|
+
throw new Error("Duplicate migration binding environment");
|
|
12916
|
+
}
|
|
12917
|
+
const files = new Set;
|
|
12918
|
+
const templates = manifest.templates.map((value2) => {
|
|
12919
|
+
const template = record(value2);
|
|
12920
|
+
keys(template, ["file", "templateSha256", "parameters"]);
|
|
12921
|
+
if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
|
|
12922
|
+
throw new Error("Invalid or duplicate migration binding template");
|
|
12923
|
+
}
|
|
12924
|
+
files.add(template.file);
|
|
12925
|
+
const placeholders = new Set;
|
|
12926
|
+
const parameters = template.parameters.map((value3) => {
|
|
12927
|
+
const parameter = record(value3);
|
|
12928
|
+
keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
|
|
12929
|
+
if (typeof parameter.placeholder !== "string" || !/^__[A-Z][A-Z0-9_]*__$/.test(parameter.placeholder) || placeholders.has(parameter.placeholder) || typeof parameter.variable !== "string" || !/^[A-Z][A-Z0-9_]*$/.test(parameter.variable) || !["uuid", "https-url", "resource-name"].includes(String(parameter.type)) || !Number.isSafeInteger(parameter.occurrences) || Number(parameter.occurrences) < 1) {
|
|
12930
|
+
throw new Error("Invalid or duplicate migration binding parameter");
|
|
12931
|
+
}
|
|
12932
|
+
placeholders.add(parameter.placeholder);
|
|
12933
|
+
return parameter;
|
|
12934
|
+
});
|
|
12935
|
+
return { file: template.file, templateSha256: template.templateSha256, parameters };
|
|
12936
|
+
});
|
|
12937
|
+
return { schema: "supacloud.migration-bindings.v1", targets, templates };
|
|
12938
|
+
}
|
|
12939
|
+
function bindingValue(parameter, values) {
|
|
12940
|
+
const value = Object.hasOwn(values, parameter.variable) ? values[parameter.variable] : undefined;
|
|
12941
|
+
if (typeof value !== "string" || !value || value.trim() !== value) {
|
|
12942
|
+
throw new Error(`Missing or invalid migration binding variable: ${parameter.variable}`);
|
|
12943
|
+
}
|
|
12944
|
+
let valid = false;
|
|
12945
|
+
if (parameter.type === "uuid") {
|
|
12946
|
+
valid = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(value);
|
|
12947
|
+
} else if (parameter.type === "resource-name") {
|
|
12948
|
+
valid = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/.test(value);
|
|
12949
|
+
} else if (/^https:\/\/[A-Za-z0-9._~:/?#[\]@!&()*+,;=%-]+$/.test(value)) {
|
|
12950
|
+
try {
|
|
12951
|
+
const url = new URL(value);
|
|
12952
|
+
valid = url.protocol === "https:" && Boolean(url.hostname) && !url.username && !url.password;
|
|
12953
|
+
} catch {}
|
|
12954
|
+
}
|
|
12955
|
+
if (!valid || value.includes("__"))
|
|
12956
|
+
throw new Error(`Invalid ${parameter.type} migration binding: ${parameter.variable}`);
|
|
12957
|
+
return value;
|
|
12958
|
+
}
|
|
12959
|
+
function renderMigrationBindings(options) {
|
|
12960
|
+
const manifest = parseMigrationBindingManifest(options.manifest);
|
|
12961
|
+
const target2 = validTarget(options.target);
|
|
12962
|
+
if (!manifest.targets.some((candidate) => candidate.environment === target2.environment && candidate.projectRef === target2.projectRef)) {
|
|
12963
|
+
throw new Error("Migration binding target is not registered in the manifest");
|
|
12964
|
+
}
|
|
12965
|
+
const sources = new Map;
|
|
12966
|
+
for (const migration of options.migrations) {
|
|
12967
|
+
if (!validFile(migration.file) || sources.has(migration.file) || typeof migration.sql !== "string" || !migration.sql.trim()) {
|
|
12968
|
+
throw new Error("Invalid, empty, or duplicate migration binding source");
|
|
12969
|
+
}
|
|
12970
|
+
sources.set(migration.file, migration);
|
|
12971
|
+
}
|
|
12972
|
+
if (manifest.templates.some((template) => !sources.has(template.file))) {
|
|
12973
|
+
throw new Error("Migration binding manifest references a missing source");
|
|
12974
|
+
}
|
|
12975
|
+
const templates = new Map(manifest.templates.map((template) => [template.file, template]));
|
|
12976
|
+
const declaredTokens = new Set(manifest.templates.flatMap((template) => template.parameters.map((parameter) => parameter.placeholder)));
|
|
12977
|
+
const files = [];
|
|
12978
|
+
const migrations = options.migrations.map((migration) => {
|
|
12979
|
+
const template = templates.get(migration.file);
|
|
12980
|
+
const templateSha256 = migrationBindingSha256(migration.sql);
|
|
12981
|
+
if (template && templateSha256 !== template.templateSha256) {
|
|
12982
|
+
throw new Error(`Migration binding template checksum mismatch: ${migration.file}`);
|
|
12983
|
+
}
|
|
12984
|
+
const parameters = new Map(template?.parameters.map((parameter) => [parameter.placeholder, parameter]));
|
|
12985
|
+
const counts = new Map;
|
|
12986
|
+
const sql = migration.sql.replace(TOKEN, (token, offset) => {
|
|
12987
|
+
const parameter = parameters.get(token);
|
|
12988
|
+
if (!parameter) {
|
|
12989
|
+
if (RESERVED_TOKEN.test(token) || declaredTokens.has(token)) {
|
|
12990
|
+
throw new Error(`Undeclared migration binding placeholder: ${migration.file}`);
|
|
12991
|
+
}
|
|
12992
|
+
return token;
|
|
12993
|
+
}
|
|
12994
|
+
if (migration.sql[offset - 1] !== "'" || migration.sql[offset + token.length] !== "'") {
|
|
12995
|
+
throw new Error(`Migration bindings must occupy a complete SQL string literal: ${migration.file}`);
|
|
12996
|
+
}
|
|
12997
|
+
counts.set(token, (counts.get(token) || 0) + 1);
|
|
12998
|
+
return bindingValue(parameter, options.values);
|
|
12999
|
+
});
|
|
13000
|
+
for (const parameter of parameters.values()) {
|
|
13001
|
+
if (counts.get(parameter.placeholder) !== parameter.occurrences) {
|
|
13002
|
+
throw new Error(`Migration binding occurrence mismatch: ${migration.file}`);
|
|
13003
|
+
}
|
|
13004
|
+
}
|
|
13005
|
+
files.push({
|
|
13006
|
+
file: migration.file,
|
|
13007
|
+
templateSha256,
|
|
13008
|
+
renderedSqlSha256: migrationBindingSha256(sql),
|
|
13009
|
+
parameters: [...parameters.values()].map(({ variable, type }) => ({ variable, type }))
|
|
13010
|
+
});
|
|
13011
|
+
return { file: migration.file, sql };
|
|
13012
|
+
});
|
|
13013
|
+
return {
|
|
13014
|
+
migrations,
|
|
13015
|
+
attestation: {
|
|
13016
|
+
schema: "supacloud.migration-binding-attestation.v1",
|
|
13017
|
+
...target2,
|
|
13018
|
+
manifestSha256: migrationBindingSha256(JSON.stringify(manifest)),
|
|
13019
|
+
files: files.sort((left, right) => left.file.localeCompare(right.file))
|
|
13020
|
+
}
|
|
13021
|
+
};
|
|
13022
|
+
}
|
|
12493
13023
|
|
|
12494
13024
|
// src/runtime/node/db-check.ts
|
|
12495
13025
|
async function loadDatabaseModules(moduleFile) {
|
|
@@ -12547,8 +13077,16 @@ function formatDatabaseModuleCheck(result) {
|
|
|
12547
13077
|
|
|
12548
13078
|
// src/runtime/node/native/readiness.ts
|
|
12549
13079
|
function liteCapabilities(engine, replicationProfile) {
|
|
13080
|
+
const common = {
|
|
13081
|
+
graphql: { status: "unverified", extension: "pg_graphql", reason: "DATABASE_NOT_INSPECTED" },
|
|
13082
|
+
runtime_mode: "development",
|
|
13083
|
+
identity: { mode: "local", verification: "built-in" },
|
|
13084
|
+
migrations: { formats: ["flat-sql", "timestamp-folder"], content_check: "supported", target_bindings: "supported" },
|
|
13085
|
+
project_release_manifests: "unsupported"
|
|
13086
|
+
};
|
|
12550
13087
|
if (engine === "pglite") {
|
|
12551
13088
|
return {
|
|
13089
|
+
...common,
|
|
12552
13090
|
engine,
|
|
12553
13091
|
state_machine_sql: "supported",
|
|
12554
13092
|
durable_workflows: "supported",
|
|
@@ -12560,6 +13098,7 @@ function liteCapabilities(engine, replicationProfile) {
|
|
|
12560
13098
|
};
|
|
12561
13099
|
}
|
|
12562
13100
|
return {
|
|
13101
|
+
...common,
|
|
12563
13102
|
engine,
|
|
12564
13103
|
state_machine_sql: "supported",
|
|
12565
13104
|
durable_workflows: "supported",
|
|
@@ -12775,162 +13314,16 @@ function sameStrings(left, right) {
|
|
|
12775
13314
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
12776
13315
|
}
|
|
12777
13316
|
|
|
12778
|
-
// src/runtime/node/project.ts
|
|
12779
|
-
import { readdir, readFile as readFile3 } from "fs/promises";
|
|
12780
|
-
import { join as join3 } from "path";
|
|
12781
|
-
async function loadSupabaseProject(projectDir, seed = {}) {
|
|
12782
|
-
const migrationsDir = join3(projectDir, "supabase", "migrations");
|
|
12783
|
-
const migrations = [];
|
|
12784
|
-
let entries = [];
|
|
12785
|
-
try {
|
|
12786
|
-
entries = await readdir(migrationsDir);
|
|
12787
|
-
} catch (error) {
|
|
12788
|
-
if (!isNotFound(error))
|
|
12789
|
-
throw error;
|
|
12790
|
-
}
|
|
12791
|
-
for (const entry of entries.sort()) {
|
|
12792
|
-
if (!entry.endsWith(".sql"))
|
|
12793
|
-
continue;
|
|
12794
|
-
const sql = await readFile3(join3(migrationsDir, entry), "utf8");
|
|
12795
|
-
migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
|
|
12796
|
-
}
|
|
12797
|
-
let seedSql;
|
|
12798
|
-
if (seed.enabled !== false) {
|
|
12799
|
-
const parts = [];
|
|
12800
|
-
const supabaseDir = join3(projectDir, "supabase");
|
|
12801
|
-
for (const configuredPath of seed.paths ?? ["seed.sql"]) {
|
|
12802
|
-
const pattern = configuredPath.replace(/^\.\//, "");
|
|
12803
|
-
const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
|
|
12804
|
-
for (const relativePath of matches) {
|
|
12805
|
-
try {
|
|
12806
|
-
parts.push(await readFile3(join3(supabaseDir, relativePath), "utf8"));
|
|
12807
|
-
} catch (error) {
|
|
12808
|
-
if (!isNotFound(error))
|
|
12809
|
-
throw error;
|
|
12810
|
-
}
|
|
12811
|
-
}
|
|
12812
|
-
}
|
|
12813
|
-
if (parts.length)
|
|
12814
|
-
seedSql = parts.join(`
|
|
12815
|
-
`);
|
|
12816
|
-
}
|
|
12817
|
-
return { migrations, seedSql };
|
|
12818
|
-
}
|
|
12819
|
-
function isNotFound(error) {
|
|
12820
|
-
return error.code === "ENOENT";
|
|
12821
|
-
}
|
|
12822
|
-
|
|
12823
|
-
// src/project-runtime.ts
|
|
12824
|
-
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
12825
|
-
import { dirname as dirname6, isAbsolute, join as join8, parse, relative, resolve as resolve4 } from "path";
|
|
12826
|
-
|
|
12827
|
-
// src/runtime/node/bun-server.ts
|
|
12828
|
-
async function serveBun(backend, opts = {}) {
|
|
12829
|
-
const host = opts.host ?? "127.0.0.1";
|
|
12830
|
-
const server = Bun.serve({
|
|
12831
|
-
port: opts.port ?? 54321,
|
|
12832
|
-
hostname: host,
|
|
12833
|
-
async fetch(req, srv) {
|
|
12834
|
-
const url = new URL(req.url);
|
|
12835
|
-
if (url.pathname.startsWith("/realtime/v1") && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
12836
|
-
const ok = srv.upgrade(req, { data: { vsn: url.searchParams.get("vsn") ?? "1.0.0" } });
|
|
12837
|
-
if (ok)
|
|
12838
|
-
return;
|
|
12839
|
-
return new Response("upgrade failed", { status: 400 });
|
|
12840
|
-
}
|
|
12841
|
-
const headers = new Headers(req.headers);
|
|
12842
|
-
headers.delete("x-supacloud-lite-remote-addr");
|
|
12843
|
-
const ip = srv.requestIP(req)?.address;
|
|
12844
|
-
if (ip)
|
|
12845
|
-
headers.set("x-supacloud-lite-remote-addr", ip);
|
|
12846
|
-
return backend.fetch(new Request(req, { headers }));
|
|
12847
|
-
},
|
|
12848
|
-
websocket: {
|
|
12849
|
-
open(ws) {
|
|
12850
|
-
const session = backend.realtime.connect({
|
|
12851
|
-
send: (data) => ws.send(data),
|
|
12852
|
-
close: (code, reason) => ws.close(code, reason)
|
|
12853
|
-
}, { vsn: ws.data.vsn });
|
|
12854
|
-
ws.data.session = session;
|
|
12855
|
-
},
|
|
12856
|
-
message(ws, message2) {
|
|
12857
|
-
ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
|
|
12858
|
-
},
|
|
12859
|
-
close(ws) {
|
|
12860
|
-
ws.data.session?.onClose();
|
|
12861
|
-
}
|
|
12862
|
-
}
|
|
12863
|
-
});
|
|
12864
|
-
return {
|
|
12865
|
-
server,
|
|
12866
|
-
port: server.port ?? opts.port ?? 54321,
|
|
12867
|
-
url: `http://${host}:${server.port ?? opts.port ?? 54321}`,
|
|
12868
|
-
close: async () => {
|
|
12869
|
-
await server.stop(true);
|
|
12870
|
-
}
|
|
12871
|
-
};
|
|
12872
|
-
}
|
|
12873
|
-
|
|
12874
|
-
// src/runtime/node/fs-driver.ts
|
|
12875
|
-
import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
12876
|
-
import { dirname as dirname4, join as join4, normalize, sep } from "path";
|
|
12877
|
-
|
|
12878
|
-
class FsStorageDriver {
|
|
12879
|
-
root;
|
|
12880
|
-
constructor(root) {
|
|
12881
|
-
this.root = root;
|
|
12882
|
-
}
|
|
12883
|
-
resolve(key) {
|
|
12884
|
-
const path = normalize(join4(this.root, key));
|
|
12885
|
-
if (!path.startsWith(normalize(this.root) + sep)) {
|
|
12886
|
-
throw new Error(`invalid storage key: ${key}`);
|
|
12887
|
-
}
|
|
12888
|
-
return path;
|
|
12889
|
-
}
|
|
12890
|
-
async put(key, data) {
|
|
12891
|
-
const path = this.resolve(key);
|
|
12892
|
-
await mkdir3(dirname4(path), { recursive: true });
|
|
12893
|
-
const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
12894
|
-
try {
|
|
12895
|
-
await writeFile3(temporaryPath, data);
|
|
12896
|
-
await rename(temporaryPath, path);
|
|
12897
|
-
} catch (error) {
|
|
12898
|
-
await rm2(temporaryPath, { force: true }).catch(() => {});
|
|
12899
|
-
throw error;
|
|
12900
|
-
}
|
|
12901
|
-
}
|
|
12902
|
-
async get(key) {
|
|
12903
|
-
try {
|
|
12904
|
-
return new Uint8Array(await readFile4(this.resolve(key)));
|
|
12905
|
-
} catch (e) {
|
|
12906
|
-
if (e.code === "ENOENT")
|
|
12907
|
-
return null;
|
|
12908
|
-
throw e;
|
|
12909
|
-
}
|
|
12910
|
-
}
|
|
12911
|
-
async getBlob(key) {
|
|
12912
|
-
const file = Bun.file(this.resolve(key));
|
|
12913
|
-
return await file.exists() ? file : null;
|
|
12914
|
-
}
|
|
12915
|
-
async delete(key) {
|
|
12916
|
-
await rm2(this.resolve(key), { force: true });
|
|
12917
|
-
}
|
|
12918
|
-
async deleteMany(keys) {
|
|
12919
|
-
for (const k of keys)
|
|
12920
|
-
await this.delete(k);
|
|
12921
|
-
}
|
|
12922
|
-
}
|
|
12923
|
-
|
|
12924
13317
|
// src/runtime/node/config-toml.ts
|
|
12925
13318
|
import { readFileSync as readFileSync2 } from "fs";
|
|
12926
|
-
import { join as
|
|
13319
|
+
import { join as join3 } from "path";
|
|
12927
13320
|
function emptyTable() {
|
|
12928
13321
|
return { values: new Map, children: new Map };
|
|
12929
13322
|
}
|
|
12930
13323
|
function loadConfigToml(projectDir, env = process.env) {
|
|
12931
13324
|
let text;
|
|
12932
13325
|
try {
|
|
12933
|
-
text = readFileSync2(
|
|
13326
|
+
text = readFileSync2(join3(projectDir, "supabase", "config.toml"), "utf8");
|
|
12934
13327
|
} catch {
|
|
12935
13328
|
return emptyTable();
|
|
12936
13329
|
}
|
|
@@ -13068,15 +13461,50 @@ function getByteSize(table, key) {
|
|
|
13068
13461
|
}
|
|
13069
13462
|
|
|
13070
13463
|
// src/runtime/node/load-config.ts
|
|
13071
|
-
function loadProjectConfig(projectDir, env = process.env) {
|
|
13464
|
+
function loadProjectConfig(projectDir, env = process.env, modeOverride) {
|
|
13072
13465
|
const root = loadConfigToml(projectDir, env);
|
|
13466
|
+
const mode = runtimeMode(modeOverride ?? env.SUPACLOUD_LITE_RUNTIME_MODE ?? getString(tableAt(root, "lite"), "runtime_mode"));
|
|
13073
13467
|
return {
|
|
13468
|
+
lite: readLite(root, mode),
|
|
13074
13469
|
auth: readAuth(root, env),
|
|
13075
13470
|
api: readApi(root),
|
|
13076
13471
|
storage: readStorage(root),
|
|
13077
13472
|
seed: readSeed(root),
|
|
13078
|
-
functions: readFunctions(root)
|
|
13473
|
+
functions: readFunctions(root, mode)
|
|
13474
|
+
};
|
|
13475
|
+
}
|
|
13476
|
+
function readLite(root, mode) {
|
|
13477
|
+
const graphql = tableAt(root, "lite.graphql");
|
|
13478
|
+
const bindings = tableAt(root, "lite.migration_bindings");
|
|
13479
|
+
const out = {
|
|
13480
|
+
runtimeMode: mode,
|
|
13481
|
+
graphql: {
|
|
13482
|
+
enabled: getBool(graphql, "enabled"),
|
|
13483
|
+
maxRequestBodyBytes: getInt(graphql, "max_request_body_bytes"),
|
|
13484
|
+
statementTimeoutMs: getInt(graphql, "statement_timeout_ms")
|
|
13485
|
+
},
|
|
13486
|
+
identityModule: getString(tableAt(root, "lite.identity"), "module")
|
|
13079
13487
|
};
|
|
13488
|
+
for (const key of ["max_request_body_bytes", "statement_timeout_ms"]) {
|
|
13489
|
+
const value = graphql?.values.get(key);
|
|
13490
|
+
if (value !== undefined && (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0)) {
|
|
13491
|
+
throw new Error(`invalid GraphQL option: ${key}`);
|
|
13492
|
+
}
|
|
13493
|
+
}
|
|
13494
|
+
if (graphql?.values.has("enabled") && getBool(graphql, "enabled") === undefined)
|
|
13495
|
+
throw new Error("invalid GraphQL enabled option");
|
|
13496
|
+
const identity = tableAt(root, "lite.identity");
|
|
13497
|
+
if (identity && !out.identityModule)
|
|
13498
|
+
throw new Error("Lite identity configuration requires a module");
|
|
13499
|
+
if (bindings) {
|
|
13500
|
+
const manifest = getString(bindings, "manifest");
|
|
13501
|
+
const environment = getString(bindings, "environment");
|
|
13502
|
+
const projectRef = getString(bindings, "project_ref");
|
|
13503
|
+
if (!manifest || !environment || !projectRef)
|
|
13504
|
+
throw new Error("Lite migration bindings require manifest, environment and project_ref");
|
|
13505
|
+
out.migrationBindings = { manifest, environment, projectRef };
|
|
13506
|
+
}
|
|
13507
|
+
return out;
|
|
13080
13508
|
}
|
|
13081
13509
|
function readAuth(root, env) {
|
|
13082
13510
|
const auth = tableAt(root, "auth");
|
|
@@ -13273,7 +13701,7 @@ function readSeed(root) {
|
|
|
13273
13701
|
out.paths = paths;
|
|
13274
13702
|
return out;
|
|
13275
13703
|
}
|
|
13276
|
-
function readFunctions(root) {
|
|
13704
|
+
function readFunctions(root, mode) {
|
|
13277
13705
|
const fns = tableAt(root, "functions");
|
|
13278
13706
|
const out = {};
|
|
13279
13707
|
if (!fns)
|
|
@@ -13294,9 +13722,29 @@ function readFunctions(root) {
|
|
|
13294
13722
|
if (framework === "fetch" || framework === "elysia" || framework === "hono") {
|
|
13295
13723
|
opts.framework = framework;
|
|
13296
13724
|
} else {
|
|
13725
|
+
if (mode === "strict")
|
|
13726
|
+
throw new Error(`invalid framework for function "${name}"`);
|
|
13297
13727
|
console.warn(` warning: [functions.${name}] framework "${framework}" is not one of fetch/elysia/hono; falling back to fetch`);
|
|
13298
13728
|
}
|
|
13299
13729
|
}
|
|
13730
|
+
if (mode === "strict") {
|
|
13731
|
+
for (const key of ["timeout_ms", "max_request_body_bytes", "max_response_body_bytes", "wait_until_timeout_ms"]) {
|
|
13732
|
+
if (t.values.has(key)) {
|
|
13733
|
+
const value = t.values.get(key);
|
|
13734
|
+
if (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) {
|
|
13735
|
+
throw new Error(`invalid strict function limit: ${name}.${key}`);
|
|
13736
|
+
}
|
|
13737
|
+
}
|
|
13738
|
+
}
|
|
13739
|
+
for (const key of ["background", "enabled", "verify_jwt"]) {
|
|
13740
|
+
if (t.values.has(key) && getBool(t, key) === undefined)
|
|
13741
|
+
throw new Error(`invalid strict function option: ${name}.${key}`);
|
|
13742
|
+
}
|
|
13743
|
+
for (const key of ["secrets", "outbound_hosts"]) {
|
|
13744
|
+
if (t.values.has(key) && getStringArray(t, key) === undefined)
|
|
13745
|
+
throw new Error(`invalid strict function option: ${name}.${key}`);
|
|
13746
|
+
}
|
|
13747
|
+
}
|
|
13300
13748
|
const timeoutMs = getInt(t, "timeout_ms");
|
|
13301
13749
|
if (timeoutMs !== undefined && timeoutMs > 0)
|
|
13302
13750
|
opts.timeoutMs = timeoutMs;
|
|
@@ -13323,13 +13771,201 @@ function readFunctions(root) {
|
|
|
13323
13771
|
return out;
|
|
13324
13772
|
}
|
|
13325
13773
|
|
|
13774
|
+
// src/runtime/node/project.ts
|
|
13775
|
+
import { lstat, readdir, readFile as readFile3 } from "fs/promises";
|
|
13776
|
+
import { join as join4 } from "path";
|
|
13777
|
+
async function loadSupabaseProject(projectDir, seed = {}) {
|
|
13778
|
+
const migrationsDir = join4(projectDir, "supabase", "migrations");
|
|
13779
|
+
let migrations = [];
|
|
13780
|
+
const sources = [];
|
|
13781
|
+
let entries = [];
|
|
13782
|
+
try {
|
|
13783
|
+
entries = await readdir(migrationsDir);
|
|
13784
|
+
} catch (error) {
|
|
13785
|
+
if (!isNotFound(error))
|
|
13786
|
+
throw error;
|
|
13787
|
+
}
|
|
13788
|
+
for (const entry of entries.sort()) {
|
|
13789
|
+
const info = await lstat(join4(migrationsDir, entry));
|
|
13790
|
+
if (info.isSymbolicLink())
|
|
13791
|
+
throw new Error(`migration paths must not be symbolic links: ${entry}`);
|
|
13792
|
+
let file;
|
|
13793
|
+
let name;
|
|
13794
|
+
if (info.isFile() && entry.endsWith(".sql")) {
|
|
13795
|
+
file = entry;
|
|
13796
|
+
name = entry.replace(/\.sql$/, "");
|
|
13797
|
+
} else if (info.isDirectory() && /^\d/.test(entry)) {
|
|
13798
|
+
file = `${entry}/migration.sql`;
|
|
13799
|
+
name = entry;
|
|
13800
|
+
const sqlInfo = await lstat(join4(migrationsDir, file)).catch((error) => {
|
|
13801
|
+
if (isNotFound(error))
|
|
13802
|
+
throw new Error(`migration folder is missing migration.sql: ${entry}`);
|
|
13803
|
+
throw error;
|
|
13804
|
+
});
|
|
13805
|
+
if (!sqlInfo.isFile() || sqlInfo.isSymbolicLink())
|
|
13806
|
+
throw new Error(`invalid migration file: ${file}`);
|
|
13807
|
+
} else
|
|
13808
|
+
continue;
|
|
13809
|
+
const sql = await readFile3(join4(migrationsDir, file), "utf8");
|
|
13810
|
+
sources.push({ file, sql });
|
|
13811
|
+
migrations.push({ name, sql });
|
|
13812
|
+
}
|
|
13813
|
+
const versions = new Set;
|
|
13814
|
+
for (const migration of migrations) {
|
|
13815
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
13816
|
+
if (versions.has(version))
|
|
13817
|
+
throw new Error(`duplicate migration version: ${version}`);
|
|
13818
|
+
versions.add(version);
|
|
13819
|
+
}
|
|
13820
|
+
const rendered = seed.bindings ? renderMigrationBindings({
|
|
13821
|
+
...seed.bindings,
|
|
13822
|
+
migrations: sources
|
|
13823
|
+
}) : undefined;
|
|
13824
|
+
if (rendered)
|
|
13825
|
+
migrations = migrations.map((migration, index) => {
|
|
13826
|
+
const bound = rendered.migrations[index];
|
|
13827
|
+
if (!bound)
|
|
13828
|
+
throw new Error(`missing rendered migration: ${migration.name}`);
|
|
13829
|
+
return { ...migration, sql: bound.sql };
|
|
13830
|
+
});
|
|
13831
|
+
else if (sources.some(({ sql }) => /__SC_BINDING_[A-Z0-9_]+__/.test(sql))) {
|
|
13832
|
+
throw new Error("migration binding placeholders require an explicit manifest and target");
|
|
13833
|
+
}
|
|
13834
|
+
let seedSql;
|
|
13835
|
+
if (seed.enabled !== false) {
|
|
13836
|
+
const parts = [];
|
|
13837
|
+
const supabaseDir = join4(projectDir, "supabase");
|
|
13838
|
+
for (const configuredPath of seed.paths ?? ["seed.sql"]) {
|
|
13839
|
+
const pattern = configuredPath.replace(/^\.\//, "");
|
|
13840
|
+
const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
|
|
13841
|
+
for (const relativePath of matches) {
|
|
13842
|
+
try {
|
|
13843
|
+
parts.push(await readFile3(join4(supabaseDir, relativePath), "utf8"));
|
|
13844
|
+
} catch (error) {
|
|
13845
|
+
if (!isNotFound(error))
|
|
13846
|
+
throw error;
|
|
13847
|
+
}
|
|
13848
|
+
}
|
|
13849
|
+
}
|
|
13850
|
+
if (parts.length)
|
|
13851
|
+
seedSql = parts.join(`
|
|
13852
|
+
`);
|
|
13853
|
+
}
|
|
13854
|
+
return { migrations, seedSql, ...rendered ? { bindingAttestation: rendered.attestation } : {} };
|
|
13855
|
+
}
|
|
13856
|
+
function isNotFound(error) {
|
|
13857
|
+
return error.code === "ENOENT";
|
|
13858
|
+
}
|
|
13859
|
+
|
|
13860
|
+
// src/project-runtime.ts
|
|
13861
|
+
import { chmod, link, lstat as lstat2, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
13862
|
+
import { dirname as dirname6, isAbsolute, join as join8, parse, relative, resolve as resolve4 } from "path";
|
|
13863
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
13864
|
+
|
|
13865
|
+
// src/runtime/node/bun-server.ts
|
|
13866
|
+
async function serveBun(backend, opts = {}) {
|
|
13867
|
+
const host = opts.host ?? "127.0.0.1";
|
|
13868
|
+
const server = Bun.serve({
|
|
13869
|
+
port: opts.port ?? 54321,
|
|
13870
|
+
hostname: host,
|
|
13871
|
+
async fetch(req, srv) {
|
|
13872
|
+
const url = new URL(req.url);
|
|
13873
|
+
if (url.pathname.startsWith("/realtime/v1") && req.headers.get("upgrade")?.toLowerCase() === "websocket") {
|
|
13874
|
+
const ok = srv.upgrade(req, { data: { vsn: url.searchParams.get("vsn") ?? "1.0.0" } });
|
|
13875
|
+
if (ok)
|
|
13876
|
+
return;
|
|
13877
|
+
return new Response("upgrade failed", { status: 400 });
|
|
13878
|
+
}
|
|
13879
|
+
const headers = new Headers(req.headers);
|
|
13880
|
+
headers.delete("x-supacloud-lite-remote-addr");
|
|
13881
|
+
const ip = srv.requestIP(req)?.address;
|
|
13882
|
+
if (ip)
|
|
13883
|
+
headers.set("x-supacloud-lite-remote-addr", ip);
|
|
13884
|
+
return backend.fetch(new Request(req, { headers }));
|
|
13885
|
+
},
|
|
13886
|
+
websocket: {
|
|
13887
|
+
open(ws) {
|
|
13888
|
+
const session = backend.realtime.connect({
|
|
13889
|
+
send: (data) => ws.send(data),
|
|
13890
|
+
close: (code, reason) => ws.close(code, reason)
|
|
13891
|
+
}, { vsn: ws.data.vsn });
|
|
13892
|
+
ws.data.session = session;
|
|
13893
|
+
},
|
|
13894
|
+
message(ws, message2) {
|
|
13895
|
+
ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
|
|
13896
|
+
},
|
|
13897
|
+
close(ws) {
|
|
13898
|
+
ws.data.session?.onClose();
|
|
13899
|
+
}
|
|
13900
|
+
}
|
|
13901
|
+
});
|
|
13902
|
+
return {
|
|
13903
|
+
server,
|
|
13904
|
+
port: server.port ?? opts.port ?? 54321,
|
|
13905
|
+
url: `http://${host}:${server.port ?? opts.port ?? 54321}`,
|
|
13906
|
+
close: async () => {
|
|
13907
|
+
await server.stop(true);
|
|
13908
|
+
}
|
|
13909
|
+
};
|
|
13910
|
+
}
|
|
13911
|
+
|
|
13912
|
+
// src/runtime/node/fs-driver.ts
|
|
13913
|
+
import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
13914
|
+
import { dirname as dirname4, join as join5, normalize, sep } from "path";
|
|
13915
|
+
|
|
13916
|
+
class FsStorageDriver {
|
|
13917
|
+
root;
|
|
13918
|
+
constructor(root) {
|
|
13919
|
+
this.root = root;
|
|
13920
|
+
}
|
|
13921
|
+
resolve(key) {
|
|
13922
|
+
const path = normalize(join5(this.root, key));
|
|
13923
|
+
if (!path.startsWith(normalize(this.root) + sep)) {
|
|
13924
|
+
throw new Error(`invalid storage key: ${key}`);
|
|
13925
|
+
}
|
|
13926
|
+
return path;
|
|
13927
|
+
}
|
|
13928
|
+
async put(key, data) {
|
|
13929
|
+
const path = this.resolve(key);
|
|
13930
|
+
await mkdir3(dirname4(path), { recursive: true });
|
|
13931
|
+
const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
13932
|
+
try {
|
|
13933
|
+
await writeFile3(temporaryPath, data);
|
|
13934
|
+
await rename(temporaryPath, path);
|
|
13935
|
+
} catch (error) {
|
|
13936
|
+
await rm2(temporaryPath, { force: true }).catch(() => {});
|
|
13937
|
+
throw error;
|
|
13938
|
+
}
|
|
13939
|
+
}
|
|
13940
|
+
async get(key) {
|
|
13941
|
+
try {
|
|
13942
|
+
return new Uint8Array(await readFile4(this.resolve(key)));
|
|
13943
|
+
} catch (e) {
|
|
13944
|
+
if (e.code === "ENOENT")
|
|
13945
|
+
return null;
|
|
13946
|
+
throw e;
|
|
13947
|
+
}
|
|
13948
|
+
}
|
|
13949
|
+
async getBlob(key) {
|
|
13950
|
+
const file = Bun.file(this.resolve(key));
|
|
13951
|
+
return await file.exists() ? file : null;
|
|
13952
|
+
}
|
|
13953
|
+
async delete(key) {
|
|
13954
|
+
await rm2(this.resolve(key), { force: true });
|
|
13955
|
+
}
|
|
13956
|
+
async deleteMany(keys2) {
|
|
13957
|
+
for (const k of keys2)
|
|
13958
|
+
await this.delete(k);
|
|
13959
|
+
}
|
|
13960
|
+
}
|
|
13961
|
+
|
|
13326
13962
|
// src/runtime/node/load-functions.ts
|
|
13327
13963
|
import { readdir as readdir2, readFile as readFile6, realpath, rm as rm4, stat as stat2 } from "fs/promises";
|
|
13328
13964
|
import { dirname as dirname5, join as join7 } from "path";
|
|
13329
13965
|
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
13330
13966
|
|
|
13331
13967
|
// src/runtime/node/bundle-function.ts
|
|
13332
|
-
import { createHash as
|
|
13968
|
+
import { createHash as createHash4 } from "crypto";
|
|
13333
13969
|
import { mkdir as mkdir4, readFile as readFile5, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
13334
13970
|
import { existsSync as existsSync3 } from "fs";
|
|
13335
13971
|
import { tmpdir as tmpdir3 } from "os";
|
|
@@ -13343,7 +13979,7 @@ function rewriteRemoteSpecifier(spec) {
|
|
|
13343
13979
|
}
|
|
13344
13980
|
var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
|
|
13345
13981
|
async function fetchModule(url) {
|
|
13346
|
-
const key =
|
|
13982
|
+
const key = createHash4("sha256").update(url).digest("hex");
|
|
13347
13983
|
const cached = join6(HTTP_CACHE, key);
|
|
13348
13984
|
if (existsSync3(cached))
|
|
13349
13985
|
return readFile5(cached, "utf8");
|
|
@@ -13377,8 +14013,9 @@ function remotePlugin() {
|
|
|
13377
14013
|
async function bundleFunction(entryPath, name) {
|
|
13378
14014
|
const outDir = join6(tmpdir3(), "supacloud-lite-fn-bundle", name);
|
|
13379
14015
|
await mkdir4(outDir, { recursive: true });
|
|
14016
|
+
let buildOutput;
|
|
13380
14017
|
try {
|
|
13381
|
-
|
|
14018
|
+
buildOutput = await Bun.build({
|
|
13382
14019
|
entrypoints: [entryPath],
|
|
13383
14020
|
format: "esm",
|
|
13384
14021
|
target: "bun",
|
|
@@ -13386,6 +14023,18 @@ async function bundleFunction(entryPath, name) {
|
|
|
13386
14023
|
naming: `${name}-[hash].[ext]`,
|
|
13387
14024
|
plugins: [remotePlugin()]
|
|
13388
14025
|
});
|
|
14026
|
+
} catch (buildThrow) {
|
|
14027
|
+
const details = buildThrow instanceof AggregateError ? buildThrow.errors.map((item) => item?.message ?? String(item)).filter(Boolean).join(`
|
|
14028
|
+
`) : buildThrow instanceof Error ? buildThrow.message : String(buildThrow);
|
|
14029
|
+
try {
|
|
14030
|
+
await rm3(outDir, { recursive: true, force: true });
|
|
14031
|
+
} catch (cleanupError) {
|
|
14032
|
+
throw new AggregateError([buildThrow, cleanupError], `failed to clean function bundle directory ${outDir}`);
|
|
14033
|
+
}
|
|
14034
|
+
throw new Error(`failed to bundle ${entryPath}
|
|
14035
|
+
${details}`, { cause: buildThrow });
|
|
14036
|
+
}
|
|
14037
|
+
try {
|
|
13389
14038
|
if (!buildOutput.success || buildOutput.outputs.length === 0) {
|
|
13390
14039
|
throw new Error(buildOutput.logs.map((item) => item.message).join(`
|
|
13391
14040
|
`) || `failed to bundle ${entryPath}`);
|
|
@@ -13429,16 +14078,18 @@ async function loadFunctionEnv(projectDir) {
|
|
|
13429
14078
|
return env;
|
|
13430
14079
|
}
|
|
13431
14080
|
var FUNCTION_FRAMEWORKS = new Set(["fetch", "elysia", "hono"]);
|
|
13432
|
-
function resolveFramework(name, value) {
|
|
14081
|
+
function resolveFramework(name, value, mode) {
|
|
13433
14082
|
if (value === undefined)
|
|
13434
14083
|
return;
|
|
13435
14084
|
if (FUNCTION_FRAMEWORKS.has(value))
|
|
13436
14085
|
return value;
|
|
14086
|
+
if (mode === "strict")
|
|
14087
|
+
throw new Error(`invalid framework for function "${name}"`);
|
|
13437
14088
|
console.warn(` warning: function "${name}" has unsupported framework "${value}", expected one of fetch/elysia/hono; falling back to fetch`);
|
|
13438
14089
|
return;
|
|
13439
14090
|
}
|
|
13440
14091
|
var loadQueue = Promise.resolve();
|
|
13441
|
-
async function loadFunctions2(projectDir, options = {}) {
|
|
14092
|
+
async function loadFunctions2(projectDir, options = {}, mode = "development") {
|
|
13442
14093
|
let releaseQueue;
|
|
13443
14094
|
const previous = loadQueue;
|
|
13444
14095
|
loadQueue = new Promise((resolveQueue) => {
|
|
@@ -13446,18 +14097,23 @@ async function loadFunctions2(projectDir, options = {}) {
|
|
|
13446
14097
|
});
|
|
13447
14098
|
await previous;
|
|
13448
14099
|
try {
|
|
13449
|
-
return await loadFunctionsUnlocked(projectDir, options);
|
|
14100
|
+
return await loadFunctionsUnlocked(projectDir, options, mode);
|
|
13450
14101
|
} finally {
|
|
13451
14102
|
releaseQueue();
|
|
13452
14103
|
}
|
|
13453
14104
|
}
|
|
13454
|
-
async function loadFunctionsUnlocked(projectDir, options) {
|
|
14105
|
+
async function loadFunctionsUnlocked(projectDir, options, mode) {
|
|
13455
14106
|
const functions = new Map;
|
|
13456
14107
|
const root = join7(projectDir, "supabase", "functions");
|
|
13457
14108
|
let entries = [];
|
|
13458
14109
|
try {
|
|
13459
14110
|
entries = await readdir2(root);
|
|
13460
|
-
} catch {
|
|
14111
|
+
} catch (error) {
|
|
14112
|
+
if (mode === "strict" && errorProperty(error, "code") !== "ENOENT")
|
|
14113
|
+
throw error;
|
|
14114
|
+
if (mode === "strict" && Object.values(options).some((option) => option.enabled !== false)) {
|
|
14115
|
+
throw new Error("declared function directory is missing");
|
|
14116
|
+
}
|
|
13461
14117
|
return functions;
|
|
13462
14118
|
}
|
|
13463
14119
|
installDenoShim();
|
|
@@ -13507,14 +14163,18 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
13507
14163
|
} : undefined;
|
|
13508
14164
|
functions.set(name, {
|
|
13509
14165
|
handler,
|
|
13510
|
-
framework: resolveFramework(name, opts?.framework),
|
|
14166
|
+
framework: resolveFramework(name, opts?.framework, mode),
|
|
13511
14167
|
...limits ? { limits } : {},
|
|
13512
14168
|
...capabilities ? { capabilities } : {}
|
|
13513
14169
|
});
|
|
13514
14170
|
} else {
|
|
14171
|
+
if (mode === "strict")
|
|
14172
|
+
throw new Error(`function "${name}" has no handler`);
|
|
13515
14173
|
console.warn(` warning: function "${name}" has no default function, handle/fetch object, or Deno.serve() handler, skipped`);
|
|
13516
14174
|
}
|
|
13517
14175
|
} catch (e) {
|
|
14176
|
+
if (mode === "strict")
|
|
14177
|
+
throw new Error(`failed to load strict function "${name}"`, { cause: e });
|
|
13518
14178
|
const msg = e instanceof Error ? e.message : String(e);
|
|
13519
14179
|
if (/Unknown file extension|Cannot find module/.test(msg)) {
|
|
13520
14180
|
console.warn(` warning: function "${name}" could not be bundled by Bun: ${msg}`);
|
|
@@ -13527,6 +14187,14 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
13527
14187
|
}
|
|
13528
14188
|
break;
|
|
13529
14189
|
}
|
|
14190
|
+
if (mode === "strict" && !functions.has(name))
|
|
14191
|
+
throw new Error(`function "${name}" has no loadable entrypoint`);
|
|
14192
|
+
}
|
|
14193
|
+
if (mode === "strict") {
|
|
14194
|
+
for (const [name, option] of Object.entries(options)) {
|
|
14195
|
+
if (option.enabled !== false && !functions.has(name))
|
|
14196
|
+
throw new Error(`declared function "${name}" was not loaded`);
|
|
14197
|
+
}
|
|
13530
14198
|
}
|
|
13531
14199
|
return functions;
|
|
13532
14200
|
}
|
|
@@ -13615,8 +14283,8 @@ class S3StorageDriver {
|
|
|
13615
14283
|
async delete(key) {
|
|
13616
14284
|
await this.client.file(this.objectKey(key)).delete();
|
|
13617
14285
|
}
|
|
13618
|
-
async deleteMany(
|
|
13619
|
-
for (const key of
|
|
14286
|
+
async deleteMany(keys2) {
|
|
14287
|
+
for (const key of keys2) {
|
|
13620
14288
|
await this.delete(key);
|
|
13621
14289
|
}
|
|
13622
14290
|
}
|
|
@@ -13676,7 +14344,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
13676
14344
|
}
|
|
13677
14345
|
async function requiredResetEntry(path) {
|
|
13678
14346
|
try {
|
|
13679
|
-
return await
|
|
14347
|
+
return await lstat2(path);
|
|
13680
14348
|
} catch (error) {
|
|
13681
14349
|
if (error.code === "ENOENT")
|
|
13682
14350
|
throw new Error(RESET_INITIALIZATION_ERROR);
|
|
@@ -13702,7 +14370,7 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2,
|
|
|
13702
14370
|
let current = target2;
|
|
13703
14371
|
while (current !== stateDir) {
|
|
13704
14372
|
try {
|
|
13705
|
-
if ((await
|
|
14373
|
+
if ((await lstat2(current)).isSymbolicLink()) {
|
|
13706
14374
|
throw new Error(`refusing to reset ${label} path through a symbolic link: ${current}`);
|
|
13707
14375
|
}
|
|
13708
14376
|
} catch (error) {
|
|
@@ -13725,7 +14393,7 @@ async function nearestExistingAncestor(target2) {
|
|
|
13725
14393
|
let current = target2;
|
|
13726
14394
|
while (true) {
|
|
13727
14395
|
try {
|
|
13728
|
-
await
|
|
14396
|
+
await lstat2(current);
|
|
13729
14397
|
return current;
|
|
13730
14398
|
} catch (error) {
|
|
13731
14399
|
if (error.code !== "ENOENT")
|
|
@@ -13787,12 +14455,15 @@ async function createProjectBackend(options = {}) {
|
|
|
13787
14455
|
const host = options.host ?? process.env.SUPACLOUD_LITE_HOST ?? "127.0.0.1";
|
|
13788
14456
|
const port = options.port ?? parsePort(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 54321);
|
|
13789
14457
|
const url = options.apiUrl ?? process.env.SUPACLOUD_LITE_API_URL ?? `http://${displayHost(host)}:${port}`;
|
|
13790
|
-
const config = loadProjectConfig(paths.projectDir);
|
|
14458
|
+
const config = loadProjectConfig(paths.projectDir, process.env, options.runtimeMode);
|
|
14459
|
+
const bindings = options.migrationBindings ?? await loadProjectBindings(paths.projectDir, config);
|
|
13791
14460
|
const project = await loadSupabaseProject(paths.projectDir, {
|
|
13792
14461
|
enabled: options.includeSeed === false ? false : config.seed.enabled,
|
|
13793
|
-
paths: config.seed.paths
|
|
14462
|
+
paths: config.seed.paths,
|
|
14463
|
+
bindings
|
|
13794
14464
|
});
|
|
13795
|
-
const
|
|
14465
|
+
const externalIdentity = options.externalIdentity ?? (options.includeIdentity !== false ? await loadProjectIdentity(paths.projectDir, options.identityModule ?? config.lite.identityModule) : undefined);
|
|
14466
|
+
const functions = options.includeFunctions === false ? new Map : await loadFunctions2(paths.projectDir, config.functions, config.lite.runtimeMode);
|
|
13796
14467
|
const functionEnv = options.includeFunctions === false ? {} : await loadFunctionEnv(paths.projectDir);
|
|
13797
14468
|
const secrets = await ensureProjectSecrets(paths);
|
|
13798
14469
|
const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
|
|
@@ -13807,11 +14478,20 @@ async function createProjectBackend(options = {}) {
|
|
|
13807
14478
|
await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
|
|
13808
14479
|
await chmod(paths.storageDir, 448);
|
|
13809
14480
|
const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
|
|
13810
|
-
|
|
14481
|
+
let engine;
|
|
14482
|
+
if (databaseEngine === "native") {
|
|
14483
|
+
const dataDir = paths.dataDir;
|
|
14484
|
+
if (dataDir === undefined)
|
|
14485
|
+
throw new Error("native PostgreSQL requires a data directory");
|
|
14486
|
+
engine = await createNativeEngine({ dataDir, installDir: options.postgresDir, log: options.log, replication });
|
|
14487
|
+
}
|
|
13811
14488
|
let backend;
|
|
13812
14489
|
try {
|
|
13813
14490
|
backend = await createBackend({
|
|
13814
14491
|
engine,
|
|
14492
|
+
runtimeMode: config.lite.runtimeMode,
|
|
14493
|
+
graphql: options.graphql ?? config.lite.graphql,
|
|
14494
|
+
externalIdentity,
|
|
13815
14495
|
dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
13816
14496
|
jwtSecret: secrets.jwtSecret,
|
|
13817
14497
|
vaultKey: secrets.vaultKey,
|
|
@@ -13861,6 +14541,7 @@ async function createProjectBackend(options = {}) {
|
|
|
13861
14541
|
throw error;
|
|
13862
14542
|
}
|
|
13863
14543
|
return {
|
|
14544
|
+
bindingAttestation: project.bindingAttestation,
|
|
13864
14545
|
backend,
|
|
13865
14546
|
config,
|
|
13866
14547
|
paths,
|
|
@@ -13875,6 +14556,30 @@ async function createProjectBackend(options = {}) {
|
|
|
13875
14556
|
replicationProfile: replication?.profile
|
|
13876
14557
|
};
|
|
13877
14558
|
}
|
|
14559
|
+
async function loadProjectBindings(projectDir, config) {
|
|
14560
|
+
const bindings = config.lite.migrationBindings;
|
|
14561
|
+
if (!bindings)
|
|
14562
|
+
return;
|
|
14563
|
+
const manifest = JSON.parse(await readFile7(resolve4(projectDir, bindings.manifest), "utf8"));
|
|
14564
|
+
return {
|
|
14565
|
+
manifest,
|
|
14566
|
+
target: { environment: bindings.environment, projectRef: bindings.projectRef },
|
|
14567
|
+
values: process.env
|
|
14568
|
+
};
|
|
14569
|
+
}
|
|
14570
|
+
async function loadProjectIdentity(projectDir, module) {
|
|
14571
|
+
if (!module)
|
|
14572
|
+
return;
|
|
14573
|
+
const imported = await import(pathToFileURL3(resolve4(projectDir, module)).href);
|
|
14574
|
+
if (!isRecord(imported) || typeof imported["default"] !== "function") {
|
|
14575
|
+
throw new Error("Lite identity module must default-export an external identity verifier");
|
|
14576
|
+
}
|
|
14577
|
+
const verify = imported["default"];
|
|
14578
|
+
return async (request) => {
|
|
14579
|
+
const claims = await verify(request);
|
|
14580
|
+
return validateExternalIdentityClaims(claims);
|
|
14581
|
+
};
|
|
14582
|
+
}
|
|
13878
14583
|
function resolveNativeReplicationOptions(options, databaseEngine = resolveDatabaseEngine(options.engine, options.memory)) {
|
|
13879
14584
|
const profile = options.replicationProfile ?? process.env.SUPACLOUD_LITE_REPLICATION_PROFILE;
|
|
13880
14585
|
if (!profile)
|
|
@@ -13906,7 +14611,7 @@ function resolveDatabaseEngine(value, memory = false) {
|
|
|
13906
14611
|
if (configured === "native" && memory)
|
|
13907
14612
|
throw new Error("--memory is only supported by the pglite engine");
|
|
13908
14613
|
if (configured === "native" && !isNativeEngineSupported()) {
|
|
13909
|
-
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64;
|
|
14614
|
+
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ${process.platform}/${process.arch} must use --engine pglite`);
|
|
13910
14615
|
}
|
|
13911
14616
|
return configured;
|
|
13912
14617
|
}
|
|
@@ -14018,7 +14723,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
14018
14723
|
}
|
|
14019
14724
|
|
|
14020
14725
|
// src/snapshot.ts
|
|
14021
|
-
import { chmod as chmod2, copyFile, lstat as
|
|
14726
|
+
import { chmod as chmod2, copyFile, lstat as lstat3, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile8, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
14022
14727
|
import { dirname as dirname7, join as join9, parse as parse2, relative as relative2, resolve as resolve5, sep as sep2 } from "path";
|
|
14023
14728
|
import { create as createTar, extract as extractTar2 } from "tar";
|
|
14024
14729
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
@@ -14107,7 +14812,10 @@ async function restoreSnapshot(options) {
|
|
|
14107
14812
|
if (manifest.storageBackend !== options.storageBackend) {
|
|
14108
14813
|
throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
|
|
14109
14814
|
}
|
|
14110
|
-
assertDatabaseSnapshotCompatible(manifest, paths);
|
|
14815
|
+
assertDatabaseSnapshotCompatible(manifest, paths, options.postgresDir);
|
|
14816
|
+
if (manifest.databaseEngine === "native" && await readPostgresMajor(join9(payloadRoot, "database")) !== manifest.postgresMajor) {
|
|
14817
|
+
throw new Error("native PostgreSQL snapshot payload does not match its declared major version");
|
|
14818
|
+
}
|
|
14111
14819
|
if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
|
|
14112
14820
|
throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
|
|
14113
14821
|
}
|
|
@@ -14174,7 +14882,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
14174
14882
|
throw new Error("snapshot state directory must not be the filesystem root");
|
|
14175
14883
|
if (paths.secretsFile !== join9(paths.stateDir, "secrets.json"))
|
|
14176
14884
|
throw new Error("snapshot secrets path must be inside the state directory");
|
|
14177
|
-
const stateInfo = await
|
|
14885
|
+
const stateInfo = await lstat3(paths.stateDir);
|
|
14178
14886
|
if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
|
|
14179
14887
|
throw new Error(`state directory must be a real directory: ${paths.stateDir}`);
|
|
14180
14888
|
} catch (error) {
|
|
@@ -14187,7 +14895,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
14187
14895
|
await assertDirectoryOrMissing(paths.dataDir);
|
|
14188
14896
|
await assertDirectoryOrMissing(paths.storageDir);
|
|
14189
14897
|
if (options.requireSecrets !== false) {
|
|
14190
|
-
const secretInfo = await
|
|
14898
|
+
const secretInfo = await lstat3(paths.secretsFile);
|
|
14191
14899
|
if (!secretInfo.isFile() || secretInfo.isSymbolicLink())
|
|
14192
14900
|
throw new Error(`secrets file must be a regular file: ${paths.secretsFile}`);
|
|
14193
14901
|
}
|
|
@@ -14198,7 +14906,7 @@ async function assertDirectoryOrMissing(path) {
|
|
|
14198
14906
|
if (resolve5(path) === parse2(resolve5(path)).root)
|
|
14199
14907
|
throw new Error(`snapshot path must not be the filesystem root: ${path}`);
|
|
14200
14908
|
try {
|
|
14201
|
-
const info = await
|
|
14909
|
+
const info = await lstat3(path);
|
|
14202
14910
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
14203
14911
|
throw new Error(`snapshot path must be a real directory: ${path}`);
|
|
14204
14912
|
} catch (error) {
|
|
@@ -14219,7 +14927,7 @@ async function assertNoDataDirectoryLock(paths) {
|
|
|
14219
14927
|
}
|
|
14220
14928
|
async function stageDirectory(root, destination) {
|
|
14221
14929
|
try {
|
|
14222
|
-
const info = await
|
|
14930
|
+
const info = await lstat3(root);
|
|
14223
14931
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
14224
14932
|
throw new Error(`snapshot path must be a real directory: ${root}`);
|
|
14225
14933
|
} catch (error) {
|
|
@@ -14269,7 +14977,7 @@ function isSnapshotManifest(value) {
|
|
|
14269
14977
|
const candidate = value;
|
|
14270
14978
|
return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true && (candidate.databaseEngine === undefined || candidate.databaseEngine === "pglite" || candidate.databaseEngine === "native") && (candidate.platform === undefined || typeof candidate.platform === "string") && (candidate.architecture === undefined || typeof candidate.architecture === "string") && (candidate.postgresMajor === undefined || typeof candidate.postgresMajor === "string");
|
|
14271
14979
|
}
|
|
14272
|
-
function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
14980
|
+
function assertDatabaseSnapshotCompatible(manifest, paths, postgresDir) {
|
|
14273
14981
|
const sourceEngine = manifest.databaseEngine ?? "pglite";
|
|
14274
14982
|
if (sourceEngine !== paths.databaseEngine) {
|
|
14275
14983
|
throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
|
|
@@ -14279,8 +14987,9 @@ function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
|
14279
14987
|
if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
|
|
14280
14988
|
throw new Error(`native PostgreSQL snapshots require the same platform and architecture; ` + `source is ${manifest.platform ?? "unknown"}/${manifest.architecture ?? "unknown"}, ` + `target is ${process.platform}/${process.arch}`);
|
|
14281
14989
|
}
|
|
14282
|
-
|
|
14283
|
-
|
|
14990
|
+
const expectedMajor = nativePostgresMajor(postgresDir);
|
|
14991
|
+
if (manifest.postgresMajor !== expectedMajor) {
|
|
14992
|
+
throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but the selected PostgreSQL installation uses ${expectedMajor}`);
|
|
14284
14993
|
}
|
|
14285
14994
|
}
|
|
14286
14995
|
async function readPostgresMajor(dataDir) {
|
|
@@ -14298,7 +15007,7 @@ async function assertSnapshotPayload(payloadRoot, manifest) {
|
|
|
14298
15007
|
const required = ["manifest.json", "secrets.json"];
|
|
14299
15008
|
for (const path of required) {
|
|
14300
15009
|
try {
|
|
14301
|
-
await
|
|
15010
|
+
await lstat3(join9(payloadRoot, path));
|
|
14302
15011
|
} catch {
|
|
14303
15012
|
throw new Error(`snapshot is missing required payload: ${path}`);
|
|
14304
15013
|
}
|
|
@@ -14370,7 +15079,7 @@ async function rollbackDirectorySwaps(swaps) {
|
|
|
14370
15079
|
}
|
|
14371
15080
|
async function existingInfo(path) {
|
|
14372
15081
|
try {
|
|
14373
|
-
return await
|
|
15082
|
+
return await lstat3(path);
|
|
14374
15083
|
} catch (error) {
|
|
14375
15084
|
if (error.code === "ENOENT")
|
|
14376
15085
|
return null;
|
|
@@ -14378,7 +15087,7 @@ async function existingInfo(path) {
|
|
|
14378
15087
|
}
|
|
14379
15088
|
}
|
|
14380
15089
|
async function copyEntry(source, target2) {
|
|
14381
|
-
const info = await
|
|
15090
|
+
const info = await lstat3(source);
|
|
14382
15091
|
if (info.isSymbolicLink())
|
|
14383
15092
|
throw new Error(`snapshot refuses symbolic link: ${source}`);
|
|
14384
15093
|
if (info.isDirectory()) {
|
|
@@ -14392,7 +15101,7 @@ async function copyEntry(source, target2) {
|
|
|
14392
15101
|
throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
|
|
14393
15102
|
}
|
|
14394
15103
|
async function hardenRestoredTree(root) {
|
|
14395
|
-
const info = await
|
|
15104
|
+
const info = await lstat3(root);
|
|
14396
15105
|
if (info.isSymbolicLink())
|
|
14397
15106
|
throw new Error(`snapshot refuses symbolic link: ${root}`);
|
|
14398
15107
|
if (info.isDirectory()) {
|
|
@@ -14485,6 +15194,12 @@ function parseArgs(argv) {
|
|
|
14485
15194
|
options.s3 = { ...options.s3, prefix: next() };
|
|
14486
15195
|
else if (argument === "--engine")
|
|
14487
15196
|
options.engine = next();
|
|
15197
|
+
else if (argument === "--runtime-mode")
|
|
15198
|
+
options.runtimeMode = runtimeMode(next());
|
|
15199
|
+
else if (argument === "--postgres-dir")
|
|
15200
|
+
options.postgresDir = resolve6(next());
|
|
15201
|
+
else if (argument === "--identity-module")
|
|
15202
|
+
options.identityModule = resolve6(next());
|
|
14488
15203
|
else if (argument === "--replication-profile") {
|
|
14489
15204
|
options.replicationProfile = next();
|
|
14490
15205
|
} else if (argument === "--replication-host")
|
|
@@ -14536,13 +15251,13 @@ async function main() {
|
|
|
14536
15251
|
}
|
|
14537
15252
|
if (options.command === "keys") {
|
|
14538
15253
|
const secrets = await ensureProjectSecrets(paths);
|
|
14539
|
-
const
|
|
15254
|
+
const keys2 = await mintProjectKeys(secrets.jwtSecret);
|
|
14540
15255
|
const privilegedKey = options.serviceRole ? `
|
|
14541
15256
|
service_role key:
|
|
14542
|
-
${
|
|
15257
|
+
${keys2.serviceRoleKey}` : `
|
|
14543
15258
|
Use --service-role to print the privileged key.`;
|
|
14544
15259
|
await writeStandardOutput(`anon key:
|
|
14545
|
-
${
|
|
15260
|
+
${keys2.anonKey}
|
|
14546
15261
|
${privilegedKey}
|
|
14547
15262
|
`);
|
|
14548
15263
|
return;
|
|
@@ -14568,6 +15283,8 @@ ${privilegedKey}
|
|
|
14568
15283
|
applyMigrations: false,
|
|
14569
15284
|
includeFunctions: false,
|
|
14570
15285
|
includeWebhooks: false,
|
|
15286
|
+
includeIdentity: false,
|
|
15287
|
+
graphql: { enabled: false },
|
|
14571
15288
|
startRuntimeServices: false,
|
|
14572
15289
|
log: quietLog
|
|
14573
15290
|
});
|
|
@@ -14591,6 +15308,8 @@ ${privilegedKey}
|
|
|
14591
15308
|
applyMigrations: false,
|
|
14592
15309
|
includeFunctions: false,
|
|
14593
15310
|
includeWebhooks: false,
|
|
15311
|
+
includeIdentity: false,
|
|
15312
|
+
graphql: { enabled: false },
|
|
14594
15313
|
startRuntimeServices: false,
|
|
14595
15314
|
log: quietLog
|
|
14596
15315
|
});
|
|
@@ -14602,24 +15321,36 @@ ${privilegedKey}
|
|
|
14602
15321
|
return;
|
|
14603
15322
|
}
|
|
14604
15323
|
if (options.command === "doctor") {
|
|
15324
|
+
const config = loadProjectConfig(paths.projectDir, process.env, options.runtimeMode);
|
|
14605
15325
|
const replication = resolveNativeReplicationOptions(options, paths.databaseEngine);
|
|
14606
15326
|
const report = liteCapabilities(paths.databaseEngine, replication?.profile);
|
|
14607
|
-
|
|
14608
|
-
|
|
14609
|
-
|
|
14610
|
-
|
|
14611
|
-
|
|
15327
|
+
report.runtime_mode = config.lite.runtimeMode;
|
|
15328
|
+
if (options.identityModule ?? config.lite.identityModule) {
|
|
15329
|
+
report.identity = { mode: "external", verification: "unverified" };
|
|
15330
|
+
}
|
|
15331
|
+
if (paths.dataDir && existsSync4(join10(paths.dataDir, "PG_VERSION"))) {
|
|
15332
|
+
const engine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, installDir: options.postgresDir, log: quietLog, replication }) : await createPgliteEngine(paths.dataDir);
|
|
14612
15333
|
try {
|
|
14613
|
-
report.
|
|
15334
|
+
report.graphql = await inspectGraphql(engine);
|
|
15335
|
+
if (replication)
|
|
15336
|
+
report.powersync_readiness = await inspectPowerSyncReadiness(engine, replication);
|
|
14614
15337
|
} finally {
|
|
14615
15338
|
await engine.close();
|
|
14616
15339
|
}
|
|
15340
|
+
} else if (replication) {
|
|
15341
|
+
throw new Error("PowerSync readiness requires an initialized native database; run migrate first");
|
|
14617
15342
|
}
|
|
15343
|
+
if (config.lite.graphql.enabled === false)
|
|
15344
|
+
report.graphql = { status: "disabled", extension: "pg_graphql" };
|
|
14618
15345
|
if (options.json)
|
|
14619
15346
|
await writeStandardOutput(`${JSON.stringify(report, null, 2)}
|
|
14620
15347
|
`);
|
|
14621
15348
|
else
|
|
14622
15349
|
await printDoctor(report);
|
|
15350
|
+
if (config.lite.graphql.enabled === true && report.graphql.status !== "supported")
|
|
15351
|
+
process.exitCode = 1;
|
|
15352
|
+
if (report.powersync_readiness?.ready === false)
|
|
15353
|
+
process.exitCode = 1;
|
|
14623
15354
|
return;
|
|
14624
15355
|
}
|
|
14625
15356
|
if (options.command === "migrate" || options.command === "status") {
|
|
@@ -14629,11 +15360,23 @@ ${privilegedKey}
|
|
|
14629
15360
|
includeFunctions: false,
|
|
14630
15361
|
includeWebhooks: false,
|
|
14631
15362
|
includeSeed: options.command === "migrate",
|
|
15363
|
+
includeIdentity: false,
|
|
15364
|
+
graphql: { enabled: false },
|
|
14632
15365
|
startRuntimeServices: false,
|
|
14633
15366
|
log: quietLog
|
|
14634
15367
|
});
|
|
14635
15368
|
try {
|
|
14636
15369
|
const applied = await project2.backend.db.listAppliedMigrations();
|
|
15370
|
+
if (options.json) {
|
|
15371
|
+
await writeStandardOutput(`${JSON.stringify({
|
|
15372
|
+
schema: "supacloud.lite-migrations.v1",
|
|
15373
|
+
command: options.command,
|
|
15374
|
+
applied,
|
|
15375
|
+
binding_attestation: project2.bindingAttestation ?? null
|
|
15376
|
+
}, null, 2)}
|
|
15377
|
+
`);
|
|
15378
|
+
return;
|
|
15379
|
+
}
|
|
14637
15380
|
const output = options.command === "migrate" ? `${applied.length} migration(s) applied.` : applied.length === 0 ? "no migrations applied" : applied.map((migration) => `${migration.version} ${migration.name ?? ""}`).join(`
|
|
14638
15381
|
`);
|
|
14639
15382
|
await writeStandardOutput(`${output}
|
|
@@ -14681,6 +15424,8 @@ async function runDbCommand(options) {
|
|
|
14681
15424
|
...options,
|
|
14682
15425
|
includeFunctions: false,
|
|
14683
15426
|
includeWebhooks: false,
|
|
15427
|
+
includeIdentity: false,
|
|
15428
|
+
graphql: { enabled: false },
|
|
14684
15429
|
startRuntimeServices: false,
|
|
14685
15430
|
log: quietLog
|
|
14686
15431
|
});
|
|
@@ -14693,10 +15438,16 @@ async function runDbCommand(options) {
|
|
|
14693
15438
|
}
|
|
14694
15439
|
return;
|
|
14695
15440
|
}
|
|
14696
|
-
const
|
|
15441
|
+
const projectDir = resolve6(options.projectDir ?? process.cwd());
|
|
15442
|
+
const config = loadProjectConfig(projectDir, process.env, options.runtimeMode);
|
|
15443
|
+
const project = await loadSupabaseProject(projectDir, {
|
|
15444
|
+
enabled: false,
|
|
15445
|
+
bindings: options.migrationBindings ?? await loadProjectBindings(projectDir, config)
|
|
15446
|
+
});
|
|
14697
15447
|
if (subcommand === "diff") {
|
|
14698
15448
|
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
|
|
14699
15449
|
dataDir: paths.dataDir,
|
|
15450
|
+
installDir: options.postgresDir,
|
|
14700
15451
|
log: quietLog,
|
|
14701
15452
|
replication: resolveNativeReplicationOptions(options, paths.databaseEngine)
|
|
14702
15453
|
}) : undefined;
|
|
@@ -14704,7 +15455,8 @@ async function runDbCommand(options) {
|
|
|
14704
15455
|
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
14705
15456
|
liveEngine,
|
|
14706
15457
|
migrations: project.migrations,
|
|
14707
|
-
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined
|
|
15458
|
+
makeShadowEngine: paths.databaseEngine === "native" ? () => createTemporaryNativeEngine(options.postgresDir) : undefined,
|
|
15459
|
+
runtimeMode: config.lite.runtimeMode
|
|
14708
15460
|
});
|
|
14709
15461
|
if (ddl.length === 0) {
|
|
14710
15462
|
await writeStandardError(`No schema changes found.
|
|
@@ -14729,6 +15481,7 @@ async function runDbCommand(options) {
|
|
|
14729
15481
|
if (subcommand === "pull") {
|
|
14730
15482
|
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({
|
|
14731
15483
|
dataDir: paths.dataDir,
|
|
15484
|
+
installDir: options.postgresDir,
|
|
14732
15485
|
log: quietLog,
|
|
14733
15486
|
replication: resolveNativeReplicationOptions(options, paths.databaseEngine)
|
|
14734
15487
|
}) : undefined;
|
|
@@ -14736,7 +15489,8 @@ async function runDbCommand(options) {
|
|
|
14736
15489
|
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
14737
15490
|
liveEngine,
|
|
14738
15491
|
migrations: project.migrations,
|
|
14739
|
-
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined,
|
|
15492
|
+
makeShadowEngine: paths.databaseEngine === "native" ? () => createTemporaryNativeEngine(options.postgresDir) : undefined,
|
|
15493
|
+
runtimeMode: config.lite.runtimeMode,
|
|
14740
15494
|
migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
|
|
14741
15495
|
name: options.positionals[1] ?? "remote_schema"
|
|
14742
15496
|
});
|
|
@@ -14755,6 +15509,8 @@ async function runDbCommand(options) {
|
|
|
14755
15509
|
applyMigrations: false,
|
|
14756
15510
|
includeFunctions: false,
|
|
14757
15511
|
includeWebhooks: false,
|
|
15512
|
+
includeIdentity: false,
|
|
15513
|
+
graphql: { enabled: false },
|
|
14758
15514
|
startRuntimeServices: false,
|
|
14759
15515
|
log: quietLog
|
|
14760
15516
|
});
|
|
@@ -14795,7 +15551,7 @@ async function runSnapshotCommand(options) {
|
|
|
14795
15551
|
const input = options.positionals[1];
|
|
14796
15552
|
if (!input)
|
|
14797
15553
|
throw new Error("snapshot restore requires a snapshot file");
|
|
14798
|
-
const result = await restoreSnapshot({ paths, storageBackend, input, force: options.force });
|
|
15554
|
+
const result = await restoreSnapshot({ paths, storageBackend, input, force: options.force, postgresDir: options.postgresDir });
|
|
14799
15555
|
const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
|
|
14800
15556
|
const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
|
|
14801
15557
|
await writeStandardOutput([
|
|
@@ -14826,6 +15582,8 @@ async function runUpgradeCommand(options) {
|
|
|
14826
15582
|
includeFunctions: false,
|
|
14827
15583
|
includeWebhooks: false,
|
|
14828
15584
|
includeSeed: true,
|
|
15585
|
+
includeIdentity: false,
|
|
15586
|
+
graphql: { enabled: false },
|
|
14829
15587
|
startRuntimeServices: false,
|
|
14830
15588
|
log: quietLog
|
|
14831
15589
|
});
|
|
@@ -14857,6 +15615,9 @@ async function printInspection(rows) {
|
|
|
14857
15615
|
}
|
|
14858
15616
|
async function printDoctor(report) {
|
|
14859
15617
|
const output = Object.entries(report).filter(([, value]) => typeof value !== "object").map(([name, value]) => `${name}: ${value}`);
|
|
15618
|
+
output.push(`graphql: ${report.graphql.status} (${report.graphql.version ?? report.graphql.reason ?? "pg_graphql"})`);
|
|
15619
|
+
output.push(`identity: ${report.identity.mode} (${report.identity.verification})`);
|
|
15620
|
+
output.push(`migration_formats: ${report.migrations.formats.join(", ")}`);
|
|
14860
15621
|
if (report.powersync_readiness) {
|
|
14861
15622
|
output.push(`powersync_ready: ${report.powersync_readiness.ready}`);
|
|
14862
15623
|
for (const blocker of report.powersync_readiness.blockers)
|
|
@@ -14926,7 +15687,10 @@ Options:
|
|
|
14926
15687
|
--replication-tls-cert <p> PostgreSQL TLS certificate
|
|
14927
15688
|
--replication-tls-key <p> PostgreSQL TLS private key
|
|
14928
15689
|
--memory use an in-memory PGlite database
|
|
14929
|
-
--json emit machine-readable doctor output
|
|
15690
|
+
--json emit machine-readable doctor/migrate/status output
|
|
15691
|
+
--runtime-mode development (default) or strict
|
|
15692
|
+
--postgres-dir operator-managed PostgreSQL installation (native only)
|
|
15693
|
+
--identity-module trusted external identity bridge module
|
|
14930
15694
|
-o, --output <p> output file for gen types
|
|
14931
15695
|
-f, --file <name> migration suffix for db diff
|
|
14932
15696
|
--module-file <p> database module manifest for db check (default supabase/db/modules.ts)
|
|
@@ -14954,4 +15718,4 @@ try {
|
|
|
14954
15718
|
`);
|
|
14955
15719
|
exitCode = 1;
|
|
14956
15720
|
}
|
|
14957
|
-
process.exit(exitCode);
|
|
15721
|
+
process.exit(exitCode || Number(process.exitCode ?? 0));
|