@supacloud/lite 0.7.3 → 0.8.1

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 (40) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/LICENSES/POSTGRESQL-17-COPYRIGHT.txt +23 -0
  3. package/LICENSES/THESEUS-POSTGRESQL-LICENSE.txt +7 -0
  4. package/THIRD_PARTY_NOTICES.md +11 -1
  5. package/dist/cli.js +2674 -375
  6. package/dist/index.d.ts +3 -2
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +2537 -305
  9. package/dist/project-runtime.d.ts +5 -0
  10. package/dist/project-runtime.d.ts.map +1 -1
  11. package/dist/runtime/auth/handler.d.ts +5 -2
  12. package/dist/runtime/auth/handler.d.ts.map +1 -1
  13. package/dist/runtime/db/data-dir-lock.d.ts +2 -0
  14. package/dist/runtime/db/data-dir-lock.d.ts.map +1 -1
  15. package/dist/runtime/db/database.d.ts.map +1 -1
  16. package/dist/runtime/db/emulated.d.ts +2 -1
  17. package/dist/runtime/db/emulated.d.ts.map +1 -1
  18. package/dist/runtime/db/pglite-engine.d.ts.map +1 -1
  19. package/dist/runtime/node/fs-driver.d.ts +2 -0
  20. package/dist/runtime/node/fs-driver.d.ts.map +1 -1
  21. package/dist/runtime/node/native/engine.d.ts +23 -0
  22. package/dist/runtime/node/native/engine.d.ts.map +1 -0
  23. package/dist/runtime/node/native/wire-engine.d.ts +9 -0
  24. package/dist/runtime/node/native/wire-engine.d.ts.map +1 -0
  25. package/dist/runtime/node/native/wire.d.ts +56 -0
  26. package/dist/runtime/node/native/wire.d.ts.map +1 -0
  27. package/dist/runtime/rest/handler.d.ts.map +1 -1
  28. package/dist/runtime/storage/handler.d.ts +5 -0
  29. package/dist/runtime/storage/handler.d.ts.map +1 -1
  30. package/dist/runtime/storage/image-transform-cache.d.ts +13 -0
  31. package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -0
  32. package/dist/runtime/storage/image-transform.d.ts +1 -1
  33. package/dist/runtime/storage/image-transform.d.ts.map +1 -1
  34. package/dist/runtime/storage/s3-driver.d.ts +2 -0
  35. package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
  36. package/dist/runtime/types.d.ts +2 -0
  37. package/dist/runtime/types.d.ts.map +1 -1
  38. package/dist/snapshot.d.ts +5 -1
  39. package/dist/snapshot.d.ts.map +1 -1
  40. package/package.json +6 -2
package/dist/cli.js CHANGED
@@ -3,13 +3,13 @@
3
3
  var __require = import.meta.require;
4
4
 
5
5
  // src/cli.ts
