@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/CHANGELOG.md +27 -0
- package/README.md +85 -2
- package/deploy/README.md +110 -0
- package/deploy/caddy/Caddyfile +15 -0
- package/deploy/systemd/supacloud-lite.service +32 -0
- package/dist/cli.js +552 -155
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +474 -129
- package/dist/project-runtime.d.ts +2 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/auth/proxy.d.ts +28 -0
- package/dist/runtime/auth/proxy.d.ts.map +1 -0
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/node/bun-server.d.ts +2 -0
- package/dist/runtime/node/bun-server.d.ts.map +1 -1
- package/dist/runtime/node/native/readiness.d.ts +2 -0
- package/dist/runtime/node/native/readiness.d.ts.map +1 -1
- package/dist/runtime/node/production-readiness.d.ts +31 -0
- package/dist/runtime/node/production-readiness.d.ts.map +1 -0
- package/dist/runtime/realtime/engine.d.ts +23 -0
- package/dist/runtime/realtime/engine.d.ts.map +1 -1
- package/dist/runtime/storage/handler.d.ts +6 -0
- package/dist/runtime/storage/handler.d.ts.map +1 -1
- package/package.json +18 -17
package/dist/cli.js
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// @bun
|
|
3
3
|
|
|
4
4
|
// src/cli.ts
|
|
5
|
-
import { existsSync as existsSync5 } from "fs";
|
|
5
|
+
import { existsSync as existsSync5, readdirSync, statSync as statSync2 } from "fs";
|
|
6
6
|
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7
|
-
import { dirname as dirname8, join as join10, resolve as
|
|
7
|
+
import { dirname as dirname8, join as join10, resolve as resolve7 } from "path";
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.
|
|
11
|
+
version: "0.34.0",
|
|
12
12
|
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
@@ -28,6 +28,7 @@ var package_default = {
|
|
|
28
28
|
"dist",
|
|
29
29
|
"!dist/standalone",
|
|
30
30
|
"README.md",
|
|
31
|
+
"deploy",
|
|
31
32
|
"CHANGELOG.md",
|
|
32
33
|
"RELEASING.md",
|
|
33
34
|
"THIRD_PARTY_NOTICES.md",
|
|
@@ -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
|
|
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.
|
|
71
|
+
"@supacloud/db": "0.20.1",
|
|
71
72
|
"exact-mirror": "1.2.6",
|
|
72
73
|
tar: "^7.5.22",
|
|
73
|
-
typebox: "1.
|
|
74
|
+
typebox: "1.4.2"
|
|
74
75
|
},
|
|
75
76
|
devDependencies: {
|
|
76
|
-
"@supacloud/app": "0.
|
|
77
|
-
"@supacloud/contracts": "0.
|
|
78
|
-
"@supacloud/compiler": "0.
|
|
79
|
-
"@supacloud/elysia": "0.
|
|
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-
|
|
84
|
-
jose: "^6.2.
|
|
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.
|
|
89
|
-
"@supacloud/app": "0.
|
|
90
|
-
"@supacloud/compiler": "0.
|
|
91
|
-
"@supacloud/elysia": "0.
|
|
92
|
-
"@supacloud/contracts": "0.
|
|
93
|
-
"@supacloud/commands": "0.11.
|
|
94
|
-
"@supacloud/delivery": "0.21.
|
|
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
|
|
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
|
|
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(
|
|
419
|
+
function parseJoseHeader(encodedProtectedHeader, ErrorClass, message) {
|
|
419
420
|
let parsed;
|
|
420
421
|
try {
|
|
421
|
-
parsed = JSON.parse(strictDecoder.decode(decode(
|
|
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,
|
|
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" && (
|
|
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 ${
|
|
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" :
|
|
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(
|
|
639
|
-
const subtle = { name: `ML-DSA-${
|
|
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(
|
|
678
|
-
return
|
|
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:
|
|
689
|
-
if (!isDisjoint(
|
|
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 = { ...
|
|
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(
|
|
704
|
-
const entry = jwsAlgorithm(alg),
|
|
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,
|
|
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
|
|
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]),
|
|
744
|
-
return Number.isFinite(
|
|
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(
|
|
757
|
-
const value =
|
|
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`,
|
|
765
|
+
throw new JWTClaimValidationFailed(`"${claim}" claim must be a number`, claimsSet, claim, "invalid");
|
|
761
766
|
return value;
|
|
762
767
|
}
|
|
763
768
|
}
|
|
764
|
-
function unexpectedClaim(
|
|
765
|
-
throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`,
|
|
769
|
+
function unexpectedClaim(claimsSet, claim) {
|
|
770
|
+
throw new JWTClaimValidationFailed(`unexpected "${claim}" claim value`, claimsSet, claim, checkFailed);
|
|
766
771
|
}
|
|
767
|
-
function validateClaimsSet(protectedHeader,
|
|
768
|
-
let
|
|
772
|
+
function validateClaimsSet(protectedHeader, claimsSetBytes, options = {}) {
|
|
773
|
+
let claimsSet;
|
|
769
774
|
try {
|
|
770
|
-
|
|
771
|
-
} catch {
|
|
772
|
-
|
|
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',
|
|
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(
|
|
781
|
-
throw new JWTClaimValidationFailed(`missing required "${claim}" claim`,
|
|
782
|
-
issuer !== undefined && !(Array.isArray(issuer) ? issuer : [issuer]).includes(
|
|
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(
|
|
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',
|
|
796
|
-
const exp = validateNumericDate(
|
|
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',
|
|
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)',
|
|
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)',
|
|
811
|
+
throw new JWTClaimValidationFailed('"iat" claim timestamp check failed (it should be in the past)', claimsSet, "iat", checkFailed);
|
|
805
812
|
}
|
|
806
|
-
return
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
9817
|
-
|
|
9818
|
-
|
|
9819
|
-
|
|
9820
|
-
|
|
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
|
|
9911
|
-
|
|
9912
|
-
|
|
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
|
|
9920
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
17543
|
-
if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${sep}`) ||
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
17962
|
-
const relativePath =
|
|
17963
|
-
if (!relativePath || relativePath.startsWith("..") ||
|
|
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 =
|
|
18011
|
-
const canonicalRelative =
|
|
18012
|
-
if (!canonicalRelative || canonicalRelative.startsWith("..") ||
|
|
18351
|
+
const canonicalTarget = resolve5(await realpath2(existingAncestor), relative3(existingAncestor, target));
|
|
18352
|
+
const canonicalRelative = relative3(canonicalStateDir, canonicalTarget);
|
|
18353
|
+
if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute3(canonicalRelative)) {
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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:
|
|
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,
|
|
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,
|
|
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:
|
|
18498
|
-
stateDir:
|
|
18499
|
-
dataDir: paths.dataDir ?
|
|
18500
|
-
storageDir:
|
|
18501
|
-
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 (
|
|
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 =
|
|
18755
|
-
const normalizedChild =
|
|
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 =
|
|
18760
|
-
const normalizedRight =
|
|
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 =
|
|
19238
|
+
options.projectDir = resolve7(next());
|
|
18893
19239
|
else if (argument === "--state-dir")
|
|
18894
|
-
options.stateDir =
|
|
19240
|
+
options.stateDir = resolve7(next());
|
|
18895
19241
|
else if (argument === "--data-dir")
|
|
18896
|
-
options.dataDir =
|
|
19242
|
+
options.dataDir = resolve7(next());
|
|
18897
19243
|
else if (argument === "--storage-dir")
|
|
18898
|
-
options.storageDir =
|
|
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 =
|
|
19254
|
+
options.postgresDir = resolve7(next());
|
|
18909
19255
|
else if (argument === "--identity-module")
|
|
18910
|
-
options.identityModule =
|
|
19256
|
+
options.identityModule = resolve7(next());
|
|
19257
|
+
else if (argument === "--external-auth-proxy-issuer")
|
|
19258
|
+
options.externalAuthProxyIssuer = next();
|
|
19259
|
+
else if (argument === "--external-auth-proxy-redirect-origin")
|
|
19260
|
+
options.externalAuthProxyRedirectOrigins = commaSeparated2(next());
|
|
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 =
|
|
19272
|
+
options.replicationTlsCertFile = resolve7(next());
|
|
18923
19273
|
else if (argument === "--replication-tls-key")
|
|
18924
|
-
options.replicationTlsKeyFile =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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
|