@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.
- package/CHANGELOG.md +28 -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 +541 -108
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +525 -99
- package/dist/launcher.cjs +35 -0
- package/dist/snapshot.d.ts.map +1 -1
- 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/data-dir-lock.d.ts +18 -0
- package/dist/vendor/tinbase/db/data-dir-lock.d.ts.map +1 -0
- 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/index.js
CHANGED
|
@@ -71,12 +71,12 @@ function randomToken(bytes = 32) {
|
|
|
71
71
|
// package.json
|
|
72
72
|
var package_default = {
|
|
73
73
|
name: "@supacloud/lite",
|
|
74
|
-
version: "0.
|
|
74
|
+
version: "0.5.1",
|
|
75
75
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
|
|
76
76
|
type: "module",
|
|
77
77
|
license: "Apache-2.0",
|
|
78
78
|
bin: {
|
|
79
|
-
"supacloud-lite": "dist/
|
|
79
|
+
"supacloud-lite": "dist/launcher.cjs"
|
|
80
80
|
},
|
|
81
81
|
exports: {
|
|
82
82
|
".": {
|
|
@@ -87,6 +87,7 @@ var package_default = {
|
|
|
87
87
|
},
|
|
88
88
|
files: [
|
|
89
89
|
"dist",
|
|
90
|
+
"!dist/standalone",
|
|
90
91
|
"README.md",
|
|
91
92
|
"CHANGELOG.md",
|
|
92
93
|
"RELEASING.md",
|
|
@@ -95,13 +96,19 @@ var package_default = {
|
|
|
95
96
|
],
|
|
96
97
|
scripts: {
|
|
97
98
|
build: "bun run build:js && bun run build:types",
|
|
98
|
-
"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",
|
|
99
|
+
"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",
|
|
100
|
+
"build:standalone:host": "bun run scripts/build-standalone.ts host",
|
|
101
|
+
"build:standalone:linux": "bun run scripts/build-standalone.ts linux-x64 linux-arm64",
|
|
102
|
+
"build:standalone:macos": "bun run scripts/build-standalone.ts macos-x64 macos-arm64",
|
|
103
|
+
"build:standalone:windows": "bun run scripts/build-standalone.ts windows-x64",
|
|
104
|
+
"build:standalone:release": "bun run scripts/build-standalone.ts linux-x64 linux-arm64 macos-x64 macos-arm64 windows-x64",
|
|
99
105
|
"build:types": "bun x tsc --emitDeclarationOnly -p tsconfig.build.json",
|
|
100
|
-
check: "bun run typecheck && bun run test && bun run build && bun run test:package",
|
|
106
|
+
check: "bun run typecheck && bun run test && bun run build && bun run test:package && bun run build:standalone:host && bun run test:standalone",
|
|
101
107
|
dev: "bun run src/cli.ts start",
|
|
102
108
|
start: "bun run src/cli.ts start",
|
|
103
109
|
test: "bun test --timeout 20000",
|
|
104
110
|
"test:package": "bun run scripts/package-smoke.ts",
|
|
111
|
+
"test:standalone": "bun run scripts/standalone-smoke.ts",
|
|
105
112
|
prepack: "bun run build",
|
|
106
113
|
typecheck: "bun x tsc --noEmit -p tsconfig.json"
|
|
107
114
|
},
|
|
@@ -1263,30 +1270,7 @@ class AuthHandler {
|
|
|
1263
1270
|
const token = body.refresh_token;
|
|
1264
1271
|
if (!token)
|
|
1265
1272
|
return authError(400, "validation_failed", "refresh_token required");
|
|
1266
|
-
|
|
1267
|
-
join auth.users u on u.id = rt.user_id
|
|
1268
|
-
where rt.token = $1`, [token]);
|
|
1269
|
-
const row = res.rows[0];
|
|
1270
|
-
if (!row || row.revoked) {
|
|
1271
|
-
return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
|
|
1272
|
-
}
|
|
1273
|
-
const now = Date.now();
|
|
1274
|
-
const lastActivity = timestampMs(row.created_at);
|
|
1275
|
-
if (this.config.sessionInactivitySeconds && lastActivity !== null && now - lastActivity >= this.config.sessionInactivitySeconds * 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 due to inactivity");
|
|
1278
|
-
}
|
|
1279
|
-
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;
|
|
1280
|
-
if (this.config.sessionTimeboxSeconds && sessionStartedAt !== null && now - sessionStartedAt >= this.config.sessionTimeboxSeconds * 1000) {
|
|
1281
|
-
await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
|
|
1282
|
-
return authError(400, "session_expired", "Session expired");
|
|
1283
|
-
}
|
|
1284
|
-
await this.db.query(`update auth.refresh_tokens set revoked = true, updated_at = now() where token = $1`, [token]);
|
|
1285
|
-
const ures = await this.db.query(`select * from auth.users where id = $1`, [row.user_id]);
|
|
1286
|
-
return json(200, await this.sessionFor(ures.rows[0], token, {
|
|
1287
|
-
sessionId: row.session_id ?? undefined,
|
|
1288
|
-
sessionStartedAt: sessionStartedAt === null ? undefined : Math.floor(sessionStartedAt / 1000)
|
|
1289
|
-
}));
|
|
1273
|
+
return this.rotateRefreshToken(token);
|
|
1290
1274
|
}
|
|
1291
1275
|
if (grantType === "pkce") {
|
|
1292
1276
|
const authCode = body.auth_code;
|
|
@@ -1710,8 +1694,8 @@ Or sign in with this link: ${link}`
|
|
|
1710
1694
|
return authError(404, "mfa_factor_not_found", "MFA factor not found");
|
|
1711
1695
|
return json(200, { id: factorId });
|
|
1712
1696
|
}
|
|
1713
|
-
async getUserFactors(userId) {
|
|
1714
|
-
const res = await
|
|
1697
|
+
async getUserFactors(userId, query = (sql, params) => this.db.query(sql, params)) {
|
|
1698
|
+
const res = await query(`select id, friendly_name, factor_type, status, created_at, updated_at
|
|
1715
1699
|
from auth.mfa_factors where user_id = $1 order by created_at`, [userId]);
|
|
1716
1700
|
return res.rows.map((f) => ({
|
|
1717
1701
|
id: f.id,
|
|
@@ -1722,8 +1706,8 @@ Or sign in with this link: ${link}`
|
|
|
1722
1706
|
updated_at: iso(f.updated_at)
|
|
1723
1707
|
}));
|
|
1724
1708
|
}
|
|
1725
|
-
async getUserIdentities(userId) {
|
|
1726
|
-
const res = await
|
|
1709
|
+
async getUserIdentities(userId, query = (sql, params) => this.db.query(sql, params)) {
|
|
1710
|
+
const res = await query(`select id, provider_id, user_id, identity_data, provider, created_at, updated_at, last_sign_in_at
|
|
1727
1711
|
from auth.identities where user_id = $1 order by created_at`, [userId]);
|
|
1728
1712
|
return res.rows.map((r) => ({
|
|
1729
1713
|
identity_id: r.id,
|
|
@@ -1736,6 +1720,46 @@ Or sign in with this link: ${link}`
|
|
|
1736
1720
|
updated_at: iso(r.updated_at)
|
|
1737
1721
|
}));
|
|
1738
1722
|
}
|
|
1723
|
+
async rotateRefreshToken(token) {
|
|
1724
|
+
return this.db.transaction(async (query) => {
|
|
1725
|
+
const claimedToken = await this.claimRefreshToken(query, token);
|
|
1726
|
+
if (!claimedToken)
|
|
1727
|
+
return authError(400, "refresh_token_not_found", "Invalid Refresh Token: Refresh Token Not Found");
|
|
1728
|
+
const sessionStartedAt = await this.refreshSessionStartedAt(query, claimedToken);
|
|
1729
|
+
const expiryError = this.refreshExpiryError(claimedToken, sessionStartedAt);
|
|
1730
|
+
if (expiryError)
|
|
1731
|
+
return expiryError;
|
|
1732
|
+
const userResult = await query(`select * from auth.users where id = $1`, [claimedToken.user_id]);
|
|
1733
|
+
const session = await this.sessionFor(userResult.rows[0], token, {
|
|
1734
|
+
sessionId: claimedToken.session_id ?? undefined,
|
|
1735
|
+
sessionStartedAt: sessionStartedAt === null ? undefined : Math.floor(sessionStartedAt / 1000)
|
|
1736
|
+
}, query);
|
|
1737
|
+
return json(200, session);
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
async claimRefreshToken(query, token) {
|
|
1741
|
+
const claimed = await query(`update auth.refresh_tokens set revoked = true, updated_at = now()
|
|
1742
|
+
where token = $1 and revoked is not true
|
|
1743
|
+
returning user_id, session_id, created_at`, [token]);
|
|
1744
|
+
return claimed.rows[0];
|
|
1745
|
+
}
|
|
1746
|
+
async refreshSessionStartedAt(query, refreshToken) {
|
|
1747
|
+
if (!refreshToken.session_id)
|
|
1748
|
+
return timestampMs(refreshToken.created_at);
|
|
1749
|
+
const sessionStart = await query(`select min(created_at) as started_at from auth.refresh_tokens where session_id = $1`, [refreshToken.session_id]);
|
|
1750
|
+
return timestampMs(sessionStart.rows[0]?.started_at ?? null);
|
|
1751
|
+
}
|
|
1752
|
+
refreshExpiryError(refreshToken, sessionStartedAt) {
|
|
1753
|
+
const now = Date.now();
|
|
1754
|
+
const lastActivity = timestampMs(refreshToken.created_at);
|
|
1755
|
+
if (this.config.sessionInactivitySeconds && lastActivity !== null && now - lastActivity >= this.config.sessionInactivitySeconds * 1000) {
|
|
1756
|
+
return authError(400, "session_expired", "Session expired due to inactivity");
|
|
1757
|
+
}
|
|
1758
|
+
if (this.config.sessionTimeboxSeconds && sessionStartedAt !== null && now - sessionStartedAt >= this.config.sessionTimeboxSeconds * 1000) {
|
|
1759
|
+
return authError(400, "session_expired", "Session expired");
|
|
1760
|
+
}
|
|
1761
|
+
return null;
|
|
1762
|
+
}
|
|
1739
1763
|
async userFromBearer(req) {
|
|
1740
1764
|
const claims = await this.claimsFromBearer(req);
|
|
1741
1765
|
if (!claims?.sub)
|
|
@@ -1773,7 +1797,7 @@ Or sign in with this link: ${link}`
|
|
|
1773
1797
|
const session = await this.sessionFor(res.rows[0]);
|
|
1774
1798
|
return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
|
|
1775
1799
|
}
|
|
1776
|
-
async sessionFor(user, parentToken, opts) {
|
|
1800
|
+
async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
|
|
1777
1801
|
const now = Math.floor(Date.now() / 1000);
|
|
1778
1802
|
const timeboxRemaining = this.config.sessionTimeboxSeconds ? opts?.sessionStartedAt !== undefined ? opts.sessionStartedAt + this.config.sessionTimeboxSeconds - now : this.config.sessionTimeboxSeconds : this.config.jwtExpiry;
|
|
1779
1803
|
const lifetime = Math.max(1, Math.min(this.config.jwtExpiry, timeboxRemaining));
|
|
@@ -1797,14 +1821,14 @@ Or sign in with this link: ${link}`
|
|
|
1797
1821
|
};
|
|
1798
1822
|
const accessToken = await signJwt(claims, this.config.jwtSecret);
|
|
1799
1823
|
const refreshToken = randomToken(24);
|
|
1800
|
-
await
|
|
1824
|
+
await query(`insert into auth.refresh_tokens (token, user_id, parent, session_id) values ($1, $2, $3, $4)`, [refreshToken, user.id, parentToken ?? null, sessionId]);
|
|
1801
1825
|
return {
|
|
1802
1826
|
access_token: accessToken,
|
|
1803
1827
|
token_type: "bearer",
|
|
1804
1828
|
expires_in: lifetime,
|
|
1805
1829
|
expires_at: expiresAt,
|
|
1806
1830
|
refresh_token: refreshToken,
|
|
1807
|
-
user: this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id))
|
|
1831
|
+
user: this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query))
|
|
1808
1832
|
};
|
|
1809
1833
|
}
|
|
1810
1834
|
}
|
|
@@ -1990,13 +2014,182 @@ function resetCapturedHandler() {
|
|
|
1990
2014
|
captured.handler = undefined;
|
|
1991
2015
|
}
|
|
1992
2016
|
|
|
2017
|
+
// src/vendor/tinbase/functions/pgredis.ts
|
|
2018
|
+
import { AsyncLocalStorage as AsyncLocalStorage2 } from "async_hooks";
|
|
2019
|
+
var CACHE_NAMESPACE = "supacloud-edge-runtime";
|
|
2020
|
+
var CACHE_TABLE = "public.supacloud_pgredis_kv";
|
|
2021
|
+
var MAX_KEY_CHARACTERS = 512;
|
|
2022
|
+
var MAX_VALUE_BYTES = 1048576;
|
|
2023
|
+
var MAX_TTL_MS = 31536000000;
|
|
2024
|
+
var CACHE_SCHEMA_SQL = `
|
|
2025
|
+
create table if not exists ${CACHE_TABLE} (
|
|
2026
|
+
namespace text not null,
|
|
2027
|
+
key text not null,
|
|
2028
|
+
value jsonb not null,
|
|
2029
|
+
expires_at timestamptz,
|
|
2030
|
+
updated_at timestamptz not null default now(),
|
|
2031
|
+
primary key (namespace, key)
|
|
2032
|
+
);
|
|
2033
|
+
|
|
2034
|
+
create index if not exists supacloud_pgredis_kv_expires_at_idx
|
|
2035
|
+
on ${CACHE_TABLE} (expires_at) where expires_at is not null;
|
|
2036
|
+
`;
|
|
2037
|
+
var CACHE_PERMISSIONS_SQL = `
|
|
2038
|
+
revoke all on table ${CACHE_TABLE} from public, anon, authenticated, service_role;
|
|
2039
|
+
`;
|
|
2040
|
+
|
|
2041
|
+
class PgredisCache {
|
|
2042
|
+
db;
|
|
2043
|
+
constructor(db) {
|
|
2044
|
+
this.db = db;
|
|
2045
|
+
}
|
|
2046
|
+
static async create(db) {
|
|
2047
|
+
await db.exec(CACHE_SCHEMA_SQL);
|
|
2048
|
+
if (!db.engine.minimalBootstrap)
|
|
2049
|
+
await db.exec(CACHE_PERMISSIONS_SQL);
|
|
2050
|
+
return new PgredisCache(db);
|
|
2051
|
+
}
|
|
2052
|
+
async get(key) {
|
|
2053
|
+
assertCacheKey(key);
|
|
2054
|
+
return this.db.transaction(async (query) => {
|
|
2055
|
+
await deleteExpiredKey(query, key);
|
|
2056
|
+
const stored = await query(`select value::text as serialized_value from ${CACHE_TABLE} where namespace = $1 and key = $2`, [CACHE_NAMESPACE, key]);
|
|
2057
|
+
return stored.rows[0] ? parseStoredValue(stored.rows[0]) : null;
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
async set(key, cacheValue, ttlMs) {
|
|
2061
|
+
assertCacheKey(key);
|
|
2062
|
+
assertTtl(ttlMs);
|
|
2063
|
+
const serializedValue = serializeCacheValue(cacheValue);
|
|
2064
|
+
await this.db.query(`insert into ${CACHE_TABLE} (namespace, key, value, expires_at, updated_at)
|
|
2065
|
+
values ($1, $2, $3::jsonb,
|
|
2066
|
+
case when $4::bigint is null then null else clock_timestamp() + $4::double precision * interval '1 millisecond' end,
|
|
2067
|
+
now())
|
|
2068
|
+
on conflict (namespace, key) do update set
|
|
2069
|
+
value = excluded.value, expires_at = excluded.expires_at, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue, ttlMs ?? null]);
|
|
2070
|
+
return true;
|
|
2071
|
+
}
|
|
2072
|
+
async delete(key) {
|
|
2073
|
+
assertCacheKey(key);
|
|
2074
|
+
return this.db.transaction(async (query) => {
|
|
2075
|
+
await deleteExpiredKey(query, key);
|
|
2076
|
+
const deleted = await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 returning key`, [CACHE_NAMESPACE, key]);
|
|
2077
|
+
return deleted.rows.length > 0;
|
|
2078
|
+
});
|
|
2079
|
+
}
|
|
2080
|
+
async ttl(key) {
|
|
2081
|
+
assertCacheKey(key);
|
|
2082
|
+
return this.db.transaction(async (query) => {
|
|
2083
|
+
await deleteExpiredKey(query, key);
|
|
2084
|
+
const stored = await query(`select case when expires_at is null then null
|
|
2085
|
+
else greatest(0, ceil(extract(epoch from (expires_at - clock_timestamp())) * 1000))::bigint
|
|
2086
|
+
end as ttl_ms from ${CACHE_TABLE} where namespace = $1 and key = $2`, [CACHE_NAMESPACE, key]);
|
|
2087
|
+
const ttlMs = stored.rows[0]?.ttl_ms;
|
|
2088
|
+
return ttlMs === null || ttlMs === undefined ? null : Number(ttlMs);
|
|
2089
|
+
});
|
|
2090
|
+
}
|
|
2091
|
+
async getset(key, cacheValue) {
|
|
2092
|
+
assertCacheKey(key);
|
|
2093
|
+
const serializedValue = serializeCacheValue(cacheValue);
|
|
2094
|
+
return this.db.transaction(async (query) => {
|
|
2095
|
+
await deleteExpiredKey(query, key);
|
|
2096
|
+
const previous = await query(`select value::text as serialized_value from ${CACHE_TABLE}
|
|
2097
|
+
where namespace = $1 and key = $2 for update`, [CACHE_NAMESPACE, key]);
|
|
2098
|
+
await upsertWithoutTtl(query, key, serializedValue);
|
|
2099
|
+
return previous.rows[0] ? parseStoredValue(previous.rows[0]) : null;
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
async getdel(key) {
|
|
2103
|
+
assertCacheKey(key);
|
|
2104
|
+
return this.db.transaction(async (query) => {
|
|
2105
|
+
await deleteExpiredKey(query, key);
|
|
2106
|
+
const deleted = await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 returning value::text as serialized_value`, [CACHE_NAMESPACE, key]);
|
|
2107
|
+
return deleted.rows[0] ? parseStoredValue(deleted.rows[0]) : null;
|
|
2108
|
+
});
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
var cacheContexts = new AsyncLocalStorage2;
|
|
2112
|
+
var cacheFacade = Object.freeze({
|
|
2113
|
+
get: async (key) => activeCache().get(key),
|
|
2114
|
+
set: async (key, cacheValue, ttlMs) => activeCache().set(key, cacheValue, ttlMs),
|
|
2115
|
+
delete: async (key) => activeCache().delete(key),
|
|
2116
|
+
ttl: async (key) => activeCache().ttl(key),
|
|
2117
|
+
getset: async (key, cacheValue) => activeCache().getset(key, cacheValue),
|
|
2118
|
+
getdel: async (key) => activeCache().getdel(key)
|
|
2119
|
+
});
|
|
2120
|
+
function installPgredisShim() {
|
|
2121
|
+
const runtime = globalThis;
|
|
2122
|
+
if (runtime.SupaCloud?.pgredis) {
|
|
2123
|
+
if (runtime.SupaCloud.pgredis === cacheFacade)
|
|
2124
|
+
return;
|
|
2125
|
+
throw new Error("globalThis.SupaCloud.pgredis is already owned by another runtime");
|
|
2126
|
+
}
|
|
2127
|
+
Object.defineProperty(runtime, "SupaCloud", {
|
|
2128
|
+
configurable: false,
|
|
2129
|
+
enumerable: true,
|
|
2130
|
+
value: Object.freeze({ ...runtime.SupaCloud ?? {}, pgredis: cacheFacade }),
|
|
2131
|
+
writable: false
|
|
2132
|
+
});
|
|
2133
|
+
}
|
|
2134
|
+
function runWithPgredisCache(cache, operation) {
|
|
2135
|
+
const context = { cache, active: true };
|
|
2136
|
+
return cacheContexts.run(context, async () => {
|
|
2137
|
+
try {
|
|
2138
|
+
return await operation();
|
|
2139
|
+
} finally {
|
|
2140
|
+
context.active = false;
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
function activeCache() {
|
|
2145
|
+
const context = cacheContexts.getStore();
|
|
2146
|
+
if (!context?.active)
|
|
2147
|
+
throw new Error("pgredis binding is unavailable outside a function request");
|
|
2148
|
+
return context.cache;
|
|
2149
|
+
}
|
|
2150
|
+
function assertCacheKey(key) {
|
|
2151
|
+
if (typeof key !== "string" || key.length < 1 || key.length > MAX_KEY_CHARACTERS) {
|
|
2152
|
+
throw new TypeError(`pgredis key must contain between 1 and ${MAX_KEY_CHARACTERS} characters`);
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function assertTtl(ttlMs) {
|
|
2156
|
+
if (ttlMs === null || ttlMs === undefined)
|
|
2157
|
+
return;
|
|
2158
|
+
if (!Number.isSafeInteger(ttlMs) || ttlMs < 0)
|
|
2159
|
+
throw new TypeError("pgredis ttlMs must be a non-negative safe integer or null");
|
|
2160
|
+
if (ttlMs > MAX_TTL_MS)
|
|
2161
|
+
throw new RangeError(`pgredis ttlMs must not exceed ${MAX_TTL_MS}`);
|
|
2162
|
+
}
|
|
2163
|
+
function serializeCacheValue(cacheValue) {
|
|
2164
|
+
const serializedValue = JSON.stringify(cacheValue);
|
|
2165
|
+
if (serializedValue === undefined)
|
|
2166
|
+
throw new TypeError("pgredis value must be JSON serializable");
|
|
2167
|
+
if (new TextEncoder().encode(serializedValue).byteLength > MAX_VALUE_BYTES) {
|
|
2168
|
+
throw new RangeError(`pgredis value must not exceed ${MAX_VALUE_BYTES} bytes`);
|
|
2169
|
+
}
|
|
2170
|
+
return serializedValue;
|
|
2171
|
+
}
|
|
2172
|
+
function parseStoredValue(stored) {
|
|
2173
|
+
return JSON.parse(stored.serialized_value);
|
|
2174
|
+
}
|
|
2175
|
+
async function deleteExpiredKey(query, key) {
|
|
2176
|
+
await query(`delete from ${CACHE_TABLE} where namespace = $1 and key = $2 and expires_at <= clock_timestamp()`, [CACHE_NAMESPACE, key]);
|
|
2177
|
+
}
|
|
2178
|
+
async function upsertWithoutTtl(query, key, serializedValue) {
|
|
2179
|
+
await query(`insert into ${CACHE_TABLE} (namespace, key, value, expires_at, updated_at)
|
|
2180
|
+
values ($1, $2, $3::jsonb, null, now())
|
|
2181
|
+
on conflict (namespace, key) do update set value = excluded.value, expires_at = null, updated_at = now()`, [CACHE_NAMESPACE, key, serializedValue]);
|
|
2182
|
+
}
|
|
2183
|
+
|
|
1993
2184
|
// src/vendor/tinbase/functions/handler.ts
|
|
1994
2185
|
class FunctionsHandler {
|
|
1995
2186
|
functions;
|
|
1996
2187
|
env;
|
|
1997
|
-
|
|
2188
|
+
pgredis;
|
|
2189
|
+
constructor(functions, env, pgredis) {
|
|
1998
2190
|
this.functions = functions;
|
|
1999
2191
|
this.env = env;
|
|
2192
|
+
this.pgredis = pgredis;
|
|
2000
2193
|
}
|
|
2001
2194
|
register(name, fn) {
|
|
2002
2195
|
this.functions.set(name, fn);
|
|
@@ -2014,7 +2207,8 @@ class FunctionsHandler {
|
|
|
2014
2207
|
return json2(404, { error: `function "${name}" not found` });
|
|
2015
2208
|
}
|
|
2016
2209
|
try {
|
|
2017
|
-
const
|
|
2210
|
+
const invoke = () => Promise.resolve(fn(req, { auth: ctx, env: this.env }));
|
|
2211
|
+
const res = await runWithDenoEnv(this.env, () => runWithPgredisCache(this.pgredis, invoke));
|
|
2018
2212
|
if (!(res instanceof Response)) {
|
|
2019
2213
|
return json2(500, { error: `function "${name}" did not return a Response` });
|
|
2020
2214
|
}
|
|
@@ -2556,7 +2750,24 @@ grant execute on function realtime.send(jsonb, text, text, boolean) to anon, aut
|
|
|
2556
2750
|
// src/vendor/tinbase/db/emulated.ts
|
|
2557
2751
|
var PGMQ_SQL = `
|
|
2558
2752
|
create schema if not exists pgmq;
|
|
2559
|
-
|
|
2753
|
+
revoke all on schema pgmq from public, anon, authenticated;
|
|
2754
|
+
grant usage on schema pgmq to service_role;
|
|
2755
|
+
|
|
2756
|
+
create table if not exists pgmq.meta (
|
|
2757
|
+
queue_name text primary key,
|
|
2758
|
+
physical_name text not null unique,
|
|
2759
|
+
is_partitioned boolean not null default false,
|
|
2760
|
+
is_unlogged boolean not null default false,
|
|
2761
|
+
created_at timestamptz not null default now()
|
|
2762
|
+
);
|
|
2763
|
+
|
|
2764
|
+
-- Preserve queues created by older Lite versions. Their physical names were
|
|
2765
|
+
-- the queue names, so only the already-truncated identifier can be recovered.
|
|
2766
|
+
insert into pgmq.meta (queue_name, physical_name)
|
|
2767
|
+
select substring(tablename from 3), substring(tablename from 3)
|
|
2768
|
+
from pg_tables
|
|
2769
|
+
where schemaname = 'pgmq' and left(tablename, 2) = 'q_'
|
|
2770
|
+
on conflict do nothing;
|
|
2560
2771
|
|
|
2561
2772
|
do $$ begin
|
|
2562
2773
|
if not exists (select 1 from pg_type t join pg_namespace n on n.oid = t.typnamespace
|
|
@@ -2567,32 +2778,78 @@ do $$ begin
|
|
|
2567
2778
|
end if;
|
|
2568
2779
|
end $$;
|
|
2569
2780
|
|
|
2781
|
+
create or replace function pgmq._normalize_queue_name(queue_name text)
|
|
2782
|
+
returns text language plpgsql immutable set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2783
|
+
declare normalized text := btrim(queue_name);
|
|
2784
|
+
begin
|
|
2785
|
+
if normalized is null or normalized !~ '^[a-z0-9][a-z0-9_-]{0,127}$' then
|
|
2786
|
+
raise exception 'invalid queue name: must match ^[a-z0-9][a-z0-9_-]{0,127}$'
|
|
2787
|
+
using errcode = '22023';
|
|
2788
|
+
end if;
|
|
2789
|
+
return normalized;
|
|
2790
|
+
end $pgmq$;
|
|
2791
|
+
|
|
2792
|
+
-- PostgreSQL identifiers are limited to 63 bytes. Keep the conventional table
|
|
2793
|
+
-- name for short queues and add a deterministic hash for valid 62-128 byte
|
|
2794
|
+
-- public names so truncation can never merge two queues.
|
|
2795
|
+
create or replace function pgmq._physical_queue_name(queue_name text)
|
|
2796
|
+
returns text language sql immutable set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2797
|
+
select case when length(normalized) <= 61 then normalized
|
|
2798
|
+
else left(normalized, 28) || '_' || md5(normalized) end
|
|
2799
|
+
from (select pgmq._normalize_queue_name(queue_name) as normalized) names;
|
|
2800
|
+
$pgmq$;
|
|
2801
|
+
|
|
2802
|
+
create or replace function pgmq._resolve_queue(queue_name text)
|
|
2803
|
+
returns text language plpgsql stable security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2804
|
+
declare normalized text := pgmq._normalize_queue_name(queue_name); physical text;
|
|
2805
|
+
begin
|
|
2806
|
+
select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
|
|
2807
|
+
if physical is null then
|
|
2808
|
+
raise exception 'queue "%" does not exist', normalized using errcode = '42P01';
|
|
2809
|
+
end if;
|
|
2810
|
+
return physical;
|
|
2811
|
+
end $pgmq$;
|
|
2812
|
+
|
|
2570
2813
|
create or replace function pgmq.create(queue_name text) returns void language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2814
|
+
declare normalized text := pgmq._normalize_queue_name(queue_name); physical text := pgmq._physical_queue_name(queue_name);
|
|
2571
2815
|
begin
|
|
2816
|
+
insert into pgmq.meta (queue_name, physical_name) values (normalized, physical)
|
|
2817
|
+
on conflict do nothing;
|
|
2818
|
+
select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
|
|
2572
2819
|
execute format('create table if not exists pgmq.%I (
|
|
2573
2820
|
msg_id bigint generated always as identity primary key,
|
|
2574
2821
|
read_ct integer not null default 0,
|
|
2575
2822
|
enqueued_at timestamptz not null default now(),
|
|
2576
2823
|
vt timestamptz not null default now(),
|
|
2577
|
-
message jsonb)', 'q_' ||
|
|
2824
|
+
message jsonb)', 'q_' || physical);
|
|
2578
2825
|
execute format('create table if not exists pgmq.%I (
|
|
2579
2826
|
msg_id bigint primary key, read_ct integer not null default 0,
|
|
2580
2827
|
enqueued_at timestamptz not null, archived_at timestamptz not null default now(),
|
|
2581
|
-
vt timestamptz, message jsonb)', 'a_' ||
|
|
2828
|
+
vt timestamptz, message jsonb)', 'a_' || physical);
|
|
2582
2829
|
end $pgmq$;
|
|
2583
2830
|
|
|
2584
2831
|
create or replace function pgmq.send(queue_name text, msg jsonb, delay integer default 0)
|
|
2585
2832
|
returns bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2586
|
-
declare id bigint;
|
|
2833
|
+
declare id bigint; physical text := pgmq._resolve_queue(queue_name);
|
|
2587
2834
|
begin
|
|
2588
|
-
|
|
2589
|
-
execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || queue_name)
|
|
2835
|
+
execute format('insert into pgmq.%I (vt, message) values (now() + make_interval(secs => $1), $2) returning msg_id', 'q_' || physical)
|
|
2590
2836
|
into id using delay, msg;
|
|
2591
2837
|
return id;
|
|
2592
2838
|
end $pgmq$;
|
|
2593
2839
|
|
|
2840
|
+
create or replace function pgmq.send_batch(queue_name text, msgs jsonb[], delay integer default 0)
|
|
2841
|
+
returns setof bigint language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2842
|
+
declare queue_message jsonb;
|
|
2843
|
+
begin
|
|
2844
|
+
perform pgmq._resolve_queue(queue_name);
|
|
2845
|
+
foreach queue_message in array msgs loop
|
|
2846
|
+
return next pgmq.send(queue_name, queue_message, delay);
|
|
2847
|
+
end loop;
|
|
2848
|
+
end $pgmq$;
|
|
2849
|
+
|
|
2594
2850
|
create or replace function pgmq.read(queue_name text, vt integer, qty integer)
|
|
2595
2851
|
returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2852
|
+
declare physical text := pgmq._resolve_queue(queue_name);
|
|
2596
2853
|
begin
|
|
2597
2854
|
return query execute format($fmt$
|
|
2598
2855
|
with cte as (
|
|
@@ -2601,52 +2858,68 @@ begin
|
|
|
2601
2858
|
update pgmq.%I m set vt = now() + make_interval(secs => $2), read_ct = read_ct + 1
|
|
2602
2859
|
from cte where m.msg_id = cte.msg_id
|
|
2603
2860
|
returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
|
|
2604
|
-
$fmt$, 'q_' ||
|
|
2861
|
+
$fmt$, 'q_' || physical, 'q_' || physical) using qty, vt;
|
|
2605
2862
|
end $pgmq$;
|
|
2606
2863
|
|
|
2607
2864
|
create or replace function pgmq.pop(queue_name text)
|
|
2608
2865
|
returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2866
|
+
declare physical text := pgmq._resolve_queue(queue_name);
|
|
2609
2867
|
begin
|
|
2610
2868
|
return query execute format($fmt$
|
|
2611
2869
|
with cte as (select msg_id from pgmq.%I where vt <= now() order by msg_id limit 1 for update skip locked)
|
|
2612
2870
|
delete from pgmq.%I m using cte where m.msg_id = cte.msg_id
|
|
2613
2871
|
returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
|
|
2614
|
-
$fmt$, 'q_' ||
|
|
2872
|
+
$fmt$, 'q_' || physical, 'q_' || physical);
|
|
2873
|
+
end $pgmq$;
|
|
2874
|
+
|
|
2875
|
+
create or replace function pgmq.set_vt(queue_name text, msg_id bigint, vt_offset integer)
|
|
2876
|
+
returns setof pgmq.message_record language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2877
|
+
declare physical text := pgmq._resolve_queue(queue_name);
|
|
2878
|
+
begin
|
|
2879
|
+
return query execute format($fmt$
|
|
2880
|
+
update pgmq.%I m set vt = now() + make_interval(secs => $1)
|
|
2881
|
+
where m.msg_id = $2
|
|
2882
|
+
returning m.msg_id, m.read_ct, m.enqueued_at, m.vt, m.message
|
|
2883
|
+
$fmt$, 'q_' || physical) using vt_offset, msg_id;
|
|
2615
2884
|
end $pgmq$;
|
|
2616
2885
|
|
|
2617
2886
|
create or replace function pgmq.delete(queue_name text, msg_id bigint)
|
|
2618
2887
|
returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2619
|
-
declare n integer;
|
|
2888
|
+
declare n integer; physical text := pgmq._resolve_queue(queue_name);
|
|
2620
2889
|
begin
|
|
2621
|
-
execute format('delete from pgmq.%I where msg_id = $1', 'q_' ||
|
|
2890
|
+
execute format('delete from pgmq.%I where msg_id = $1', 'q_' || physical) using msg_id;
|
|
2622
2891
|
get diagnostics n = row_count;
|
|
2623
2892
|
return n > 0;
|
|
2624
2893
|
end $pgmq$;
|
|
2625
2894
|
|
|
2626
2895
|
create or replace function pgmq.archive(queue_name text, msg_id bigint)
|
|
2627
2896
|
returns boolean language plpgsql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2628
|
-
declare n integer;
|
|
2897
|
+
declare n integer; physical text := pgmq._resolve_queue(queue_name);
|
|
2629
2898
|
begin
|
|
2630
2899
|
execute format($fmt$
|
|
2631
2900
|
with del as (delete from pgmq.%I where msg_id = $1 returning *)
|
|
2632
2901
|
insert into pgmq.%I (msg_id, read_ct, enqueued_at, vt, message)
|
|
2633
2902
|
select msg_id, read_ct, enqueued_at, vt, message from del
|
|
2634
|
-
$fmt$, 'q_' ||
|
|
2903
|
+
$fmt$, 'q_' || physical, 'a_' || physical) using msg_id;
|
|
2635
2904
|
get diagnostics n = row_count;
|
|
2636
2905
|
return n > 0;
|
|
2637
2906
|
end $pgmq$;
|
|
2638
2907
|
|
|
2639
2908
|
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$
|
|
2909
|
+
declare normalized text := pgmq._normalize_queue_name(queue_name); physical text;
|
|
2640
2910
|
begin
|
|
2641
|
-
|
|
2642
|
-
|
|
2911
|
+
select physical_name into physical from pgmq.meta where pgmq.meta.queue_name = normalized;
|
|
2912
|
+
if physical is null then return false; end if;
|
|
2913
|
+
execute format('drop table if exists pgmq.%I', 'q_' || physical);
|
|
2914
|
+
execute format('drop table if exists pgmq.%I', 'a_' || physical);
|
|
2915
|
+
delete from pgmq.meta where pgmq.meta.queue_name = normalized;
|
|
2643
2916
|
return true;
|
|
2644
2917
|
end $pgmq$;
|
|
2645
2918
|
|
|
2646
2919
|
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$
|
|
2647
|
-
declare n bigint;
|
|
2920
|
+
declare n bigint; physical text := pgmq._resolve_queue(queue_name);
|
|
2648
2921
|
begin
|
|
2649
|
-
execute format('delete from pgmq.%I', 'q_' ||
|
|
2922
|
+
execute format('delete from pgmq.%I', 'q_' || physical);
|
|
2650
2923
|
get diagnostics n = row_count;
|
|
2651
2924
|
return n;
|
|
2652
2925
|
end $pgmq$;
|
|
@@ -2654,11 +2927,47 @@ end $pgmq$;
|
|
|
2654
2927
|
create or replace function pgmq.list_queues()
|
|
2655
2928
|
returns table(queue_name text, is_partitioned boolean, is_unlogged boolean, created_at timestamptz)
|
|
2656
2929
|
language sql security definer set search_path = pgmq, pg_catalog, public as $pgmq$
|
|
2657
|
-
select
|
|
2658
|
-
from pg_tables where schemaname = 'pgmq' and left(tablename, 2) = 'q_';
|
|
2930
|
+
select queue_name, is_partitioned, is_unlogged, created_at from pgmq.meta;
|
|
2659
2931
|
$pgmq$;
|
|
2660
2932
|
|
|
2661
|
-
|
|
2933
|
+
revoke all on all functions in schema pgmq from public, anon, authenticated;
|
|
2934
|
+
grant execute on all functions in schema pgmq to service_role;
|
|
2935
|
+
|
|
2936
|
+
create schema if not exists pgmq_public;
|
|
2937
|
+
grant usage on schema pgmq_public to anon, authenticated, service_role;
|
|
2938
|
+
|
|
2939
|
+
create or replace function pgmq_public.send(queue_name text, message 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(queue_name, message, sleep_seconds);
|
|
2942
|
+
$pgmq_public$;
|
|
2943
|
+
|
|
2944
|
+
create or replace function pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer default 0)
|
|
2945
|
+
returns setof bigint language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
|
|
2946
|
+
select * from pgmq.send_batch(queue_name, messages, sleep_seconds);
|
|
2947
|
+
$pgmq_public$;
|
|
2948
|
+
|
|
2949
|
+
create or replace function pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
|
|
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.read(queue_name, sleep_seconds, n);
|
|
2952
|
+
$pgmq_public$;
|
|
2953
|
+
|
|
2954
|
+
create or replace function pgmq_public.pop(queue_name text)
|
|
2955
|
+
returns setof pgmq.message_record language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
|
|
2956
|
+
select * from pgmq.pop(queue_name);
|
|
2957
|
+
$pgmq_public$;
|
|
2958
|
+
|
|
2959
|
+
create or replace function pgmq_public.archive(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.archive(queue_name, message_id);
|
|
2962
|
+
$pgmq_public$;
|
|
2963
|
+
|
|
2964
|
+
create or replace function pgmq_public."delete"(queue_name text, message_id bigint)
|
|
2965
|
+
returns boolean language sql volatile security definer set search_path = pgmq, pg_catalog, public as $pgmq_public$
|
|
2966
|
+
select pgmq.delete(queue_name, message_id);
|
|
2967
|
+
$pgmq_public$;
|
|
2968
|
+
|
|
2969
|
+
revoke all on all functions in schema pgmq_public from public;
|
|
2970
|
+
grant execute on all functions in schema pgmq_public to anon, authenticated, service_role;
|
|
2662
2971
|
`;
|
|
2663
2972
|
var CRON_SQL = `
|
|
2664
2973
|
create schema if not exists cron;
|
|
@@ -2986,11 +3295,66 @@ function pickTag(text, base) {
|
|
|
2986
3295
|
}
|
|
2987
3296
|
|
|
2988
3297
|
// src/vendor/tinbase/db/pglite-engine.ts
|
|
2989
|
-
import { mkdir, open,
|
|
3298
|
+
import { mkdir, open, unlink as unlink2 } from "fs/promises";
|
|
2990
3299
|
import { dirname, resolve } from "path";
|
|
3300
|
+
|
|
3301
|
+
// src/vendor/tinbase/db/data-dir-lock.ts
|
|
3302
|
+
import { readFile, unlink } from "fs/promises";
|
|
3303
|
+
async function recoverStaleDataDirLock(lockPath) {
|
|
3304
|
+
const lockState = await inspectDataDirLock(lockPath);
|
|
3305
|
+
if (lockState.kind !== "stale")
|
|
3306
|
+
return lockState;
|
|
3307
|
+
try {
|
|
3308
|
+
await unlink(lockPath);
|
|
3309
|
+
return { kind: "missing" };
|
|
3310
|
+
} catch (error) {
|
|
3311
|
+
if (error.code === "ENOENT")
|
|
3312
|
+
return { kind: "missing" };
|
|
3313
|
+
throw error;
|
|
3314
|
+
}
|
|
3315
|
+
}
|
|
3316
|
+
async function readDataDirLockOwner(lockPath) {
|
|
3317
|
+
const lockState = await inspectDataDirLock(lockPath);
|
|
3318
|
+
return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
|
|
3319
|
+
}
|
|
3320
|
+
async function inspectDataDirLock(lockPath) {
|
|
3321
|
+
let contents;
|
|
3322
|
+
try {
|
|
3323
|
+
contents = await readFile(lockPath, "utf8");
|
|
3324
|
+
} catch (error) {
|
|
3325
|
+
return error.code === "ENOENT" ? { kind: "missing" } : { kind: "unreadable" };
|
|
3326
|
+
}
|
|
3327
|
+
const owner = parseDataDirLockOwner(contents);
|
|
3328
|
+
if (!owner)
|
|
3329
|
+
return { kind: "unreadable" };
|
|
3330
|
+
return isProcessAlive(owner.pid) ? { kind: "active", owner } : { kind: "stale", owner };
|
|
3331
|
+
}
|
|
3332
|
+
function parseDataDirLockOwner(contents) {
|
|
3333
|
+
try {
|
|
3334
|
+
const value = JSON.parse(contents);
|
|
3335
|
+
return typeof value.pid === "number" && Number.isInteger(value.pid) && value.pid > 0 && typeof value.nonce === "string" ? { pid: value.pid, nonce: value.nonce } : null;
|
|
3336
|
+
} catch {
|
|
3337
|
+
return null;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
function isProcessAlive(pid) {
|
|
3341
|
+
try {
|
|
3342
|
+
process.kill(pid, 0);
|
|
3343
|
+
return true;
|
|
3344
|
+
} catch (error) {
|
|
3345
|
+
return error.code === "EPERM";
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// src/standalone-assets-protocol.ts
|
|
3350
|
+
var STANDALONE_PGLITE_ASSETS = Symbol.for("supacloud-lite.pglite-standalone-assets");
|
|
3351
|
+
|
|
3352
|
+
// src/vendor/tinbase/db/pglite-engine.ts
|
|
2991
3353
|
async function createPgliteEngine(dataDir) {
|
|
2992
3354
|
const releaseLock = await acquireDataDirLock(dataDir);
|
|
2993
3355
|
let PGlite, extensions;
|
|
3356
|
+
const standaloneAssets = getStandaloneAssets();
|
|
3357
|
+
let cleanupStandaloneBundles = async () => {};
|
|
2994
3358
|
try {
|
|
2995
3359
|
({ PGlite } = await import("@electric-sql/pglite"));
|
|
2996
3360
|
const [uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch] = await Promise.all([
|
|
@@ -3003,19 +3367,48 @@ async function createPgliteEngine(dataDir) {
|
|
|
3003
3367
|
import("@electric-sql/pglite/contrib/fuzzystrmatch").then((m) => m.fuzzystrmatch)
|
|
3004
3368
|
]);
|
|
3005
3369
|
extensions = { uuid_ossp, pgcrypto, citext, pg_trgm, ltree, hstore, fuzzystrmatch };
|
|
3006
|
-
} catch (
|
|
3370
|
+
} catch (error) {
|
|
3007
3371
|
await releaseLock();
|
|
3008
|
-
if (
|
|
3009
|
-
throw
|
|
3372
|
+
if (error instanceof Error && /wasm/.test(error.message))
|
|
3373
|
+
throw error;
|
|
3010
3374
|
throw new Error("the PGlite WASM engine is not available in this build");
|
|
3011
3375
|
}
|
|
3376
|
+
if (standaloneAssets) {
|
|
3377
|
+
try {
|
|
3378
|
+
const standaloneBundles = await standaloneAssets.prepareExtensionBundles();
|
|
3379
|
+
cleanupStandaloneBundles = standaloneBundles.cleanup;
|
|
3380
|
+
extensions = {
|
|
3381
|
+
uuid_ossp: withEmbeddedBundle(extensions.uuid_ossp, standaloneBundles.bundles.uuid_ossp),
|
|
3382
|
+
pgcrypto: withEmbeddedBundle(extensions.pgcrypto, standaloneBundles.bundles.pgcrypto),
|
|
3383
|
+
citext: withEmbeddedBundle(extensions.citext, standaloneBundles.bundles.citext),
|
|
3384
|
+
pg_trgm: withEmbeddedBundle(extensions.pg_trgm, standaloneBundles.bundles.pg_trgm),
|
|
3385
|
+
ltree: withEmbeddedBundle(extensions.ltree, standaloneBundles.bundles.ltree),
|
|
3386
|
+
hstore: withEmbeddedBundle(extensions.hstore, standaloneBundles.bundles.hstore),
|
|
3387
|
+
fuzzystrmatch: withEmbeddedBundle(extensions.fuzzystrmatch, standaloneBundles.bundles.fuzzystrmatch)
|
|
3388
|
+
};
|
|
3389
|
+
} catch (error) {
|
|
3390
|
+
await removePreparedBundles(cleanupStandaloneBundles);
|
|
3391
|
+
await releaseLock();
|
|
3392
|
+
throw error;
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3012
3395
|
let pg;
|
|
3013
3396
|
try {
|
|
3014
|
-
pg = new PGlite({
|
|
3397
|
+
pg = new PGlite({
|
|
3398
|
+
dataDir,
|
|
3399
|
+
extensions,
|
|
3400
|
+
...standaloneAssets ? {
|
|
3401
|
+
pgliteWasmModule: standaloneAssets.pgliteWasmModule,
|
|
3402
|
+
initdbWasmModule: standaloneAssets.initdbWasmModule,
|
|
3403
|
+
fsBundle: standaloneAssets.fsBundle
|
|
3404
|
+
} : {}
|
|
3405
|
+
});
|
|
3015
3406
|
await pg.waitReady;
|
|
3016
3407
|
} catch (error) {
|
|
3017
3408
|
await releaseLock();
|
|
3018
3409
|
throw error;
|
|
3410
|
+
} finally {
|
|
3411
|
+
await removePreparedBundles(cleanupStandaloneBundles);
|
|
3019
3412
|
}
|
|
3020
3413
|
let closed = false;
|
|
3021
3414
|
return {
|
|
@@ -3054,6 +3447,25 @@ async function createPgliteEngine(dataDir) {
|
|
|
3054
3447
|
}
|
|
3055
3448
|
};
|
|
3056
3449
|
}
|
|
3450
|
+
function getStandaloneAssets() {
|
|
3451
|
+
return globalThis[STANDALONE_PGLITE_ASSETS];
|
|
3452
|
+
}
|
|
3453
|
+
function withEmbeddedBundle(extension, bundlePath) {
|
|
3454
|
+
return {
|
|
3455
|
+
...extension,
|
|
3456
|
+
setup: async (pg, emscriptenOpts, clientOnly) => ({
|
|
3457
|
+
...await extension.setup(pg, emscriptenOpts, clientOnly),
|
|
3458
|
+
bundlePath
|
|
3459
|
+
})
|
|
3460
|
+
};
|
|
3461
|
+
}
|
|
3462
|
+
async function removePreparedBundles(cleanup) {
|
|
3463
|
+
try {
|
|
3464
|
+
await cleanup();
|
|
3465
|
+
} catch (error) {
|
|
3466
|
+
console.error("Unable to remove temporary PGlite extension bundles:", error);
|
|
3467
|
+
}
|
|
3468
|
+
}
|
|
3057
3469
|
async function acquireDataDirLock(dataDir) {
|
|
3058
3470
|
if (!dataDir || dataDir.includes("://"))
|
|
3059
3471
|
return async () => {};
|
|
@@ -3061,52 +3473,56 @@ async function acquireDataDirLock(dataDir) {
|
|
|
3061
3473
|
const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
|
|
3062
3474
|
await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
|
|
3063
3475
|
const nonce = crypto.randomUUID();
|
|
3064
|
-
|
|
3065
|
-
try {
|
|
3066
|
-
handle = await open(lockPath, "wx", 384);
|
|
3067
|
-
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
3068
|
-
`);
|
|
3069
|
-
} catch (error) {
|
|
3070
|
-
await handle?.close().catch(() => {});
|
|
3071
|
-
if (error.code !== "EEXIST")
|
|
3072
|
-
throw error;
|
|
3073
|
-
const owner = await readLockOwner(lockPath);
|
|
3074
|
-
if (owner && isProcessAlive(owner.pid)) {
|
|
3075
|
-
throw new Error(`PGlite data directory is already in use: ${absoluteDataDir} (pid ${owner.pid})`);
|
|
3076
|
-
}
|
|
3077
|
-
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.`);
|
|
3078
|
-
}
|
|
3476
|
+
const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
|
|
3079
3477
|
let released = false;
|
|
3080
3478
|
return async () => {
|
|
3081
3479
|
if (released)
|
|
3082
3480
|
return;
|
|
3083
3481
|
released = true;
|
|
3084
3482
|
await handle?.close();
|
|
3085
|
-
const owner = await
|
|
3483
|
+
const owner = await readDataDirLockOwner(lockPath);
|
|
3086
3484
|
if (owner?.nonce !== nonce)
|
|
3087
3485
|
return;
|
|
3088
|
-
await
|
|
3486
|
+
await unlink2(lockPath).catch((error) => {
|
|
3089
3487
|
if (error.code !== "ENOENT")
|
|
3090
3488
|
throw error;
|
|
3091
3489
|
});
|
|
3092
3490
|
};
|
|
3093
3491
|
}
|
|
3094
|
-
async function
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
|
|
3492
|
+
async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
|
|
3493
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
3494
|
+
try {
|
|
3495
|
+
return await writeDataDirLock(lockPath, nonce);
|
|
3496
|
+
} catch (error) {
|
|
3497
|
+
if (error.code !== "EEXIST")
|
|
3498
|
+
throw error;
|
|
3499
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
3500
|
+
if (lockState.kind === "active")
|
|
3501
|
+
throw lockInUseError(absoluteDataDir, lockState.owner.pid);
|
|
3502
|
+
if (lockState.kind === "unreadable")
|
|
3503
|
+
throw unreadableLockError(lockPath);
|
|
3504
|
+
}
|
|
3100
3505
|
}
|
|
3506
|
+
throw unreadableLockError(lockPath);
|
|
3101
3507
|
}
|
|
3102
|
-
function
|
|
3508
|
+
async function writeDataDirLock(lockPath, nonce) {
|
|
3509
|
+
const handle = await open(lockPath, "wx", 384);
|
|
3103
3510
|
try {
|
|
3104
|
-
|
|
3105
|
-
|
|
3511
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
3512
|
+
`);
|
|
3513
|
+
return handle;
|
|
3106
3514
|
} catch (error) {
|
|
3107
|
-
|
|
3515
|
+
await handle.close().catch(() => {});
|
|
3516
|
+
await unlink2(lockPath).catch(() => {});
|
|
3517
|
+
throw error;
|
|
3108
3518
|
}
|
|
3109
3519
|
}
|
|
3520
|
+
function lockInUseError(dataDir, pid) {
|
|
3521
|
+
return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
|
|
3522
|
+
}
|
|
3523
|
+
function unreadableLockError(lockPath) {
|
|
3524
|
+
return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
|
|
3525
|
+
}
|
|
3110
3526
|
|
|
3111
3527
|
// src/vendor/tinbase/db/database.ts
|
|
3112
3528
|
var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
|
|
@@ -3143,6 +3559,9 @@ class Database {
|
|
|
3143
3559
|
exec(sql) {
|
|
3144
3560
|
return this.engine.exec(sql);
|
|
3145
3561
|
}
|
|
3562
|
+
transaction(fn) {
|
|
3563
|
+
return this.engine.transaction((tx) => fn((sql, params) => tx.query(sql, params)));
|
|
3564
|
+
}
|
|
3146
3565
|
async withContext(ctx, fn) {
|
|
3147
3566
|
return this.engine.transaction(async (tx) => {
|
|
3148
3567
|
await tx.query(`select set_config('role', $1, true),
|
|
@@ -6911,6 +7330,7 @@ class RetentionService {
|
|
|
6911
7330
|
await this.run(`delete from auth.one_time_tokens where expires_at < now()`);
|
|
6912
7331
|
await this.run(`delete from auth.mfa_challenges where expires_at < now()`);
|
|
6913
7332
|
await this.run(`delete from auth.flow_state where expires_at < now()`);
|
|
7333
|
+
await this.run(`delete from public.supacloud_pgredis_kv where expires_at <= now()`);
|
|
6914
7334
|
if (this.refreshTokenDays > 0) {
|
|
6915
7335
|
const cutoff = new Date(now.getTime() - this.refreshTokenDays * DAY_MS).toISOString();
|
|
6916
7336
|
await this.run(`delete from auth.refresh_tokens where revoked = true and updated_at < $1`, [cutoff]);
|
|
@@ -7233,11 +7653,13 @@ async function createBackend(config = {}) {
|
|
|
7233
7653
|
} catch (e) {
|
|
7234
7654
|
await failStartup(e);
|
|
7235
7655
|
}
|
|
7656
|
+
const pgredis = await PgredisCache.create(db).catch(failStartup);
|
|
7236
7657
|
const now = Math.floor(Date.now() / 1000);
|
|
7237
7658
|
const tenYears = 10 * 365 * 24 * 3600;
|
|
7238
7659
|
const anonKey = await signJwt({ iss: "supabase", ref: "local", role: "anon", iat: now, exp: now + tenYears }, jwtSecret);
|
|
7239
7660
|
const serviceRoleKey = await signJwt({ iss: "supabase", ref: "local", role: "service_role", iat: now, exp: now + tenYears }, jwtSecret);
|
|
7240
|
-
const
|
|
7661
|
+
const exposedSchemas = [...new Set(config.dbSchemas ?? ["public", "pgmq_public"])];
|
|
7662
|
+
const rest = new RestHandler(db, { exposedSchemas, maxRows: config.maxRows });
|
|
7241
7663
|
const exposed = isNetworkExposed(config.host);
|
|
7242
7664
|
const inbox = config.mailer || exposed ? null : new InboxMailer((msg) => log(config.logMailBody ? `[mail] to=${msg.to} subject="${msg.subject}"
|
|
7243
7665
|
${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
@@ -7298,7 +7720,12 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
7298
7720
|
...config.functionEnv ?? {}
|
|
7299
7721
|
};
|
|
7300
7722
|
installDenoShim();
|
|
7301
|
-
|
|
7723
|
+
try {
|
|
7724
|
+
installPgredisShim();
|
|
7725
|
+
} catch (error) {
|
|
7726
|
+
await failStartup(error);
|
|
7727
|
+
}
|
|
7728
|
+
const functions = new FunctionsHandler(fnMap, fnEnv, pgredis);
|
|
7302
7729
|
async function resolveContext(req, url) {
|
|
7303
7730
|
const authz = req.headers.get("authorization");
|
|
7304
7731
|
const bearer = authz?.toLowerCase().startsWith("bearer ") ? authz.slice(7) : null;
|
|
@@ -7640,7 +8067,7 @@ async function serveBun(backend, opts = {}) {
|
|
|
7640
8067
|
};
|
|
7641
8068
|
}
|
|
7642
8069
|
// src/project-runtime.ts
|
|
7643
|
-
import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as
|
|
8070
|
+
import { chmod, link, lstat, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink3, writeFile as writeFile3 } from "fs/promises";
|
|
7644
8071
|
import { dirname as dirname3, isAbsolute, join as join6, parse, relative, resolve as resolve2 } from "path";
|
|
7645
8072
|
|
|
7646
8073
|
// src/vendor/tinbase/node/config-toml.ts
|
|
@@ -8260,7 +8687,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
8260
8687
|
throw error2;
|
|
8261
8688
|
stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
|
|
8262
8689
|
} finally {
|
|
8263
|
-
await
|
|
8690
|
+
await unlink3(temporaryFile).catch((error2) => {
|
|
8264
8691
|
if (error2.code !== "ENOENT")
|
|
8265
8692
|
throw error2;
|
|
8266
8693
|
});
|
|
@@ -8634,13 +9061,12 @@ async function assertNoDataDirectoryLock(paths) {
|
|
|
8634
9061
|
if (!paths.dataDir)
|
|
8635
9062
|
return;
|
|
8636
9063
|
const lockPath = `${paths.dataDir}.supacloud-lite.lock`;
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
8640
|
-
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
}
|
|
9064
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
9065
|
+
if (lockState.kind === "missing")
|
|
9066
|
+
return;
|
|
9067
|
+
if (lockState.kind === "active")
|
|
9068
|
+
throw new Error(`data directory is already in use: ${paths.dataDir} (pid ${lockState.owner.pid})`);
|
|
9069
|
+
throw new Error(`data directory has an unreadable lock: ${lockPath}; confirm Lite is stopped, then remove the lock manually`);
|
|
8644
9070
|
}
|
|
8645
9071
|
async function stageDirectory(root, destination) {
|
|
8646
9072
|
try {
|