@rstreamlabs/tunnels 3.7.0 → 3.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-K6TZT3PB.mjs → chunk-3VBF43NB.mjs} +1 -1
- package/dist/index.d.mts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +85 -14
- package/dist/index.mjs +85 -14
- package/dist/openapi.js +1 -1
- package/dist/openapi.mjs +1 -1
- package/package.json +5 -5
|
@@ -243,7 +243,7 @@ var routingPathSchema = z3.object({
|
|
|
243
243
|
crossRegionRoutingAllowed: z3.boolean(),
|
|
244
244
|
ingress: routingNodeSchema,
|
|
245
245
|
owner: routingNodeSchema,
|
|
246
|
-
|
|
246
|
+
anchor: routingNodeSchema
|
|
247
247
|
});
|
|
248
248
|
var routingSchema = z3.discriminatedUnion("decision", [
|
|
249
249
|
z3.object({
|
package/dist/index.d.mts
CHANGED
|
@@ -614,7 +614,7 @@ declare const streamSummaryEventSchema: z.ZodObject<{
|
|
|
614
614
|
region: z.ZodString;
|
|
615
615
|
generation: z.ZodOptional<z.ZodString>;
|
|
616
616
|
}, z.core.$strip>;
|
|
617
|
-
|
|
617
|
+
anchor: z.ZodObject<{
|
|
618
618
|
engineId: z.ZodString;
|
|
619
619
|
region: z.ZodString;
|
|
620
620
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -2747,7 +2747,7 @@ declare const projectLogEventsSchema: z.ZodObject<{
|
|
|
2747
2747
|
region: z.ZodString;
|
|
2748
2748
|
generation: z.ZodOptional<z.ZodString>;
|
|
2749
2749
|
}, z.core.$strip>;
|
|
2750
|
-
|
|
2750
|
+
anchor: z.ZodObject<{
|
|
2751
2751
|
engineId: z.ZodString;
|
|
2752
2752
|
region: z.ZodString;
|
|
2753
2753
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -4878,7 +4878,7 @@ declare const eventSchema: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodObject
|
|
|
4878
4878
|
region: z.ZodString;
|
|
4879
4879
|
generation: z.ZodOptional<z.ZodString>;
|
|
4880
4880
|
}, z.core.$strip>;
|
|
4881
|
-
|
|
4881
|
+
anchor: z.ZodObject<{
|
|
4882
4882
|
engineId: z.ZodString;
|
|
4883
4883
|
region: z.ZodString;
|
|
4884
4884
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -6174,7 +6174,7 @@ declare const streamSummarySchema: z.ZodObject<{
|
|
|
6174
6174
|
region: z.ZodString;
|
|
6175
6175
|
generation: z.ZodOptional<z.ZodString>;
|
|
6176
6176
|
}, z.core.$strip>;
|
|
6177
|
-
|
|
6177
|
+
anchor: z.ZodObject<{
|
|
6178
6178
|
engineId: z.ZodString;
|
|
6179
6179
|
region: z.ZodString;
|
|
6180
6180
|
generation: z.ZodOptional<z.ZodString>;
|
package/dist/index.d.ts
CHANGED
|
@@ -614,7 +614,7 @@ declare const streamSummaryEventSchema: z.ZodObject<{
|
|
|
614
614
|
region: z.ZodString;
|
|
615
615
|
generation: z.ZodOptional<z.ZodString>;
|
|
616
616
|
}, z.core.$strip>;
|
|
617
|
-
|
|
617
|
+
anchor: z.ZodObject<{
|
|
618
618
|
engineId: z.ZodString;
|
|
619
619
|
region: z.ZodString;
|
|
620
620
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -2747,7 +2747,7 @@ declare const projectLogEventsSchema: z.ZodObject<{
|
|
|
2747
2747
|
region: z.ZodString;
|
|
2748
2748
|
generation: z.ZodOptional<z.ZodString>;
|
|
2749
2749
|
}, z.core.$strip>;
|
|
2750
|
-
|
|
2750
|
+
anchor: z.ZodObject<{
|
|
2751
2751
|
engineId: z.ZodString;
|
|
2752
2752
|
region: z.ZodString;
|
|
2753
2753
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -4878,7 +4878,7 @@ declare const eventSchema: z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodObject
|
|
|
4878
4878
|
region: z.ZodString;
|
|
4879
4879
|
generation: z.ZodOptional<z.ZodString>;
|
|
4880
4880
|
}, z.core.$strip>;
|
|
4881
|
-
|
|
4881
|
+
anchor: z.ZodObject<{
|
|
4882
4882
|
engineId: z.ZodString;
|
|
4883
4883
|
region: z.ZodString;
|
|
4884
4884
|
generation: z.ZodOptional<z.ZodString>;
|
|
@@ -6174,7 +6174,7 @@ declare const streamSummarySchema: z.ZodObject<{
|
|
|
6174
6174
|
region: z.ZodString;
|
|
6175
6175
|
generation: z.ZodOptional<z.ZodString>;
|
|
6176
6176
|
}, z.core.$strip>;
|
|
6177
|
-
|
|
6177
|
+
anchor: z.ZodObject<{
|
|
6178
6178
|
engineId: z.ZodString;
|
|
6179
6179
|
region: z.ZodString;
|
|
6180
6180
|
generation: z.ZodOptional<z.ZodString>;
|
package/dist/index.js
CHANGED
|
@@ -541,7 +541,7 @@ var routingPathSchema = z3.object({
|
|
|
541
541
|
crossRegionRoutingAllowed: z3.boolean(),
|
|
542
542
|
ingress: routingNodeSchema,
|
|
543
543
|
owner: routingNodeSchema,
|
|
544
|
-
|
|
544
|
+
anchor: routingNodeSchema
|
|
545
545
|
});
|
|
546
546
|
var routingSchema = z3.discriminatedUnion("decision", [
|
|
547
547
|
z3.object({
|
|
@@ -1781,6 +1781,7 @@ var defaultTURNPort = 3478;
|
|
|
1781
1781
|
var defaultTURNSPort = 5349;
|
|
1782
1782
|
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
1783
1783
|
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
1784
|
+
var maxTURNKeyringBytes = 8 * 1024;
|
|
1784
1785
|
var turnTokenClaimsSchema = z8.object({
|
|
1785
1786
|
exp: z8.number().int().optional(),
|
|
1786
1787
|
iat: z8.number().int().optional(),
|
|
@@ -1909,6 +1910,75 @@ function resolveMode(credentials, mode) {
|
|
|
1909
1910
|
}
|
|
1910
1911
|
return "api";
|
|
1911
1912
|
}
|
|
1913
|
+
async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
1914
|
+
const result = await reader.read();
|
|
1915
|
+
if (result.done) {
|
|
1916
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
1917
|
+
}
|
|
1918
|
+
const nextSize = size + result.value.byteLength;
|
|
1919
|
+
if (nextSize > maxTURNKeyringBytes) {
|
|
1920
|
+
await reader.cancel();
|
|
1921
|
+
throw new Error("TURN keyring response exceeds the 8 KiB limit.");
|
|
1922
|
+
}
|
|
1923
|
+
return await readBoundedTURNKeyring(
|
|
1924
|
+
reader,
|
|
1925
|
+
[...chunks, result.value],
|
|
1926
|
+
nextSize
|
|
1927
|
+
);
|
|
1928
|
+
}
|
|
1929
|
+
function parseTURNServerPublicKeyHex(value) {
|
|
1930
|
+
const normalized = value.trim();
|
|
1931
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
|
|
1932
|
+
throw new Error("TURN keyring must contain hexadecimal DER key material.");
|
|
1933
|
+
}
|
|
1934
|
+
try {
|
|
1935
|
+
const key = import_crypto.default.createPublicKey({
|
|
1936
|
+
key: Buffer.from(normalized, "hex"),
|
|
1937
|
+
format: "der",
|
|
1938
|
+
type: "spki"
|
|
1939
|
+
});
|
|
1940
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
1941
|
+
throw new Error("TURN keyring public key must be an EC key.");
|
|
1942
|
+
}
|
|
1943
|
+
return key;
|
|
1944
|
+
} catch (error) {
|
|
1945
|
+
if (error instanceof Error && error.message.startsWith("TURN keyring")) {
|
|
1946
|
+
throw error;
|
|
1947
|
+
}
|
|
1948
|
+
throw new Error("TURN keyring contains an invalid DER public key.", {
|
|
1949
|
+
cause: error
|
|
1950
|
+
});
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function parseAPPPrivateKey(value) {
|
|
1954
|
+
try {
|
|
1955
|
+
const key = import_crypto.default.createPrivateKey({
|
|
1956
|
+
key: Buffer.from(value, "hex"),
|
|
1957
|
+
format: "der",
|
|
1958
|
+
type: "pkcs8"
|
|
1959
|
+
});
|
|
1960
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
1961
|
+
throw new Error("TURN APP client secret must contain an EC private key.");
|
|
1962
|
+
}
|
|
1963
|
+
return key;
|
|
1964
|
+
} catch (error) {
|
|
1965
|
+
if (error instanceof Error && error.message.startsWith("TURN APP")) {
|
|
1966
|
+
throw error;
|
|
1967
|
+
}
|
|
1968
|
+
throw new Error("TURN APP client secret contains an invalid private key.", {
|
|
1969
|
+
cause: error
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
function requireMatchingCurve(privateKey, publicKey) {
|
|
1974
|
+
const privateCurve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
1975
|
+
const publicCurve = publicKey.asymmetricKeyDetails?.namedCurve;
|
|
1976
|
+
if (!privateCurve || privateCurve !== publicCurve) {
|
|
1977
|
+
throw new Error(
|
|
1978
|
+
"TURN APP client and server keys must use the same named curve."
|
|
1979
|
+
);
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1912
1982
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
1913
1983
|
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
1914
1984
|
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
@@ -1916,11 +1986,19 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
1916
1986
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
1917
1987
|
keyringBaseURL
|
|
1918
1988
|
);
|
|
1919
|
-
const response = await fetch(url);
|
|
1989
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
1990
|
+
if (response.status >= 300 && response.status < 400) {
|
|
1991
|
+
throw new Error(
|
|
1992
|
+
"TURN keyring request was redirected; redirects are refused."
|
|
1993
|
+
);
|
|
1994
|
+
}
|
|
1920
1995
|
if (!response.ok) {
|
|
1921
|
-
throw new Error(
|
|
1996
|
+
throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
|
|
1997
|
+
}
|
|
1998
|
+
if (!response.body) {
|
|
1999
|
+
throw new Error("TURN keyring response body is missing.");
|
|
1922
2000
|
}
|
|
1923
|
-
return (await response.
|
|
2001
|
+
return (await readBoundedTURNKeyring(response.body.getReader())).trim();
|
|
1924
2002
|
}
|
|
1925
2003
|
async function createAPITURNCredentials(options) {
|
|
1926
2004
|
const projectId = normalizeOptionalString2(options.projectId);
|
|
@@ -2019,17 +2097,10 @@ async function createAPPTURNCredentials(options) {
|
|
|
2019
2097
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
2020
2098
|
const exp = normalizeNow(options.now) + ttlSeconds;
|
|
2021
2099
|
const username = `v1:${exp}:app:${projectEndpoint}:${clientId}`;
|
|
2022
|
-
const clientPrivateKey =
|
|
2023
|
-
key: Buffer.from(clientSecret, "hex"),
|
|
2024
|
-
format: "der",
|
|
2025
|
-
type: "pkcs8"
|
|
2026
|
-
});
|
|
2100
|
+
const clientPrivateKey = parseAPPPrivateKey(clientSecret);
|
|
2027
2101
|
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
2028
|
-
const serverPublicKey =
|
|
2029
|
-
|
|
2030
|
-
format: "der",
|
|
2031
|
-
type: "spki"
|
|
2032
|
-
});
|
|
2102
|
+
const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
|
|
2103
|
+
requireMatchingCurve(clientPrivateKey, serverPublicKey);
|
|
2033
2104
|
const sharedSecret = import_crypto.default.diffieHellman({
|
|
2034
2105
|
privateKey: clientPrivateKey,
|
|
2035
2106
|
publicKey: serverPublicKey
|
package/dist/index.mjs
CHANGED
|
@@ -48,7 +48,7 @@ import {
|
|
|
48
48
|
webTTYTransportSchema,
|
|
49
49
|
webhookEventsSchema,
|
|
50
50
|
wsEventsSchema
|
|
51
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-3VBF43NB.mjs";
|
|
52
52
|
|
|
53
53
|
// src/auth-resource.ts
|
|
54
54
|
import { createAuthTokenParamsSchema } from "@rstreamlabs/rstream/auth-token";
|
|
@@ -418,6 +418,7 @@ var defaultTURNPort = 3478;
|
|
|
418
418
|
var defaultTURNSPort = 5349;
|
|
419
419
|
var defaultTURNCredentialTTLSeconds = 10 * 60;
|
|
420
420
|
var maxTURNCredentialTTLSeconds = 60 * 60;
|
|
421
|
+
var maxTURNKeyringBytes = 8 * 1024;
|
|
421
422
|
var turnTokenClaimsSchema = z.object({
|
|
422
423
|
exp: z.number().int().optional(),
|
|
423
424
|
iat: z.number().int().optional(),
|
|
@@ -546,6 +547,75 @@ function resolveMode(credentials, mode) {
|
|
|
546
547
|
}
|
|
547
548
|
return "api";
|
|
548
549
|
}
|
|
550
|
+
async function readBoundedTURNKeyring(reader, chunks = [], size = 0) {
|
|
551
|
+
const result = await reader.read();
|
|
552
|
+
if (result.done) {
|
|
553
|
+
return Buffer.concat(chunks, size).toString("utf8");
|
|
554
|
+
}
|
|
555
|
+
const nextSize = size + result.value.byteLength;
|
|
556
|
+
if (nextSize > maxTURNKeyringBytes) {
|
|
557
|
+
await reader.cancel();
|
|
558
|
+
throw new Error("TURN keyring response exceeds the 8 KiB limit.");
|
|
559
|
+
}
|
|
560
|
+
return await readBoundedTURNKeyring(
|
|
561
|
+
reader,
|
|
562
|
+
[...chunks, result.value],
|
|
563
|
+
nextSize
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
function parseTURNServerPublicKeyHex(value) {
|
|
567
|
+
const normalized = value.trim();
|
|
568
|
+
if (normalized.length === 0 || normalized.length % 2 !== 0 || !/^[0-9a-f]+$/i.test(normalized)) {
|
|
569
|
+
throw new Error("TURN keyring must contain hexadecimal DER key material.");
|
|
570
|
+
}
|
|
571
|
+
try {
|
|
572
|
+
const key = crypto.createPublicKey({
|
|
573
|
+
key: Buffer.from(normalized, "hex"),
|
|
574
|
+
format: "der",
|
|
575
|
+
type: "spki"
|
|
576
|
+
});
|
|
577
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
578
|
+
throw new Error("TURN keyring public key must be an EC key.");
|
|
579
|
+
}
|
|
580
|
+
return key;
|
|
581
|
+
} catch (error) {
|
|
582
|
+
if (error instanceof Error && error.message.startsWith("TURN keyring")) {
|
|
583
|
+
throw error;
|
|
584
|
+
}
|
|
585
|
+
throw new Error("TURN keyring contains an invalid DER public key.", {
|
|
586
|
+
cause: error
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
function parseAPPPrivateKey(value) {
|
|
591
|
+
try {
|
|
592
|
+
const key = crypto.createPrivateKey({
|
|
593
|
+
key: Buffer.from(value, "hex"),
|
|
594
|
+
format: "der",
|
|
595
|
+
type: "pkcs8"
|
|
596
|
+
});
|
|
597
|
+
if (key.asymmetricKeyType !== "ec") {
|
|
598
|
+
throw new Error("TURN APP client secret must contain an EC private key.");
|
|
599
|
+
}
|
|
600
|
+
return key;
|
|
601
|
+
} catch (error) {
|
|
602
|
+
if (error instanceof Error && error.message.startsWith("TURN APP")) {
|
|
603
|
+
throw error;
|
|
604
|
+
}
|
|
605
|
+
throw new Error("TURN APP client secret contains an invalid private key.", {
|
|
606
|
+
cause: error
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
function requireMatchingCurve(privateKey, publicKey) {
|
|
611
|
+
const privateCurve = privateKey.asymmetricKeyDetails?.namedCurve;
|
|
612
|
+
const publicCurve = publicKey.asymmetricKeyDetails?.namedCurve;
|
|
613
|
+
if (!privateCurve || privateCurve !== publicCurve) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
"TURN APP client and server keys must use the same named curve."
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
549
619
|
async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
550
620
|
const configuredKey = normalizeOptionalString2(options.keyringBaseUrl);
|
|
551
621
|
const keyringBaseURL = configuredKey ?? normalizeOptionalString2(options.apiUrl) ?? "https://rstream.io";
|
|
@@ -553,11 +623,19 @@ async function loadTURNServerPublicKeyHex(clusterDomain, options) {
|
|
|
553
623
|
`/keyrings/turn/${clusterDomain}.spki.der.hex`,
|
|
554
624
|
keyringBaseURL
|
|
555
625
|
);
|
|
556
|
-
const response = await fetch(url);
|
|
626
|
+
const response = await fetch(url, { redirect: "manual" });
|
|
627
|
+
if (response.status >= 300 && response.status < 400) {
|
|
628
|
+
throw new Error(
|
|
629
|
+
"TURN keyring request was redirected; redirects are refused."
|
|
630
|
+
);
|
|
631
|
+
}
|
|
557
632
|
if (!response.ok) {
|
|
558
|
-
throw new Error(
|
|
633
|
+
throw new Error(`TURN keyring request returned HTTP ${response.status}.`);
|
|
634
|
+
}
|
|
635
|
+
if (!response.body) {
|
|
636
|
+
throw new Error("TURN keyring response body is missing.");
|
|
559
637
|
}
|
|
560
|
-
return (await response.
|
|
638
|
+
return (await readBoundedTURNKeyring(response.body.getReader())).trim();
|
|
561
639
|
}
|
|
562
640
|
async function createAPITURNCredentials(options) {
|
|
563
641
|
const projectId = normalizeOptionalString2(options.projectId);
|
|
@@ -656,17 +734,10 @@ async function createAPPTURNCredentials(options) {
|
|
|
656
734
|
const ttlSeconds = normalizeTTLSeconds(options.ttlSeconds);
|
|
657
735
|
const exp = normalizeNow(options.now) + ttlSeconds;
|
|
658
736
|
const username = `v1:${exp}:app:${projectEndpoint}:${clientId}`;
|
|
659
|
-
const clientPrivateKey =
|
|
660
|
-
key: Buffer.from(clientSecret, "hex"),
|
|
661
|
-
format: "der",
|
|
662
|
-
type: "pkcs8"
|
|
663
|
-
});
|
|
737
|
+
const clientPrivateKey = parseAPPPrivateKey(clientSecret);
|
|
664
738
|
const serverPublicKeyHex = normalizeOptionalString2(options.serverPublicKeyHex) ?? await loadTURNServerPublicKeyHex(clusterDomain, options);
|
|
665
|
-
const serverPublicKey =
|
|
666
|
-
|
|
667
|
-
format: "der",
|
|
668
|
-
type: "spki"
|
|
669
|
-
});
|
|
739
|
+
const serverPublicKey = parseTURNServerPublicKeyHex(serverPublicKeyHex);
|
|
740
|
+
requireMatchingCurve(clientPrivateKey, serverPublicKey);
|
|
670
741
|
const sharedSecret = crypto.diffieHellman({
|
|
671
742
|
privateKey: clientPrivateKey,
|
|
672
743
|
publicKey: serverPublicKey
|
package/dist/openapi.js
CHANGED
|
@@ -280,7 +280,7 @@ var routingPathSchema = z3.object({
|
|
|
280
280
|
crossRegionRoutingAllowed: z3.boolean(),
|
|
281
281
|
ingress: routingNodeSchema,
|
|
282
282
|
owner: routingNodeSchema,
|
|
283
|
-
|
|
283
|
+
anchor: routingNodeSchema
|
|
284
284
|
});
|
|
285
285
|
var routingSchema = z3.discriminatedUnion("decision", [
|
|
286
286
|
z3.object({
|
package/dist/openapi.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rstreamlabs/tunnels",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.1",
|
|
4
4
|
"description": "JS/TS SDK for the rstream Engine API and tunnel runtime APIs.",
|
|
5
5
|
"author": "@uartnet <hello@rstream.io>",
|
|
6
6
|
"repository": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"type-check": "tsc --noEmit"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@turbo/gen": "^2.10.
|
|
41
|
+
"@turbo/gen": "^2.10.6",
|
|
42
42
|
"@types/node": "^26",
|
|
43
|
-
"eslint": "9.39.
|
|
43
|
+
"eslint": "9.39.5",
|
|
44
44
|
"eslint-config": "*",
|
|
45
45
|
"tsup": "^8.5.1",
|
|
46
|
-
"typescript": "
|
|
46
|
+
"typescript": "6.0.3",
|
|
47
47
|
"typescript-config": "*"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@rstreamlabs/rstream": "3.
|
|
50
|
+
"@rstreamlabs/rstream": "3.8.0",
|
|
51
51
|
"@types/jsonwebtoken": "^9.0.10",
|
|
52
52
|
"jsonwebtoken": "^9.0.3",
|
|
53
53
|
"zod": "^4.4.3"
|