@supacloud/lite 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/LICENSES/BUN-1.3.14-RUNTIME-NOTICES.txt +78 -0
- package/LICENSES/CHOWNR-BLUEOAK-1.0.0.txt +63 -0
- package/LICENSES/FS-MINIPASS-ISC.txt +15 -0
- package/LICENSES/MINIPASS-BLUEOAK-1.0.0.txt +55 -0
- package/LICENSES/MINIZLIB-MIT.txt +26 -0
- package/LICENSES/NODE-TAR-BLUEOAK-1.0.0.txt +55 -0
- package/LICENSES/YALLIST-BLUEOAK-1.0.0.txt +63 -0
- package/README.md +74 -4
- package/RELEASING.md +14 -1
- package/THIRD_PARTY_NOTICES.md +38 -2
- package/dist/cli.js +487 -85
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +477 -78
- package/dist/launcher.cjs +22 -0
- package/dist/standalone-assets-protocol.d.ts +13 -0
- package/dist/standalone-assets-protocol.d.ts.map +1 -0
- package/dist/vendor/tinbase/auth/handler.d.ts +4 -0
- package/dist/vendor/tinbase/auth/handler.d.ts.map +1 -1
- package/dist/vendor/tinbase/db/database.d.ts +5 -1
- package/dist/vendor/tinbase/db/database.d.ts.map +1 -1
- package/dist/vendor/tinbase/db/emulated.d.ts +1 -1
- package/dist/vendor/tinbase/db/emulated.d.ts.map +1 -1
- package/dist/vendor/tinbase/db/pglite-engine.d.ts.map +1 -1
- package/dist/vendor/tinbase/functions/handler.d.ts +3 -1
- package/dist/vendor/tinbase/functions/handler.d.ts.map +1 -1
- package/dist/vendor/tinbase/functions/pgredis.d.ts +26 -0
- package/dist/vendor/tinbase/functions/pgredis.d.ts.map +1 -0
- package/dist/vendor/tinbase/index.d.ts +1 -0
- package/dist/vendor/tinbase/index.d.ts.map +1 -1
- package/dist/vendor/tinbase/retention/service.d.ts.map +1 -1
- 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.
|
|
11
|
+
version: "0.5.0",
|
|
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/
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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_' ||
|
|
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_' ||
|
|
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
|
-
|
|
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_' ||
|
|
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_' ||
|
|
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_' ||
|
|
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_' ||
|
|
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
|
-
|
|
2637
|
-
|
|
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_' ||
|
|
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
|
|
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
|
-
|
|
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;
|
|
@@ -2983,9 +3292,16 @@ function pickTag(text, base) {
|
|
|
2983
3292
|
// src/vendor/tinbase/db/pglite-engine.ts
|
|
2984
3293
|
import { mkdir, open, readFile, unlink } from "fs/promises";
|
|
2985
3294
|
import { dirname, resolve } from "path";
|
|
3295
|
+
|
|
3296
|
+
// src/standalone-assets-protocol.ts
|
|
3297
|
+
var STANDALONE_PGLITE_ASSETS = Symbol.for("supacloud-lite.pglite-standalone-assets");
|
|
3298
|
+
|
|
3299
|
+
// src/vendor/tinbase/db/pglite-engine.ts
|
|
2986
3300
|
async function createPgliteEngine(dataDir) {
|
|
2987
3301
|
const releaseLock = await acquireDataDirLock(dataDir);
|
|
2988
3302
|
let PGlite, extensions;
|
|
3303
|
+
const standaloneAssets = getStandaloneAssets();
|
|
3304
|
+
let cleanupStandaloneBundles = async () => {};
|
|
2989
3305
|
try {
|
|
2990
3306
|
({ PGlite } = await import("@electric-sql/pglite"));
|
|
2991
3307
|
const [uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch] = await Promise.all([
|
|
@@ -2998,19 +3314,48 @@ async function createPgliteEngine(dataDir) {
|
|
|
2998
3314
|
import("@electric-sql/pglite/contrib/fuzzystrmatch").then((m) => m.fuzzystrmatch)
|
|
2999
3315
|
]);
|
|
3000
3316
|
extensions = { uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch };
|
|
3001
|
-
} catch (
|
|
3317
|
+
} catch (error) {
|
|
3002
3318
|
await releaseLock();
|
|
3003
|
-
if (
|
|
3004
|
-
throw
|
|
3319
|
+
if (error instanceof Error && /wasm/.test(error.message))
|
|
3320
|
+
throw error;
|
|
3005
3321
|
throw new Error("the PGlite WASM engine is not available in this build");
|
|
3006
3322
|
}
|
|
3323
|
+
if (standaloneAssets) {
|
|
3324
|
+
try {
|
|
3325
|
+
const standaloneBundles = await standaloneAssets.prepareExtensionBundles();
|
|
3326
|
+
cleanupStandaloneBundles = standaloneBundles.cleanup;
|
|
3327
|
+
extensions = {
|
|
3328
|
+
uuid_ossp: withEmbeddedBundle(extensions.uuid_ossp, standaloneBundles.bundles.uuid_ossp),
|
|
3329
|
+
pgcrypto: withEmbeddedBundle(extensions.pgcrypto, standaloneBundles.bundles.pgcrypto),
|
|
3330
|
+
citext: withEmbeddedBundle(extensions.citext, standaloneBundles.bundles.citext),
|
|
3331
|
+
pg_trgm: withEmbeddedBundle(extensions.pg_trgm, standaloneBundles.bundles.pg_trgm),
|
|
3332
|
+
ltree: withEmbeddedBundle(extensions.ltree, standaloneBundles.bundles.ltree),
|
|
3333
|
+
hstore: withEmbeddedBundle(extensions.hstore, standaloneBundles.bundles.hstore),
|
|
3334
|
+
fuzzystrmatch: withEmbeddedBundle(extensions.fuzzystrmatch, standaloneBundles.bundles.fuzzystrmatch)
|
|
3335
|
+
};
|
|
3336
|
+
} catch (error) {
|
|
3337
|
+
await removePreparedBundles(cleanupStandaloneBundles);
|
|
3338
|
+
await releaseLock();
|
|
3339
|
+
throw error;
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3007
3342
|
let pg;
|
|
3008
3343
|
try {
|
|
3009
|
-
pg = new PGlite({
|
|
3344
|
+
pg = new PGlite({
|
|
3345
|
+
dataDir,
|
|
3346
|
+
extensions,
|
|
3347
|
+
...standaloneAssets ? {
|
|
3348
|
+
pgliteWasmModule: standaloneAssets.pgliteWasmModule,
|
|
3349
|
+
initdbWasmModule: standaloneAssets.initdbWasmModule,
|
|
3350
|
+
fsBundle: standaloneAssets.fsBundle
|
|
3351
|
+
} : {}
|
|
3352
|
+
});
|
|
3010
3353
|
await pg.waitReady;
|
|
3011
3354
|
} catch (error) {
|
|
3012
3355
|
await releaseLock();
|
|
3013
3356
|
throw error;
|
|
3357
|
+
} finally {
|
|
3358
|
+
await removePreparedBundles(cleanupStandaloneBundles);
|
|
3014
3359
|
}
|
|
3015
3360
|
let closed = false;
|
|
3016
3361
|
return {
|
|
@@ -3049,6 +3394,25 @@ async function createPgliteEngine(dataDir) {
|
|
|
3049
3394
|
}
|
|
3050
3395
|
};
|
|
3051
3396
|
}
|
|
3397
|
+
function getStandaloneAssets() {
|
|
3398
|
+
return globalThis[STANDALONE_PGLITE_ASSETS];
|
|
3399
|
+
}
|
|
3400
|
+
function withEmbeddedBundle(extension, bundlePath) {
|
|
3401
|
+
return {
|
|
3402
|
+
...extension,
|
|
3403
|
+
setup: async (pg, emscriptenOpts, clientOnly) => ({
|
|
3404
|
+
...await extension.setup(pg, emscriptenOpts, clientOnly),
|
|
3405
|
+
bundlePath
|
|
3406
|
+
})
|
|
3407
|
+
};
|
|
3408
|
+
}
|
|
3409
|
+
async function removePreparedBundles(cleanup) {
|
|
3410
|
+
try {
|
|
3411
|
+
await cleanup();
|
|
3412
|
+
} catch (error) {
|
|
3413
|
+
console.error("Unable to remove temporary PGlite extension bundles:", error);
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3052
3416
|
async function acquireDataDirLock(dataDir) {
|
|
3053
3417
|
if (!dataDir || dataDir.includes("://"))
|
|
3054
3418
|
return async () => {};
|
|
@@ -3056,21 +3420,7 @@ async function acquireDataDirLock(dataDir) {
|
|
|
3056
3420
|
const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
|
|
3057
3421
|
await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
|
|
3058
3422
|
const nonce = crypto.randomUUID();
|
|
3059
|
-
|
|
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
|
-
}
|
|
3423
|
+
const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
|
|
3074
3424
|
let released = false;
|
|
3075
3425
|
return async () => {
|
|
3076
3426
|
if (released)
|
|
@@ -3086,10 +3436,48 @@ async function acquireDataDirLock(dataDir) {
|
|
|
3086
3436
|
});
|
|
3087
3437
|
};
|
|
3088
3438
|
}
|
|
3439
|
+
async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
|
|
3440
|
+
for (let attempt = 0;attempt < 2; attempt++) {
|
|
3441
|
+
try {
|
|
3442
|
+
return await writeDataDirLock(lockPath, nonce);
|
|
3443
|
+
} catch (error) {
|
|
3444
|
+
if (error.code !== "EEXIST")
|
|
3445
|
+
throw error;
|
|
3446
|
+
const owner = await readLockOwner(lockPath);
|
|
3447
|
+
if (owner && isProcessAlive(owner.pid))
|
|
3448
|
+
throw lockInUseError(absoluteDataDir, owner.pid);
|
|
3449
|
+
if (!owner || attempt > 0)
|
|
3450
|
+
throw unreadableLockError(lockPath);
|
|
3451
|
+
await unlink(lockPath).catch((unlinkError) => {
|
|
3452
|
+
if (unlinkError.code !== "ENOENT")
|
|
3453
|
+
throw unlinkError;
|
|
3454
|
+
});
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
throw unreadableLockError(lockPath);
|
|
3458
|
+
}
|
|
3459
|
+
async function writeDataDirLock(lockPath, nonce) {
|
|
3460
|
+
const handle = await open(lockPath, "wx", 384);
|
|
3461
|
+
try {
|
|
3462
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
3463
|
+
`);
|
|
3464
|
+
return handle;
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
await handle.close().catch(() => {});
|
|
3467
|
+
await unlink(lockPath).catch(() => {});
|
|
3468
|
+
throw error;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
function lockInUseError(dataDir, pid) {
|
|
3472
|
+
return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
|
|
3473
|
+
}
|
|
3474
|
+
function unreadableLockError(lockPath) {
|
|
3475
|
+
return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
|
|
3476
|
+
}
|
|
3089
3477
|
async function readLockOwner(lockPath) {
|
|
3090
3478
|
try {
|
|
3091
3479
|
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;
|
|
3480
|
+
return typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.nonce === "string" ? { pid: value.pid, nonce: value.nonce } : null;
|
|
3093
3481
|
} catch {
|
|
3094
3482
|
return null;
|
|
3095
3483
|
}
|
|
@@ -3138,6 +3526,9 @@ class Database {
|
|
|
3138
3526
|
exec(sql) {
|
|
3139
3527
|
return this.engine.exec(sql);
|
|
3140
3528
|
}
|
|
3529
|
+
transaction(fn) {
|
|
3530
|
+
return this.engine.transaction((tx) => fn((sql, params) => tx.query(sql, params)));
|
|
3531
|
+
}
|
|
3141
3532
|
async withContext(ctx, fn) {
|
|
3142
3533
|
return this.engine.transaction(async (tx) => {
|
|
3143
3534
|
await tx.query(`select set_config('role', $1, true),
|
|
@@ -6906,6 +7297,7 @@ class RetentionService {
|
|
|
6906
7297
|
await this.run(`delete from auth.one_time_tokens where expires_at < now()`);
|
|
6907
7298
|
await this.run(`delete from auth.mfa_challenges where expires_at < now()`);
|
|
6908
7299
|
await this.run(`delete from auth.flow_state where expires_at < now()`);
|
|
7300
|
+
await this.run(`delete from public.supacloud_pgredis_kv where expires_at <= now()`);
|
|
6909
7301
|
if (this.refreshTokenDays > 0) {
|
|
6910
7302
|
const cutoff = new Date(now.getTime() - this.refreshTokenDays * DAY_MS).toISOString();
|
|
6911
7303
|
await this.run(`delete from auth.refresh_tokens where revoked = true and updated_at < $1`, [cutoff]);
|
|
@@ -7389,11 +7781,13 @@ async function createBackend(config = {}) {
|
|
|
7389
7781
|
} catch (e) {
|
|
7390
7782
|
await failStartup(e);
|
|
7391
7783
|
}
|
|
7784
|
+
const pgredis = await PgredisCache.create(db).catch(failStartup);
|
|
7392
7785
|
const now = Math.floor(Date.now() / 1000);
|
|
7393
7786
|
const tenYears = 10 * 365 * 24 * 3600;
|
|
7394
7787
|
const anonKey = await signJwt({ iss: "supabase", ref: "local", role: "anon", iat: now, exp: now + tenYears }, jwtSecret);
|
|
7395
7788
|
const serviceRoleKey = await signJwt({ iss: "supabase", ref: "local", role: "service_role", iat: now, exp: now + tenYears }, jwtSecret);
|
|
7396
|
-
const
|
|
7789
|
+
const exposedSchemas = [...new Set(config.dbSchemas ?? ["public", "pgmq_public"])];
|
|
7790
|
+
const rest = new RestHandler(db, { exposedSchemas, maxRows: config.maxRows });
|
|
7397
7791
|
const exposed = isNetworkExposed(config.host);
|
|
7398
7792
|
const inbox = config.mailer || exposed ? null : new InboxMailer((msg) => log(config.logMailBody ? `[mail] to=${msg.to} subject="${msg.subject}"
|
|
7399
7793
|
${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
@@ -7454,7 +7848,12 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
7454
7848
|
...config.functionEnv ?? {}
|
|
7455
7849
|
};
|
|
7456
7850
|
installDenoShim();
|
|
7457
|
-
|
|
7851
|
+
try {
|
|
7852
|
+
installPgredisShim();
|
|
7853
|
+
} catch (error) {
|
|
7854
|
+
await failStartup(error);
|
|
7855
|
+
}
|
|
7856
|
+
const functions = new FunctionsHandler(fnMap, fnEnv, pgredis);
|
|
7458
7857
|
async function resolveContext(req, url) {
|
|
7459
7858
|
const authz = req.headers.get("authorization");
|
|
7460
7859
|
const bearer = authz?.toLowerCase().startsWith("bearer ") ? authz.slice(7) : null;
|
|
@@ -9287,8 +9686,7 @@ Use --service-role to print the privileged key.`);
|
|
|
9287
9686
|
Run "supacloud-lite keys" for the anon key.
|
|
9288
9687
|
Run "supacloud-lite keys --service-role" only when privileged access is required.
|
|
9289
9688
|
`);
|
|
9290
|
-
await waitForShutdown();
|
|
9291
|
-
await project.close();
|
|
9689
|
+
await waitForShutdown(() => project.close());
|
|
9292
9690
|
}
|
|
9293
9691
|
async function runDbCommand(options) {
|
|
9294
9692
|
const subcommand = options.positionals[0];
|
|
@@ -9417,11 +9815,15 @@ function timestamp() {
|
|
|
9417
9815
|
return new Date().toISOString().replace(/[-:T]/g, "").slice(0, 14);
|
|
9418
9816
|
}
|
|
9419
9817
|
function quietLog() {}
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
const
|
|
9423
|
-
|
|
9424
|
-
|
|
9818
|
+
function waitForShutdown(closeProject) {
|
|
9819
|
+
return new Promise((resolveShutdown, rejectShutdown) => {
|
|
9820
|
+
const closeOnSignal = () => {
|
|
9821
|
+
process.off("SIGINT", closeOnSignal);
|
|
9822
|
+
process.off("SIGTERM", closeOnSignal);
|
|
9823
|
+
closeProject().then(resolveShutdown, rejectShutdown);
|
|
9824
|
+
};
|
|
9825
|
+
process.once("SIGINT", closeOnSignal);
|
|
9826
|
+
process.once("SIGTERM", closeOnSignal);
|
|
9425
9827
|
});
|
|
9426
9828
|
}
|
|
9427
9829
|
function printHelp() {
|