@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +33 -4
  3. package/dist/cli.js +1028 -264
  4. package/dist/index.d.ts +3 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +850 -99
  7. package/dist/project-runtime.d.ts +14 -0
  8. package/dist/project-runtime.d.ts.map +1 -1
  9. package/dist/runtime/auth/handler.d.ts.map +1 -1
  10. package/dist/runtime/auth/inbox.d.ts.map +1 -1
  11. package/dist/runtime/auth/oauth.d.ts.map +1 -1
  12. package/dist/runtime/auth/rate-limit.d.ts.map +1 -1
  13. package/dist/runtime/auth/sms-inbox.d.ts.map +1 -1
  14. package/dist/runtime/cron/service.d.ts.map +1 -1
  15. package/dist/runtime/db/bootstrap.d.ts +1 -1
  16. package/dist/runtime/db/bootstrap.d.ts.map +1 -1
  17. package/dist/runtime/db/database.d.ts +4 -0
  18. package/dist/runtime/db/database.d.ts.map +1 -1
  19. package/dist/runtime/db/engine.d.ts.map +1 -1
  20. package/dist/runtime/db/sql-compat.d.ts +1 -1
  21. package/dist/runtime/db/sql-compat.d.ts.map +1 -1
  22. package/dist/runtime/functions/handler.d.ts +3 -1
  23. package/dist/runtime/functions/handler.d.ts.map +1 -1
  24. package/dist/runtime/functions/pgredis.d.ts.map +1 -1
  25. package/dist/runtime/functions/profile.d.ts +5 -0
  26. package/dist/runtime/functions/profile.d.ts.map +1 -0
  27. package/dist/runtime/graphql.d.ts +31 -0
  28. package/dist/runtime/graphql.d.ts.map +1 -0
  29. package/dist/runtime/identity.d.ts +27 -0
  30. package/dist/runtime/identity.d.ts.map +1 -0
  31. package/dist/runtime/index.d.ts +2 -0
  32. package/dist/runtime/index.d.ts.map +1 -1
  33. package/dist/runtime/log-buffer.d.ts.map +1 -1
  34. package/dist/runtime/net/service.d.ts.map +1 -1
  35. package/dist/runtime/node/bundle-function.d.ts.map +1 -1
  36. package/dist/runtime/node/fs-driver.d.ts.map +1 -1
  37. package/dist/runtime/node/load-config.d.ts +13 -1
  38. package/dist/runtime/node/load-config.d.ts.map +1 -1
  39. package/dist/runtime/node/load-functions.d.ts +2 -1
  40. package/dist/runtime/node/load-functions.d.ts.map +1 -1
  41. package/dist/runtime/node/native/engine.d.ts +3 -0
  42. package/dist/runtime/node/native/engine.d.ts.map +1 -1
  43. package/dist/runtime/node/native/readiness.d.ts +14 -0
  44. package/dist/runtime/node/native/readiness.d.ts.map +1 -1
  45. package/dist/runtime/node/native/wire.d.ts.map +1 -1
  46. package/dist/runtime/node/project.d.ts +7 -0
  47. package/dist/runtime/node/project.d.ts.map +1 -1
  48. package/dist/runtime/realtime/engine.d.ts +4 -1
  49. package/dist/runtime/realtime/engine.d.ts.map +1 -1
  50. package/dist/runtime/rest/build.d.ts.map +1 -1
  51. package/dist/runtime/rest/handler.d.ts.map +1 -1
  52. package/dist/runtime/retention/service.d.ts.map +1 -1
  53. package/dist/runtime/storage/driver.d.ts.map +1 -1
  54. package/dist/runtime/storage/handler.d.ts.map +1 -1
  55. package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -1
  56. package/dist/runtime/storage/s3-driver.d.ts +1 -1
  57. package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
  58. package/dist/runtime/types.d.ts +4 -0
  59. package/dist/runtime/types.d.ts.map +1 -1
  60. package/dist/runtime/validation.d.ts +4 -0
  61. package/dist/runtime/validation.d.ts.map +1 -0
  62. package/dist/runtime/webhooks/service.d.ts.map +1 -1
  63. package/dist/snapshot.d.ts +1 -0
  64. package/dist/snapshot.d.ts.map +1 -1
  65. 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.13.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.2.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
- constructor(functions, env, pgredis) {
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
- grant usage on schema public to anon, authenticated, service_role;
3232
- grant all on all tables in schema public to anon, authenticated, service_role;
3233
- grant all on all sequences in schema public to anon, authenticated, service_role;
3234
-
3235
- alter default privileges in schema public
3236
- grant all on tables to anon, authenticated, service_role;
3237
- alter default privileges in schema public
3238
- grant all on sequences to anon, authenticated, service_role;
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
- const seen = await this.engine.query(`select 1 from supabase_migrations.schema_migrations where version = $1`, [version]);
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 ctx = await this.contextFromToken(msg.payload?.access_token);
6584
- const channel = conn.channels.get(msg.topic);
6585
- if (channel)
6586
- channel.ctx = ctx;
6587
- else
6588
- for (const c of conn.channels.values())
6589
- c.ctx = ctx;
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 ctx = await this.contextFromToken(msg.payload?.access_token);
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
- const claims = await verifyJwt(token, jwtSecret);
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) => db.runMigrations(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
- await client.open(opts);
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 response = await callback(transactionClient);
12086
+ const response2 = await callback(transactionClient);
11682
12087
  await queryClient.exec("commit");
11683
- return response;
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 response = await fetch(url);
11905
- if (response.ok || response.status < 500)
11906
- return response;
11907
- lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
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
- const buildOutput = await Bun.build({
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
- const migrations = [];
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
- if (!entry.endsWith(".sql"))
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, entry), "utf8");
12912
- migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
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 functions = options.includeFunctions === false ? new Map : await loadFunctions2(paths.projectDir, config.functions);
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
- const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log, replication }) : undefined;
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; ` + `${process.platform}/${process.arch} must use --engine pglite`);
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 lstat2, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
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 lstat2(paths.stateDir);
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 lstat2(paths.secretsFile);
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 lstat2(path);
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 lstat2(root);
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
- if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
13498
- throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
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 lstat2(join8(payloadRoot, path));
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 lstat2(path);
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 lstat2(source);
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 lstat2(root);
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,