@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/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.1",
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",
@@ -47,7 +48,7 @@ var package_default = {
47
48
  clean: "bun -e 'import { $ } from \"bun\"; await $`rm -rf dist`'",
48
49
  dev: "bun run src/cli.ts start",
49
50
  start: "bun run src/cli.ts start",
50
- test: "bun test --timeout 20000",
51
+ test: "bun run scripts/run-tests.ts",
51
52
  "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",
52
53
  "test:graphql": "SUPACLOUD_LITE_TEST_GRAPHQL=1 bun test test/graphql-native.test.ts --timeout 180000",
53
54
  benchmark: "bun run scripts/benchmark.ts",
@@ -67,31 +68,31 @@ var package_default = {
67
68
  "@electric-sql/pglite-pgmq": "0.0.8",
68
69
  "@electric-sql/pglite-pgvector": "0.0.11",
69
70
  "@electric-sql/pglite-postgis": "0.2.10",
70
- "@supacloud/db": "0.20.0",
71
+ "@supacloud/db": "0.20.1",
71
72
  "exact-mirror": "1.2.6",
72
73
  tar: "^7.5.22",
73
- typebox: "1.3.37"
74
+ typebox: "1.4.2"
74
75
  },
75
76
  devDependencies: {
76
- "@supacloud/app": "0.31.0",
77
- "@supacloud/contracts": "0.13.0",
78
- "@supacloud/compiler": "0.50.0",
79
- "@supacloud/elysia": "0.35.1",
77
+ "@supacloud/app": "0.33.0",
78
+ "@supacloud/contracts": "0.15.1",
79
+ "@supacloud/compiler": "0.51.1",
80
+ "@supacloud/elysia": "0.36.1",
80
81
  "@supabase/supabase-js": "^2.118.0",
81
82
  "@types/bun": "^1.4.3",
82
83
  effect: "4.0.5",
83
- elysia: "2.0.0-beta.32",
84
- jose: "^6.2.12",
84
+ elysia: "2.0.0-rc.0",
85
+ jose: "^6.2.13",
85
86
  typescript: "^7.0.2"
86
87
  },
87
88
  overrides: {
88
- "@supacloud/db": "0.20.0",
89
- "@supacloud/app": "0.31.0",
90
- "@supacloud/compiler": "0.50.0",
91
- "@supacloud/elysia": "0.35.1",
92
- "@supacloud/contracts": "0.13.0",
93
- "@supacloud/commands": "0.11.0",
94
- "@supacloud/delivery": "0.21.0",
89
+ "@supacloud/db": "0.20.1",
90
+ "@supacloud/app": "0.33.0",
91
+ "@supacloud/compiler": "0.51.1",
92
+ "@supacloud/elysia": "0.36.1",
93
+ "@supacloud/contracts": "0.15.1",
94
+ "@supacloud/commands": "0.11.1",
95
+ "@supacloud/delivery": "0.21.1",
95
96
  "@graphql-tools/utils": "^12.0.4"
96
97
  },
97
98
  engines: {
@@ -245,7 +246,7 @@ function isAllowedRedirect(requested, siteUrl, allowList = []) {
245
246
  var encoder2 = new TextEncoder;
246
247
  var decoder2 = new TextDecoder;
247
248
  var strictDecoder = new TextDecoder("utf-8", { fatal: true });
248
- var MAX_INT32 = 2 ** 32;
249
+ var UINT32_RANGE = 2 ** 32;
249
250
  function concat(...buffers) {
250
251
  const size = buffers.reduce((acc, { length }) => acc + length, 0), buf = new Uint8Array(size);
251
252
  let i = 0;
@@ -404,23 +405,23 @@ function isDisjoint(...headers) {
404
405
  function decodeBase64url(value, label, ErrorClass) {
405
406
  try {
406
407
  return decode(value);
407
- } catch {
408
- throw new ErrorClass(`Failed to base64url decode the ${label}`);
408
+ } catch (cause) {
409
+ throw new ErrorClass(`Failed to base64url decode the ${label}`, { cause });
409
410
  }
410
411
  }
411
- function encodeBase64url(value, label, ErrorClass) {
412
+ function encodeAsciiMember(value, label, ErrorClass) {
412
413
  try {
413
414
  return encode(value);
414
- } catch {
415
- throw new ErrorClass(`The ${label} is not a valid base64url string`);
415
+ } catch (cause) {
416
+ throw new ErrorClass(`The ${label} is not a valid base64url string`, { cause });
416
417
  }
417
418
  }
418
- function parseJoseHeader(b64, ErrorClass, message) {
419
+ function parseJoseHeader(encodedProtectedHeader, ErrorClass, message) {
419
420
  let parsed;
420
421
  try {
421
- parsed = JSON.parse(strictDecoder.decode(decode(b64)));
422
- } catch {
423
- throw new ErrorClass(message);
422
+ parsed = JSON.parse(strictDecoder.decode(decode(encodedProtectedHeader)));
423
+ } catch (cause) {
424
+ throw new ErrorClass(message, { cause });
424
425
  }
425
426
  if (!isObject(parsed))
426
427
  throw new ErrorClass(message);
@@ -478,15 +479,15 @@ var jwkMatchesOp = (entry, key, usage) => {
478
479
  }
479
480
  };
480
481
  async function prepareKey(entry, key, usage) {
481
- const { alg, secret } = entry, privateKey = usage === "decrypt" || usage === "sign";
482
+ const { alg, secret } = entry, requiresPrivateKey = usage === "decrypt" || usage === "sign";
482
483
  if (secret && key instanceof Uint8Array)
483
484
  return key;
484
485
  let normalized, keyObject;
485
486
  if (isObject(key)) {
486
487
  if (normalized = normalizeJwk(key), typeof normalized.kty != "string")
487
488
  throw invalidKeyType(alg, key, secret);
488
- 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)))
489
- 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`);
489
+ 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)))
490
+ 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`);
490
491
  if (jwkMatchesOp(entry, normalized, usage), normalized.kty === "oct")
491
492
  return decode(normalized.k);
492
493
  if (!Object.isFrozen(key)) {
@@ -496,7 +497,7 @@ async function prepareKey(entry, key, usage) {
496
497
  } else {
497
498
  if (!isKeyLike(key))
498
499
  throw invalidKeyType(alg, key, secret);
499
- const expectedType = secret ? "secret" : privateKey ? "private" : "public";
500
+ const expectedType = secret ? "secret" : requiresPrivateKey ? "private" : "public";
500
501
  if (key.type !== expectedType && (secret || ["secret", "public", "private"].includes(key.type)))
501
502
  throw new TypeError(`${tag(key)} instances must be of type "${expectedType}" for the ${alg} algorithm`);
502
503
  if (isCryptoKey(key))
@@ -635,8 +636,8 @@ function eddsa() {
635
636
  usages: sig
636
637
  };
637
638
  }
638
- function mldsa(bits) {
639
- const subtle = { name: `ML-DSA-${bits}` };
639
+ function mldsa(parameterSet) {
640
+ const subtle = { name: `ML-DSA-${parameterSet}` };
640
641
  return {
641
642
  kty: ["AKP"],
642
643
  subtle,
@@ -674,21 +675,23 @@ function jwsAlgorithm(alg) {
674
675
  function prepareVerify(options) {
675
676
  return [options && validateAlgorithms("algorithms", options.algorithms), options?.crit];
676
677
  }
677
- function parseProtectedHeader(encodedProtected) {
678
- return encodedProtected === undefined ? {} : parseJoseHeader(encodedProtected, JWSInvalid, "JWS Protected Header is invalid");
678
+ function parseProtectedHeader(encodedProtectedHeader) {
679
+ return encodedProtectedHeader === undefined ? {} : parseJoseHeader(encodedProtectedHeader, JWSInvalid, "JWS Protected Header is invalid");
679
680
  }
680
681
  function encodeCompactUnencodedPayload(payload) {
681
682
  try {
682
683
  return encode(payload);
683
- } catch {
684
- throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters");
684
+ } catch (cause) {
685
+ throw new JWSInvalid("JWS Compact Serialization payload must use only ASCII characters", {
686
+ cause
687
+ });
685
688
  }
686
689
  }
687
690
  async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedProtected) {
688
- const { protected: encodedProtected, header, payload: inputPayload } = jws, parsedProt = parsedProtected ?? parseProtectedHeader(encodedProtected);
689
- if (!isDisjoint(parsedProt, header))
691
+ const { protected: encodedProtectedHeader, header, payload: inputPayload } = jws, protectedHeader = parsedProtected ?? parseProtectedHeader(encodedProtectedHeader);
692
+ if (!isDisjoint(protectedHeader, header))
690
693
  throw new JWSInvalid("JWS Protected and JWS Unprotected Header Parameter names must be disjoint");
691
- const joseHeader = { ...parsedProt, ...header }, b64 = validateB64(parsedProt, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], parsedProt, joseHeader)), { alg } = joseHeader;
694
+ const joseHeader = { ...protectedHeader, ...header }, b64 = validateB64(protectedHeader, validateCrit(JWSInvalid, JWS_RECOGNIZED, shared[1], protectedHeader, joseHeader)), { alg } = joseHeader;
692
695
  if (typeof alg != "string" || !alg)
693
696
  throw new JWSInvalid('JWS "alg" (Algorithm) Header Parameter missing or invalid');
694
697
  if (shared[0] && !shared[0].has(alg))
@@ -700,17 +703,19 @@ async function verifySignature(jws, shared, key, encodeUnencodedPayload, parsedP
700
703
  throw new JWSInvalid("JWS Payload must be a string or an Uint8Array instance");
701
704
  const signingPayload = b64 || typeof inputPayload != "string" ? inputPayload : encodeUnencodedPayload(inputPayload);
702
705
  let resolvedKey = false;
703
- typeof key == "function" && (key = await key(parsedProt, jws), resolvedKey = true);
704
- 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");
706
+ typeof key == "function" && (key = await key(protectedHeader, jws), resolvedKey = true);
707
+ 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");
705
708
  entry.minRsaBits && checkModulusLength(entry.alg, cryptoKey);
706
709
  let verified = false;
707
710
  try {
708
- verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, data);
709
- } catch {}
711
+ verified = await crypto.subtle.verify(entry.signing, cryptoKey, signature, signingInput);
712
+ } catch (cause) {
713
+ throw new JWSSignatureVerificationFailed(undefined, { cause });
714
+ }
710
715
  if (!verified)
711
716
  throw new JWSSignatureVerificationFailed;
712
717
  const result = { payload: typeof signingPayload == "string" ? decodeBase64url(signingPayload, "payload", JWSInvalid) : signingPayload };
713
- return encodedProtected !== undefined && (result.protectedHeader = parsedProt), header !== undefined && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: k }, b64] : [result, b64];
718
+ return encodedProtectedHeader !== undefined && (result.protectedHeader = protectedHeader), header !== undefined && (result.unprotectedHeader = header), resolvedKey ? [{ ...result, key: verificationKey }, b64] : [result, b64];
714
719
  }
715
720
  async function verifyCompact(jws, shared, key) {
716
721
  if (jws instanceof Uint8Array && (jws = decoder2.decode(jws)), typeof jws != "string")
@@ -740,8 +745,8 @@ function secs(str) {
740
745
  typeof str != "string" && invalidDuration();
741
746
  const matched = REGEX.exec(str);
742
747
  (!matched || matched[4] && matched[1]) && invalidDuration();
743
- const value = parseFloat(matched[2]), numericDate2 = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
744
- return Number.isFinite(numericDate2) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -numericDate2 : numericDate2;
748
+ const value = parseFloat(matched[2]), durationSeconds = Math.round(value * multipliers[matched[3][0].toLowerCase()]);
749
+ return Number.isFinite(durationSeconds) || invalidDuration(), matched[1] === "-" || matched[4] === "ago" ? -durationSeconds : durationSeconds;
745
750
  }
746
751
  function validateInput(label, input) {
747
752
  if (!Number.isFinite(input))
@@ -753,33 +758,35 @@ var normalizeTyp = (value) => {
753
758
  return value.includes("/") ? normalized : `application/${normalized}`;
754
759
  };
755
760
  var checkAudiencePresence = (audPayload, audOption) => typeof audPayload == "string" ? audOption.includes(audPayload) : Array.isArray(audPayload) ? audOption.some((aud) => audPayload.includes(aud)) : false;
756
- function validateNumericDate(payload, claim, required = false) {
757
- const value = payload[claim];
761
+ function validateNumericDate(claimsSet, claim, required = false) {
762
+ const value = claimsSet[claim];
758
763
  if (!(value === undefined && !required)) {
759
764
  if (typeof value != "number")
760
- throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, payload, claim, "invalid");
765
+ throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, claimsSet, claim, "invalid");
761
766
  return value;
762
767
  }
763
768
  }
764
- function unexpectedClaim(payload, claim) {
765
- throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, payload, claim, checkFailed);
769
+ function unexpectedClaim(claimsSet, claim) {
770
+ throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, claimsSet, claim, checkFailed);
766
771
  }
767
- function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
768
- let payload;
772
+ function validateClaimsSet(protectedHeader, claimsSetBytes, options = {}) {
773
+ let claimsSet;
769
774
  try {
770
- payload = JSON.parse(strictDecoder.decode(encodedPayload));
771
- } catch {}
772
- if (!isObject(payload))
775
+ claimsSet = JSON.parse(strictDecoder.decode(claimsSetBytes));
776
+ } catch (cause) {
777
+ throw new JWTInvalid("JWT Claims Set must be a top-level JSON object", { cause });
778
+ }
779
+ if (!isObject(claimsSet))
773
780
  throw new JWTInvalid("JWT Claims Set must be a top-level JSON object");
774
781
  const { typ } = options;
775
782
  if (typ !== undefined && (typeof protectedHeader.typ != "string" || normalizeTyp(protectedHeader.typ) !== normalizeTyp(typ)))
776
- throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', payload, "typ", checkFailed);
783
+ throw new JWTClaimValidationFailed('unexpected "typ" JWT header value', claimsSet, "typ", checkFailed);
777
784
  const { requiredClaims = [], issuer, subject, audience, maxTokenAge } = options, presenceCheck = [...requiredClaims];
778
785
  maxTokenAge !== undefined && presenceCheck.push("iat"), audience !== undefined && presenceCheck.push("aud"), subject !== undefined && presenceCheck.push("sub"), issuer !== undefined && presenceCheck.push("iss");
779
786
  for (const claim of new Set(presenceCheck.reverse()))
780
- if (!Object.hasOwn(payload, claim))
781
- throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, payload, claim, "missing");
782
- 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");
787
+ if (!Object.hasOwn(claimsSet, claim))
788
+ throw new JWTClaimValidationFailed(`missing required "${claim}" claim`, claimsSet, claim, "missing");
789
+ 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");
783
790
  const { clockTolerance } = options;
784
791
  let tolerance = 0;
785
792
  if (typeof clockTolerance == "string")
@@ -790,20 +797,20 @@ function validateClaimsSet(protectedHeader, encodedPayload, options = {}) {
790
797
  tolerance = clockTolerance;
791
798
  }
792
799
  validateInput("clockTolerance option", tolerance);
793
- const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === undefined ? /* @__PURE__ */ new Date : currentDate)), iat = validateNumericDate(payload, "iat", maxTokenAge !== undefined), nbf = validateNumericDate(payload, "nbf");
800
+ const { currentDate } = options, now = validateInput("currentDate option", epoch(currentDate === undefined ? /* @__PURE__ */ new Date : currentDate)), iat = validateNumericDate(claimsSet, "iat", maxTokenAge !== undefined), nbf = validateNumericDate(claimsSet, "nbf");
794
801
  if (nbf !== undefined && nbf > now + tolerance)
795
- throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', payload, "nbf", checkFailed);
796
- const exp = validateNumericDate(payload, "exp");
802
+ throw new JWTClaimValidationFailed('"nbf" claim timestamp check failed', claimsSet, "nbf", checkFailed);
803
+ const exp = validateNumericDate(claimsSet, "exp");
797
804
  if (exp !== undefined && exp <= now - tolerance)
798
- throw new JWTExpired('"exp" claim timestamp check failed', payload, "exp", checkFailed);
805
+ throw new JWTExpired('"exp" claim timestamp check failed', claimsSet, "exp", checkFailed);
799
806
  if (maxTokenAge !== undefined) {
800
807
  const age = now - iat, max = validateInput("maxTokenAge option", typeof maxTokenAge == "number" ? maxTokenAge : secs(maxTokenAge));
801
808
  if (age - tolerance > max)
802
- throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', payload, "iat", checkFailed);
809
+ throw new JWTExpired('"iat" claim timestamp check failed (too far in the past)', claimsSet, "iat", checkFailed);
803
810
  if (age < -tolerance)
804
- throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', payload, "iat", checkFailed);
811
+ throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', claimsSet, "iat", checkFailed);
805
812
  }
806
- return payload;
813
+ return claimsSet;
807
814
  }
808
815
 
809
816
  // node_modules/jose/dist/webapi/jwt/verify.js
@@ -838,7 +845,9 @@ function createLocalJWKSet(jwks) {
838
845
  let snapshot;
839
846
  try {
840
847
  snapshot = structuredClone(jwks);
841
- } catch {}
848
+ } catch (cause) {
849
+ throw new JWKSInvalid("JSON Web Key Set malformed", { cause });
850
+ }
842
851
  if (!isJwkSet(snapshot))
843
852
  throw new JWKSInvalid("JSON Web Key Set malformed");
844
853
  const metadata = snapshot.keys.map((jwk) => {
@@ -872,7 +881,7 @@ function isCloudflareWorkers() {
872
881
  return typeof WebSocketPair < "u" || typeof navigator < "u" && navigator.userAgent === "Cloudflare-Workers" || typeof EdgeRuntime < "u" && EdgeRuntime === "vercel";
873
882
  }
874
883
  var USER_AGENT;
875
- (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.12");
884
+ (typeof navigator > "u" || !navigator.userAgent?.startsWith?.("Mozilla/5.0 ")) && (USER_AGENT = "jose/v6.2.13");
876
885
  var customFetch = /* @__PURE__ */ Symbol();
877
886
  async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
878
887
  const response = await fetchImpl(url, {
@@ -881,14 +890,14 @@ async function fetchJwks(url, headers, signal, fetchImpl = fetch) {
881
890
  redirect: "manual",
882
891
  headers
883
892
  }).catch((err) => {
884
- throw err.name === "TimeoutError" ? new JWKSTimeout : err;
893
+ throw err.name === "TimeoutError" ? new JWKSTimeout(undefined, { cause: err }) : err;
885
894
  });
886
895
  if (response.status !== 200)
887
896
  throw new JOSEError("Expected 200 OK from the JSON Web Key Set HTTP response");
888
897
  try {
889
898
  return await response.json();
890
- } catch {
891
- throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON");
899
+ } catch (cause) {
900
+ throw new JOSEError("Failed to parse the JSON Web Key Set HTTP response as JSON", { cause });
892
901
  }
893
902
  }
894
903
  var jwksCache = /* @__PURE__ */ Symbol();
@@ -9522,10 +9531,21 @@ async function sha256Hex2(text) {
9522
9531
  }
9523
9532
 
9524
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
+
9525
9541
  class RealtimeEngine {
9526
9542
  db;
9527
9543
  jwtSecret;
9528
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;
9529
9549
  connections = new Set;
9530
9550
  bindingCounter = 1;
9531
9551
  phxRefCounter = 1;
@@ -9572,6 +9592,47 @@ class RealtimeEngine {
9572
9592
  return { read: false, write: false };
9573
9593
  }
9574
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
+ }
9575
9636
  dispatchDbBroadcast(msg) {
9576
9637
  const full = `realtime:${msg.topic}`;
9577
9638
  for (const conn of this.connections) {
@@ -9772,8 +9833,7 @@ class RealtimeEngine {
9772
9833
  private: isPrivate,
9773
9834
  canBroadcast
9774
9835
  };
9775
- if (this.resolveExternalToken)
9776
- this.leaveChannel(conn, msg.topic);
9836
+ this.leaveChannel(conn, msg.topic);
9777
9837
  conn.channels.set(msg.topic, channel);
9778
9838
  this.scheduleExternalExpiry(conn, channel);
9779
9839
  this.reply(conn, msg, "ok", {
@@ -9813,21 +9873,32 @@ class RealtimeEngine {
9813
9873
  if (channel.expiryTimer)
9814
9874
  clearTimeout(channel.expiryTimer);
9815
9875
  conn.channels.delete(topic);
9816
- const state = this.presence.get(topic);
9817
- if (state?.has(channel.presenceKey)) {
9818
- const metas = state.get(channel.presenceKey);
9819
- if (!metas)
9820
- return;
9876
+ this.removePresence(channel);
9877
+ }
9878
+ removePresence(channel) {
9879
+ const presenceRef = channel.presenceRef;
9880
+ if (!presenceRef)
9881
+ return;
9882
+ channel.presenceRef = undefined;
9883
+ const state = this.presence.get(channel.topic);
9884
+ const current = state?.get(channel.presenceKey);
9885
+ const leaving = current?.metas.find((meta) => meta.phx_ref === presenceRef);
9886
+ if (!state || !current || !leaving)
9887
+ return;
9888
+ const remaining = current.metas.filter((meta) => meta.phx_ref !== presenceRef);
9889
+ if (remaining.length > 0) {
9890
+ state.set(channel.presenceKey, { metas: remaining });
9891
+ } else {
9821
9892
  state.delete(channel.presenceKey);
9822
9893
  if (state.size === 0)
9823
- this.presence.delete(topic);
9824
- this.broadcastToTopic(topic, {
9825
- topic,
9826
- event: "presence_diff",
9827
- payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
9828
- ref: null
9829
- });
9894
+ this.presence.delete(channel.topic);
9830
9895
  }
9896
+ this.broadcastToTopic(channel.topic, {
9897
+ topic: channel.topic,
9898
+ event: "presence_diff",
9899
+ payload: { joins: {}, leaves: { [channel.presenceKey]: { metas: [leaving] } } },
9900
+ ref: null
9901
+ });
9831
9902
  }
9832
9903
  scheduleExternalExpiry(conn, channel) {
9833
9904
  if (channel.expiryTimer)
@@ -9907,23 +9978,30 @@ class RealtimeEngine {
9907
9978
  return;
9908
9979
  const event = String(msg.payload?.event ?? "").toLowerCase();
9909
9980
  if (event === "track") {
9910
- const meta = {
9911
- ...msg.payload?.payload ?? {},
9912
- phx_ref: `F${this.phxRefCounter++}`
9913
- };
9981
+ const payload = { ...msg.payload?.payload ?? {} };
9982
+ delete payload.phx_ref;
9983
+ delete payload.phx_ref_prev;
9914
9984
  let state = this.presence.get(msg.topic);
9915
9985
  if (!state) {
9916
9986
  state = new Map;
9917
9987
  this.presence.set(msg.topic, state);
9918
9988
  }
9919
- const previous = state.get(channel.presenceKey);
9920
- state.set(channel.presenceKey, { metas: [meta] });
9989
+ const current = state.get(channel.presenceKey);
9990
+ const previous = current?.metas.find((meta) => meta.phx_ref === channel.presenceRef);
9991
+ const meta = {
9992
+ ...payload,
9993
+ phx_ref: `F${this.phxRefCounter++}`,
9994
+ ...previous ? { phx_ref_prev: previous.phx_ref } : {}
9995
+ };
9996
+ const others = current?.metas.filter((meta) => meta.phx_ref !== channel.presenceRef) ?? [];
9997
+ state.set(channel.presenceKey, { metas: [...others, meta] });
9998
+ channel.presenceRef = meta.phx_ref;
9921
9999
  this.broadcastToTopic(msg.topic, {
9922
10000
  topic: msg.topic,
9923
10001
  event: "presence_diff",
9924
10002
  payload: {
9925
10003
  joins: { [channel.presenceKey]: { metas: [meta] } },
9926
- leaves: previous ? { [channel.presenceKey]: previous } : {}
10004
+ leaves: previous ? { [channel.presenceKey]: { metas: [previous] } } : {}
9927
10005
  },
9928
10006
  ref: null
9929
10007
  });
@@ -9932,19 +10010,7 @@ class RealtimeEngine {
9932
10010
  return;
9933
10011
  }
9934
10012
  if (event === "untrack") {
9935
- const state = this.presence.get(msg.topic);
9936
- const metas = state?.get(channel.presenceKey);
9937
- if (state && metas) {
9938
- state.delete(channel.presenceKey);
9939
- if (state.size === 0)
9940
- this.presence.delete(msg.topic);
9941
- this.broadcastToTopic(msg.topic, {
9942
- topic: msg.topic,
9943
- event: "presence_diff",
9944
- payload: { joins: {}, leaves: { [channel.presenceKey]: metas } },
9945
- ref: null
9946
- });
9947
- }
10013
+ this.removePresence(channel);
9948
10014
  if (msg.ref)
9949
10015
  this.reply(conn, msg, "ok", {});
9950
10016
  }
@@ -11929,6 +11995,11 @@ class StorageHandler {
11929
11995
  const rest = url.pathname.replace(/^\/storage\/v1\/?/, "").replace(/\/+$/, "");
11930
11996
  const method = req.method.toUpperCase();
11931
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);
11932
12003
  if (rest === "bucket" && method === "POST")
11933
12004
  return await this.createBucket(req, ctx);
11934
12005
  if (rest === "bucket" && method === "GET")
@@ -12049,6 +12120,37 @@ class StorageHandler {
12049
12120
  release();
12050
12121
  }
12051
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
+ }
12052
12154
  requireService(ctx) {
12053
12155
  if (ctx.role !== "service_role") {
12054
12156
  return storageError(403, "Unauthorized", "Bucket management requires the service_role key");
@@ -14416,6 +14518,9 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
14416
14518
  if (smsInbox && (path === "/sms-inbox" || path.startsWith("/sms-inbox/"))) {
14417
14519
  return withCors(smsInbox.serve(req, url));
14418
14520
  }
14521
+ if (path === "/storage/v1/status" && req.method === "GET") {
14522
+ return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
14523
+ }
14419
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/")) {
14420
14525
  if (req.method === "GET" || req.method === "HEAD") {
14421
14526
  return withCors(await storage.handle(req, { role: "anon", claims: null }, url));
@@ -14451,6 +14556,50 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
14451
14556
  const ctx = await resolveContext(req, url);
14452
14557
  if (ctx instanceof Response)
14453
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
+ }
14454
14603
  if (path.startsWith("/rest/v1"))
14455
14604
  return withCors(await rest.handle(req, ctx, url));
14456
14605
  if (path === "/graphql/v1" || path === "/graphql/v1/")
@@ -16889,6 +17038,81 @@ function sameStrings(left, right) {
16889
17038
  return left.length === right.length && left.every((value, index) => value === right[index]);
16890
17039
  }
16891
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
+
16892
17116
  // src/runtime/node/config-toml.ts
16893
17117
  import { readFileSync as readFileSync2 } from "fs";
16894
17118
  import { join as join3 } from "path";
@@ -17477,7 +17701,7 @@ function isNotFound(error) {
17477
17701
 
17478
17702
  // src/project-runtime.ts
17479
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";
17480
- 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";
17481
17705
  import { pathToFileURL as pathToFileURL3 } from "url";
17482
17706
 
17483
17707
  // src/runtime/node/bun-server.ts
@@ -17499,7 +17723,11 @@ async function serveBun(backend, opts = {}) {
17499
17723
  const ip = srv.requestIP(req)?.address;
17500
17724
  if (ip)
17501
17725
  headers.set("x-supacloud-lite-remote-addr", ip);
17502
- 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);
17503
17731
  },
17504
17732
  websocket: {
17505
17733
  open(ws) {
@@ -17529,7 +17757,7 @@ async function serveBun(backend, opts = {}) {
17529
17757
 
17530
17758
  // src/runtime/node/fs-driver.ts
17531
17759
  import { mkdir as mkdir3, readFile as readFile4, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
17532
- 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";
17533
17761
 
17534
17762
  class FsStorageDriver {
17535
17763
  root;
@@ -17539,8 +17767,8 @@ class FsStorageDriver {
17539
17767
  }
17540
17768
  resolve(key) {
17541
17769
  const path = join5(this.root, key);
17542
- const relativePath = relative(this.root, path);
17543
- if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute(relativePath)) {
17770
+ const relativePath = relative2(this.root, path);
17771
+ if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) || isAbsolute2(relativePath)) {
17544
17772
  throw new Error(`invalid storage key: ${key}`);
17545
17773
  }
17546
17774
  return path;
@@ -17917,11 +18145,124 @@ class S3StorageDriver {
17917
18145
  }
17918
18146
  }
17919
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
+
17920
18261
  // src/project-runtime.ts
17921
18262
  var RESET_INITIALIZATION_ERROR = 'db reset requires initialized state; run "supacloud-lite migrate" first';
17922
18263
  var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets marker; restore the state before retrying";
17923
18264
  function resolveProjectPaths(options = {}) {
17924
- const projectDir = resolve4(options.projectDir ?? process.cwd());
18265
+ const projectDir = resolve5(options.projectDir ?? process.cwd());
17925
18266
  const stateDir = resolvePath2(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
17926
18267
  const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
17927
18268
  const dataDir = options.memory ? undefined : resolvePath2(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
@@ -17936,7 +18277,7 @@ function resolveProjectPaths(options = {}) {
17936
18277
  };
17937
18278
  }
17938
18279
  async function assertResetPathsSafe(paths) {
17939
- const stateDir = resolve4(paths.stateDir);
18280
+ const stateDir = resolve5(paths.stateDir);
17940
18281
  if (stateDir === parse(stateDir).root)
17941
18282
  throw new Error("refusing to use the filesystem root as the state directory");
17942
18283
  const stateInfo = await requiredResetEntry(stateDir);
@@ -17944,7 +18285,7 @@ async function assertResetPathsSafe(paths) {
17944
18285
  throw new Error(`refusing to reset through an invalid state directory: ${stateDir}`);
17945
18286
  }
17946
18287
  const canonicalStateDir = await realpath2(stateDir);
17947
- const secretsFile = resolve4(paths.secretsFile);
18288
+ const secretsFile = resolve5(paths.secretsFile);
17948
18289
  if (secretsFile !== join8(stateDir, "secrets.json")) {
17949
18290
  throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
17950
18291
  }
@@ -17958,9 +18299,9 @@ async function assertResetPathsSafe(paths) {
17958
18299
  ["storage", paths.storageDir]
17959
18300
  ];
17960
18301
  for (const [label, targetPath] of targets) {
17961
- const target = resolve4(targetPath);
17962
- const relativePath = relative2(stateDir, target);
17963
- if (!relativePath || relativePath.startsWith("..") || isAbsolute2(relativePath)) {
18302
+ const target = resolve5(targetPath);
18303
+ const relativePath = relative3(stateDir, target);
18304
+ if (!relativePath || relativePath.startsWith("..") || isAbsolute3(relativePath)) {
17964
18305
  throw new Error(`refusing to reset ${label} path outside the state directory: ${target}`);
17965
18306
  }
17966
18307
  await assertResetTargetCanonical(stateDir, canonicalStateDir, target, label);
@@ -18007,9 +18348,9 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
18007
18348
  current = parent;
18008
18349
  }
18009
18350
  const existingAncestor = await nearestExistingAncestor(target);
18010
- const canonicalTarget = resolve4(await realpath2(existingAncestor), relative2(existingAncestor, target));
18011
- const canonicalRelative = relative2(canonicalStateDir, canonicalTarget);
18012
- 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)) {
18013
18354
  throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target}`);
18014
18355
  }
18015
18356
  }
@@ -18185,7 +18526,7 @@ async function loadProjectBindings(projectDir, config) {
18185
18526
  const bindings = config.lite.migrationBindings;
18186
18527
  if (!bindings)
18187
18528
  return;
18188
- const manifest = JSON.parse(await readFile7(resolve4(projectDir, bindings.manifest), "utf8"));
18529
+ const manifest = JSON.parse(await readFile7(resolve5(projectDir, bindings.manifest), "utf8"));
18189
18530
  return {
18190
18531
  manifest,
18191
18532
  target: { environment: bindings.environment, projectRef: bindings.projectRef },
@@ -18195,7 +18536,7 @@ async function loadProjectBindings(projectDir, config) {
18195
18536
  async function loadProjectIdentity(projectDir, module) {
18196
18537
  if (!module)
18197
18538
  return;
18198
- const imported = await import(pathToFileURL3(resolve4(projectDir, module)).href);
18539
+ const imported = await import(pathToFileURL3(resolve5(projectDir, module)).href);
18199
18540
  if (!isRecord(imported) || typeof imported["default"] !== "function") {
18200
18541
  throw new Error("Lite identity module must default-export an external identity verifier");
18201
18542
  }
@@ -18261,7 +18602,11 @@ async function startProjectServer(options = {}) {
18261
18602
  const resolvedOptions = options.port === 0 ? { ...options, port: await findEphemeralPort(options.host) } : options;
18262
18603
  const project = await createProjectBackend({ ...resolvedOptions, startRuntimeServices: true });
18263
18604
  try {
18264
- 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 });
18265
18610
  let closePromise = null;
18266
18611
  return {
18267
18612
  ...project,
@@ -18303,7 +18648,7 @@ async function loadWebhooks(projectDir) {
18303
18648
  }
18304
18649
  }
18305
18650
  function resolvePath2(projectDir, path) {
18306
- return isAbsolute2(path) ? path : resolve4(projectDir, path);
18651
+ return isAbsolute3(path) ? path : resolve5(projectDir, path);
18307
18652
  }
18308
18653
  function randomHex(bytes) {
18309
18654
  const value = crypto.getRandomValues(new Uint8Array(bytes));
@@ -18349,7 +18694,7 @@ async function findEphemeralPort(host = "127.0.0.1") {
18349
18694
 
18350
18695
  // src/snapshot.ts
18351
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";
18352
- 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";
18353
18698
  import { create as createTar, extract as extractTar2 } from "tar";
18354
18699
  var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
18355
18700
  var SNAPSHOT_VERSION = 1;
@@ -18373,7 +18718,7 @@ async function createSnapshot(options) {
18373
18718
  postgresMajor: await readPostgresMajor(paths.dataDir)
18374
18719
  } : {}
18375
18720
  };
18376
- const output = resolve5(options.output);
18721
+ const output = resolve6(options.output);
18377
18722
  if (await existingInfo(output))
18378
18723
  throw new Error(`snapshot output already exists: ${output}`);
18379
18724
  await mkdir6(dirname7(output), { recursive: true });
@@ -18414,7 +18759,7 @@ async function restoreSnapshot(options) {
18414
18759
  await mkdir6(payloadRoot, { recursive: true });
18415
18760
  await extractTar2({
18416
18761
  cwd: payloadRoot,
18417
- file: resolve5(options.input),
18762
+ file: resolve6(options.input),
18418
18763
  preserveOwner: false,
18419
18764
  preservePaths: false,
18420
18765
  strict: true,
@@ -18457,10 +18802,10 @@ async function restoreSnapshot(options) {
18457
18802
  await mkdir6(stateStage, { recursive: true });
18458
18803
  await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
18459
18804
  if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
18460
- 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)));
18461
18806
  }
18462
18807
  if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
18463
- 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)));
18464
18809
  }
18465
18810
  const swaps = [];
18466
18811
  try {
@@ -18494,11 +18839,11 @@ async function restoreSnapshot(options) {
18494
18839
  function normalizePaths(paths) {
18495
18840
  return {
18496
18841
  ...paths,
18497
- projectDir: resolve5(paths.projectDir),
18498
- stateDir: resolve5(paths.stateDir),
18499
- dataDir: paths.dataDir ? resolve5(paths.dataDir) : undefined,
18500
- storageDir: resolve5(paths.storageDir),
18501
- 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)
18502
18847
  };
18503
18848
  }
18504
18849
  async function assertSnapshotPaths(paths, options = {}) {
@@ -18528,7 +18873,7 @@ async function assertSnapshotPaths(paths, options = {}) {
18528
18873
  async function assertDirectoryOrMissing(path) {
18529
18874
  if (!path)
18530
18875
  return;
18531
- if (resolve5(path) === parse2(resolve5(path)).root)
18876
+ if (resolve6(path) === parse2(resolve6(path)).root)
18532
18877
  throw new Error(`snapshot path must not be the filesystem root: ${path}`);
18533
18878
  try {
18534
18879
  const info = await lstat3(path);
@@ -18751,13 +19096,13 @@ async function assertNoSymlinks(root) {
18751
19096
  }
18752
19097
  }
18753
19098
  function isWithin(parent, child) {
18754
- const normalizedParent = resolve5(parent);
18755
- const normalizedChild = resolve5(child);
19099
+ const normalizedParent = resolve6(parent);
19100
+ const normalizedChild = resolve6(child);
18756
19101
  return normalizedChild !== normalizedParent && normalizedChild.startsWith(`${normalizedParent}${sep2}`);
18757
19102
  }
18758
19103
  function pathsOverlap(left, right) {
18759
- const normalizedLeft = resolve5(left);
18760
- const normalizedRight = resolve5(right);
19104
+ const normalizedLeft = resolve6(left);
19105
+ const normalizedRight = resolve6(right);
18761
19106
  return normalizedLeft === normalizedRight || isWithin(normalizedLeft, normalizedRight) || isWithin(normalizedRight, normalizedLeft);
18762
19107
  }
18763
19108
 
@@ -18870,7 +19215,8 @@ function parseArgs(argv) {
18870
19215
  port: process.env.SUPACLOUD_LITE_PORT || process.env.PORT ? Number.parseInt(process.env.SUPACLOUD_LITE_PORT ?? process.env.PORT, 10) : 54321,
18871
19216
  serviceRole: false,
18872
19217
  force: false,
18873
- json: false
19218
+ json: false,
19219
+ production: false
18874
19220
  };
18875
19221
  for (let index = 0;index < args.length; index++) {
18876
19222
  const argument = args[index];
@@ -18889,13 +19235,13 @@ function parseArgs(argv) {
18889
19235
  else if (argument === "--site-url")
18890
19236
  options.siteUrl = next();
18891
19237
  else if (argument === "--project-dir" || argument === "--dir")
18892
- options.projectDir = resolve6(next());
19238
+ options.projectDir = resolve7(next());
18893
19239
  else if (argument === "--state-dir")
18894
- options.stateDir = resolve6(next());
19240
+ options.stateDir = resolve7(next());
18895
19241
  else if (argument === "--data-dir")
18896
- options.dataDir = resolve6(next());
19242
+ options.dataDir = resolve7(next());
18897
19243
  else if (argument === "--storage-dir")
18898
- options.storageDir = resolve6(next());
19244
+ options.storageDir = resolve7(next());
18899
19245
  else if (argument === "--storage-backend")
18900
19246
  options.storageBackend = next();
18901
19247
  else if (argument === "--s3-prefix")
@@ -18905,9 +19251,13 @@ function parseArgs(argv) {
18905
19251
  else if (argument === "--runtime-mode")
18906
19252
  options.runtimeMode = runtimeMode(next());
18907
19253
  else if (argument === "--postgres-dir")
18908
- options.postgresDir = resolve6(next());
19254
+ options.postgresDir = resolve7(next());
18909
19255
  else if (argument === "--identity-module")
18910
- 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());
18911
19261
  else if (argument === "--replication-profile") {
18912
19262
  options.replicationProfile = next();
18913
19263
  } else if (argument === "--replication-host")
@@ -18919,25 +19269,27 @@ function parseArgs(argv) {
18919
19269
  else if (argument === "--powersync-tables")
18920
19270
  options.powersyncPublicationTables = commaSeparated2(next());
18921
19271
  else if (argument === "--replication-tls-cert")
18922
- options.replicationTlsCertFile = resolve6(next());
19272
+ options.replicationTlsCertFile = resolve7(next());
18923
19273
  else if (argument === "--replication-tls-key")
18924
- options.replicationTlsKeyFile = resolve6(next());
19274
+ options.replicationTlsKeyFile = resolve7(next());
18925
19275
  else if (argument === "--memory")
18926
19276
  options.memory = true;
18927
19277
  else if (argument === "--output" || argument === "-o")
18928
- options.output = resolve6(next());
19278
+ options.output = resolve7(next());
18929
19279
  else if (argument === "--file" || argument === "-f")
18930
19280
  options.diffFile = next();
18931
19281
  else if (argument === "--baseline")
18932
19282
  options.baseline = true;
18933
19283
  else if (argument === "--module-file")
18934
- options.moduleFile = resolve6(next());
19284
+ options.moduleFile = resolve7(next());
18935
19285
  else if (argument === "--service-role")
18936
19286
  options.serviceRole = true;
18937
19287
  else if (argument === "--force")
18938
19288
  options.force = true;
18939
19289
  else if (argument === "--json")
18940
19290
  options.json = true;
19291
+ else if (argument === "--production")
19292
+ options.production = true;
18941
19293
  else if (argument === "--email")
18942
19294
  options.email = next();
18943
19295
  else if (argument === "--password-stdin")
@@ -19085,6 +19437,23 @@ ${privilegedKey}
19085
19437
  }
19086
19438
  if (config.lite.graphql.enabled === false)
19087
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
+ }
19088
19457
  if (options.json)
19089
19458
  await writeStandardOutput(`${JSON.stringify(report, null, 2)}
19090
19459
  `);
@@ -19094,6 +19463,8 @@ ${privilegedKey}
19094
19463
  process.exitCode = 1;
19095
19464
  if (report.powersync_readiness?.ready === false)
19096
19465
  process.exitCode = 1;
19466
+ if (report.production_readiness?.ready === false)
19467
+ process.exitCode = 1;
19097
19468
  return;
19098
19469
  }
19099
19470
  if (options.command === "migrate" || options.command === "status") {
@@ -19181,7 +19552,7 @@ async function runDbCommand(options) {
19181
19552
  }
19182
19553
  return;
19183
19554
  }
19184
- const projectDir = resolve6(options.projectDir ?? process.cwd());
19555
+ const projectDir = resolve7(options.projectDir ?? process.cwd());
19185
19556
  const config = loadProjectConfig(projectDir, process.env, options.runtimeMode);
19186
19557
  const project = await loadSupabaseProject(projectDir, {
19187
19558
  enabled: false,
@@ -19302,7 +19673,7 @@ async function runSnapshotCommand(options) {
19302
19673
  const rollbackLines = result.rollbackPaths.map((rollbackPath) => `Previous state retained at ${rollbackPath}`);
19303
19674
  const reconnectLine = result.manifest.storageBackend === "s3" ? ["Reconnect the original S3 bucket/prefix before starting Lite."] : [];
19304
19675
  await writeStandardOutput([
19305
- `Snapshot restored from ${resolve6(input)}`,
19676
+ `Snapshot restored from ${resolve7(input)}`,
19306
19677
  ...rollbackLines,
19307
19678
  ...reconnectLine
19308
19679
  ].join(`
@@ -19345,6 +19716,21 @@ async function runUpgradeCommand(options) {
19345
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)}`);
19346
19717
  }
19347
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
+ }
19348
19734
  async function printInspection(rows) {
19349
19735
  if (rows.length === 0) {
19350
19736
  await writeStandardOutput(`No tables in schema "public".
@@ -19374,6 +19760,13 @@ async function printDoctor(report) {
19374
19760
  for (const warning of report.powersync_readiness.warnings)
19375
19761
  output.push(`warning: ${warning}`);
19376
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
+ }
19377
19770
  await writeStandardOutput(`${output.join(`
19378
19771
  `)}
19379
19772
  `);
@@ -19444,6 +19837,10 @@ Options:
19444
19837
  --runtime-mode development (default) or strict
19445
19838
  --postgres-dir operator-managed PostgreSQL installation (native only)
19446
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)
19447
19844
  -o, --output <p> output file for gen types
19448
19845
  -f, --file <name> migration suffix for db diff
19449
19846
  --baseline db pull: write and record already-present schema as a migration