@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/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 resolve6 } from "path";
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.33.2",
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 isAbsolute2, join as join8, parse, relative as relative2, resolve as resolve4 } from "path";
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
- return backend.fetch(new Request(req, { headers }));
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 = relative(this.root, path);
17556
- if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
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 = resolve4(options.projectDir ?? process.cwd());
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 = resolve4(paths.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 = resolve4(paths.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 = resolve4(targetPath);
17975
- const relativePath = relative2(stateDir, target);
17976
- if (!relativePath || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
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 = resolve4(await realpath2(existingAncestor), relative2(existingAncestor, target));
18024
- const canonicalRelative = relative2(canonicalStateDir, canonicalTarget);
18025
- if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute2(canonicalRelative)) {
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(resolve4(projectDir, bindings.manifest), "utf8"));
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(resolve4(projectDir, module)).href);
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 server = await serveBun(project.backend, { host: project.host, port: project.port });
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 isAbsolute2(path) ? path : resolve4(projectDir, path);
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 relative3, resolve as resolve5, sep as sep2 } from "path";
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 = resolve5(options.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: resolve5(options.input),
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, relative3(paths.stateDir, paths.dataDir)));
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, relative3(paths.stateDir, paths.storageDir)));
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: resolve5(paths.projectDir),
18511
- stateDir: resolve5(paths.stateDir),
18512
- dataDir: paths.dataDir ? resolve5(paths.dataDir) : undefined,
18513
- storageDir: resolve5(paths.storageDir),
18514
- secretsFile: resolve5(paths.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 (resolve5(path) === parse2(resolve5(path)).root)
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 = resolve5(parent);
18768
- const normalizedChild = resolve5(child);
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 = resolve5(left);
18773
- const normalizedRight = resolve5(right);
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 = resolve6(next());
19238
+ options.projectDir = resolve7(next());
18906
19239
  else if (argument === "--state-dir")
18907
- options.stateDir = resolve6(next());
19240
+ options.stateDir = resolve7(next());
18908
19241
  else if (argument === "--data-dir")
18909
- options.dataDir = resolve6(next());
19242
+ options.dataDir = resolve7(next());
18910
19243
  else if (argument === "--storage-dir")
18911
- options.storageDir = resolve6(next());
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 = resolve6(next());
19254
+ options.postgresDir = resolve7(next());
18922
19255
  else if (argument === "--identity-module")
18923
- options.identityModule = resolve6(next());
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 = resolve6(next());
19272
+ options.replicationTlsCertFile = resolve7(next());
18936
19273
  else if (argument === "--replication-tls-key")
18937
- options.replicationTlsKeyFile = resolve6(next());
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 = resolve6(next());
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 = resolve6(next());
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 = resolve6(options.projectDir ?? process.cwd());
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 ${resolve6(input)}`,
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