From 7474bb5c6d1d03dfc9a481d7561a904169bf3a16 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 17 Aug 2026 00:22:30 +0000 Subject: [PATCH] feat(scripts): add remote-fs script for SFTP and FTP access Co-authored-by: Nasyarobby Putra --- packages/server/package.json | 2 + packages/server/script-sandbox.js | 3 + packages/server/scripts/remote-fs.js | 521 +++++++++++++++++++++++++++ pnpm-lock.yaml | 118 ++++++ 4 files changed, 644 insertions(+) create mode 100644 packages/server/scripts/remote-fs.js diff --git a/packages/server/package.json b/packages/server/package.json index 8a8b4c5..f3af2cc 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -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", "yaml": "^2.9.0" } } diff --git a/packages/server/script-sandbox.js b/packages/server/script-sandbox.js index d02cdc1..faa8d98 100644 --- a/packages/server/script-sandbox.js +++ b/packages/server/script-sandbox.js @@ -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", ]); const BUILTIN_NAMES = [ diff --git a/packages/server/scripts/remote-fs.js b/packages/server/scripts/remote-fs.js new file mode 100644 index 0000000..c043b4e --- /dev/null +++ b/packages/server/scripts/remote-fs.js @@ -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} */ + 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} */ + 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} */ + 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; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc69c89..98adc01 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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 yaml: specifier: ^2.9.0 version: 2.9.0 @@ -973,6 +979,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==} @@ -995,6 +1004,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 @@ -1021,6 +1037,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'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1047,6 +1070,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'} @@ -1068,6 +1095,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 @@ -1757,6 +1788,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} @@ -1892,6 +1926,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'} @@ -2006,6 +2044,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==} @@ -2016,6 +2062,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==} + stylis@4.4.0: resolution: {integrity: sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==} @@ -2065,12 +2114,21 @@ 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 peerDependencies: browserslist: '>= 4.21.0' + 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 @@ -3032,6 +3090,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: {} @@ -3055,6 +3117,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: @@ -3079,6 +3147,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 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3098,6 +3171,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 @@ -3116,6 +3196,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: @@ -3810,6 +3896,9 @@ snapshots: mustache@4.2.0: {} + nan@2.28.0: + optional: true + nanoid@3.3.18: {} node-addon-api@8.9.2: {} @@ -3927,6 +4016,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: {} @@ -4040,6 +4135,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: {} @@ -4052,6 +4160,10 @@ snapshots: fastseries: 1.7.2 reusify: 1.1.0 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + stylis@4.4.0: {} supports-preserve-symlinks-flag@1.0.0: {} @@ -4083,12 +4195,18 @@ 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 escalade: 3.2.0 picocolors: 1.1.1 + 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):