@dexto/tools-builtins 1.15.0 → 1.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/implementations/http-request-tool.cjs +47 -8
- package/dist/implementations/http-request-tool.d.ts.map +1 -1
- package/dist/implementations/http-request-tool.js +47 -8
- package/dist/implementations/http-request-tool.test.cjs +77 -0
- package/dist/implementations/http-request-tool.test.js +77 -0
- package/package.json +3 -3
|
@@ -52,6 +52,7 @@ function safeJsonParse(text) {
|
|
|
52
52
|
}
|
|
53
53
|
const BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost"]);
|
|
54
54
|
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
55
|
+
const MAX_REDIRECTS = 20;
|
|
55
56
|
const SAFE_DISPATCHER = createSafeDispatcher();
|
|
56
57
|
function isPrivateIpv4(ip) {
|
|
57
58
|
const parts = ip.split(".").map((part) => Number(part));
|
|
@@ -82,20 +83,26 @@ function isPrivateIpv4(ip) {
|
|
|
82
83
|
return false;
|
|
83
84
|
}
|
|
84
85
|
function isPrivateIpv6(ip) {
|
|
85
|
-
const normalized = ip.
|
|
86
|
+
const normalized = new URL(`http://[${ip.split("%")[0]}]/`).hostname.slice(1, -1);
|
|
86
87
|
if (normalized === "::" || normalized === "::1") return true;
|
|
87
|
-
if (normalized.startsWith("
|
|
88
|
+
if (normalized.startsWith("64:ff9b:1:")) return true;
|
|
89
|
+
if (/^fe[89ab][0-9a-f]:/.test(normalized)) return true;
|
|
88
90
|
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
|
|
89
91
|
if (normalized.startsWith("::ffff:")) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
+
const parts = normalized.slice("::ffff:".length).split(":");
|
|
93
|
+
if (parts.length === 2) {
|
|
94
|
+
const high = Number.parseInt(parts[0] ?? "", 16);
|
|
95
|
+
const low = Number.parseInt(parts[1] ?? "", 16);
|
|
96
|
+
return isPrivateIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
97
|
+
}
|
|
92
98
|
}
|
|
93
99
|
return false;
|
|
94
100
|
}
|
|
95
101
|
function isPrivateAddress(ip) {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
if (version ===
|
|
102
|
+
const address = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
|
|
103
|
+
const version = (0, import_node_net.isIP)(address);
|
|
104
|
+
if (version === 4) return isPrivateIpv4(address);
|
|
105
|
+
if (version === 6) return isPrivateIpv6(address);
|
|
99
106
|
return false;
|
|
100
107
|
}
|
|
101
108
|
function createSafeLookup(config) {
|
|
@@ -331,7 +338,39 @@ function createHttpRequestTool() {
|
|
|
331
338
|
if (requestBody !== void 0) {
|
|
332
339
|
requestInit.body = requestBody;
|
|
333
340
|
}
|
|
334
|
-
|
|
341
|
+
requestInit.redirect = "manual";
|
|
342
|
+
let currentUrl = requestUrl;
|
|
343
|
+
let response;
|
|
344
|
+
for (let redirects = 0; ; redirects++) {
|
|
345
|
+
await assertSafeUrl(currentUrl);
|
|
346
|
+
response = await fetch(currentUrl.toString(), requestInit);
|
|
347
|
+
const location = response.headers.get("location");
|
|
348
|
+
if (![301, 302, 303, 307, 308].includes(response.status) || !location) break;
|
|
349
|
+
await response.body?.cancel();
|
|
350
|
+
if (redirects >= MAX_REDIRECTS) {
|
|
351
|
+
throw new import_errors.DextoRuntimeError(
|
|
352
|
+
"HTTP_REQUEST_TOO_MANY_REDIRECTS",
|
|
353
|
+
import_errors.ErrorScope.TOOLS,
|
|
354
|
+
import_errors.ErrorType.THIRD_PARTY,
|
|
355
|
+
`HTTP request exceeded ${MAX_REDIRECTS} redirects`
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
const nextUrl = new URL(location, currentUrl);
|
|
359
|
+
if (nextUrl.origin !== currentUrl.origin) {
|
|
360
|
+
for (const key of Object.keys(requestHeaders)) {
|
|
361
|
+
delete requestHeaders[key];
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
if (response.status === 303 || (response.status === 301 || response.status === 302) && requestInit.method === "POST") {
|
|
365
|
+
requestInit.method = "GET";
|
|
366
|
+
delete requestInit.body;
|
|
367
|
+
for (const key of Object.keys(requestHeaders)) {
|
|
368
|
+
if (["content-type", "content-length"].includes(key.toLowerCase()))
|
|
369
|
+
delete requestHeaders[key];
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
currentUrl = nextUrl;
|
|
373
|
+
}
|
|
335
374
|
const responseText = await readResponseTextWithLimit(response);
|
|
336
375
|
const contentType = response.headers.get("content-type");
|
|
337
376
|
const json = isJsonContentType(contentType) ? safeJsonParse(responseText) : void 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"http-request-tool.d.ts","sourceRoot":"","sources":["../../src/implementations/http-request-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,mBAAmB,CAAC;AAQpE,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAKnF,QAAA,MAAM,sBAAsB;;;;;;;;;;;;;kBA2Bf,CAAC;
|
|
1
|
+
{"version":3,"file":"http-request-tool.d.ts","sourceRoot":"","sources":["../../src/implementations/http-request-tool.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,IAAI,EAAwB,MAAM,mBAAmB,CAAC;AAQpE,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,KAAK,aAAa,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAKnF,QAAA,MAAM,sBAAsB;;;;;;;;;;;;;kBA2Bf,CAAC;AA+Bd,KAAK,cAAc,GACb,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC,GAC9E,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,cAAc,GAAG,IAAI,EAAE,SAAS,EAAE,aAAa,EAAE,KAAK,IAAI,CAAC,CAAC;AA4DhF,wBAAgB,gBAAgB,CAAC,MAAM,CAAC,EAAE;IACtC,SAAS,CAAC,EAAE,OAAO,GAAG,CAAC,MAAM,CAAC;CACjC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,QAAQ,EAAE,cAAc,KAAK,IAAI,CAyI/E;AA2FD;;GAEG;AACH,wBAAgB,qBAAqB,IAAI,IAAI,CAAC,OAAO,sBAAsB,CAAC,CA4I3E"}
|
|
@@ -33,6 +33,7 @@ function safeJsonParse(text) {
|
|
|
33
33
|
}
|
|
34
34
|
const BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost"]);
|
|
35
35
|
const MAX_RESPONSE_BYTES = 5 * 1024 * 1024;
|
|
36
|
+
const MAX_REDIRECTS = 20;
|
|
36
37
|
const SAFE_DISPATCHER = createSafeDispatcher();
|
|
37
38
|
function isPrivateIpv4(ip) {
|
|
38
39
|
const parts = ip.split(".").map((part) => Number(part));
|
|
@@ -63,20 +64,26 @@ function isPrivateIpv4(ip) {
|
|
|
63
64
|
return false;
|
|
64
65
|
}
|
|
65
66
|
function isPrivateIpv6(ip) {
|
|
66
|
-
const normalized = ip.
|
|
67
|
+
const normalized = new URL(`http://[${ip.split("%")[0]}]/`).hostname.slice(1, -1);
|
|
67
68
|
if (normalized === "::" || normalized === "::1") return true;
|
|
68
|
-
if (normalized.startsWith("
|
|
69
|
+
if (normalized.startsWith("64:ff9b:1:")) return true;
|
|
70
|
+
if (/^fe[89ab][0-9a-f]:/.test(normalized)) return true;
|
|
69
71
|
if (normalized.startsWith("fc") || normalized.startsWith("fd")) return true;
|
|
70
72
|
if (normalized.startsWith("::ffff:")) {
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
+
const parts = normalized.slice("::ffff:".length).split(":");
|
|
74
|
+
if (parts.length === 2) {
|
|
75
|
+
const high = Number.parseInt(parts[0] ?? "", 16);
|
|
76
|
+
const low = Number.parseInt(parts[1] ?? "", 16);
|
|
77
|
+
return isPrivateIpv4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
78
|
+
}
|
|
73
79
|
}
|
|
74
80
|
return false;
|
|
75
81
|
}
|
|
76
82
|
function isPrivateAddress(ip) {
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
if (version ===
|
|
83
|
+
const address = ip.startsWith("[") && ip.endsWith("]") ? ip.slice(1, -1) : ip;
|
|
84
|
+
const version = isIP(address);
|
|
85
|
+
if (version === 4) return isPrivateIpv4(address);
|
|
86
|
+
if (version === 6) return isPrivateIpv6(address);
|
|
80
87
|
return false;
|
|
81
88
|
}
|
|
82
89
|
function createSafeLookup(config) {
|
|
@@ -312,7 +319,39 @@ function createHttpRequestTool() {
|
|
|
312
319
|
if (requestBody !== void 0) {
|
|
313
320
|
requestInit.body = requestBody;
|
|
314
321
|
}
|
|
315
|
-
|
|
322
|
+
requestInit.redirect = "manual";
|
|
323
|
+
let currentUrl = requestUrl;
|
|
324
|
+
let response;
|
|
325
|
+
for (let redirects = 0; ; redirects++) {
|
|
326
|
+
await assertSafeUrl(currentUrl);
|
|
327
|
+
response = await fetch(currentUrl.toString(), requestInit);
|
|
328
|
+
const location = response.headers.get("location");
|
|
329
|
+
if (![301, 302, 303, 307, 308].includes(response.status) || !location) break;
|
|
330
|
+
await response.body?.cancel();
|
|
331
|
+
if (redirects >= MAX_REDIRECTS) {
|
|
332
|
+
throw new DextoRuntimeError(
|
|
333
|
+
"HTTP_REQUEST_TOO_MANY_REDIRECTS",
|
|
334
|
+
ErrorScope.TOOLS,
|
|
335
|
+
ErrorType.THIRD_PARTY,
|
|
336
|
+
`HTTP request exceeded ${MAX_REDIRECTS} redirects`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
const nextUrl = new URL(location, currentUrl);
|
|
340
|
+
if (nextUrl.origin !== currentUrl.origin) {
|
|
341
|
+
for (const key of Object.keys(requestHeaders)) {
|
|
342
|
+
delete requestHeaders[key];
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
if (response.status === 303 || (response.status === 301 || response.status === 302) && requestInit.method === "POST") {
|
|
346
|
+
requestInit.method = "GET";
|
|
347
|
+
delete requestInit.body;
|
|
348
|
+
for (const key of Object.keys(requestHeaders)) {
|
|
349
|
+
if (["content-type", "content-length"].includes(key.toLowerCase()))
|
|
350
|
+
delete requestHeaders[key];
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
currentUrl = nextUrl;
|
|
354
|
+
}
|
|
316
355
|
const responseText = await readResponseTextWithLimit(response);
|
|
317
356
|
const contentType = response.headers.get("content-type");
|
|
318
357
|
const json = isJsonContentType(contentType) ? safeJsonParse(responseText) : void 0;
|
|
@@ -47,4 +47,81 @@ var import_http_request_tool = require("./http-request-tool.js");
|
|
|
47
47
|
code: "HTTP_REQUEST_UNSAFE_TARGET"
|
|
48
48
|
});
|
|
49
49
|
});
|
|
50
|
+
import_vitest.it.each([
|
|
51
|
+
"[::1]",
|
|
52
|
+
"[0:0:0:0:0:0:0:1]",
|
|
53
|
+
"[::ffff:127.0.0.1]",
|
|
54
|
+
"[::ffff:10.0.0.1]",
|
|
55
|
+
"[64:ff9b:1::a00:1]"
|
|
56
|
+
])("blocks private IPv6 literal %s before fetching", async (host) => {
|
|
57
|
+
const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch");
|
|
58
|
+
try {
|
|
59
|
+
await (0, import_vitest.expect)(
|
|
60
|
+
(0, import_http_request_tool.createHttpRequestTool)().execute(
|
|
61
|
+
{ url: `http://${host}/`, method: "GET", timeoutMs: 1e3 },
|
|
62
|
+
{}
|
|
63
|
+
)
|
|
64
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_UNSAFE_TARGET" });
|
|
65
|
+
(0, import_vitest.expect)(fetchSpy).not.toHaveBeenCalled();
|
|
66
|
+
} finally {
|
|
67
|
+
fetchSpy.mockRestore();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
(0, import_vitest.it)("blocks a redirect to a private literal before the next request", async () => {
|
|
71
|
+
const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
72
|
+
new Response(null, { status: 302, headers: { location: "http://[::1]/secret" } })
|
|
73
|
+
);
|
|
74
|
+
try {
|
|
75
|
+
await (0, import_vitest.expect)(
|
|
76
|
+
(0, import_http_request_tool.createHttpRequestTool)().execute(
|
|
77
|
+
{ url: "https://example.com/", method: "GET", timeoutMs: 1e3 },
|
|
78
|
+
{}
|
|
79
|
+
)
|
|
80
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_UNSAFE_TARGET" });
|
|
81
|
+
(0, import_vitest.expect)(fetchSpy).toHaveBeenCalledTimes(1);
|
|
82
|
+
(0, import_vitest.expect)(fetchSpy.mock.calls[0]?.[1]).toMatchObject({ redirect: "manual" });
|
|
83
|
+
} finally {
|
|
84
|
+
fetchSpy.mockRestore();
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
(0, import_vitest.it)("follows relative redirects and limits redirect loops", async () => {
|
|
88
|
+
const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
89
|
+
async () => new Response(null, { status: 302, headers: { location: "/next" } })
|
|
90
|
+
);
|
|
91
|
+
try {
|
|
92
|
+
await (0, import_vitest.expect)(
|
|
93
|
+
(0, import_http_request_tool.createHttpRequestTool)().execute(
|
|
94
|
+
{ url: "https://example.com/", method: "GET", timeoutMs: 1e3 },
|
|
95
|
+
{}
|
|
96
|
+
)
|
|
97
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_TOO_MANY_REDIRECTS" });
|
|
98
|
+
(0, import_vitest.expect)(fetchSpy).toHaveBeenCalledTimes(21);
|
|
99
|
+
(0, import_vitest.expect)(fetchSpy.mock.calls[1]?.[0]).toBe("https://example.com/next");
|
|
100
|
+
} finally {
|
|
101
|
+
fetchSpy.mockRestore();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
(0, import_vitest.it)("drops credentials when following a cross-origin redirect", async () => {
|
|
105
|
+
const fetchSpy = import_vitest.vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
|
|
106
|
+
new Response(null, { status: 302, headers: { location: "https://other.example/" } })
|
|
107
|
+
).mockResolvedValueOnce(new Response("ok"));
|
|
108
|
+
try {
|
|
109
|
+
await (0, import_http_request_tool.createHttpRequestTool)().execute(
|
|
110
|
+
{
|
|
111
|
+
url: "https://example.com/",
|
|
112
|
+
method: "GET",
|
|
113
|
+
headers: {
|
|
114
|
+
Authorization: "secret",
|
|
115
|
+
Cookie: "session=value",
|
|
116
|
+
"X-API-Key": "secret"
|
|
117
|
+
},
|
|
118
|
+
timeoutMs: 1e3
|
|
119
|
+
},
|
|
120
|
+
{}
|
|
121
|
+
);
|
|
122
|
+
(0, import_vitest.expect)(fetchSpy.mock.calls[1]?.[1]?.headers).toEqual({});
|
|
123
|
+
} finally {
|
|
124
|
+
fetchSpy.mockRestore();
|
|
125
|
+
}
|
|
126
|
+
});
|
|
50
127
|
});
|
|
@@ -46,4 +46,81 @@ describe("http_request tool", () => {
|
|
|
46
46
|
code: "HTTP_REQUEST_UNSAFE_TARGET"
|
|
47
47
|
});
|
|
48
48
|
});
|
|
49
|
+
it.each([
|
|
50
|
+
"[::1]",
|
|
51
|
+
"[0:0:0:0:0:0:0:1]",
|
|
52
|
+
"[::ffff:127.0.0.1]",
|
|
53
|
+
"[::ffff:10.0.0.1]",
|
|
54
|
+
"[64:ff9b:1::a00:1]"
|
|
55
|
+
])("blocks private IPv6 literal %s before fetching", async (host) => {
|
|
56
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch");
|
|
57
|
+
try {
|
|
58
|
+
await expect(
|
|
59
|
+
createHttpRequestTool().execute(
|
|
60
|
+
{ url: `http://${host}/`, method: "GET", timeoutMs: 1e3 },
|
|
61
|
+
{}
|
|
62
|
+
)
|
|
63
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_UNSAFE_TARGET" });
|
|
64
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
65
|
+
} finally {
|
|
66
|
+
fetchSpy.mockRestore();
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
it("blocks a redirect to a private literal before the next request", async () => {
|
|
70
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValue(
|
|
71
|
+
new Response(null, { status: 302, headers: { location: "http://[::1]/secret" } })
|
|
72
|
+
);
|
|
73
|
+
try {
|
|
74
|
+
await expect(
|
|
75
|
+
createHttpRequestTool().execute(
|
|
76
|
+
{ url: "https://example.com/", method: "GET", timeoutMs: 1e3 },
|
|
77
|
+
{}
|
|
78
|
+
)
|
|
79
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_UNSAFE_TARGET" });
|
|
80
|
+
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
81
|
+
expect(fetchSpy.mock.calls[0]?.[1]).toMatchObject({ redirect: "manual" });
|
|
82
|
+
} finally {
|
|
83
|
+
fetchSpy.mockRestore();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
it("follows relative redirects and limits redirect loops", async () => {
|
|
87
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(
|
|
88
|
+
async () => new Response(null, { status: 302, headers: { location: "/next" } })
|
|
89
|
+
);
|
|
90
|
+
try {
|
|
91
|
+
await expect(
|
|
92
|
+
createHttpRequestTool().execute(
|
|
93
|
+
{ url: "https://example.com/", method: "GET", timeoutMs: 1e3 },
|
|
94
|
+
{}
|
|
95
|
+
)
|
|
96
|
+
).rejects.toMatchObject({ code: "HTTP_REQUEST_TOO_MANY_REDIRECTS" });
|
|
97
|
+
expect(fetchSpy).toHaveBeenCalledTimes(21);
|
|
98
|
+
expect(fetchSpy.mock.calls[1]?.[0]).toBe("https://example.com/next");
|
|
99
|
+
} finally {
|
|
100
|
+
fetchSpy.mockRestore();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
it("drops credentials when following a cross-origin redirect", async () => {
|
|
104
|
+
const fetchSpy = vi.spyOn(globalThis, "fetch").mockResolvedValueOnce(
|
|
105
|
+
new Response(null, { status: 302, headers: { location: "https://other.example/" } })
|
|
106
|
+
).mockResolvedValueOnce(new Response("ok"));
|
|
107
|
+
try {
|
|
108
|
+
await createHttpRequestTool().execute(
|
|
109
|
+
{
|
|
110
|
+
url: "https://example.com/",
|
|
111
|
+
method: "GET",
|
|
112
|
+
headers: {
|
|
113
|
+
Authorization: "secret",
|
|
114
|
+
Cookie: "session=value",
|
|
115
|
+
"X-API-Key": "secret"
|
|
116
|
+
},
|
|
117
|
+
timeoutMs: 1e3
|
|
118
|
+
},
|
|
119
|
+
{}
|
|
120
|
+
);
|
|
121
|
+
expect(fetchSpy.mock.calls[1]?.[1]?.headers).toEqual({});
|
|
122
|
+
} finally {
|
|
123
|
+
fetchSpy.mockRestore();
|
|
124
|
+
}
|
|
125
|
+
});
|
|
49
126
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dexto/tools-builtins",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"description": "Built-in tools factory for Dexto agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"undici": "^7.30.1",
|
|
21
21
|
"zod": "^4.3.6",
|
|
22
|
-
"@dexto/agent-config": "1.
|
|
23
|
-
"@dexto/core": "1.
|
|
22
|
+
"@dexto/agent-config": "1.16.0",
|
|
23
|
+
"@dexto/core": "1.16.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"tsup": "^8.0.0",
|