@supacloud/lite 0.33.1 → 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/index.js CHANGED
@@ -88,7 +88,7 @@ function randomToken(bytes = 32) {
88
88
  // package.json
89
89
  var package_default = {
90
90
  name: "@supacloud/lite",
91
- version: "0.33.1",
91
+ version: "0.34.0",
92
92
  description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
93
93
  type: "module",
94
94
  license: "Apache-2.0",
@@ -108,6 +108,7 @@ var package_default = {
108
108
  "dist",
109
109
  "!dist/standalone",
110
110
  "README.md",
111
+ "deploy",
111
112
  "CHANGELOG.md",
112
113
  "RELEASING.md",
113
114
  "THIRD_PARTY_NOTICES.md",
@@ -127,7 +128,7 @@ var package_default = {
127
128
  clean: "bun -e 'import { $ } from \"bun\"; await $`rm -rf dist`'",
128
129
  dev: "bun run src/cli.ts start",
129
130
  start: "bun run src/cli.ts start",
130
- test: "bun test --timeout 20000",
131
+ test: "bun run scripts/run-tests.ts",
131
132
  "test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts test/qauth-workflow-matrix.test.ts test/maker-checker-workflow.test.ts test/commands-artifacts.test.ts --timeout 180000",
132
133
  "test:graphql": "SUPACLOUD_LITE_TEST_GRAPHQL=1 bun test test/graphql-native.test.ts --timeout 180000",
133
134
  benchmark: "bun run scripts/benchmark.ts",
@@ -147,31 +148,31 @@ var package_default = {
147
148
  "@electric-sql/pglite-pgmq": "0.0.8",
148
149
  "@electric-sql/pglite-pgvector": "0.0.11",
149
150
  "@electric-sql/pglite-postgis": "0.2.10",
150
- "@supacloud/db": "0.20.0",
151
+ "@supacloud/db": "0.20.1",
151
152
  "exact-mirror": "1.2.6",
152
153
  tar: "^7.5.22",
153
- typebox: "1.3.37"
154
+ typebox: "1.4.2"
154
155
  },
155
156
  devDependencies: {
156
- "@supacloud/app": "0.31.0",
157
- "@supacloud/contracts": "0.13.0",
158
- "@supacloud/compiler": "0.50.0",
159
- "@supacloud/elysia": "0.35.1",
157
+ "@supacloud/app": "0.33.0",
158
+ "@supacloud/contracts": "0.15.1",
159
+ "@supacloud/compiler": "0.51.1",
160
+ "@supacloud/elysia": "0.36.1",
160
161
  "@supabase/supabase-js": "^2.118.0",
161
162
  "@types/bun": "^1.4.3",
162
163
  effect: "4.0.5",
163
- elysia: "2.0.0-beta.32",
164
- jose: "^6.2.12",
164
+ elysia: "2.0.0-rc.0",
165
+ jose: "^6.2.13",
165
166
  typescript: "^7.0.2"
166
167
  },
167
168
  overrides: {
168
- "@supacloud/db": "0.20.0",
169
- "@supacloud/app": "0.31.0",
170
- "@supacloud/compiler": "0.50.0",
171
- "@supacloud/elysia": "0.35.1",
172
- "@supacloud/contracts": "0.13.0",
173
- "@supacloud/commands": "0.11.0",
174
- "@supacloud/delivery": "0.21.0",
169
+ "@supacloud/db": "0.20.1",
170
+ "@supacloud/app": "0.33.0",
171
+ "@supacloud/compiler": "0.51.1",
172
+ "@supacloud/elysia": "0.36.1",
173
+ "@supacloud/contracts": "0.15.1",
174
+ "@supacloud/commands": "0.11.1",
175
+ "@supacloud/delivery": "0.21.1",
175
176
  "@graphql-tools/utils": "^12.0.4"
176
177
  },
177
178
  engines: {
@@ -251,7 +252,7 @@ function isAllowedRedirect(requested, siteUrl, allowList = []) {
251
252
  var encoder2 = new TextEncoder;
252
253
  var decoder2 = new TextDecoder;
253
254
  var strictDecoder = new TextDecoder("utf-8", { fatal: true });
254
- var MAX_INT32 = 2 ** 32;
255
+ var UINT32_RANGE = 2 ** 32;
255
256
  function concat(...buffers) {
256
257
  const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
257
258
  let i = 0;
@@ -410,23 +411,23 @@ function isDisjoint(...headers) {
410
411
  function decodeBase64url(value, label, ErrorClass) {
411
412
  try {
412
413
  return decode(value);
413
- } catch {
414
- throw new ErrorClass(`Failed to base64url decode the ${label}`);
414
+ } catch (cause) {
415
+ throw new ErrorClass(`Failed to base64url decode the ${label}`, { cause });
415
416
  }
416
417
  }
417
- function encodeBase64url(value, label, ErrorClass) {
418
+ function encodeAsciiMember(value, label, ErrorClass) {
418
419
  try {
419
420
  return encode(value);
420
- } catch {
421
- throw new ErrorClass(`The ${label} is not a valid base64url string`);
421
+ } catch (cause) {
422
+ throw new ErrorClass(`The ${label} is not a valid base64url string`, { cause });
422
423
  }
423
424
  }
424
- function parseJoseHeader(b64, ErrorClass, message) {
425
+ function parseJoseHeader(encodedProtectedHeader, ErrorClass, message) {
425
426
  let parsed;
426
427
  try {
427
- parsed = JSON.parse(strictDecoder.decode(decode(b64)));
428
- } catch {
429
- throw new ErrorClass(message);
428
+ parsed = JSON.parse(strictDecoder.decode(decode(encodedProtectedHeader)));
429
+ } catch (cause) {
430
+ throw new ErrorClass(message, { cause });
430
431
  }
431
432
  if (!isObject(parsed))
432
433
  throw new ErrorClass(message);
@@ -484,15 +485,15 @@ var jwkMatchesOp = (entry, key, usage) => {
484
485
  }
485
486
  };
486
487
  async function prepareKey(entry, key, usage) {
487
- const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
488
+ const { alg, secret } = entry, requiresPrivateKey = usage === "decrypt" || usage === "sign";
488
489
  if (secret && key instanceof Uint8Array)
489
490
  return key;
490
491
  let normalized, keyObject;
491
492
  if (isObject(key)) {
492
493
  if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
493
494
  throw invalidKeyType(alg, key, secret);
494
- if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (privateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === undefined && normalized.priv === undefined)))
495
- throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${privateKey ? "private" : "public"} JWK`);
495
+ if (!(secret ? normalized.kty === "oct" && typeof normalized.k == "string" : normalized.kty !== "oct" && (requiresPrivateKey ? normalized.kty === "AKP" && typeof normalized.priv == "string" || typeof normalized.d == "string" : normalized.d === undefined && normalized.priv === undefined)))
496
+ throw new TypeError(secret ? 'JSON Web Key for symmetric algorithms must have JWK "kty" (Key Type) equal to "oct" and the JWK "k" (Key Value) present' : `JSON Web Key for this operation must be a ${requiresPrivateKey ? "private" : "public"} JWK`);
496
497
  if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
497
498
  return decode(normalized.k);
498
499
  if (!Object.isFrozen(key)) {
@@ -502,7 +503,7 @@ async function prepareKey(entry, key, usage) {
502
503
  } else {
503
504
  if (!isKeyLike(key))
504
505
  throw invalidKeyType(alg, key, secret);
505
- const expectedType = secret ? "secret" : privateKey ? "private" : "public";
506
+ const expectedType = secret ? "secret" : requiresPrivateKey ? "private" : "public";
506
507
  if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
507
508
  throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
508
509
  if (isCryptoKey(key))
@@ -641,8 +642,8 @@ function eddsa() {
641
642
  usages: sig
642
643
  };
643
644
  }
644
- function mldsa(bits) {
645
- const subtle = { name: `ML-DSA-${bits}` };
645
+ function mldsa(parameterSet) {
646
+ const subtle = { name: `ML-DSA-${parameterSet}` };
646
647
  return {
647
648
  kty: ["AKP"],
648
649
  subtle,
@@ -680,21 +681,23 @@ function jwsAlgorithm(alg) {
680
681
  function prepareVerify(options) {
681
682
  return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
682
683
  }
683
- function parseProtectedHeader(encodedProtected) {
684
- return encodedProtected === undefined ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid");
684
+ function parseProtectedHeader(encodedProtectedHeader) {
685
+ return encodedProtectedHeader === undefined ? {} : parseJoseHeader(encodedProtectedHeader, JWSInvalid, "JWS Protected Header is invalid");
685
686
  }
686
687
  function encodeCompactUnencodedPayload(payload) {
687
688
  try {
688
689
  return encode(payload);
689
- } catch {
690
- throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters");
690
+ } catch (cause) {
691
+ throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters", {
692
+ cause
693
+ });
691
694
  }
692
695
  }
693
696
  async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) {
694
- const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected);
695
- if (!isDisjoint(parsedProt, header))
697
+ const { protected: encodedProtectedHeader, header, payload: inputPayload } = jws, protectedHeader = parsedProtected ?? parseProtectedHeader(encodedProtectedHeader);
698
+ if (!isDisjoint(protectedHeader, header))
696
699
  throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
697
- const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader;
700
+ const joseHeader = { ...protectedHeader, ...header }, b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], protectedHeader, joseHeader)), { alg } = joseHeader;
698
701
  if (typeof alg != "string" || !alg)
699
702
  throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
700
703
  if (shared[0] && !shared[0].has(alg))
@@ -706,17 +709,19 @@ async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedP
706
709
  throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
707
710
  const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload);
708
711
  let resolvedKey = false;
709
- typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = true);
710
- const entry = jwsAlgorithm(alg), data = concat(encodedProtected !== undefined ? encode(encodedProtected) : new Uint8Array, encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), k = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(k, entry.subtle, "verify");
712
+ typeof key == "function" && (key = await key(protectedHeader, jws), resolvedKey = true);
713
+ const entry = jwsAlgorithm(alg), signingInput = concat(encodedProtectedHeader !== undefined ? encode(encodedProtectedHeader) : new Uint8Array, encode("."), typeof signingPayload == "string" ? shared[2] ??= encodeAsciiMember(signingPayload, "payload", JWSInvalid) : signingPayload), signature = decodeBase64url(jws.signature, "signature", JWSInvalid), verificationKey = await prepareKey(entry, key, "verify"), cryptoKey = await rawKey(verificationKey, entry.subtle, "verify");
711
714
  entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey);
712
715
  let verified = false;
713
716
  try {
714
- verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
715
- } catch {}
717
+ verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, signingInput);
718
+ } catch (cause) {
719
+ throw new JWSSignatureVerificationFailed(undefined, { cause });
720
+ }
716
721
  if (!verified)
717
722
  throw new JWSSignatureVerificationFailed;
718
723
  const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload };
719
- return encodedProtected !== undefined && (result.protectedHeader = parsedProt), header !== undefined && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64];
724
+ return encodedProtectedHeader !== undefined && (result.protectedHeader = protectedHeader), header !== undefined && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: verificationKey }, b64] : [result, b64];
720
725
  }
721
726
  async function verifyCompact(jws, shared, key) {
722
727
  if (jws instanceof Uint8Array && (jws = decoder2.decode(jws)), typeof jws != "string")
@@ -746,8 +751,8 @@ function secs(str) {
746
751
  typeof str != "string" && invalidDuration();
747
752
  const matched = REGEX.exec(str);
748
753
  (!matched || matched[4] && matched[1]) && invalidDuration();
749
- const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
750
- return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
754
+ const value = parseFloat(matched[2]), durationSeconds = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
755
+ return Number.isFinite(durationSeconds) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -durationSeconds : durationSeconds;
751
756
  }
752
757
  function validateInput(label, input) {
753
758
  if (!Number.isFinite(input))
@@ -759,33 +764,35 @@ var normalizeTyp = (value) => {
759
764
  return value.includes("/") ? normalized : `application/${normalized}`;
760
765
  };
761
766
  var checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : false;
762
- function validateNumericDate(payload, claim, required = false) {
763
- const value = payload[claim];
767
+ function validateNumericDate(claimsSet, claim, required = false) {
768
+ const value = claimsSet[claim];
764
769
  if (!(value === undefined && !required)) {
765
770
  if (typeof value != "number")
766
- throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid");
771
+ throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, claimsSet, claim, "invalid");
767
772
  return value;
768
773
  }
769
774
  }
770
- function unexpectedClaim(payload, claim) {
771
- throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed);
775
+ function unexpectedClaim(claimsSet, claim) {
776
+ throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, claimsSet, claim, checkFailed);
772
777
  }
773
- function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
774
- let payload;
778
+ function validateClaimsSet(protectedHeader, claimsSetBytes, options = {}) {
779
+ let claimsSet;
775
780
  try {
776
- payload = JSON.parse(strictDecoder.decode(encodedPayload));
777
- } catch {}
778
- if (!isObject(payload))
781
+ claimsSet = JSON.parse(strictDecoder.decode(claimsSetBytes));
782
+ } catch (cause) {
783
+ throw new JWTInvalid("JWT Claims Set must be a top-level JSON object", { cause });
784
+ }
785
+ if (!isObject(claimsSet))
779
786
  throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
780
787
  const { typ } = options;
781
788
  if (typ !== undefined && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
782
- throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
789
+ throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', claimsSet, "typ", checkFailed);
783
790
  const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
784
791
  maxTokenAge !== undefined && presenceCheck.push("iat"), audience !== undefined && presenceCheck.push("aud"), subject !== undefined && presenceCheck.push("sub"), issuer !== undefined && presenceCheck.push("iss");
785
792
  for (const claim of new Set(presenceCheck.reverse()))
786
- if (!Object.hasOwn(payload, claim))
787
- throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
788
- issuer !== undefined && !(Array.isArray(issuer) ? issuer : [issuer]).includes(payload.iss) && unexpectedClaim(payload, "iss"), subject !== undefined && payload.sub !== subject && unexpectedClaim(payload, "sub"), audience !== undefined && !checkAudiencePresence(payload.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(payload, "aud");
793
+ if (!Object.hasOwn(claimsSet, claim))
794
+ throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claimsSet, claim, "missing");
795
+ issuer !== undefined && !(Array.isArray(issuer) ? issuer : [issuer]).includes(claimsSet.iss) && unexpectedClaim(claimsSet, "iss"), subject !== undefined && claimsSet.sub !== subject && unexpectedClaim(claimsSet, "sub"), audience !== undefined && !checkAudiencePresence(claimsSet.aud, typeof audience == "string" ? [audience] : audience) && unexpectedClaim(claimsSet, "aud");
789
796
  const { clockTolerance } = options;
790
797
  let tolerance = 0;
791
798
  if (typeof clockTolerance == "string")
@@ -796,20 +803,20 @@ function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
796
803
  tolerance = clockTolerance;
797
804
  }
798
805
  validateInput("clockTolerance option", tolerance);
799
- const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === undefined ? /* @__PURE__ */ new Date : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== undefined), nbf = validateNumericDate(payload, "nbf");
806
+ const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === undefined ? /* @__PURE__ */ new Date : currentDate)), iat = validateNumericDate(claimsSet, "iat", maxTokenAge !== undefined), nbf = validateNumericDate(claimsSet, "nbf");
800
807
  if (nbf !== undefined && nbf > now + tolerance)
801
- throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
802
- const exp = validateNumericDate(payload, "exp");
808
+ throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', claimsSet, "nbf", checkFailed);
809
+ const exp = validateNumericDate(claimsSet, "exp");
803
810
  if (exp !== undefined && exp <= now - tolerance)
804
- throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
811
+ throw new JWTExpired('"exp" claim timestamp check failed', claimsSet, "exp", checkFailed);
805
812
  if (maxTokenAge !== undefined) {
806
813
  const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
807
814
  if (age - tolerance > max)
808
- throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
815
+ throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', claimsSet, "iat", checkFailed);
809
816
  if (age < -tolerance)
810
- throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
817
+ throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', claimsSet, "iat", checkFailed);
811
818
  }
812
- return payload;
819
+ return claimsSet;
813
820
  }
814
821
 
815
822
  // node_modules/jose/dist/webapi/jwt/verify.js
@@ -844,7 +851,9 @@ function createLocalJWKSet(jwks) {
844
851
  let snapshot;
845
852
  try {
846
853
  snapshot = structuredClone(jwks);
847
- } catch {}
854
+ } catch (cause) {
855
+ throw new JWKSInvalid("JSON Web Key Set malformed", { cause });
856
+ }
848
857
  if (!isJwkSet(snapshot))
849
858
  throw new JWKSInvalid("JSON Web Key Set malformed");
850
859
  const metadata = snapshot.keys.map((jwk) => {
@@ -878,7 +887,7 @@ function isCloudflareWorkers() {
878
887
  return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel";
879
888
  }
880
889
  var USER_AGENT;
881
- (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12");
890
+ (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.13");
882
891
  var customFetch = /* @__PURE__ */ Symbol();
883
892
  async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
884
893
  const response = await fetchImpl(url, {
@@ -887,14 +896,14 @@ async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
887
896
  redirect: "manual",
888
897
  headers
889
898
  }).catch((err) => {
890
- throw err.name === "TimeoutError" ? new JWKSTimeout : err;
899
+ throw err.name === "TimeoutError" ? new JWKSTimeout(undefined, { cause: err }) : err;
891
900
  });
892
901
  if (response.status !== 200)
893
902
  throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response");
894
903
  try {
895
904
  return await response.json();
896
- } catch {
897
- throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON");
905
+ } catch (cause) {
906
+ throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON", { cause });
898
907
  }
899
908
  }
900
909
  var jwksCache = /* @__PURE__ */ Symbol();
@@ -9516,10 +9525,21 @@ async function sha256Hex2(text) {
9516
9525
  }
9517
9526
 
9518
9527
  // src/runtime/realtime/engine.ts
9528
+ function normalizeBroadcastTopic(raw) {
9529
+ const topic = raw.trim().replace(/^realtime:/, "");
9530
+ if (!topic)
9531
+ throw new Error("broadcast topic is required");
9532
+ return topic;
9533
+ }
9534
+
9519
9535
  class RealtimeEngine {
9520
9536
  db;
9521
9537
  jwtSecret;
9522
9538
  resolveExternalToken;
9539
+ static MAX_HTTP_BROADCAST_MESSAGES = 100;
9540
+ static MAX_HTTP_BROADCAST_TOPIC_BYTES = 256;
9541
+ static MAX_HTTP_BROADCAST_EVENT_BYTES = 256;
9542
+ static MAX_HTTP_BROADCAST_PAYLOAD_BYTES = 1024 * 1024;
9523
9543
  connections = new Set;
9524
9544
  bindingCounter = 1;
9525
9545
  phxRefCounter = 1;
@@ -9566,6 +9586,47 @@ class RealtimeEngine {
9566
9586
  return { read: false, write: false };
9567
9587
  }
9568
9588
  }
9589
+ async broadcastHttp(messages, ctx) {
9590
+ if (messages.length === 0)
9591
+ throw new Error("messages must not be empty");
9592
+ if (messages.length > RealtimeEngine.MAX_HTTP_BROADCAST_MESSAGES) {
9593
+ throw new Error(`messages must contain at most ${RealtimeEngine.MAX_HTTP_BROADCAST_MESSAGES} items`);
9594
+ }
9595
+ const normalized = [];
9596
+ for (const message of messages) {
9597
+ if (typeof message.topic !== "string")
9598
+ throw new Error("broadcast topic must be a string");
9599
+ if (typeof message.event !== "string")
9600
+ throw new Error("broadcast event must be a string");
9601
+ const topic = normalizeBroadcastTopic(message.topic);
9602
+ if (new TextEncoder().encode(topic).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_TOPIC_BYTES) {
9603
+ throw new Error("broadcast topic is too large");
9604
+ }
9605
+ const event = message.event.trim();
9606
+ if (!event)
9607
+ throw new Error("broadcast event is required");
9608
+ if (new TextEncoder().encode(event).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_EVENT_BYTES) {
9609
+ throw new Error("broadcast event is too large");
9610
+ }
9611
+ let payloadText;
9612
+ try {
9613
+ payloadText = JSON.stringify(message.payload ?? null);
9614
+ } catch {
9615
+ throw new Error("broadcast payload must be JSON serializable");
9616
+ }
9617
+ if (new TextEncoder().encode(payloadText).byteLength > RealtimeEngine.MAX_HTTP_BROADCAST_PAYLOAD_BYTES) {
9618
+ throw new Error("broadcast payload is too large");
9619
+ }
9620
+ if (message.private === true) {
9621
+ const access = await this.authorizePrivate(topic, ctx);
9622
+ if (!access.write)
9623
+ throw new Error(`not authorized to broadcast on private topic: ${topic}`);
9624
+ }
9625
+ normalized.push({ topic, event, payload: message.payload ?? null });
9626
+ }
9627
+ for (const message of normalized)
9628
+ this.dispatchDbBroadcast(message);
9629
+ }
9569
9630
  dispatchDbBroadcast(msg) {
9570
9631
  const full = `realtime:${msg.topic}`;
9571
9632
  for (const conn of this.connections) {
@@ -9766,8 +9827,7 @@ class RealtimeEngine {
9766
9827
  private: isPrivate,
9767
9828
  canBroadcast
9768
9829
  };
9769
- if (this.resolveExternalToken)
9770
- this.leaveChannel(conn, msg.topic);
9830
+ this.leaveChannel(conn, msg.topic);
9771
9831
  conn.channels.set(msg.topic, channel);
9772
9832
  this.scheduleExternalExpiry(conn, channel);
9773
9833
  this.reply(conn, msg, "ok", {
@@ -9807,21 +9867,32 @@ class RealtimeEngine {
9807
9867
  if (channel.expiryTimer)
9808
9868
  clearTimeout(channel.expiryTimer);
9809
9869
  conn.channels.delete(topic);
9810
- const state = this.presence.get(topic);
9811
- if (state?.has(channel.presenceKey)) {
9812
- const metas = state.get(channel.presenceKey);
9813
- if (!metas)
9814
- return;
9870
+ this.removePresence(channel);
9871
+ }
9872
+ removePresence(channel) {
9873
+ const presenceRef = channel.presenceRef;
9874
+ if (!presenceRef)
9875
+ return;
9876
+ channel.presenceRef = undefined;
9877
+ const state = this.presence.get(channel.topic);
9878
+ const current = state?.get(channel.presenceKey);
9879
+ const leaving = current?.metas.find((meta) => meta.phx_ref === presenceRef);
9880
+ if (!state || !current || !leaving)
9881
+ return;
9882
+ const remaining = current.metas.filter((meta) => meta.phx_ref !== presenceRef);
9883
+ if (remaining.length > 0) {
9884
+ state.set(channel.presenceKey, { metas: remaining });
9885
+ } else {
9815
9886
  state.delete(channel.presenceKey);
9816
9887
  if (state.size === 0)
9817
- this.presence.delete(topic);
9818
- this.broadcastToTopic(topic, {
9819
- topic,
9820
- event: "presence_diff",
9821
- payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
9822
- ref: null
9823
- });
9888
+ this.presence.delete(channel.topic);
9824
9889
  }
9890
+ this.broadcastToTopic(channel.topic, {
9891
+ topic: channel.topic,
9892
+ event: "presence_diff",
9893
+ payload: { joins: {}, leaves: { [channel.presenceKey]: { metas: [leaving] } } },
9894
+ ref: null
9895
+ });
9825
9896
  }
9826
9897
  scheduleExternalExpiry(conn, channel) {
9827
9898
  if (channel.expiryTimer)
@@ -9901,23 +9972,30 @@ class RealtimeEngine {
9901
9972
  return;
9902
9973
  const event = String(msg.payload?.event ?? "").toLowerCase();
9903
9974
  if (event === "track") {
9904
- const meta = {
9905
- ...msg.payload?.payload ?? {},
9906
- phx_ref: `F${this.phxRefCounter++}`
9907
- };
9975
+ const payload = { ...msg.payload?.payload ?? {} };
9976
+ delete payload.phx_ref;
9977
+ delete payload.phx_ref_prev;
9908
9978
  let state = this.presence.get(msg.topic);
9909
9979
  if (!state) {
9910
9980
  state = new Map;
9911
9981
  this.presence.set(msg.topic, state);
9912
9982
  }
9913
- const previous = state.get(channel.presenceKey);
9914
- state.set(channel.presenceKey, { metas: [meta] });
9983
+ const current = state.get(channel.presenceKey);
9984
+ const previous = current?.metas.find((meta) => meta.phx_ref === channel.presenceRef);
9985
+ const meta = {
9986
+ ...payload,
9987
+ phx_ref: `F${this.phxRefCounter++}`,
9988
+ ...previous ? { phx_ref_prev: previous.phx_ref } : {}
9989
+ };
9990
+ const others = current?.metas.filter((meta) => meta.phx_ref !== channel.presenceRef) ?? [];
9991
+ state.set(channel.presenceKey, { metas: [...others, meta] });
9992
+ channel.presenceRef = meta.phx_ref;
9915
9993
  this.broadcastToTopic(msg.topic, {
9916
9994
  topic: msg.topic,
9917
9995
  event: "presence_diff",
9918
9996
  payload: {
9919
9997
  joins: { [channel.presenceKey]: { metas: [meta] } },
9920
- leaves: previous ? { [channel.presenceKey]: previous } : {}
9998
+ leaves: previous ? { [channel.presenceKey]: { metas: [previous] } } : {}
9921
9999
  },
9922
10000
  ref: null
9923
10001
  });
@@ -9926,19 +10004,7 @@ class RealtimeEngine {
9926
10004
  return;
9927
10005
  }
9928
10006
  if (event === "untrack") {
9929
- const state = this.presence.get(msg.topic);
9930
- const metas = state?.get(channel.presenceKey);
9931
- if (state && metas) {
9932
- state.delete(channel.presenceKey);
9933
- if (state.size === 0)
9934
- this.presence.delete(msg.topic);
9935
- this.broadcastToTopic(msg.topic, {
9936
- topic: msg.topic,
9937
- event: "presence_diff",
9938
- payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
9939
- ref: null
9940
- });
9941
- }
10007
+ this.removePresence(channel);
9942
10008
  if (msg.ref)
9943
10009
  this.reply(conn, msg, "ok", {});
9944
10010
  }
@@ -11923,6 +11989,11 @@ class StorageHandler {
11923
11989
  const rest = url.pathname.replace(/^\/storage\/v1\/?/, "").replace(/\/+$/, "");
11924
11990
  const method = req.method.toUpperCase();
11925
11991
  try {
11992
+ if (rest === "status" && method === "GET") {
11993
+ return json3(200, { status: "ok", name: "supacloud-lite-storage", version: SUPACLOUD_LITE_VERSION });
11994
+ }
11995
+ if (rest === "metrics" && method === "GET")
11996
+ return await this.metrics(ctx);
11926
11997
  if (rest === "bucket" && method === "POST")
11927
11998
  return await this.createBucket(req, ctx);
11928
11999
  if (rest === "bucket" && method === "GET")
@@ -12043,6 +12114,37 @@ class StorageHandler {
12043
12114
  release();
12044
12115
  }
12045
12116
  }
12117
+ async metrics(ctx) {
12118
+ const denied = this.requireService(ctx);
12119
+ if (denied)
12120
+ return denied;
12121
+ const [buckets, objects] = await Promise.all([
12122
+ this.db.query(`select count(*)::int as count from storage.buckets`),
12123
+ this.db.query(`select count(*)::int as count,
12124
+ coalesce(sum(case when metadata->>'size' ~ '^[0-9]+$' then (metadata->>'size')::bigint else 0 end), 0) as bytes
12125
+ from storage.objects`)
12126
+ ]);
12127
+ const bucketCount = buckets.rows[0]?.count ?? 0;
12128
+ const objectCount = objects.rows[0]?.count ?? 0;
12129
+ const objectBytes = Number(objects.rows[0]?.bytes ?? 0);
12130
+ const body = [
12131
+ "# HELP storage_buckets_total Number of storage buckets.",
12132
+ "# TYPE storage_buckets_total gauge",
12133
+ `storage_buckets_total ${bucketCount}`,
12134
+ "# HELP storage_objects_total Number of storage object metadata rows.",
12135
+ "# TYPE storage_objects_total gauge",
12136
+ `storage_objects_total ${objectCount}`,
12137
+ "# HELP storage_object_bytes_total Sum of declared object sizes in bytes.",
12138
+ "# TYPE storage_object_bytes_total gauge",
12139
+ `storage_object_bytes_total ${Number.isFinite(objectBytes) ? objectBytes : 0}`,
12140
+ ""
12141
+ ].join(`
12142
+ `);
12143
+ return new Response(body, {
12144
+ status: 200,
12145
+ headers: { "content-type": "text/plain; version=0.0.4; charset=utf-8" }
12146
+ });
12147
+ }
12046
12148
  requireService(ctx) {
12047
12149
  if (ctx.role !== "service_role") {
12048
12150
  return storageError(403, "Unauthorized", "Bucket management requires the service_role key");
@@ -14161,6 +14263,9 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
14161
14263
  if (smsInbox && (path === "/sms-inbox" || path.startsWith("/sms-inbox/"))) {
14162
14264
  return withCors(smsInbox.serve(req, url));
14163
14265
  }
14266
+ if (path === "/storage/v1/status" && req.method === "GET") {
14267
+ return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
14268
+ }
14164
14269
  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/")) {
14165
14270
  if (req.method === "GET" || req.method === "HEAD") {
14166
14271
  return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
@@ -14196,6 +14301,50 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
14196
14301
  const ctx = await resolveContext(req, url);
14197
14302
  if (ctx instanceof Response)
14198
14303
  return ctx;
14304
+ if (path === "/realtime/v1/api/broadcast" || path === "/realtime/v1/api/broadcast/") {
14305
+ if (req.method !== "POST") {
14306
+ return withCors(new Response(JSON.stringify({ message: "Method not allowed" }), {
14307
+ status: 405,
14308
+ headers: { "content-type": "application/json", allow: "POST" }
14309
+ }));
14310
+ }
14311
+ const contentType = req.headers.get("content-type") ?? "";
14312
+ if (contentType && !contentType.includes("application/json")) {
14313
+ return withCors(new Response(JSON.stringify({ message: "Content-Type must be application/json" }), {
14314
+ status: 415,
14315
+ headers: { "content-type": "application/json" }
14316
+ }));
14317
+ }
14318
+ let body;
14319
+ try {
14320
+ body = await req.json();
14321
+ } catch {
14322
+ return withCors(new Response(JSON.stringify({ message: "Invalid JSON body" }), {
14323
+ status: 400,
14324
+ headers: { "content-type": "application/json" }
14325
+ }));
14326
+ }
14327
+ const messages = typeof body === "object" && body !== null && "messages" in body ? body.messages : undefined;
14328
+ if (!Array.isArray(messages)) {
14329
+ return withCors(new Response(JSON.stringify({ message: "messages must be an array" }), {
14330
+ status: 400,
14331
+ headers: { "content-type": "application/json" }
14332
+ }));
14333
+ }
14334
+ try {
14335
+ await realtime.broadcastHttp(messages, ctx);
14336
+ } catch (error) {
14337
+ const message = error instanceof Error ? error.message : "Broadcast failed";
14338
+ return withCors(new Response(JSON.stringify({ message }), {
14339
+ status: message.startsWith("not authorized") ? 403 : 400,
14340
+ headers: { "content-type": "application/json" }
14341
+ }));
14342
+ }
14343
+ return withCors(new Response(JSON.stringify({ message: "Broadcast sent" }), {
14344
+ status: 202,
14345
+ headers: { "content-type": "application/json" }
14346
+ }));
14347
+ }
14199
14348
  if (path.startsWith("/rest/v1"))
14200
14349
  return withCors(await rest.handle(req, ctx, url));
14201
14350
  if (path === "/graphql/v1" || path === "/graphql/v1/")
@@ -14397,6 +14546,118 @@ class S3StorageDriver {
14397
14546
  return `${this.prefix}${normalizeKey(key)}`;
14398
14547
  }
14399
14548
  }
14549
+ // src/runtime/auth/proxy.ts
14550
+ var ALLOWED_ROUTES = {
14551
+ "/auth/v1/oauth/token": new Set(["POST"]),
14552
+ "/auth/v1/oauth/userinfo": new Set(["GET"]),
14553
+ "/auth/v1/logout": new Set(["GET", "POST"])
14554
+ };
14555
+ var FORWARD_REQUEST_HEADERS = ["accept", "authorization", "content-type", "apikey"];
14556
+ var FORWARD_RESPONSE_HEADERS = [
14557
+ "content-type",
14558
+ "cache-control",
14559
+ "retry-after",
14560
+ "www-authenticate"
14561
+ ];
14562
+ function proxyError(status, code, message, allow) {
14563
+ const headers = new Headers({
14564
+ "content-type": "application/json; charset=utf-8",
14565
+ "cache-control": "no-store"
14566
+ });
14567
+ if (allow)
14568
+ headers.set("allow", allow);
14569
+ return new Response(JSON.stringify({ code, message }), { status, headers });
14570
+ }
14571
+ function upstreamBase(issuer) {
14572
+ let url;
14573
+ try {
14574
+ url = new URL(issuer);
14575
+ } catch {
14576
+ throw new TypeError("External auth issuer must be an absolute URL");
14577
+ }
14578
+ const loopback = url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "[::1]" || url.hostname === "::1";
14579
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && loopback)) {
14580
+ throw new TypeError("External auth issuer must use HTTPS (HTTP is allowed only on loopback)");
14581
+ }
14582
+ if (url.username || url.password || url.search || url.hash) {
14583
+ throw new TypeError("External auth issuer must not contain credentials, query or fragment");
14584
+ }
14585
+ if (url.pathname.replace(/\/+$/, "") !== "/auth/v1") {
14586
+ throw new TypeError("External auth issuer must end with /auth/v1");
14587
+ }
14588
+ return url;
14589
+ }
14590
+ function redirectOrigins(base, extra = []) {
14591
+ const origins = new Set([base.origin]);
14592
+ for (const value of extra) {
14593
+ let origin;
14594
+ try {
14595
+ origin = new URL(value);
14596
+ } catch {
14597
+ throw new TypeError("External auth redirect origin must be an absolute URL");
14598
+ }
14599
+ const loopback = origin.hostname === "127.0.0.1" || origin.hostname === "localhost" || origin.hostname === "[::1]" || origin.hostname === "::1";
14600
+ if (origin.protocol !== "https:" && !(origin.protocol === "http:" && loopback)) {
14601
+ throw new TypeError("External auth redirect origin must use HTTPS (HTTP is allowed only on loopback)");
14602
+ }
14603
+ if (origin.username || origin.password || origin.search || origin.hash) {
14604
+ throw new TypeError("External auth redirect origin must not contain credentials, query or fragment");
14605
+ }
14606
+ origins.add(origin.origin);
14607
+ }
14608
+ return origins;
14609
+ }
14610
+ function createExternalAuthProxy(options) {
14611
+ const base = upstreamBase(options.issuer);
14612
+ const allowedRedirects = redirectOrigins(base, options.allowedRedirectOrigins);
14613
+ const fetcher = options.fetch ?? globalThis.fetch;
14614
+ if (typeof fetcher !== "function")
14615
+ throw new TypeError("External auth proxy requires fetch");
14616
+ return async (request) => {
14617
+ const source = new URL(request.url);
14618
+ const methods = ALLOWED_ROUTES[source.pathname];
14619
+ if (!methods)
14620
+ return null;
14621
+ if (!methods.has(request.method)) {
14622
+ return proxyError(405, "AUTH_PROXY_METHOD_NOT_ALLOWED", "Auth proxy method is not allowed", [...methods].join(", "));
14623
+ }
14624
+ const target = new URL(source.pathname + source.search, base.origin);
14625
+ const headers = new Headers;
14626
+ for (const name of FORWARD_REQUEST_HEADERS) {
14627
+ const value = request.headers.get(name);
14628
+ if (value !== null)
14629
+ headers.set(name, value);
14630
+ }
14631
+ const body = request.method === "GET" || request.method === "HEAD" ? undefined : await request.arrayBuffer();
14632
+ let upstream;
14633
+ try {
14634
+ upstream = await fetcher(target, {
14635
+ method: request.method,
14636
+ headers,
14637
+ body,
14638
+ redirect: "manual"
14639
+ });
14640
+ } catch {
14641
+ return proxyError(503, "AUTH_PROXY_UNAVAILABLE", "Identity service is temporarily unavailable");
14642
+ }
14643
+ const responseHeaders = new Headers;
14644
+ for (const name of FORWARD_RESPONSE_HEADERS) {
14645
+ const value = upstream.headers.get(name);
14646
+ if (value !== null)
14647
+ responseHeaders.set(name, value);
14648
+ }
14649
+ const location = upstream.headers.get("location");
14650
+ if (location !== null) {
14651
+ const resolved = new URL(location, base.origin);
14652
+ if (!allowedRedirects.has(resolved.origin)) {
14653
+ return proxyError(502, "AUTH_PROXY_REDIRECT_REJECTED", "Identity service redirect was rejected");
14654
+ }
14655
+ responseHeaders.set("location", resolved.href);
14656
+ }
14657
+ responseHeaders.set("cache-control", "no-store");
14658
+ return new Response(upstream.body, { status: upstream.status, headers: responseHeaders });
14659
+ };
14660
+ }
14400
14661
  // src/runtime/node/fs-driver.ts
14401
14662
  import { mkdir as mkdir2, readFile as readFile2, rename, rm, writeFile } from "fs/promises";
14402
14663
  import { dirname as dirname2, isAbsolute, join, relative, resolve as resolvePath, sep } from "path";
@@ -14467,7 +14728,11 @@ async function serveBun(backend, opts = {}) {
14467
14728
  const ip = srv.requestIP(req)?.address;
14468
14729
  if (ip)
14469
14730
  headers.set("x-supacloud-lite-remote-addr", ip);
14470
- return backend.fetch(new Request(req, { headers }));
14731
+ const request = new Request(req, { headers });
14732
+ const hookResponse = await opts.beforeRequest?.(request);
14733
+ if (hookResponse)
14734
+ return hookResponse;
14735
+ return backend.fetch(request);
14471
14736
  },
14472
14737
  websocket: {
14473
14738
  open(ws) {
@@ -15601,6 +15866,80 @@ function removeStalePidFile(pidPath) {
15601
15866
  }
15602
15867
  } catch {}
15603
15868
  }
15869
+ // src/runtime/node/production-readiness.ts
15870
+ import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve3 } from "path";
15871
+ var SNAPSHOT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
15872
+ function within(parent, child) {
15873
+ const rel = relative2(resolve3(parent), resolve3(child));
15874
+ return rel === "" || !rel.startsWith("..") && !isAbsolute2(rel);
15875
+ }
15876
+ function loopback(host) {
15877
+ return host === "127.0.0.1" || host === "localhost" || host === "::1" || host === "[::1]";
15878
+ }
15879
+ function absoluteHttpUrl(value) {
15880
+ try {
15881
+ const url = new URL(value);
15882
+ return (url.protocol === "https:" || url.protocol === "http:" && loopback(url.hostname)) && !url.username && !url.password;
15883
+ } catch {
15884
+ return false;
15885
+ }
15886
+ }
15887
+ function productionReadiness(input) {
15888
+ const blockers = [];
15889
+ const warnings = [];
15890
+ const now = input.now ?? Date.now();
15891
+ if (input.runtimeMode !== "strict") {
15892
+ blockers.push("RUNTIME_MODE_NOT_STRICT: production functions require strict runtime mode");
15893
+ }
15894
+ if (input.storageBackend === "memory") {
15895
+ blockers.push("MEMORY_STORAGE_NOT_DURABLE: memory storage loses objects on restart");
15896
+ }
15897
+ if (input.storageBackend === "fs") {
15898
+ warnings.push("LOCAL_STORAGE_REQUIRES_DURABLE_VOLUME_AND_BACKUP");
15899
+ }
15900
+ if (input.storageBackend === "s3") {
15901
+ warnings.push("S3_OBJECTS_NOT_INCLUDED_IN_SNAPSHOT: back up the bucket/prefix separately");
15902
+ }
15903
+ if (!loopback(input.host)) {
15904
+ warnings.push("DIRECT_NETWORK_BIND_WITHOUT_LITE_TLS: terminate TLS in a trusted proxy or restrict the network boundary");
15905
+ }
15906
+ if (input.engine === "pglite") {
15907
+ warnings.push("PGLITE_SINGLE_PROCESS_CAPACITY: validate the real schema, concurrency and Realtime load");
15908
+ }
15909
+ if (input.identityMode === "local") {
15910
+ warnings.push("LOCAL_AUTH_IN_PRODUCTION: own account lifecycle, confirmation and password reset");
15911
+ }
15912
+ if (input.identityMode === "external" && !input.externalAuthProxyIssuer) {
15913
+ warnings.push("EXTERNAL_AUTH_PROXY_NOT_CONFIGURED: cross-origin browser token/userinfo calls may fail CORS");
15914
+ }
15915
+ if (input.externalAuthProxyIssuer && !absoluteHttpUrl(input.externalAuthProxyIssuer)) {
15916
+ blockers.push("EXTERNAL_AUTH_PROXY_ISSUER_INVALID: use an HTTPS issuer, or HTTP only on loopback");
15917
+ }
15918
+ if (!input.siteUrl) {
15919
+ warnings.push("SITE_URL_NOT_CONFIGURED: set [auth].site_url for stable redirects");
15920
+ } else if (!absoluteHttpUrl(input.siteUrl)) {
15921
+ warnings.push("SITE_URL_NOT_HTTPS: production redirects should use HTTPS");
15922
+ }
15923
+ if (!input.apiUrl) {
15924
+ warnings.push("API_URL_NOT_CONFIGURED: set the public API URL used by Auth and Functions");
15925
+ } else if (!absoluteHttpUrl(input.apiUrl)) {
15926
+ warnings.push("API_URL_NOT_HTTPS: production API URLs should use HTTPS");
15927
+ }
15928
+ if (within(input.projectDir, input.stateDir)) {
15929
+ warnings.push("STATE_INSIDE_PROJECT: keep mutable state outside the reviewed project checkout");
15930
+ }
15931
+ if (input.dataDir && input.dataDir === input.storageDir) {
15932
+ blockers.push("DATABASE_AND_STORAGE_PATH_OVERLAP: use separate directories");
15933
+ } else if (input.dataDir && (within(input.dataDir, input.storageDir) || within(input.storageDir, input.dataDir))) {
15934
+ warnings.push("DATABASE_AND_STORAGE_PATH_NESTED: keep database and storage trees independent");
15935
+ }
15936
+ if (input.latestSnapshotAt === undefined) {
15937
+ warnings.push("NO_SNAPSHOT_FOUND: create and test a restore snapshot before promotion");
15938
+ } else if (now - input.latestSnapshotAt > SNAPSHOT_MAX_AGE_MS) {
15939
+ warnings.push("SNAPSHOT_STALE: newest snapshot is older than 7 days");
15940
+ }
15941
+ return { ready: blockers.length === 0, blockers, warnings };
15942
+ }
15604
15943
  // src/runtime/node/native/readiness.ts
15605
15944
  function liteCapabilities(engine, replicationProfile, authEnabled = true) {
15606
15945
  const extensionSource = engine === "native" ? "native-postgresql" : "pglite-bundled";
@@ -15889,7 +16228,7 @@ async function inspectPostgresExtensions(engine, source) {
15889
16228
  }
15890
16229
  // src/project-runtime.ts
15891
16230
  import { chmod, link, lstat as lstat2, mkdir as mkdir4, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile4 } from "fs/promises";
15892
- import { dirname as dirname4, isAbsolute as isAbsolute2, join as join7, parse, relative as relative2, resolve as resolve3 } from "path";
16231
+ import { dirname as dirname4, isAbsolute as isAbsolute3, join as join7, parse, relative as relative3, resolve as resolve4 } from "path";
15893
16232
  import { pathToFileURL as pathToFileURL2 } from "url";
15894
16233
 
15895
16234
  // src/runtime/node/config-toml.ts
@@ -17080,7 +17419,7 @@ function isNotFound(error) {
17080
17419
 
17081
17420
  // src/project-runtime.ts
17082
17421
  function resolveProjectPaths(options = {}) {
17083
- const projectDir = resolve3(options.projectDir ?? process.cwd());
17422
+ const projectDir = resolve4(options.projectDir ?? process.cwd());
17084
17423
  const stateDir = resolvePath2(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
17085
17424
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
17086
17425
  const dataDir = options.memory ? undefined : resolvePath2(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join7(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -17250,7 +17589,7 @@ async function loadProjectBindings(projectDir, config) {
17250
17589
  const bindings = config.lite.migrationBindings;
17251
17590
  if (!bindings)
17252
17591
  return;
17253
- const manifest = JSON.parse(await readFile6(resolve3(projectDir, bindings.manifest), "utf8"));
17592
+ const manifest = JSON.parse(await readFile6(resolve4(projectDir, bindings.manifest), "utf8"));
17254
17593
  return {
17255
17594
  manifest,
17256
17595
  target: { environment: bindings.environment, projectRef: bindings.projectRef },
@@ -17260,7 +17599,7 @@ async function loadProjectBindings(projectDir, config) {
17260
17599
  async function loadProjectIdentity(projectDir, module) {
17261
17600
  if (!module)
17262
17601
  return;
17263
- const imported = await import(pathToFileURL2(resolve3(projectDir, module)).href);
17602
+ const imported = await import(pathToFileURL2(resolve4(projectDir, module)).href);
17264
17603
  if (!isRecord(imported) || typeof imported["default"] !== "function") {
17265
17604
  throw new Error("Lite identity module must default-export an external identity verifier");
17266
17605
  }
@@ -17326,7 +17665,11 @@ async function startProjectServer(options = {}) {
17326
17665
  const resolvedOptions = options.port === 0 ? { ...options, port: await findEphemeralPort(options.host) } : options;
17327
17666
  const project = await createProjectBackend({ ...resolvedOptions, startRuntimeServices: true });
17328
17667
  try {
17329
- const server = await serveBun(project.backend, { host: project.host, port: project.port });
17668
+ const beforeRequest = options.externalAuthProxyIssuer ? createExternalAuthProxy({
17669
+ issuer: options.externalAuthProxyIssuer,
17670
+ allowedRedirectOrigins: options.externalAuthProxyRedirectOrigins
17671
+ }) : undefined;
17672
+ const server = await serveBun(project.backend, { host: project.host, port: project.port, beforeRequest });
17330
17673
  let closePromise = null;
17331
17674
  return {
17332
17675
  ...project,
@@ -17368,7 +17711,7 @@ async function loadWebhooks(projectDir) {
17368
17711
  }
17369
17712
  }
17370
17713
  function resolvePath2(projectDir, path) {
17371
- return isAbsolute2(path) ? path : resolve3(projectDir, path);
17714
+ return isAbsolute3(path) ? path : resolve4(projectDir, path);
17372
17715
  }
17373
17716
  function randomHex(bytes) {
17374
17717
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -17413,7 +17756,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
17413
17756
  }
17414
17757
  // src/snapshot.ts
17415
17758
  import { chmod as chmod2, copyFile, lstat as lstat3, mkdir as mkdir5, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile5 } from "fs/promises";
17416
- import { dirname as dirname5, join as join8, parse as parse2, relative as relative3, resolve as resolve4, sep as sep2 } from "path";
17759
+ import { dirname as dirname5, join as join8, parse as parse2, relative as relative4, resolve as resolve5, sep as sep2 } from "path";
17417
17760
  import { create as createTar, extract as extractTar2 } from "tar";
17418
17761
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
17419
17762
  var SNAPSHOT_VERSION = 1;
@@ -17437,7 +17780,7 @@ async function createSnapshot(options) {
17437
17780
  postgresMajor: await readPostgresMajor(paths.dataDir)
17438
17781
  } : {}
17439
17782
  };
17440
- const output = resolve4(options.output);
17783
+ const output = resolve5(options.output);
17441
17784
  if (await existingInfo(output))
17442
17785
  throw new Error(`snapshot output already exists: ${output}`);
17443
17786
  await mkdir5(dirname5(output), { recursive: true });
@@ -17478,7 +17821,7 @@ async function restoreSnapshot(options) {
17478
17821
  await mkdir5(payloadRoot, { recursive: true });
17479
17822
  await extractTar2({
17480
17823
  cwd: payloadRoot,
17481
- file: resolve4(options.input),
17824
+ file: resolve5(options.input),
17482
17825
  preserveOwner: false,
17483
17826
  preservePaths: false,
17484
17827
  strict: true,
@@ -17521,10 +17864,10 @@ async function restoreSnapshot(options) {
17521
17864
  await mkdir5(stateStage, { recursive: true });
17522
17865
  await copyEntry(join8(payloadRoot, "secrets.json"), join8(stateStage, "secrets.json"));
17523
17866
  if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
17524
- await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative3(paths.stateDir, paths.dataDir)));
17867
+ await copyEntry(join8(payloadRoot, "database"), join8(stateStage, relative4(paths.stateDir, paths.dataDir)));
17525
17868
  }
17526
17869
  if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
17527
- await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative3(paths.stateDir, paths.storageDir)));
17870
+ await copyEntry(join8(payloadRoot, "storage"), join8(stateStage, relative4(paths.stateDir, paths.storageDir)));
17528
17871
  }
17529
17872
  const swaps = [];
17530
17873
  try {
@@ -17558,11 +17901,11 @@ async function restoreSnapshot(options) {
17558
17901
  function normalizePaths(paths) {
17559
17902
  return {
17560
17903
  ...paths,
17561
- projectDir: resolve4(paths.projectDir),
17562
- stateDir: resolve4(paths.stateDir),
17563
- dataDir: paths.dataDir ? resolve4(paths.dataDir) : undefined,
17564
- storageDir: resolve4(paths.storageDir),
17565
- secretsFile: resolve4(paths.secretsFile)
17904
+ projectDir: resolve5(paths.projectDir),
17905
+ stateDir: resolve5(paths.stateDir),
17906
+ dataDir: paths.dataDir ? resolve5(paths.dataDir) : undefined,
17907
+ storageDir: resolve5(paths.storageDir),
17908
+ secretsFile: resolve5(paths.secretsFile)
17566
17909
  };
17567
17910
  }
17568
17911
  async function assertSnapshotPaths(paths, options = {}) {
@@ -17592,7 +17935,7 @@ async function assertSnapshotPaths(paths, options = {}) {
17592
17935
  async function assertDirectoryOrMissing(path) {
17593
17936
  if (!path)
17594
17937
  return;
17595
- if (resolve4(path) === parse2(resolve4(path)).root)
17938
+ if (resolve5(path) === parse2(resolve5(path)).root)
17596
17939
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
17597
17940
  try {
17598
17941
  const info = await lstat3(path);
@@ -17815,13 +18158,13 @@ async function assertNoSymlinks(root) {
17815
18158
  }
17816
18159
  }
17817
18160
  function isWithin(parent, child) {
17818
- const normalizedParent = resolve4(parent);
17819
- const normalizedChild = resolve4(child);
18161
+ const normalizedParent = resolve5(parent);
18162
+ const normalizedChild = resolve5(child);
17820
18163
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
17821
18164
  }
17822
18165
  function pathsOverlap(left, right) {
17823
- const normalizedLeft = resolve4(left);
17824
- const normalizedRight = resolve4(right);
18166
+ const normalizedLeft = resolve5(left);
18167
+ const normalizedRight = resolve5(right);
17825
18168
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
17826
18169
  }
17827
18170
  export {
@@ -17832,6 +18175,7 @@ export {
17832
18175
  S3StorageDriver,
17833
18176
  SUPACLOUD_LITE_VERSION,
17834
18177
  buildPowerSyncPostgresArgs,
18178
+ createExternalAuthProxy,
17835
18179
  createBackend as createLiteBackend,
17836
18180
  createNativeEngine,
17837
18181
  createPgliteEngine,
@@ -17850,6 +18194,7 @@ export {
17850
18194
  isNativeEngineSupported,
17851
18195
  liteCapabilities,
17852
18196
  mintProjectKeys,
18197
+ productionReadiness,
17853
18198
  resolveDatabaseEngine,
17854
18199
  resolveNativeReplicationOptions,
17855
18200
  resolveProjectPaths,