@supacloud/lite 0.3.0 → 0.5.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 (36) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSES/BUN-1.3.14-RUNTIME-NOTICES.txt +78 -0
  3. package/LICENSES/CHOWNR-BLUEOAK-1.0.0.txt +63 -0
  4. package/LICENSES/FS-MINIPASS-ISC.txt +15 -0
  5. package/LICENSES/MINIPASS-BLUEOAK-1.0.0.txt +55 -0
  6. package/LICENSES/MINIZLIB-MIT.txt +26 -0
  7. package/LICENSES/NODE-TAR-BLUEOAK-1.0.0.txt +55 -0
  8. package/LICENSES/YALLIST-BLUEOAK-1.0.0.txt +63 -0
  9. package/README.md +74 -4
  10. package/RELEASING.md +14 -1
  11. package/THIRD_PARTY_NOTICES.md +38 -2
  12. package/dist/cli.js +541 -108
  13. package/dist/index.d.ts +1 -0
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +525 -99
  16. package/dist/launcher.cjs +35 -0
  17. package/dist/snapshot.d.ts.map +1 -1
  18. package/dist/standalone-assets-protocol.d.ts +13 -0
  19. package/dist/standalone-assets-protocol.d.ts.map +1 -0
  20. package/dist/vendor/tinbase/auth/handler.d.ts +4 -0
  21. package/dist/vendor/tinbase/auth/handler.d.ts.map +1 -1
  22. package/dist/vendor/tinbase/db/data-dir-lock.d.ts +18 -0
  23. package/dist/vendor/tinbase/db/data-dir-lock.d.ts.map +1 -0
  24. package/dist/vendor/tinbase/db/database.d.ts +5 -1
  25. package/dist/vendor/tinbase/db/database.d.ts.map +1 -1
  26. package/dist/vendor/tinbase/db/emulated.d.ts +1 -1
  27. package/dist/vendor/tinbase/db/emulated.d.ts.map +1 -1
  28. package/dist/vendor/tinbase/db/pglite-engine.d.ts.map +1 -1
  29. package/dist/vendor/tinbase/functions/handler.d.ts +3 -1
  30. package/dist/vendor/tinbase/functions/handler.d.ts.map +1 -1
  31. package/dist/vendor/tinbase/functions/pgredis.d.ts +26 -0
  32. package/dist/vendor/tinbase/functions/pgredis.d.ts.map +1 -0
  33. package/dist/vendor/tinbase/index.d.ts +1 -0
  34. package/dist/vendor/tinbase/index.d.ts.map +1 -1
  35. package/dist/vendor/tinbase/retention/service.d.ts.map +1 -1
  36. package/package.json +11 -4
package/dist/cli.js CHANGED
@@ -8,12 +8,12 @@ import { dirname as dirname5, join as join9, resolve as resolve4 } from "path";
8
8
  // package.json
