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