6
- import { mkdir as mkdir7, rm as rm5, writeFile as writeFile6 } from "fs/promises";
7
- import { dirname as dirname6, join as join9, resolve as resolve4 } from "path";
6
+ import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
7
+ import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.7.3",
12
- description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
11
+ version: "0.8.1",
12
+ description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
15
15
  bin: {
@@ -45,6 +45,10 @@ var package_default = {
45
45
  dev: "bun run src/cli.ts start",
46
46
  start: "bun run src/cli.ts start",
47
47
  test: "bun test --timeout 20000",
48
+ "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts --timeout 180000",
49
+ parity: "bun run parity/harness.ts",
50
+ "parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
51
+ "check:native": "bun run test:native && bun run parity:native",
48
52
  "test:package": "bun run scripts/package-smoke.ts",
49
53
  "test:standalone": "bun run scripts/standalone-smoke.ts",
50
54
  prepack: "bun run build",
@@ -1292,7 +1296,8 @@ class AuthHandler {
1292
1296
  return await this.oauth.authorize(url);
1293
1297
  }
1294
1298
  if (path === "callback" && (method === "GET" || method === "POST")) {
1295
- return await this.oauth.callback(url, (userId) => this.sessionTokensFor(userId));
1299
+ const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
1300
+ return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
1296
1301
  }
1297
1302
  if (path.startsWith("admin/"))
1298
1303
  return await this.admin(req, ctx, path, method);
@@ -1302,6 +1307,16 @@ class AuthHandler {
1302
1307
  return authError(500, "unexpected_failure", msg);
1303
1308
  }
1304
1309
  }
1310
+ async oauthCallbackUrl(req, url) {
1311
+ const callbackUrl = new URL(url);
1312
+ const form = await req.formData();
1313
+ for (const field of ["code", "state"]) {
1314
+ const value = form.get(field);
1315
+ if (typeof value === "string" && value)
1316
+ callbackUrl.searchParams.set(field, value);
1317
+ }
1318
+ return callbackUrl;
1319
+ }
1305
1320
  async signup(req) {
1306
1321
  const body = await req.json().catch(() => ({}));
1307
1322
  if (!body.email && !body.password) {
@@ -1379,7 +1394,7 @@ class AuthHandler {
1379
1394
  if (!userId)
1380
1395
  return authError(403, "flow_state_not_found", "invalid or expired auth code");
1381
1396
  const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
1382
- return json(200, await this.sessionFor(ures.rows[0]));
1397
+ return json(200, await this.sessionForOAuth(ures.rows[0]));
1383
1398
  }
1384
1399
  return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
1385
1400
  }
@@ -1424,17 +1439,18 @@ class AuthHandler {
1424
1439
  sets.push(`is_anonymous = false`);
1425
1440
  sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
1426
1441
  }
1427
- if (sets.length === 0)
1428
- return json(200, this.userJson(user));
1429
- params.push(user.id);
1430
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1431
- const updated = res.rows[0];
1432
- if (upgradingAnon) {
1433
- await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
1434
- values ($1, 'email', $2, $3)
1435
- on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
1442
+ if (sets.length === 0) {
1443
+ return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
1436
1444
  }
1437
- return json(200, this.userJson(updated));
1445
+ params.push(user.id);
1446
+ const payload = await this.db.transaction(async (query) => {
1447
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1448
+ const updated = res.rows[0];
1449
+ if (body.email)
1450
+ await this.ensureEmailIdentity(updated, query);
1451
+ return this.userJsonWithRelations(updated, query);
1452
+ });
1453
+ return json(200, payload);
1438
1454
  }
1439
1455
  async logout(req, url) {
1440
1456
  const claims = await this.claimsFromBearer(req);
@@ -1464,7 +1480,7 @@ class AuthHandler {
1464
1480
  let user = res.rows[0];
1465
1481
  if (!user) {
1466
1482
  if (!createUser)
1467
- return authError(422, "otp_disabled", "Signups not allowed for otp");
1483
+ return json(200, {});
1468
1484
  if (this.settings.disableSignup)
1469
1485
  return authError(422, "signup_disabled", "Signups not allowed for this instance");
1470
1486
  res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
@@ -1796,6 +1812,8 @@ Or sign in with this link: ${link}`
1796
1812
  }
1797
1813
  const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
1798
1814
  const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
1815
+ const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
1816
+ const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
1799
1817
  if (path === "admin/generate_link" && method === "POST") {
1800
1818
  return await this.generateAdminMagicLink(req);
1801
1819
  }
@@ -1807,32 +1825,49 @@ Or sign in with this link: ${link}`
1807
1825
  if (exportMatch && method === "GET") {
1808
1826
  return await this.exportUser(exportMatch[1]);
1809
1827
  }
1828
+ if (factorsMatch && method === "GET") {
1829
+ const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
1830
+ if (user.rows.length === 0)
1831
+ return authError(404, "user_not_found", "User not found");
1832
+ return json(200, await this.getUserFactors(factorsMatch[1]));
1833
+ }
1834
+ if (factorMatch && method === "DELETE") {
1835
+ const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
1836
+ if (deleted.rows.length === 0)
1837
+ return authError(404, "mfa_factor_not_found", "MFA factor not found");
1838
+ return json(200, { id: factorMatch[2] });
1839
+ }
1810
1840
  if (path === "admin/users" && method === "GET") {
1811
1841
  const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
1812
- return json(200, { users: res.rows.map((u) => this.userJson(u)), aud: "authenticated" });
1842
+ return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
1813
1843
  }
1814
1844
  if (path === "admin/users" && method === "POST") {
1815
1845
  const body = await req.json().catch(() => ({}));
1816
1846
  if (!body.email)
1817
1847
  return authError(400, "validation_failed", "email is required");
1818
1848
  const hashed = body.password ? await hashPassword(body.password) : null;
1819
- const res = await this.db.query(`insert into auth.users
1820
- (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1821
- values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1822
- returning *`, [
1823
- body.email.toLowerCase().trim(),
1824
- hashed,
1825
- body.email_confirm ?? true,
1826
- JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1827
- JSON.stringify(body.user_metadata ?? {})
1828
- ]);
1829
- return json(200, this.userJson(res.rows[0]));
1849
+ const created = await this.db.transaction(async (query) => {
1850
+ const res = await query(`insert into auth.users
1851
+ (aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
1852
+ values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
1853
+ returning *`, [
1854
+ body.email.toLowerCase().trim(),
1855
+ hashed,
1856
+ body.email_confirm ?? true,
1857
+ JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
1858
+ JSON.stringify(body.user_metadata ?? {})
1859
+ ]);
1860
+ const user = res.rows[0];
1861
+ await this.ensureEmailIdentity(user, query);
1862
+ return this.userJsonWithRelations(user, query);
1863
+ });
1864
+ return json(200, created);
1830
1865
  }
1831
1866
  if (idMatch && method === "GET") {
1832
1867
  const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
1833
1868
  if (res.rows.length === 0)
1834
1869
  return authError(404, "user_not_found", "User not found");
1835
- return json(200, this.userJson(res.rows[0]));
1870
+ return json(200, await this.userJsonWithRelations(res.rows[0]));
1836
1871
  }
1837
1872
  if (idMatch && method === "PUT") {
1838
1873
  const body = await req.json().catch(() => ({}));
@@ -1859,10 +1894,18 @@ Or sign in with this link: ${link}`
1859
1894
  if (sets.length === 0)
1860
1895
  return authError(400, "validation_failed", "nothing to update");
1861
1896
  params.push(idMatch[1]);
1862
- const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1863
- if (res.rows.length === 0)
1897
+ const updated = await this.db.transaction(async (query) => {
1898
+ const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
1899
+ const user = res.rows[0];
1900
+ if (!user)
1901
+ return null;
1902
+ if (typeof body.email === "string")
1903
+ await this.ensureEmailIdentity(user, query);
1904
+ return this.userJsonWithRelations(user, query);
1905
+ });
1906
+ if (!updated)
1864
1907
  return authError(404, "user_not_found", "User not found");
1865
- return json(200, this.userJson(res.rows[0]));
1908
+ return json(200, updated);
1866
1909
  }
1867
1910
  if (idMatch && method === "DELETE") {
1868
1911
  return await this.eraseUser(idMatch[1]);
@@ -2058,17 +2101,26 @@ Or sign in with this link: ${link}`
2058
2101
  if (!await verifyTotp(factor.secret, body.code ?? "")) {
2059
2102
  return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
2060
2103
  }
2061
- await this.db.query(`update auth.mfa_challenges set verified_at = now() where id = $1`, [challenge.id]);
2062
- if (factor.status !== "verified") {
2063
- await this.db.query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2064
- }
2065
- const session = await this.sessionFor(user, undefined, {
2066
- aal: "aal2",
2067
- amr: [
2068
- { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2069
- { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2070
- ]
2104
+ const session = await this.db.transaction(async (query) => {
2105
+ const claimed = await query(`update auth.mfa_challenges set verified_at = now()
2106
+ where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
2107
+ returning id`, [challenge.id, factorId]);
2108
+ if (claimed.rows.length === 0)
2109
+ return null;
2110
+ if (factor.status !== "verified") {
2111
+ await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
2112
+ }
2113
+ return this.sessionFor(user, undefined, {
2114
+ aal: "aal2",
2115
+ amr: [
2116
+ { method: "password", timestamp: Math.floor(Date.now() / 1000) },
2117
+ { method: "totp", timestamp: Math.floor(Date.now() / 1000) }
2118
+ ]
2119
+ }, query);
2071
2120
  });
2121
+ if (!session) {
2122
+ return authError(422, "mfa_verification_failed", "This challenge has already been verified");
2123
+ }
2072
2124
  return json(200, session);
2073
2125
  }
2074
2126
  async unenrollFactor(req, factorId) {
@@ -2106,6 +2158,17 @@ Or sign in with this link: ${link}`
2106
2158
  updated_at: iso(r.updated_at)
2107
2159
  }));
2108
2160
  }
2161
+ async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
2162
+ if (!user.email)
2163
+ return;
2164
+ await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
2165
+ values ($1::uuid, 'email', $1::text, $2::jsonb)
2166
+ on conflict (provider, provider_id) do update
2167
+ set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
2168
+ }
2169
+ async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
2170
+ return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
2171
+ }
2109
2172
  async rotateRefreshToken(token) {
2110
2173
  return this.db.transaction(async (query) => {
2111
2174
  const claimedToken = await this.claimRefreshToken(query, token);
@@ -2179,9 +2242,14 @@ Or sign in with this link: ${link}`
2179
2242
  is_anonymous: u.is_anonymous ?? false
2180
2243
  };
2181
2244
  }
2182
- async sessionTokensFor(userId) {
2245
+ sessionForOAuth(user) {
2246
+ return this.sessionFor(user, undefined, {
2247
+ amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
2248
+ });
2249
+ }
2250
+ async oauthSessionTokensFor(userId) {
2183
2251
  const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
2184
- const session = await this.sessionFor(res.rows[0]);
2252
+ const session = await this.sessionForOAuth(res.rows[0]);
2185
2253
  return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
2186
2254
  }
2187
2255
  async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
@@ -3545,41 +3613,1158 @@ $pgmq$;
3545
3613
  revoke all on all functions in schema pgmq from public, anon, authenticated;
3546
3614
  grant execute on all functions in schema pgmq to service_role;
3547
3615
 
3548
- create schema if not exists pgmq_public;
3549
- grant usage on schema pgmq_public to anon, authenticated, service_role;
3550
-
3551
- create or replace function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer default 0)
3552
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3553
- select * from pgmq.send(queue_name, message, sleep_seconds);
3554
- $pgmq_public$;
3555
-
3556
- create or replace function pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer default 0)
3557
- returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3558
- select * from pgmq.send_batch(queue_name, messages, sleep_seconds);
3559
- $pgmq_public$;
3560
-
3561
- create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3562
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3563
- select * from pgmq.read(queue_name, sleep_seconds, n);
3564
- $pgmq_public$;
3565
-
3566
- create or replace function pgmq_public.pop(queue_name text)
3567
- returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3568
- select * from pgmq.pop(queue_name);
3569
- $pgmq_public$;
3570
-
3571
- create or replace function pgmq_public.archive(queue_name text, message_id bigint)
3572
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3573
- select pgmq.archive(queue_name, message_id);
3574
- $pgmq_public$;
3575
-
3576
- create or replace function pgmq_public."delete"(queue_name text, message_id bigint)
3577
- returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
3578
- select pgmq.delete(queue_name, message_id);
3579
- $pgmq_public$;
3580
-
3581
- revoke all on all functions in schema pgmq_public from public;
3582
- grant execute on all functions in schema pgmq_public to anon, authenticated, service_role;
3616
+ -- supacloud:sql-module:pgmq-public:start
3617
+ DO $pgmq_extension$
3618
+ BEGIN
3619
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3620
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3621
+ END IF;
3622
+ END
3623
+ $pgmq_extension$;
3624
+
3625
+ CREATE SCHEMA IF NOT EXISTS pgmq_public;
3626
+ GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
3627
+
3628
+ CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
3629
+ RETURNS text
3630
+ LANGUAGE plpgsql
3631
+ IMMUTABLE
3632
+ SET search_path = ''
3633
+ AS $$
3634
+ DECLARE
3635
+ normalized_queue_name text := lower(btrim(queue_name));
3636
+ BEGIN
3637
+ IF normalized_queue_name IS NULL
3638
+ OR left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN
3639
+ RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';
3640
+ END IF;
3641
+ RETURN normalized_queue_name;
3642
+ END;
3643
+ $$;
3644
+
3645
+ CREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)
3646
+ RETURNS SETOF bigint
3647
+ LANGUAGE sql
3648
+ VOLATILE
3649
+ SECURITY DEFINER
3650
+ SET search_path = ''
3651
+ AS $$ SELECT * FROM pgmq.send(pgmq_public.require_public_queue(queue_name), message, sleep_seconds); $$;
3652
+
3653
+ CREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)
3654
+ RETURNS SETOF bigint
3655
+ LANGUAGE sql
3656
+ VOLATILE
3657
+ SECURITY DEFINER
3658
+ SET search_path = ''
3659
+ AS $$ SELECT * FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), messages, sleep_seconds); $$;
3660
+
3661
+ CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
3662
+ RETURNS SETOF pgmq.message_record
3663
+ LANGUAGE sql
3664
+ VOLATILE
3665
+ SECURITY DEFINER
3666
+ SET search_path = ''
3667
+ AS $$ SELECT * FROM pgmq.read(pgmq_public.require_public_queue(queue_name), sleep_seconds, n); $$;
3668
+
3669
+ CREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)
3670
+ RETURNS SETOF pgmq.message_record
3671
+ LANGUAGE sql
3672
+ VOLATILE
3673
+ SECURITY DEFINER
3674
+ SET search_path = ''
3675
+ AS $$ SELECT * FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)); $$;
3676
+
3677
+ CREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)
3678
+ RETURNS boolean
3679
+ LANGUAGE sql
3680
+ VOLATILE
3681
+ SECURITY DEFINER
3682
+ SET search_path = ''
3683
+ AS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), message_id); $$;
3684
+
3685
+ CREATE OR REPLACE FUNCTION pgmq_public."delete"(queue_name text, message_id bigint)
3686
+ RETURNS boolean
3687
+ LANGUAGE sql
3688
+ VOLATILE
3689
+ SECURITY DEFINER
3690
+ SET search_path = ''
3691
+ AS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), message_id); $$;
3692
+
3693
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;
3694
+ GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;
3695
+ -- supacloud:sql-module:pgmq-public:end
3696
+ `;
3697
+ var WORKFLOWS_SQL = `
3698
+ -- supacloud:sql-module:workflows-public:start
3699
+ DO $pgmq_extension$
3700
+ BEGIN
3701
+ IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
3702
+ EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
3703
+ END IF;
3704
+ END
3705
+ $pgmq_extension$;
3706
+ CREATE SCHEMA IF NOT EXISTS supacloud_workflows;
3707
+ REVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;
3708
+ GRANT USAGE ON SCHEMA supacloud_workflows TO service_role;
3709
+
3710
+ SELECT pgmq.create('supacloud_internal_workflows');
3711
+
3712
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.runs (
3713
+ id uuid PRIMARY KEY,
3714
+ workflow_name text NOT NULL
3715
+ CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3716
+ workflow_version text NOT NULL
3717
+ CHECK (char_length(workflow_version) BETWEEN 1 AND 80),
3718
+ status text NOT NULL DEFAULT 'queued'
3719
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
3720
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3721
+ CHECK (jsonb_typeof(input) = 'object'),
3722
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3723
+ CHECK (jsonb_typeof(output) = 'object'),
3724
+ error_message text NOT NULL DEFAULT ''
3725
+ CHECK (char_length(error_message) <= 4000),
3726
+ row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),
3727
+ created_at timestamptz NOT NULL DEFAULT now(),
3728
+ started_at timestamptz,
3729
+ completed_at timestamptz,
3730
+ updated_at timestamptz NOT NULL DEFAULT now()
3731
+ );
3732
+
3733
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.steps (
3734
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
3735
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3736
+ step_key text NOT NULL
3737
+ CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
3738
+ status text NOT NULL DEFAULT 'queued'
3739
+ CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),
3740
+ input jsonb NOT NULL DEFAULT '{}'::jsonb
3741
+ CHECK (jsonb_typeof(input) = 'object'),
3742
+ output jsonb NOT NULL DEFAULT '{}'::jsonb
3743
+ CHECK (jsonb_typeof(output) = 'object'),
3744
+ error_message text NOT NULL DEFAULT ''
3745
+ CHECK (char_length(error_message) <= 4000),
3746
+ attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
3747
+ max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),
3748
+ retry_delay_seconds integer NOT NULL DEFAULT 0
3749
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400),
3750
+ queue_message_id bigint NOT NULL UNIQUE,
3751
+ claimed_by text,
3752
+ claimed_at timestamptz,
3753
+ completed_at timestamptz,
3754
+ next_step_key text,
3755
+ created_at timestamptz NOT NULL DEFAULT now(),
3756
+ updated_at timestamptz NOT NULL DEFAULT now(),
3757
+ UNIQUE (run_id, step_key)
3758
+ );
3759
+
3760
+ ALTER TABLE supacloud_workflows.steps
3761
+ ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0
3762
+ CHECK (retry_delay_seconds BETWEEN 0 AND 86400);
3763
+
3764
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx
3765
+ ON supacloud_workflows.steps (run_id)
3766
+ WHERE status IN ('queued', 'running');
3767
+
3768
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx
3769
+ ON supacloud_workflows.runs (status, updated_at DESC, id);
3770
+
3771
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx
3772
+ ON supacloud_workflows.steps (run_id, created_at, id);
3773
+
3774
+ CREATE TABLE IF NOT EXISTS supacloud_workflows.events (
3775
+ id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
3776
+ run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
3777
+ step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,
3778
+ event_type text NOT NULL
3779
+ CHECK (event_type IN (
3780
+ 'run_started', 'step_claimed', 'step_retried', 'step_completed',
3781
+ 'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'
3782
+ )),
3783
+ attempt integer CHECK (attempt IS NULL OR attempt > 0),
3784
+ details jsonb NOT NULL DEFAULT '{}'::jsonb
3785
+ CHECK (jsonb_typeof(details) = 'object'),
3786
+ created_at timestamptz NOT NULL DEFAULT now()
3787
+ );
3788
+
3789
+ CREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx
3790
+ ON supacloud_workflows.events (run_id, id);
3791
+
3792
+ CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx
3793
+ ON supacloud_workflows.events (step_id, attempt)
3794
+ WHERE event_type IN ('step_retried', 'step_dead_lettered')
3795
+ AND details ->> 'operation' = 'retry';
3796
+
3797
+ CREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(
3798
+ p_run_id uuid,
3799
+ p_idempotent boolean DEFAULT false
3800
+ ) RETURNS jsonb
3801
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
3802
+ SELECT jsonb_build_object(
3803
+ 'runId', run.id,
3804
+ 'workflowName', run.workflow_name,
3805
+ 'workflowVersion', run.workflow_version,
3806
+ 'status', run.status,
3807
+ 'input', run.input,
3808
+ 'output', run.output,
3809
+ 'errorMessage', run.error_message,
3810
+ 'rowVersion', run.row_version::text,
3811
+ 'createdAt', run.created_at,
3812
+ 'startedAt', run.started_at,
3813
+ 'completedAt', run.completed_at,
3814
+ 'updatedAt', run.updated_at,
3815
+ 'idempotent', p_idempotent,
3816
+ 'steps', coalesce((
3817
+ SELECT jsonb_agg(jsonb_build_object(
3818
+ 'stepId', step.id,
3819
+ 'stepKey', step.step_key,
3820
+ 'status', step.status,
3821
+ 'input', step.input,
3822
+ 'output', step.output,
3823
+ 'errorMessage', step.error_message,
3824
+ 'attempts', step.attempts,
3825
+ 'maxAttempts', step.max_attempts,
3826
+ 'retryDelaySeconds', step.retry_delay_seconds,
3827
+ 'queueMessageId', step.queue_message_id::text,
3828
+ 'claimedBy', step.claimed_by,
3829
+ 'claimedAt', step.claimed_at,
3830
+ 'completedAt', step.completed_at,
3831
+ 'nextStepKey', step.next_step_key,
3832
+ 'createdAt', step.created_at,
3833
+ 'updatedAt', step.updated_at
3834
+ ) ORDER BY step.created_at, step.id)
3835
+ FROM supacloud_workflows.steps step
3836
+ WHERE step.run_id = run.id
3837
+ ), '[]'::jsonb)
3838
+ )
3839
+ FROM supacloud_workflows.runs run
3840
+ WHERE run.id = p_run_id
3841
+ $$;
3842
+
3843
+ CREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(
3844
+ p_run_id uuid,
3845
+ p_step_key text,
3846
+ p_input jsonb,
3847
+ p_max_attempts integer
3848
+ ) RETURNS supacloud_workflows.steps
3849
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3850
+ DECLARE
3851
+ normalized_step_key text := nullif(btrim(p_step_key), '');
3852
+ step_id uuid := gen_random_uuid();
3853
+ message_id bigint;
3854
+ created_step supacloud_workflows.steps%ROWTYPE;
3855
+ BEGIN
3856
+ IF normalized_step_key IS NULL
3857
+ OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3858
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3859
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3860
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';
3861
+ END IF;
3862
+
3863
+ IF NOT EXISTS (
3864
+ SELECT 1 FROM supacloud_workflows.runs run
3865
+ WHERE run.id = p_run_id AND run.status IN ('queued', 'running')
3866
+ ) THEN
3867
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
3868
+ END IF;
3869
+
3870
+ SELECT queued_id INTO message_id
3871
+ FROM pgmq.send(
3872
+ 'supacloud_internal_workflows',
3873
+ jsonb_build_object('run_id', p_run_id, 'step_id', step_id),
3874
+ 0
3875
+ ) AS queued_id;
3876
+
3877
+ INSERT INTO supacloud_workflows.steps (
3878
+ id, run_id, step_key, input, max_attempts, queue_message_id
3879
+ ) VALUES (
3880
+ step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
3881
+ ) RETURNING * INTO created_step;
3882
+
3883
+ RETURN created_step;
3884
+ END;
3885
+ $$;
3886
+
3887
+ -- Clean-code exception: private transitions keep typed PostgreSQL arguments and
3888
+ -- the complete lock/queue/ledger/event mutation in one transaction. The public
3889
+ -- contract already uses one JSON request; revisit if a private routine gains a
3890
+ -- second caller or any transition can be decomposed without weakening atomicity.
3891
+ CREATE OR REPLACE FUNCTION supacloud_workflows.start_run(
3892
+ p_run_id uuid,
3893
+ p_workflow_name text,
3894
+ p_workflow_version text,
3895
+ p_first_step_key text,
3896
+ p_input jsonb,
3897
+ p_max_attempts integer
3898
+ ) RETURNS jsonb
3899
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3900
+ DECLARE
3901
+ normalized_name text := nullif(btrim(p_workflow_name), '');
3902
+ normalized_version text := nullif(btrim(p_workflow_version), '');
3903
+ normalized_first_step_key text := nullif(btrim(p_first_step_key), '');
3904
+ existing_run supacloud_workflows.runs%ROWTYPE;
3905
+ existing_step supacloud_workflows.steps%ROWTYPE;
3906
+ first_step supacloud_workflows.steps%ROWTYPE;
3907
+ BEGIN
3908
+ IF p_run_id IS NULL
3909
+ OR normalized_name IS NULL
3910
+ OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3911
+ OR normalized_version IS NULL
3912
+ OR char_length(normalized_version) > 80
3913
+ OR normalized_first_step_key IS NULL
3914
+ OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
3915
+ OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
3916
+ OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
3917
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
3918
+ END IF;
3919
+
3920
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
3921
+ SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
3922
+ IF FOUND THEN
3923
+ SELECT * INTO existing_step
3924
+ FROM supacloud_workflows.steps
3925
+ WHERE run_id = p_run_id
3926
+ ORDER BY created_at, id
3927
+ LIMIT 1;
3928
+ IF NOT FOUND THEN
3929
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3930
+ END IF;
3931
+ IF existing_run.workflow_name <> normalized_name
3932
+ OR existing_run.workflow_version <> normalized_version
3933
+ OR existing_run.input <> p_input
3934
+ OR existing_step.step_key <> normalized_first_step_key
3935
+ OR existing_step.input <> p_input
3936
+ OR existing_step.max_attempts <> p_max_attempts THEN
3937
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
3938
+ END IF;
3939
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
3940
+ END IF;
3941
+
3942
+ INSERT INTO supacloud_workflows.runs (
3943
+ id, workflow_name, workflow_version, input
3944
+ ) VALUES (
3945
+ p_run_id, normalized_name, normalized_version, p_input
3946
+ );
3947
+ first_step := supacloud_workflows.enqueue_step(
3948
+ p_run_id, normalized_first_step_key, p_input, p_max_attempts
3949
+ );
3950
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)
3951
+ VALUES (p_run_id, first_step.id, 'run_started');
3952
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
3953
+ END;
3954
+ $$;
3955
+
3956
+ CREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(
3957
+ p_worker_id text,
3958
+ p_visibility_timeout_seconds integer
3959
+ ) RETURNS jsonb
3960
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
3961
+ DECLARE
3962
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
3963
+ queued_message pgmq.message_record;
3964
+ message_run_id text;
3965
+ message_step_id text;
3966
+ candidate_run_id uuid;
3967
+ claimed_step supacloud_workflows.steps%ROWTYPE;
3968
+ claimed_run supacloud_workflows.runs%ROWTYPE;
3969
+ BEGIN
3970
+ IF normalized_worker_id IS NULL
3971
+ OR char_length(normalized_worker_id) > 200
3972
+ OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN
3973
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
3974
+ END IF;
3975
+
3976
+ SELECT * INTO queued_message
3977
+ FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)
3978
+ LIMIT 1;
3979
+ IF NOT FOUND THEN RETURN NULL; END IF;
3980
+
3981
+ message_run_id := queued_message.message ->> 'run_id';
3982
+ message_step_id := queued_message.message ->> 'step_id';
3983
+ IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'
3984
+ OR message_run_id IS NULL
3985
+ OR message_step_id IS NULL
3986
+ OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
3987
+ OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
3988
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
3989
+ RETURN jsonb_build_object(
3990
+ 'status', 'discarded',
3991
+ 'reason', 'invalid_message',
3992
+ 'messageId', queued_message.msg_id::text
3993
+ );
3994
+ END IF;
3995
+
3996
+ SELECT step.run_id INTO candidate_run_id
3997
+ FROM supacloud_workflows.steps step
3998
+ WHERE step.id::text = lower(message_step_id)
3999
+ AND step.run_id::text = lower(message_run_id)
4000
+ AND step.queue_message_id = queued_message.msg_id;
4001
+ IF NOT FOUND THEN
4002
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
4003
+ RETURN jsonb_build_object(
4004
+ 'status', 'discarded',
4005
+ 'reason', 'orphaned_message',
4006
+ 'messageId', queued_message.msg_id::text
4007
+ );
4008
+ END IF;
4009
+
4010
+ IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
4011
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
4012
+ END IF;
4013
+ SELECT * INTO claimed_step
4014
+ FROM supacloud_workflows.steps
4015
+ WHERE id::text = lower(message_step_id)
4016
+ AND run_id = candidate_run_id
4017
+ AND queue_message_id = queued_message.msg_id
4018
+ FOR UPDATE;
4019
+ IF NOT FOUND THEN
4020
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
4021
+ RETURN jsonb_build_object(
4022
+ 'status', 'discarded',
4023
+ 'reason', 'orphaned_message',
4024
+ 'messageId', queued_message.msg_id::text
4025
+ );
4026
+ END IF;
4027
+
4028
+ SELECT * INTO claimed_run
4029
+ FROM supacloud_workflows.runs
4030
+ WHERE id = claimed_step.run_id
4031
+ FOR UPDATE;
4032
+ IF claimed_run.status NOT IN ('queued', 'running')
4033
+ OR claimed_step.status NOT IN ('queued', 'running') THEN
4034
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
4035
+ RETURN jsonb_build_object(
4036
+ 'status', 'discarded',
4037
+ 'reason', 'step_not_claimable',
4038
+ 'runId', claimed_step.run_id,
4039
+ 'stepId', claimed_step.id,
4040
+ 'messageId', queued_message.msg_id::text
4041
+ );
4042
+ END IF;
4043
+
4044
+ IF queued_message.read_ct > claimed_step.max_attempts THEN
4045
+ UPDATE supacloud_workflows.steps
4046
+ SET status = 'dead_lettered', attempts = queued_message.read_ct,
4047
+ error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()
4048
+ WHERE id = claimed_step.id;
4049
+ UPDATE supacloud_workflows.runs
4050
+ SET status = 'failed', error_message = 'maximum attempts exceeded',
4051
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4052
+ WHERE id = claimed_step.run_id;
4053
+ PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
4054
+ INSERT INTO supacloud_workflows.events (
4055
+ run_id, step_id, event_type, attempt, details
4056
+ ) VALUES (
4057
+ claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,
4058
+ jsonb_build_object('errorMessage', 'maximum attempts exceeded')
4059
+ );
4060
+ RETURN jsonb_build_object(
4061
+ 'status', 'dead_lettered',
4062
+ 'runId', claimed_step.run_id,
4063
+ 'stepId', claimed_step.id,
4064
+ 'stepKey', claimed_step.step_key,
4065
+ 'messageId', queued_message.msg_id::text,
4066
+ 'attempt', queued_message.read_ct,
4067
+ 'maxAttempts', claimed_step.max_attempts
4068
+ );
4069
+ END IF;
4070
+
4071
+ UPDATE supacloud_workflows.steps
4072
+ SET status = 'running', attempts = queued_message.read_ct,
4073
+ retry_delay_seconds = 0, claimed_by = normalized_worker_id,
4074
+ claimed_at = now(), updated_at = now()
4075
+ WHERE id = claimed_step.id;
4076
+ UPDATE supacloud_workflows.runs
4077
+ SET status = 'running', started_at = coalesce(started_at, now()),
4078
+ updated_at = now(), row_version = row_version + 1
4079
+ WHERE id = claimed_step.run_id;
4080
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)
4081
+ VALUES (
4082
+ claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,
4083
+ jsonb_build_object('workerId', normalized_worker_id)
4084
+ );
4085
+
4086
+ RETURN jsonb_build_object(
4087
+ 'status', 'claimed',
4088
+ 'runId', claimed_step.run_id,
4089
+ 'workflowName', claimed_run.workflow_name,
4090
+ 'workflowVersion', claimed_run.workflow_version,
4091
+ 'stepId', claimed_step.id,
4092
+ 'stepKey', claimed_step.step_key,
4093
+ 'input', claimed_step.input,
4094
+ 'messageId', queued_message.msg_id::text,
4095
+ 'attempt', queued_message.read_ct,
4096
+ 'maxAttempts', claimed_step.max_attempts,
4097
+ 'workerId', normalized_worker_id
4098
+ );
4099
+ END;
4100
+ $$;
4101
+
4102
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(
4103
+ p_step_id uuid
4104
+ ) RETURNS supacloud_workflows.steps
4105
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4106
+ DECLARE
4107
+ candidate_run_id uuid;
4108
+ active_step supacloud_workflows.steps%ROWTYPE;
4109
+ BEGIN
4110
+ SELECT step.run_id INTO candidate_run_id
4111
+ FROM supacloud_workflows.steps step
4112
+ WHERE step.id = p_step_id;
4113
+ IF NOT FOUND THEN
4114
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4115
+ END IF;
4116
+ PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
4117
+ SELECT * INTO active_step
4118
+ FROM supacloud_workflows.steps
4119
+ WHERE id = p_step_id
4120
+ FOR UPDATE;
4121
+ IF NOT FOUND THEN
4122
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
4123
+ END IF;
4124
+ RETURN active_step;
4125
+ END;
4126
+ $$;
4127
+
4128
+ CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(
4129
+ p_step_id uuid,
4130
+ p_message_id bigint,
4131
+ p_attempt integer,
4132
+ p_worker_id text
4133
+ ) RETURNS supacloud_workflows.steps
4134
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4135
+ DECLARE
4136
+ active_step supacloud_workflows.steps%ROWTYPE;
4137
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4138
+ BEGIN
4139
+ active_step := supacloud_workflows.lock_step(p_step_id);
4140
+ IF normalized_worker_id IS NULL
4141
+ OR p_message_id IS NULL
4142
+ OR p_attempt IS NULL
4143
+ OR active_step.queue_message_id IS DISTINCT FROM p_message_id
4144
+ OR active_step.attempts IS DISTINCT FROM p_attempt
4145
+ OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN
4146
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4147
+ END IF;
4148
+ RETURN active_step;
4149
+ END;
4150
+ $$;
4151
+
4152
+ CREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(
4153
+ p_step_id uuid,
4154
+ p_message_id bigint,
4155
+ p_attempt integer,
4156
+ p_worker_id text,
4157
+ p_output jsonb,
4158
+ p_next_step_key text,
4159
+ p_next_input jsonb,
4160
+ p_next_max_attempts integer
4161
+ ) RETURNS jsonb
4162
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4163
+ DECLARE
4164
+ current_step supacloud_workflows.steps%ROWTYPE;
4165
+ next_step supacloud_workflows.steps%ROWTYPE;
4166
+ normalized_next_step_key text := nullif(btrim(p_next_step_key), '');
4167
+ archived boolean;
4168
+ BEGIN
4169
+ IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'
4170
+ OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'
4171
+ OR normalized_next_step_key IS NULL
4172
+ OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
4173
+ OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN
4174
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4175
+ END IF;
4176
+ current_step := supacloud_workflows.lock_step_attempt(
4177
+ p_step_id, p_message_id, p_attempt, p_worker_id
4178
+ );
4179
+
4180
+ IF current_step.status = 'completed' THEN
4181
+ SELECT * INTO next_step
4182
+ FROM supacloud_workflows.steps
4183
+ WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
4184
+ IF NOT FOUND
4185
+ OR current_step.output IS DISTINCT FROM p_output
4186
+ OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
4187
+ OR next_step.input IS DISTINCT FROM p_next_input
4188
+ OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN
4189
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4190
+ END IF;
4191
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4192
+ END IF;
4193
+ IF current_step.status <> 'running' THEN
4194
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4195
+ END IF;
4196
+
4197
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4198
+ IF archived IS DISTINCT FROM true THEN
4199
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4200
+ END IF;
4201
+ UPDATE supacloud_workflows.steps
4202
+ SET status = 'completed', output = p_output, error_message = '',
4203
+ completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()
4204
+ WHERE id = current_step.id;
4205
+ INSERT INTO supacloud_workflows.events (
4206
+ run_id, step_id, event_type, attempt, details
4207
+ ) VALUES (
4208
+ current_step.run_id, current_step.id, 'step_completed', p_attempt,
4209
+ jsonb_build_object('nextStepKey', normalized_next_step_key)
4210
+ );
4211
+ next_step := supacloud_workflows.enqueue_step(
4212
+ current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts
4213
+ );
4214
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4215
+ END;
4216
+ $$;
4217
+
4218
+ CREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(
4219
+ p_step_id uuid,
4220
+ p_message_id bigint,
4221
+ p_attempt integer,
4222
+ p_worker_id text,
4223
+ p_step_output jsonb,
4224
+ p_run_output jsonb
4225
+ ) RETURNS jsonb
4226
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4227
+ DECLARE
4228
+ current_step supacloud_workflows.steps%ROWTYPE;
4229
+ current_run supacloud_workflows.runs%ROWTYPE;
4230
+ archived boolean;
4231
+ BEGIN
4232
+ IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'
4233
+ OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN
4234
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4235
+ END IF;
4236
+ current_step := supacloud_workflows.lock_step_attempt(
4237
+ p_step_id, p_message_id, p_attempt, p_worker_id
4238
+ );
4239
+ SELECT * INTO current_run
4240
+ FROM supacloud_workflows.runs
4241
+ WHERE id = current_step.run_id
4242
+ FOR UPDATE;
4243
+
4244
+ IF current_step.status = 'completed' THEN
4245
+ IF current_step.next_step_key IS NOT NULL
4246
+ OR current_step.output IS DISTINCT FROM p_step_output
4247
+ OR current_run.status IS DISTINCT FROM 'completed'
4248
+ OR current_run.output IS DISTINCT FROM p_run_output THEN
4249
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4250
+ END IF;
4251
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4252
+ END IF;
4253
+ IF current_step.status <> 'running' THEN
4254
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4255
+ END IF;
4256
+ IF current_run.status <> 'running' THEN
4257
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4258
+ END IF;
4259
+
4260
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4261
+ IF archived IS DISTINCT FROM true THEN
4262
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4263
+ END IF;
4264
+ UPDATE supacloud_workflows.steps
4265
+ SET status = 'completed', output = p_step_output, error_message = '',
4266
+ completed_at = now(), updated_at = now()
4267
+ WHERE id = current_step.id;
4268
+ UPDATE supacloud_workflows.runs
4269
+ SET status = 'completed', output = p_run_output, error_message = '',
4270
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4271
+ WHERE id = current_step.run_id AND status = 'running';
4272
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4273
+ VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);
4274
+ INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
4275
+ VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);
4276
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4277
+ END;
4278
+ $$;
4279
+
4280
+ CREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(
4281
+ p_step_id uuid,
4282
+ p_message_id bigint,
4283
+ p_attempt integer,
4284
+ p_worker_id text,
4285
+ p_error_message text,
4286
+ p_delay_seconds integer
4287
+ ) RETURNS jsonb
4288
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4289
+ DECLARE
4290
+ current_step supacloud_workflows.steps%ROWTYPE;
4291
+ normalized_error text := nullif(btrim(p_error_message), '');
4292
+ normalized_worker_id text := nullif(btrim(p_worker_id), '');
4293
+ retry_receipt jsonb;
4294
+ queue_message_updated boolean;
4295
+ archived boolean;
4296
+ BEGIN
4297
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000
4298
+ OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN
4299
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4300
+ END IF;
4301
+ current_step := supacloud_workflows.lock_step(p_step_id);
4302
+ SELECT event.details INTO retry_receipt
4303
+ FROM supacloud_workflows.events event
4304
+ WHERE event.step_id = current_step.id
4305
+ AND event.attempt = p_attempt
4306
+ AND event.event_type IN ('step_retried', 'step_dead_lettered')
4307
+ AND event.details ->> 'operation' = 'retry'
4308
+ ORDER BY event.id DESC
4309
+ LIMIT 1;
4310
+ IF FOUND THEN
4311
+ IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text
4312
+ OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id
4313
+ OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error
4314
+ OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN
4315
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4316
+ END IF;
4317
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4318
+ END IF;
4319
+ IF normalized_worker_id IS NULL
4320
+ OR p_message_id IS NULL
4321
+ OR p_attempt IS NULL
4322
+ OR current_step.queue_message_id IS DISTINCT FROM p_message_id
4323
+ OR current_step.attempts IS DISTINCT FROM p_attempt
4324
+ OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id
4325
+ OR current_step.status <> 'running' THEN
4326
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4327
+ END IF;
4328
+
4329
+ IF current_step.attempts >= current_step.max_attempts THEN
4330
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4331
+ IF archived IS DISTINCT FROM true THEN
4332
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4333
+ END IF;
4334
+ UPDATE supacloud_workflows.steps
4335
+ SET status = 'dead_lettered', error_message = normalized_error,
4336
+ completed_at = now(), updated_at = now()
4337
+ WHERE id = current_step.id;
4338
+ UPDATE supacloud_workflows.runs
4339
+ SET status = 'failed', error_message = normalized_error,
4340
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4341
+ WHERE id = current_step.run_id AND status = 'running';
4342
+ IF NOT FOUND THEN
4343
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4344
+ END IF;
4345
+ INSERT INTO supacloud_workflows.events (
4346
+ run_id, step_id, event_type, attempt, details
4347
+ ) VALUES (
4348
+ current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt,
4349
+ jsonb_build_object(
4350
+ 'operation', 'retry',
4351
+ 'messageId', p_message_id::text,
4352
+ 'workerId', normalized_worker_id,
4353
+ 'errorMessage', normalized_error,
4354
+ 'delaySeconds', p_delay_seconds
4355
+ )
4356
+ );
4357
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4358
+ END IF;
4359
+
4360
+ SELECT EXISTS (
4361
+ SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)
4362
+ ) INTO queue_message_updated;
4363
+ IF queue_message_updated IS DISTINCT FROM true THEN
4364
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4365
+ END IF;
4366
+ UPDATE supacloud_workflows.steps
4367
+ SET status = 'queued', error_message = normalized_error,
4368
+ retry_delay_seconds = p_delay_seconds, updated_at = now()
4369
+ WHERE id = current_step.id;
4370
+ INSERT INTO supacloud_workflows.events (
4371
+ run_id, step_id, event_type, attempt, details
4372
+ ) VALUES (
4373
+ current_step.run_id, current_step.id, 'step_retried', p_attempt,
4374
+ jsonb_build_object(
4375
+ 'operation', 'retry',
4376
+ 'messageId', p_message_id::text,
4377
+ 'workerId', normalized_worker_id,
4378
+ 'errorMessage', normalized_error,
4379
+ 'delaySeconds', p_delay_seconds
4380
+ )
4381
+ );
4382
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4383
+ END;
4384
+ $$;
4385
+
4386
+ CREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(
4387
+ p_step_id uuid,
4388
+ p_message_id bigint,
4389
+ p_attempt integer,
4390
+ p_worker_id text,
4391
+ p_error_message text
4392
+ ) RETURNS jsonb
4393
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4394
+ DECLARE
4395
+ current_step supacloud_workflows.steps%ROWTYPE;
4396
+ current_run supacloud_workflows.runs%ROWTYPE;
4397
+ normalized_error text := nullif(btrim(p_error_message), '');
4398
+ archived boolean;
4399
+ BEGIN
4400
+ IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN
4401
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4402
+ END IF;
4403
+ current_step := supacloud_workflows.lock_step_attempt(
4404
+ p_step_id, p_message_id, p_attempt, p_worker_id
4405
+ );
4406
+ SELECT * INTO current_run
4407
+ FROM supacloud_workflows.runs
4408
+ WHERE id = current_step.run_id
4409
+ FOR UPDATE;
4410
+
4411
+ IF current_step.status = 'failed' THEN
4412
+ IF current_step.error_message IS DISTINCT FROM normalized_error
4413
+ OR current_run.status IS DISTINCT FROM 'failed'
4414
+ OR current_run.error_message IS DISTINCT FROM normalized_error THEN
4415
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4416
+ END IF;
4417
+ RETURN supacloud_workflows.snapshot(current_step.run_id, true);
4418
+ END IF;
4419
+ IF current_step.status <> 'running' THEN
4420
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
4421
+ END IF;
4422
+ IF current_run.status <> 'running' THEN
4423
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4424
+ END IF;
4425
+
4426
+ SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
4427
+ IF archived IS DISTINCT FROM true THEN
4428
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
4429
+ END IF;
4430
+ UPDATE supacloud_workflows.steps
4431
+ SET status = 'failed', error_message = normalized_error,
4432
+ completed_at = now(), updated_at = now()
4433
+ WHERE id = current_step.id;
4434
+ UPDATE supacloud_workflows.runs
4435
+ SET status = 'failed', error_message = normalized_error,
4436
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4437
+ WHERE id = current_step.run_id AND status = 'running';
4438
+ INSERT INTO supacloud_workflows.events (
4439
+ run_id, step_id, event_type, attempt, details
4440
+ ) VALUES (
4441
+ current_step.run_id, current_step.id, 'step_failed', p_attempt,
4442
+ jsonb_build_object('errorMessage', normalized_error)
4443
+ );
4444
+ RETURN supacloud_workflows.snapshot(current_step.run_id, false);
4445
+ END;
4446
+ $$;
4447
+
4448
+ CREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(
4449
+ p_run_id uuid,
4450
+ p_reason text
4451
+ ) RETURNS jsonb
4452
+ LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
4453
+ DECLARE
4454
+ normalized_reason text := nullif(btrim(p_reason), '');
4455
+ locked_run supacloud_workflows.runs%ROWTYPE;
4456
+ active_step supacloud_workflows.steps%ROWTYPE;
4457
+ BEGIN
4458
+ IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN
4459
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4460
+ END IF;
4461
+ PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
4462
+ SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
4463
+ IF NOT FOUND THEN
4464
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
4465
+ END IF;
4466
+ IF locked_run.status = 'cancelled' THEN
4467
+ IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN
4468
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
4469
+ END IF;
4470
+ RETURN supacloud_workflows.snapshot(p_run_id, true);
4471
+ END IF;
4472
+ IF locked_run.status NOT IN ('queued', 'running') THEN
4473
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
4474
+ END IF;
4475
+ SELECT * INTO active_step
4476
+ FROM supacloud_workflows.steps
4477
+ WHERE run_id = p_run_id AND status IN ('queued', 'running')
4478
+ FOR UPDATE;
4479
+ IF FOUND THEN
4480
+ PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
4481
+ UPDATE supacloud_workflows.steps
4482
+ SET status = 'cancelled', error_message = normalized_reason,
4483
+ completed_at = now(), updated_at = now()
4484
+ WHERE id = active_step.id;
4485
+ END IF;
4486
+ UPDATE supacloud_workflows.runs
4487
+ SET status = 'cancelled', error_message = normalized_reason,
4488
+ completed_at = now(), updated_at = now(), row_version = row_version + 1
4489
+ WHERE id = p_run_id;
4490
+ INSERT INTO supacloud_workflows.events (
4491
+ run_id, step_id, event_type, details
4492
+ ) VALUES (
4493
+ p_run_id, active_step.id, 'run_cancelled',
4494
+ jsonb_build_object('reason', normalized_reason)
4495
+ );
4496
+ RETURN supacloud_workflows.snapshot(p_run_id, false);
4497
+ END;
4498
+ $$;
4499
+
4500
+ CREATE OR REPLACE FUNCTION supacloud_workflows.run_events(
4501
+ p_run_id uuid,
4502
+ p_after_event_id bigint,
4503
+ p_limit integer
4504
+ ) RETURNS jsonb
4505
+ LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
4506
+ SELECT coalesce(jsonb_agg(jsonb_build_object(
4507
+ 'eventId', page.id::text,
4508
+ 'runId', page.run_id,
4509
+ 'stepId', page.step_id,
4510
+ 'eventType', page.event_type,
4511
+ 'attempt', page.attempt,
4512
+ 'details', page.details,
4513
+ 'createdAt', page.created_at
4514
+ ) ORDER BY page.id), '[]'::jsonb)
4515
+ FROM (
4516
+ SELECT event.*
4517
+ FROM supacloud_workflows.events event
4518
+ WHERE event.run_id = p_run_id AND event.id > p_after_event_id
4519
+ ORDER BY event.id
4520
+ LIMIT p_limit
4521
+ ) page
4522
+ $$;
4523
+
4524
+ CREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(
4525
+ request jsonb,
4526
+ key text
4527
+ ) RETURNS uuid
4528
+ LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$
4529
+ DECLARE
4530
+ uuid_text text;
4531
+ BEGIN
4532
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4533
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4534
+ END IF;
4535
+ uuid_text := request ->> key;
4536
+ IF uuid_text IS NULL
4537
+ OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
4538
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
4539
+ END IF;
4540
+ RETURN uuid_text::uuid;
4541
+ END;
4542
+ $$;
4543
+
4544
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)
4545
+ RETURNS jsonb
4546
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4547
+ DECLARE
4548
+ max_attempts integer;
4549
+ BEGIN
4550
+ max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
4551
+ RETURN supacloud_workflows.start_run(
4552
+ supacloud_workflows.request_uuid(request, 'runId'),
4553
+ request ->> 'workflowName',
4554
+ request ->> 'workflowVersion',
4555
+ request ->> 'firstStepKey',
4556
+ coalesce(request -> 'input', '{}'::jsonb),
4557
+ max_attempts
4558
+ );
4559
+ EXCEPTION
4560
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4561
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
4562
+ END;
4563
+ $$;
4564
+
4565
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)
4566
+ RETURNS jsonb
4567
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4568
+ DECLARE
4569
+ visibility_timeout_seconds integer;
4570
+ BEGIN
4571
+ IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
4572
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4573
+ END IF;
4574
+ visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);
4575
+ RETURN supacloud_workflows.claim_step(
4576
+ request ->> 'workerId', visibility_timeout_seconds
4577
+ );
4578
+ EXCEPTION
4579
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4580
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
4581
+ END;
4582
+ $$;
4583
+
4584
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)
4585
+ RETURNS jsonb
4586
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4587
+ DECLARE
4588
+ message_id bigint;
4589
+ attempt integer;
4590
+ next_max_attempts integer;
4591
+ BEGIN
4592
+ message_id := (request ->> 'messageId')::bigint;
4593
+ attempt := (request ->> 'attempt')::integer;
4594
+ next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);
4595
+ IF message_id <= 0 OR attempt <= 0 THEN
4596
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4597
+ END IF;
4598
+ RETURN supacloud_workflows.advance_step(
4599
+ supacloud_workflows.request_uuid(request, 'stepId'),
4600
+ message_id,
4601
+ attempt,
4602
+ request ->> 'workerId',
4603
+ coalesce(request -> 'output', '{}'::jsonb),
4604
+ request ->> 'nextStepKey',
4605
+ coalesce(request -> 'nextInput', '{}'::jsonb),
4606
+ next_max_attempts
4607
+ );
4608
+ EXCEPTION
4609
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4610
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
4611
+ END;
4612
+ $$;
4613
+
4614
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)
4615
+ RETURNS jsonb
4616
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4617
+ DECLARE
4618
+ message_id bigint;
4619
+ attempt integer;
4620
+ BEGIN
4621
+ message_id := (request ->> 'messageId')::bigint;
4622
+ attempt := (request ->> 'attempt')::integer;
4623
+ IF message_id <= 0 OR attempt <= 0 THEN
4624
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4625
+ END IF;
4626
+ RETURN supacloud_workflows.complete_run(
4627
+ supacloud_workflows.request_uuid(request, 'stepId'),
4628
+ message_id,
4629
+ attempt,
4630
+ request ->> 'workerId',
4631
+ coalesce(request -> 'stepOutput', '{}'::jsonb),
4632
+ coalesce(request -> 'runOutput', '{}'::jsonb)
4633
+ );
4634
+ EXCEPTION
4635
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4636
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
4637
+ END;
4638
+ $$;
4639
+
4640
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)
4641
+ RETURNS jsonb
4642
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4643
+ DECLARE
4644
+ message_id bigint;
4645
+ attempt integer;
4646
+ delay_seconds integer;
4647
+ BEGIN
4648
+ message_id := (request ->> 'messageId')::bigint;
4649
+ attempt := (request ->> 'attempt')::integer;
4650
+ delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);
4651
+ IF message_id <= 0 OR attempt <= 0 THEN
4652
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4653
+ END IF;
4654
+ RETURN supacloud_workflows.retry_step(
4655
+ supacloud_workflows.request_uuid(request, 'stepId'),
4656
+ message_id,
4657
+ attempt,
4658
+ request ->> 'workerId',
4659
+ request ->> 'errorMessage',
4660
+ delay_seconds
4661
+ );
4662
+ EXCEPTION
4663
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4664
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
4665
+ END;
4666
+ $$;
4667
+
4668
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)
4669
+ RETURNS jsonb
4670
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4671
+ DECLARE
4672
+ message_id bigint;
4673
+ attempt integer;
4674
+ BEGIN
4675
+ message_id := (request ->> 'messageId')::bigint;
4676
+ attempt := (request ->> 'attempt')::integer;
4677
+ IF message_id <= 0 OR attempt <= 0 THEN
4678
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4679
+ END IF;
4680
+ RETURN supacloud_workflows.fail_step(
4681
+ supacloud_workflows.request_uuid(request, 'stepId'),
4682
+ message_id,
4683
+ attempt,
4684
+ request ->> 'workerId',
4685
+ request ->> 'errorMessage'
4686
+ );
4687
+ EXCEPTION
4688
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4689
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
4690
+ END;
4691
+ $$;
4692
+
4693
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)
4694
+ RETURNS jsonb
4695
+ LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
4696
+ BEGIN
4697
+ RETURN supacloud_workflows.cancel_run(
4698
+ supacloud_workflows.request_uuid(request, 'runId'),
4699
+ request ->> 'reason'
4700
+ );
4701
+ EXCEPTION
4702
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4703
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
4704
+ END;
4705
+ $$;
4706
+
4707
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)
4708
+ RETURNS jsonb
4709
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4710
+ BEGIN
4711
+ RETURN supacloud_workflows.snapshot(
4712
+ supacloud_workflows.request_uuid(request, 'runId'), false
4713
+ );
4714
+ EXCEPTION
4715
+ WHEN invalid_parameter_value OR invalid_text_representation THEN
4716
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';
4717
+ END;
4718
+ $$;
4719
+
4720
+ CREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)
4721
+ RETURNS jsonb
4722
+ LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
4723
+ DECLARE
4724
+ after_event_id bigint;
4725
+ event_limit integer;
4726
+ BEGIN
4727
+ after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);
4728
+ event_limit := coalesce((request ->> 'limit')::integer, 100);
4729
+ IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN
4730
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4731
+ END IF;
4732
+ RETURN supacloud_workflows.run_events(
4733
+ supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit
4734
+ );
4735
+ EXCEPTION
4736
+ WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
4737
+ RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
4738
+ END;
4739
+ $$;
4740
+
4741
+ REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows
4742
+ FROM PUBLIC, anon, authenticated, service_role;
4743
+ REVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows
4744
+ FROM PUBLIC, anon, authenticated, service_role;
4745
+ REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows
4746
+ FROM PUBLIC, anon, authenticated, service_role;
4747
+
4748
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;
4749
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;
4750
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;
4751
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;
4752
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;
4753
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;
4754
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;
4755
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;
4756
+ REVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;
4757
+
4758
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;
4759
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;
4760
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;
4761
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;
4762
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;
4763
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;
4764
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;
4765
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
4766
+ GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
4767
+ -- supacloud:sql-module:workflows-public:end
3583
4768
  `;
3584
4769
  var CRON_SQL = `
