@supacloud/lite 0.33.2 → 0.34.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 +15 -0
- package/README.md +85 -2
- package/deploy/README.md +110 -0
- package/deploy/caddy/Caddyfile +15 -0
- package/deploy/systemd/supacloud-lite.service +32 -0
- package/dist/cli.js +433 -49
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +355 -23
- package/dist/project-runtime.d.ts +2 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/auth/proxy.d.ts +28 -0
- package/dist/runtime/auth/proxy.d.ts.map +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/node/bun-server.d.ts +2 -0
- package/dist/runtime/node/bun-server.d.ts.map +1 -1
- package/dist/runtime/node/native/readiness.d.ts +2 -0
- package/dist/runtime/node/native/readiness.d.ts.map +1 -1
- package/dist/runtime/node/production-readiness.d.ts +31 -0
- package/dist/runtime/node/production-readiness.d.ts.map +1 -0
- package/dist/runtime/realtime/engine.d.ts +22 -0
- package/dist/runtime/realtime/engine.d.ts.map +1 -1
- package/dist/runtime/storage/handler.d.ts +6 -0
- package/dist/runtime/storage/handler.d.ts.map +1 -1
- package/package.json +2 -1
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as existsSync5 } from "fs";
|
|
5
|
+
import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
6
6
|
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7
|
-
import { dirname as dirname8, join as join10, resolve as
|
|
7
|
+
import { dirname as dirname8, join as join10, resolve as resolve7 } from "path";
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.
|
|
11
|
+
version: "0.34.0",
|
|
12
12
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
@@ -28,6 +28,7 @@ var package_default = {
|
|
|
28
28
|
"dist",
|
|
29
29
|
"!dist/standalone",
|
|
30
30
|
"README.md",
|
|
31
|
+
"deploy",
|
|
31
32
|
"CHANGELOG.md",
|
|
32
33
|
"RELEASING.md",
|
|
33
34
|
"THIRD_PARTY_NOTICES.md",
|
|
@@ -9530,10 +9531,21 @@ async function sha256Hex2(text) {
|
|
|
9530
9531
|
}
|
|
9531
9532
|
|
|
9532
9533
|
// src/runtime/realtime/engine.ts
|
|
9534
|
+
function normalizeBroadcastTopic(raw) {
|
|
9535
|
+
const topic = raw.trim().replace(/^realtime:/, "");
|
|
9536
|
+
if (!topic)
|
|
9537
|
+
throw new Error("broadcast topic is required");
|
|
9538
|
+
return topic;
|
|
9539
|
+
}
|
|
9540
|
+
|
|
9533
9541
|
class RealtimeEngine {
|
|
9534
9542
|
db;
|
|
9535
9543
|
jwtSecret;
|
|
9536
9544
|
resolveExternalToken;
|
|
9545
|
+
static MAX_HTTP_BROADCAST_MESSAGES = 100;
|
|
9546
|
+
static MAX_HTTP_BROADCAST_TOPIC_BYTES = 256;
|
|
9547
|
+
static MAX_HTTP_BROADCAST_EVENT_BYTES = 256;
|
|
9548
|
+
static MAX_HTTP_BROADCAST_PAYLOAD_BYTES = 1024 * 1024;
|
|
9537
9549
|
connections = new Set;
|
|
9538
9550
|
bindingCounter = 1;
|
|
9539
9551
|
phxRefCounter = 1;
|
|
@@ -9580,6 +9592,47 @@ class RealtimeEngine {
|
|
|
9580
9592
|
return { read: false, write: false };
|
|
9581
9593
|
}
|
|
9582
9594
|
}
|
|
9595
|
+
async broadcastHttp(messages, ctx) {
|
|
9596
|
+
if (messages.length === 0)
|
|
9597
|
+
throw new Error("messages must not be empty");
|
|
9598
|
+
if (messages.length > RealtimeEngine.MAX_HTTP_BROADCAST_MESSAGES) {
|
|
9599
|
+
throw new Error(`messages must contain at most ${RealtimeEngine.MAX_HTTP_BROADCAST_MESSAGES} items`);
|
|
9600
|
+
}
|
|
9601
|
+
const normalized = [];
|
|
9602
|
+
for (const message of messages) {
|
|
9603
|
+
if (typeof message.topic !== "string")
|
|
9604
|
+
throw new Error("broadcast topic must be a string");
|
|
9605
|
+
if (typeof message.event !== "string")
|
|
9606
|
+
throw new Error("broadcast event must be a string");
|
|
9607
|
+
const topic = normalizeBroadcastTopic(message.topic);
|
|
9608
|
+
if (new TextEncoder().encode(topic).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_TOPIC_BYTES) {
|
|
9609
|
+
throw new Error("broadcast topic is too large");
|
|
9610
|
+
}
|
|
9611
|
+
const event = message.event.trim();
|
|
9612
|
+
if (!event)
|
|
9613
|
+
throw new Error("broadcast event is required");
|
|
9614
|
+
if (new TextEncoder().encode(event).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_EVENT_BYTES) {
|
|
9615
|
+
throw new Error("broadcast event is too large");
|
|
9616
|
+
}
|
|
9617
|
+
let payloadText;
|
|
9618
|
+
try {
|
|
9619
|
+
payloadText = JSON.stringify(message.payload ?? null);
|
|
9620
|
+
} catch {
|
|
9621
|
+
throw new Error("broadcast payload must be JSON serializable");
|
|
9622
|
+
}
|
|
9623
|
+
if (new TextEncoder().encode(payloadText).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_PAYLOAD_BYTES) {
|
|
9624
|
+
throw new Error("broadcast payload is too large");
|
|
9625
|
+
}
|
|
9626
|
+
if (message.private === true) {
|
|
9627
|
+
const access = await this.authorizePrivate(topic, ctx);
|
|
9628
|
+
if (!access.write)
|
|
9629
|
+
throw new Error(`not authorized to broadcast on private topic: ${topic}`);
|
|
9630
|
+
}
|
|
9631
|
+
normalized.push({ topic, event, payload: message.payload ?? null });
|
|
9632
|
+
}
|
|
9633
|
+
for (const message of normalized)
|
|
9634
|
+
this.dispatchDbBroadcast(message);
|
|
9635
|
+
}
|
|
9583
9636
|
dispatchDbBroadcast(msg) {
|
|
9584
9637
|
const full = `realtime:${msg.topic}`;
|
|
9585
9638
|
for (const conn of this.connections) {
|
|
@@ -11942,6 +11995,11 @@ class StorageHandler {
|
|
|
11942
11995
|
const rest = url.pathname.replace(/^\/storage\/v1\/?/, "").replace(/\/+$/, "");
|
|
11943
11996
|
const method = req.method.toUpperCase();
|
|
11944
11997
|
try {
|
|
11998
|
+
if (rest === "status" && method === "GET") {
|
|
11999
|
+
return json3(200, { status: "ok", name: "supacloud-lite-storage", version: SUPACLOUD_LITE_VERSION });
|
|
12000
|
+
}
|
|
12001
|
+
if (rest === "metrics" && method === "GET")
|
|
12002
|
+
return await this.metrics(ctx);
|
|
11945
12003
|
if (rest === "bucket" && method === "POST")
|
|
11946
12004
|
return await this.createBucket(req, ctx);
|
|
11947
12005
|
if (rest === "bucket" && method === "GET")
|
|
@@ -12062,6 +12120,37 @@ class StorageHandler {
|
|
|
12062
12120
|
release();
|
|
12063
12121
|
}
|
|
12064
12122
|
}
|
|
12123
|
+
async metrics(ctx) {
|
|
12124
|
+
const denied = this.requireService(ctx);
|
|
12125
|
+
if (denied)
|
|
12126
|
+
return denied;
|
|
12127
|
+
const [buckets, objects] = await Promise.all([
|
|
12128
|
+
this.db.query(`select count(*)::int as count from storage.buckets`),
|
|
12129
|
+
this.db.query(`select count(*)::int as count,
|
|
12130
|
+
coalesce(sum(case when metadata->>'size' ~ '^[0-9]+$' then (metadata->>'size')::bigint else 0 end), 0) as bytes
|
|
12131
|
+
from storage.objects`)
|
|
12132
|
+
]);
|
|
12133
|
+
const bucketCount = buckets.rows[0]?.count ?? 0;
|
|
12134
|
+
const objectCount = objects.rows[0]?.count ?? 0;
|
|
12135
|
+
const objectBytes = Number(objects.rows[0]?.bytes ?? 0);
|
|
12136
|
+
const body = [
|
|
12137
|
+
"# HELP storage_buckets_total Number of storage buckets.",
|
|
12138
|
+
"# TYPE storage_buckets_total gauge",
|
|
12139
|
+
`storage_buckets_total ${bucketCount}`,
|
|
12140
|
+
"# HELP storage_objects_total Number of storage object metadata rows.",
|
|
12141
|
+
"# TYPE storage_objects_total gauge",
|
|
12142
|
+
`storage_objects_total ${objectCount}`,
|
|
12143
|
+
"# HELP storage_object_bytes_total Sum of declared object sizes in bytes.",
|
|
12144
|
+
"# TYPE storage_object_bytes_total gauge",
|
|
12145
|
+
`storage_object_bytes_total ${Number.isFinite(objectBytes) ? objectBytes : 0}`,
|
|
12146
|
+
""
|
|
12147
|
+
].join(`
|
|
12148
|
+
`);
|
|
12149
|
+
return new Response(body, {
|
|
12150
|
+
status: 200,
|
|
12151
|
+
headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" }
|
|
12152
|
+
});
|
|
12153
|
+
}
|
|
12065
12154
|
requireService(ctx) {
|
|
12066
12155
|
if (ctx.role !== "service_role") {
|
|
12067
12156
|
return storageError(403, "Unauthorized", "Bucket management requires the service_role key");
|
|
@@ -14429,6 +14518,9 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
14429
14518
|
if (smsInbox && (path === "/sms-inbox" || path.startsWith("/sms-inbox/"))) {
|
|
14430
14519
|
return withCors(smsInbox.serve(req, url));
|
|
14431
14520
|
}
|
|
14521
|
+
if (path === "/storage/v1/status" && req.method === "GET") {
|
|
14522
|
+
return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
|
|
14523
|
+
}
|
|
14432
14524
|
if (path.startsWith("/storage/v1/object/public/") || path.startsWith("/storage/v1/object/sign/") || path.startsWith("/storage/v1/render/image/public/") || path.startsWith("/storage/v1/render/image/sign/")) {
|
|
14433
14525
|
if (req.method === "GET" || req.method === "HEAD") {
|
|
14434
14526
|
return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
|
|
@@ -14464,6 +14556,50 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
14464
14556
|
const ctx = await resolveContext(req, url);
|
|
14465
14557
|
if (ctx instanceof Response)
|
|
14466
14558
|
return ctx;
|
|
14559
|
+
if (path === "/realtime/v1/api/broadcast" || path === "/realtime/v1/api/broadcast/") {
|
|
14560
|
+
if (req.method !== "POST") {
|
|
14561
|
+
return withCors(new Response(JSON.stringify({ message: "Method not allowed" }), {
|
|
14562
|
+
status: 405,
|
|
14563
|
+
headers: { "content-type": "application/json", allow: "POST" }
|
|
14564
|
+
}));
|
|
14565
|
+
}
|
|
14566
|
+
const contentType = req.headers.get("content-type") ?? "";
|
|
14567
|
+
if (contentType && !contentType.includes("application/json")) {
|
|
14568
|
+
return withCors(new Response(JSON.stringify({ message: "Content-Type must be application/json" }), {
|
|
14569
|
+
status: 415,
|
|
14570
|
+
headers: { "content-type": "application/json" }
|
|
14571
|
+
}));
|
|
14572
|
+
}
|
|
14573
|
+
let body;
|
|
14574
|
+
try {
|
|
14575
|
+
body = await req.json();
|
|
14576
|
+
} catch {
|
|
14577
|
+
return withCors(new Response(JSON.stringify({ message: "Invalid JSON body" }), {
|
|
14578
|
+
status: 400,
|
|
14579
|
+
headers: { "content-type": "application/json" }
|
|
14580
|
+
}));
|
|
14581
|
+
}
|
|
14582
|
+
const messages = typeof body === "object" && body !== null && "messages" in body ? body.messages : undefined;
|
|
14583
|
+
if (!Array.isArray(messages)) {
|
|
14584
|
+
return withCors(new Response(JSON.stringify({ message: "messages must be an array" }), {
|
|
14585
|
+
status: 400,
|
|
14586
|
+
headers: { "content-type": "application/json" }
|
|
14587
|
+
}));
|
|
14588
|
+
}
|
|
14589
|
+
try {
|
|
14590
|
+
await realtime.broadcastHttp(messages, ctx);
|
|
14591
|
+
} catch (error) {
|
|
14592
|
+
const message = error instanceof Error ? error.message : "Broadcast failed";
|
|
14593
|
+
return withCors(new Response(JSON.stringify({ message }), {
|
|
14594
|
+
status: message.startsWith("not authorized") ? 403 : 400,
|
|
14595
|
+
headers: { "content-type": "application/json" }
|
|
14596
|
+
}));
|
|
14597
|
+
}
|
|
14598
|
+
return withCors(new Response(JSON.stringify({ message: "Broadcast sent" }), {
|
|
14599
|
+
status: 202,
|
|
14600
|
+
headers: { "content-type": "application/json" }
|
|
14601
|
+
}));
|
|
14602
|
+
}
|
|
14467
14603
|
if (path.startsWith("/rest/v1"))
|
|
14468
14604
|
return withCors(await rest.handle(req, ctx, url));
|
|
14469
14605
|
if (path === "/graphql/v1" || path === "/graphql/v1/")
|
|
@@ -16902,6 +17038,81 @@ function sameStrings(left, right) {
|
|
|
16902
17038
|
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
16903
17039
|
}
|
|
16904
17040
|
|
|
17041
|
+
// src/runtime/node/production-readiness.ts
|
|
17042
|
+
import { isAbsolute, relative, resolve as resolve4 } from "path";
|
|
17043
|
+
var SNAPSHOT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
17044
|
+
function within(parent, child) {
|
|
17045
|
+
const rel = relative(resolve4(parent), resolve4(child));
|
|
17046
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
17047
|
+
}
|
|
17048
|
+
function loopback(host) {
|
|
17049
|
+
return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
|
|
17050
|
+
}
|
|
17051
|
+
function absoluteHttpUrl(value) {
|
|
17052
|
+
try {
|
|
17053
|
+
const url = new URL(value);
|
|
17054
|
+
return (url.protocol === "https:" || url.protocol === "http:" && loopback(url.hostname)) && !url.username && !url.password;
|
|
17055
|
+
} catch {
|
|
17056
|
+
return false;
|
|
17057
|
+
}
|
|
17058
|
+
}
|
|
17059
|
+
function productionReadiness(input) {
|
|
17060
|
+
const blockers = [];
|
|
17061
|
+
const warnings = [];
|
|
17062
|
+
const now = input.now ?? Date.now();
|
|
17063
|
+
if (input.runtimeMode !== "strict") {
|
|
17064
|
+
blockers.push("RUNTIME_MODE_NOT_STRICT: production functions require strict runtime mode");
|
|
17065
|
+
}
|
|
17066
|
+
if (input.storageBackend === "memory") {
|
|
17067
|
+
blockers.push("MEMORY_STORAGE_NOT_DURABLE: memory storage loses objects on restart");
|
|
17068
|
+
}
|
|
17069
|
+
if (input.storageBackend === "fs") {
|
|
17070
|
+
warnings.push("LOCAL_STORAGE_REQUIRES_DURABLE_VOLUME_AND_BACKUP");
|
|
17071
|
+
}
|
|
17072
|
+
if (input.storageBackend === "s3") {
|
|
17073
|
+
warnings.push("S3_OBJECTS_NOT_INCLUDED_IN_SNAPSHOT: back up the bucket/prefix separately");
|
|
17074
|
+
}
|
|
17075
|
+
if (!loopback(input.host)) {
|
|
17076
|
+
warnings.push("DIRECT_NETWORK_BIND_WITHOUT_LITE_TLS: terminate TLS in a trusted proxy or restrict the network boundary");
|
|
17077
|
+
}
|
|
17078
|
+
if (input.engine === "pglite") {
|
|
17079
|
+
warnings.push("PGLITE_SINGLE_PROCESS_CAPACITY: validate the real schema, concurrency and Realtime load");
|
|
17080
|
+
}
|
|
17081
|
+
if (input.identityMode === "local") {
|
|
17082
|
+
warnings.push("LOCAL_AUTH_IN_PRODUCTION: own account lifecycle, confirmation and password reset");
|
|
17083
|
+
}
|
|
17084
|
+
if (input.identityMode === "external" && !input.externalAuthProxyIssuer) {
|
|
17085
|
+
warnings.push("EXTERNAL_AUTH_PROXY_NOT_CONFIGURED: cross-origin browser token/userinfo calls may fail CORS");
|
|
17086
|
+
}
|
|
17087
|
+
if (input.externalAuthProxyIssuer && !absoluteHttpUrl(input.externalAuthProxyIssuer)) {
|
|
17088
|
+
blockers.push("EXTERNAL_AUTH_PROXY_ISSUER_INVALID: use an HTTPS issuer, or HTTP only on loopback");
|
|
17089
|
+
}
|
|
17090
|
+
if (!input.siteUrl) {
|
|
17091
|
+
warnings.push("SITE_URL_NOT_CONFIGURED: set [auth].site_url for stable redirects");
|
|
17092
|
+
} else if (!absoluteHttpUrl(input.siteUrl)) {
|
|
17093
|
+
warnings.push("SITE_URL_NOT_HTTPS: production redirects should use HTTPS");
|
|
17094
|
+
}
|
|
17095
|
+
if (!input.apiUrl) {
|
|
17096
|
+
warnings.push("API_URL_NOT_CONFIGURED: set the public API URL used by Auth and Functions");
|
|
17097
|
+
} else if (!absoluteHttpUrl(input.apiUrl)) {
|
|
17098
|
+
warnings.push("API_URL_NOT_HTTPS: production API URLs should use HTTPS");
|
|
17099
|
+
}
|
|
17100
|
+
if (within(input.projectDir, input.stateDir)) {
|
|
17101
|
+
warnings.push("STATE_INSIDE_PROJECT: keep mutable state outside the reviewed project checkout");
|
|
17102
|
+
}
|
|
17103
|
+
if (input.dataDir && input.dataDir === input.storageDir) {
|
|
17104
|
+
blockers.push("DATABASE_AND_STORAGE_PATH_OVERLAP: use separate directories");
|
|
17105
|
+
} else if (input.dataDir && (within(input.dataDir, input.storageDir) || within(input.storageDir, input.dataDir))) {
|
|
17106
|
+
warnings.push("DATABASE_AND_STORAGE_PATH_NESTED: keep database and storage trees independent");
|
|
17107
|
+
}
|
|
17108
|
+
if (input.latestSnapshotAt === undefined) {
|
|
17109
|
+
warnings.push("NO_SNAPSHOT_FOUND: create and test a restore snapshot before promotion");
|
|
17110
|
+
} else if (now - input.latestSnapshotAt > SNAPSHOT_MAX_AGE_MS) {
|
|
17111
|
+
warnings.push("SNAPSHOT_STALE: newest snapshot is older than 7 days");
|
|
17112
|
+
}
|
|
17113
|
+
return { ready: blockers.length === 0, blockers, warnings };
|
|
17114
|
+
}
|
|
17115
|
+
|
|
16905
17116
|
// src/runtime/node/config-toml.ts
|
|
16906
17117
|
import { readFileSync as readFileSync2 } from "fs";
|
|
16907
17118
|
import { join as join3 } from "path";
|
|
@@ -17490,7 +17701,7 @@ function isNotFound(error) {
|
|
|
17490
17701
|
|
|
17491
17702
|
// src/project-runtime.ts
|
|
17492
17703
|
import { chmod, link as link2, lstat as lstat2, mkdir as mkdir5, readFile as readFile7, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
17493
|
-
import { dirname as dirname6, isAbsolute as
|
|
17704
|
+
import { dirname as dirname6, isAbsolute as isAbsolute3, join as join8, parse, relative as relative3, resolve as resolve5 } from "path";
|
|
17494
17705
|
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
17495
17706
|
|
|
17496
17707
|
// src/runtime/node/bun-server.ts
|
|
@@ -17512,7 +17723,11 @@ async function serveBun(backend, opts = {}) {
|
|
|
17512
17723
|
const ip = srv.requestIP(req)?.address;
|
|
17513
17724
|
if (ip)
|
|
17514
17725
|
headers.set("x-supacloud-lite-remote-addr", ip);
|
|
17515
|
-
|
|
17726
|
+
const request = new Request(req, { headers });
|
|
17727
|
+
const hookResponse = await opts.beforeRequest?.(request);
|
|
17728
|
+
if (hookResponse)
|
|
17729
|
+
return hookResponse;
|
|
17730
|
+
return backend.fetch(request);
|
|
17516
17731
|
},
|
|
17517
17732
|
websocket: {
|
|
17518
17733
|
open(ws) {
|
|
@@ -17542,7 +17757,7 @@ async function serveBun(backend, opts = {}) {
|
|
|
17542
17757
|
|
|
17543
17758
|
// src/runtime/node/fs-driver.ts
|
|
17544
17759
|
import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
17545
|
-
import { dirname as dirname4, isAbsolute, join as join5, relative, resolve as resolvePath, sep } from "path";
|
|
17760
|
+
import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, relative as relative2, resolve as resolvePath, sep } from "path";
|
|
17546
17761
|
|
|
17547
17762
|
class FsStorageDriver {
|
|
17548
17763
|
root;
|
|
@@ -17552,8 +17767,8 @@ class FsStorageDriver {
|
|
|
17552
17767
|
}
|
|
17553
17768
|
resolve(key) {
|
|
17554
17769
|
const path = join5(this.root, key);
|
|
17555
|
-
const relativePath =
|
|
17556
|
-
if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) ||
|
|
17770
|
+
const relativePath = relative2(this.root, path);
|
|
17771
|
+
if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute2(relativePath)) {
|
|
17557
17772
|
throw new Error(`invalid storage key: ${key}`);
|
|
17558
17773
|
}
|
|
17559
17774
|
return path;
|
|
@@ -17930,11 +18145,124 @@ class S3StorageDriver {
|
|
|
17930
18145
|
}
|
|
17931
18146
|
}
|
|
17932
18147
|
|
|
18148
|
+
// src/runtime/auth/proxy.ts
|
|
18149
|
+
var ALLOWED_ROUTES = {
|
|
18150
|
+
"/auth/v1/oauth/token": new Set(["POST"]),
|
|
18151
|
+
"/auth/v1/oauth/userinfo": new Set(["GET"]),
|
|
18152
|
+
"/auth/v1/logout": new Set(["GET", "POST"])
|
|
18153
|
+
};
|
|
18154
|
+
var FORWARD_REQUEST_HEADERS = ["accept", "authorization", "content-type", "apikey"];
|
|
18155
|
+
var FORWARD_RESPONSE_HEADERS = [
|
|
18156
|
+
"content-type",
|
|
18157
|
+
"cache-control",
|
|
18158
|
+
"retry-after",
|
|
18159
|
+
"www-authenticate"
|
|
18160
|
+
];
|
|
18161
|
+
function proxyError(status, code, message, allow) {
|
|
18162
|
+
const headers = new Headers({
|
|
18163
|
+
"content-type": "application/json; charset=utf-8",
|
|
18164
|
+
"cache-control": "no-store"
|
|
18165
|
+
});
|
|
18166
|
+
if (allow)
|
|
18167
|
+
headers.set("allow", allow);
|
|
18168
|
+
return new Response(JSON.stringify({ code, message }), { status, headers });
|
|
18169
|
+
}
|
|
18170
|
+
function upstreamBase(issuer) {
|
|
18171
|
+
let url;
|
|
18172
|
+
try {
|
|
18173
|
+
url = new URL(issuer);
|
|
18174
|
+
} catch {
|
|
18175
|
+
throw new TypeError("External auth issuer must be an absolute URL");
|
|
18176
|
+
}
|
|
18177
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1";
|
|
18178
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
|
|
18179
|
+
throw new TypeError("External auth issuer must use HTTPS (HTTP is allowed only on loopback)");
|
|
18180
|
+
}
|
|
18181
|
+
if (url.username || url.password || url.search || url.hash) {
|
|
18182
|
+
throw new TypeError("External auth issuer must not contain credentials, query or fragment");
|
|
18183
|
+
}
|
|
18184
|
+
if (url.pathname.replace(/\/+$/, "") !== "/auth/v1") {
|
|
18185
|
+
throw new TypeError("External auth issuer must end with /auth/v1");
|
|
18186
|
+
}
|
|
18187
|
+
return url;
|
|
18188
|
+
}
|
|
18189
|
+
function redirectOrigins(base, extra = []) {
|
|
18190
|
+
const origins = new Set([base.origin]);
|
|
18191
|
+
for (const value of extra) {
|
|
18192
|
+
let origin;
|
|
18193
|
+
try {
|
|
18194
|
+
origin = new URL(value);
|
|
18195
|
+
} catch {
|
|
18196
|
+
throw new TypeError("External auth redirect origin must be an absolute URL");
|
|
18197
|
+
}
|
|
18198
|
+
const loopback = origin.hostname === "127.0.0.1" || origin.hostname === "localhost" || origin.hostname === "[::1]" || origin.hostname === "::1";
|
|
18199
|
+
if (origin.protocol !== "https:" && !(origin.protocol === "http:" && loopback)) {
|
|
18200
|
+
throw new TypeError("External auth redirect origin must use HTTPS (HTTP is allowed only on loopback)");
|
|
18201
|
+
}
|
|
18202
|
+
if (origin.username || origin.password || origin.search || origin.hash) {
|
|
18203
|
+
throw new TypeError("External auth redirect origin must not contain credentials, query or fragment");
|
|
18204
|
+
}
|
|
18205
|
+
origins.add(origin.origin);
|
|
18206
|
+
}
|
|
18207
|
+
return origins;
|
|
18208
|
+
}
|
|
18209
|
+
function createExternalAuthProxy(options) {
|
|
18210
|
+
const base = upstreamBase(options.issuer);
|
|
18211
|
+
const allowedRedirects = redirectOrigins(base, options.allowedRedirectOrigins);
|
|
18212
|
+
const fetcher = options.fetch ?? globalThis.fetch;
|
|
18213
|
+
if (typeof fetcher !== "function")
|
|
18214
|
+
throw new TypeError("External auth proxy requires fetch");
|
|
18215
|
+
return async (request) => {
|
|
18216
|
+
const source = new URL(request.url);
|
|
18217
|
+
const methods = ALLOWED_ROUTES[source.pathname];
|
|
18218
|
+
if (!methods)
|
|
18219
|
+
return null;
|
|
18220
|
+
if (!methods.has(request.method)) {
|
|
18221
|
+
return proxyError(405, "AUTH_PROXY_METHOD_NOT_ALLOWED", "Auth proxy method is not allowed", [...methods].join(", "));
|
|
18222
|
+
}
|
|
18223
|
+
const target = new URL(source.pathname + source.search, base.origin);
|
|
18224
|
+
const headers = new Headers;
|
|
18225
|
+
for (const name of FORWARD_REQUEST_HEADERS) {
|
|
18226
|
+
const value = request.headers.get(name);
|
|
18227
|
+
if (value !== null)
|
|
18228
|
+
headers.set(name, value);
|
|
18229
|
+
}
|
|
18230
|
+
const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.arrayBuffer();
|
|
18231
|
+
let upstream;
|
|
18232
|
+
try {
|
|
18233
|
+
upstream = await fetcher(target, {
|
|
18234
|
+
method: request.method,
|
|
18235
|
+
headers,
|
|
18236
|
+
body,
|
|
18237
|
+
redirect: "manual"
|
|
18238
|
+
});
|
|
18239
|
+
} catch {
|
|
18240
|
+
return proxyError(503, "AUTH_PROXY_UNAVAILABLE", "Identity service is temporarily unavailable");
|
|
18241
|
+
}
|
|
18242
|
+
const responseHeaders = new Headers;
|
|
18243
|
+
for (const name of FORWARD_RESPONSE_HEADERS) {
|
|
18244
|
+
const value = upstream.headers.get(name);
|
|
18245
|
+
if (value !== null)
|
|
18246
|
+
responseHeaders.set(name, value);
|
|
18247
|
+
}
|
|
18248
|
+
const location = upstream.headers.get("location");
|
|
18249
|
+
if (location !== null) {
|
|
18250
|
+
const resolved = new URL(location, base.origin);
|
|
18251
|
+
if (!allowedRedirects.has(resolved.origin)) {
|
|
18252
|
+
return proxyError(502, "AUTH_PROXY_REDIRECT_REJECTED", "Identity service redirect was rejected");
|
|
18253
|
+
}
|
|
18254
|
+
responseHeaders.set("location", resolved.href);
|
|
18255
|
+
}
|
|
18256
|
+
responseHeaders.set("cache-control", "no-store");
|
|
18257
|
+
return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
|
|
18258
|
+
};
|
|
18259
|
+
}
|
|
18260
|
+
|
|
17933
18261
|
// src/project-runtime.ts
|
|
17934
18262
|
var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
|
|
17935
18263
|
var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
|
|
17936
18264
|
function resolveProjectPaths(options = {}) {
|
|
17937
|
-
const projectDir =
|
|
18265
|
+
const projectDir = resolve5(options.projectDir ?? process.cwd());
|
|
17938
18266
|
const stateDir = resolvePath2(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
|
|
17939
18267
|
const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
|
|
17940
18268
|
const dataDir = options.memory ? undefined : resolvePath2(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
|
|
@@ -17949,7 +18277,7 @@ function resolveProjectPaths(options = {}) {
|
|
|
17949
18277
|
};
|
|
17950
18278
|
}
|
|
17951
18279
|
async function assertResetPathsSafe(paths) {
|
|
17952
|
-
const stateDir =
|
|
18280
|
+
const stateDir = resolve5(paths.stateDir);
|
|
17953
18281
|
if (stateDir === parse(stateDir).root)
|
|
17954
18282
|
throw new Error("refusing to use the filesystem root as the state directory");
|
|
17955
18283
|
const stateInfo = await requiredResetEntry(stateDir);
|
|
@@ -17957,7 +18285,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
17957
18285
|
throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
|
|
17958
18286
|
}
|
|
17959
18287
|
const canonicalStateDir = await realpath2(stateDir);
|
|
17960
|
-
const secretsFile =
|
|
18288
|
+
const secretsFile = resolve5(paths.secretsFile);
|
|
17961
18289
|
if (secretsFile !== join8(stateDir, "secrets.json")) {
|
|
17962
18290
|
throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
|
|
17963
18291
|
}
|
|
@@ -17971,9 +18299,9 @@ async function assertResetPathsSafe(paths) {
|
|
|
17971
18299
|
["storage", paths.storageDir]
|
|
17972
18300
|
];
|
|
17973
18301
|
for (const [label, targetPath] of targets) {
|
|
17974
|
-
const target =
|
|
17975
|
-
const relativePath =
|
|
17976
|
-
if (!relativePath || relativePath.startsWith("..") ||
|
|
18302
|
+
const target = resolve5(targetPath);
|
|
18303
|
+
const relativePath = relative3(stateDir, target);
|
|
18304
|
+
if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
|
|
17977
18305
|
throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
|
|
17978
18306
|
}
|
|
17979
18307
|
await assertResetTargetCanonical(stateDir, canonicalStateDir, target, label);
|
|
@@ -18020,9 +18348,9 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
|
|
|
18020
18348
|
current = parent;
|
|
18021
18349
|
}
|
|
18022
18350
|
const existingAncestor = await nearestExistingAncestor(target);
|
|
18023
|
-
const canonicalTarget =
|
|
18024
|
-
const canonicalRelative =
|
|
18025
|
-
if (!canonicalRelative || canonicalRelative.startsWith("..") ||
|
|
18351
|
+
const canonicalTarget = resolve5(await realpath2(existingAncestor), relative3(existingAncestor, target));
|
|
18352
|
+
const canonicalRelative = relative3(canonicalStateDir, canonicalTarget);
|
|
18353
|
+
if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute3(canonicalRelative)) {
|
|
18026
18354
|
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target}`);
|
|
18027
18355
|
}
|
|
18028
18356
|
}
|
|
@@ -18198,7 +18526,7 @@ async function loadProjectBindings(projectDir, config) {
|
|
|
18198
18526
|
const bindings = config.lite.migrationBindings;
|
|
18199
18527
|
if (!bindings)
|
|
18200
18528
|
return;
|
|
18201
|
-
const manifest = JSON.parse(await readFile7(
|
|
18529
|
+
const manifest = JSON.parse(await readFile7(resolve5(projectDir, bindings.manifest), "utf8"));
|
|
18202
18530
|
return {
|
|
18203
18531
|
manifest,
|
|
18204
18532
|
target: { environment: bindings.environment, projectRef: bindings.projectRef },
|
|
@@ -18208,7 +18536,7 @@ async function loadProjectBindings(projectDir, config) {
|
|
|
18208
18536
|
async function loadProjectIdentity(projectDir, module) {
|
|
18209
18537
|
if (!module)
|
|
18210
18538
|
return;
|
|
18211
|
-
const imported = await import(pathToFileURL3(
|
|
18539
|
+
const imported = await import(pathToFileURL3(resolve5(projectDir, module)).href);
|
|
18212
18540
|
if (!isRecord(imported) || typeof imported["default"] !== "function") {
|
|
18213
18541
|
throw new Error("Lite identity module must default-export an external identity verifier");
|
|
18214
18542
|
}
|
|
@@ -18274,7 +18602,11 @@ async function startProjectServer(options = {}) {
|
|
|
18274
18602
|
const resolvedOptions = options.port === 0 ? { ...options, port: await findEphemeralPort(options.host) } : options;
|
|
18275
18603
|
const project = await createProjectBackend({ ...resolvedOptions, startRuntimeServices: true });
|
|
18276
18604
|
try {
|
|
18277
|
-
const
|
|
18605
|
+
const beforeRequest = options.externalAuthProxyIssuer ? createExternalAuthProxy({
|
|
18606
|
+
issuer: options.externalAuthProxyIssuer,
|
|
18607
|
+
allowedRedirectOrigins: options.externalAuthProxyRedirectOrigins
|
|
18608
|
+
}) : undefined;
|
|
18609
|
+
const server = await serveBun(project.backend, { host: project.host, port: project.port, beforeRequest });
|
|
18278
18610
|
let closePromise = null;
|
|
18279
18611
|
return {
|
|
18280
18612
|
...project,
|
|
@@ -18316,7 +18648,7 @@ async function loadWebhooks(projectDir) {
|
|
|
18316
18648
|
}
|
|
18317
18649
|
}
|
|
18318
18650
|
function resolvePath2(projectDir, path) {
|
|
18319
|
-
return
|
|
18651
|
+
return isAbsolute3(path) ? path : resolve5(projectDir, path);
|
|
18320
18652
|
}
|
|
18321
18653
|
function randomHex(bytes) {
|
|
18322
18654
|
const value = crypto.getRandomValues(new Uint8Array(bytes));
|
|
@@ -18362,7 +18694,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
18362
18694
|
|
|
18363
18695
|
// src/snapshot.ts
|
|
18364
18696
|
import { chmod as chmod2, copyFile, lstat as lstat3, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile8, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
18365
|
-
import { dirname as dirname7, join as join9, parse as parse2, relative as
|
|
18697
|
+
import { dirname as dirname7, join as join9, parse as parse2, relative as relative4, resolve as resolve6, sep as sep2 } from "path";
|
|
18366
18698
|
import { create as createTar, extract as extractTar2 } from "tar";
|
|
18367
18699
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
18368
18700
|
var SNAPSHOT_VERSION = 1;
|
|
@@ -18386,7 +18718,7 @@ async function createSnapshot(options) {
|
|
|
18386
18718
|
postgresMajor: await readPostgresMajor(paths.dataDir)
|
|
18387
18719
|
} : {}
|
|
18388
18720
|
};
|
|
18389
|
-
const output =
|
|
18721
|
+
const output = resolve6(options.output);
|
|
18390
18722
|
if (await existingInfo(output))
|
|
18391
18723
|
throw new Error(`snapshot output already exists: ${output}`);
|
|
18392
18724
|
await mkdir6(dirname7(output), { recursive: true });
|
|
@@ -18427,7 +18759,7 @@ async function restoreSnapshot(options) {
|
|
|
18427
18759
|
await mkdir6(payloadRoot, { recursive: true });
|
|
18428
18760
|
await extractTar2({
|
|
18429
18761
|
cwd: payloadRoot,
|
|
18430
|
-
file:
|
|
18762
|
+
file: resolve6(options.input),
|
|
18431
18763
|
preserveOwner: false,
|
|
18432
18764
|
preservePaths: false,
|
|
18433
18765
|
strict: true,
|
|
@@ -18470,10 +18802,10 @@ async function restoreSnapshot(options) {
|
|
|
18470
18802
|
await mkdir6(stateStage, { recursive: true });
|
|
18471
18803
|
await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
|
|
18472
18804
|
if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
|
|
18473
|
-
await copyEntry(join9(payloadRoot, "database"), join9(stateStage,
|
|
18805
|
+
await copyEntry(join9(payloadRoot, "database"), join9(stateStage, relative4(paths.stateDir, paths.dataDir)));
|
|
18474
18806
|
}
|
|
18475
18807
|
if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
|
|
18476
|
-
await copyEntry(join9(payloadRoot, "storage"), join9(stateStage,
|
|
18808
|
+
await copyEntry(join9(payloadRoot, "storage"), join9(stateStage, relative4(paths.stateDir, paths.storageDir)));
|
|
18477
18809
|
}
|
|
18478
18810
|
const swaps = [];
|
|
18479
18811
|
try {
|
|
@@ -18507,11 +18839,11 @@ async function restoreSnapshot(options) {
|
|
|
18507
18839
|
function normalizePaths(paths) {
|
|
18508
18840
|
return {
|
|
18509
18841
|
...paths,
|
|
18510
|
-
projectDir:
|
|
18511
|
-
stateDir:
|
|
18512
|
-
dataDir: paths.dataDir ?
|
|
18513
|
-
storageDir:
|
|
18514
|
-
secretsFile:
|
|
18842
|
+
projectDir: resolve6(paths.projectDir),
|
|
18843
|
+
stateDir: resolve6(paths.stateDir),
|
|
18844
|
+
dataDir: paths.dataDir ? resolve6(paths.dataDir) : undefined,
|
|
18845
|
+
storageDir: resolve6(paths.storageDir),
|
|
18846
|
+
secretsFile: resolve6(paths.secretsFile)
|
|
18515
18847
|
};
|
|
18516
18848
|
}
|
|
18517
18849
|
async function assertSnapshotPaths(paths, options = {}) {
|
|
@@ -18541,7 +18873,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
18541
18873
|
async function assertDirectoryOrMissing(path) {
|
|
18542
18874
|
if (!path)
|
|
18543
18875
|
return;
|
|
18544
|
-
if (
|
|
18876
|
+
if (resolve6(path) === parse2(resolve6(path)).root)
|
|
18545
18877
|
throw new Error(`snapshot path must not be the filesystem root: ${path}`);
|
|
18546
18878
|
try {
|
|
18547
18879
|
const info = await lstat3(path);
|
|
@@ -18764,13 +19096,13 @@ async function assertNoSymlinks(root) {
|
|
|
18764
19096
|
}
|
|
18765
19097
|
}
|
|
18766
19098
|
function isWithin(parent, child) {
|
|
18767
|
-
const normalizedParent =
|
|
18768
|
-
const normalizedChild =
|
|
19099
|
+
const normalizedParent = resolve6(parent);
|
|
19100
|
+
const normalizedChild = resolve6(child);
|
|
18769
19101
|
return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
|
|
18770
19102
|
}
|
|
18771
19103
|
function pathsOverlap(left, right) {
|
|
18772
|
-
const normalizedLeft =
|
|
18773
|
-
const normalizedRight =
|
|
19104
|
+
const normalizedLeft = resolve6(left);
|
|
19105
|
+
const normalizedRight = resolve6(right);
|
|
18774
19106
|
return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
|
|
18775
19107
|
}
|
|
18776
19108
|
|
|
@@ -18883,7 +19215,8 @@ function parseArgs(argv) {
|
|
|
18883
19215
|
port: process.env.SUPACLOUD_LITE_PORT || process.env.PORT ? Number.parseInt(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 10) : 54321,
|
|
18884
19216
|
serviceRole: false,
|
|
18885
19217
|
force: false,
|
|
18886
|
-
json: false
|
|
19218
|
+
json: false,
|
|
19219
|
+
production: false
|
|
18887
19220
|
};
|
|
18888
19221
|
for (let index = 0;index < args.length; index++) {
|
|
18889
19222
|
const argument = args[index];
|
|
@@ -18902,13 +19235,13 @@ function parseArgs(argv) {
|
|
|
18902
19235
|
else if (argument === "--site-url")
|
|
18903
19236
|
options.siteUrl = next();
|
|
18904
19237
|
else if (argument === "--project-dir" || argument === "--dir")
|
|
18905
|
-
options.projectDir =
|
|
19238
|
+
options.projectDir = resolve7(next());
|
|
18906
19239
|
else if (argument === "--state-dir")
|
|
18907
|
-
options.stateDir =
|
|
19240
|
+
options.stateDir = resolve7(next());
|
|
18908
19241
|
else if (argument === "--data-dir")
|
|
18909
|
-
options.dataDir =
|
|
19242
|
+
options.dataDir = resolve7(next());
|
|
18910
19243
|
else if (argument === "--storage-dir")
|
|
18911
|
-
options.storageDir =
|
|
19244
|
+
options.storageDir = resolve7(next());
|
|
18912
19245
|
else if (argument === "--storage-backend")
|
|
18913
19246
|
options.storageBackend = next();
|
|
18914
19247
|
else if (argument === "--s3-prefix")
|
|
@@ -18918,9 +19251,13 @@ function parseArgs(argv) {
|
|
|
18918
19251
|
else if (argument === "--runtime-mode")
|
|
18919
19252
|
options.runtimeMode = runtimeMode(next());
|
|
18920
19253
|
else if (argument === "--postgres-dir")
|
|
18921
|
-
options.postgresDir =
|
|
19254
|
+
options.postgresDir = resolve7(next());
|
|
18922
19255
|
else if (argument === "--identity-module")
|
|
18923
|
-
options.identityModule =
|
|
19256
|
+
options.identityModule = resolve7(next());
|
|
19257
|
+
else if (argument === "--external-auth-proxy-issuer")
|
|
19258
|
+
options.externalAuthProxyIssuer = next();
|
|
19259
|
+
else if (argument === "--external-auth-proxy-redirect-origin")
|
|
19260
|
+
options.externalAuthProxyRedirectOrigins = commaSeparated2(next());
|
|
18924
19261
|
else if (argument === "--replication-profile") {
|
|
18925
19262
|
options.replicationProfile = next();
|
|
18926
19263
|
} else if (argument === "--replication-host")
|
|
@@ -18932,25 +19269,27 @@ function parseArgs(argv) {
|
|
|
18932
19269
|
else if (argument === "--powersync-tables")
|
|
18933
19270
|
options.powersyncPublicationTables = commaSeparated2(next());
|
|
18934
19271
|
else if (argument === "--replication-tls-cert")
|
|
18935
|
-
options.replicationTlsCertFile =
|
|
19272
|
+
options.replicationTlsCertFile = resolve7(next());
|
|
18936
19273
|
else if (argument === "--replication-tls-key")
|
|
18937
|
-
options.replicationTlsKeyFile =
|
|
19274
|
+
options.replicationTlsKeyFile = resolve7(next());
|
|
18938
19275
|
else if (argument === "--memory")
|
|
18939
19276
|
options.memory = true;
|
|
18940
19277
|
else if (argument === "--output" || argument === "-o")
|
|
18941
|
-
options.output =
|
|
19278
|
+
options.output = resolve7(next());
|
|
18942
19279
|
else if (argument === "--file" || argument === "-f")
|
|
18943
19280
|
options.diffFile = next();
|
|
18944
19281
|
else if (argument === "--baseline")
|
|
18945
19282
|
options.baseline = true;
|
|
18946
19283
|
else if (argument === "--module-file")
|
|
18947
|
-
options.moduleFile =
|
|
19284
|
+
options.moduleFile = resolve7(next());
|
|
18948
19285
|
else if (argument === "--service-role")
|
|
18949
19286
|
options.serviceRole = true;
|
|
18950
19287
|
else if (argument === "--force")
|
|
18951
19288
|
options.force = true;
|
|
18952
19289
|
else if (argument === "--json")
|
|
18953
19290
|
options.json = true;
|
|
19291
|
+
else if (argument === "--production")
|
|
19292
|
+
options.production = true;
|
|
18954
19293
|
else if (argument === "--email")
|
|
18955
19294
|
options.email = next();
|
|
18956
19295
|
else if (argument === "--password-stdin")
|
|
@@ -19098,6 +19437,23 @@ ${privilegedKey}
|
|
|
19098
19437
|
}
|
|
19099
19438
|
if (config.lite.graphql.enabled === false)
|
|
19100
19439
|
report.graphql = { status: "disabled", extension: "pg_graphql" };
|
|
19440
|
+
if (options.production) {
|
|
19441
|
+
report.production_readiness = productionReadiness({
|
|
19442
|
+
engine: paths.databaseEngine,
|
|
19443
|
+
runtimeMode: config.lite.runtimeMode,
|
|
19444
|
+
storageBackend: resolveStorageBackend(options.storageBackend),
|
|
19445
|
+
identityMode: options.identityModule ?? config.lite.identityModule ? "external" : "local",
|
|
19446
|
+
host: options.host ?? "127.0.0.1",
|
|
19447
|
+
projectDir: paths.projectDir,
|
|
19448
|
+
stateDir: paths.stateDir,
|
|
19449
|
+
storageDir: paths.storageDir,
|
|
19450
|
+
dataDir: paths.dataDir,
|
|
19451
|
+
apiUrl: options.apiUrl ?? process.env.SUPACLOUD_LITE_API_URL,
|
|
19452
|
+
siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL,
|
|
19453
|
+
externalAuthProxyIssuer: options.externalAuthProxyIssuer,
|
|
19454
|
+
latestSnapshotAt: latestSnapshotTimestamp(paths.stateDir)
|
|
19455
|
+
});
|
|
19456
|
+
}
|
|
19101
19457
|
if (options.json)
|
|
19102
19458
|
await writeStandardOutput(`${JSON.stringify(report, null, 2)}
|
|
19103
19459
|
`);
|
|
@@ -19107,6 +19463,8 @@ ${privilegedKey}
|
|
|
19107
19463
|
process.exitCode = 1;
|
|
19108
19464
|
if (report.powersync_readiness?.ready === false)
|
|
19109
19465
|
process.exitCode = 1;
|
|
19466
|
+
if (report.production_readiness?.ready === false)
|
|
19467
|
+
process.exitCode = 1;
|
|
19110
19468
|
return;
|
|
19111
19469
|
}
|
|
19112
19470
|
if (options.command === "migrate" || options.command === "status") {
|
|
@@ -19194,7 +19552,7 @@ async function runDbCommand(options) {
|
|
|
19194
19552
|
}
|
|
19195
19553
|
return;
|
|
19196
19554
|
}
|
|
19197
|
-
const projectDir =
|
|
19555
|
+
const projectDir = resolve7(options.projectDir ?? process.cwd());
|
|
19198
19556
|
const config = loadProjectConfig(projectDir, process.env, options.runtimeMode);
|
|
19199
19557
|
const project = await loadSupabaseProject(projectDir, {
|
|
19200
19558
|
enabled: false,
|
|
@@ -19315,7 +19673,7 @@ async function runSnapshotCommand(options) {
|
|
|
19315
19673
|
const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
|
|
19316
19674
|
const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
|
|
19317
19675
|
await writeStandardOutput([
|
|
19318
|
-
`Snapshot restored from ${
|
|
19676
|
+
`Snapshot restored from ${resolve7(input)}`,
|
|
19319
19677
|
...rollbackLines,
|
|
19320
19678
|
...reconnectLine
|
|
19321
19679
|
].join(`
|
|
@@ -19358,6 +19716,21 @@ async function runUpgradeCommand(options) {
|
|
|
19358
19716
|
throw new Error(`upgrade failed; snapshot retained at ${output}. Restore it with ` + `"supacloud-lite snapshot restore ${output} --force". ${error instanceof Error ? error.message : String(error)}`);
|
|
19359
19717
|
}
|
|
19360
19718
|
}
|
|
19719
|
+
function latestSnapshotTimestamp(stateDir) {
|
|
19720
|
+
let newest;
|
|
19721
|
+
for (const directory of [stateDir, join10(stateDir, "backups")]) {
|
|
19722
|
+
if (!existsSync5(directory))
|
|
19723
|
+
continue;
|
|
19724
|
+
for (const name of readdirSync(directory)) {
|
|
19725
|
+
if (!name.endsWith(".tar.gz"))
|
|
19726
|
+
continue;
|
|
19727
|
+
const modified = statSync2(join10(directory, name)).mtimeMs;
|
|
19728
|
+
if (newest === undefined || modified > newest)
|
|
19729
|
+
newest = modified;
|
|
19730
|
+
}
|
|
19731
|
+
}
|
|
19732
|
+
return newest;
|
|
19733
|
+
}
|
|
19361
19734
|
async function printInspection(rows) {
|
|
19362
19735
|
if (rows.length === 0) {
|
|
19363
19736
|
await writeStandardOutput(`No tables in schema "public".
|
|
@@ -19387,6 +19760,13 @@ async function printDoctor(report) {
|
|
|
19387
19760
|
for (const warning of report.powersync_readiness.warnings)
|
|
19388
19761
|
output.push(`warning: ${warning}`);
|
|
19389
19762
|
}
|
|
19763
|
+
if (report.production_readiness) {
|
|
19764
|
+
output.push(`production_ready: ${report.production_readiness.ready}`);
|
|
19765
|
+
for (const blocker of report.production_readiness.blockers)
|
|
19766
|
+
output.push(`production_blocker: ${blocker}`);
|
|
19767
|
+
for (const warning of report.production_readiness.warnings)
|
|
19768
|
+
output.push(`production_warning: ${warning}`);
|
|
19769
|
+
}
|
|
19390
19770
|
await writeStandardOutput(`${output.join(`
|
|
19391
19771
|
`)}
|
|
19392
19772
|
`);
|
|
@@ -19457,6 +19837,10 @@ Options:
|
|
|
19457
19837
|
--runtime-mode development (default) or strict
|
|
19458
19838
|
--postgres-dir operator-managed PostgreSQL installation (native only)
|
|
19459
19839
|
--identity-module trusted external identity bridge module
|
|
19840
|
+
--external-auth-proxy-issuer <url>
|
|
19841
|
+
proxy OAuth token/userinfo/logout to this external issuer
|
|
19842
|
+
--external-auth-proxy-redirect-origin <origin>
|
|
19843
|
+
allowed logout redirect origin (repeatable list via commas)
|
|
19460
19844
|
-o, --output <p> output file for gen types
|
|
19461
19845
|
-f, --file <name> migration suffix for db diff
|
|
19462
19846
|
--baseline db pull: write and record already-present schema as a migration
|