9
9
  var package_default = {
10
10
  name: "@supacloud/lite",
11
- version: "0.3.0",
11
+ version: "0.5.1",
12
12
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
13
13
  type: "module",
14
14
  license: "Apache-2.0",
15
15
  bin: {
16
- "supacloud-lite": "dist/cli.js"
16
+ "supacloud-lite": "dist/launcher.cjs"
17
17
  },
18
18
  exports: {
19
19
  ".": {
@@ -24,6 +24,7 @@ var package_default = {
24
24
  },
25
25
  files: [
26
26
  "dist",
27
+ "!dist/standalone",
27
28
  "README.md",
28
29
  "CHANGELOG.md",
29
30
  "RELEASING.md",
@@ -32,13 +33,19 @@ var package_default = {
32
33
  ],
33
34
  scripts: {
34
35
  build: "bun run build:js && bun run build:types",
35
- "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar",
36
+ "build:js": "bun build src/index.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun build src/cli.ts --target=bun --format=esm --outdir=dist --external @electric-sql/pglite --external tar && bun run scripts/build-launcher.ts",
37
+ "build:standalone:host": "bun run scripts/build-standalone.ts host",
38
+ "build:standalone:linux": "bun run scripts/build-standalone.ts linux-x64 linux-arm64",
39
+ "build:standalone:macos": "bun run scripts/build-standalone.ts macos-x64 macos-arm64",
40
+ "build:standalone:windows": "bun run scripts/build-standalone.ts windows-x64",
41
+ "build:standalone:release": "bun run scripts/build-standalone.ts linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64",
36
42
  "build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
37
- check: "bun run typecheck && bun run test && bun run build && bun run test:package",
43
+ check: "bun run typecheck && bun run test && bun run build && bun run test:package && bun run build:standalone:host && bun run test:standalone",
38
44
  dev: "bun run src/cli.ts start",
39
45
  start: "bun run src/cli.ts start",
40
46
  test: "bun test --timeout 20000",
41
47
  "test:package": "bun run scripts/package-smoke.ts",
48
+ "test:standalone": "bun run scripts/standalone-smoke.ts",
42
49
  prepack: "bun run build",
43
50
  typecheck: "bun x tsc --noEmit -p tsconfig.json"
44
51
  },
@@ -1258,30 +1265,7 @@ class AuthHandler {
1258
1265
  const token = body.refresh_token;
1259
1266
  if (!token)
1260
1267
  return authError(400, "validation_failed", "refresh_token required");
1261
- const res = await this.db.query(`select rt.*, u.id as uid from auth.refresh_tokens rt
1262
- join auth.users u on u.id = rt.user_id
1263
- where rt.token = $1`, [token]);
1264
- const row = res.rows[0];
1265
- if (!row || row.revoked) {
1266
- return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
1267
- }
1268
- const now = Date.now();
1269
- const lastActivity = timestampMs(row.created_at);
1270
- if (this.config.sessionInactivitySeconds && lastActivity !== null && now - lastActivity >= this.config.sessionInactivitySeconds * 1000) {
1271
- await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
1272
- return authError(400, "session_expired", "Session expired due to inactivity");
1273
- }
1274
- const sessionStartedAt = row.session_id ? timestampMs((await this.db.query(`select min(created_at) as started_at from auth.refresh_tokens where session_id = $1`, [row.session_id])).rows[0]?.started_at ?? null) : lastActivity;
1275
- if (this.config.sessionTimeboxSeconds && sessionStartedAt !== null && now - sessionStartedAt >= this.config.sessionTimeboxSeconds * 1000) {
1276
- await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
1277
- return authError(400, "session_expired", "Session expired");
1278
- }
1279
- await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
1280
- const ures = await this.db.query(`select * from auth.users where id = $1`, [row.user_id]);
1281
- return json(200, await this.sessionFor(ures.rows[0], token, {
1282
- sessionId: row.session_id ?? undefined,
1283
- sessionStartedAt: sessionStartedAt === null ? undefined : Math.floor(sessionStartedAt / 1000)
1284
- }));
1268
+ return this.rotateRefreshToken(token);
1285
1269
  }
1286
1270
  if (grantType === "pkce") {
1287
1271
  const authCode = body.auth_code;
@@ -1705,8 +1689,8 @@ Or sign in with this link: ${link}`
1705
1689
  return authError(404, "mfa_factor_not_found", "MFA factor not found");
1706
1690
  return json(200, { id: factorId });
1707
1691
  }
1708
- async getUserFactors(userId) {
1709
- const res = await this.db.query(`select id, friendly_name, factor_type, status, created_at, updated_at
1692
+ async getUserFactors(userId, query = (sql, params) => this.db.query(sql, params)) {
1693
+ const res = await query(`select id, friendly_name, factor_type, status, created_at, updated_at
1710
1694
  from auth.mfa_factors where user_id = $1 order by created_at`, [userId]);
1711
1695
  return res.rows.map((f) => ({
1712
1696
  id: f.id,
@@ -1717,8 +1701,8 @@ Or sign in with this link: ${link}`
1717
1701
  updated_at: iso(f.updated_at)
1718
1702
  }));
1719
1703
  }
1720
- async getUserIdentities(userId) {
1721
- const res = await this.db.query(`select id, provider_id, user_id, identity_data, provider, created_at, updated_at, last_sign_in_at
1704
+ async getUserIdentities(userId, query = (sql, params) => this.db.query(sql, params)) {
1705
+ const res = await query(`select id, provider_id, user_id, identity_data, provider, created_at, updated_at, last_sign_in_at
1722
1706
  from auth.identities where user_id = $1 order by created_at`, [userId]);
1723
1707
  return res.rows.map((r) => ({
1724
1708
  identity_id: r.id,
@@ -1731,6 +1715,46 @@ Or sign in with this link: ${link}`
1731
1715
  updated_at: iso(r.updated_at)
1732
1716
  }));
1733
1717
  }
1718
+ async rotateRefreshToken(token) {
1719
+ return this.db.transaction(async (query) => {
1720
+ const claimedToken = await this.claimRefreshToken(query, token);
1721
+ if (!claimedToken)
1722
+ return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
1723
+ const sessionStartedAt = await this.refreshSessionStartedAt(query, claimedToken);
1724
+ const expiryError = this.refreshExpiryError(claimedToken, sessionStartedAt);
1725
+ if (expiryError)
1726
+ return expiryError;
1727
+ const userResult = await query(`select * from auth.users where id = $1`, [claimedToken.user_id]);
1728
+ const session = await this.sessionFor(userResult.rows[0], token, {
1729
+ sessionId: claimedToken.session_id ?? undefined,
1730
+ sessionStartedAt: sessionStartedAt === null ? undefined : Math.floor(sessionStartedAt / 1000)
1731
+ }, query);
1732
+ return json(200, session);
1733
+ });
1734
+ }
1735
+ async claimRefreshToken(query, token) {
1736
+ const claimed = await query(`update auth.refresh_tokens set revoked = true, updated_at = now()
1737
+ where token = $1 and revoked is not true
1738
+ returning user_id, session_id, created_at`, [token]);
1739
+ return claimed.rows[0];
1740
+ }
1741
+ async refreshSessionStartedAt(query, refreshToken) {
1742
+ if (!refreshToken.session_id)
1743
+ return timestampMs(refreshToken.created_at);
1744
+ const sessionStart = await query(`select min(created_at) as started_at from auth.refresh_tokens where session_id = $1`, [refreshToken.session_id]);
1745
+ return timestampMs(sessionStart.rows[0]?.started_at ?? null);
1746
+ }
1747
+ refreshExpiryError(refreshToken, sessionStartedAt) {
1748
+ const now = Date.now();
1749
+ const lastActivity = timestampMs(refreshToken.created_at);
1750
+ if (this.config.sessionInactivitySeconds && lastActivity !== null && now - lastActivity >= this.config.sessionInactivitySeconds * 1000) {
1751
+ return authError(400, "session_expired", "Session expired due to inactivity");
1752
+ }
1753
+ if (this.config.sessionTimeboxSeconds && sessionStartedAt !== null && now - sessionStartedAt >= this.config.sessionTimeboxSeconds * 1000) {
1754
+ return authError(400, "session_expired", "Session expired");
1755
+ }
1756
+ return null;
1757
+ }
1734
1758
  async userFromBearer(req) {
1735
1759
  const claims = await this.claimsFromBearer(req);
1736
1760
  if (!claims?.sub)
@@ -1768,7 +1792,7 @@ Or sign in with this link: ${link}`
1768
1792
  const session = await this.sessionFor(res.rows[0]);
1769
1793
  return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
1770
1794
  }
1771
- async sessionFor(user, parentToken, opts) {
1795
+ async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
1772
1796
  const now = Math.floor(Date.now() / 1000);
1773
1797
  const timeboxRemaining = this.config.sessionTimeboxSeconds ? opts?.sessionStartedAt !== undefined ? opts.sessionStartedAt + this.config.sessionTimeboxSeconds - now : this.config.sessionTimeboxSeconds : this.config.jwtExpiry;
1774
1798
  const lifetime = Math.max(1, Math.min(this.config.jwtExpiry, timeboxRemaining));
@@ -1792,14 +1816,14 @@ Or sign in with this link: ${link}`
1792
1816
  };
1793
1817
  const accessToken = await signJwt(claims, this.config.jwtSecret);
1794
1818
  const refreshToken = randomToken(24);
1795
- await this.db.query(`insert into auth.refresh_tokens (token, user_id, parent, session_id) values ($1, $2, $3, $4)`, [refreshToken, user.id, parentToken ?? null, sessionId]);
1819
+ await query(`insert into auth.refresh_tokens (token, user_id, parent, session_id) values ($1, $2, $3, $4)`, [refreshToken, user.id, parentToken ?? null, sessionId]);
1796
1820
  return {
1797
1821
  access_token: accessToken,
1798
1822
  token_type: "bearer",
1799
1823
  expires_in: lifetime,
1800
1824
  expires_at: expiresAt,
1801
1825
  refresh_token: refreshToken,
1802
- user: this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id))
1826
+ user: this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query))
1803
1827
  };
1804
1828
  }
1805
1829
  }
@@ -1985,13 +2009,182 @@ function resetCapturedHandler() {
1985
2009
  captured.handler = undefined;
1986
2010
  }
1987
2011
 
2012
+ // src/vendor/tinbase/functions/pgredis.ts
2013
+ import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
2014
+ var CACHE_NAMESPACE = "supacloud-edge-runtime";
2015
+ var CACHE_TABLE = "public.supacloud_pgredis_kv";
2016
+ var MAX_KEY_CHARACTERS = 512;
2017
+ var MAX_VALUE_BYTES = 1048576;
2018
+ var MAX_TTL_MS = 31536000000;
2019
+ var CACHE_SCHEMA_SQL = `
2020
+ create table if not exists ${CACHE_TABLE} (
2021
+ namespace text not null,
2022
+ key text not null,
2023
+ value jsonb not null,
2024
+ expires_at timestamptz,
2025
+ updated_at timestamptz not null default now(),
2026
+ primary key (namespace, key)
2027
+ );
2028
+
2029
+ create index if not exists supacloud_pgredis_kv_expires_at_idx
2030
+ on ${CACHE_TABLE} (expires_at) where expires_at is not null;
2031
+ `;
2032
+ var CACHE_PERMISSIONS_SQL = `
2033
+ revoke all on table ${CACHE_TABLE} from public, anon, authenticated, service_role;
2034
+ `;
2035
+
2036
+ class PgredisCache {
2037
+ db;
2038
+ constructor(db) {
2039
+ this.db = db;
2040
+ }
2041
+ static async create(db) {
2042
+ await db.exec(CACHE_SCHEMA_SQL);
2043
+ if (!db.engine.minimalBootstrap)
2044
+ await db.exec(CACHE_PERMISSIONS_SQL);
2045
+ return new PgredisCache(db);
2046
+ }
2047
+ async get(key) {
2048
+ assertCacheKey(key);
2049
+ return this.db.transaction(async (query) => {
2050
+ await deleteExpiredKey(query, key);
2051
+ const stored = await query(`select value::text as serialized_value from ${CACHE_TABLE} where namespace = $1 and key = $2`, [CACHE_NAMESPACE, key]);
2052
+ return stored.rows[0] ? parseStoredValue(stored.rows[0]) : null;
2053
+ });
2054
+ }
2055
+ async set(key, cacheValue, ttlMs) {
2056
+ assertCacheKey(key);
2057
+ assertTtl(ttlMs);
2058
+ const serializedValue = serializeCacheValue(cacheValue);
2059
+ await this.db.query(`insert into ${CACHE_TABLE} (namespace, key, value, expires_at, updated_at)
2060
+ values ($1, $2, $3::jsonb,
2061
+ case when $4::bigint is null then null else clock_timestamp() + $4::double precision * interval '1 millisecond' end,
2062
+ now())
2063
+ on conflict (namespace, key) do update set
2064
+ value = excluded.value, expires_at = excluded.expires_at, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue, ttlMs ?? null]);
2065
+ return true;
2066
+ }
2067
+ async delete(key) {
2068
+ assertCacheKey(key);
2069
+ return this.db.transaction(async (query) => {
2070
+ await deleteExpiredKey(query, key);
2071
+ const deleted = await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 returning key`, [CACHE_NAMESPACE, key]);
2072
+ return deleted.rows.length > 0;
2073
+ });
2074
+ }
2075
+ async ttl(key) {
2076
+ assertCacheKey(key);
2077
+ return this.db.transaction(async (query) => {
2078
+ await deleteExpiredKey(query, key);
2079
+ const stored = await query(`select case when expires_at is null then null
2080
+ else greatest(0, ceil(extract(epoch from (expires_at - clock_timestamp())) * 1000))::bigint
2081
+ end as ttl_ms from ${CACHE_TABLE} where namespace = $1 and key = $2`, [CACHE_NAMESPACE, key]);
2082
+ const ttlMs = stored.rows[0]?.ttl_ms;
2083
+ return ttlMs === null || ttlMs === undefined ? null : Number(ttlMs);
2084
+ });
2085
+ }
2086
+ async getset(key, cacheValue) {
2087
+ assertCacheKey(key);
2088
+ const serializedValue = serializeCacheValue(cacheValue);
2089
+ return this.db.transaction(async (query) => {
2090
+ await deleteExpiredKey(query, key);
2091
+ const previous = await query(`select value::text as serialized_value from ${CACHE_TABLE}
2092
+ where namespace = $1 and key = $2 for update`, [CACHE_NAMESPACE, key]);
2093
+ await upsertWithoutTtl(query, key, serializedValue);
2094
+ return previous.rows[0] ? parseStoredValue(previous.rows[0]) : null;
2095
+ });
2096
+ }
2097
+ async getdel(key) {
2098
+ assertCacheKey(key);
2099
+ return this.db.transaction(async (query) => {
2100
+ await deleteExpiredKey(query, key);
2101
+ const deleted = await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 returning value::text as serialized_value`, [CACHE_NAMESPACE, key]);
2102
+ return deleted.rows[0] ? parseStoredValue(deleted.rows[0]) : null;
2103
+ });
2104
+ }
2105
+ }
2106
+ var cacheContexts = new AsyncLocalStorage2;
2107
+ var cacheFacade = Object.freeze({
2108
+ get: async (key) => activeCache().get(key),
2109
+ set: async (key, cacheValue, ttlMs) => activeCache().set(key, cacheValue, ttlMs),
2110
+ delete: async (key) => activeCache().delete(key),
2111
+ ttl: async (key) => activeCache().ttl(key),
2112
+ getset: async (key, cacheValue) => activeCache().getset(key, cacheValue),
2113
+ getdel: async (key) => activeCache().getdel(key)
2114
+ });
2115
+ function installPgredisShim() {
2116
+ const runtime = globalThis;
2117
+ if (runtime.SupaCloud?.pgredis) {
2118
+ if (runtime.SupaCloud.pgredis === cacheFacade)
2119
+ return;
2120
+ throw new Error("globalThis.SupaCloud.pgredis is already owned by another runtime");
2121
+ }
2122
+ Object.defineProperty(runtime, "SupaCloud", {
2123
+ configurable: false,
2124
+ enumerable: true,
2125
+ value: Object.freeze({ ...runtime.SupaCloud ?? {}, pgredis: cacheFacade }),
2126
+ writable: false
2127
+ });
2128
+ }
2129
+ function runWithPgredisCache(cache, operation) {
2130
+ const context = { cache, active: true };
2131
+ return cacheContexts.run(context, async () => {
2132
+ try {
2133
+ return await operation();
2134
+ } finally {
2135
+ context.active = false;
2136
+ }
2137
+ });
2138
+ }
2139
+ function activeCache() {
2140
+ const context = cacheContexts.getStore();
2141
+ if (!context?.active)
2142
+ throw new Error("pgredis binding is unavailable outside a function request");
2143
+ return context.cache;
2144
+ }
2145
+ function assertCacheKey(key) {
2146
+ if (typeof key !== "string" || key.length < 1 || key.length > MAX_KEY_CHARACTERS) {
2147
+ throw new TypeError(`pgredis key must contain between 1 and ${MAX_KEY_CHARACTERS} characters`);
2148
+ }
2149
+ }
2150
+ function assertTtl(ttlMs) {
2151
+ if (ttlMs === null || ttlMs === undefined)
2152
+ return;
2153
+ if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
2154
+ throw new TypeError("pgredis ttlMs must be a non-negative safe integer or null");
2155
+ if (ttlMs > MAX_TTL_MS)
2156
+ throw new RangeError(`pgredis ttlMs must not exceed ${MAX_TTL_MS}`);
2157
+ }
2158
+ function serializeCacheValue(cacheValue) {
2159
+ const serializedValue = JSON.stringify(cacheValue);
2160
+ if (serializedValue === undefined)
2161
+ throw new TypeError("pgredis value must be JSON serializable");
2162
+ if (new TextEncoder().encode(serializedValue).byteLength > MAX_VALUE_BYTES) {
2163
+ throw new RangeError(`pgredis value must not exceed ${MAX_VALUE_BYTES} bytes`);
2164
+ }
2165
+ return serializedValue;
2166
+ }
2167
+ function parseStoredValue(stored) {
2168
+ return JSON.parse(stored.serialized_value);
2169
+ }
2170
+ async function deleteExpiredKey(query, key) {
2171
+ await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 and expires_at <= clock_timestamp()`, [CACHE_NAMESPACE, key]);
2172
+ }
2173
+ async function upsertWithoutTtl(query, key, serializedValue) {
2174
+ await query(`insert into ${CACHE_TABLE} (namespace, key, value, expires_at, updated_at)
2175
+ values ($1, $2, $3::jsonb, null, now())
2176
+ on conflict (namespace, key) do update set value = excluded.value, expires_at = null, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue]);
2177
+ }
2178
+
1988
2179
  // src/vendor/tinbase/functions/handler.ts
1989
2180
  class FunctionsHandler {
1990
2181
  functions;
1991
2182
  env;
1992
- constructor(functions, env) {
2183
+ pgredis;
2184
+ constructor(functions, env, pgredis) {
1993
2185
  this.functions = functions;
1994
2186
  this.env = env;
2187
+ this.pgredis = pgredis;
1995
2188
  }
1996
2189
  register(name, fn) {
1997
2190
  this.functions.set(name, fn);
@@ -2009,7 +2202,8 @@ class FunctionsHandler {
2009
2202
  return json2(404, { error: `function "${name}" not found` });
2010
2203
  }
2011
2204
  try {
2012
- const res = await runWithDenoEnv(this.env, () => Promise.resolve(fn(req, { auth: ctx, env: this.env })));
2205
+ const invoke = () => Promise.resolve(fn(req, { auth: ctx, env: this.env }));
2206
+ const res = await runWithDenoEnv(this.env, () => runWithPgredisCache(this.pgredis, invoke));
2013
2207
  if (!(res instanceof Response)) {
2014
2208
  return json2(500, { error: `function "${name}" did not return a Response` });
2015
2209
  }
@@ -2551,7 +2745,24 @@ grant execute on function realtime.send(jsonb, text, text, boolean) to anon, aut
2551
2745
  // src/vendor/tinbase/db/emulated.ts
2552
2746
  var PGMQ_SQL = `
2553
2747
  create schema if not exists pgmq;
2554
- grant usage on schema pgmq to anon, authenticated, service_role;
2748
+ revoke all on schema pgmq from public, anon, authenticated;
2749
+ grant usage on schema pgmq to service_role;
2750
+
2751
+ create table if not exists pgmq.meta (
2752
+ queue_name text primary key,
2753
+ physical_name text not null unique,
2754
+ is_partitioned boolean not null default false,
2755
+ is_unlogged boolean not null default false,
2756
+ created_at timestamptz not null default now()
2757
+ );
2758
+
2759
+ -- Preserve queues created by older Lite versions. Their physical names were
2760
+ -- the queue names, so only the already-truncated identifier can be recovered.
2761
+ insert into pgmq.meta (queue_name, physical_name)
2762
+ select substring(tablename from 3), substring(tablename from 3)
2763
+ from pg_tables
2764
+ where schemaname = 'pgmq' and left(tablename, 2) = 'q_'
2765
+ on conflict do nothing;
2555
2766
 
2556
2767
  do $$ begin
2557
2768
  if not exists (select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace
@@ -2562,32 +2773,78 @@ do $$ begin
2562
2773
  end if;
2563
2774
  end $$;
2564
2775
 
2776
+ create or replace function pgmq._normalize_queue_name(queue_name text)
2777
+ returns text language plpgsql immutable set search_path = pgmq, pg_catalog, public as $pgmq$
2778
+ declare normalized text := btrim(queue_name);
2779
+ begin
2780
+ if normalized is null or normalized !~ '^[a-z0-9][a-z0-9_-]{0,127}$' then
2781
+ raise exception 'invalid queue name: must match ^[a-z0-9][a-z0-9_-]{0,127}$'
2782
+ using errcode = '22023';
2783
+ end if;
2784
+ return normalized;
2785
+ end $pgmq$;
2786
+
2787
+ -- PostgreSQL identifiers are limited to 63 bytes. Keep the conventional table
2788
+ -- name for short queues and add a deterministic hash for valid 62-128 byte
2789
+ -- public names so truncation can never merge two queues.
2790
+ create or replace function pgmq._physical_queue_name(queue_name text)
2791
+ returns text language sql immutable set search_path = pgmq, pg_catalog, public as $pgmq$
2792
+ select case when length(normalized) <= 61 then normalized
2793
+ else left(normalized, 28) || '_' || md5(normalized) end
2794
+ from (select pgmq._normalize_queue_name(queue_name) as normalized) names;
2795
+ $pgmq$;
2796
+
2797
+ create or replace function pgmq._resolve_queue(queue_name text)
2798
+ returns text language plpgsql stable security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2799
+ declare normalized text := pgmq._normalize_queue_name(queue_name); physical text;
2800
+ begin
2801
+ select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
2802
+ if physical is null then
2803
+ raise exception 'queue "%" does not exist', normalized using errcode = '42P01';
2804
+ end if;
2805
+ return physical;
2806
+ end $pgmq$;
2807
+
2565
2808
  create or replace function pgmq.create(queue_name text) returns void language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2809
+ declare normalized text := pgmq._normalize_queue_name(queue_name); physical text := pgmq._physical_queue_name(queue_name);
2566
2810
  begin
2811
+ insert into pgmq.meta (queue_name, physical_name) values (normalized, physical)
2812
+ on conflict do nothing;
2813
+ select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
2567
2814
  execute format('create table if not exists pgmq.%I (
2568
2815
  msg_id bigint generated always as identity primary key,
2569
2816
  read_ct integer not null default 0,
2570
2817
  enqueued_at timestamptz not null default now(),
2571
2818
  vt timestamptz not null default now(),
2572
- message jsonb)', 'q_' || queue_name);
2819
+ message jsonb)', 'q_' || physical);
2573
2820
  execute format('create table if not exists pgmq.%I (
2574
2821
  msg_id bigint primary key, read_ct integer not null default 0,
2575
2822
  enqueued_at timestamptz not null, archived_at timestamptz not null default now(),
2576
- vt timestamptz, message jsonb)', 'a_' || queue_name);
2823
+ vt timestamptz, message jsonb)', 'a_' || physical);
2577
2824
  end $pgmq$;
2578
2825
 
2579
2826
  create or replace function pgmq.send(queue_name text, msg jsonb, delay integer default 0)
2580
2827
  returns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2581
- declare id bigint;
2828
+ declare id bigint; physical text := pgmq._resolve_queue(queue_name);
2582
2829
  begin
2583
- perform pgmq.create(queue_name);
2584
- execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || queue_name)
2830
+ execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || physical)
2585
2831
  into id using delay, msg;
2586
2832
  return id;
2587
2833
  end $pgmq$;
2588
2834
 
2835
+ create or replace function pgmq.send_batch(queue_name text, msgs jsonb[], delay integer default 0)
2836
+ returns setof bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2837
+ declare queue_message jsonb;
2838
+ begin
2839
+ perform pgmq._resolve_queue(queue_name);
2840
+ foreach queue_message in array msgs loop
2841
+ return next pgmq.send(queue_name, queue_message, delay);
2842
+ end loop;
2843
+ end $pgmq$;
2844
+
2589
2845
  create or replace function pgmq.read(queue_name text, vt integer, qty integer)
2590
2846
  returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2847
+ declare physical text := pgmq._resolve_queue(queue_name);
2591
2848
  begin
2592
2849
  return query execute format($fmt$
2593
2850
  with cte as (
@@ -2596,52 +2853,68 @@ begin
2596
2853
  update pgmq.%I m set vt = now() + make_interval(secs => $2), read_ct = read_ct + 1
2597
2854
  from cte where m.msg_id = cte.msg_id
2598
2855
  returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
2599
- $fmt$, 'q_' || queue_name, 'q_' || queue_name) using qty, vt;
2856
+ $fmt$, 'q_' || physical, 'q_' || physical) using qty, vt;
2600
2857
  end $pgmq$;
2601
2858
 
2602
2859
  create or replace function pgmq.pop(queue_name text)
2603
2860
  returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2861
+ declare physical text := pgmq._resolve_queue(queue_name);
2604
2862
  begin
2605
2863
  return query execute format($fmt$
2606
2864
  with cte as (select msg_id from pgmq.%I where vt <= now() order by msg_id limit 1 for update skip locked)
2607
2865
  delete from pgmq.%I m using cte where m.msg_id = cte.msg_id
2608
2866
  returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
2609
- $fmt$, 'q_' || queue_name, 'q_' || queue_name);
2867
+ $fmt$, 'q_' || physical, 'q_' || physical);
2868
+ end $pgmq$;
2869
+
2870
+ create or replace function pgmq.set_vt(queue_name text, msg_id bigint, vt_offset integer)
2871
+ returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2872
+ declare physical text := pgmq._resolve_queue(queue_name);
2873
+ begin
2874
+ return query execute format($fmt$
2875
+ update pgmq.%I m set vt = now() + make_interval(secs => $1)
2876
+ where m.msg_id = $2
2877
+ returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
2878
+ $fmt$, 'q_' || physical) using vt_offset, msg_id;
2610
2879
  end $pgmq$;
2611
2880
 
2612
2881
  create or replace function pgmq.delete(queue_name text, msg_id bigint)
2613
2882
  returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2614
- declare n integer;
2883
+ declare n integer; physical text := pgmq._resolve_queue(queue_name);
2615
2884
  begin
2616
- execute format('delete from pgmq.%I where msg_id = $1', 'q_' || queue_name) using msg_id;
2885
+ execute format('delete from pgmq.%I where msg_id = $1', 'q_' || physical) using msg_id;
2617
2886
  get diagnostics n = row_count;
2618
2887
  return n > 0;
2619
2888
  end $pgmq$;
2620
2889
 
2621
2890
  create or replace function pgmq.archive(queue_name text, msg_id bigint)
2622
2891
  returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2623
- declare n integer;
2892
+ declare n integer; physical text := pgmq._resolve_queue(queue_name);
2624
2893
  begin
2625
2894
  execute format($fmt$
2626
2895
  with del as (delete from pgmq.%I where msg_id = $1 returning *)
2627
2896
  insert into pgmq.%I (msg_id, read_ct, enqueued_at, vt, message)
2628
2897
  select msg_id, read_ct, enqueued_at, vt, message from del
2629
- $fmt$, 'q_' || queue_name, 'a_' || queue_name) using msg_id;
2898
+ $fmt$, 'q_' || physical, 'a_' || physical) using msg_id;
2630
2899
  get diagnostics n = row_count;
2631
2900
  return n > 0;
2632
2901
  end $pgmq$;
2633
2902
 
2634
2903
  create or replace function pgmq.drop_queue(queue_name text) returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2904
+ declare normalized text := pgmq._normalize_queue_name(queue_name); physical text;
2635
2905
  begin
2636
- execute format('drop table if exists pgmq.%I', 'q_' || queue_name);
2637
- execute format('drop table if exists pgmq.%I', 'a_' || queue_name);
2906
+ select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
2907
+ if physical is null then return false; end if;
2908
+ execute format('drop table if exists pgmq.%I', 'q_' || physical);
2909
+ execute format('drop table if exists pgmq.%I', 'a_' || physical);
2910
+ delete from pgmq.meta where pgmq.meta.queue_name = normalized;
2638
2911
  return true;
2639
2912
  end $pgmq$;
2640
2913
 
2641
2914
  create or replace function pgmq.purge_queue(queue_name text) returns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2642
- declare n bigint;
2915
+ declare n bigint; physical text := pgmq._resolve_queue(queue_name);
2643
2916
  begin
2644
- execute format('delete from pgmq.%I', 'q_' || queue_name);
2917
+ execute format('delete from pgmq.%I', 'q_' || physical);
2645
2918
  get diagnostics n = row_count;
2646
2919
  return n;
2647
2920
  end $pgmq$;
@@ -2649,11 +2922,47 @@ end $pgmq$;
2649
2922
  create or replace function pgmq.list_queues()
2650
2923
  returns table(queue_name text, is_partitioned boolean, is_unlogged boolean, created_at timestamptz)
2651
2924
  language sql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
2652
- select substring(tablename from 3), false, false, now()
2653
- from pg_tables where schemaname = 'pgmq' and left(tablename, 2) = 'q_';
2925
+ select queue_name, is_partitioned, is_unlogged, created_at from pgmq.meta;
2654
2926
  $pgmq$;
2655
2927
 
2656
- grant execute on all functions in schema pgmq to anon, authenticated, service_role;
2928
+ revoke all on all functions in schema pgmq from public, anon, authenticated;
2929
+ grant execute on all functions in schema pgmq to service_role;
2930
+
2931
+ create schema if not exists pgmq_public;
2932
+ grant usage on schema pgmq_public to anon, authenticated, service_role;
2933
+
2934
+ create or replace function pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer default 0)
2935
+ returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2936
+ select * from pgmq.send(queue_name, message, sleep_seconds);
2937
+ $pgmq_public$;
2938
+
2939
+ create or replace function pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer default 0)
2940
+ returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2941
+ select * from pgmq.send_batch(queue_name, messages, sleep_seconds);
2942
+ $pgmq_public$;
2943
+
2944
+ create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
2945
+ returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2946
+ select * from pgmq.read(queue_name, sleep_seconds, n);
2947
+ $pgmq_public$;
2948
+
2949
+ create or replace function pgmq_public.pop(queue_name text)
2950
+ returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2951
+ select * from pgmq.pop(queue_name);
2952
+ $pgmq_public$;
2953
+
2954
+ create or replace function pgmq_public.archive(queue_name text, message_id bigint)
2955
+ returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2956
+ select pgmq.archive(queue_name, message_id);
2957
+ $pgmq_public$;
2958
+
2959
+ create or replace function pgmq_public."delete"(queue_name text, message_id bigint)
2960
+ returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
2961
+ select pgmq.delete(queue_name, message_id);
2962
+ $pgmq_public$;
2963
+
2964
+ revoke all on all functions in schema pgmq_public from public;
2965
+ grant execute on all functions in schema pgmq_public to anon, authenticated, service_role;
2657
2966
  `;
2658
2967
  var CRON_SQL = `
2659
2968
  create schema if not exists cron;
@@ -2981,11 +3290,66 @@ function pickTag(text, base) {
2981
3290
  }
2982
3291
 
2983
3292
  // src/vendor/tinbase/db/pglite-engine.ts
2984
- import { mkdir, open, readFile, unlink } from "fs/promises";
3293
+ import { mkdir, open, unlink as unlink2 } from "fs/promises";
2985
3294
  import { dirname, resolve } from "path";
3295
+
3296
+ // src/vendor/tinbase/db/data-dir-lock.ts
3297
+ import { readFile, unlink } from "fs/promises";
3298
+ async function recoverStaleDataDirLock(lockPath) {
3299
+ const lockState = await inspectDataDirLock(lockPath);
3300
+ if (lockState.kind !== "stale")
3301
+ return lockState;
3302
+ try {
3303
+ await unlink(lockPath);
3304
+ return { kind: "missing" };
3305
+ } catch (error) {
3306
+ if (error.code === "ENOENT")
3307
+ return { kind: "missing" };
3308
+ throw error;
3309
+ }
3310
+ }
3311
+ async function readDataDirLockOwner(lockPath) {
3312
+ const lockState = await inspectDataDirLock(lockPath);
3313
+ return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
3314
+ }
3315
+ async function inspectDataDirLock(lockPath) {
3316
+ let contents;
3317
+ try {
3318
+ contents = await readFile(lockPath, "utf8");
3319
+ } catch (error) {
3320
+ return error.code === "ENOENT" ? { kind: "missing" } : { kind: "unreadable" };
3321
+ }
3322
+ const owner = parseDataDirLockOwner(contents);
3323
+ if (!owner)
3324
+ return { kind: "unreadable" };
3325
+ return isProcessAlive(owner.pid) ? { kind: "active", owner } : { kind: "stale", owner };
3326
+ }
3327
+ function parseDataDirLockOwner(contents) {
3328
+ try {
3329
+ const value = JSON.parse(contents);
3330
+ return typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.nonce === "string" ? { pid: value.pid, nonce: value.nonce } : null;
3331
+ } catch {
3332
+ return null;
3333
+ }
3334
+ }
3335
+ function isProcessAlive(pid) {
3336
+ try {
3337
+ process.kill(pid, 0);
3338
+ return true;
3339
+ } catch (error) {
3340
+ return error.code === "EPERM";
3341
+ }
3342
+ }
3343
+
3344
+ // src/standalone-assets-protocol.ts
3345
+ var STANDALONE_PGLITE_ASSETS = Symbol.for("supacloud-lite.pglite-standalone-assets");
3346
+
3347
+ // src/vendor/tinbase/db/pglite-engine.ts
2986
3348
  async function createPgliteEngine(dataDir) {
2987
3349
  const releaseLock = await acquireDataDirLock(dataDir);
2988
3350
  let PGlite, extensions;
3351
+ const standaloneAssets = getStandaloneAssets();
3352
+ let cleanupStandaloneBundles = async () => {};
2989
3353
  try {
2990
3354
  ({ PGlite } = await import("@electric-sql/pglite"));
2991
3355
  const [uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch] = await Promise.all([
@@ -2998,19 +3362,48 @@ async function createPgliteEngine(dataDir) {
2998
3362
  import("@electric-sql/pglite/contrib/fuzzystrmatch").then((m) => m.fuzzystrmatch)
2999
3363
  ]);
3000
3364
  extensions = { uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch };
3001
- } catch (e) {
3365
+ } catch (error) {
3002
3366
  await releaseLock();
3003
- if (e instanceof Error && /wasm/.test(e.message))
3004
- throw e;
3367
+ if (error instanceof Error && /wasm/.test(error.message))
3368
+ throw error;
3005
3369
  throw new Error("the PGlite WASM engine is not available in this build");
3006
3370
  }
3371
+ if (standaloneAssets) {
3372
+ try {
3373
+ const standaloneBundles = await standaloneAssets.prepareExtensionBundles();
3374
+ cleanupStandaloneBundles = standaloneBundles.cleanup;
3375
+ extensions = {
3376
+ uuid_ossp: withEmbeddedBundle(extensions.uuid_ossp, standaloneBundles.bundles.uuid_ossp),
3377
+ pgcrypto: withEmbeddedBundle(extensions.pgcrypto, standaloneBundles.bundles.pgcrypto),
3378
+ citext: withEmbeddedBundle(extensions.citext, standaloneBundles.bundles.citext),
3379
+ pg_trgm: withEmbeddedBundle(extensions.pg_trgm, standaloneBundles.bundles.pg_trgm),
3380
+ ltree: withEmbeddedBundle(extensions.ltree, standaloneBundles.bundles.ltree),
3381
+ hstore: withEmbeddedBundle(extensions.hstore, standaloneBundles.bundles.hstore),
3382
+ fuzzystrmatch: withEmbeddedBundle(extensions.fuzzystrmatch, standaloneBundles.bundles.fuzzystrmatch)
3383
+ };
3384
+ } catch (error) {
3385
+ await removePreparedBundles(cleanupStandaloneBundles);
3386
+ await releaseLock();
3387
+ throw error;
3388
+ }
3389
+ }
3007
3390
  let pg;
3008
3391
  try {
3009
- pg = new PGlite({ dataDir, extensions });
3392
+ pg = new PGlite({
3393
+ dataDir,
3394
+ extensions,
3395
+ ...standaloneAssets ? {
3396
+ pgliteWasmModule: standaloneAssets.pgliteWasmModule,
3397
+ initdbWasmModule: standaloneAssets.initdbWasmModule,
3398
+ fsBundle: standaloneAssets.fsBundle
3399
+ } : {}
3400
+ });
3010
3401
  await pg.waitReady;
3011
3402
  } catch (error) {
3012
3403
  await releaseLock();
3013
3404
  throw error;
3405
+ } finally {
3406
+ await removePreparedBundles(cleanupStandaloneBundles);
3014
3407
  }
3015
3408
  let closed = false;
3016
3409
  return {
@@ -3049,6 +3442,25 @@ async function createPgliteEngine(dataDir) {
3049
3442
  }
3050
3443
  };
3051
3444
  }
3445
+ function getStandaloneAssets() {
3446
+ return globalThis[STANDALONE_PGLITE_ASSETS];
3447
+ }
3448
+ function withEmbeddedBundle(extension, bundlePath) {
3449
+ return {
3450
+ ...extension,
3451
+ setup: async (pg, emscriptenOpts, clientOnly) => ({
3452
+ ...await extension.setup(pg, emscriptenOpts, clientOnly),
3453
+ bundlePath
3454
+ })
3455
+ };
3456
+ }
3457
+ async function removePreparedBundles(cleanup) {
3458
+ try {
3459
+ await cleanup();
3460
+ } catch (error) {
3461
+ console.error("Unable to remove temporary PGlite extension bundles:", error);
3462
+ }
3463
+ }
3052
3464
  async function acquireDataDirLock(dataDir) {
3053
3465
  if (!dataDir || dataDir.includes("://"))
3054
3466
  return async () => {};
@@ -3056,52 +3468,56 @@ async function acquireDataDirLock(dataDir) {
3056
3468
  const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
3057
3469
  await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
3058
3470
  const nonce = crypto.randomUUID();
3059
- let handle;
3060
- try {
3061
- handle = await open(lockPath, "wx", 384);
3062
- await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
3063
- `);
3064
- } catch (error) {
3065
- await handle?.close().catch(() => {});
3066
- if (error.code !== "EEXIST")
3067
- throw error;
3068
- const owner = await readLockOwner(lockPath);
3069
- if (owner && isProcessAlive(owner.pid)) {
3070
- throw new Error(`PGlite data directory is already in use: ${absoluteDataDir} (pid ${owner.pid})`);
3071
- }
3072
- throw new Error(`PGlite data directory has a stale or unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
3073
- }
3471
+ const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
3074
3472
  let released = false;
3075
3473
  return async () => {
3076
3474
  if (released)
3077
3475
  return;
3078
3476
  released = true;
3079
3477
  await handle?.close();
3080
- const owner = await readLockOwner(lockPath);
3478
+ const owner = await readDataDirLockOwner(lockPath);
3081
3479
  if (owner?.nonce !== nonce)
3082
3480
  return;
3083
- await unlink(lockPath).catch((error) => {
3481
+ await unlink2(lockPath).catch((error) => {
3084
3482
  if (error.code !== "ENOENT")
3085
3483
  throw error;
3086
3484
  });
3087
3485
  };
3088
3486
  }
3089
- async function readLockOwner(lockPath) {
3090
- try {
3091
- const value = JSON.parse(await readFile(lockPath, "utf8"));
3092
- return typeof value.pid === "number" && Number.isInteger(value.pid) && typeof value.nonce === "string" ? { pid: value.pid, nonce: value.nonce } : null;
3093
- } catch {
3094
- return null;
3487
+ async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
3488
+ for (let attempt = 0;attempt < 3; attempt++) {
3489
+ try {
3490
+ return await writeDataDirLock(lockPath, nonce);
3491
+ } catch (error) {
3492
+ if (error.code !== "EEXIST")
3493
+ throw error;
3494
+ const lockState = await recoverStaleDataDirLock(lockPath);
3495
+ if (lockState.kind === "active")
3496
+ throw lockInUseError(absoluteDataDir, lockState.owner.pid);
3497
+ if (lockState.kind === "unreadable")
3498
+ throw unreadableLockError(lockPath);
3499
+ }
3095
3500
  }
3501
+ throw unreadableLockError(lockPath);
3096
3502
  }
3097
- function isProcessAlive(pid) {
3503
+ async function writeDataDirLock(lockPath, nonce) {
3504
+ const handle = await open(lockPath, "wx", 384);
3098
3505
  try {
3099
- process.kill(pid, 0);
3100
- return true;
3506
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
3507
+ `);
3508
+ return handle;
3101
3509
  } catch (error) {
3102
- return error.code === "EPERM";
3510
+ await handle.close().catch(() => {});
3511
+ await unlink2(lockPath).catch(() => {});
3512
+ throw error;
3103
3513
  }
3104
3514
  }
3515
+ function lockInUseError(dataDir, pid) {
3516
+ return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
3517
+ }
3518
+ function unreadableLockError(lockPath) {
3519
+ return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
3520
+ }
3105
3521
 
3106
3522
  // src/vendor/tinbase/db/database.ts
3107
3523
  var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
@@ -3138,6 +3554,9 @@ class Database {
3138
3554
  exec(sql) {
3139
3555
  return this.engine.exec(sql);
3140
3556
  }
3557
+ transaction(fn) {
3558
+ return this.engine.transaction((tx) => fn((sql, params) => tx.query(sql, params)));
3559
+ }
3141
3560
  async withContext(ctx, fn) {
3142
3561
  return this.engine.transaction(async (tx) => {
3143
3562
  await tx.query(`select set_config('role', $1, true),
@@ -6906,6 +7325,7 @@ class RetentionService {
6906
7325
  await this.run(`delete from auth.one_time_tokens where expires_at < now()`);
6907
7326
  await this.run(`delete from auth.mfa_challenges where expires_at < now()`);
6908
7327
  await this.run(`delete from auth.flow_state where expires_at < now()`);
7328
+ await this.run(`delete from public.supacloud_pgredis_kv where expires_at <= now()`);
6909
7329
  if (this.refreshTokenDays > 0) {
6910
7330
  const cutoff = new Date(now.getTime() - this.refreshTokenDays * DAY_MS).toISOString();
6911
7331
  await this.run(`delete from auth.refresh_tokens where revoked = true and updated_at < $1`, [cutoff]);
@@ -7389,11 +7809,13 @@ async function createBackend(config = {}) {
7389
7809
  } catch (e) {
7390
7810
  await failStartup(e);
7391
7811
  }
7812
+ const pgredis = await PgredisCache.create(db).catch(failStartup);
7392
7813
  const now = Math.floor(Date.now() / 1000);
7393
7814
  const tenYears = 10 * 365 * 24 * 3600;
7394
7815
  const anonKey = await signJwt({ iss: "supabase", ref: "local", role: "anon", iat: now, exp: now + tenYears }, jwtSecret);
7395
7816
  const serviceRoleKey = await signJwt({ iss: "supabase", ref: "local", role: "service_role", iat: now, exp: now + tenYears }, jwtSecret);
7396
- const rest = new RestHandler(db, { exposedSchemas: config.dbSchemas, maxRows: config.maxRows });
7817
+ const exposedSchemas = [...new Set(config.dbSchemas ?? ["public", "pgmq_public"])];
7818
+ const rest = new RestHandler(db, { exposedSchemas, maxRows: config.maxRows });
7397
7819
  const exposed = isNetworkExposed(config.host);
7398
7820
  const inbox = config.mailer || exposed ? null : new InboxMailer((msg) => log(config.logMailBody ? `[mail] to=${msg.to} subject="${msg.subject}"
7399
7821
  ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
@@ -7454,7 +7876,12 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
7454
7876
  ...config.functionEnv ?? {}
7455
7877
  };
7456
7878
  installDenoShim();
7457
- const functions = new FunctionsHandler(fnMap, fnEnv);
7879
+ try {
7880
+ installPgredisShim();
7881
+ } catch (error) {
7882
+ await failStartup(error);
7883
+ }
7884
+ const functions = new FunctionsHandler(fnMap, fnEnv, pgredis);
7458
7885
  async function resolveContext(req, url) {
7459
7886
  const authz = req.headers.get("authorization");
7460
7887
  const bearer = authz?.toLowerCase().startsWith("bearer ") ? authz.slice(7) : null;
@@ -7732,7 +8159,7 @@ function isNotFound(error) {
7732
8159
  }
7733
8160
 
7734
8161
  // src/project-runtime.ts
7735
- import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
8162
+ import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink3, writeFile as writeFile4 } from "fs/promises";
7736
8163
  import { dirname as dirname3, isAbsolute, join as join7, parse, relative, resolve as resolve2 } from "path";
7737
8164
 
7738
8165
  // src/vendor/tinbase/node/bun-server.ts
@@ -8548,7 +8975,7 @@ async function ensureProjectSecrets(paths) {
8548
8975
  throw error2;
8549
8976
  stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
8550
8977
  } finally {
8551
- await unlink2(temporaryFile).catch((error2) => {
8978
+ await unlink3(temporaryFile).catch((error2) => {
8552
8979
  if (error2.code !== "ENOENT")
8553
8980
  throw error2;
8554
8981
  });
@@ -8923,13 +9350,12 @@ async function assertNoDataDirectoryLock(paths) {
8923
9350
  if (!paths.dataDir)
8924
9351
  return;
8925
9352
  const lockPath = `${paths.dataDir}.supacloud-lite.lock`;
8926
- try {
8927
- await lstat2(lockPath);
8928
- throw new Error(`data directory is in use or has a stale lock: ${lockPath}; stop Lite and remove the lock manually if it is stale`);
8929
- } catch (error) {
8930
- if (error.code !== "ENOENT")
8931
- throw error;
8932
- }
9353
+ const lockState = await recoverStaleDataDirLock(lockPath);
9354
+ if (lockState.kind === "missing")
9355
+ return;
9356
+ if (lockState.kind === "active")
9357
+ throw new Error(`data directory is already in use: ${paths.dataDir} (pid ${lockState.owner.pid})`);
9358
+ throw new Error(`data directory has an unreadable lock: ${lockPath}; confirm Lite is stopped, then remove the lock manually`);
8933
9359
  }
8934
9360
  async function stageDirectory(root, destination) {
8935
9361
  try {
@@ -9123,6 +9549,20 @@ function pathsOverlap(left, right) {
9123
9549
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
9124
9550
  }
9125
9551
 
9552
+ // src/shutdown.ts
9553
+ async function waitForShutdown(closeProject, signalSource = process) {
9554
+ await new Promise((resolveShutdown) => {
9555
+ const resolveOnSignal = () => {
9556
+ signalSource.off("SIGINT", resolveOnSignal);
9557
+ signalSource.off("SIGTERM", resolveOnSignal);
9558
+ resolveShutdown();
9559
+ };
9560
+ signalSource.once("SIGINT", resolveOnSignal);
9561
+ signalSource.once("SIGTERM", resolveOnSignal);
9562
+ });
9563
+ await closeProject();
9564
+ }
9565
+
9126
9566
  // src/cli.ts
9127
9567
  function parseArgs(argv) {
9128
9568
  const args = [...argv];
@@ -9287,8 +9727,8 @@ Use --service-role to print the privileged key.`);
9287
9727
  Run "supacloud-lite keys" for the anon key.
9288
9728
  Run "supacloud-lite keys --service-role" only when privileged access is required.
9289
9729
  `);
9290
- await waitForShutdown();
9291
- await project.close();
9730
+ await waitForShutdown(() => project.close());
9731
+ process.exitCode = 0;
9292
9732
  }
9293
9733
  async function runDbCommand(options) {
9294
9734
  const subcommand = options.positionals[0];
@@ -9417,13 +9857,6 @@ function timestamp() {
9417
9857
  return new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
9418
9858
  }
9419
9859
  function quietLog() {}
9420
- async function waitForShutdown() {
9421
- await new Promise((resolveShutdown) => {
9422
- const stop = () => resolveShutdown();
9423
- process.once("SIGINT", stop);
9424
- process.once("SIGTERM", stop);
9425
- });
9426
- }
9427
9860
  function printHelp() {
9428
9861
  console.log(`supacloud-lite - Bun-native Supabase-compatible backend on PGlite
9429
9862