@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/index.js
CHANGED
|
@@ -85,7 +85,7 @@ function randomToken(bytes = 32) {
|
|
|
85
85
|
// package.json
|
|
86
86
|
var package_default = {
|
|
87
87
|
name: "@supacloud/lite",
|
|
88
|
-
version: "0.
|
|
88
|
+
version: "0.14.0",
|
|
89
89
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
90
90
|
type: "module",
|
|
91
91
|
license: "Apache-2.0",
|
|
@@ -124,6 +124,8 @@ var package_default = {
|
|
|
124
124
|
start: "bun run src/cli.ts start",
|
|
125
125
|
test: "bun test --timeout 20000",
|
|
126
126
|
"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",
|
|
127
|
+
"test:graphql": "SUPACLOUD_LITE_TEST_GRAPHQL=1 bun test test/graphql-native.test.ts --timeout 180000",
|
|
128
|
+
benchmark: "bun run scripts/benchmark.ts",
|
|
127
129
|
parity: "bun run parity/harness.ts",
|
|
128
130
|
"parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
|
|
129
131
|
"check:native": "bun run test:native && bun run parity:native",
|
|
@@ -135,13 +137,17 @@ var package_default = {
|
|
|
135
137
|
},
|
|
136
138
|
dependencies: {
|
|
137
139
|
"@electric-sql/pglite": "0.5.8",
|
|
138
|
-
"@supacloud/db": "^0.
|
|
140
|
+
"@supacloud/db": "^0.6.0",
|
|
139
141
|
tar: "^7.5.22"
|
|
140
142
|
},
|
|
141
143
|
devDependencies: {
|
|
144
|
+
"@supacloud/app": "^0.11.0",
|
|
145
|
+
"@supacloud/compiler": "^0.13.1",
|
|
146
|
+
"@supacloud/elysia": "^0.10.0",
|
|
142
147
|
"@supabase/supabase-js": "^2.115.0",
|
|
143
148
|
"@types/bun": "^1.4.0",
|
|
144
149
|
elysia: "^1.4.30",
|
|
150
|
+
jose: "^6.2.11",
|
|
145
151
|
typescript: "^7.0.2"
|
|
146
152
|
},
|
|
147
153
|
engines: {
|
|
@@ -2788,6 +2794,38 @@ async function upsertWithoutTtl(query, key, serializedValue) {
|
|
|
2788
2794
|
on conflict (namespace, key) do update set value = excluded.value, expires_at = null, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue]);
|
|
2789
2795
|
}
|
|
2790
2796
|
|
|
2797
|
+
// src/runtime/functions/profile.ts
|
|
2798
|
+
function runtimeMode(value) {
|
|
2799
|
+
if (value === undefined)
|
|
2800
|
+
return "development";
|
|
2801
|
+
if (value !== "development" && value !== "strict")
|
|
2802
|
+
throw new Error("Lite runtime mode must be development or strict");
|
|
2803
|
+
return value;
|
|
2804
|
+
}
|
|
2805
|
+
function strictFunction(entry) {
|
|
2806
|
+
const limits = {
|
|
2807
|
+
timeoutMs: 900000,
|
|
2808
|
+
maxRequestBodyBytes: 30 * 1024 * 1024,
|
|
2809
|
+
maxResponseBodyBytes: 30 * 1024 * 1024,
|
|
2810
|
+
waitUntilTimeoutMs: 300000,
|
|
2811
|
+
...entry.limits
|
|
2812
|
+
};
|
|
2813
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
2814
|
+
const maximum = name.includes("Body") ? 30 * 1024 * 1024 : name === "waitUntilTimeoutMs" ? 300000 : 900000;
|
|
2815
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > maximum) {
|
|
2816
|
+
throw new Error(`invalid strict function limit: ${name}`);
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
if (entry.framework !== undefined && !["fetch", "elysia", "hono"].includes(entry.framework)) {
|
|
2820
|
+
throw new Error("invalid strict function framework");
|
|
2821
|
+
}
|
|
2822
|
+
return {
|
|
2823
|
+
...entry,
|
|
2824
|
+
limits,
|
|
2825
|
+
capabilities: { background: false, ...entry.capabilities }
|
|
2826
|
+
};
|
|
2827
|
+
}
|
|
2828
|
+
|
|
2791
2829
|
// src/runtime/functions/handler.ts
|
|
2792
2830
|
var VERIFIED_JWT_SUBJECT_HEADER = "x-supacloud-jwt-sub";
|
|
2793
2831
|
var UNSAFE_VERIFIED_JWT_SUBJECT = /[\u0000-\u001F\u007F-\u009F\u0100-\u{10FFFF}]/u;
|
|
@@ -2838,12 +2876,19 @@ class FunctionsHandler {
|
|
|
2838
2876
|
functions;
|
|
2839
2877
|
env;
|
|
2840
2878
|
pgredis;
|
|
2841
|
-
|
|
2879
|
+
mode;
|
|
2880
|
+
constructor(functions, env, pgredis, mode = "development") {
|
|
2842
2881
|
this.functions = functions;
|
|
2843
2882
|
this.env = env;
|
|
2844
2883
|
this.pgredis = pgredis;
|
|
2884
|
+
this.mode = mode;
|
|
2885
|
+
if (mode === "strict")
|
|
2886
|
+
for (const entry of functions.values())
|
|
2887
|
+
strictFunction(normalizeEntry(entry));
|
|
2845
2888
|
}
|
|
2846
2889
|
register(name, fn) {
|
|
2890
|
+
if (this.mode === "strict")
|
|
2891
|
+
strictFunction(normalizeEntry(fn));
|
|
2847
2892
|
this.functions.set(name, fn);
|
|
2848
2893
|
}
|
|
2849
2894
|
list() {
|
|
@@ -2858,7 +2903,7 @@ class FunctionsHandler {
|
|
|
2858
2903
|
if (!value) {
|
|
2859
2904
|
return json2(404, { error: `function "${name}" not found` });
|
|
2860
2905
|
}
|
|
2861
|
-
const entry = normalizeEntry(value);
|
|
2906
|
+
const entry = this.mode === "strict" ? strictFunction(normalizeEntry(value)) : normalizeEntry(value);
|
|
2862
2907
|
const limits = entry.limits;
|
|
2863
2908
|
const capabilities = entry.capabilities;
|
|
2864
2909
|
const maxBody = limits?.maxRequestBodyBytes;
|
|
@@ -3228,14 +3273,17 @@ alter role anon set search_path to "$user", public, extensions;
|
|
|
3228
3273
|
alter role authenticated set search_path to "$user", public, extensions;
|
|
3229
3274
|
alter role service_role set search_path to "$user", public, extensions;
|
|
3230
3275
|
|
|
3231
|
-
|
|
3232
|
-
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3276
|
+
-- Initialize defaults once. Restart/maintenance must not undo application ACLs.
|
|
3277
|
+
do $$
|
|
3278
|
+
begin
|
|
3279
|
+
if to_regclass('supabase_migrations.schema_migrations') is null then
|
|
3280
|
+
grant usage on schema public to anon, authenticated, service_role;
|
|
3281
|
+
alter default privileges in schema public
|
|
3282
|
+
grant all on tables to anon, authenticated, service_role;
|
|
3283
|
+
alter default privileges in schema public
|
|
3284
|
+
grant all on sequences to anon, authenticated, service_role;
|
|
3285
|
+
end if;
|
|
3286
|
+
end $$;
|
|
3239
3287
|
|
|
3240
3288
|
-- PostgreSQL already grants EXECUTE on new functions to PUBLIC. Keep that
|
|
3241
3289
|
-- default so project migrations can revoke PUBLIC and grant only selected
|
|
@@ -5695,7 +5743,9 @@ grant execute on function vault.create_secret(text,text,text,uuid), vault.update
|
|
|
5695
5743
|
`;
|
|
5696
5744
|
|
|
5697
5745
|
// src/runtime/db/sql-compat.ts
|
|
5698
|
-
function rewriteMigrationSql(sql) {
|
|
5746
|
+
function rewriteMigrationSql(sql, strict = false) {
|
|
5747
|
+
if (strict)
|
|
5748
|
+
return sql;
|
|
5699
5749
|
const out = [];
|
|
5700
5750
|
const n = sql.length;
|
|
5701
5751
|
let i = 0;
|
|
@@ -5769,6 +5819,8 @@ function rewriteStatement(stmt) {
|
|
|
5769
5819
|
const trivia = LEADING_TRIVIA.exec(stmt)?.[0] ?? "";
|
|
5770
5820
|
const rest = stmt.slice(trivia.length);
|
|
5771
5821
|
if (/^(?:create|drop)\s+extension\b/i.test(rest)) {
|
|
5822
|
+
if (extensionName(rest) === "pg_graphql")
|
|
5823
|
+
return stmt;
|
|
5772
5824
|
const semi = rest.lastIndexOf(";");
|
|
5773
5825
|
const bare = (semi !== -1 && rest.slice(semi + 1).trim() === "" ? rest.slice(0, semi) : rest).trim();
|
|
5774
5826
|
if (!bare)
|
|
@@ -5782,6 +5834,26 @@ function rewriteStatement(stmt) {
|
|
|
5782
5834
|
}
|
|
5783
5835
|
return stmt;
|
|
5784
5836
|
}
|
|
5837
|
+
function extensionName(statement) {
|
|
5838
|
+
let rest = statement;
|
|
5839
|
+
const tokens = [];
|
|
5840
|
+
while (tokens.length < 6) {
|
|
5841
|
+
rest = rest.slice(LEADING_TRIVIA.exec(rest)?.[0].length ?? 0);
|
|
5842
|
+
const token = /^(?:"((?:""|[^"])*)"|([A-Za-z_][A-Za-z_0-9]*))/.exec(rest);
|
|
5843
|
+
if (!token)
|
|
5844
|
+
break;
|
|
5845
|
+
const quoted = token[1];
|
|
5846
|
+
const bare = token[2];
|
|
5847
|
+
if (quoted !== undefined)
|
|
5848
|
+
tokens.push(quoted.replaceAll('""', '"'));
|
|
5849
|
+
else if (bare !== undefined)
|
|
5850
|
+
tokens.push(bare.toLowerCase());
|
|
5851
|
+
else
|
|
5852
|
+
throw new Error("invalid migration SQL token");
|
|
5853
|
+
rest = rest.slice(token[0].length);
|
|
5854
|
+
}
|
|
5855
|
+
return tokens[2] === "if" ? tokens[tokens[3] === "not" ? 5 : 4] : tokens[2];
|
|
5856
|
+
}
|
|
5785
5857
|
function pickTag(text, base) {
|
|
5786
5858
|
let tag = `$${base}$`;
|
|
5787
5859
|
let k = 0;
|
|
@@ -6043,19 +6115,51 @@ async function removePreparedBundles(cleanup) {
|
|
|
6043
6115
|
}
|
|
6044
6116
|
}
|
|
6045
6117
|
|
|
6118
|
+
// src/runtime/db/engine.ts
|
|
6119
|
+
class Mutex {
|
|
6120
|
+
tail = Promise.resolve();
|
|
6121
|
+
async lock() {
|
|
6122
|
+
let release;
|
|
6123
|
+
const next = new Promise((r) => release = r);
|
|
6124
|
+
const prev = this.tail;
|
|
6125
|
+
this.tail = this.tail.then(() => next);
|
|
6126
|
+
await prev;
|
|
6127
|
+
return release;
|
|
6128
|
+
}
|
|
6129
|
+
async run(fn) {
|
|
6130
|
+
const release = await this.lock();
|
|
6131
|
+
try {
|
|
6132
|
+
return await fn();
|
|
6133
|
+
} finally {
|
|
6134
|
+
release();
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
}
|
|
6138
|
+
|
|
6139
|
+
// src/runtime/validation.ts
|
|
6140
|
+
function isRecord(value) {
|
|
6141
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
6142
|
+
}
|
|
6143
|
+
function errorProperty(error, key) {
|
|
6144
|
+
return isRecord(error) ? error[key] : undefined;
|
|
6145
|
+
}
|
|
6146
|
+
|
|
6046
6147
|
// src/runtime/db/database.ts
|
|
6047
6148
|
var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
|
|
6048
6149
|
|
|
6049
6150
|
class Database {
|
|
6050
6151
|
engine;
|
|
6152
|
+
strict;
|
|
6153
|
+
migrationMutex = new Mutex;
|
|
6051
6154
|
schemaCache = new Map;
|
|
6052
6155
|
fnCache = new Map;
|
|
6053
6156
|
cdcListeners = new Set;
|
|
6054
6157
|
cdcStarting = null;
|
|
6055
6158
|
cdcStopping = null;
|
|
6056
6159
|
cdcUnsubscribe = null;
|
|
6057
|
-
constructor(engine) {
|
|
6160
|
+
constructor(engine, strict = false) {
|
|
6058
6161
|
this.engine = engine;
|
|
6162
|
+
this.strict = strict;
|
|
6059
6163
|
}
|
|
6060
6164
|
static async create(dataDirOrEngine, opts) {
|
|
6061
6165
|
const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
|
|
@@ -6074,7 +6178,7 @@ class Database {
|
|
|
6074
6178
|
await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
|
|
6075
6179
|
}
|
|
6076
6180
|
}
|
|
6077
|
-
return new Database(engine);
|
|
6181
|
+
return new Database(engine, opts?.strict);
|
|
6078
6182
|
} catch (error) {
|
|
6079
6183
|
try {
|
|
6080
6184
|
await engine.close();
|
|
@@ -6101,16 +6205,48 @@ class Database {
|
|
|
6101
6205
|
});
|
|
6102
6206
|
}
|
|
6103
6207
|
async runMigrations(migrations, seedSql) {
|
|
6208
|
+
return this.migrationMutex.run(() => this.applyMigrations(migrations, seedSql));
|
|
6209
|
+
}
|
|
6210
|
+
async applyMigrations(migrations, seedSql) {
|
|
6104
6211
|
const applied = [];
|
|
6105
6212
|
const sorted = [...migrations].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
6213
|
+
const versions = new Set;
|
|
6214
|
+
const history = await this.engine.query("select version, statements from supabase_migrations.schema_migrations");
|
|
6215
|
+
const recorded = new Map;
|
|
6216
|
+
for (const row of history.rows) {
|
|
6217
|
+
if (!isRecord(row) || typeof row["version"] !== "string")
|
|
6218
|
+
throw new Error("invalid migration history record");
|
|
6219
|
+
const statements = row["statements"];
|
|
6220
|
+
if (statements !== null && (!Array.isArray(statements) || !statements.every((sql) => typeof sql === "string"))) {
|
|
6221
|
+
throw new Error(`invalid migration history SQL: ${row["version"]}`);
|
|
6222
|
+
}
|
|
6223
|
+
recorded.set(row["version"], statements);
|
|
6224
|
+
}
|
|
6225
|
+
for (const migration of sorted) {
|
|
6226
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
6227
|
+
if (versions.has(version))
|
|
6228
|
+
throw new Error(`duplicate migration version: ${version}`);
|
|
6229
|
+
versions.add(version);
|
|
6230
|
+
}
|
|
6231
|
+
for (const migration of sorted) {
|
|
6232
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
6233
|
+
if (recorded.has(version)) {
|
|
6234
|
+
const statements = recorded.get(version);
|
|
6235
|
+
if (!statements?.length)
|
|
6236
|
+
throw new Error(`migration history has no verifiable SQL: ${version}`);
|
|
6237
|
+
if (statements.join(`
|
|
6238
|
+
`) !== migration.sql) {
|
|
6239
|
+
throw new Error(`applied migration content mismatch: ${version}; add a forward migration instead`);
|
|
6240
|
+
}
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6106
6243
|
for (const m of sorted) {
|
|
6107
6244
|
const version = m.name.match(/^(\d+)/)?.[1] ?? m.name;
|
|
6108
|
-
|
|
6109
|
-
if (seen.rows.length > 0)
|
|
6245
|
+
if (recorded.has(version))
|
|
6110
6246
|
continue;
|
|
6111
6247
|
const applyMigration = () => this.engine.transaction(async (tx) => {
|
|
6112
6248
|
await tx.exec(DEFAULT_SEARCH_PATH_SQL);
|
|
6113
|
-
await tx.exec(rewriteMigrationSql(m.sql));
|
|
6249
|
+
await tx.exec(rewriteMigrationSql(m.sql, this.strict));
|
|
6114
6250
|
await tx.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
|
|
6115
6251
|
values ($1, $2, $3)`, [version, m.name, [m.sql]]);
|
|
6116
6252
|
});
|
|
@@ -6132,7 +6268,7 @@ class Database {
|
|
|
6132
6268
|
if (seen.rows.length === 0) {
|
|
6133
6269
|
await this.engine.transaction(async (tx) => {
|
|
6134
6270
|
await tx.exec(DEFAULT_SEARCH_PATH_SQL);
|
|
6135
|
-
await tx.exec(rewriteMigrationSql(seedSql));
|
|
6271
|
+
await tx.exec(rewriteMigrationSql(seedSql, this.strict));
|
|
6136
6272
|
await tx.query(`insert into supabase_migrations.seed_files (path, hash) values ('supabase/seed.sql', $1)
|
|
6137
6273
|
on conflict (path) do update set hash = excluded.hash, applied_at = now()`, [hash]);
|
|
6138
6274
|
});
|
|
@@ -6429,19 +6565,23 @@ async function sha256Hex2(text) {
|
|
|
6429
6565
|
class RealtimeEngine {
|
|
6430
6566
|
db;
|
|
6431
6567
|
jwtSecret;
|
|
6568
|
+
resolveExternalToken;
|
|
6432
6569
|
connections = new Set;
|
|
6433
6570
|
bindingCounter = 1;
|
|
6434
6571
|
phxRefCounter = 1;
|
|
6435
6572
|
presence = new Map;
|
|
6436
6573
|
stopCdc = null;
|
|
6437
6574
|
stopDbBroadcast = null;
|
|
6438
|
-
constructor(db, jwtSecret) {
|
|
6575
|
+
constructor(db, jwtSecret, resolveExternalToken) {
|
|
6439
6576
|
this.db = db;
|
|
6440
6577
|
this.jwtSecret = jwtSecret;
|
|
6578
|
+
this.resolveExternalToken = resolveExternalToken;
|
|
6441
6579
|
}
|
|
6442
6580
|
async contextFromToken(token) {
|
|
6443
6581
|
if (!token || !this.jwtSecret)
|
|
6444
6582
|
return { role: "anon", claims: null };
|
|
6583
|
+
if (this.resolveExternalToken)
|
|
6584
|
+
return this.resolveExternalToken(token);
|
|
6445
6585
|
const claims = await verifyJwt(token, this.jwtSecret);
|
|
6446
6586
|
if (!claims)
|
|
6447
6587
|
return { role: "anon", claims: null };
|
|
@@ -6501,8 +6641,11 @@ class RealtimeEngine {
|
|
|
6501
6641
|
const stopDbBroadcast = this.stopDbBroadcast;
|
|
6502
6642
|
this.stopCdc = null;
|
|
6503
6643
|
this.stopDbBroadcast = null;
|
|
6504
|
-
for (const conn of this.connections)
|
|
6644
|
+
for (const conn of this.connections) {
|
|
6645
|
+
for (const topic of conn.channels.keys())
|
|
6646
|
+
this.leaveChannel(conn, topic);
|
|
6505
6647
|
conn.socket.close(1001, "server shutting down");
|
|
6648
|
+
}
|
|
6506
6649
|
this.connections.clear();
|
|
6507
6650
|
const [cdcStop, broadcastStop] = await Promise.allSettled([
|
|
6508
6651
|
Promise.resolve().then(() => stopCdc?.()),
|
|
@@ -6580,13 +6723,29 @@ class RealtimeEngine {
|
|
|
6580
6723
|
break;
|
|
6581
6724
|
case "access_token":
|
|
6582
6725
|
{
|
|
6583
|
-
const
|
|
6584
|
-
const
|
|
6585
|
-
|
|
6586
|
-
|
|
6587
|
-
|
|
6588
|
-
|
|
6589
|
-
|
|
6726
|
+
const selected = conn.channels.get(msg.topic);
|
|
6727
|
+
const channels = selected ? [selected] : [...conn.channels.values()];
|
|
6728
|
+
try {
|
|
6729
|
+
const token = msg.payload?.access_token;
|
|
6730
|
+
if (token !== undefined && typeof token !== "string")
|
|
6731
|
+
throw new Error("Invalid Realtime access token");
|
|
6732
|
+
const ctx = await this.contextFromToken(token);
|
|
6733
|
+
for (const channel of channels) {
|
|
6734
|
+
if (this.resolveExternalToken && channel.private) {
|
|
6735
|
+
const access = await this.authorizePrivate(channel.topic.replace(/^realtime:/, ""), ctx);
|
|
6736
|
+
if (!access.read)
|
|
6737
|
+
throw new Error("Realtime authentication denied");
|
|
6738
|
+
channel.canBroadcast = access.write;
|
|
6739
|
+
}
|
|
6740
|
+
channel.ctx = ctx;
|
|
6741
|
+
this.scheduleExternalExpiry(conn, channel);
|
|
6742
|
+
}
|
|
6743
|
+
} catch (error) {
|
|
6744
|
+
if (this.resolveExternalToken)
|
|
6745
|
+
for (const channel of channels)
|
|
6746
|
+
this.leaveChannel(conn, channel.topic);
|
|
6747
|
+
throw error;
|
|
6748
|
+
}
|
|
6590
6749
|
}
|
|
6591
6750
|
break;
|
|
6592
6751
|
default:
|
|
@@ -6598,7 +6757,10 @@ class RealtimeEngine {
|
|
|
6598
6757
|
}
|
|
6599
6758
|
async handleJoin(conn, msg) {
|
|
6600
6759
|
const config = msg.payload?.config ?? {};
|
|
6601
|
-
const
|
|
6760
|
+
const token = msg.payload?.access_token;
|
|
6761
|
+
if (token !== undefined && typeof token !== "string")
|
|
6762
|
+
throw new Error("Invalid Realtime access token");
|
|
6763
|
+
const ctx = await this.contextFromToken(token);
|
|
6602
6764
|
const isPrivate = config.private === true;
|
|
6603
6765
|
let canBroadcast = true;
|
|
6604
6766
|
if (isPrivate && !this.db.engine.minimalBootstrap) {
|
|
@@ -6650,7 +6812,10 @@ class RealtimeEngine {
|
|
|
6650
6812
|
private: isPrivate,
|
|
6651
6813
|
canBroadcast
|
|
6652
6814
|
};
|
|
6815
|
+
if (this.resolveExternalToken)
|
|
6816
|
+
this.leaveChannel(conn, msg.topic);
|
|
6653
6817
|
conn.channels.set(msg.topic, channel);
|
|
6818
|
+
this.scheduleExternalExpiry(conn, channel);
|
|
6654
6819
|
this.reply(conn, msg, "ok", {
|
|
6655
6820
|
postgres_changes: bindings.map((b) => ({
|
|
6656
6821
|
id: b.id,
|
|
@@ -6685,6 +6850,8 @@ class RealtimeEngine {
|
|
|
6685
6850
|
const channel = conn.channels.get(topic);
|
|
6686
6851
|
if (!channel)
|
|
6687
6852
|
return;
|
|
6853
|
+
if (channel.expiryTimer)
|
|
6854
|
+
clearTimeout(channel.expiryTimer);
|
|
6688
6855
|
conn.channels.delete(topic);
|
|
6689
6856
|
const state = this.presence.get(topic);
|
|
6690
6857
|
if (state?.has(channel.presenceKey)) {
|
|
@@ -6700,6 +6867,20 @@ class RealtimeEngine {
|
|
|
6700
6867
|
});
|
|
6701
6868
|
}
|
|
6702
6869
|
}
|
|
6870
|
+
scheduleExternalExpiry(conn, channel) {
|
|
6871
|
+
if (channel.expiryTimer)
|
|
6872
|
+
clearTimeout(channel.expiryTimer);
|
|
6873
|
+
const expires = channel.ctx.claims?.exp;
|
|
6874
|
+
if (!this.resolveExternalToken || typeof expires !== "number")
|
|
6875
|
+
return;
|
|
6876
|
+
channel.expiryTimer = setTimeout(() => {
|
|
6877
|
+
if (expires * 1000 > Date.now())
|
|
6878
|
+
return this.scheduleExternalExpiry(conn, channel);
|
|
6879
|
+
this.leaveChannel(conn, channel.topic);
|
|
6880
|
+
this.send(conn, { topic: channel.topic, event: "phx_error", payload: { reason: "token expired" }, ref: null });
|
|
6881
|
+
}, Math.max(0, Math.min(expires * 1000 - Date.now(), 2147483647)));
|
|
6882
|
+
channel.expiryTimer.unref?.();
|
|
6883
|
+
}
|
|
6703
6884
|
handleBinary(conn, bytes) {
|
|
6704
6885
|
if (bytes.length < 7 || bytes[0] !== 3)
|
|
6705
6886
|
return;
|
|
@@ -10237,6 +10418,208 @@ function assertSecretsSafe(input) {
|
|
|
10237
10418
|
warn("\u26A0 Vault key derived from the JWT secret - set an independent vaultKey before exposing this server.");
|
|
10238
10419
|
}
|
|
10239
10420
|
}
|
|
10421
|
+
|
|
10422
|
+
// src/runtime/graphql.ts
|
|
10423
|
+
async function inspectGraphql(engine) {
|
|
10424
|
+
const result = await engine.query(`
|
|
10425
|
+
select e.extversion, exists (
|
|
10426
|
+
select 1 from pg_proc p
|
|
10427
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
10428
|
+
join pg_depend d on d.classid = 'pg_proc'::regclass and d.objid = p.oid
|
|
10429
|
+
and d.refclassid = 'pg_extension'::regclass and d.refobjid = e.oid and d.deptype = 'e'
|
|
10430
|
+
where n.nspname = 'graphql' and p.proname = 'resolve' and not p.prosecdef
|
|
10431
|
+
and p.oid = to_regprocedure('graphql.resolve(text,jsonb,text,jsonb)')
|
|
10432
|
+
) as resolver from pg_extension e where e.extname = 'pg_graphql'
|
|
10433
|
+
`);
|
|
10434
|
+
const row = result.rows[0];
|
|
10435
|
+
if (row === undefined) {
|
|
10436
|
+
return { status: "unsupported", extension: "pg_graphql", reason: "PG_GRAPHQL_NOT_INSTALLED" };
|
|
10437
|
+
}
|
|
10438
|
+
if (!isRecord(row) || typeof row["extversion"] !== "string" || !row["extversion"] || typeof row["resolver"] !== "boolean" || result.rows.length !== 1) {
|
|
10439
|
+
return { status: "unsupported", extension: "pg_graphql", reason: "PG_GRAPHQL_INVALID_CATALOG" };
|
|
10440
|
+
}
|
|
10441
|
+
const version = row["extversion"];
|
|
10442
|
+
if (row["resolver"]) {
|
|
10443
|
+
try {
|
|
10444
|
+
const probe = await engine.query(`select graphql.resolve('{ __typename }', '{}'::jsonb, null, '{}'::jsonb) as result`);
|
|
10445
|
+
const probeRow = probe.rows[0];
|
|
10446
|
+
if (probe.rows.length !== 1 || !isRecord(probeRow) || !isGraphqlEnvelope(probeRow["result"])) {
|
|
10447
|
+
throw new Error("Invalid GraphQL probe response");
|
|
10448
|
+
}
|
|
10449
|
+
} catch {
|
|
10450
|
+
return { status: "unsupported", extension: "pg_graphql", version, reason: "PG_GRAPHQL_RESOLVER_FAILED" };
|
|
10451
|
+
}
|
|
10452
|
+
}
|
|
10453
|
+
return row["resolver"] ? { status: "supported", extension: "pg_graphql", version } : {
|
|
10454
|
+
status: "unsupported",
|
|
10455
|
+
extension: "pg_graphql",
|
|
10456
|
+
reason: "PG_GRAPHQL_RESOLVER_UNAVAILABLE"
|
|
10457
|
+
};
|
|
10458
|
+
}
|
|
10459
|
+
|
|
10460
|
+
class GraphqlHandler {
|
|
10461
|
+
db;
|
|
10462
|
+
capability;
|
|
10463
|
+
schemas;
|
|
10464
|
+
maxBytes;
|
|
10465
|
+
timeoutMs;
|
|
10466
|
+
constructor(db, capability, options = {}, schemas = ["public"]) {
|
|
10467
|
+
this.db = db;
|
|
10468
|
+
this.capability = capability;
|
|
10469
|
+
this.schemas = schemas;
|
|
10470
|
+
this.maxBytes = positive(options.maxRequestBodyBytes ?? 1024 * 1024);
|
|
10471
|
+
this.timeoutMs = positive(options.statementTimeoutMs ?? 30000);
|
|
10472
|
+
}
|
|
10473
|
+
async handle(request, context) {
|
|
10474
|
+
if (this.capability.status !== "supported") {
|
|
10475
|
+
return response(this.capability.status === "disabled" ? 404 : 501, {
|
|
10476
|
+
errors: [{
|
|
10477
|
+
message: "GraphQL requires the real pg_graphql extension and graphql.resolve.",
|
|
10478
|
+
extensions: { code: this.capability.reason ?? "GRAPHQL_DISABLED" }
|
|
10479
|
+
}]
|
|
10480
|
+
});
|
|
10481
|
+
}
|
|
10482
|
+
if (request.method !== "POST")
|
|
10483
|
+
return response(405, { errors: [{ message: "Use POST for GraphQL requests." }] }, { allow: "POST" });
|
|
10484
|
+
try {
|
|
10485
|
+
if (request.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase() !== "application/json") {
|
|
10486
|
+
throw new ApiError(415, { message: "GraphQL requests require application/json" });
|
|
10487
|
+
}
|
|
10488
|
+
const body = JSON.parse(await boundedBody(request, this.maxBytes));
|
|
10489
|
+
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") {
|
|
10490
|
+
throw new ApiError(400, { message: "Invalid GraphQL request" });
|
|
10491
|
+
}
|
|
10492
|
+
const result = await this.db.withContext(context, async (query) => {
|
|
10493
|
+
await query(`select set_config('statement_timeout', $1, true)`, [String(this.timeoutMs)]);
|
|
10494
|
+
await query(`select set_config('search_path', $1, true)`, [
|
|
10495
|
+
this.schemas.map((schema) => `"${schema.replaceAll('"', '""')}"`).join(",")
|
|
10496
|
+
]);
|
|
10497
|
+
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 ?? {})]);
|
|
10498
|
+
});
|
|
10499
|
+
const row = result.rows[0];
|
|
10500
|
+
if (result.rows.length !== 1 || !isRecord(row) || !isGraphqlEnvelope(row["result"])) {
|
|
10501
|
+
throw new Error("Invalid GraphQL resolver response");
|
|
10502
|
+
}
|
|
10503
|
+
return response(200, row["result"]);
|
|
10504
|
+
} catch (error) {
|
|
10505
|
+
if (error instanceof ApiError)
|
|
10506
|
+
return response(error.status, { errors: [{ message: error.message }] });
|
|
10507
|
+
if (error instanceof SyntaxError)
|
|
10508
|
+
return response(400, { errors: [{ message: "Invalid JSON request" }] });
|
|
10509
|
+
const code = errorProperty(error, "errno") ?? errorProperty(error, "code");
|
|
10510
|
+
const status = code === "42501" ? 403 : code === "57014" ? 504 : 500;
|
|
10511
|
+
return response(status, { errors: [{
|
|
10512
|
+
message: "GraphQL execution failed",
|
|
10513
|
+
extensions: { code: status === 403 ? "GRAPHQL_ACCESS_DENIED" : status === 504 ? "GRAPHQL_TIMEOUT" : "GRAPHQL_EXECUTION_FAILED" }
|
|
10514
|
+
}] });
|
|
10515
|
+
}
|
|
10516
|
+
}
|
|
10517
|
+
}
|
|
10518
|
+
async function boundedBody(request, limit) {
|
|
10519
|
+
if (Number(request.headers.get("content-length")) > limit)
|
|
10520
|
+
throw new ApiError(413, { message: "GraphQL request too large" });
|
|
10521
|
+
const reader = request.body?.getReader();
|
|
10522
|
+
if (!reader)
|
|
10523
|
+
throw new ApiError(400, { message: "GraphQL request body required" });
|
|
10524
|
+
const chunks = [];
|
|
10525
|
+
let size = 0;
|
|
10526
|
+
try {
|
|
10527
|
+
while (true) {
|
|
10528
|
+
const chunk = await reader.read();
|
|
10529
|
+
if (chunk.done)
|
|
10530
|
+
break;
|
|
10531
|
+
size += chunk.value.byteLength;
|
|
10532
|
+
if (size > limit) {
|
|
10533
|
+
await reader.cancel().catch(() => {});
|
|
10534
|
+
throw new ApiError(413, { message: "GraphQL request too large" });
|
|
10535
|
+
}
|
|
10536
|
+
chunks.push(chunk.value);
|
|
10537
|
+
}
|
|
10538
|
+
} finally {
|
|
10539
|
+
reader.releaseLock();
|
|
10540
|
+
}
|
|
10541
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
10542
|
+
}
|
|
10543
|
+
function isObject(value) {
|
|
10544
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10545
|
+
}
|
|
10546
|
+
function isGraphqlEnvelope(value) {
|
|
10547
|
+
if (!isRecord(value) || !("data" in value) && !("errors" in value))
|
|
10548
|
+
return false;
|
|
10549
|
+
if ("data" in value && value["data"] !== null && !isRecord(value["data"]))
|
|
10550
|
+
return false;
|
|
10551
|
+
if ("extensions" in value && !isRecord(value["extensions"]))
|
|
10552
|
+
return false;
|
|
10553
|
+
if ("errors" in value) {
|
|
10554
|
+
const errors = value["errors"];
|
|
10555
|
+
if (!Array.isArray(errors) || errors.length === 0 || !errors.every((error) => isRecord(error) && typeof error["message"] === "string"))
|
|
10556
|
+
return false;
|
|
10557
|
+
}
|
|
10558
|
+
return true;
|
|
10559
|
+
}
|
|
10560
|
+
function positive(value) {
|
|
10561
|
+
if (!Number.isSafeInteger(value) || value <= 0 || value > 2147483647)
|
|
10562
|
+
throw new Error("GraphQL limits must be positive bounded integers");
|
|
10563
|
+
return value;
|
|
10564
|
+
}
|
|
10565
|
+
function response(status, body, headers = {}) {
|
|
10566
|
+
return Response.json(body, { status, headers: { "cache-control": "no-store", ...headers } });
|
|
10567
|
+
}
|
|
10568
|
+
|
|
10569
|
+
// src/runtime/identity.ts
|
|
10570
|
+
function validateExternalIdentityClaims(value) {
|
|
10571
|
+
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()) {
|
|
10572
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10573
|
+
}
|
|
10574
|
+
for (const key of ["iss", "aud", "email"]) {
|
|
10575
|
+
if (key in value && typeof value[key] !== "string")
|
|
10576
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10577
|
+
}
|
|
10578
|
+
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"])))) {
|
|
10579
|
+
throw new ApiError(401, { message: "Invalid external identity" });
|
|
10580
|
+
}
|
|
10581
|
+
return value;
|
|
10582
|
+
}
|
|
10583
|
+
function createSupAuthLiteIdentity(options) {
|
|
10584
|
+
if (!options.projectId.trim())
|
|
10585
|
+
throw new Error("SupAuth Lite projectId is required");
|
|
10586
|
+
return async (request) => {
|
|
10587
|
+
try {
|
|
10588
|
+
const context = await options.context(request);
|
|
10589
|
+
const { identity, access } = context;
|
|
10590
|
+
if (identity.authenticated !== true || !identity.subject || !identity.clientId || new URL(identity.issuer).protocol !== "https:" || access.projectId !== options.projectId || !access.tenantId || !Array.isArray(access.permissions) || access.permissions.some((value) => typeof value !== "string" || !value.trim())) {
|
|
10591
|
+
throw new ApiError(403, { message: "Application access denied" });
|
|
10592
|
+
}
|
|
10593
|
+
const subject = await options.resolveLocalSubject(identity);
|
|
10594
|
+
if (!subject || !/^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(subject)) {
|
|
10595
|
+
throw new ApiError(403, { message: "External identity has no local subject mapping" });
|
|
10596
|
+
}
|
|
10597
|
+
const bearer = request.headers.get("authorization")?.match(/^Bearer ([^\s]+)$/i)?.[1];
|
|
10598
|
+
const payload = bearer ? decodeJwt(bearer) : null;
|
|
10599
|
+
if (typeof payload?.exp !== "number" || !Number.isSafeInteger(payload.exp) || payload.exp * 1000 <= Date.now()) {
|
|
10600
|
+
throw new ApiError(401, { message: "External identity expired" });
|
|
10601
|
+
}
|
|
10602
|
+
return {
|
|
10603
|
+
role: "authenticated",
|
|
10604
|
+
sub: subject,
|
|
10605
|
+
external_sub: identity.subject,
|
|
10606
|
+
iss: identity.issuer,
|
|
10607
|
+
client_id: identity.clientId,
|
|
10608
|
+
exp: payload.exp,
|
|
10609
|
+
project_id: access.projectId,
|
|
10610
|
+
tenant_id: access.tenantId,
|
|
10611
|
+
permissions: [...new Set(access.permissions)]
|
|
10612
|
+
};
|
|
10613
|
+
} catch (error) {
|
|
10614
|
+
if (error instanceof ApiError)
|
|
10615
|
+
throw error;
|
|
10616
|
+
const status = errorProperty(error, "status");
|
|
10617
|
+
throw new ApiError(status === 401 || status === 403 ? status : 503, {
|
|
10618
|
+
message: status === 401 ? "Authentication required" : status === 403 ? "Application access denied" : "Identity verification unavailable"
|
|
10619
|
+
});
|
|
10620
|
+
}
|
|
10621
|
+
};
|
|
10622
|
+
}
|
|
10240
10623
|
// src/runtime/gen-types.ts
|
|
10241
10624
|
var JSON_TYPE = "Json";
|
|
10242
10625
|
async function generateTypes(db, schema = "public") {
|
|
@@ -10488,6 +10871,7 @@ var CORS_HEADERS = {
|
|
|
10488
10871
|
"access-control-max-age": "86400"
|
|
10489
10872
|
};
|
|
10490
10873
|
async function createBackend(config = {}) {
|
|
10874
|
+
const mode = runtimeMode(config.runtimeMode);
|
|
10491
10875
|
const jwtSecret = config.jwtSecret ?? randomSecret();
|
|
10492
10876
|
const apiUrl = config.apiUrl ?? "http://localhost:54321";
|
|
10493
10877
|
const siteUrl = config.siteUrl ?? apiUrl;
|
|
@@ -10502,7 +10886,7 @@ async function createBackend(config = {}) {
|
|
|
10502
10886
|
const vaultKey = config.vaultKey ?? `supacloud-lite-vault:${jwtSecret}`;
|
|
10503
10887
|
assertSecretsSafe({ host: config.host, jwtSecret, vaultKeyDerived, warn: log });
|
|
10504
10888
|
const engine = config.engine;
|
|
10505
|
-
const db = await Database.create(engine ?? config.dataDir, { vaultKey });
|
|
10889
|
+
const db = await Database.create(engine ?? config.dataDir, { vaultKey, strict: mode === "strict" });
|
|
10506
10890
|
const cleanup = [];
|
|
10507
10891
|
const failStartup = async (e) => {
|
|
10508
10892
|
for (const fn of cleanup.reverse())
|
|
@@ -10519,6 +10903,11 @@ async function createBackend(config = {}) {
|
|
|
10519
10903
|
} catch (e) {
|
|
10520
10904
|
await failStartup(e);
|
|
10521
10905
|
}
|
|
10906
|
+
const graphqlCapability = config.graphql?.enabled === false ? { status: "disabled", extension: "pg_graphql" } : await inspectGraphql(db.engine).catch(failStartup);
|
|
10907
|
+
if (config.graphql?.enabled === true && graphqlCapability.status !== "supported") {
|
|
10908
|
+
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`));
|
|
10909
|
+
}
|
|
10910
|
+
const graphql = await Promise.resolve().then(() => new GraphqlHandler(db, graphqlCapability, config.graphql, config.dbSchemas)).catch(failStartup);
|
|
10522
10911
|
const pgredis = await PgredisCache.create(db).catch(failStartup);
|
|
10523
10912
|
const { anonKey, serviceRoleKey } = await mintProjectApiKeys(jwtSecret);
|
|
10524
10913
|
const exposedSchemas = [...new Set(config.dbSchemas ?? ["public", "pgmq_public"])];
|
|
@@ -10559,7 +10948,13 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10559
10948
|
rateLimiter: new RateLimiter(config.authRateLimits)
|
|
10560
10949
|
});
|
|
10561
10950
|
cleanup.push(() => auth.stop());
|
|
10562
|
-
const realtime = new RealtimeEngine(db, jwtSecret)
|
|
10951
|
+
const realtime = new RealtimeEngine(db, jwtSecret, config.externalIdentity ? async (token) => {
|
|
10952
|
+
const request = new Request(`${apiUrl}/realtime/v1`, { headers: { authorization: `Bearer ${token}` } });
|
|
10953
|
+
const context = await resolveContext(request, new URL(request.url));
|
|
10954
|
+
if (context instanceof Response)
|
|
10955
|
+
throw new Error("Realtime authentication denied");
|
|
10956
|
+
return context;
|
|
10957
|
+
} : undefined);
|
|
10563
10958
|
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));
|
|
10564
10959
|
const cron = new CronService(db);
|
|
10565
10960
|
const retention = new RetentionService(db, config.retention);
|
|
@@ -10594,7 +10989,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10594
10989
|
} catch (error) {
|
|
10595
10990
|
await failStartup(error);
|
|
10596
10991
|
}
|
|
10597
|
-
const functions = new FunctionsHandler(fnMap, fnEnv, pgredis);
|
|
10992
|
+
const functions = await Promise.resolve().then(() => new FunctionsHandler(fnMap, fnEnv, pgredis, mode)).catch(failStartup);
|
|
10598
10993
|
async function resolveContext(req, url) {
|
|
10599
10994
|
const authz = req.headers.get("authorization");
|
|
10600
10995
|
const bearer = authz?.toLowerCase().startsWith("bearer ") ? authz.slice(7) : null;
|
|
@@ -10605,7 +11000,15 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10605
11000
|
headers: { "content-type": "application/json" }
|
|
10606
11001
|
}));
|
|
10607
11002
|
}
|
|
10608
|
-
|
|
11003
|
+
let claims = await verifyJwt(token, jwtSecret);
|
|
11004
|
+
if (config.externalIdentity && token !== anonKey && token !== serviceRoleKey) {
|
|
11005
|
+
try {
|
|
11006
|
+
claims = validateExternalIdentityClaims(await config.externalIdentity(req));
|
|
11007
|
+
} catch (error) {
|
|
11008
|
+
const status = error instanceof ApiError && [401, 403, 503].includes(error.status) ? error.status : 503;
|
|
11009
|
+
return withCors(Response.json({ message: status === 503 ? "Identity verification unavailable" : "External access denied" }, { status }));
|
|
11010
|
+
}
|
|
11011
|
+
}
|
|
10609
11012
|
if (!claims) {
|
|
10610
11013
|
return withCors(new Response(JSON.stringify({ message: "Invalid API key" }), {
|
|
10611
11014
|
status: 401,
|
|
@@ -10647,7 +11050,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10647
11050
|
return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
|
|
10648
11051
|
}
|
|
10649
11052
|
}
|
|
10650
|
-
if (config.authEnabled === false && path.startsWith("/auth/v1")) {
|
|
11053
|
+
if ((config.authEnabled === false || config.externalIdentity) && path.startsWith("/auth/v1")) {
|
|
10651
11054
|
return withCors(new Response(JSON.stringify({ message: "Auth service is disabled" }), {
|
|
10652
11055
|
status: 404,
|
|
10653
11056
|
headers: { "content-type": "application/json" }
|
|
@@ -10679,6 +11082,8 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10679
11082
|
return ctx;
|
|
10680
11083
|
if (path.startsWith("/rest/v1"))
|
|
10681
11084
|
return withCors(await rest.handle(req, ctx, url));
|
|
11085
|
+
if (path === "/graphql/v1" || path === "/graphql/v1/")
|
|
11086
|
+
return withCors(await graphql.handle(req, ctx));
|
|
10682
11087
|
if (path.startsWith("/functions/v1"))
|
|
10683
11088
|
return withCors(await functions.handle(req, ctx, url));
|
|
10684
11089
|
if (path.startsWith("/storage/v1"))
|
|
@@ -10723,6 +11128,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10723
11128
|
return loggedFetch(request);
|
|
10724
11129
|
}, { preconnect: fetch.preconnect });
|
|
10725
11130
|
return {
|
|
11131
|
+
graphql: graphqlCapability,
|
|
10726
11132
|
fetch: publicFetch,
|
|
10727
11133
|
db,
|
|
10728
11134
|
realtime,
|
|
@@ -10737,7 +11143,16 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
10737
11143
|
logs,
|
|
10738
11144
|
inbox,
|
|
10739
11145
|
smsInbox,
|
|
10740
|
-
migrate: (migrations, seedSql) =>
|
|
11146
|
+
migrate: async (migrations, seedSql) => {
|
|
11147
|
+
const applied = await db.runMigrations(migrations, seedSql);
|
|
11148
|
+
if (config.graphql?.enabled !== false) {
|
|
11149
|
+
const capability = await inspectGraphql(db.engine);
|
|
11150
|
+
delete graphqlCapability.version;
|
|
11151
|
+
delete graphqlCapability.reason;
|
|
11152
|
+
Object.assign(graphqlCapability, capability);
|
|
11153
|
+
}
|
|
11154
|
+
return applied;
|
|
11155
|
+
},
|
|
10741
11156
|
close: () => {
|
|
10742
11157
|
closePromise ??= (async () => {
|
|
10743
11158
|
let firstError;
|
|
@@ -11245,12 +11660,19 @@ class PgWireClient {
|
|
|
11245
11660
|
onNotification = null;
|
|
11246
11661
|
static async connect(opts) {
|
|
11247
11662
|
const client = new PgWireClient;
|
|
11248
|
-
|
|
11663
|
+
try {
|
|
11664
|
+
await client.open(opts);
|
|
11665
|
+
} catch (error) {
|
|
11666
|
+
client.socket.destroy();
|
|
11667
|
+
throw error;
|
|
11668
|
+
}
|
|
11249
11669
|
return client;
|
|
11250
11670
|
}
|
|
11251
11671
|
open(opts) {
|
|
11252
11672
|
return new Promise((resolve3, reject) => {
|
|
11673
|
+
let startupComplete = false;
|
|
11253
11674
|
this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
|
|
11675
|
+
this.socket.setTimeout(1e4, () => this.socket.destroy(new Error("PostgreSQL handshake timed out")));
|
|
11254
11676
|
this.socket.on("error", (e) => {
|
|
11255
11677
|
if (this.pending)
|
|
11256
11678
|
this.pending.reject(e);
|
|
@@ -11259,6 +11681,8 @@ class PgWireClient {
|
|
|
11259
11681
|
this.socket.on("close", () => {
|
|
11260
11682
|
this.closed = true;
|
|
11261
11683
|
this.pending?.reject(new Error("connection closed"));
|
|
11684
|
+
if (!startupComplete)
|
|
11685
|
+
reject(new Error("connection closed during PostgreSQL handshake"));
|
|
11262
11686
|
});
|
|
11263
11687
|
this.socket.on("connect", () => {
|
|
11264
11688
|
const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
|
|
@@ -11342,6 +11766,8 @@ class PgWireClient {
|
|
|
11342
11766
|
reject(new PgWireError(parseErrorFields(payload)));
|
|
11343
11767
|
return;
|
|
11344
11768
|
} else if (type === 90) {
|
|
11769
|
+
startupComplete = true;
|
|
11770
|
+
this.socket.setTimeout(0);
|
|
11345
11771
|
this.socket.off("data", startupHandler);
|
|
11346
11772
|
this.socket.on("data", (c) => {
|
|
11347
11773
|
this.buffer = Buffer.concat([this.buffer, c]);
|
|
@@ -11619,27 +12045,6 @@ function parsePgArray(text) {
|
|
|
11619
12045
|
return out;
|
|
11620
12046
|
}
|
|
11621
12047
|
|
|
11622
|
-
// src/runtime/db/engine.ts
|
|
11623
|
-
class Mutex {
|
|
11624
|
-
tail = Promise.resolve();
|
|
11625
|
-
async lock() {
|
|
11626
|
-
let release;
|
|
11627
|
-
const next = new Promise((r) => release = r);
|
|
11628
|
-
const prev = this.tail;
|
|
11629
|
-
this.tail = this.tail.then(() => next);
|
|
11630
|
-
await prev;
|
|
11631
|
-
return release;
|
|
11632
|
-
}
|
|
11633
|
-
async run(fn) {
|
|
11634
|
-
const release = await this.lock();
|
|
11635
|
-
try {
|
|
11636
|
-
return await fn();
|
|
11637
|
-
} finally {
|
|
11638
|
-
release();
|
|
11639
|
-
}
|
|
11640
|
-
}
|
|
11641
|
-
}
|
|
11642
|
-
|
|
11643
12048
|
// src/runtime/node/native/wire-engine.ts
|
|
11644
12049
|
async function buildWireEngine(options) {
|
|
11645
12050
|
const queryClient = await options.connect();
|
|
@@ -11678,9 +12083,9 @@ async function buildWireEngine(options) {
|
|
|
11678
12083
|
return queryMutex.run(async () => {
|
|
11679
12084
|
await queryClient.exec("begin");
|
|
11680
12085
|
try {
|
|
11681
|
-
const
|
|
12086
|
+
const response2 = await callback(transactionClient);
|
|
11682
12087
|
await queryClient.exec("commit");
|
|
11683
|
-
return
|
|
12088
|
+
return response2;
|
|
11684
12089
|
} catch (error) {
|
|
11685
12090
|
await queryClient.exec("rollback").catch(() => {});
|
|
11686
12091
|
throw error;
|
|
@@ -11752,6 +12157,14 @@ function toPgArrayLiteral(array) {
|
|
|
11752
12157
|
var DEFAULT_PG_VERSION = "17.7.0";
|
|
11753
12158
|
var POSTGRES_MIRROR_URL_ERROR = "SUPACLOUD_LITE_POSTGRES_MIRROR must be an absolute HTTPS URL or a loopback HTTP URL";
|
|
11754
12159
|
var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
|
|
12160
|
+
function nativePostgresMajor(installDir = process.env.SUPACLOUD_LITE_POSTGRES_DIR) {
|
|
12161
|
+
if (!installDir)
|
|
12162
|
+
return NATIVE_POSTGRES_MAJOR;
|
|
12163
|
+
const version = execFileSync(join2(installDir, "bin", "postgres"), ["--version"], { encoding: "utf8", timeout: 1e4 }).match(/PostgreSQL\)\s+(\d+)/)?.[1];
|
|
12164
|
+
if (!version)
|
|
12165
|
+
throw new Error("unable to identify the PostgreSQL installation major version");
|
|
12166
|
+
return version;
|
|
12167
|
+
}
|
|
11755
12168
|
var GLIBC_DYNAMIC_LOADERS = {
|
|
11756
12169
|
x64: [
|
|
11757
12170
|
"/lib64/ld-linux-x86-64.so.2",
|
|
@@ -11901,10 +12314,10 @@ async function fetchRelease(url) {
|
|
|
11901
12314
|
let lastError;
|
|
11902
12315
|
for (let attempt = 1;attempt <= 3; attempt++) {
|
|
11903
12316
|
try {
|
|
11904
|
-
const
|
|
11905
|
-
if (
|
|
11906
|
-
return
|
|
11907
|
-
lastError = new Error(`failed to download ${url}: HTTP ${
|
|
12317
|
+
const response2 = await fetch(url, { signal: AbortSignal.timeout(30000) });
|
|
12318
|
+
if (response2.ok || response2.status < 500)
|
|
12319
|
+
return response2;
|
|
12320
|
+
lastError = new Error(`failed to download ${url}: HTTP ${response2.status}`);
|
|
11908
12321
|
} catch (error) {
|
|
11909
12322
|
lastError = error;
|
|
11910
12323
|
}
|
|
@@ -11930,8 +12343,16 @@ async function createNativeEngine(opts) {
|
|
|
11930
12343
|
let postgres;
|
|
11931
12344
|
let removeExitHandler;
|
|
11932
12345
|
try {
|
|
11933
|
-
const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
|
|
12346
|
+
const installDir = opts.installDir ?? process.env.SUPACLOUD_LITE_POSTGRES_DIR ?? await ensurePostgres(opts.version, opts.cacheDir, opts.log, opts.downloadMirror);
|
|
11934
12347
|
const bin = (name) => join2(installDir, "bin", name);
|
|
12348
|
+
if (!existsSync2(bin("postgres")) || !existsSync2(bin("initdb"))) {
|
|
12349
|
+
throw new Error("PostgreSQL installation requires bin/postgres and bin/initdb");
|
|
12350
|
+
}
|
|
12351
|
+
const serverVersion = nativePostgresMajor(installDir);
|
|
12352
|
+
const storedMajor = existsSync2(join2(opts.dataDir, "PG_VERSION")) ? readFileSync(join2(opts.dataDir, "PG_VERSION"), "utf8").trim() : undefined;
|
|
12353
|
+
if (!serverVersion || storedMajor && storedMajor !== serverVersion) {
|
|
12354
|
+
throw new Error("PostgreSQL installation major version does not match the data directory; use a logical migration");
|
|
12355
|
+
}
|
|
11935
12356
|
if (!existsSync2(join2(opts.dataDir, "PG_VERSION"))) {
|
|
11936
12357
|
mkdirSync(opts.dataDir, { recursive: true });
|
|
11937
12358
|
try {
|
|
@@ -12051,8 +12472,16 @@ function removeStalePidFile(pidPath) {
|
|
|
12051
12472
|
}
|
|
12052
12473
|
// src/runtime/node/native/readiness.ts
|
|
12053
12474
|
function liteCapabilities(engine, replicationProfile) {
|
|
12475
|
+
const common = {
|
|
12476
|
+
graphql: { status: "unverified", extension: "pg_graphql", reason: "DATABASE_NOT_INSPECTED" },
|
|
12477
|
+
runtime_mode: "development",
|
|
12478
|
+
identity: { mode: "local", verification: "built-in" },
|
|
12479
|
+
migrations: { formats: ["flat-sql", "timestamp-folder"], content_check: "supported", target_bindings: "supported" },
|
|
12480
|
+
project_release_manifests: "unsupported"
|
|
12481
|
+
};
|
|
12054
12482
|
if (engine === "pglite") {
|
|
12055
12483
|
return {
|
|
12484
|
+
...common,
|
|
12056
12485
|
engine,
|
|
12057
12486
|
state_machine_sql: "supported",
|
|
12058
12487
|
durable_workflows: "supported",
|
|
@@ -12064,6 +12493,7 @@ function liteCapabilities(engine, replicationProfile) {
|
|
|
12064
12493
|
};
|
|
12065
12494
|
}
|
|
12066
12495
|
return {
|
|
12496
|
+
...common,
|
|
12067
12497
|
engine,
|
|
12068
12498
|
state_machine_sql: "supported",
|
|
12069
12499
|
durable_workflows: "supported",
|
|
@@ -12279,8 +12709,9 @@ function sameStrings(left, right) {
|
|
|
12279
12709
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
12280
12710
|
}
|
|
12281
12711
|
// src/project-runtime.ts
|
|
12282
|
-
import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
|
|
12712
|
+
import { chmod, link, lstat as lstat2, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
|
|
12283
12713
|
import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve3 } from "path";
|
|
12714
|
+
import { pathToFileURL as pathToFileURL2 } from "url";
|
|
12284
12715
|
|
|
12285
12716
|
// src/runtime/node/config-toml.ts
|
|
12286
12717
|
import { readFileSync as readFileSync2 } from "fs";
|
|
@@ -12429,16 +12860,51 @@ function getByteSize(table, key) {
|
|
|
12429
12860
|
}
|
|
12430
12861
|
|
|
12431
12862
|
// src/runtime/node/load-config.ts
|
|
12432
|
-
function loadProjectConfig(projectDir, env = process.env) {
|
|
12863
|
+
function loadProjectConfig(projectDir, env = process.env, modeOverride) {
|
|
12433
12864
|
const root = loadConfigToml(projectDir, env);
|
|
12865
|
+
const mode = runtimeMode(modeOverride ?? env.SUPACLOUD_LITE_RUNTIME_MODE ?? getString(tableAt(root, "lite"), "runtime_mode"));
|
|
12434
12866
|
return {
|
|
12867
|
+
lite: readLite(root, mode),
|
|
12435
12868
|
auth: readAuth(root, env),
|
|
12436
12869
|
api: readApi(root),
|
|
12437
12870
|
storage: readStorage(root),
|
|
12438
12871
|
seed: readSeed(root),
|
|
12439
|
-
functions: readFunctions(root)
|
|
12872
|
+
functions: readFunctions(root, mode)
|
|
12440
12873
|
};
|
|
12441
12874
|
}
|
|
12875
|
+
function readLite(root, mode) {
|
|
12876
|
+
const graphql = tableAt(root, "lite.graphql");
|
|
12877
|
+
const bindings = tableAt(root, "lite.migration_bindings");
|
|
12878
|
+
const out = {
|
|
12879
|
+
runtimeMode: mode,
|
|
12880
|
+
graphql: {
|
|
12881
|
+
enabled: getBool(graphql, "enabled"),
|
|
12882
|
+
maxRequestBodyBytes: getInt(graphql, "max_request_body_bytes"),
|
|
12883
|
+
statementTimeoutMs: getInt(graphql, "statement_timeout_ms")
|
|
12884
|
+
},
|
|
12885
|
+
identityModule: getString(tableAt(root, "lite.identity"), "module")
|
|
12886
|
+
};
|
|
12887
|
+
for (const key of ["max_request_body_bytes", "statement_timeout_ms"]) {
|
|
12888
|
+
const value = graphql?.values.get(key);
|
|
12889
|
+
if (value !== undefined && (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0)) {
|
|
12890
|
+
throw new Error(`invalid GraphQL option: ${key}`);
|
|
12891
|
+
}
|
|
12892
|
+
}
|
|
12893
|
+
if (graphql?.values.has("enabled") && getBool(graphql, "enabled") === undefined)
|
|
12894
|
+
throw new Error("invalid GraphQL enabled option");
|
|
12895
|
+
const identity = tableAt(root, "lite.identity");
|
|
12896
|
+
if (identity && !out.identityModule)
|
|
12897
|
+
throw new Error("Lite identity configuration requires a module");
|
|
12898
|
+
if (bindings) {
|
|
12899
|
+
const manifest = getString(bindings, "manifest");
|
|
12900
|
+
const environment = getString(bindings, "environment");
|
|
12901
|
+
const projectRef = getString(bindings, "project_ref");
|
|
12902
|
+
if (!manifest || !environment || !projectRef)
|
|
12903
|
+
throw new Error("Lite migration bindings require manifest, environment and project_ref");
|
|
12904
|
+
out.migrationBindings = { manifest, environment, projectRef };
|
|
12905
|
+
}
|
|
12906
|
+
return out;
|
|
12907
|
+
}
|
|
12442
12908
|
function readAuth(root, env) {
|
|
12443
12909
|
const auth = tableAt(root, "auth");
|
|
12444
12910
|
const out = { settings: readAuthSettings(root), oauthProviders: readOAuthProviders(root, env) };
|
|
@@ -12634,7 +13100,7 @@ function readSeed(root) {
|
|
|
12634
13100
|
out.paths = paths;
|
|
12635
13101
|
return out;
|
|
12636
13102
|
}
|
|
12637
|
-
function readFunctions(root) {
|
|
13103
|
+
function readFunctions(root, mode) {
|
|
12638
13104
|
const fns = tableAt(root, "functions");
|
|
12639
13105
|
const out = {};
|
|
12640
13106
|
if (!fns)
|
|
@@ -12655,9 +13121,29 @@ function readFunctions(root) {
|
|
|
12655
13121
|
if (framework === "fetch" || framework === "elysia" || framework === "hono") {
|
|
12656
13122
|
opts.framework = framework;
|
|
12657
13123
|
} else {
|
|
13124
|
+
if (mode === "strict")
|
|
13125
|
+
throw new Error(`invalid framework for function "${name}"`);
|
|
12658
13126
|
console.warn(` warning: [functions.${name}] framework "${framework}" is not one of fetch/elysia/hono; falling back to fetch`);
|
|
12659
13127
|
}
|
|
12660
13128
|
}
|
|
13129
|
+
if (mode === "strict") {
|
|
13130
|
+
for (const key of ["timeout_ms", "max_request_body_bytes", "max_response_body_bytes", "wait_until_timeout_ms"]) {
|
|
13131
|
+
if (t.values.has(key)) {
|
|
13132
|
+
const value = t.values.get(key);
|
|
13133
|
+
if (typeof value !== "string" || !/^\d+$/.test(value) || !Number.isSafeInteger(Number(value)) || Number(value) <= 0) {
|
|
13134
|
+
throw new Error(`invalid strict function limit: ${name}.${key}`);
|
|
13135
|
+
}
|
|
13136
|
+
}
|
|
13137
|
+
}
|
|
13138
|
+
for (const key of ["background", "enabled", "verify_jwt"]) {
|
|
13139
|
+
if (t.values.has(key) && getBool(t, key) === undefined)
|
|
13140
|
+
throw new Error(`invalid strict function option: ${name}.${key}`);
|
|
13141
|
+
}
|
|
13142
|
+
for (const key of ["secrets", "outbound_hosts"]) {
|
|
13143
|
+
if (t.values.has(key) && getStringArray(t, key) === undefined)
|
|
13144
|
+
throw new Error(`invalid strict function option: ${name}.${key}`);
|
|
13145
|
+
}
|
|
13146
|
+
}
|
|
12661
13147
|
const timeoutMs = getInt(t, "timeout_ms");
|
|
12662
13148
|
if (timeoutMs !== undefined && timeoutMs > 0)
|
|
12663
13149
|
opts.timeoutMs = timeoutMs;
|
|
@@ -12738,8 +13224,9 @@ function remotePlugin() {
|
|
|
12738
13224
|
async function bundleFunction(entryPath, name) {
|
|
12739
13225
|
const outDir = join4(tmpdir2(), "supacloud-lite-fn-bundle", name);
|
|
12740
13226
|
await mkdir3(outDir, { recursive: true });
|
|
13227
|
+
let buildOutput;
|
|
12741
13228
|
try {
|
|
12742
|
-
|
|
13229
|
+
buildOutput = await Bun.build({
|
|
12743
13230
|
entrypoints: [entryPath],
|
|
12744
13231
|
format: "esm",
|
|
12745
13232
|
target: "bun",
|
|
@@ -12747,6 +13234,18 @@ async function bundleFunction(entryPath, name) {
|
|
|
12747
13234
|
naming: `${name}-[hash].[ext]`,
|
|
12748
13235
|
plugins: [remotePlugin()]
|
|
12749
13236
|
});
|
|
13237
|
+
} catch (buildThrow) {
|
|
13238
|
+
const details = buildThrow instanceof AggregateError ? buildThrow.errors.map((item) => item?.message ?? String(item)).filter(Boolean).join(`
|
|
13239
|
+
`) : buildThrow instanceof Error ? buildThrow.message : String(buildThrow);
|
|
13240
|
+
try {
|
|
13241
|
+
await rm2(outDir, { recursive: true, force: true });
|
|
13242
|
+
} catch (cleanupError) {
|
|
13243
|
+
throw new AggregateError([buildThrow, cleanupError], `failed to clean function bundle directory ${outDir}`);
|
|
13244
|
+
}
|
|
13245
|
+
throw new Error(`failed to bundle ${entryPath}
|
|
13246
|
+
${details}`, { cause: buildThrow });
|
|
13247
|
+
}
|
|
13248
|
+
try {
|
|
12750
13249
|
if (!buildOutput.success || buildOutput.outputs.length === 0) {
|
|
12751
13250
|
throw new Error(buildOutput.logs.map((item) => item.message).join(`
|
|
12752
13251
|
`) || `failed to bundle ${entryPath}`);
|
|
@@ -12790,16 +13289,18 @@ async function loadFunctionEnv(projectDir) {
|
|
|
12790
13289
|
return env;
|
|
12791
13290
|
}
|
|
12792
13291
|
var FUNCTION_FRAMEWORKS = new Set(["fetch", "elysia", "hono"]);
|
|
12793
|
-
function resolveFramework(name, value) {
|
|
13292
|
+
function resolveFramework(name, value, mode) {
|
|
12794
13293
|
if (value === undefined)
|
|
12795
13294
|
return;
|
|
12796
13295
|
if (FUNCTION_FRAMEWORKS.has(value))
|
|
12797
13296
|
return value;
|
|
13297
|
+
if (mode === "strict")
|
|
13298
|
+
throw new Error(`invalid framework for function "${name}"`);
|
|
12798
13299
|
console.warn(` warning: function "${name}" has unsupported framework "${value}", expected one of fetch/elysia/hono; falling back to fetch`);
|
|
12799
13300
|
return;
|
|
12800
13301
|
}
|
|
12801
13302
|
var loadQueue = Promise.resolve();
|
|
12802
|
-
async function loadFunctions2(projectDir, options = {}) {
|
|
13303
|
+
async function loadFunctions2(projectDir, options = {}, mode = "development") {
|
|
12803
13304
|
let releaseQueue;
|
|
12804
13305
|
const previous = loadQueue;
|
|
12805
13306
|
loadQueue = new Promise((resolveQueue) => {
|
|
@@ -12807,18 +13308,23 @@ async function loadFunctions2(projectDir, options = {}) {
|
|
|
12807
13308
|
});
|
|
12808
13309
|
await previous;
|
|
12809
13310
|
try {
|
|
12810
|
-
return await loadFunctionsUnlocked(projectDir, options);
|
|
13311
|
+
return await loadFunctionsUnlocked(projectDir, options, mode);
|
|
12811
13312
|
} finally {
|
|
12812
13313
|
releaseQueue();
|
|
12813
13314
|
}
|
|
12814
13315
|
}
|
|
12815
|
-
async function loadFunctionsUnlocked(projectDir, options) {
|
|
13316
|
+
async function loadFunctionsUnlocked(projectDir, options, mode) {
|
|
12816
13317
|
const functions = new Map;
|
|
12817
13318
|
const root = join5(projectDir, "supabase", "functions");
|
|
12818
13319
|
let entries = [];
|
|
12819
13320
|
try {
|
|
12820
13321
|
entries = await readdir(root);
|
|
12821
|
-
} catch {
|
|
13322
|
+
} catch (error) {
|
|
13323
|
+
if (mode === "strict" && errorProperty(error, "code") !== "ENOENT")
|
|
13324
|
+
throw error;
|
|
13325
|
+
if (mode === "strict" && Object.values(options).some((option) => option.enabled !== false)) {
|
|
13326
|
+
throw new Error("declared function directory is missing");
|
|
13327
|
+
}
|
|
12822
13328
|
return functions;
|
|
12823
13329
|
}
|
|
12824
13330
|
installDenoShim();
|
|
@@ -12868,14 +13374,18 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
12868
13374
|
} : undefined;
|
|
12869
13375
|
functions.set(name, {
|
|
12870
13376
|
handler,
|
|
12871
|
-
framework: resolveFramework(name, opts?.framework),
|
|
13377
|
+
framework: resolveFramework(name, opts?.framework, mode),
|
|
12872
13378
|
...limits ? { limits } : {},
|
|
12873
13379
|
...capabilities ? { capabilities } : {}
|
|
12874
13380
|
});
|
|
12875
13381
|
} else {
|
|
13382
|
+
if (mode === "strict")
|
|
13383
|
+
throw new Error(`function "${name}" has no handler`);
|
|
12876
13384
|
console.warn(` warning: function "${name}" has no default function, handle/fetch object, or Deno.serve() handler, skipped`);
|
|
12877
13385
|
}
|
|
12878
13386
|
} catch (e) {
|
|
13387
|
+
if (mode === "strict")
|
|
13388
|
+
throw new Error(`failed to load strict function "${name}"`, { cause: e });
|
|
12879
13389
|
const msg = e instanceof Error ? e.message : String(e);
|
|
12880
13390
|
if (/Unknown file extension|Cannot find module/.test(msg)) {
|
|
12881
13391
|
console.warn(` warning: function "${name}" could not be bundled by Bun: ${msg}`);
|
|
@@ -12888,16 +13398,174 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
12888
13398
|
}
|
|
12889
13399
|
break;
|
|
12890
13400
|
}
|
|
13401
|
+
if (mode === "strict" && !functions.has(name))
|
|
13402
|
+
throw new Error(`function "${name}" has no loadable entrypoint`);
|
|
13403
|
+
}
|
|
13404
|
+
if (mode === "strict") {
|
|
13405
|
+
for (const [name, option] of Object.entries(options)) {
|
|
13406
|
+
if (option.enabled !== false && !functions.has(name))
|
|
13407
|
+
throw new Error(`declared function "${name}" was not loaded`);
|
|
13408
|
+
}
|
|
12891
13409
|
}
|
|
12892
13410
|
return functions;
|
|
12893
13411
|
}
|
|
12894
13412
|
|
|
12895
13413
|
// src/runtime/node/project.ts
|
|
12896
|
-
import { readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
13414
|
+
import { lstat, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
12897
13415
|
import { join as join6 } from "path";
|
|
13416
|
+
|
|
13417
|
+
// node_modules/@supacloud/db/dist/index.js
|
|
13418
|
+
import { createHash as createHash4 } from "crypto";
|
|
13419
|
+
var TOKEN = /__[A-Z][A-Z0-9_]*__/g;
|
|
13420
|
+
var RESERVED_TOKEN = /__SC_BINDING_[A-Z0-9_]+__/;
|
|
13421
|
+
var HASH = /^[a-f0-9]{64}$/;
|
|
13422
|
+
function migrationBindingSha256(value) {
|
|
13423
|
+
return createHash4("sha256").update(value).digest("hex");
|
|
13424
|
+
}
|
|
13425
|
+
function record(value) {
|
|
13426
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
13427
|
+
throw new Error("Invalid migration binding object");
|
|
13428
|
+
}
|
|
13429
|
+
return value;
|
|
13430
|
+
}
|
|
13431
|
+
function keys(row, expected) {
|
|
13432
|
+
if (Object.keys(row).sort().join(",") !== expected.sort().join(",")) {
|
|
13433
|
+
throw new Error("Unexpected migration binding fields");
|
|
13434
|
+
}
|
|
13435
|
+
}
|
|
13436
|
+
function validTarget(value) {
|
|
13437
|
+
const target2 = record(value);
|
|
13438
|
+
keys(target2, ["environment", "projectRef"]);
|
|
13439
|
+
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)) {
|
|
13440
|
+
throw new Error("Invalid migration binding target");
|
|
13441
|
+
}
|
|
13442
|
+
return target2;
|
|
13443
|
+
}
|
|
13444
|
+
function validFile(file) {
|
|
13445
|
+
return typeof file === "string" && /^[a-zA-Z0-9_./-]+\.sql$/.test(file) && !file.startsWith("/") && file.split("/").every((part) => part && part !== "." && part !== "..");
|
|
13446
|
+
}
|
|
13447
|
+
function parseMigrationBindingManifest(value) {
|
|
13448
|
+
const manifest = record(value);
|
|
13449
|
+
keys(manifest, ["schema", "targets", "templates"]);
|
|
13450
|
+
if (manifest.schema !== "supacloud.migration-bindings.v1" || !Array.isArray(manifest.targets) || !manifest.targets.length || !Array.isArray(manifest.templates)) {
|
|
13451
|
+
throw new Error("Invalid migration binding manifest");
|
|
13452
|
+
}
|
|
13453
|
+
const targets = manifest.targets.map(validTarget);
|
|
13454
|
+
if (new Set(targets.map((target2) => target2.environment)).size !== targets.length) {
|
|
13455
|
+
throw new Error("Duplicate migration binding environment");
|
|
13456
|
+
}
|
|
13457
|
+
const files = new Set;
|
|
13458
|
+
const templates = manifest.templates.map((value2) => {
|
|
13459
|
+
const template = record(value2);
|
|
13460
|
+
keys(template, ["file", "templateSha256", "parameters"]);
|
|
13461
|
+
if (!validFile(template.file) || files.has(template.file) || typeof template.templateSha256 !== "string" || !HASH.test(template.templateSha256) || !Array.isArray(template.parameters) || !template.parameters.length) {
|
|
13462
|
+
throw new Error("Invalid or duplicate migration binding template");
|
|
13463
|
+
}
|
|
13464
|
+
files.add(template.file);
|
|
13465
|
+
const placeholders = new Set;
|
|
13466
|
+
const parameters = template.parameters.map((value3) => {
|
|
13467
|
+
const parameter = record(value3);
|
|
13468
|
+
keys(parameter, ["placeholder", "variable", "type", "occurrences"]);
|
|
13469
|
+
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) {
|
|
13470
|
+
throw new Error("Invalid or duplicate migration binding parameter");
|
|
13471
|
+
}
|
|
13472
|
+
placeholders.add(parameter.placeholder);
|
|
13473
|
+
return parameter;
|
|
13474
|
+
});
|
|
13475
|
+
return { file: template.file, templateSha256: template.templateSha256, parameters };
|
|
13476
|
+
});
|
|
13477
|
+
return { schema: "supacloud.migration-bindings.v1", targets, templates };
|
|
13478
|
+
}
|
|
13479
|
+
function bindingValue(parameter, values) {
|
|
13480
|
+
const value = Object.hasOwn(values, parameter.variable) ? values[parameter.variable] : undefined;
|
|
13481
|
+
if (typeof value !== "string" || !value || value.trim() !== value) {
|
|
13482
|
+
throw new Error(`Missing or invalid migration binding variable: ${parameter.variable}`);
|
|
13483
|
+
}
|
|
13484
|
+
let valid = false;
|
|
13485
|
+
if (parameter.type === "uuid") {
|
|
13486
|
+
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);
|
|
13487
|
+
} else if (parameter.type === "resource-name") {
|
|
13488
|
+
valid = /^[A-Za-z0-9][A-Za-z0-9._/-]{0,254}$/.test(value);
|
|
13489
|
+
} else if (/^https:\/\/[A-Za-z0-9._~:/?#[\]@!&()*+,;=%-]+$/.test(value)) {
|
|
13490
|
+
try {
|
|
13491
|
+
const url = new URL(value);
|
|
13492
|
+
valid = url.protocol === "https:" && Boolean(url.hostname) && !url.username && !url.password;
|
|
13493
|
+
} catch {}
|
|
13494
|
+
}
|
|
13495
|
+
if (!valid || value.includes("__"))
|
|
13496
|
+
throw new Error(`Invalid ${parameter.type} migration binding: ${parameter.variable}`);
|
|
13497
|
+
return value;
|
|
13498
|
+
}
|
|
13499
|
+
function renderMigrationBindings(options) {
|
|
13500
|
+
const manifest = parseMigrationBindingManifest(options.manifest);
|
|
13501
|
+
const target2 = validTarget(options.target);
|
|
13502
|
+
if (!manifest.targets.some((candidate) => candidate.environment === target2.environment && candidate.projectRef === target2.projectRef)) {
|
|
13503
|
+
throw new Error("Migration binding target is not registered in the manifest");
|
|
13504
|
+
}
|
|
13505
|
+
const sources = new Map;
|
|
13506
|
+
for (const migration of options.migrations) {
|
|
13507
|
+
if (!validFile(migration.file) || sources.has(migration.file) || typeof migration.sql !== "string" || !migration.sql.trim()) {
|
|
13508
|
+
throw new Error("Invalid, empty, or duplicate migration binding source");
|
|
13509
|
+
}
|
|
13510
|
+
sources.set(migration.file, migration);
|
|
13511
|
+
}
|
|
13512
|
+
if (manifest.templates.some((template) => !sources.has(template.file))) {
|
|
13513
|
+
throw new Error("Migration binding manifest references a missing source");
|
|
13514
|
+
}
|
|
13515
|
+
const templates = new Map(manifest.templates.map((template) => [template.file, template]));
|
|
13516
|
+
const declaredTokens = new Set(manifest.templates.flatMap((template) => template.parameters.map((parameter) => parameter.placeholder)));
|
|
13517
|
+
const files = [];
|
|
13518
|
+
const migrations = options.migrations.map((migration) => {
|
|
13519
|
+
const template = templates.get(migration.file);
|
|
13520
|
+
const templateSha256 = migrationBindingSha256(migration.sql);
|
|
13521
|
+
if (template && templateSha256 !== template.templateSha256) {
|
|
13522
|
+
throw new Error(`Migration binding template checksum mismatch: ${migration.file}`);
|
|
13523
|
+
}
|
|
13524
|
+
const parameters = new Map(template?.parameters.map((parameter) => [parameter.placeholder, parameter]));
|
|
13525
|
+
const counts = new Map;
|
|
13526
|
+
const sql = migration.sql.replace(TOKEN, (token, offset) => {
|
|
13527
|
+
const parameter = parameters.get(token);
|
|
13528
|
+
if (!parameter) {
|
|
13529
|
+
if (RESERVED_TOKEN.test(token) || declaredTokens.has(token)) {
|
|
13530
|
+
throw new Error(`Undeclared migration binding placeholder: ${migration.file}`);
|
|
13531
|
+
}
|
|
13532
|
+
return token;
|
|
13533
|
+
}
|
|
13534
|
+
if (migration.sql[offset - 1] !== "'" || migration.sql[offset + token.length] !== "'") {
|
|
13535
|
+
throw new Error(`Migration bindings must occupy a complete SQL string literal: ${migration.file}`);
|
|
13536
|
+
}
|
|
13537
|
+
counts.set(token, (counts.get(token) || 0) + 1);
|
|
13538
|
+
return bindingValue(parameter, options.values);
|
|
13539
|
+
});
|
|
13540
|
+
for (const parameter of parameters.values()) {
|
|
13541
|
+
if (counts.get(parameter.placeholder) !== parameter.occurrences) {
|
|
13542
|
+
throw new Error(`Migration binding occurrence mismatch: ${migration.file}`);
|
|
13543
|
+
}
|
|
13544
|
+
}
|
|
13545
|
+
files.push({
|
|
13546
|
+
file: migration.file,
|
|
13547
|
+
templateSha256,
|
|
13548
|
+
renderedSqlSha256: migrationBindingSha256(sql),
|
|
13549
|
+
parameters: [...parameters.values()].map(({ variable, type }) => ({ variable, type }))
|
|
13550
|
+
});
|
|
13551
|
+
return { file: migration.file, sql };
|
|
13552
|
+
});
|
|
13553
|
+
return {
|
|
13554
|
+
migrations,
|
|
13555
|
+
attestation: {
|
|
13556
|
+
schema: "supacloud.migration-binding-attestation.v1",
|
|
13557
|
+
...target2,
|
|
13558
|
+
manifestSha256: migrationBindingSha256(JSON.stringify(manifest)),
|
|
13559
|
+
files: files.sort((left, right) => left.file.localeCompare(right.file))
|
|
13560
|
+
}
|
|
13561
|
+
};
|
|
13562
|
+
}
|
|
13563
|
+
|
|
13564
|
+
// src/runtime/node/project.ts
|
|
12898
13565
|
async function loadSupabaseProject(projectDir, seed = {}) {
|
|
12899
13566
|
const migrationsDir = join6(projectDir, "supabase", "migrations");
|
|
12900
|
-
|
|
13567
|
+
let migrations = [];
|
|
13568
|
+
const sources = [];
|
|
12901
13569
|
let entries = [];
|
|
12902
13570
|
try {
|
|
12903
13571
|
entries = await readdir2(migrationsDir);
|
|
@@ -12906,10 +13574,50 @@ async function loadSupabaseProject(projectDir, seed = {}) {
|
|
|
12906
13574
|
throw error;
|
|
12907
13575
|
}
|
|
12908
13576
|
for (const entry of entries.sort()) {
|
|
12909
|
-
|
|
13577
|
+
const info = await lstat(join6(migrationsDir, entry));
|
|
13578
|
+
if (info.isSymbolicLink())
|
|
13579
|
+
throw new Error(`migration paths must not be symbolic links: ${entry}`);
|
|
13580
|
+
let file;
|
|
13581
|
+
let name;
|
|
13582
|
+
if (info.isFile() && entry.endsWith(".sql")) {
|
|
13583
|
+
file = entry;
|
|
13584
|
+
name = entry.replace(/\.sql$/, "");
|
|
13585
|
+
} else if (info.isDirectory() && /^\d/.test(entry)) {
|
|
13586
|
+
file = `${entry}/migration.sql`;
|
|
13587
|
+
name = entry;
|
|
13588
|
+
const sqlInfo = await lstat(join6(migrationsDir, file)).catch((error) => {
|
|
13589
|
+
if (isNotFound(error))
|
|
13590
|
+
throw new Error(`migration folder is missing migration.sql: ${entry}`);
|
|
13591
|
+
throw error;
|
|
13592
|
+
});
|
|
13593
|
+
if (!sqlInfo.isFile() || sqlInfo.isSymbolicLink())
|
|
13594
|
+
throw new Error(`invalid migration file: ${file}`);
|
|
13595
|
+
} else
|
|
12910
13596
|
continue;
|
|
12911
|
-
const sql = await readFile5(join6(migrationsDir,
|
|
12912
|
-
|
|
13597
|
+
const sql = await readFile5(join6(migrationsDir, file), "utf8");
|
|
13598
|
+
sources.push({ file, sql });
|
|
13599
|
+
migrations.push({ name, sql });
|
|
13600
|
+
}
|
|
13601
|
+
const versions = new Set;
|
|
13602
|
+
for (const migration of migrations) {
|
|
13603
|
+
const version = migration.name.match(/^(\d+)/)?.[1] ?? migration.name;
|
|
13604
|
+
if (versions.has(version))
|
|
13605
|
+
throw new Error(`duplicate migration version: ${version}`);
|
|
13606
|
+
versions.add(version);
|
|
13607
|
+
}
|
|
13608
|
+
const rendered = seed.bindings ? renderMigrationBindings({
|
|
13609
|
+
...seed.bindings,
|
|
13610
|
+
migrations: sources
|
|
13611
|
+
}) : undefined;
|
|
13612
|
+
if (rendered)
|
|
13613
|
+
migrations = migrations.map((migration, index) => {
|
|
13614
|
+
const bound = rendered.migrations[index];
|
|
13615
|
+
if (!bound)
|
|
13616
|
+
throw new Error(`missing rendered migration: ${migration.name}`);
|
|
13617
|
+
return { ...migration, sql: bound.sql };
|
|
13618
|
+
});
|
|
13619
|
+
else if (sources.some(({ sql }) => /__SC_BINDING_[A-Z0-9_]+__/.test(sql))) {
|
|
13620
|
+
throw new Error("migration binding placeholders require an explicit manifest and target");
|
|
12913
13621
|
}
|
|
12914
13622
|
let seedSql;
|
|
12915
13623
|
if (seed.enabled !== false) {
|
|
@@ -12931,7 +13639,7 @@ async function loadSupabaseProject(projectDir, seed = {}) {
|
|
|
12931
13639
|
seedSql = parts.join(`
|
|
12932
13640
|
`);
|
|
12933
13641
|
}
|
|
12934
|
-
return { migrations, seedSql };
|
|
13642
|
+
return { migrations, seedSql, ...rendered ? { bindingAttestation: rendered.attestation } : {} };
|
|
12935
13643
|
}
|
|
12936
13644
|
function isNotFound(error) {
|
|
12937
13645
|
return error.code === "ENOENT";
|
|
@@ -13003,12 +13711,15 @@ async function createProjectBackend(options = {}) {
|
|
|
13003
13711
|
const host = options.host ?? process.env.SUPACLOUD_LITE_HOST ?? "127.0.0.1";
|
|
13004
13712
|
const port = options.port ?? parsePort(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 54321);
|
|
13005
13713
|
const url = options.apiUrl ?? process.env.SUPACLOUD_LITE_API_URL ?? `http://${displayHost(host)}:${port}`;
|
|
13006
|
-
const config = loadProjectConfig(paths.projectDir);
|
|
13714
|
+
const config = loadProjectConfig(paths.projectDir, process.env, options.runtimeMode);
|
|
13715
|
+
const bindings = options.migrationBindings ?? await loadProjectBindings(paths.projectDir, config);
|
|
13007
13716
|
const project = await loadSupabaseProject(paths.projectDir, {
|
|
13008
13717
|
enabled: options.includeSeed === false ? false : config.seed.enabled,
|
|
13009
|
-
paths: config.seed.paths
|
|
13718
|
+
paths: config.seed.paths,
|
|
13719
|
+
bindings
|
|
13010
13720
|
});
|
|
13011
|
-
const
|
|
13721
|
+
const externalIdentity = options.externalIdentity ?? (options.includeIdentity !== false ? await loadProjectIdentity(paths.projectDir, options.identityModule ?? config.lite.identityModule) : undefined);
|
|
13722
|
+
const functions = options.includeFunctions === false ? new Map : await loadFunctions2(paths.projectDir, config.functions, config.lite.runtimeMode);
|
|
13012
13723
|
const functionEnv = options.includeFunctions === false ? {} : await loadFunctionEnv(paths.projectDir);
|
|
13013
13724
|
const secrets = await ensureProjectSecrets(paths);
|
|
13014
13725
|
const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
|
|
@@ -13023,11 +13734,20 @@ async function createProjectBackend(options = {}) {
|
|
|
13023
13734
|
await mkdir4(paths.storageDir, { recursive: true, mode: 448 });
|
|
13024
13735
|
await chmod(paths.storageDir, 448);
|
|
13025
13736
|
const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
|
|
13026
|
-
|
|
13737
|
+
let engine;
|
|
13738
|
+
if (databaseEngine === "native") {
|
|
13739
|
+
const dataDir = paths.dataDir;
|
|
13740
|
+
if (dataDir === undefined)
|
|
13741
|
+
throw new Error("native PostgreSQL requires a data directory");
|
|
13742
|
+
engine = await createNativeEngine({ dataDir, installDir: options.postgresDir, log: options.log, replication });
|
|
13743
|
+
}
|
|
13027
13744
|
let backend;
|
|
13028
13745
|
try {
|
|
13029
13746
|
backend = await createBackend({
|
|
13030
13747
|
engine,
|
|
13748
|
+
runtimeMode: config.lite.runtimeMode,
|
|
13749
|
+
graphql: options.graphql ?? config.lite.graphql,
|
|
13750
|
+
externalIdentity,
|
|
13031
13751
|
dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
13032
13752
|
jwtSecret: secrets.jwtSecret,
|
|
13033
13753
|
vaultKey: secrets.vaultKey,
|
|
@@ -13077,6 +13797,7 @@ async function createProjectBackend(options = {}) {
|
|
|
13077
13797
|
throw error;
|
|
13078
13798
|
}
|
|
13079
13799
|
return {
|
|
13800
|
+
bindingAttestation: project.bindingAttestation,
|
|
13080
13801
|
backend,
|
|
13081
13802
|
config,
|
|
13082
13803
|
paths,
|
|
@@ -13091,6 +13812,30 @@ async function createProjectBackend(options = {}) {
|
|
|
13091
13812
|
replicationProfile: replication?.profile
|
|
13092
13813
|
};
|
|
13093
13814
|
}
|
|
13815
|
+
async function loadProjectBindings(projectDir, config) {
|
|
13816
|
+
const bindings = config.lite.migrationBindings;
|
|
13817
|
+
if (!bindings)
|
|
13818
|
+
return;
|
|
13819
|
+
const manifest = JSON.parse(await readFile6(resolve3(projectDir, bindings.manifest), "utf8"));
|
|
13820
|
+
return {
|
|
13821
|
+
manifest,
|
|
13822
|
+
target: { environment: bindings.environment, projectRef: bindings.projectRef },
|
|
13823
|
+
values: process.env
|
|
13824
|
+
};
|
|
13825
|
+
}
|
|
13826
|
+
async function loadProjectIdentity(projectDir, module) {
|
|
13827
|
+
if (!module)
|
|
13828
|
+
return;
|
|
13829
|
+
const imported = await import(pathToFileURL2(resolve3(projectDir, module)).href);
|
|
13830
|
+
if (!isRecord(imported) || typeof imported["default"] !== "function") {
|
|
13831
|
+
throw new Error("Lite identity module must default-export an external identity verifier");
|
|
13832
|
+
}
|
|
13833
|
+
const verify = imported["default"];
|
|
13834
|
+
return async (request) => {
|
|
13835
|
+
const claims = await verify(request);
|
|
13836
|
+
return validateExternalIdentityClaims(claims);
|
|
13837
|
+
};
|
|
13838
|
+
}
|
|
13094
13839
|
function resolveNativeReplicationOptions(options, databaseEngine = resolveDatabaseEngine(options.engine, options.memory)) {
|
|
13095
13840
|
const profile = options.replicationProfile ?? process.env.SUPACLOUD_LITE_REPLICATION_PROFILE;
|
|
13096
13841
|
if (!profile)
|
|
@@ -13122,7 +13867,7 @@ function resolveDatabaseEngine(value, memory = false) {
|
|
|
13122
13867
|
if (configured === "native" && memory)
|
|
13123
13868
|
throw new Error("--memory is only supported by the pglite engine");
|
|
13124
13869
|
if (configured === "native" && !isNativeEngineSupported()) {
|
|
13125
|
-
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64;
|
|
13870
|
+
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ${process.platform}/${process.arch} must use --engine pglite`);
|
|
13126
13871
|
}
|
|
13127
13872
|
return configured;
|
|
13128
13873
|
}
|
|
@@ -13233,7 +13978,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
13233
13978
|
return port;
|
|
13234
13979
|
}
|
|
13235
13980
|
// src/snapshot.ts
|
|
13236
|
-
import { chmod as chmod2, copyFile, lstat as
|
|
13981
|
+
import { chmod as chmod2, copyFile, lstat as lstat3, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
|
|
13237
13982
|
import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve4, sep as sep2 } from "path";
|
|
13238
13983
|
import { create as createTar, extract as extractTar2 } from "tar";
|
|
13239
13984
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
@@ -13322,7 +14067,10 @@ async function restoreSnapshot(options) {
|
|
|
13322
14067
|
if (manifest.storageBackend !== options.storageBackend) {
|
|
13323
14068
|
throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
|
|
13324
14069
|
}
|
|
13325
|
-
assertDatabaseSnapshotCompatible(manifest, paths);
|
|
14070
|
+
assertDatabaseSnapshotCompatible(manifest, paths, options.postgresDir);
|
|
14071
|
+
if (manifest.databaseEngine === "native" && await readPostgresMajor(join8(payloadRoot, "database")) !== manifest.postgresMajor) {
|
|
14072
|
+
throw new Error("native PostgreSQL snapshot payload does not match its declared major version");
|
|
14073
|
+
}
|
|
13326
14074
|
if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
|
|
13327
14075
|
throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
|
|
13328
14076
|
}
|
|
@@ -13389,7 +14137,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
13389
14137
|
throw new Error("snapshot state directory must not be the filesystem root");
|
|
13390
14138
|
if (paths.secretsFile !== join8(paths.stateDir, "secrets.json"))
|
|
13391
14139
|
throw new Error("snapshot secrets path must be inside the state directory");
|
|
13392
|
-
const stateInfo = await
|
|
14140
|
+
const stateInfo = await lstat3(paths.stateDir);
|
|
13393
14141
|
if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
|
|
13394
14142
|
throw new Error(`state directory must be a real directory: ${paths.stateDir}`);
|
|
13395
14143
|
} catch (error) {
|
|
@@ -13402,7 +14150,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
13402
14150
|
await assertDirectoryOrMissing(paths.dataDir);
|
|
13403
14151
|
await assertDirectoryOrMissing(paths.storageDir);
|
|
13404
14152
|
if (options.requireSecrets !== false) {
|
|
13405
|
-
const secretInfo = await
|
|
14153
|
+
const secretInfo = await lstat3(paths.secretsFile);
|
|
13406
14154
|
if (!secretInfo.isFile() || secretInfo.isSymbolicLink())
|
|
13407
14155
|
throw new Error(`secrets file must be a regular file: ${paths.secretsFile}`);
|
|
13408
14156
|
}
|
|
@@ -13413,7 +14161,7 @@ async function assertDirectoryOrMissing(path) {
|
|
|
13413
14161
|
if (resolve4(path) === parse2(resolve4(path)).root)
|
|
13414
14162
|
throw new Error(`snapshot path must not be the filesystem root: ${path}`);
|
|
13415
14163
|
try {
|
|
13416
|
-
const info = await
|
|
14164
|
+
const info = await lstat3(path);
|
|
13417
14165
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
13418
14166
|
throw new Error(`snapshot path must be a real directory: ${path}`);
|
|
13419
14167
|
} catch (error) {
|
|
@@ -13434,7 +14182,7 @@ async function assertNoDataDirectoryLock(paths) {
|
|
|
13434
14182
|
}
|
|
13435
14183
|
async function stageDirectory(root, destination) {
|
|
13436
14184
|
try {
|
|
13437
|
-
const info = await
|
|
14185
|
+
const info = await lstat3(root);
|
|
13438
14186
|
if (!info.isDirectory() || info.isSymbolicLink())
|
|
13439
14187
|
throw new Error(`snapshot path must be a real directory: ${root}`);
|
|
13440
14188
|
} catch (error) {
|
|
@@ -13484,7 +14232,7 @@ function isSnapshotManifest(value) {
|
|
|
13484
14232
|
const candidate = value;
|
|
13485
14233
|
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");
|
|
13486
14234
|
}
|
|
13487
|
-
function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
14235
|
+
function assertDatabaseSnapshotCompatible(manifest, paths, postgresDir) {
|
|
13488
14236
|
const sourceEngine = manifest.databaseEngine ?? "pglite";
|
|
13489
14237
|
if (sourceEngine !== paths.databaseEngine) {
|
|
13490
14238
|
throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
|
|
@@ -13494,8 +14242,9 @@ function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
|
13494
14242
|
if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
|
|
13495
14243
|
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}`);
|
|
13496
14244
|
}
|
|
13497
|
-
|
|
13498
|
-
|
|
14245
|
+
const expectedMajor = nativePostgresMajor(postgresDir);
|
|
14246
|
+
if (manifest.postgresMajor !== expectedMajor) {
|
|
14247
|
+
throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but the selected PostgreSQL installation uses ${expectedMajor}`);
|
|
13499
14248
|
}
|
|
13500
14249
|
}
|
|
13501
14250
|
async function readPostgresMajor(dataDir) {
|
|
@@ -13513,7 +14262,7 @@ async function assertSnapshotPayload(payloadRoot, manifest) {
|
|
|
13513
14262
|
const required = ["manifest.json", "secrets.json"];
|
|
13514
14263
|
for (const path of required) {
|
|
13515
14264
|
try {
|
|
13516
|
-
await
|
|
14265
|
+
await lstat3(join8(payloadRoot, path));
|
|
13517
14266
|
} catch {
|
|
13518
14267
|
throw new Error(`snapshot is missing required payload: ${path}`);
|
|
13519
14268
|
}
|
|
@@ -13585,7 +14334,7 @@ async function rollbackDirectorySwaps(swaps) {
|
|
|
13585
14334
|
}
|
|
13586
14335
|
async function existingInfo(path) {
|
|
13587
14336
|
try {
|
|
13588
|
-
return await
|
|
14337
|
+
return await lstat3(path);
|
|
13589
14338
|
} catch (error) {
|
|
13590
14339
|
if (error.code === "ENOENT")
|
|
13591
14340
|
return null;
|
|
@@ -13593,7 +14342,7 @@ async function existingInfo(path) {
|
|
|
13593
14342
|
}
|
|
13594
14343
|
}
|
|
13595
14344
|
async function copyEntry(source, target2) {
|
|
13596
|
-
const info = await
|
|
14345
|
+
const info = await lstat3(source);
|
|
13597
14346
|
if (info.isSymbolicLink())
|
|
13598
14347
|
throw new Error(`snapshot refuses symbolic link: ${source}`);
|
|
13599
14348
|
if (info.isDirectory()) {
|
|
@@ -13607,7 +14356,7 @@ async function copyEntry(source, target2) {
|
|
|
13607
14356
|
throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
|
|
13608
14357
|
}
|
|
13609
14358
|
async function hardenRestoredTree(root) {
|
|
13610
|
-
const info = await
|
|
14359
|
+
const info = await lstat3(root);
|
|
13611
14360
|
if (info.isSymbolicLink())
|
|
13612
14361
|
throw new Error(`snapshot refuses symbolic link: ${root}`);
|
|
13613
14362
|
if (info.isDirectory()) {
|
|
@@ -13654,12 +14403,14 @@ export {
|
|
|
13654
14403
|
createPgliteEngine,
|
|
13655
14404
|
createProjectBackend,
|
|
13656
14405
|
createSnapshot,
|
|
14406
|
+
createSupAuthLiteIdentity,
|
|
13657
14407
|
decodeJwt,
|
|
13658
14408
|
ensurePostgres,
|
|
13659
14409
|
ensurePowerSyncReplicationCatalog,
|
|
13660
14410
|
ensureProjectSecrets,
|
|
13661
14411
|
generateTypes,
|
|
13662
14412
|
inspectDb,
|
|
14413
|
+
inspectGraphql,
|
|
13663
14414
|
inspectPowerSyncReadiness,
|
|
13664
14415
|
isNativeEngineSupported,
|
|
13665
14416
|
liteCapabilities,
|