One TypeScript SDK for moving files across every storage system you actually use.
FTP · FTPS · SFTP · HTTP(S) · WebDAV · S3-compatible · Azure Blob · GCS · Google Drive · Dropbox · OneDrive · Local
ZeroTransfer is a unified, TypeScript-first file transfer SDK for Node.js. One typed API speaks to every backend you actually deploy against - classic protocols, web endpoints, object storage, cloud drives, and local disks - with streaming, resume, verification, dry-run plans, MFT-style scheduling, audit logs, and webhook delivery built in.
import {
createDefaultRetryPolicy,
createS3ProviderFactory,
createTransferClient,
uploadFile,
} from "@zero-transfer/sdk";
const client = createTransferClient({
providers: [createS3ProviderFactory({ region: "us-east-1" })],
defaults: {
retry: createDefaultRetryPolicy(),
timeout: { stallTimeoutMs: 30_000 },
},
});
// One call, any provider you registered above.
await uploadFile({
client,
localPath: "./dist/app.tar.gz",
destination: {
path: "/lake/bronze/app.tar.gz",
profile: {
provider: "s3",
host: "data-lake-bronze",
username: { env: "AWS_ACCESS_KEY_ID" },
password: { env: "AWS_SECRET_ACCESS_KEY" },
},
},
});- One API, every provider. Replace bespoke FTP, SFTP, S3, and cloud-drive code with a single
TransferClientand provider-neutral sessions. - TypeScript-first. Strict types, exact optional properties, exhaustive capability discovery, and typed errors for every protocol failure mode.
- Checkpointed resume. Interrupted transfers pick up from the committed byte watermark - across retries, fresh calls, and process restarts. Checkpoints are keyed by source+destination path, fingerprint the source (size/mtime/etag) so a changed file never resumes onto stale bytes, and extend to whole batches:
runResumableBatch()re-runs a plan and skips every step that already succeeded. - Built for throughput. Pipelined SFTP (64 in-flight requests x 32 KiB, OpenSSH parity) saturates high-latency links; S3 multipart and Azure staged blocks upload parts in parallel with progress that only ever reports the contiguous completed prefix.
- Streaming everywhere. Backpressure via async iterables, byte-range downloads, multipart/staged/resumable-session uploads on every object store and cloud drive. Memory-bounded end to end - no whole-file buffering, and framing layers cap declared packet sizes.
- Resilient by default.
createDefaultRetryPolicy()retries only retryable failures with exponential backoff + full jitter and honorsRetry-Afterhints; job-scope and attempt-scope timeouts plus a stall watchdog catch connections that go silent; every receipt records per-attempt history. - Dry-run-first sync. Diff remote trees, generate
TransferPlans, and review every step before any byte moves. - MFT batteries. Routes, cron + interval schedules, audit logs, HMAC-signed webhooks, retention policies, and approval gates that block on human sign-off.
- Security by default. Profile redaction in every log, host-key pinning, certificate fingerprint pinning, OAuth refresh, and SecretSource adapters for vaults / env / files / commands.
- Observable. Structured logger, redaction-safe diagnostics, immutable transfer receipts, and per-attempt history for compliance.
# Batteries-included SDK (every provider):
npm install @zero-transfer/sdk
# Or pick a scoped package with a narrowed export surface:
npm install @zero-transfer/sftp
npm install @zero-transfer/s3
npm install @zero-transfer/mftRequires Node.js >=20.
ZeroTransfer publishes 14 scoped packages under the @zero-transfer npm organization. @zero-transfer/sdk is the batteries-included distribution; the other 13 are narrowly scoped packages that publish only the symbols listed in their scope page. Pick one to keep your dependency tree tight, or install the SDK if you want every provider in one go.
Every protocol-scoped package (everything except @zero-transfer/core itself) automatically pulls in @zero-transfer/core as a transitive dependency and re-exports the full core surface (createTransferClient, uploadFile, downloadFile, profiles, errors, sync planner, …). A single import { … } from "@zero-transfer/<scope>" is all you need - no separate @zero-transfer/core install. If your app uses multiple protocols, install the umbrella @zero-transfer/sdk instead of multiple scoped packages.
| Package | Summary | Docs |
|---|---|---|
@zero-transfer/sdk |
Batteries-included distribution. Every provider, every helper. | API reference |
@zero-transfer/core |
Provider-neutral contracts, transfer engine, queue, profiles, errors. | Scope page |
@zero-transfer/classic |
FTP + FTPS + SFTP in one install. | Scope page |
@zero-transfer/ftp |
Classic FTP with EPSV/PASV streaming and REST resume. | Scope page |
@zero-transfer/ftps |
Explicit + implicit FTPS with full TLS profile support. | Scope page |
@zero-transfer/sftp |
SFTP with SSH key auth, known_hosts, and jump-host support. | Scope page |
@zero-transfer/ssh |
Standalone SSH 2.0 transport, auth, and channel primitives (exec/subsystem). | Scope page |
@zero-transfer/http |
HTTP(S) and signed-URL provider with ranged downloads. | Scope page |
@zero-transfer/webdav |
WebDAV with PROPFIND listings and ranged downloads. | Scope page |
@zero-transfer/s3 |
S3-compatible storage with SigV4, multipart upload, and cross-process resume. | Scope page |
@zero-transfer/google-drive |
Google Drive with OAuth, folder paths, md5 checksums. | Scope page |
@zero-transfer/dropbox |
Dropbox with content-hash verification. | Scope page |
@zero-transfer/azure-blob |
Azure Blob Storage with SAS or AAD bearer auth. | Scope page |
@zero-transfer/mft |
Routes, schedules, audit logs, webhooks, approval gates. | Scope page |
The full per-scope index lives at docs/scopes/.
import { createSftpProviderFactory, createTransferClient } from "@zero-transfer/sdk";
const client = createTransferClient({
providers: [createSftpProviderFactory()],
});
const session = await client.connect({
provider: "sftp",
host: "files.example.com",
username: { env: "ZT_USER" },
password: { env: "ZT_PASSWORD" },
ssh: {
knownHosts: { path: "./known_hosts" },
pinnedHostKeySha256: "SHA256:base64-encoded-host-key-digest",
},
});
const releases = await session.fs.list("/releases");
await session.disconnect();import { uploadFile, type ConnectionProfile } from "@zero-transfer/sdk";
const sftpProfile: ConnectionProfile = {
host: "files.example.com",
provider: "sftp",
username: { env: "ZT_USER" },
ssh: {
privateKey: { path: "./keys/id_ed25519" },
pinnedHostKeySha256: "SHA256:base64-encoded-host-key-digest",
},
};
await uploadFile({
client,
localPath: "./dist/app.tar.gz",
destination: { path: "/releases/2026.04.28/app.tar.gz", profile: sftpProfile },
onProgress: (event) => console.log(`${event.bytesTransferred}/${event.totalBytes ?? "?"}`),
});The
profileshape is the same provider-neutralConnectionProfileused byclient.connect(). See Connection profiles below for the full field reference and security guidance.
import { createSyncPlan, diffRemoteTrees, summarizeTransferPlan } from "@zero-transfer/sdk";
const diff = await diffRemoteTrees(srcSession.fs, "/dist", dstSession.fs, "/releases/current");
const plan = createSyncPlan({
id: "release-sync",
diff,
source: { provider: "sftp", rootPath: "/dist" },
destination: { provider: "s3", rootPath: "/releases/current" },
deletePolicy: "mirror",
});
console.table(summarizeTransferPlan(plan));import {
ApprovalRegistry,
MftScheduler,
RouteRegistry,
ScheduleRegistry,
createApprovalGate,
runRoute,
} from "@zero-transfer/sdk";
const approvals = new ApprovalRegistry();
const scheduler = new MftScheduler({
client,
routes: new RouteRegistry([route]),
schedules: scheduleRegistry,
runner: createApprovalGate({
approvalId: ({ route }) => `release:${route.id}:${Date.now()}`,
registry: approvals,
runner: ({ client: c, route: r, signal }) => runRoute({ client: c, route: r, signal }),
}),
onResult: ({ receipt }) => console.log(`Released ${receipt.jobId}`),
});
scheduler.start();Every operation that touches a remote system takes a ConnectionProfile. Profiles are provider-neutral data - you build one once and pass it to client.connect(), uploadFile(), downloadFile(), copyBetween(), MFT routes, and diagnostics. The same shape works for every provider; only the optional auth blocks (ssh, tls, oauth, s3, …) change.
| Field | Type | Notes |
|---|---|---|
host |
string |
Remote hostname / IP / bucket / drive identifier (provider-specific). Always required. |
provider |
ProviderId |
One of "ftp", "ftps", "sftp", "http", "https", "webdav", "s3", "azure-blob", "gcs", "google-drive", "dropbox", "one-drive", "local", "memory", or any custom id you registered. |
| Field | Type | Notes |
|---|---|---|
port |
number |
Provider applies a sensible default when omitted. |
username |
SecretSource |
String, { env }, { path }, { base64Env }, { value }, or callback. |
password |
SecretSource |
Same shapes as username. Used as bearer token for cloud providers. |
secure |
boolean |
Request encrypted transport when the protocol allows opt-in TLS. |
tls |
TlsProfile |
CA bundle, mTLS cert/key, fingerprint pinning, min/max TLS version. |
ssh |
SshProfile |
Private key, passphrase, known_hosts, host-key pin, agent, algorithms. |
timeoutMs |
number |
Connection / operation timeout. |
signal |
AbortSignal |
Cancels connection setup and long-running operations. |
logger |
ZeroTransferLogger |
Per-profile structured logger override (still redaction-safe). |
Every credential field (username, password, tls.ca, tls.key, ssh.privateKey, ssh.knownHosts, ssh.passphrase, …) accepts a SecretSource. Inline strings work for prototypes, but production code should pull from the environment, a file, or a callback so secrets stay out of source control and out of process memory dumps.
// Inline string - fine for tests, avoid in production.
password: "hunter2";
// Read from an environment variable.
password: {
env: "SFTP_PASSWORD";
}
// Read from a file (e.g. a Docker / Kubernetes secret mount).
privateKey: {
path: "/run/secrets/sftp_id_ed25519";
}
// Read base64-encoded binary from an environment variable.
ca: {
base64Env: "FTPS_CA_BUNDLE_B64";
}
// Pull from your vault / credential broker on demand.
password: async () => await vault.read("kv/sftp/deploy");Profiles are run through redactConnectionProfile() before any log line is emitted, so secret values never appear in logs, audit entries, or diagnostics.
// SFTP with public-key auth + host-key pin (production-hardened)
const sftpProfile: ConnectionProfile = {
host: "sftp.example.com",
provider: "sftp",
username: "deploy",
ssh: {
privateKey: { path: "./keys/id_ed25519" },
pinnedHostKeySha256: "SHA256:abc123basesixfourpinFromKnownHosts=",
},
};
// FTPS with mTLS + private CA bundle
const ftpsProfile: ConnectionProfile = {
host: "ftps.internal.example",
provider: "ftps",
username: "audit",
tls: {
ca: { path: "./certs/ca-bundle.pem" },
cert: { path: "./certs/client.crt" },
key: { path: "./certs/client.key" },
pinnedFingerprint256:
"AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF:00:11:22:33:44:55:66:77:88:99",
},
};
// S3-compatible bucket
const s3Profile: ConnectionProfile = {
host: "data-lake-bronze",
provider: "s3",
username: { env: "AWS_ACCESS_KEY_ID" },
password: { env: "AWS_SECRET_ACCESS_KEY" },
};
// Cloud drive (OAuth bearer token in `password`)
const dropboxProfile: ConnectionProfile = {
host: "",
provider: "dropbox",
password: { env: "DROPBOX_ACCESS_TOKEN" },
};- Pin host keys for SSH/SFTP. Without
ssh.knownHostsorssh.pinnedHostKeySha256the SSH session accepts any key the server presents - a MITM risk. - Pin TLS fingerprints when you control the server.
tls.pinnedFingerprint256is defence-in-depth on top ofrejectUnauthorized: trueand a CA bundle. - Never set
tls.rejectUnauthorized: falsein production. Pair self-signed servers withtls.cainstead. - Prefer
{ env },{ path }, or callback secrets over inline strings or hard-coded values. - See
examples/sftp-private-key.ts,examples/ftps-client-certificate.ts, andexamples/profile-from-env.tsfor end-to-end hardened profile builds.
Full per-field reference: ConnectionProfile, SshProfile, TlsProfile, SecretSource.
Every provider advertises its own CapabilitySet. The full programmatic matrix is exposed via getBuiltinCapabilityMatrix() and renders to Markdown via formatCapabilityMatrixMarkdown().
| Provider | Streaming | Resume | Server-side copy | Multipart upload | Checksum exposed |
|---|---|---|---|---|---|
| FTP | ✅ | ⬆/⬇ via REST | - | - | - |
| FTPS | ✅ | ⬆/⬇ via REST | - | - | - |
| SFTP | ✅ | ⬆/⬇ | rename | - | - |
| HTTP(S) | ✅ (read) | ⬇ via Range | - | - | ETag |
| WebDAV | ✅ | ⬇ via Range | COPY | - | ETag |
| S3-compatible | ✅ | ⬆ via multipart resume / ⬇ Range | CopyObject | ✅ parallel | SHA-256 / md5 |
| Azure Blob | ✅ | ⬆ via staged blocks / ⬇ Range | - | ✅ parallel | md5 |
| GCS | ✅ | ⬇ via Range | - | ✅ | crc32c / md5 |
| Google Drive | ✅ | ⬇ via Range | - | ✅ | md5 |
| Dropbox | ✅ | ⬇ via Range | - | ✅ | content_hash |
| OneDrive | ✅ | ⬇ via Range | - | ✅ | sha256 / sha1 / quickXor |
| Local | ✅ | ⬆/⬇ | - | - | - |
| Memory | ✅ | ⬆/⬇ | - | - | - |
Real-world examples live in examples/. Run them with tsx examples/<file>.
| Example | What it shows |
|---|---|
local-copy-file.ts |
Zero-config local-to-local copy via copyBetween. |
ftp-basic.ts |
Plain FTP upload + download round-trip with username/password. |
ftp-directory-ops.ts |
FTP session.fs: list, stat, mkdir, rename, remove, rmdir. |
ftps-basic.ts |
FTPS with username/password over a public-CA endpoint. |
ftps-client-certificate.ts |
FTPS hardened: mTLS + private CA bundle + fingerprint pinning. |
ftps-directory-ops.ts |
FTPS session.fs: list, stat, mkdir, rename, remove, rmdir. |
sftp-basic.ts |
Minimal SFTP with username/password (no host-key pinning). |
sftp-private-key.ts |
SFTP hardened: private-key auth + pinned host-key SHA-256. |
sftp-directory-ops.ts |
SFTP session.fs: list, stat, mkdir, rename, remove, rmdir. |
ssh-exec-command.ts |
Standalone SSH stack: handshake, auth, run a remote command. |
s3-compatible-upload.ts |
S3 parallel multipart upload with resumable checkpoints. |
webdav-sync.ts |
WebDAV diff + sync plan with deterministic ordering. |
signed-url-download.ts |
HTTPS signed-URL download with progress reporting. |
transfer-queue.ts |
Concurrent transfers with TransferQueue + executor. |
retry-and-timeouts.ts |
Retry policy, timeout scopes, stall watchdog, client defaults. |
resume-checkpoints.ts |
Dropped transfer resuming from the checkpoint watermark. |
resumable-batch.ts |
Crash-safe batch plan: re-runs skip already-completed steps. |
dry-run-sync.ts |
Plan a sync, print a summary, never touch bytes. |
mft-route.ts |
SFTP→S3 cron-scheduled MFT route with audit hooks. |
profile-from-env.ts |
Build a ConnectionProfile from env / file / base64-env secrets. |
diagnose-connection.ts |
Provider summary + redaction-safe connection probe. |
approval-gated-route.ts |
Two-person rule: scheduled route blocks until approval lands. |
multi-cloud-orchestration.ts |
Fan-out SFTP → S3 + Azure + Local with webhook audit. |
atomic-deploy-with-rollback.ts |
Blue/green-style deploy plan with rollback path. |
- Full API reference (HTML) - TypeDoc HTML site, deployed from
mainon every push. - Full API reference (Markdown) - every public symbol with parameter / property / type tables.
- Per-scope pages - one page per
@zero-transfer/*package. - Examples directory - runnable real-world flows.
Regenerate everything locally:
npm run docs:all # HTML + Markdown api refs + per-scope pages + per-package READMEsZeroTransfer is in alpha under the alpha npm dist-tag. The provider-neutral foundation, transfer engine, queue, sync planner, atomic deploy planner, MFT layer, friendly client surface, and diagnostics module are stable. The massive-file engine is in place: unified checkpoint/resume (cross-process, fingerprint-validated, batch-aware via runResumableBatch), pipelined SFTP transfers, parallel multipart uploads on S3 and Azure, and streaming upload sessions across every cloud drive (Dropbox, Google Drive, OneDrive, GCS), on top of the resilience layer (default retry policy with backoff + jitter, job/attempt timeout scopes, stall detection, redaction-safe error logging, memory-bounded streaming). Next up: verification engine, segmented parallel downloads, and proxy/HTTP2 support.
git clone https://github.com/molexxxx/zero-transfer.git
cd zero-transfer
npm install
npm run ci # lint, format check, typecheck, tests with coverage, build, pack dry-run
npm run test:watch # iterateIssues and PRs welcome. Provider integration tests are gated behind opt-in env vars - see test/integration/ for the full list.
MIT © Tony Wiedman