Merge pull request #11 from nasyarobby/cursor/ftp-sftp-script-3038

feat(scripts): add remote-fs script for SFTP and FTP access
This commit is contained in:
2026-08-19 15:42:15 +07:00
committed by GitHub
4 changed files with 644 additions and 0 deletions
+2
View File
@@ -17,6 +17,7 @@
"@fastify/jwt": "^9.1.0",
"@fastify/static": "^8.2.0",
"axios": "^1.19.0",
"basic-ftp": "^6.2.0",
"bcryptjs": "^3.0.2",
"better-sqlite3": "^13.0.3",
"fastify": "^5.12.0",
@@ -28,6 +29,7 @@
"nodemailer": "^9.0.5",
"pino": "^10.3.1",
"pino-roll": "^4.0.0",
"ssh2-sftp-client": "^12.1.1",
"webdav": "^5.10.0",
"yaml": "^2.9.0"
}
+3
View File
@@ -18,11 +18,14 @@ const ALLOWED_MODULES = new Set([
"@aws-sdk/client-s3",
"@aws-sdk/s3-request-presigner",
"axios",
"basic-ftp",
"jsonata",
"mustache",
"node-html-parser",
"node:stream",
"nodemailer",
"rss-parser",
"ssh2-sftp-client",
"webdav",
]);
+521
View File
@@ -0,0 +1,521 @@
import SftpClient from "ssh2-sftp-client";
import { Client } from "basic-ftp";
import { Readable, Writable } from "node:stream";
const PROTOCOLS = new Set(["ftp", "sftp"]);
const ACTIONS = new Set(["list", "read", "write", "delete", "stat", "mkdir", "rename"]);
function passContext(ctx) {
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
return { ...ctx.context };
}
return {};
}
function mergeData(data) {
if (data != null && typeof data === "object" && !Array.isArray(data)) {
return { ...data };
}
return {};
}
function resolveProtocol(ctx) {
const raw = ctx.config?.protocol ?? ctx.data?.protocol ?? "sftp";
if (typeof raw !== "string" || raw.length === 0) {
throw new Error("remote-fs: protocol must be a non-empty string");
}
const protocol = raw.toLowerCase();
if (!PROTOCOLS.has(protocol)) {
throw new Error(`remote-fs: unsupported protocol "${raw}" (use ftp or sftp)`);
}
return protocol;
}
function resolveAction(ctx) {
const raw = ctx.config?.action ?? ctx.data?.action ?? "list";
if (typeof raw !== "string" || raw.length === 0) {
throw new Error("remote-fs: action must be a non-empty string");
}
const action = raw.toLowerCase();
if (!ACTIONS.has(action)) {
throw new Error(`remote-fs: unsupported action "${raw}"`);
}
return action;
}
function resolvePath(ctx, { required = false, label = "path" } = {}) {
const path = ctx.config?.path ?? ctx.data?.path;
if (path == null || path === "") {
if (required) throw new Error(`remote-fs: ${label} is required`);
return ".";
}
if (typeof path !== "string") {
throw new Error(`remote-fs: ${label} must be a string`);
}
return path;
}
function resolveHost(ctx) {
const host = ctx.config?.host ?? ctx.data?.host;
if (typeof host !== "string" || host.length === 0) {
throw new Error("remote-fs: host is required (ctx.config.host or ctx.data.host)");
}
return host;
}
function resolvePort(ctx, protocol) {
const raw = ctx.config?.port ?? ctx.data?.port;
if (raw == null || raw === "") {
return protocol === "sftp" ? 22 : 21;
}
const port = Number(raw);
if (!Number.isFinite(port) || port <= 0) {
throw new Error("remote-fs: port must be a positive number");
}
return port;
}
async function resolveSecretValue(secretName, label) {
if (typeof secretName !== "string" || secretName.length === 0) {
throw new Error(`remote-fs: ${label} is required`);
}
return $secrets.reveal(await $secrets.get(secretName));
}
async function resolveAuth(ctx) {
const username =
typeof ctx.config?.usernameSecret === "string" && ctx.config.usernameSecret.length > 0
? await resolveSecretValue(ctx.config.usernameSecret, "usernameSecret")
: (ctx.config?.username ?? ctx.data?.username);
if (typeof username !== "string" || username.length === 0) {
throw new Error("remote-fs: username is required");
}
let password;
if (typeof ctx.config?.passwordSecret === "string" && ctx.config.passwordSecret.length > 0) {
password = await resolveSecretValue(ctx.config.passwordSecret, "passwordSecret");
} else if (typeof ctx.config?.password === "string") {
password = ctx.config.password;
} else if (typeof ctx.data?.password === "string") {
password = ctx.data.password;
}
let privateKey;
if (typeof ctx.config?.privateKeySecret === "string" && ctx.config.privateKeySecret.length > 0) {
privateKey = await resolveSecretValue(ctx.config.privateKeySecret, "privateKeySecret");
} else if (typeof ctx.config?.privateKey === "string") {
privateKey = ctx.config.privateKey;
}
let passphrase;
if (typeof ctx.config?.passphraseSecret === "string" && ctx.config.passphraseSecret.length > 0) {
passphrase = await resolveSecretValue(ctx.config.passphraseSecret, "passphraseSecret");
} else if (typeof ctx.config?.passphrase === "string") {
passphrase = ctx.config.passphrase;
}
if (!password && !privateKey) {
throw new Error(
"remote-fs: password or private key is required (passwordSecret/privateKeySecret or inline values)",
);
}
return { username, password, privateKey, passphrase };
}
function toIsoDate(value) {
if (value == null) return null;
const date = value instanceof Date ? value : new Date(value);
if (Number.isNaN(date.getTime())) return null;
return date.toISOString();
}
function joinRemotePath(parentPath, name) {
if (!parentPath || parentPath === ".") return name;
if (parentPath.endsWith("/")) return `${parentPath}${name}`;
return `${parentPath}/${name}`;
}
function normalizeSftpEntry(entry, parentPath) {
const name = entry.name;
const type = entry.type === "d" ? "directory" : "file";
return {
name,
path: joinRemotePath(parentPath, name),
type,
size: type === "directory" ? null : typeof entry.size === "number" ? entry.size : null,
modified: toIsoDate(entry.modifyTime),
};
}
function normalizeFtpEntry(entry, parentPath) {
const type = entry.type === 2 ? "directory" : "file";
return {
name: entry.name,
path: joinRemotePath(parentPath, entry.name),
type,
size: type === "directory" ? null : typeof entry.size === "number" ? entry.size : null,
modified: toIsoDate(entry.modifiedAt ?? entry.rawModifiedAt),
};
}
function bufferFromWritable(writeFn) {
return new Promise((resolve, reject) => {
/** @type {Buffer[]} */
const chunks = [];
const writable = new Writable({
write(chunk, _encoding, callback) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
callback();
},
});
writable.on("finish", () => resolve(Buffer.concat(chunks)));
writable.on("error", reject);
Promise.resolve(writeFn(writable)).catch(reject);
});
}
function resolveWriteBody(ctx) {
if (ctx.config != null && typeof ctx.config === "object" && "body" in ctx.config) {
const body = ctx.config.body;
if (Buffer.isBuffer(body) || body instanceof Uint8Array) return Buffer.from(body);
if (typeof body === "string") return Buffer.from(body, "utf8");
return Buffer.from(JSON.stringify(body), "utf8");
}
if (ctx.data?.file != null) {
const file = ctx.data.file;
if (Buffer.isBuffer(file) || file instanceof Uint8Array) return Buffer.from(file);
}
if (ctx.data?.body != null) {
const body = ctx.data.body;
if (Buffer.isBuffer(body) || body instanceof Uint8Array) return Buffer.from(body);
if (typeof body === "string") return Buffer.from(body, "utf8");
return Buffer.from(JSON.stringify(body), "utf8");
}
throw new Error("remote-fs: write requires ctx.config.body, ctx.data.body, or ctx.data.file");
}
async function withSftp(ctx, protocol, fn) {
const auth = await resolveAuth(ctx);
const client = new SftpClient();
/** @type {Record<string, unknown>} */
const connectOptions = {
host: resolveHost(ctx),
port: resolvePort(ctx, protocol),
username: auth.username,
};
if (auth.privateKey) {
connectOptions.privateKey = auth.privateKey;
if (auth.passphrase) connectOptions.passphrase = auth.passphrase;
} else {
connectOptions.password = auth.password;
}
if (ctx.config?.readyTimeout != null) {
connectOptions.readyTimeout = Number(ctx.config.readyTimeout);
}
await client.connect(connectOptions);
try {
return await fn(client);
} finally {
await client.end();
}
}
async function withFtp(ctx, protocol, fn) {
const auth = await resolveAuth(ctx);
const client = new Client(
typeof ctx.config?.timeout === "number" ? ctx.config.timeout : 30000,
);
const secure = ctx.config?.secure === true || ctx.config?.secure === "implicit";
await client.access({
host: resolveHost(ctx),
port: resolvePort(ctx, protocol),
user: auth.username,
password: auth.password ?? "",
secure,
});
if (ctx.config?.passive === false) {
client.ftp.passive = false;
}
try {
return await fn(client);
} finally {
client.close();
}
}
async function runAction(protocol, ctx, action) {
const path = resolvePath(ctx, { required: action !== "list", label: "path" });
if (protocol === "sftp") {
return withSftp(ctx, protocol, async (client) => {
switch (action) {
case "list": {
const listPath = resolvePath(ctx);
const items = await client.list(listPath);
const entries = items.map((item) => normalizeSftpEntry(item, listPath));
return { path: listPath, entries, count: entries.length };
}
case "read": {
const outputVar =
typeof ctx.config?.outputVar === "string" && ctx.config.outputVar.length > 0
? ctx.config.outputVar
: "file";
const file = await client.get(path);
const buffer = Buffer.isBuffer(file) ? file : Buffer.from(file);
const encoding = ctx.config?.encoding ?? ctx.data?.encoding;
/** @type {Record<string, unknown>} */
const out = {
path,
[outputVar]: buffer,
contentLength: buffer.length,
};
if (encoding === "utf8" || encoding === "text") out.text = buffer.toString("utf8");
if (encoding === "base64") out.base64 = buffer.toString("base64");
return out;
}
case "write": {
const body = resolveWriteBody(ctx);
await client.put(body, path);
return { path, written: true, contentLength: body.length };
}
case "delete": {
await client.delete(path);
return { path, deleted: true };
}
case "stat": {
const stat = await client.stat(path);
return {
path,
type: stat.isDirectory ? "directory" : "file",
size: typeof stat.size === "number" ? stat.size : null,
modified: toIsoDate(stat.modifyTime),
accessed: toIsoDate(stat.accessTime),
};
}
case "mkdir": {
const recursive = ctx.config?.recursive !== false;
await client.mkdir(path, recursive);
return { path, created: true, recursive };
}
case "rename": {
const destination = ctx.config?.destination ?? ctx.data?.destination;
if (typeof destination !== "string" || destination.length === 0) {
throw new Error("remote-fs: rename requires destination");
}
await client.rename(path, destination);
return { path, destination, renamed: true };
}
default:
throw new Error(`remote-fs: unsupported action "${action}"`);
}
});
}
return withFtp(ctx, protocol, async (client) => {
switch (action) {
case "list": {
const listPath = resolvePath(ctx);
const items = await client.list(listPath === "." ? undefined : listPath);
const entries = items.map((item) => normalizeFtpEntry(item, listPath));
return { path: listPath, entries, count: entries.length };
}
case "read": {
const outputVar =
typeof ctx.config?.outputVar === "string" && ctx.config.outputVar.length > 0
? ctx.config.outputVar
: "file";
const buffer = await bufferFromWritable((writable) => client.downloadTo(writable, path));
const encoding = ctx.config?.encoding ?? ctx.data?.encoding;
/** @type {Record<string, unknown>} */
const out = {
path,
[outputVar]: buffer,
contentLength: buffer.length,
};
if (encoding === "utf8" || encoding === "text") out.text = buffer.toString("utf8");
if (encoding === "base64") out.base64 = buffer.toString("base64");
return out;
}
case "write": {
const body = resolveWriteBody(ctx);
const stream = Readable.from(body);
await client.uploadFrom(stream, path);
return { path, written: true, contentLength: body.length };
}
case "delete": {
await client.remove(path);
return { path, deleted: true };
}
case "stat": {
const size = await client.size(path);
const modified = await client.lastMod(path);
return {
path,
type: "file",
size: typeof size === "number" ? size : null,
modified: toIsoDate(modified),
};
}
case "mkdir": {
await client.ensureDir(path);
return { path, created: true, recursive: true };
}
case "rename": {
const destination = ctx.config?.destination ?? ctx.data?.destination;
if (typeof destination !== "string" || destination.length === 0) {
throw new Error("remote-fs: rename requires destination");
}
await client.rename(path, destination);
return { path, destination, renamed: true };
}
default:
throw new Error(`remote-fs: unsupported action "${action}"`);
}
});
}
async function remoteFs(ctx) {
const protocol = resolveProtocol(ctx);
const action = resolveAction(ctx);
const host = resolveHost(ctx);
const port = resolvePort(ctx, protocol);
log.info({ protocol, action, host, port }, "remote-fs: starting");
const result = await runAction(protocol, ctx, action);
const output = {
protocol,
action,
host,
...result,
};
log.info(
{ protocol, action, path: output.path ?? null, count: output.count ?? null },
"remote-fs: complete",
);
return {
output: { ...mergeData(ctx.data), ...output },
context: { ...passContext(ctx), ...output },
};
}
remoteFs.meta = {
description: "Access remote files over SFTP or FTP/FTPS",
previewConfigKey: "protocol",
tags: ["SFTP", "FTP", "storage"],
config: {
protocol: {
type: "string",
default: "sftp",
enum: ["sftp", "ftp"],
description: "Transfer protocol",
},
action: {
type: "string",
default: "list",
enum: ["list", "read", "write", "delete", "stat", "mkdir", "rename"],
description: "Operation to perform",
},
host: { type: "string", required: true, description: "Server hostname" },
port: { type: "number", required: false, description: "Port (default 22 for SFTP, 21 for FTP)" },
path: {
type: "string",
required: false,
description: "Remote directory for list, or file path for other actions",
},
username: { type: "string", required: false, description: "Login username" },
usernameSecret: { type: "string", required: false, description: "Named secret for username" },
password: { type: "string", required: false, description: "Login password" },
passwordSecret: { type: "string", required: false, description: "Named secret for password" },
privateKeySecret: {
type: "string",
required: false,
description: "Named secret holding an SFTP private key (PEM)",
},
privateKey: { type: "string", required: false, description: "Inline SFTP private key (PEM)" },
passphraseSecret: {
type: "string",
required: false,
description: "Named secret for encrypted private key passphrase",
},
passphrase: { type: "string", required: false, description: "Private key passphrase" },
secure: {
type: "boolean",
default: false,
description: "Use FTPS for FTP protocol",
},
passive: {
type: "boolean",
default: true,
description: "Use passive FTP mode",
},
recursive: {
type: "boolean",
default: true,
description: "Create parent directories for mkdir",
},
destination: {
type: "string",
required: false,
description: "Destination path for rename",
},
body: { type: "any", required: false, description: "Write payload" },
outputVar: {
type: "string",
default: "file",
description: "Output key for read action bytes",
},
encoding: {
type: "string",
required: false,
enum: ["utf8", "text", "base64"],
description: "Optional read decoding helper",
},
timeout: { type: "number", required: false, description: "FTP client timeout in ms" },
readyTimeout: { type: "number", required: false, description: "SFTP ready timeout in ms" },
},
input: {
protocol: { type: "string", required: false },
action: { type: "string", required: false },
host: { type: "string", required: false },
path: { type: "string", required: false },
username: { type: "string", required: false },
password: { type: "string", required: false },
body: { type: "any", required: false },
file: { type: "buffer", required: false },
destination: { type: "string", required: false },
},
output: {
protocol: { type: "string" },
action: { type: "string" },
host: { type: "string" },
entries: { type: "array", required: false, description: "list results" },
file: { type: "buffer", required: false, description: "read bytes (or outputVar)" },
written: { type: "boolean", required: false },
deleted: { type: "boolean", required: false },
renamed: { type: "boolean", required: false },
created: { type: "boolean", required: false },
},
context: {
protocol: { type: "string" },
action: { type: "string" },
host: { type: "string" },
},
example: {
data: {},
config: {
protocol: "sftp",
action: "list",
host: "sftp.example.com",
path: "/incoming",
username: "deploy",
passwordSecret: "sftp_password",
},
},
};
export default remoteFs;
+118
View File
@@ -35,6 +35,9 @@ importers:
axios:
specifier: ^1.19.0
version: 1.19.0
basic-ftp:
specifier: ^6.2.0
version: 6.2.0
bcryptjs:
specifier: ^3.0.2
version: 3.0.3
@@ -68,6 +71,9 @@ importers:
pino-roll:
specifier: ^4.0.0
version: 4.0.0
ssh2-sftp-client:
specifier: ^12.1.1
version: 12.1.1
webdav:
specifier: ^5.10.0
version: 5.10.0
@@ -985,6 +991,9 @@ packages:
asn1.js@5.4.1:
resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==}
asn1@0.2.6:
resolution: {integrity: sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
@@ -1013,6 +1022,13 @@ packages:
engines: {node: '>=6.0.0'}
hasBin: true
basic-ftp@6.2.0:
resolution: {integrity: sha512-H8eLjhoYPbOI717FLP8fGE7XInkMI7Ucj/ft0fp1avABtSiV5Bb2kYzScnOqlvJZs/KJZypr9Mp5zJmhaPFAEg==}
engines: {node: '>=10.0.0'}
bcrypt-pbkdf@1.0.2:
resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==}
bcryptjs@3.0.3:
resolution: {integrity: sha512-GlF5wPWnSa/X5LKM1o0wz0suXIINz1iHRLvTS+sLyi7XPbe5ycmYI3DlZqVGZZtDgl4DmasFg7gOB3JYbphV5g==}
hasBin: true
@@ -1042,6 +1058,13 @@ packages:
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
buildcheck@0.0.7:
resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==}
engines: {node: '>=10.0.0'}
byte-length@1.0.2:
resolution: {integrity: sha512-ovBpjmsgd/teRmgcPh23d4gJvxDoXtAzEL9xTfMU8Yc2kqCDb7L9jAG0XHl1nzuGl+h3ebCIF1i62UFyA9V/2Q==}
@@ -1074,6 +1097,10 @@ packages:
resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==}
engines: {node: '>= 12'}
concat-stream@2.0.0:
resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==}
engines: {'0': node >= 6.0}
content-disposition@0.5.4:
resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==}
engines: {node: '>= 0.6'}
@@ -1095,6 +1122,10 @@ packages:
cose-base@2.2.0:
resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==}
cpu-features@0.0.10:
resolution: {integrity: sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==}
engines: {node: '>=10.0.0'}
cronstrue@3.24.0:
resolution: {integrity: sha512-t/Ji3Ur2c/pzhIAWNwC0ftl3JAE4dLfCjAdZoTZXmPDZwcispnS1PaMcMS4OmIIXyIVouAz+yw+mfQiE3hz5OQ==}
hasBin: true
@@ -1829,6 +1860,9 @@ packages:
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
hasBin: true
nan@2.28.0:
resolution: {integrity: sha512-fTsDz99OTq2sVePhGdp4qQhggZFtKr64ZNVyVajRKtMOkJxYekplBh577PiJB12v/D3s2E5cGtOI45LWp6rnLQ==}
nanoid@3.3.18:
resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
@@ -1986,6 +2020,10 @@ packages:
resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==}
engines: {node: '>=0.10.0'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
real-require@0.2.0:
resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==}
engines: {node: '>= 12.13.0'}
@@ -2103,6 +2141,14 @@ packages:
resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==}
engines: {node: '>= 10.x'}
ssh2-sftp-client@12.1.1:
resolution: {integrity: sha512-wYVDgwkpcKG2iPGQQ+QR33xkWqLFIaVrYvA+uON4pmxTPaPuB81f1aooUEPN75e/9DCK6rrKYXb6zR6zP3+EtA==}
engines: {node: '>=18.20.4'}
ssh2@1.17.0:
resolution: {integrity: sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==}
engines: {node: '>=10.16.0'}
state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
@@ -2113,6 +2159,9 @@ packages:
steed@1.1.3:
resolution: {integrity: sha512-EUkci0FAUiE4IvGTSKcDJIQ/eRUP2JJb56+fvZ4sdnguLTqIdKjSxUe138poW8mkvKWXW2sFPrgTsxqoISnmoA==}
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
strnum@2.4.2:
resolution: {integrity: sha512-rDG3Ah4TV0k1hWvLSzkZtMmLN9+eS+h3knq4MP6A42Y3Yh5qGNnOUs1jJkoSr8FG5dsL28c7KgkIBzSEykqtuw==}
@@ -2165,6 +2214,12 @@ packages:
tslib@2.8.1:
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
tweetnacl@0.14.5:
resolution: {integrity: sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==}
typedarray@0.0.6:
resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==}
update-browserslist-db@1.3.1:
resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==}
hasBin: true
@@ -2178,6 +2233,9 @@ packages:
url-parse@1.5.10:
resolution: {integrity: sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
uuid@14.0.1:
resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==}
hasBin: true
@@ -3159,6 +3217,10 @@ snapshots:
minimalistic-assert: 1.0.1
safer-buffer: 2.1.2
asn1@0.2.6:
dependencies:
safer-buffer: 2.1.2
asynckit@0.4.0: {}
atomic-sleep@1.0.0: {}
@@ -3186,6 +3248,12 @@ snapshots:
baseline-browser-mapping@2.11.14: {}
basic-ftp@6.2.0: {}
bcrypt-pbkdf@1.0.2:
dependencies:
tweetnacl: 0.14.5
bcryptjs@3.0.3: {}
better-sqlite3@13.0.3:
@@ -3214,6 +3282,11 @@ snapshots:
node-releases: 2.0.53
update-browserslist-db: 1.3.1(browserslist@4.28.8)
buffer-from@1.1.2: {}
buildcheck@0.0.7:
optional: true
byte-length@1.0.2: {}
call-bind-apply-helpers@1.0.2:
@@ -3237,6 +3310,13 @@ snapshots:
commander@8.3.0: {}
concat-stream@2.0.0:
dependencies:
buffer-from: 1.1.2
inherits: 2.0.4
readable-stream: 3.6.2
typedarray: 0.0.6
content-disposition@0.5.4:
dependencies:
safe-buffer: 5.2.1
@@ -3255,6 +3335,12 @@ snapshots:
dependencies:
layout-base: 2.0.1
cpu-features@0.0.10:
dependencies:
buildcheck: 0.0.7
nan: 2.28.0
optional: true
cronstrue@3.24.0: {}
cross-spawn@7.0.6:
@@ -3996,6 +4082,9 @@ snapshots:
mustache@4.2.0: {}
nan@2.28.0:
optional: true
nanoid@3.3.18: {}
nested-property@4.0.0: {}
@@ -4129,6 +4218,12 @@ snapshots:
react@19.2.8: {}
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
real-require@0.2.0: {}
real-require@1.0.0: {}
@@ -4244,6 +4339,19 @@ snapshots:
split2@4.2.0: {}
ssh2-sftp-client@12.1.1:
dependencies:
concat-stream: 2.0.0
ssh2: 1.17.0
ssh2@1.17.0:
dependencies:
asn1: 0.2.6
bcrypt-pbkdf: 1.0.2
optionalDependencies:
cpu-features: 0.0.10
nan: 2.28.0
state-local@1.0.7: {}
statuses@2.0.2: {}
@@ -4256,6 +4364,10 @@ snapshots:
fastseries: 1.7.2
reusify: 1.1.0
string_decoder@1.3.0:
dependencies:
safe-buffer: 5.2.1
strnum@2.4.2:
dependencies:
anynum: 1.0.1
@@ -4291,6 +4403,10 @@ snapshots:
tslib@2.8.1: {}
tweetnacl@0.14.5: {}
typedarray@0.0.6: {}
update-browserslist-db@1.3.1(browserslist@4.28.8):
dependencies:
browserslist: 4.28.8
@@ -4304,6 +4420,8 @@ snapshots:
querystringify: 2.2.0
requires-port: 1.0.0
util-deprecate@1.0.2: {}
uuid@14.0.1: {}
vite@7.3.6(jiti@2.7.0)(lightningcss@1.32.0)(yaml@2.9.0):