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