3585
4770
  create schema if not exists cron;
@@ -3906,12 +5091,32 @@ function pickTag(text, base) {
3906
5091
  return tag;
3907
5092
  }
3908
5093
 
3909
- // src/runtime/db/pglite-engine.ts
3910
- import { mkdir, open, unlink as unlink2 } from "fs/promises";
3911
- import { dirname, resolve } from "path";
3912
-
3913
5094
  // src/runtime/db/data-dir-lock.ts
3914
- import { readFile, unlink } from "fs/promises";
5095
+ import { mkdir, open, readFile, unlink } from "fs/promises";
5096
+ import { dirname, resolve } from "path";
5097
+ async function acquireDataDirLock(dataDir, engineName = "database") {
5098
+ if (!dataDir || dataDir.includes("://"))
5099
+ return async () => {};
5100
+ const absoluteDataDir = resolve(dataDir);
5101
+ const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
5102
+ await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
5103
+ const nonce = crypto.randomUUID();
5104
+ const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce, engineName);
5105
+ let released = false;
5106
+ return async () => {
5107
+ if (released)
5108
+ return;
5109
+ released = true;
5110
+ await handle.close();
5111
+ const owner = await readDataDirLockOwner(lockPath);
5112
+ if (owner?.nonce !== nonce)
5113
+ return;
5114
+ await unlink(lockPath).catch((error) => {
5115
+ if (error.code !== "ENOENT")
5116
+ throw error;
5117
+ });
5118
+ };
5119
+ }
3915
5120
  async function recoverStaleDataDirLock(lockPath) {
3916
5121
  const lockState = await inspectDataDirLock(lockPath);
3917
5122
  if (lockState.kind !== "stale")
@@ -3929,6 +5134,50 @@ async function readDataDirLockOwner(lockPath) {
3929
5134
  const lockState = await inspectDataDirLock(lockPath);
3930
5135
  return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
3931
5136
  }
5137
+ async function assertDataDirUnlocked(dataDir) {
5138
+ if (!dataDir)
5139
+ return;
5140
+ const lockPath = `${resolve(dataDir)}.supacloud-lite.lock`;
5141
+ const lockState = await recoverStaleDataDirLock(lockPath);
5142
+ if (lockState.kind === "missing")
5143
+ return;
5144
+ if (lockState.kind === "active") {
5145
+ throw new Error(`database data directory is already in use: ${resolve(dataDir)} (pid ${lockState.owner.pid})`);
5146
+ }
5147
+ throw unreadableLockError(lockPath, "database");
5148
+ }
5149
+ async function createDataDirLock(absoluteDataDir, lockPath, nonce, engineName) {
5150
+ for (let attempt = 0;attempt < 3; attempt++) {
5151
+ try {
5152
+ return await writeDataDirLock(lockPath, nonce);
5153
+ } catch (error) {
5154
+ if (error.code !== "EEXIST")
5155
+ throw error;
5156
+ const lockState = await recoverStaleDataDirLock(lockPath);
5157
+ if (lockState.kind === "active") {
5158
+ throw new Error(`${engineName} data directory is already in use: ${absoluteDataDir} (pid ${lockState.owner.pid})`);
5159
+ }
5160
+ if (lockState.kind === "unreadable")
5161
+ throw unreadableLockError(lockPath, engineName);
5162
+ }
5163
+ }
5164
+ throw unreadableLockError(lockPath, engineName);
5165
+ }
5166
+ async function writeDataDirLock(lockPath, nonce) {
5167
+ const handle = await open(lockPath, "wx", 384);
5168
+ try {
5169
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
5170
+ `);
5171
+ return handle;
5172
+ } catch (error) {
5173
+ await handle.close().catch(() => {});
5174
+ await unlink(lockPath).catch(() => {});
5175
+ throw error;
5176
+ }
5177
+ }
5178
+ function unreadableLockError(lockPath, engineName) {
5179
+ return new Error(`${engineName} data directory has an unreadable lock: ${lockPath}. ` + "Confirm no SupaCloud Lite process is using it, then remove the lock manually.");
5180
+ }
3932
5181
  async function inspectDataDirLock(lockPath) {
3933
5182
  let contents;
3934
5183
  try {
@@ -3982,7 +5231,7 @@ begin
3982
5231
  end $$;
3983
5232
  `;
3984
5233
  async function createPgliteEngine(dataDir) {
3985
- const releaseLock = await acquireDataDirLock(dataDir);
5234
+ const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
3986
5235
  let PGlite, extensions;
3987
5236
  const standaloneAssets = getStandaloneAssets();
3988
5237
  let cleanupStandaloneBundles = async () => {};
@@ -4106,63 +5355,6 @@ async function removePreparedBundles(cleanup) {
4106
5355
  console.error("Unable to remove temporary PGlite extension bundles:", error);
4107
5356
  }
4108
5357
  }
4109
- async function acquireDataDirLock(dataDir) {
4110
- if (!dataDir || dataDir.includes("://"))
4111
- return async () => {};
4112
- const absoluteDataDir = resolve(dataDir);
4113
- const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
4114
- await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
4115
- const nonce = crypto.randomUUID();
4116
- const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
4117
- let released = false;
4118
- return async () => {
4119
- if (released)
4120
- return;
4121
- released = true;
4122
- await handle?.close();
4123
- const owner = await readDataDirLockOwner(lockPath);
4124
- if (owner?.nonce !== nonce)
4125
- return;
4126
- await unlink2(lockPath).catch((error) => {
4127
- if (error.code !== "ENOENT")
4128
- throw error;
4129
- });
4130
- };
4131
- }
4132
- async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
4133
- for (let attempt = 0;attempt < 3; attempt++) {
4134
- try {
4135
- return await writeDataDirLock(lockPath, nonce);
4136
- } catch (error) {
4137
- if (error.code !== "EEXIST")
4138
- throw error;
4139
- const lockState = await recoverStaleDataDirLock(lockPath);
4140
- if (lockState.kind === "active")
4141
- throw lockInUseError(absoluteDataDir, lockState.owner.pid);
4142
- if (lockState.kind === "unreadable")
4143
- throw unreadableLockError(lockPath);
4144
- }
4145
- }
4146
- throw unreadableLockError(lockPath);
4147
- }
4148
- async function writeDataDirLock(lockPath, nonce) {
4149
- const handle = await open(lockPath, "wx", 384);
4150
- try {
4151
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
4152
- `);
4153
- return handle;
4154
- } catch (error) {
4155
- await handle.close().catch(() => {});
4156
- await unlink2(lockPath).catch(() => {});
4157
- throw error;
4158
- }
4159
- }
4160
- function lockInUseError(dataDir, pid) {
4161
- return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
4162
- }
4163
- function unreadableLockError(lockPath) {
4164
- return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
4165
- }
4166
5358
 
4167
5359
  // src/runtime/db/database.ts
4168
5360
  var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
@@ -4180,20 +5372,30 @@ class Database {
4180
5372
  }
4181
5373
  static async create(dataDirOrEngine, opts) {
4182
5374
  const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
4183
- if (engine.minimalBootstrap) {
4184
- await engine.exec(MINIMAL_BOOTSTRAP_SQL);
4185
- } else {
4186
- await engine.exec(BOOTSTRAP_SQL);
4187
- await engine.exec(PGMQ_SQL);
4188
- await engine.exec(CRON_SQL);
4189
- await engine.exec(NET_SQL);
4190
- await engine.exec(EXT_COMPAT_SQL);
4191
- await engine.exec(VAULT_SQL);
4192
- if (opts?.vaultKey) {
4193
- await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5375
+ try {
5376
+ if (engine.minimalBootstrap) {
5377
+ await engine.exec(MINIMAL_BOOTSTRAP_SQL);
5378
+ } else {
5379
+ await engine.exec(BOOTSTRAP_SQL);
5380
+ await engine.exec(PGMQ_SQL);
5381
+ await engine.exec(WORKFLOWS_SQL);
5382
+ await engine.exec(CRON_SQL);
5383
+ await engine.exec(NET_SQL);
5384
+ await engine.exec(EXT_COMPAT_SQL);
5385
+ await engine.exec(VAULT_SQL);
5386
+ if (opts?.vaultKey) {
5387
+ await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
5388
+ }
5389
+ }
5390
+ return new Database(engine);
5391
+ } catch (error) {
5392
+ try {
5393
+ await engine.close();
5394
+ } catch (cleanupError) {
5395
+ throw new AggregateError([error, cleanupError], "database bootstrap and cleanup failed");
4194
5396
  }
5397
+ throw error;
4195
5398
  }
4196
- return new Database(engine);
4197
5399
  }
4198
5400
  query(sql, params) {
4199
5401
  return this.engine.query(sql, params);
@@ -5932,6 +7134,25 @@ function parsePrefer(header) {
5932
7134
  }
5933
7135
  return prefer;
5934
7136
  }
7137
+ function applyRequestRange(query, request) {
7138
+ const range = request.headers.get("range");
7139
+ if (!range || query.limits.has("") || query.offsets.has(""))
7140
+ return;
7141
+ const unit = request.headers.get("range-unit");
7142
+ if (unit !== null && unit.toLowerCase() !== "items")
7143
+ throw new ParseError(`unsupported range unit: ${unit}`);
7144
+ const match = range.match(/^(\d+)-(\d*)$/);
7145
+ if (!match)
7146
+ throw new ParseError(`invalid range: ${range}`);
7147
+ const start = Number(match[1]);
7148
+ const end = match[2] ? Number(match[2]) : undefined;
7149
+ if (!Number.isSafeInteger(start) || end !== undefined && (!Number.isSafeInteger(end) || end < start)) {
7150
+ throw new ParseError(`invalid range: ${range}`);
7151
+ }
7152
+ query.offsets.set("", start);
7153
+ if (end !== undefined)
7154
+ query.limits.set("", end - start + 1);
7155
+ }
5935
7156
  var OBJECT_MEDIA = "application/vnd.pgrst.object+json";
5936
7157
  var CSV_MEDIA = "text/csv";
5937
7158
  var PLAN_MEDIA = "application/vnd.pgrst.plan";
@@ -6008,6 +7229,8 @@ class RestHandler {
6008
7229
  const wantsObject = accept.includes(OBJECT_MEDIA);
6009
7230
  const wantsCsv = accept.includes(CSV_MEDIA);
6010
7231
  const q = parseQuery(url.searchParams);
7232
+ if (method === "GET" || method === "HEAD")
7233
+ applyRequestRange(q, req);
6011
7234
  if (this.maxRows !== undefined && (method === "GET" || method === "HEAD")) {
6012
7235
  const requested = q.limits.get("");
6013
7236
  q.limits.set("", requested === undefined ? this.maxRows : Math.min(requested, this.maxRows));
@@ -6050,10 +7273,11 @@ class RestHandler {
6050
7273
  }
6051
7274
  return { rows: res.rows[0].body, count: count2 };
6052
7275
  });
7276
+ const offset = q.offsets.get("") ?? 0;
6053
7277
  return this.dataResponse(rows, {
6054
- status: 200,
7278
+ status: count !== null && (offset > 0 || rows.length < count) ? 206 : 200,
6055
7279
  count,
6056
- offset: q.offsets.get("") ?? 0,
7280
+ offset,
6057
7281
  wantsObject,
6058
7282
  wantsCsv,
6059
7283
  head: method === "HEAD"
@@ -6484,8 +7708,10 @@ function applyResize(image, metadata, options) {
6484
7708
  const proportionalWidth = Math.max(1, Math.round(metadata.width * options.height / metadata.height));
6485
7709
  image.resize(proportionalWidth);
6486
7710
  }
6487
- async function transformImage(bytes, options) {
6488
- if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7711
+ async function transformImage(source, options, knownSourceSize) {
7712
+ const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
7713
+ const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
7714
+ if (sourceSize > MAX_TRANSFORM_BYTES) {
6489
7715
  return {
6490
7716
  ok: false,
6491
7717
  status: 413,
@@ -6493,9 +7719,10 @@ async function transformImage(bytes, options) {
6493
7719
  message: "The source image exceeds the 25MB transformation limit"
6494
7720
  };
6495
7721
  }
7722
+ const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
6496
7723
  let metadata;
6497
7724
  try {
6498
- metadata = await new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS }).metadata();
7725
+ metadata = await image.metadata();
6499
7726
  } catch (error) {
6500
7727
  return mapImageError(error);
6501
7728
  }
@@ -6512,7 +7739,6 @@ async function transformImage(bytes, options) {
6512
7739
  message: `The source format ${outputFormat} is not supported by this runtime`
6513
7740
  };
6514
7741
  }
6515
- const image = new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS });
6516
7742
  applyResize(image, metadata, options);
6517
7743
  if (options.format === "jpeg" || options.format === "origin" && outputFormat === "jpeg" && options.quality !== undefined) {
6518
7744
  image.jpeg(options.quality === undefined ? undefined : { quality: options.quality });
@@ -6522,12 +7748,127 @@ async function transformImage(bytes, options) {
6522
7748
  image.webp(options.quality === undefined ? undefined : { quality: options.quality });
6523
7749
  }
6524
7750
  try {
6525
- return { ok: true, bytes: await image.bytes(), contentType };
7751
+ const bytes = await image.bytes();
7752
+ if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
7753
+ return {
7754
+ ok: false,
7755
+ status: 413,
7756
+ error: "ImageTooLarge",
7757
+ message: "The transformed image exceeds the 25MB transformation limit"
7758
+ };
7759
+ }
7760
+ return { ok: true, bytes, contentType };
6526
7761
  } catch (error) {
6527
7762
  return mapImageError(error);
6528
7763
  }
6529
7764
  }
6530
7765
 
7766
+ // src/runtime/storage/image-transform-cache.ts
7767
+ var DEFAULT_MAX_CACHE_BYTES = 64 * 1024 * 1024;
7768
+ var DEFAULT_MAX_CACHE_ENTRIES = 128;
7769
+ var OBJECT_VERSION_PREFIX = "v2-";
7770
+
7771
+ class Semaphore {
7772
+ limit;
7773
+ active = 0;
7774
+ waiters = [];
7775
+ constructor(limit) {
7776
+ this.limit = limit;
7777
+ }
7778
+ async run(operation) {
7779
+ await this.acquire();
7780
+ try {
7781
+ return await operation();
7782
+ } finally {
7783
+ this.release();
7784
+ }
7785
+ }
7786
+ async acquire() {
7787
+ if (this.active < this.limit) {
7788
+ this.active += 1;
7789
+ return;
7790
+ }
7791
+ await new Promise((resolve2) => this.waiters.push(resolve2));
7792
+ }
7793
+ release() {
7794
+ const next = this.waiters.shift();
7795
+ if (next) {
7796
+ next();
7797
+ return;
7798
+ }
7799
+ this.active -= 1;
7800
+ }
7801
+ }
7802
+ var globalImageTransformSemaphore = new Semaphore(1);
7803
+ function imageTransformCacheKey(version, options) {
7804
+ if (!version?.startsWith(OBJECT_VERSION_PREFIX))
7805
+ return null;
7806
+ return [
7807
+ version,
7808
+ options.width ?? "",
7809
+ options.height ?? "",
7810
+ options.resize,
7811
+ options.quality ?? "",
7812
+ options.format
7813
+ ].join("\x00");
7814
+ }
7815
+
7816
+ class ImageTransformCache {
7817
+ maxBytes;
7818
+ maxEntries;
7819
+ entries = new Map;
7820
+ inFlight = new Map;
7821
+ cachedBytes = 0;
7822
+ constructor(maxBytes = DEFAULT_MAX_CACHE_BYTES, maxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
7823
+ this.maxBytes = maxBytes;
7824
+ this.maxEntries = maxEntries;
7825
+ }
7826
+ async getOrTransform(key, operation) {
7827
+ if (key === null)
7828
+ return globalImageTransformSemaphore.run(operation);
7829
+ const cached = this.entries.get(key);
7830
+ if (cached) {
7831
+ this.entries.delete(key);
7832
+ this.entries.set(key, cached);
7833
+ return cached.transform;
7834
+ }
7835
+ const pending = this.inFlight.get(key);
7836
+ if (pending)
7837
+ return pending;
7838
+ const transform = globalImageTransformSemaphore.run(operation);
7839
+ this.inFlight.set(key, transform);
7840
+ try {
7841
+ const transformResult = await transform;
7842
+ if (transformResult.ok)
7843
+ this.store(key, transformResult);
7844
+ return transformResult;
7845
+ } finally {
7846
+ if (this.inFlight.get(key) === transform)
7847
+ this.inFlight.delete(key);
7848
+ }
7849
+ }
7850
+ store(key, transform) {
7851
+ const size = transform.bytes.byteLength;
7852
+ if (size > this.maxBytes || this.maxEntries === 0)
7853
+ return;
7854
+ const previous = this.entries.get(key);
7855
+ if (previous) {
7856
+ this.cachedBytes -= previous.size;
7857
+ this.entries.delete(key);
7858
+ }
7859
+ this.entries.set(key, { transform, size });
7860
+ this.cachedBytes += size;
7861
+ while (this.entries.size > this.maxEntries || this.cachedBytes > this.maxBytes) {
7862
+ const oldestKey = this.entries.keys().next().value;
7863
+ if (oldestKey === undefined)
7864
+ break;
7865
+ const oldest = this.entries.get(oldestKey);
7866
+ this.entries.delete(oldestKey);
7867
+ this.cachedBytes -= oldest.size;
7868
+ }
7869
+ }
7870
+ }
7871
+
6531
7872
  // src/runtime/storage/handler.ts
6532
7873
  var MAX_SIGNED_URL_EXPIRY = 7 * 24 * 60 * 60;
6533
7874
  function clampExpiry(expiresIn) {
@@ -6606,7 +7947,7 @@ var MAX_COMPLETED_TUS_UPLOADS = 64;
6606
7947
  var COMPLETED_TUS_RETENTION_MS = 60 * 60 * 1000;
6607
7948
  var PREFLIGHT_ROLLBACK = Symbol("storage-preflight-rollback");
6608
7949
  var INTERNAL_STORAGE_BUCKET = ".supacloud-lite";
6609
- var OBJECT_VERSION_PREFIX = "v2-";
7950
+ var OBJECT_VERSION_PREFIX2 = "v2-";
6610
7951
 
6611
7952
  class StorageHandler {
6612
7953
  db;
@@ -6614,6 +7955,7 @@ class StorageHandler {
6614
7955
  config;
6615
7956
  tusUploads = new Map;
6616
7957
  mutationTail = Promise.resolve();
7958
+ imageTransforms = new ImageTransformCache;
6617
7959
  constructor(db, driver, config) {
6618
7960
  this.db = db;
6619
7961
  this.driver = driver;
@@ -6656,15 +7998,21 @@ class StorageHandler {
6656
7998
  const bucket2 = parts[3];
6657
7999
  const key2 = parts.slice(4).join("/");
6658
8000
  if (kind === "public" && (method === "GET" || method === "HEAD")) {
6659
- const source = await this.downloadPublic(bucket2, key2, false);
8001
+ const source = await this.loadPublicObject(bucket2, key2);
8002
+ if (source instanceof Response)
8003
+ return source;
6660
8004
  return await this.transformImageResponse(source, url, method === "HEAD");
6661
8005
  }
6662
8006
  if (kind === "authenticated" && (method === "GET" || method === "HEAD")) {
6663
- const source = await this.download(ctx, bucket2, key2, false);
8007
+ const source = await this.loadAuthenticatedObject(ctx, bucket2, key2);
8008
+ if (source instanceof Response)
8009
+ return source;
6664
8010
  return await this.transformImageResponse(source, url, method === "HEAD");
6665
8011
  }
6666
8012
  if (kind === "sign" && method === "GET") {
6667
- const source = await this.redeemSignedUrl(url, bucket2, key2);
8013
+ const source = await this.loadSignedObject(url, bucket2, key2);
8014
+ if (source instanceof Response)
8015
+ return source;
6668
8016
  return await this.transformImageResponse(source, url, false);
6669
8017
  }
6670
8018
  return storageError(404, "not_found", `unknown render endpoint: ${rest}`);
@@ -6877,21 +8225,31 @@ class StorageHandler {
6877
8225
  throw e;
6878
8226
  }
6879
8227
  }
6880
- async transformImageResponse(source, url, head) {
6881
- if (!source.ok)
6882
- return source;
8228
+ async transformImageResponse(row, url, head) {
6883
8229
  const parsed = parseImageTransform(url.searchParams);
6884
8230
  if (!parsed.ok)
6885
8231
  return storageError(parsed.status, parsed.error, parsed.message);
6886
- const result = await transformImage(new Uint8Array(await source.arrayBuffer()), parsed.value);
8232
+ let result;
8233
+ try {
8234
+ result = await this.imageTransforms.getOrTransform(imageTransformCacheKey(row.version, parsed.value), async () => {
8235
+ const source = await this.readObjectSource(row);
8236
+ if (source === null)
8237
+ throw new StorageObjectMissingError;
8238
+ return transformImage(source, parsed.value, objectSize(row));
8239
+ });
8240
+ } catch (error) {
8241
+ if (error instanceof StorageObjectMissingError) {
8242
+ return storageError(404, "not_found", "Object not found");
8243
+ }
8244
+ throw error;
8245
+ }
6887
8246
  if (!result.ok)
6888
8247
  return storageError(result.status, result.error, result.message);
6889
- const headers = new Headers(source.headers);
8248
+ const headers = objectHeaders(row, result.bytes.length);
6890
8249
  headers.delete("content-disposition");
6891
8250
  headers.delete("etag");
6892
8251
  headers.set("content-type", result.contentType);
6893
- headers.set("content-length", String(result.bytes.length));
6894
- return new Response(head ? null : result.bytes, { status: source.status, headers });
8252
+ return new Response(head ? null : result.bytes, { status: 200, headers });
6895
8253
  }
6896
8254
  async persistObject(ctx, bucketId, key, bytes, contentType, cacheControl, upsert) {
6897
8255
  const metadata = objectMetadata(bytes.length, contentType, cacheControl);
@@ -7161,13 +8519,25 @@ class StorageHandler {
7161
8519
  return null;
7162
8520
  }
7163
8521
  async download(ctx, bucketId, key, head) {
8522
+ const row = await this.loadAuthenticatedObject(ctx, bucketId, key);
8523
+ if (row instanceof Response)
8524
+ return row;
8525
+ return this.serveObject(row, head);
8526
+ }
8527
+ async loadAuthenticatedObject(ctx, bucketId, key) {
7164
8528
  const res = await this.db.withContext(ctx, (q) => q(`select * from storage.objects where bucket_id = $1 and name = $2`, [bucketId, key]));
7165
8529
  const row = res.rows[0];
7166
8530
  if (!row)
7167
8531
  return storageError(404, "not_found", "Object not found");
7168
- return this.serveObject(row, head);
8532
+ return row;
7169
8533
  }
7170
8534
  async downloadPublic(bucketId, key, head) {
8535
+ const row = await this.loadPublicObject(bucketId, key);
8536
+ if (row instanceof Response)
8537
+ return row;
8538
+ return this.serveObject(row, head);
8539
+ }
8540
+ async loadPublicObject(bucketId, key) {
7171
8541
  const bucket = await this.loadBucket(bucketId);
7172
8542
  if (!bucket?.public)
7173
8543
  return storageError(400, "not_found", "Bucket is not public");
@@ -7178,24 +8548,16 @@ class StorageHandler {
7178
8548
  const row = res.rows[0];
7179
8549
  if (!row)
7180
8550
  return storageError(404, "not_found", "Object not found");
7181
- return this.serveObject(row, head);
8551
+ return row;
7182
8552
  }
7183
8553
  async serveObject(row, head) {
7184
8554
  const bytes = await this.readObjectBytes(row);
7185
8555
  if (bytes === null)
7186
8556
  return storageError(404, "not_found", "Object not found");
7187
- const meta = row.metadata ?? {};
7188
- const contentType = String(meta.mimetype ?? "application/octet-stream");
7189
- const headers = {
7190
- "content-type": contentType,
7191
- "content-length": String(bytes.length),
7192
- "cache-control": String(meta.cacheControl ?? "no-cache"),
7193
- etag: String(meta.eTag ?? '""'),
7194
- "last-modified": new Date(String(meta.lastModified ?? Date.now())).toUTCString(),
7195
- "x-content-type-options": "nosniff"
7196
- };
8557
+ const contentType = String(row.metadata?.mimetype ?? "application/octet-stream");
8558
+ const headers = objectHeaders(row, bytes.length);
7197
8559
  if (isRenderableActiveType(contentType))
7198
- headers["content-disposition"] = "attachment";
8560
+ headers.set("content-disposition", "attachment");
7199
8561
  return new Response(head ? null : bytes, { status: 200, headers });
7200
8562
  }
7201
8563
  async removeObjects(req, ctx, bucketId) {
@@ -7306,9 +8668,11 @@ class StorageHandler {
7306
8668
  throw error;
7307
8669
  }
7308
8670
  async readObjectBytes(row) {
7309
- if (isVersionedObjectVersion(row.version))
7310
- return this.driver.get(objectVersionKey(row.version));
7311
- return this.driver.get(legacyObjectKey(row));
8671
+ return this.driver.get(storageKey(row));
8672
+ }
8673
+ async readObjectSource(row) {
8674
+ const key = storageKey(row);
8675
+ return this.driver.getBlob ? this.driver.getBlob(key) : this.driver.get(key);
7312
8676
  }
7313
8677
  async cleanupObjectRows(rows) {
7314
8678
  const keys = rows.flatMap((row) => [
@@ -7411,6 +8775,12 @@ class StorageHandler {
7411
8775
  return json3(200, out);
7412
8776
  }
7413
8777
  async redeemSignedUrl(url, bucketId, key) {
8778
+ const row = await this.loadSignedObject(url, bucketId, key);
8779
+ if (row instanceof Response)
8780
+ return row;
8781
+ return this.serveObject(row, false);
8782
+ }
8783
+ async loadSignedObject(url, bucketId, key) {
7414
8784
  const token = url.searchParams.get("token") ?? "";
7415
8785
  const claims = await verifyJwt(token, this.config.jwtSecret);
7416
8786
  if (!claims || claims.url !== `${bucketId}/${key}` || claims.type !== "download") {
@@ -7423,7 +8793,7 @@ class StorageHandler {
7423
8793
  const row = res.rows[0];
7424
8794
  if (!row)
7425
8795
  return storageError(404, "not_found", "Object not found");
7426
- return this.serveObject(row, false);
8796
+ return row;
7427
8797
  }
7428
8798
  async signUploadUrl(ctx, bucketId, key) {
7429
8799
  const keyErr = invalidObjectKey(key);
@@ -7481,6 +8851,9 @@ function objectJson(r) {
7481
8851
 
7482
8852
  class StorageValidationError extends Error {
7483
8853
  }
8854
+
8855
+ class StorageObjectMissingError extends Error {
8856
+ }
7484
8857
  function parseSizeLimit(v) {
7485
8858
  if (v === null || v === undefined || v === "")
7486
8859
  return null;
@@ -7503,14 +8876,32 @@ function objectMetadata(size, contentType, cacheControl) {
7503
8876
  httpStatusCode: 200
7504
8877
  };
7505
8878
  }
8879
+ function objectHeaders(row, contentLength) {
8880
+ const metadata = row.metadata ?? {};
8881
+ return new Headers({
8882
+ "content-type": String(metadata.mimetype ?? "application/octet-stream"),
8883
+ "content-length": String(contentLength),
8884
+ "cache-control": String(metadata.cacheControl ?? "no-cache"),
8885
+ etag: String(metadata.eTag ?? '""'),
8886
+ "last-modified": new Date(String(metadata.lastModified ?? Date.now())).toUTCString(),
8887
+ "x-content-type-options": "nosniff"
8888
+ });
8889
+ }
8890
+ function objectSize(row) {
8891
+ const size = Number(row.metadata?.size);
8892
+ return Number.isFinite(size) && size >= 0 ? size : undefined;
8893
+ }
7506
8894
  function objectVersionKey(version) {
7507
8895
  return `.supacloud-lite/objects/${version}`;
7508
8896
  }
8897
+ function storageKey(row) {
8898
+ return isVersionedObjectVersion(row.version) ? objectVersionKey(row.version) : legacyObjectKey(row);
8899
+ }
7509
8900
  function createObjectVersion() {
7510
- return `${OBJECT_VERSION_PREFIX}${crypto.randomUUID()}`;
8901
+ return `${OBJECT_VERSION_PREFIX2}${crypto.randomUUID()}`;
7511
8902
  }
7512
8903
  function isVersionedObjectVersion(version) {
7513
- return version?.startsWith(OBJECT_VERSION_PREFIX) ?? false;
8904
+ return version?.startsWith(OBJECT_VERSION_PREFIX2) ?? false;
7514
8905
  }
7515
8906
  function isInternalStorageBucket(bucketId) {
7516
8907
  return bucketId === INTERNAL_STORAGE_BUCKET || bucketId.startsWith(`${INTERNAL_STORAGE_BUCKET}/`);
@@ -8859,21 +10250,797 @@ function withCors(res) {
8859
10250
  }
8860
10251
 
8861
10252
  // src/runtime/node/db-diff.ts
8862
- import { mkdir as mkdir2, writeFile } from "fs/promises";
10253
+ import { mkdtempSync as mkdtempSync2 } from "fs";
10254
+ import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
10255
+ import { tmpdir as tmpdir2 } from "os";
10256
+ import { dirname as dirname2, join as join2 } from "path";
10257
+
10258
+ // src/runtime/node/native/engine.ts
10259
+ import { execFileSync, spawn } from "child_process";
10260
+ import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
10261
+ import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
10262
+ import { writeFile } from "fs/promises";
10263
+ import { homedir, tmpdir } from "os";
8863
10264
  import { join } from "path";
10265
+ import { extract as extractTar } from "tar";
10266
+
10267
+ // src/runtime/node/native/wire.ts
10268
+ import { createConnection } from "net";
10269
+ import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
10270
+
10271
+ class PgWireError extends Error {
10272
+ code;
10273
+ detail;
10274
+ hint;
10275
+ severity;
10276
+ constructor(fields) {
10277
+ super(fields.get("M") ?? "postgres error");
10278
+ this.code = fields.get("C");
10279
+ this.detail = fields.get("D");
10280
+ this.hint = fields.get("H");
10281
+ this.severity = fields.get("S");
10282
+ }
10283
+ }
10284
+
10285
+ class PgWireClient {
10286
+ socket;
10287
+ buffer = Buffer.alloc(0);
10288
+ pending = null;
10289
+ queue = Promise.resolve();
10290
+ closed = false;
10291
+ onNotification = null;
10292
+ static async connect(opts) {
10293
+ const client = new PgWireClient;
10294
+ await client.open(opts);
10295
+ return client;
10296
+ }
10297
+ open(opts) {
10298
+ return new Promise((resolve2, reject) => {
10299
+ this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
10300
+ this.socket.on("error", (e) => {
10301
+ if (this.pending)
10302
+ this.pending.reject(e);
10303
+ reject(e);
10304
+ });
10305
+ this.socket.on("close", () => {
10306
+ this.closed = true;
10307
+ this.pending?.reject(new Error("connection closed"));
10308
+ });
10309
+ this.socket.on("connect", () => {
10310
+ const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
10311
+ const body = Buffer.from(params, "utf8");
10312
+ const msg = Buffer.alloc(8 + body.length);
10313
+ msg.writeInt32BE(8 + body.length, 0);
10314
+ msg.writeInt32BE(196608, 4);
10315
+ body.copy(msg, 8);
10316
+ this.socket.write(msg);
10317
+ });
10318
+ let clientNonce = "";
10319
+ let clientFirstBare = "";
10320
+ let serverSignature = "";
10321
+ const needPassword = () => {
10322
+ if (opts.password == null) {
10323
+ reject(new Error("the server requested a password but none was provided"));
10324
+ return false;
10325
+ }
10326
+ return true;
10327
+ };
10328
+ const startupHandler = (chunk) => {
10329
+ this.buffer = Buffer.concat([this.buffer, chunk]);
10330
+ let msg;
10331
+ while ((msg = this.nextMessage()) !== null) {
10332
+ const [type, payload] = msg;
10333
+ if (type === 82) {
10334
+ const code = payload.readInt32BE(0);
10335
+ if (code === 0) {} else if (code === 3) {
10336
+ if (!needPassword())
10337
+ return;
10338
+ this.socket.write(message(112, cstring(opts.password)));
10339
+ } else if (code === 5) {
10340
+ if (!needPassword())
10341
+ return;
10342
+ const salt = payload.subarray(4, 8);
10343
+ const inner = md5Hex(Buffer.from(opts.password + opts.user, "utf8"));
10344
+ const token = "md5" + md5Hex(Buffer.concat([Buffer.from(inner, "utf8"), salt]));
10345
+ this.socket.write(message(112, cstring(token)));
10346
+ } else if (code === 10) {
10347
+ if (!needPassword())
10348
+ return;
10349
+ const mechs = payload.subarray(4).toString("utf8").split("\x00").filter(Boolean);
10350
+ if (!mechs.includes("SCRAM-SHA-256")) {
10351
+ reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(", ")})`));
10352
+ return;
10353
+ }
10354
+ clientNonce = randomBytes(18).toString("base64");
10355
+ clientFirstBare = `n=,r=${clientNonce}`;
10356
+ const initial = Buffer.from(`n,,${clientFirstBare}`, "utf8");
10357
+ this.socket.write(message(112, Buffer.concat([cstring("SCRAM-SHA-256"), int32(initial.length), initial])));
10358
+ } else if (code === 11) {
10359
+ const serverFirst = payload.subarray(4).toString("utf8");
10360
+ const attrs = scramAttrs(serverFirst);
10361
+ if (!attrs.r?.startsWith(clientNonce)) {
10362
+ reject(new Error("SCRAM: server nonce does not extend client nonce"));
10363
+ return;
10364
+ }
10365
+ const salt = Buffer.from(attrs.s, "base64");
10366
+ const iterations = parseInt(attrs.i, 10);
10367
+ const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, "sha256");
10368
+ const clientKey = hmac(saltedPassword, "Client Key");
10369
+ const storedKey = sha256(clientKey);
10370
+ const finalNoProof = `c=biws,r=${attrs.r}`;
10371
+ const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
10372
+ const clientSignature = hmac(storedKey, authMessage);
10373
+ const proof = xorBuffers(clientKey, clientSignature);
10374
+ serverSignature = hmac(hmac(saltedPassword, "Server Key"), authMessage).toString("base64");
10375
+ const clientFinal = `${finalNoProof},p=${proof.toString("base64")}`;
10376
+ this.socket.write(message(112, Buffer.from(clientFinal, "utf8")));
10377
+ } else if (code === 12) {
10378
+ const v = scramAttrs(payload.subarray(4).toString("utf8")).v;
10379
+ if (v && serverSignature && v !== serverSignature) {
10380
+ reject(new Error("SCRAM: server signature verification failed"));
10381
+ return;
10382
+ }
10383
+ } else {
10384
+ reject(new Error(`unsupported auth method ${code}`));
10385
+ return;
10386
+ }
10387
+ } else if (type === 69) {
10388
+ reject(new PgWireError(parseErrorFields(payload)));
10389
+ return;
10390
+ } else if (type === 90) {
10391
+ this.socket.off("data", startupHandler);
10392
+ this.socket.on("data", (c) => {
10393
+ this.buffer = Buffer.concat([this.buffer, c]);
10394
+ this.processMessages();
10395
+ });
10396
+ resolve2();
10397
+ return;
10398
+ }
10399
+ }
10400
+ };
10401
+ this.socket.on("data", startupHandler);
10402
+ });
10403
+ }
10404
+ run(send) {
10405
+ const op = this.queue.then(() => new Promise((resolve2, reject) => {
10406
+ if (this.closed)
10407
+ return reject(new Error("connection closed"));
10408
+ this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
10409
+ send();
10410
+ }));
10411
+ this.queue = op.catch(() => {});
10412
+ return op;
10413
+ }
10414
+ async exec(sql) {
10415
+ return this.run(() => this.socket.write(message(81, cstring(sql))));
10416
+ }
10417
+ async query(sql, params = []) {
10418
+ const results = await this.run(() => {
10419
+ const parse = message(80, Buffer.concat([cstring(""), cstring(sql), int16(0)]));
10420
+ const paramBufs = [int16(0), int16(params.length)];
10421
+ for (const p of params) {
10422
+ if (p === null || p === undefined) {
10423
+ paramBufs.push(int32(-1));
10424
+ } else {
10425
+ const b = Buffer.from(String(p), "utf8");
10426
+ paramBufs.push(int32(b.length), b);
10427
+ }
10428
+ }
10429
+ paramBufs.push(int16(0));
10430
+ const bind = message(66, Buffer.concat([cstring(""), cstring(""), ...paramBufs]));
10431
+ const describe = message(68, Buffer.concat([Buffer.from("P"), cstring("")]));
10432
+ const execute = message(69, Buffer.concat([cstring(""), int32(0)]));
10433
+ const sync = message(83, Buffer.alloc(0));
10434
+ this.socket.write(Buffer.concat([parse, bind, describe, execute, sync]));
10435
+ });
10436
+ return results[0] ?? { rows: [] };
10437
+ }
10438
+ close() {
10439
+ return new Promise((resolve2) => {
10440
+ if (this.closed)
10441
+ return resolve2();
10442
+ this.socket.write(message(88, Buffer.alloc(0)));
10443
+ this.socket.end(() => resolve2());
10444
+ });
10445
+ }
10446
+ nextMessage() {
10447
+ if (this.buffer.length < 5)
10448
+ return null;
10449
+ const type = this.buffer[0];
10450
+ const length = this.buffer.readInt32BE(1);
10451
+ if (this.buffer.length < 1 + length)
10452
+ return null;
10453
+ const payload = this.buffer.subarray(5, 1 + length);
10454
+ this.buffer = this.buffer.subarray(1 + length);
10455
+ return [type, Buffer.from(payload)];
10456
+ }
10457
+ processMessages() {
10458
+ let msg;
10459
+ while ((msg = this.nextMessage()) !== null) {
10460
+ const [type, payload] = msg;
10461
+ const p = this.pending;
10462
+ switch (type) {
10463
+ case 84: {
10464
+ if (!p)
10465
+ break;
10466
+ const count = payload.readInt16BE(0);
10467
+ let off = 2;
10468
+ const columns = [];
10469
+ for (let i = 0;i < count; i++) {
10470
+ const end = payload.indexOf(0, off);
10471
+ const name = payload.toString("utf8", off, end);
10472
+ off = end + 1;
10473
+ const typeOid = payload.readInt32BE(off + 6);
10474
+ off += 18;
10475
+ columns.push({ name, typeOid });
10476
+ }
10477
+ p.columns = columns;
10478
+ break;
10479
+ }
10480
+ case 68: {
10481
+ if (!p)
10482
+ break;
10483
+ const count = payload.readInt16BE(0);
10484
+ let off = 2;
10485
+ const row = {};
10486
+ for (let i = 0;i < count; i++) {
10487
+ const len = payload.readInt32BE(off);
10488
+ off += 4;
10489
+ let value = null;
10490
+ if (len >= 0) {
10491
+ value = decodeValue(payload.toString("utf8", off, off + len), p.columns[i]?.typeOid ?? 25);
10492
+ off += len;
10493
+ }
10494
+ row[p.columns[i]?.name ?? `col${i}`] = value;
10495
+ }
10496
+ if (p.results.length === 0)
10497
+ p.results.push({ rows: [] });
10498
+ p.results[p.results.length - 1].rows.push(row);
10499
+ break;
10500
+ }
10501
+ case 67: {
10502
+ if (!p)
10503
+ break;
10504
+ const tag = payload.toString("utf8", 0, payload.length - 1);
10505
+ const parts = tag.split(" ");
10506
+ const affected = parseInt(parts[parts.length - 1], 10);
10507
+ if (p.results.length === 0)
10508
+ p.results.push({ rows: [] });
10509
+ const current = p.results[p.results.length - 1];
10510
+ if (!Number.isNaN(affected))
10511
+ current.affectedRows = affected;
10512
+ p.results.push({ rows: [] });
10513
+ p.columns = [];
10514
+ break;
10515
+ }
10516
+ case 69: {
10517
+ if (p)
10518
+ p.error = new PgWireError(parseErrorFields(payload));
10519
+ break;
10520
+ }
10521
+ case 65: {
10522
+ payload.readInt32BE(0);
10523
+ const channelEnd = payload.indexOf(0, 4);
10524
+ const channel = payload.toString("utf8", 4, channelEnd);
10525
+ const payloadEnd = payload.indexOf(0, channelEnd + 1);
10526
+ const body = payload.toString("utf8", channelEnd + 1, payloadEnd);
10527
+ this.onNotification?.(channel, body);
10528
+ break;
10529
+ }
10530
+ case 90: {
10531
+ if (!p)
10532
+ break;
10533
+ this.pending = null;
10534
+ if (p.error)
10535
+ p.reject(p.error);
10536
+ else {
10537
+ const results = p.results.filter((r, i) => i < p.results.length - 1 || r.rows.length > 0 || r.affectedRows !== undefined);
10538
+ p.resolve(results.length > 0 ? results : [{ rows: [] }]);
10539
+ }
10540
+ break;
10541
+ }
10542
+ }
10543
+ }
10544
+ }
10545
+ }
10546
+ var hmac = (key, data) => createHmac("sha256", key).update(data, "utf8").digest();
10547
+ var sha256 = (b) => createHash("sha256").update(b).digest();
10548
+ var md5Hex = (b) => createHash("md5").update(b).digest("hex");
10549
+ function xorBuffers(a, b) {
10550
+ const out = Buffer.alloc(a.length);
10551
+ for (let i = 0;i < a.length; i++)
10552
+ out[i] = a[i] ^ b[i];
10553
+ return out;
10554
+ }
10555
+ function scramAttrs(s) {
10556
+ const out = {};
10557
+ for (const part of s.split(",")) {
10558
+ const eq = part.indexOf("=");
10559
+ if (eq > 0)
10560
+ out[part.slice(0, eq)] = part.slice(eq + 1);
10561
+ }
10562
+ return out;
10563
+ }
10564
+ function message(type, body) {
10565
+ const out = Buffer.alloc(5 + body.length);
10566
+ out[0] = type;
10567
+ out.writeInt32BE(4 + body.length, 1);
10568
+ body.copy(out, 5);
10569
+ return out;
10570
+ }
10571
+ var cstring = (s) => Buffer.from(s + "\x00", "utf8");
10572
+ var int16 = (n) => {
10573
+ const b = Buffer.alloc(2);
10574
+ b.writeInt16BE(n);
10575
+ return b;
10576
+ };
10577
+ var int32 = (n) => {
10578
+ const b = Buffer.alloc(4);
10579
+ b.writeInt32BE(n);
10580
+ return b;
10581
+ };
10582
+ function parseErrorFields(payload) {
10583
+ const fields = new Map;
10584
+ let off = 0;
10585
+ while (off < payload.length && payload[off] !== 0) {
10586
+ const key = String.fromCharCode(payload[off]);
10587
+ const end = payload.indexOf(0, off + 1);
10588
+ fields.set(key, payload.toString("utf8", off + 1, end));
10589
+ off = end + 1;
10590
+ }
10591
+ return fields;
10592
+ }
10593
+ function decodeValue(text, oid) {
10594
+ switch (oid) {
10595
+ case 16:
10596
+ return text === "t";
10597
+ case 20: {
10598
+ const n = Number(text);
10599
+ return Number.isSafeInteger(n) ? n : text;
10600
+ }
10601
+ case 21:
10602
+ case 23:
10603
+ case 26:
10604
+ return Number(text);
10605
+ case 700:
10606
+ case 701:
10607
+ return Number(text);
10608
+ case 114:
10609
+ case 3802:
10610
+ return JSON.parse(text);
10611
+ case 1114:
10612
+ return new Date(text.replace(" ", "T") + "Z");
10613
+ case 1184: {
10614
+ let iso3 = text.replace(" ", "T");
10615
+ if (/[+-]\d\d$/.test(iso3))
10616
+ iso3 += ":00";
10617
+ return new Date(iso3);
10618
+ }
10619
+ case 1000:
10620
+ return parsePgArray(text).map((v) => v === "t");
10621
+ case 1007:
10622
+ return parsePgArray(text).map((v) => v === null ? null : Number(v));
10623
+ case 1016:
10624
+ return parsePgArray(text).map((v) => {
10625
+ if (v === null)
10626
+ return null;
10627
+ const n = Number(v);
10628
+ return Number.isSafeInteger(n) ? n : v;
10629
+ });
10630
+ case 1003:
10631
+ case 1009:
10632
+ case 1015:
10633
+ return parsePgArray(text);
10634
+ default:
10635
+ return text;
10636
+ }
10637
+ }
10638
+ function parsePgArray(text) {
10639
+ const out = [];
10640
+ if (text.length < 2)
10641
+ return out;
10642
+ let i = 1;
10643
+ while (i < text.length - 1) {
10644
+ if (text[i] === ",") {
10645
+ i++;
10646
+ continue;
10647
+ }
10648
+ if (text[i] === '"') {
10649
+ let value = "";
10650
+ i++;
10651
+ while (text[i] !== '"') {
10652
+ if (text[i] === "\\")
10653
+ i++;
10654
+ value += text[i++];
10655
+ }
10656
+ i++;
10657
+ out.push(value);
10658
+ } else {
10659
+ let value = "";
10660
+ while (i < text.length - 1 && text[i] !== ",")
10661
+ value += text[i++];
10662
+ out.push(value === "NULL" ? null : value);
10663
+ }
10664
+ }
10665
+ return out;
10666
+ }
10667
+
10668
+ // src/runtime/db/engine.ts
10669
+ class Mutex {
10670
+ tail = Promise.resolve();
10671
+ async lock() {
10672
+ let release;
10673
+ const next = new Promise((r) => release = r);
10674
+ const prev = this.tail;
10675
+ this.tail = this.tail.then(() => next);
10676
+ await prev;
10677
+ return release;
10678
+ }
10679
+ async run(fn) {
10680
+ const release = await this.lock();
10681
+ try {
10682
+ return await fn();
10683
+ } finally {
10684
+ release();
10685
+ }
10686
+ }
10687
+ }
10688
+
10689
+ // src/runtime/node/native/wire-engine.ts
10690
+ async function buildWireEngine(options) {
10691
+ const queryClient = await options.connect();
10692
+ let listenerClient;
10693
+ try {
10694
+ listenerClient = await options.connect();
10695
+ } catch (error) {
10696
+ await queryClient.close().catch(() => {});
10697
+ throw error;
10698
+ }
10699
+ const queryMutex = new Mutex;
10700
+ const listenerMutex = new Mutex;
10701
+ const listeners = new Map;
10702
+ listenerClient.onNotification = (channel, payload) => {
10703
+ for (const listener of listeners.get(channel) ?? [])
10704
+ listener(payload);
10705
+ };
10706
+ const transactionClient = {
10707
+ async query(sql, params) {
10708
+ const queryResult = await queryClient.query(sql, normalizeParams(params));
10709
+ return { rows: queryResult.rows, affectedRows: queryResult.affectedRows };
10710
+ },
10711
+ async exec(sql) {
10712
+ await queryClient.exec(sql);
10713
+ }
10714
+ };
10715
+ let closePromise = null;
10716
+ return {
10717
+ query(sql, params) {
10718
+ return queryMutex.run(() => transactionClient.query(sql, params));
10719
+ },
10720
+ exec(sql) {
10721
+ return queryMutex.run(() => transactionClient.exec(sql));
10722
+ },
10723
+ transaction(callback) {
10724
+ return queryMutex.run(async () => {
10725
+ await queryClient.exec("begin");
10726
+ try {
10727
+ const response = await callback(transactionClient);
10728
+ await queryClient.exec("commit");
10729
+ return response;
10730
+ } catch (error) {
10731
+ await queryClient.exec("rollback").catch(() => {});
10732
+ throw error;
10733
+ }
10734
+ });
10735
+ },
10736
+ async listen(channel, listener) {
10737
+ return listenerMutex.run(async () => {
10738
+ let channelListeners = listeners.get(channel);
10739
+ if (!channelListeners) {
10740
+ channelListeners = new Set;
10741
+ await listenerClient.exec(`listen "${channel.replaceAll('"', '""')}"`);
10742
+ listeners.set(channel, channelListeners);
10743
+ }
10744
+ channelListeners.add(listener);
10745
+ return () => {
10746
+ channelListeners.delete(listener);
10747
+ };
10748
+ });
10749
+ },
10750
+ close() {
10751
+ closePromise ??= closeWireEngine(queryClient, listenerClient, options.onClose);
10752
+ return closePromise;
10753
+ }
10754
+ };
10755
+ }
10756
+ async function closeWireEngine(queryClient, listenerClient, onClose) {
10757
+ const closeResults = await Promise.allSettled([queryClient.close(), listenerClient.close()]);
10758
+ let engineCleanupError;
10759
+ try {
10760
+ await onClose?.();
10761
+ } catch (error) {
10762
+ engineCleanupError = error;
10763
+ }
10764
+ const connectionErrors = closeResults.flatMap((closeResult) => closeResult.status === "rejected" ? [closeResult.reason] : []);
10765
+ if (engineCleanupError !== undefined)
10766
+ connectionErrors.push(engineCleanupError);
10767
+ if (connectionErrors.length > 0)
10768
+ throw new AggregateError(connectionErrors, "native database cleanup failed");
10769
+ }
10770
+ function normalizeParams(params) {
10771
+ return params?.map((parameter) => {
10772
+ if (parameter === null || parameter === undefined)
10773
+ return null;
10774
+ if (Array.isArray(parameter))
10775
+ return toPgArrayLiteral(parameter);
10776
+ if (parameter instanceof Date)
10777
+ return parameter.toISOString();
10778
+ if (typeof parameter === "object")
10779
+ return JSON.stringify(parameter);
10780
+ return parameter;
10781
+ });
10782
+ }
10783
+ function toPgArrayLiteral(array) {
10784
+ const encoded = array.map((element) => {
10785
+ if (element === null || element === undefined)
10786
+ return "NULL";
10787
+ if (Array.isArray(element))
10788
+ return toPgArrayLiteral(element);
10789
+ if (typeof element === "number" || typeof element === "boolean")
10790
+ return String(element);
10791
+ const text = typeof element === "object" ? JSON.stringify(element) : String(element);
10792
+ return `"${text.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
10793
+ });
10794
+ return `{${encoded.join(",")}}`;
10795
+ }
10796
+
10797
+ // src/runtime/node/native/engine.ts
10798
+ var DEFAULT_PG_VERSION = "17.7.0";
10799
+ var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
10800
+ function isNativeEngineSupported() {
10801
+ return (process.platform === "darwin" || process.platform === "linux") && (process.arch === "arm64" || process.arch === "x64") && (process.platform !== "linux" || isGlibcLinux());
10802
+ }
10803
+ function isGlibcLinux() {
10804
+ try {
10805
+ const version = execFileSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
10806
+ return /glibc|gnu libc/i.test(version);
10807
+ } catch {
10808
+ return false;
10809
+ }
10810
+ }
10811
+ function target() {
10812
+ const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
10813
+ if (!arch)
10814
+ throw new Error(`unsupported architecture for native engine: ${process.arch}`);
10815
+ if (process.platform === "darwin")
10816
+ return `${arch}-apple-darwin`;
10817
+ if (process.platform === "linux")
10818
+ return `${arch}-unknown-linux-gnu`;
10819
+ throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
10820
+ }
10821
+ function isCompleteInstall(dir) {
10822
+ return existsSync(join(dir, "bin", "postgres")) && existsSync(join(dir, "share", "postgres.bki"));
10823
+ }
10824
+ var PINNED_SHA256 = {
10825
+ "postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
10826
+ "postgresql-17.7.0-aarch64-unknown-linux-gnu": "89cc2f089880cc8e5e6b7a29387829ec4e4779427855bc0b9fa187c8fce33c8b",
10827
+ "postgresql-17.7.0-x86_64-apple-darwin": "0dd8c25173524bad4ae8ef6b970da1ac40f4c1f231150c416ccb8cd06feff8f2",
10828
+ "postgresql-17.7.0-aarch64-apple-darwin": "727ac08d20a704014a0d51eb3300aa0c8e292c1cf0a1c99d4f4b1002e1420220"
10829
+ };
10830
+ async function verifyTarball(tarball, key, url) {
10831
+ const actual = createHash2("sha256").update(readFileSync(tarball)).digest("hex");
10832
+ const pinned = PINNED_SHA256[key];
10833
+ if (pinned) {
10834
+ if (actual !== pinned) {
10835
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${pinned}, got ${actual}`);
10836
+ }
10837
+ return;
10838
+ }
10839
+ const res = await fetchRelease(`${url}.sha256`);
10840
+ if (!res.ok)
10841
+ throw new Error(`could not fetch checksum for ${key}: HTTP ${res.status}`);
10842
+ const expected = (await res.text()).trim().split(/\s+/)[0].toLowerCase();
10843
+ if (!/^[0-9a-f]{64}$/.test(expected))
10844
+ throw new Error(`malformed published checksum for ${key}`);
10845
+ if (actual !== expected) {
10846
+ throw new Error(`postgres binary checksum mismatch for ${key}: expected ${expected}, got ${actual}`);
10847
+ }
10848
+ }
10849
+ async function ensurePostgres(version = DEFAULT_PG_VERSION, cacheDir, log) {
10850
+ const t = target();
10851
+ const root = cacheDir ?? join(homedir(), ".cache", "supacloud-lite");
10852
+ const dir = join(root, `postgresql-${version}-${t}`);
10853
+ if (isCompleteInstall(dir))
10854
+ return dir;
10855
+ const url = `https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/postgresql-${version}-${t}.tar.gz`;
10856
+ mkdirSync(root, { recursive: true });
10857
+ const uniq = `${process.pid}-${randomBytes2(6).toString("hex")}`;
10858
+ const tarball = join(root, `pg-${version}-${uniq}.tar.gz`);
10859
+ const tmpDir = join(root, `.tmp-${version}-${t}-${uniq}`);
10860
+ try {
10861
+ if (isCompleteInstall(dir))
10862
+ return dir;
10863
+ log?.(`downloading postgres ${version} (${t})\u2026`);
10864
+ const res = await fetchRelease(url);
10865
+ if (!res.ok)
10866
+ throw new Error(`failed to download ${url}: HTTP ${res.status}`);
10867
+ await writeFile(tarball, Buffer.from(await res.arrayBuffer()));
10868
+ await verifyTarball(tarball, `postgresql-${version}-${t}`, url);
10869
+ mkdirSync(tmpDir, { recursive: true });
10870
+ await extractTar({ cwd: tmpDir, file: tarball, gzip: true, preserveOwner: false, strict: true, strip: 1 });
10871
+ if (!isCompleteInstall(tmpDir))
10872
+ throw new Error("postgres archive extracted incompletely");
10873
+ try {
10874
+ renameSync(tmpDir, dir);
10875
+ } catch {
10876
+ if (!isCompleteInstall(dir)) {
10877
+ rmSync(dir, { recursive: true, force: true });
10878
+ renameSync(tmpDir, dir);
10879
+ }
10880
+ }
10881
+ log?.(`postgres installed to ${dir}`);
10882
+ return dir;
10883
+ } finally {
10884
+ rmSync(tarball, { force: true });
10885
+ rmSync(tmpDir, { recursive: true, force: true });
10886
+ }
10887
+ }
10888
+ async function fetchRelease(url) {
10889
+ let lastError;
10890
+ for (let attempt = 1;attempt <= 3; attempt++) {
10891
+ try {
10892
+ const response = await fetch(url);
10893
+ if (response.ok || response.status < 500)
10894
+ return response;
10895
+ lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
10896
+ } catch (error) {
10897
+ lastError = error;
10898
+ }
10899
+ if (attempt < 3)
10900
+ await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
10901
+ }
10902
+ throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
10903
+ }
10904
+ var TUNED_CONF = `
10905
+ # supacloud-lite: memory-lean settings for an embedded, single-app Postgres
10906
+ listen_addresses = ''
10907
+ shared_buffers = 16MB
10908
+ dynamic_shared_memory_type = posix
10909
+ max_connections = 10
10910
+ wal_level = minimal
10911
+ max_wal_senders = 0
10912
+ logging_collector = off
10913
+ `;
10914
+ async function createNativeEngine(opts) {
10915
+ const releaseLock = await acquireDataDirLock(opts.dataDir, "native PostgreSQL");
10916
+ let socketDirectory;
10917
+ let postgres;
10918
+ let removeExitHandler;
10919
+ try {
10920
+ const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log);
10921
+ const bin = (name) => join(installDir, "bin", name);
10922
+ if (!existsSync(join(opts.dataDir, "PG_VERSION"))) {
10923
+ mkdirSync(opts.dataDir, { recursive: true });
10924
+ try {
10925
+ execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
10926
+ stdio: "pipe"
10927
+ });
10928
+ } catch (error) {
10929
+ const stderr = error.stderr?.toString() ?? "";
10930
+ throw new Error(`initdb failed:
10931
+ ${stderr || error.message}`);
10932
+ }
10933
+ appendFileSync(join(opts.dataDir, "postgresql.conf"), TUNED_CONF);
10934
+ }
10935
+ removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
10936
+ socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
10937
+ chmodSync(socketDirectory, 448);
10938
+ postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
10939
+ stdio: ["ignore", "ignore", "pipe"],
10940
+ detached: false
10941
+ });
10942
+ let postgresExited = false;
10943
+ let postgresStderr = "";
10944
+ postgres.stderr?.on("data", (chunk) => {
10945
+ postgresStderr = (postgresStderr + chunk.toString()).slice(-4000);
10946
+ });
10947
+ postgres.on("exit", () => postgresExited = true);
10948
+ const killPostgres = () => {
10949
+ if (!postgresExited)
10950
+ postgres?.kill("SIGTERM");
10951
+ };
10952
+ process.once("exit", killPostgres);
10953
+ removeExitHandler = () => process.off("exit", killPostgres);
10954
+ const socketPath = join(socketDirectory, ".s.PGSQL.5432");
10955
+ const connect = async () => {
10956
+ const deadline = Date.now() + 20000;
10957
+ while (Date.now() <= deadline) {
10958
+ try {
10959
+ return await PgWireClient.connect({ socketPath, user: "postgres", database: "postgres" });
10960
+ } catch (error) {
10961
+ if (postgresExited) {
10962
+ const detail = postgresStderr.trim();
10963
+ throw new Error(`embedded postgres failed to start${detail ? `:
10964
+ ${detail}` : " (no output)"}
10965
+
10966
+ ` + `data dir: ${opts.dataDir}
10967
+ ` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
10968
+ }
10969
+ await new Promise((resolve2) => setTimeout(resolve2, 150));
10970
+ }
10971
+ }
10972
+ throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
10973
+ };
10974
+ return await buildWireEngine({
10975
+ connect,
10976
+ onClose: async () => {
10977
+ removeExitHandler?.();
10978
+ await stopPostgres(postgres, () => postgresExited);
10979
+ rmSync(socketDirectory, { recursive: true, force: true });
10980
+ await releaseLock();
10981
+ }
10982
+ });
10983
+ } catch (error) {
10984
+ removeExitHandler?.();
10985
+ if (postgres)
10986
+ await stopPostgres(postgres, () => postgres.exitCode !== null);
10987
+ if (socketDirectory)
10988
+ rmSync(socketDirectory, { recursive: true, force: true });
10989
+ await releaseLock();
10990
+ throw error;
10991
+ }
10992
+ }
10993
+ async function stopPostgres(postgres, hasExited) {
10994
+ if (hasExited())
10995
+ return;
10996
+ postgres.kill("SIGINT");
10997
+ await new Promise((resolve2) => {
10998
+ const killTimeout = setTimeout(() => {
10999
+ postgres.kill("SIGKILL");
11000
+ resolve2();
11001
+ }, 5000);
11002
+ postgres.once("exit", () => {
11003
+ clearTimeout(killTimeout);
11004
+ resolve2();
11005
+ });
11006
+ });
11007
+ }
11008
+ function removeStalePidFile(pidPath) {
11009
+ if (!existsSync(pidPath))
11010
+ return;
11011
+ try {
11012
+ const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
11013
+ `)[0]?.trim() ?? "", 10);
11014
+ if (!pid) {
11015
+ rmSync(pidPath, { force: true });
11016
+ return;
11017
+ }
11018
+ try {
11019
+ process.kill(pid, 0);
11020
+ } catch {
11021
+ rmSync(pidPath, { force: true });
11022
+ }
11023
+ } catch {}
11024
+ }
11025
+
11026
+ // src/runtime/node/db-diff.ts
8864
11027
  async function computeDbDiff(opts) {
8865
11028
  const schema = opts.schema ?? "public";
8866
- const shadow = await createBackend({
8867
- engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
8868
- migrations: opts.migrations,
8869
- startRuntimeServices: false
8870
- });
11029
+ let shadow;
8871
11030
  let live;
11031
+ let unclaimedLiveEngine = opts.liveEngine;
8872
11032
  let operationFailed = false;
8873
11033
  try {
11034
+ shadow = await createBackend({
11035
+ engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
11036
+ migrations: opts.migrations,
11037
+ startRuntimeServices: false
11038
+ });
11039
+ const liveEngine = unclaimedLiveEngine;
11040
+ unclaimedLiveEngine = undefined;
8874
11041
  live = await createBackend({
8875
- engine: opts.liveEngine,
8876
- dataDir: opts.liveEngine ? undefined : opts.liveDataDir,
11042
+ engine: liveEngine,
11043
+ dataDir: liveEngine ? undefined : opts.liveDataDir,
8877
11044
  migrations: opts.migrations,
8878
11045
  startRuntimeServices: false
8879
11046
  });
@@ -8885,26 +11052,57 @@ async function computeDbDiff(opts) {
8885
11052
  throw error;
8886
11053
  } finally {
8887
11054
  try {
8888
- await closeBackends(live, shadow);
11055
+ await closeResources(unclaimedLiveEngine, live, shadow);
8889
11056
  } catch (error) {
8890
11057
  if (!operationFailed)
8891
11058
  throw error;
8892
11059
  }
8893
11060
  }
8894
11061
  }
11062
+ function shadowNativeDataDir() {
11063
+ return join2(mkdtempSync2(join2(tmpdir2(), "supacloud-lite-shadow-")), "pg");
11064
+ }
11065
+ async function createTemporaryNativeEngine() {
11066
+ const dataDir = shadowNativeDataDir();
11067
+ let engine;
11068
+ try {
11069
+ engine = await createNativeEngine({ dataDir });
11070
+ } catch (error) {
11071
+ try {
11072
+ await rm(dirname2(dataDir), { recursive: true, force: true });
11073
+ } catch (cleanupError) {
11074
+ throw new AggregateError([error, cleanupError], "temporary native database initialization cleanup failed");
11075
+ }
11076
+ throw error;
11077
+ }
11078
+ return {
11079
+ ...engine,
11080
+ async close() {
11081
+ try {
11082
+ await engine.close();
11083
+ } finally {
11084
+ await rm(dirname2(dataDir), { recursive: true, force: true });
11085
+ }
11086
+ }
11087
+ };
11088
+ }
8895
11089
  async function pullSchema(opts) {
8896
11090
  const schema = opts.schema ?? "public";
8897
- const shadow = await createBackend({
8898
- engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
8899
- migrations: opts.migrations,
8900
- startRuntimeServices: false
8901
- });
11091
+ let shadow;
8902
11092
  let live;
11093
+ let unclaimedLiveEngine = opts.liveEngine;
8903
11094
  let operationFailed = false;
8904
11095
  try {
11096
+ shadow = await createBackend({
11097
+ engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
11098
+ migrations: opts.migrations,
11099
+ startRuntimeServices: false
11100
+ });
11101
+ const liveEngine = unclaimedLiveEngine;
11102
+ unclaimedLiveEngine = undefined;
8905
11103
  live = await createBackend({
8906
- engine: opts.liveEngine,
8907
- dataDir: opts.liveEngine ? undefined : opts.liveDataDir,
11104
+ engine: liveEngine,
11105
+ dataDir: liveEngine ? undefined : opts.liveDataDir,
8908
11106
  migrations: opts.migrations,
8909
11107
  startRuntimeServices: false
8910
11108
  });
@@ -8920,8 +11118,8 @@ async function pullSchema(opts) {
8920
11118
  let path = null;
8921
11119
  if (opts.migrationsDir) {
8922
11120
  await mkdir2(opts.migrationsDir, { recursive: true });
8923
- path = join(opts.migrationsDir, `${stamp}_${name}.sql`);
8924
- await writeFile(path, body);
11121
+ path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
11122
+ await writeFile2(path, body);
8925
11123
  }
8926
11124
  await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
8927
11125
  values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
@@ -8931,15 +11129,15 @@ async function pullSchema(opts) {
8931
11129
  throw error;
8932
11130
  } finally {
8933
11131
  try {
8934
- await closeBackends(live, shadow);
11132
+ await closeResources(unclaimedLiveEngine, live, shadow);
8935
11133
  } catch (error) {
8936
11134
  if (!operationFailed)
8937
11135
  throw error;
8938
11136
  }
8939
11137
  }
8940
11138
  }
8941
- async function closeBackends(...backends) {
8942
- const results = await Promise.allSettled(backends.filter((backend) => backend !== undefined).map(async (backend) => await backend.close()));
11139
+ async function closeResources(...resources) {
11140
+ const results = await Promise.allSettled(resources.filter((resource) => resource !== undefined).map(async (resource) => await resource.close()));
8943
11141
  const failed = results.find((result) => result.status === "rejected");
8944
11142
  if (failed?.status === "rejected")
8945
11143
  throw failed.reason;
@@ -8947,9 +11145,9 @@ async function closeBackends(...backends) {
8947
11145
 
8948
11146
  // src/runtime/node/project.ts
8949
11147
  import { readdir, readFile as readFile2 } from "fs/promises";
8950
- import { join as join2 } from "path";
11148
+ import { join as join3 } from "path";
8951
11149
  async function loadSupabaseProject(projectDir, seed = {}) {
8952
- const migrationsDir = join2(projectDir, "supabase", "migrations");
11150
+ const migrationsDir = join3(projectDir, "supabase", "migrations");
8953
11151
  const migrations = [];
8954
11152
  let entries = [];
8955
11153
  try {
@@ -8961,19 +11159,19 @@ async function loadSupabaseProject(projectDir, seed = {}) {
8961
11159
  for (const entry of entries.sort()) {
8962
11160
  if (!entry.endsWith(".sql"))
8963
11161
  continue;
8964
- const sql = await readFile2(join2(migrationsDir, entry), "utf8");
11162
+ const sql = await readFile2(join3(migrationsDir, entry), "utf8");
8965
11163
  migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
8966
11164
  }
8967
11165
  let seedSql;
8968
11166
  if (seed.enabled !== false) {
8969
11167
  const parts = [];
8970
- const supabaseDir = join2(projectDir, "supabase");
11168
+ const supabaseDir = join3(projectDir, "supabase");
8971
11169
  for (const configuredPath of seed.paths ?? ["seed.sql"]) {
8972
11170
  const pattern = configuredPath.replace(/^\.\//, "");
8973
11171
  const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
8974
11172
  for (const relativePath of matches) {
8975
11173
  try {
8976
- parts.push(await readFile2(join2(supabaseDir, relativePath), "utf8"));
11174
+ parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
8977
11175
  } catch (error) {
8978
11176
  if (!isNotFound(error))
8979
11177
  throw error;
@@ -8991,8 +11189,8 @@ function isNotFound(error) {
8991
11189
  }
8992
11190
 
8993
11191
  // src/project-runtime.ts
8994
- import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
8995
- import { dirname as dirname4, isAbsolute, join as join7, parse, relative, resolve as resolve2 } from "path";
11192
+ import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
11193
+ import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve2 } from "path";
8996
11194
 
8997
11195
  // src/runtime/node/bun-server.ts
8998
11196
  async function serveBun(backend, opts = {}) {
@@ -9023,8 +11221,8 @@ async function serveBun(backend, opts = {}) {
9023
11221
  }, { vsn: ws.data.vsn });
9024
11222
  ws.data.session = session;
9025
11223
  },
9026
- message(ws, message) {
9027
- ws.data.session?.onMessage(typeof message === "string" ? message : new Uint8Array(message));
11224
+ message(ws, message2) {
11225
+ ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
9028
11226
  },
9029
11227
  close(ws) {
9030
11228
  ws.data.session?.onClose();
@@ -9042,8 +11240,8 @@ async function serveBun(backend, opts = {}) {
9042
11240
  }
9043
11241
 
9044
11242
  // src/runtime/node/fs-driver.ts
9045
- import { mkdir as mkdir3, readFile as readFile3, rename, rm, writeFile as writeFile2 } from "fs/promises";
9046
- import { dirname as dirname2, join as join3, normalize, sep } from "path";
11243
+ import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
11244
+ import { dirname as dirname3, join as join4, normalize, sep } from "path";
9047
11245
 
9048
11246
  class FsStorageDriver {
9049
11247
  root;
@@ -9051,7 +11249,7 @@ class FsStorageDriver {
9051
11249
  this.root = root;
9052
11250
  }
9053
11251
  resolve(key) {
9054
- const path = normalize(join3(this.root, key));
11252
+ const path = normalize(join4(this.root, key));
9055
11253
  if (!path.startsWith(normalize(this.root) + sep)) {
9056
11254
  throw new Error(`invalid storage key: ${key}`);
9057
11255
  }
@@ -9059,13 +11257,13 @@ class FsStorageDriver {
9059
11257
  }
9060
11258
  async put(key, data) {
9061
11259
  const path = this.resolve(key);
9062
- await mkdir3(dirname2(path), { recursive: true });
11260
+ await mkdir3(dirname3(path), { recursive: true });
9063
11261
  const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
9064
11262
  try {
9065
- await writeFile2(temporaryPath, data);
11263
+ await writeFile3(temporaryPath, data);
9066
11264
  await rename(temporaryPath, path);
9067
11265
  } catch (error) {
9068
- await rm(temporaryPath, { force: true }).catch(() => {});
11266
+ await rm2(temporaryPath, { force: true }).catch(() => {});
9069
11267
  throw error;
9070
11268
  }
9071
11269
  }
@@ -9078,8 +11276,12 @@ class FsStorageDriver {
9078
11276
  throw e;
9079
11277
  }
9080
11278
  }
11279
+ async getBlob(key) {
11280
+ const file = Bun.file(this.resolve(key));
11281
+ return await file.exists() ? file : null;
11282
+ }
9081
11283
  async delete(key) {
9082
- await rm(this.resolve(key), { force: true });
11284
+ await rm2(this.resolve(key), { force: true });
9083
11285
  }
9084
11286
  async deleteMany(keys) {
9085
11287
  for (const k of keys)
@@ -9088,15 +11290,15 @@ class FsStorageDriver {
9088
11290
  }
9089
11291
 
9090
11292
  // src/runtime/node/config-toml.ts
9091
- import { readFileSync } from "fs";
9092
- import { join as join4 } from "path";
11293
+ import { readFileSync as readFileSync2 } from "fs";
11294
+ import { join as join5 } from "path";
9093
11295
  function emptyTable() {
9094
11296
  return { values: new Map, children: new Map };
9095
11297
  }
9096
11298
  function loadConfigToml(projectDir, env = process.env) {
9097
11299
  let text;
9098
11300
  try {
9099
- text = readFileSync(join4(projectDir, "supabase", "config.toml"), "utf8");
11301
+ text = readFileSync2(join5(projectDir, "supabase", "config.toml"), "utf8");
9100
11302
  } catch {
9101
11303
  return emptyTable();
9102
11304
  }
@@ -9461,16 +11663,16 @@ function readFunctions(root) {
9461
11663
  }
9462
11664
 
9463
11665
  // src/runtime/node/load-functions.ts
9464
- import { readdir as readdir2, readFile as readFile5, realpath, rm as rm3, stat } from "fs/promises";
9465
- import { dirname as dirname3, join as join6 } from "path";
11666
+ import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
11667
+ import { dirname as dirname4, join as join7 } from "path";
9466
11668
  import { pathToFileURL } from "url";
9467
11669
 
9468
11670
  // src/runtime/node/bundle-function.ts
9469
- import { createHash } from "crypto";
9470
- import { mkdir as mkdir4, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
9471
- import { existsSync } from "fs";
9472
- import { tmpdir } from "os";
9473
- import { join as join5 } from "path";
11671
+ import { createHash as createHash3 } from "crypto";
11672
+ import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
11673
+ import { existsSync as existsSync2 } from "fs";
11674
+ import { tmpdir as tmpdir3 } from "os";
11675
+ import { join as join6 } from "path";
9474
11676
  function rewriteRemoteSpecifier(spec) {
9475
11677
  if (spec.startsWith("npm:"))
9476
11678
  return `https://esm.sh/${spec.slice(4)}`;
@@ -9478,18 +11680,18 @@ function rewriteRemoteSpecifier(spec) {
9478
11680
  return `https://esm.sh/jsr/${spec.slice(4)}`;
9479
11681
  return spec;
9480
11682
  }
9481
- var HTTP_CACHE = join5(tmpdir(), "supacloud-lite-fn-http");
11683
+ var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
9482
11684
  async function fetchModule(url) {
9483
- const key = createHash("sha256").update(url).digest("hex");
9484
- const cached = join5(HTTP_CACHE, key);
9485
- if (existsSync(cached))
11685
+ const key = createHash3("sha256").update(url).digest("hex");
11686
+ const cached = join6(HTTP_CACHE, key);
11687
+ if (existsSync2(cached))
9486
11688
  return readFile4(cached, "utf8");
9487
11689
  const res = await fetch(url, { redirect: "follow" });
9488
11690
  if (!res.ok)
9489
11691
  throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
9490
11692
  const body = await res.text();
9491
11693
  await mkdir4(HTTP_CACHE, { recursive: true });
9492
- await writeFile3(cached, body);
11694
+ await writeFile4(cached, body);
9493
11695
  return body;
9494
11696
  }
9495
11697
  function remotePlugin() {
@@ -9512,7 +11714,7 @@ function remotePlugin() {
9512
11714
  };
9513
11715
  }
9514
11716
  async function bundleFunction(entryPath, name) {
9515
- const outDir = join5(tmpdir(), "supacloud-lite-fn-bundle", name);
11717
+ const outDir = join6(tmpdir3(), "supacloud-lite-fn-bundle", name);
9516
11718
  await mkdir4(outDir, { recursive: true });
9517
11719
  try {
9518
11720
  const buildOutput = await Bun.build({
@@ -9530,7 +11732,7 @@ async function bundleFunction(entryPath, name) {
9530
11732
  return buildOutput.outputs[0].path;
9531
11733
  } catch (buildError) {
9532
11734
  try {
9533
- await rm2(outDir, { recursive: true, force: true });
11735
+ await rm3(outDir, { recursive: true, force: true });
9534
11736
  } catch (cleanupError) {
9535
11737
  throw new AggregateError([buildError, cleanupError], `failed to clean function bundle directory ${outDir}`);
9536
11738
  }
@@ -9542,7 +11744,7 @@ async function bundleFunction(entryPath, name) {
9542
11744
  async function loadFunctionEnv(projectDir) {
9543
11745
  let text;
9544
11746
  try {
9545
- text = await readFile5(join6(projectDir, "supabase", "functions", ".env"), "utf8");
11747
+ text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
9546
11748
  } catch {
9547
11749
  return {};
9548
11750
  }
@@ -9581,7 +11783,7 @@ async function loadFunctions2(projectDir, options = {}) {
9581
11783
  }
9582
11784
  async function loadFunctionsUnlocked(projectDir, options) {
9583
11785
  const functions = new Map;
9584
- const root = join6(projectDir, "supabase", "functions");
11786
+ const root = join7(projectDir, "supabase", "functions");
9585
11787
  let entries = [];
9586
11788
  try {
9587
11789
  entries = await readdir2(root);
@@ -9594,10 +11796,10 @@ async function loadFunctionsUnlocked(projectDir, options) {
9594
11796
  continue;
9595
11797
  if (options[name]?.enabled === false)
9596
11798
  continue;
9597
- const dir = join6(root, name);
11799
+ const dir = join7(root, name);
9598
11800
  if (!(await stat(dir)).isDirectory())
9599
11801
  continue;
9600
- const candidates = options[name]?.entrypoint ? [join6(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join6(dir, f));
11802
+ const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
9601
11803
  for (const path of candidates) {
9602
11804
  try {
9603
11805
  await stat(path);
@@ -9634,7 +11836,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
9634
11836
  }
9635
11837
  } finally {
9636
11838
  if (bundledPath)
9637
- await rm3(dirname3(bundledPath), { recursive: true, force: true }).catch(() => {});
11839
+ await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
9638
11840
  }
9639
11841
  break;
9640
11842
  }
@@ -9708,6 +11910,21 @@ class S3StorageDriver {
9708
11910
  throw error;
9709
11911
  }
9710
11912
  }
11913
+ async getBlob(key) {
11914
+ const file = this.client.file(this.objectKey(key));
11915
+ if (!await file.exists())
11916
+ return null;
11917
+ if (typeof file.arrayBuffer === "function")
11918
+ return file;
11919
+ try {
11920
+ const bytes = Uint8Array.from(await file.bytes());
11921
+ return new Blob([bytes.buffer]);
11922
+ } catch (error) {
11923
+ if (isNotFoundError(error))
11924
+ return null;
11925
+ throw error;
11926
+ }
11927
+ }
9711
11928
  async delete(key) {
9712
11929
  await this.client.file(this.objectKey(key)).delete();
9713
11930
  }
@@ -9727,14 +11944,16 @@ var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets mar
9727
11944
  function resolveProjectPaths(options = {}) {
9728
11945
  const projectDir = resolve2(options.projectDir ?? process.cwd());
9729
11946
  const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
9730
- const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, "db"));
9731
- const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join7(stateDir, "storage"));
11947
+ const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
11948
+ const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
11949
+ const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join8(stateDir, "storage"));
9732
11950
  return {
9733
11951
  projectDir,
9734
11952
  stateDir,
9735
11953
  dataDir,
9736
11954
  storageDir,
9737
- secretsFile: join7(stateDir, "secrets.json")
11955
+ secretsFile: join8(stateDir, "secrets.json"),
11956
+ databaseEngine
9738
11957
  };
9739
11958
  }
9740
11959
  async function assertResetPathsSafe(paths) {
@@ -9747,7 +11966,7 @@ async function assertResetPathsSafe(paths) {
9747
11966
  }
9748
11967
  const canonicalStateDir = await realpath2(stateDir);
9749
11968
  const secretsFile = resolve2(paths.secretsFile);
9750
- if (secretsFile !== join7(stateDir, "secrets.json")) {
11969
+ if (secretsFile !== join8(stateDir, "secrets.json")) {
9751
11970
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
9752
11971
  }
9753
11972
  const markerInfo = await requiredResetEntry(secretsFile);
@@ -9760,12 +11979,12 @@ async function assertResetPathsSafe(paths) {
9760
11979
  ["storage", paths.storageDir]
9761
11980
  ];
9762
11981
  for (const [label, targetPath] of targets) {
9763
- const target = resolve2(targetPath);
9764
- const relativePath = relative(stateDir, target);
11982
+ const target2 = resolve2(targetPath);
11983
+ const relativePath = relative(stateDir, target2);
9765
11984
  if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
9766
- throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
11985
+ throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
9767
11986
  }
9768
- await assertResetTargetCanonical(stateDir, canonicalStateDir, target, label);
11987
+ await assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label);
9769
11988
  }
9770
11989
  }
9771
11990
  async function requiredResetEntry(path) {
@@ -9792,8 +12011,8 @@ async function assertResetSecretsValid(path) {
9792
12011
  throw new Error(RESET_INVALID_SECRETS_ERROR);
9793
12012
  }
9794
12013
  }
9795
- async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, label) {
9796
- let current = target;
12014
+ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label) {
12015
+ let current = target2;
9797
12016
  while (current !== stateDir) {
9798
12017
  try {
9799
12018
  if ((await lstat(current)).isSymbolicLink()) {
@@ -9803,20 +12022,20 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
9803
12022
  if (error.code !== "ENOENT")
9804
12023
  throw error;
9805
12024
  }
9806
- const parent = dirname4(current);
12025
+ const parent = dirname5(current);
9807
12026
  if (parent === current)
9808
- throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
12027
+ throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
9809
12028
  current = parent;
9810
12029
  }
9811
- const existingAncestor = await nearestExistingAncestor(target);
9812
- const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target));
12030
+ const existingAncestor = await nearestExistingAncestor(target2);
12031
+ const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target2));
9813
12032
  const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
9814
12033
  if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
9815
- throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target}`);
12034
+ throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
9816
12035
  }
9817
12036
  }
9818
- async function nearestExistingAncestor(target) {
9819
- let current = target;
12037
+ async function nearestExistingAncestor(target2) {
12038
+ let current = target2;
9820
12039
  while (true) {
9821
12040
  try {
9822
12041
  await lstat(current);
@@ -9825,9 +12044,9 @@ async function nearestExistingAncestor(target) {
9825
12044
  if (error.code !== "ENOENT")
9826
12045
  throw error;
9827
12046
  }
9828
- const parent = dirname4(current);
12047
+ const parent = dirname5(current);
9829
12048
  if (parent === current)
9830
- throw new Error(`unable to resolve an existing ancestor for ${target}`);
12049
+ throw new Error(`unable to resolve an existing ancestor for ${target2}`);
9831
12050
  current = parent;
9832
12051
  }
9833
12052
  }
@@ -9851,7 +12070,7 @@ async function ensureProjectSecrets(paths) {
9851
12070
  };
9852
12071
  const temporaryFile = `${paths.secretsFile}.${crypto.randomUUID()}.tmp`;
9853
12072
  try {
9854
- await writeFile4(temporaryFile, `${JSON.stringify(candidate, null, 2)}
12073
+ await writeFile5(temporaryFile, `${JSON.stringify(candidate, null, 2)}
9855
12074
  `, { mode: 384, flag: "wx" });
9856
12075
  await link(temporaryFile, paths.secretsFile);
9857
12076
  stored = candidate;
@@ -9860,7 +12079,7 @@ async function ensureProjectSecrets(paths) {
9860
12079
  throw error2;
9861
12080
  stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
9862
12081
  } finally {
9863
- await unlink3(temporaryFile).catch((error2) => {
12082
+ await unlink2(temporaryFile).catch((error2) => {
9864
12083
  if (error2.code !== "ENOENT")
9865
12084
  throw error2;
9866
12085
  });
@@ -9892,42 +12111,58 @@ async function createProjectBackend(options = {}) {
9892
12111
  const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
9893
12112
  const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
9894
12113
  const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
12114
+ const databaseEngine = paths.databaseEngine;
9895
12115
  if (paths.dataDir) {
9896
12116
  await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
9897
12117
  await chmod(paths.dataDir, 448);
9898
12118
  }
9899
12119
  await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
9900
12120
  await chmod(paths.storageDir, 448);
9901
- const backend = await createBackend({
9902
- dataDir: paths.dataDir,
9903
- jwtSecret: secrets.jwtSecret,
9904
- vaultKey: secrets.vaultKey,
9905
- apiUrl: url,
9906
- siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
9907
- host,
9908
- jwtExpiry: config.auth.jwtExpiry,
9909
- uriAllowList: config.auth.uriAllowList,
9910
- authEnabled: config.auth.enabled,
9911
- authSettings: config.auth.settings,
9912
- authRateLimits: config.auth.rateLimits,
9913
- sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
9914
- sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
9915
- oauthProviders: config.auth.oauthProviders,
9916
- smsSender: options.smsSender,
9917
- dbSchemas: config.api.schemas,
9918
- maxRows: config.api.maxRows,
9919
- storageFileSizeLimit: config.storage.fileSizeLimit,
9920
- buckets: config.storage.buckets,
9921
- migrations: options.applyMigrations === false ? [] : project.migrations,
9922
- seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
9923
- functions,
9924
- functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
9925
- functionEnv,
9926
- webhooks,
9927
- startRuntimeServices: options.startRuntimeServices,
9928
- storageDriver: options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3),
9929
- log: options.log
9930
- });
12121
+ const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
12122
+ const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
12123
+ let backend;
12124
+ try {
12125
+ backend = await createBackend({
12126
+ engine,
12127
+ dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
12128
+ jwtSecret: secrets.jwtSecret,
12129
+ vaultKey: secrets.vaultKey,
12130
+ apiUrl: url,
12131
+ siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
12132
+ host,
12133
+ jwtExpiry: config.auth.jwtExpiry,
12134
+ uriAllowList: config.auth.uriAllowList,
12135
+ authEnabled: config.auth.enabled,
12136
+ authSettings: config.auth.settings,
12137
+ authRateLimits: config.auth.rateLimits,
12138
+ sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
12139
+ sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
12140
+ oauthProviders: config.auth.oauthProviders,
12141
+ smsSender: options.smsSender,
12142
+ dbSchemas: config.api.schemas,
12143
+ maxRows: config.api.maxRows,
12144
+ storageFileSizeLimit: config.storage.fileSizeLimit,
12145
+ buckets: config.storage.buckets,
12146
+ migrations: options.applyMigrations === false ? [] : project.migrations,
12147
+ seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
12148
+ functions,
12149
+ functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
12150
+ functionEnv,
12151
+ webhooks,
12152
+ startRuntimeServices: options.startRuntimeServices,
12153
+ storageDriver,
12154
+ log: options.log
12155
+ });
12156
+ } catch (error) {
12157
+ if (engine) {
12158
+ try {
12159
+ await engine.close();
12160
+ } catch (cleanupError) {
12161
+ throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
12162
+ }
12163
+ }
12164
+ throw error;
12165
+ }
9931
12166
  return {
9932
12167
  backend,
9933
12168
  config,
@@ -9938,9 +12173,22 @@ async function createProjectBackend(options = {}) {
9938
12173
  migrationCount: project.migrations.length,
9939
12174
  functionNames: [...functions.keys()],
9940
12175
  webhookCount: webhooks.length,
9941
- storageBackend
12176
+ storageBackend,
12177
+ databaseEngine
9942
12178
  };
9943
12179
  }
12180
+ function resolveDatabaseEngine(value, memory = false) {
12181
+ const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
12182
+ if (configured !== "pglite" && configured !== "native") {
12183
+ throw new Error(`unsupported SUPACLOUD_LITE_ENGINE: ${configured}`);
12184
+ }
12185
+ if (configured === "native" && memory)
12186
+ throw new Error("--memory is only supported by the pglite engine");
12187
+ if (configured === "native" && !isNativeEngineSupported()) {
12188
+ throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ` + `${process.platform}/${process.arch} must use --engine pglite`);
12189
+ }
12190
+ return configured;
12191
+ }
9944
12192
  function resolveStorageBackend(value) {
9945
12193
  const configured = value ?? process.env.SUPACLOUD_LITE_STORAGE_BACKEND ?? "fs";
9946
12194
  if (configured === "fs" || configured === "memory" || configured === "s3")
@@ -9995,7 +12243,7 @@ async function startProjectServer(options = {}) {
9995
12243
  }
9996
12244
  async function loadWebhooks(projectDir) {
9997
12245
  try {
9998
- const parsed = JSON.parse(await readFile6(join7(projectDir, "supabase", "webhooks.json"), "utf8"));
12246
+ const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
9999
12247
  return Array.isArray(parsed) ? parsed : [];
10000
12248
  } catch (error) {
10001
12249
  if (error.code === "ENOENT")
@@ -10046,9 +12294,9 @@ async function findEphemeralPort(host = "127.0.0.1") {
10046
12294
  }
10047
12295
 
10048
12296
  // src/snapshot.ts
10049
- import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
10050
- import { dirname as dirname5, join as join8, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
10051
- import { create as createTar, extract as extractTar } from "tar";
12297
+ import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
12298
+ import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
12299
+ import { create as createTar, extract as extractTar2 } from "tar";
10052
12300
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
10053
12301
  var SNAPSHOT_VERSION = 1;
10054
12302
  async function createSnapshot(options) {
@@ -10063,21 +12311,27 @@ async function createSnapshot(options) {
10063
12311
  storageBackend: options.storageBackend,
10064
12312
  includesDatabase: Boolean(paths.dataDir),
10065
12313
  includesLocalStorage: options.storageBackend === "fs",
10066
- includesSecrets: true
12314
+ includesSecrets: true,
12315
+ databaseEngine: paths.databaseEngine,
12316
+ ...paths.databaseEngine === "native" ? {
12317
+ platform: process.platform,
12318
+ architecture: process.arch,
12319
+ postgresMajor: await readPostgresMajor(paths.dataDir)
12320
+ } : {}
10067
12321
  };
10068
12322
  const output = resolve3(options.output);
10069
12323
  if (await existingInfo(output))
10070
12324
  throw new Error(`snapshot output already exists: ${output}`);
10071
- await mkdir6(dirname5(output), { recursive: true });
10072
- const stagingRoot = await mkdtemp(join8(dirname5(output), ".supacloud-lite-snapshot-"));
12325
+ await mkdir6(dirname6(output), { recursive: true });
12326
+ const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
10073
12327
  try {
10074
- await writeFile5(join8(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
12328
+ await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
10075
12329
  `);
10076
- await stageFile(paths.secretsFile, join8(stagingRoot, "secrets.json"));
12330
+ await stageFile(paths.secretsFile, join9(stagingRoot, "secrets.json"));
10077
12331
  if (paths.dataDir)
10078
- await stageDirectory(paths.dataDir, join8(stagingRoot, "database"));
12332
+ await stageDirectory(paths.dataDir, join9(stagingRoot, "database"));
10079
12333
  if (options.storageBackend === "fs")
10080
- await stageDirectory(paths.storageDir, join8(stagingRoot, "storage"));
12334
+ await stageDirectory(paths.storageDir, join9(stagingRoot, "storage"));
10081
12335
  const entries = ["manifest.json", "secrets.json"];
10082
12336
  if (paths.dataDir)
10083
12337
  entries.push("database");
@@ -10088,23 +12342,23 @@ async function createSnapshot(options) {
10088
12342
  await chmod2(output, 384);
10089
12343
  return manifest;
10090
12344
  } catch (error) {
10091
- await rm4(output, { force: true });
12345
+ await rm5(output, { force: true });
10092
12346
  throw error;
10093
12347
  } finally {
10094
- await rm4(stagingRoot, { recursive: true, force: true });
12348
+ await rm5(stagingRoot, { recursive: true, force: true });
10095
12349
  }
10096
12350
  }
10097
12351
  async function restoreSnapshot(options) {
10098
12352
  const paths = normalizePaths(options.paths);
10099
12353
  await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
10100
12354
  await assertNoDataDirectoryLock(paths);
10101
- const stagingRoot = await mkdtemp(join8(dirname5(paths.stateDir), ".supacloud-lite-restore-"));
10102
- const payloadRoot = join8(stagingRoot, "payload");
12355
+ const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
12356
+ const payloadRoot = join9(stagingRoot, "payload");
10103
12357
  const rollbackId = crypto.randomUUID();
10104
12358
  const rollbackPaths = [];
10105
12359
  try {
10106
12360
  await mkdir6(payloadRoot, { recursive: true });
10107
- await extractTar({
12361
+ await extractTar2({
10108
12362
  cwd: payloadRoot,
10109
12363
  file: resolve3(options.input),
10110
12364
  preserveOwner: false,
@@ -10129,6 +12383,7 @@ async function restoreSnapshot(options) {
10129
12383
  if (manifest.storageBackend !== options.storageBackend) {
10130
12384
  throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
10131
12385
  }
12386
+ assertDatabaseSnapshotCompatible(manifest, paths);
10132
12387
  if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
10133
12388
  throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
10134
12389
  }
@@ -10137,27 +12392,27 @@ async function restoreSnapshot(options) {
10137
12392
  }
10138
12393
  await assertSnapshotPayload(payloadRoot, manifest);
10139
12394
  if (manifest.includesDatabase)
10140
- await mkdir6(join8(payloadRoot, "database"), { recursive: true });
12395
+ await mkdir6(join9(payloadRoot, "database"), { recursive: true });
10141
12396
  if (manifest.includesLocalStorage)
10142
- await mkdir6(join8(payloadRoot, "storage"), { recursive: true });
12397
+ await mkdir6(join9(payloadRoot, "storage"), { recursive: true });
10143
12398
  await assertRestoreTargets(paths, manifest, options.force === true);
10144
- const stateStage = join8(stagingRoot, "state");
12399
+ const stateStage = join9(stagingRoot, "state");
10145
12400
  await mkdir6(stateStage, { recursive: true });
10146
- await copyEntry(join8(payloadRoot, "secrets.json"), join8(stateStage, "secrets.json"));
12401
+ await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
10147
12402
  if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
10148
- await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative2(paths.stateDir, paths.dataDir)));
12403
+ await copyEntry(join9(payloadRoot, "database"), join9(stateStage, relative2(paths.stateDir, paths.dataDir)));
10149
12404
  }
10150
12405
  if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
10151
- await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative2(paths.stateDir, paths.storageDir)));
12406
+ await copyEntry(join9(payloadRoot, "storage"), join9(stateStage, relative2(paths.stateDir, paths.storageDir)));
10152
12407
  }
10153
12408
  const swaps = [];
10154
12409
  try {
10155
12410
  await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
10156
12411
  if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
10157
- await applyDirectorySwap(join8(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
12412
+ await applyDirectorySwap(join9(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
10158
12413
  }
10159
12414
  if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
10160
- await applyDirectorySwap(join8(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
12415
+ await applyDirectorySwap(join9(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
10161
12416
  }
10162
12417
  } catch (error) {
10163
12418
  await rollbackDirectorySwaps(swaps);
@@ -10176,7 +12431,7 @@ async function restoreSnapshot(options) {
10176
12431
  } catch (error) {
10177
12432
  throw error instanceof Error ? error : new Error(String(error));
10178
12433
  } finally {
10179
- await rm4(stagingRoot, { recursive: true, force: true });
12434
+ await rm5(stagingRoot, { recursive: true, force: true });
10180
12435
  }
10181
12436
  }
10182
12437
  function normalizePaths(paths) {
@@ -10193,7 +12448,7 @@ async function assertSnapshotPaths(paths, options = {}) {
10193
12448
  try {
10194
12449
  if (paths.stateDir === parse2(paths.stateDir).root)
10195
12450
  throw new Error("snapshot state directory must not be the filesystem root");
10196
- if (paths.secretsFile !== join8(paths.stateDir, "secrets.json"))
12451
+ if (paths.secretsFile !== join9(paths.stateDir, "secrets.json"))
10197
12452
  throw new Error("snapshot secrets path must be inside the state directory");
10198
12453
  const stateInfo = await lstat2(paths.stateDir);
10199
12454
  if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
@@ -10251,10 +12506,10 @@ async function stageDirectory(root, destination) {
10251
12506
  throw error;
10252
12507
  }
10253
12508
  await mkdir6(destination, { recursive: true });
10254
- const walk = async (current, target) => {
12509
+ const walk = async (current, target2) => {
10255
12510
  for (const entry of await readdir3(current, { withFileTypes: true })) {
10256
- const fullPath = join8(current, entry.name);
10257
- const targetPath = join8(target, entry.name);
12511
+ const fullPath = join9(current, entry.name);
12512
+ const targetPath = join9(target2, entry.name);
10258
12513
  if (entry.isSymbolicLink())
10259
12514
  throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
10260
12515
  if (entry.isDirectory()) {
@@ -10269,14 +12524,14 @@ async function stageDirectory(root, destination) {
10269
12524
  };
10270
12525
  await walk(root, destination);
10271
12526
  }
10272
- async function stageFile(source, target) {
10273
- await mkdir6(dirname5(target), { recursive: true });
10274
- await copyFile(source, target);
12527
+ async function stageFile(source, target2) {
12528
+ await mkdir6(dirname6(target2), { recursive: true });
12529
+ await copyFile(source, target2);
10275
12530
  }
10276
12531
  async function readManifest(payloadRoot) {
10277
12532
  let parsed;
10278
12533
  try {
10279
- parsed = JSON.parse(await readFile7(join8(payloadRoot, "manifest.json"), "utf8"));
12534
+ parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
10280
12535
  } catch (error) {
10281
12536
  throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
10282
12537
  }
@@ -10288,13 +12543,38 @@ function isSnapshotManifest(value) {
10288
12543
  if (!value || typeof value !== "object")
10289
12544
  return false;
10290
12545
  const candidate = value;
10291
- 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;
12546
+ 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");
12547
+ }
12548
+ function assertDatabaseSnapshotCompatible(manifest, paths) {
12549
+ const sourceEngine = manifest.databaseEngine ?? "pglite";
12550
+ if (sourceEngine !== paths.databaseEngine) {
12551
+ throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
12552
+ }
12553
+ if (sourceEngine !== "native")
12554
+ return;
12555
+ if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
12556
+ 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}`);
12557
+ }
12558
+ if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
12559
+ throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
12560
+ }
12561
+ }
12562
+ async function readPostgresMajor(dataDir) {
12563
+ if (!dataDir)
12564
+ return;
12565
+ try {
12566
+ return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
12567
+ } catch (error) {
12568
+ if (error.code === "ENOENT")
12569
+ return;
12570
+ throw error;
12571
+ }
10292
12572
  }
10293
12573
  async function assertSnapshotPayload(payloadRoot, manifest) {
10294
12574
  const required = ["manifest.json", "secrets.json"];
10295
12575
  for (const path of required) {
10296
12576
  try {
10297
- await lstat2(join8(payloadRoot, path));
12577
+ await lstat2(join9(payloadRoot, path));
10298
12578
  } catch {
10299
12579
  throw new Error(`snapshot is missing required payload: ${path}`);
10300
12580
  }
@@ -10317,9 +12597,9 @@ async function assertRestoreTargets(paths, manifest, force) {
10317
12597
  if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
10318
12598
  targets.push(paths.storageDir);
10319
12599
  if (!force) {
10320
- for (const target of targets) {
10321
- if (await directoryHasEntries(target))
10322
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
12600
+ for (const target2 of targets) {
12601
+ if (await directoryHasEntries(target2))
12602
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
10323
12603
  }
10324
12604
  }
10325
12605
  }
@@ -10332,34 +12612,34 @@ async function directoryHasEntries(path) {
10332
12612
  throw error;
10333
12613
  }
10334
12614
  }
10335
- async function applyDirectorySwap(source, target, force, rollbackId, swaps) {
10336
- const targetInfo = await existingInfo(target);
12615
+ async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
12616
+ const targetInfo = await existingInfo(target2);
10337
12617
  if (targetInfo && !targetInfo.isDirectory())
10338
- throw new Error(`restore target is not a directory: ${target}`);
10339
- const swap = { target };
12618
+ throw new Error(`restore target is not a directory: ${target2}`);
12619
+ const swap = { target: target2 };
10340
12620
  if (targetInfo) {
10341
12621
  if (!force) {
10342
- if (await directoryHasEntries(target))
10343
- throw new Error(`restore target is not empty: ${target}; pass --force to replace it`);
10344
- await rm4(target, { recursive: true, force: true });
12622
+ if (await directoryHasEntries(target2))
12623
+ throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
12624
+ await rm5(target2, { recursive: true, force: true });
10345
12625
  } else {
10346
- swap.rollbackPath = join8(dirname5(target), `.${target.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
10347
- await rename2(target, swap.rollbackPath);
12626
+ swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
12627
+ await rename2(target2, swap.rollbackPath);
10348
12628
  }
10349
12629
  }
10350
12630
  try {
10351
- await mkdir6(dirname5(target), { recursive: true });
10352
- await rename2(source, target);
12631
+ await mkdir6(dirname6(target2), { recursive: true });
12632
+ await rename2(source, target2);
10353
12633
  swaps.push(swap);
10354
12634
  } catch (error) {
10355
12635
  if (swap.rollbackPath)
10356
- await rename2(swap.rollbackPath, target).catch(() => {});
12636
+ await rename2(swap.rollbackPath, target2).catch(() => {});
10357
12637
  throw error;
10358
12638
  }
10359
12639
  }
10360
12640
  async function rollbackDirectorySwaps(swaps) {
10361
12641
  for (const swap of [...swaps].reverse()) {
10362
- await rm4(swap.target, { recursive: true, force: true });
12642
+ await rm5(swap.target, { recursive: true, force: true });
10363
12643
  if (swap.rollbackPath)
10364
12644
  await rename2(swap.rollbackPath, swap.target);
10365
12645
  }
@@ -10373,17 +12653,17 @@ async function existingInfo(path) {
10373
12653
  throw error;
10374
12654
  }
10375
12655
  }
10376
- async function copyEntry(source, target) {
12656
+ async function copyEntry(source, target2) {
10377
12657
  const info = await lstat2(source);
10378
12658
  if (info.isSymbolicLink())
10379
12659
  throw new Error(`snapshot refuses symbolic link: ${source}`);
10380
12660
  if (info.isDirectory()) {
10381
- await mkdir6(target, { recursive: true });
12661
+ await mkdir6(target2, { recursive: true });
10382
12662
  for (const entry of await readdir3(source))
10383
- await copyEntry(join8(source, entry), join8(target, entry));
12663
+ await copyEntry(join9(source, entry), join9(target2, entry));
10384
12664
  } else if (info.isFile()) {
10385
- await mkdir6(dirname5(target), { recursive: true });
10386
- await Bun.write(target, Bun.file(source));
12665
+ await mkdir6(dirname6(target2), { recursive: true });
12666
+ await Bun.write(target2, Bun.file(source));
10387
12667
  } else
10388
12668
  throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
10389
12669
  }
@@ -10394,7 +12674,7 @@ async function hardenRestoredTree(root) {
10394
12674
  if (info.isDirectory()) {
10395
12675
  await chmod2(root, 448);
10396
12676
  for (const entry of await readdir3(root))
10397
- await hardenRestoredTree(join8(root, entry));
12677
+ await hardenRestoredTree(join9(root, entry));
10398
12678
  return;
10399
12679
  }
10400
12680
  if (info.isFile()) {
@@ -10405,7 +12685,7 @@ async function hardenRestoredTree(root) {
10405
12685
  }
10406
12686
  async function assertNoSymlinks(root) {
10407
12687
  for (const entry of await readdir3(root, { withFileTypes: true })) {
10408
- const fullPath = join8(root, entry.name);
12688
+ const fullPath = join9(root, entry.name);
10409
12689
  if (entry.isSymbolicLink())
10410
12690
  throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
10411
12691
  if (entry.isDirectory())
@@ -10478,6 +12758,8 @@ function parseArgs(argv) {
10478
12758
  options.storageBackend = next();
10479
12759
  else if (argument === "--s3-prefix")
10480
12760
  options.s3 = { ...options.s3, prefix: next() };
12761
+ else if (argument === "--engine")
12762
+ options.engine = next();
10481
12763
  else if (argument === "--memory")
10482
12764
  options.memory = true;
10483
12765
  else if (argument === "--output" || argument === "-o")
@@ -10540,6 +12822,7 @@ ${privilegedKey}
10540
12822
  }
10541
12823
  const project2 = await createProjectBackend({
10542
12824
  ...options,
12825
+ applyMigrations: false,
10543
12826
  includeFunctions: false,
10544
12827
  includeWebhooks: false,
10545
12828
  startRuntimeServices: false,
@@ -10548,8 +12831,8 @@ ${privilegedKey}
10548
12831
  try {
10549
12832
  const source = await generateTypes(project2.backend.db, "public");
10550
12833
  if (options.output) {
10551
- await mkdir7(dirname6(options.output), { recursive: true });
10552
- await writeFile6(options.output, source);
12834
+ await mkdir7(dirname7(options.output), { recursive: true });
12835
+ await writeFile7(options.output, source);
10553
12836
  await writeStandardOutput(`Wrote ${options.output}
10554
12837
  `);
10555
12838
  } else
@@ -10562,6 +12845,7 @@ ${privilegedKey}
10562
12845
  if (options.command === "inspect") {
10563
12846
  const project2 = await createProjectBackend({
10564
12847
  ...options,
12848
+ applyMigrations: false,
10565
12849
  includeFunctions: false,
10566
12850
  includeWebhooks: false,
10567
12851
  startRuntimeServices: false,
@@ -10597,13 +12881,13 @@ ${privilegedKey}
10597
12881
  }
10598
12882
  if (options.command !== "start")
10599
12883
  throw new Error(`unknown command: ${options.command}`);
10600
- const project = await startProjectServer({ ...options, log: (message) => console.log(` ${message}`) });
12884
+ const project = await startProjectServer({ ...options, log: (message2) => console.log(` ${message2}`) });
10601
12885
  const shutdown = waitForShutdown(() => project.close());
10602
12886
  await writeStandardOutput(`
10603
12887
  SupaCloud Lite running
10604
12888
 
10605
12889
  API URL: ${project.url}
10606
- Engine: PGlite${paths.dataDir ? ` (${paths.dataDir})` : " (memory)"}
12890
+ Engine: ${formatDatabaseEngine(project.databaseEngine, paths.dataDir)}
10607
12891
  Storage: ${formatStorage(project.storageBackend, paths.storageDir)}
10608
12892
  Migrations: ${project.migrationCount} file(s)
10609
12893
  Functions: ${project.functionNames.length ? project.functionNames.join(", ") : "none"}
@@ -10625,9 +12909,10 @@ async function runDbCommand(options) {
10625
12909
  throw new Error("db reset refuses the s3 storage backend because remote objects cannot be deleted atomically");
10626
12910
  }
10627
12911
  await assertResetPathsSafe(paths);
12912
+ await assertDataDirUnlocked(paths.dataDir);
10628
12913
  if (paths.dataDir)
10629
- await rm5(paths.dataDir, { recursive: true, force: true });
10630
- await rm5(paths.storageDir, { recursive: true, force: true });
12914
+ await rm6(paths.dataDir, { recursive: true, force: true });
12915
+ await rm6(paths.storageDir, { recursive: true, force: true });
10631
12916
  const project2 = await createProjectBackend({
10632
12917
  ...options,
10633
12918
  includeFunctions: false,
@@ -10646,7 +12931,13 @@ async function runDbCommand(options) {
10646
12931
  }
10647
12932
  const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
10648
12933
  if (subcommand === "diff") {
10649
- const ddl = await computeDbDiff({ liveDataDir: paths.dataDir, migrations: project.migrations });
12934
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
12935
+ const ddl = await computeDbDiff({
12936
+ liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
12937
+ liveEngine,
12938
+ migrations: project.migrations,
12939
+ makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined
12940
+ });
10650
12941
  if (ddl.length === 0) {
10651
12942
  await writeStandardError(`No schema changes found.
10652
12943
  `);
@@ -10658,9 +12949,9 @@ async function runDbCommand(options) {
10658
12949
  `;
10659
12950
  if (options.diffFile) {
10660
12951
  const stamp = timestamp();
10661
- const output = join9(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
10662
- await mkdir7(join9(paths.projectDir, "supabase", "migrations"), { recursive: true });
10663
- await writeFile6(output, source);
12952
+ const output = join10(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
12953
+ await mkdir7(join10(paths.projectDir, "supabase", "migrations"), { recursive: true });
12954
+ await writeFile7(output, source);
10664
12955
  await writeStandardOutput(`Wrote ${output}
10665
12956
  `);
10666
12957
  } else
@@ -10668,10 +12959,13 @@ async function runDbCommand(options) {
10668
12959
  return;
10669
12960
  }
10670
12961
  if (subcommand === "pull") {
12962
+ const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
10671
12963
  const result = await pullSchema({
10672
- liveDataDir: paths.dataDir,
12964
+ liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
12965
+ liveEngine,
10673
12966
  migrations: project.migrations,
10674
- migrationsDir: join9(paths.projectDir, "supabase", "migrations"),
12967
+ makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined,
12968
+ migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
10675
12969
  name: options.positionals[1] ?? "remote_schema"
10676
12970
  });
10677
12971
  if (!result.path)
@@ -10692,7 +12986,7 @@ async function runSnapshotCommand(options) {
10692
12986
  throw new Error("snapshot does not support --memory because the database is not durable");
10693
12987
  if (subcommand === "create") {
10694
12988
  await ensureProjectSecrets(paths);
10695
- const output = options.output ?? join9(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
12989
+ const output = options.output ?? join10(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
10696
12990
  const manifest = await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
10697
12991
  await writeStandardOutput(`Snapshot created: ${output}
10698
12992
  `);
@@ -10726,7 +13020,7 @@ async function runUpgradeCommand(options) {
10726
13020
  const paths = resolveProjectPaths(options);
10727
13021
  const storageBackend = resolveStorageBackend(options.storageBackend);
10728
13022
  await ensureProjectSecrets(paths);
10729
- const output = options.output ?? join9(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
13023
+ const output = options.output ?? join10(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
10730
13024
  await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
10731
13025
  await writeStandardOutput(`Pre-upgrade snapshot: ${output}
10732
13026
  `);
@@ -10810,6 +13104,7 @@ Options:
10810
13104
  --storage-dir <p> object storage directory
10811
13105
  --storage-backend <b> fs, memory, or s3 (default fs)
10812
13106
  --s3-prefix <p> optional key prefix for the s3 backend
13107
+ --engine <e> pglite (default) or native (macOS/glibc Linux x64/arm64)
10813
13108
  --memory use an in-memory PGlite database
10814
13109
  -o, --output <p> output file for gen types
10815
13110
  -f, --file <name> migration suffix for db diff
@@ -10825,6 +13120,10 @@ function formatStorage(backend, storageDir) {
10825
13120
  return "custom driver";
10826
13121
  return storageDir;
10827
13122
  }
13123
+ function formatDatabaseEngine(engine, dataDir) {
13124
+ const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
13125
+ return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
13126
+ }
10828
13127
  var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
10829
13128
  var exitCode = 0;
10830
13129
  try {