npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ontemper/sftp

v1.1.0

Published

An SFTP client with structured logging.

Readme

Temper SFTP Client

A TypeScript SFTP client with structured logging and distributed tracing capabilities, built on top of ssh2-sftp-client and integrated with unnbound-logger-sdk.

Features

  • Full SFTP Operations: Complete set of SFTP operations including file transfer, directory management, and file system operations
  • Structured Logging: Built-in integration with unnbound-logger-sdk for comprehensive operation tracking
  • Distributed Tracing: Automatic span creation for all SFTP operations with detailed payload information
  • Type Safety: Full TypeScript support with comprehensive type definitions
  • Performance Monitoring: Built-in metrics collection for file sizes, operation counts, and timing
  • Flexible Configuration: Support for various authentication methods and connection options

Installation

npm install @ontemper/sftp
# or
bun add @ontemper/sftp
# or
yarn add @ontemper/sftp

From unnbound-sftp-sdk

This package was published as unnbound-sftp-sdk up to 1.0.28. That release stays on npm, frozen, for existing workflows. @ontemper/[email protected] has the same client under new names: UnnboundSftpClient is TemperSftpClient, UnnboundSftpClientOptions is TemperSftpClientOptions, and UnnboundSftpConnectOptions is TemperSftpConnectOptions. Span names and log payloads do not change. 1.0.0 also carries the changes listed under 1.0.0 in the CHANGELOG, including host-key pinning from #3029, and sends the SSH ident temper by default.

Quick Start

import { TemperSftpClient } from '@ontemper/sftp';
import { logger } from 'unnbound-logger-sdk';

const sftp = new TemperSftpClient({
  host: 'your-sftp-server.com',
  port: 22,
  username: 'your-username',
  privateKey: 'your-private-key', // or use password
  hostKeyFingerprints: ['SHA256:base64-fingerprint-without-padding'],
});

// Connect to the SFTP server
await sftp.connect();

// Get current working directory
const currentPath = await sftp.cwd();
console.log('Current path:', currentPath);

// Create a directory
await sftp.mkdir('./uploads', true); // recursive

// Upload a file
const fileContent = Buffer.from('Hello, World!');
await sftp.put(fileContent, './uploads/hello.txt');

// Download a file
const downloadedContent = await sftp.get('./uploads/hello.txt');
console.log('Downloaded:', downloadedContent.toString());

// List files in a directory
const files = await sftp.list('./uploads');
console.log(
  'Files:',
  files.map((f) => f.name),
);

// Close the connection
await sftp.close();

API Reference

Constructor

new TemperSftpClient(options: TemperSftpClientOptions)

Options

| Property | Type | Required | Description | | ---------------------- | ---------------- | -------- | ----------------------------------------------------------------- | | host | string | Yes | SFTP server hostname | | port | number | No | SFTP server port (default: 22) | | username | string | No | Username for authentication | | password | string | No | Password for authentication | | privateKey | string | No | Private key for key-based authentication | | passphrase | string | No | Passphrase for encrypted private keys | | hostKeyFingerprints | string[] | No | Accepted server host-key SHA256 fingerprints | | hostVerifier | function | No | Custom host-key check; with fingerprints, both must accept | | hostHash | string | No | Hash applied to the key before hostVerifier (e.g. sha256) | | allowUnpinnedHostKey | boolean | No | Explicitly allow an unpinned Temper-hosted connection | | maxBufferBytes | number | No | Maximum file size for get (default: 256 MiB) | | streamChunkBytes | number | No | Chunk size: 32 KiB–1 MiB; default upload 64 KiB, download 256 KiB | | streamConcurrency | number | No | Requests per window: 1–64; default upload 32, download 16 | | localHostname | string | No | Local hostname for hostbased authentication | | localUsername | string | No | Local username for hostbased authentication | | keepaliveInterval | number | No | SSH keepalive interval in milliseconds (0 to disable) | | keepaliveCountMax | number | No | Max unanswered keepalive packets before disconnection | | readyTimeout | number | No | SSH handshake timeout in milliseconds | | localAddress | string | No | Local network interface IP address | | localPort | number | No | Local port number | | timeout | number | No | Socket timeout in milliseconds | | ident | string | No | Server software identifier (default: 'temper') | | logger | UnnboundLogger | No | Custom logger instance (uses default if not provided) |

From a platform sftp Resource

The package does not depend on @ontemper/resources, but the constructor accepts a resolved revision-2 sftp record. It maps the record's auth variant (password, private_key, or private_key_and_password) to ssh2 options. The two-factor variant uses ssh2's authentication handler to send the password after public-key authentication. It also maps the record's algorithms and verifies hostKeyFingerprints.

import { env, SftpResource } from '@ontemper/resources';
import { TemperSftpClient } from '@ontemper/sftp';

const record = await env.resources.resolve('partner-sftp', SftpResource);
const sftp = new TemperSftpClient(record);
await sftp.connect();

Connection Management

connect(): Promise<void>

Establishes a connection to the SFTP server.

await sftp.connect();

Server host-key pinning

Host-key pinning comes from #3029. Set hostKeyFingerprints to the fingerprints that you trust. The client rejects the connection before authentication if the server offers a different key. The extra sessions that downloadDir and uploadDir open check the same fingerprints.

const sftp = new TemperSftpClient({
  host: 'sftp.gotemper.com',
  username: 'myuser',
  password: process.env.SFTP_PASSWORD,
  hostKeyFingerprints: ['SHA256:base64-fingerprint-without-padding'],
});

TEMPER_SFTP_HOST_KEY_FINGERPRINTS supplies a comma-separated fallback only for Temper-hosted endpoints (sftp.gotemper.com and *.sftp.dev.gotemper.com). Those endpoints reject unpinned connections. Set allowUnpinnedHostKey: true only when you explicitly accept that risk. A caller-supplied hostVerifier also satisfies this requirement, so it must enforce your own host-key trust policy; with hostKeyFingerprints set, both must accept. The environment fallback is ignored for other hosts, which connect without pinning for compatibility; configure hostKeyFingerprints explicitly when another server operator publishes stable fingerprints.

For Temper-hosted SFTP, copy the published fingerprint from Settings → SFTP access. For another server, obtain its fingerprint from its operator. The following command is valid only when it runs on the server whose host key you are pinning:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub -E sha256

close(): Promise<boolean>

Closes the SFTP connection.

const closed = await sftp.close();

File Operations

Use downloadFile, uploadFile, downloadDir, and uploadDir for anything larger than a few megabytes. They stream between disk and the server with up to 64 requests in flight per file. With the defaults, downloadDir and uploadDir move up to 12 files at once (three per session on four sessions), so a server can see up to 768 outstanding requests. get reads with pipelined requests but holds the whole file in memory, up to maxBufferBytes. put uses one request at a time; against a server about 85 ms away, that is about 0.4 MB/s. Both log the full file content. getStream and putStream pipeline without holding the file.

downloadFile(remotePath: string, localPath: string, options?: { useFastget?: boolean }): Promise<number>

Downloads one file to a local path with pipelined reads. Resolves to the byte count. Logs carry paths and size, not content. { useFastget: false } streams the file with get instead, for a server that mishandles parallel requests.

const bytes = await sftp.downloadFile('/inbound/orders.csv', '/tmp/orders.csv');

uploadFile(localPath: string, remotePath: string, options?: { useFastput?: boolean }): Promise<number>

Uploads one local file with pipelined writes. Resolves to the byte count. Logs carry paths and size, not content. { useFastput: false } streams the file with put instead, for a server that mishandles parallel requests.

await sftp.uploadFile('/tmp/result.csv', '/outbound/result.csv');

get(path: string): Promise<Buffer>

Downloads a small file into memory. The client rejects a remote file larger than maxBufferBytes (256 MiB by default): before downloading when the server reports the size, otherwise as soon as the limit is exceeded. Use getStream or downloadFile for large files.

const content = await sftp.get('/remote/path/file.txt');

put(input: Buffer, path: string): Promise<string>

Uploads an existing Buffer. The SDK enforces no size limit; the caller controls how much it holds in memory. Use putStream for large files.

const data = Buffer.from('file content');
await sftp.put(data, '/remote/path/file.txt');

getStream(path: string): NodeJS.ReadableStream

Streams a remote file through bounded, concurrent reads, delivered in file-offset order. The default window is 16 × 256 KiB (4 MiB). Consumer backpressure prevents the next window from being read.

import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream/promises';

await pipeline(sftp.getStream('/remote/path/large-file.dat'), createWriteStream('./large-file.dat'));

putStream(path: string, input: NodeJS.ReadableStream): Promise<string>

Streams a local file through bounded, vectorized writes. The default batch is 32 × 64 KiB (2 MiB). The SDK waits for every write in a batch before reading another batch. A failed upload ends the connection so the server can discard its incomplete transfer.

import { createReadStream } from 'node:fs';

await sftp.putStream('/remote/path/large-file.dat', createReadStream('./large-file.dat'));

The get and put methods hold the whole file in one Buffer. get enforces maxBufferBytes, which defaults to 256 MiB; put has no SDK limit. Use getStream and putStream for large files so file contents are not held in one Buffer. get and getStream share one pipelined reader (set streamConcurrency: 1 for a server that mishandles parallel reads); when the server reports no file size or a size of 0, they read until the end of the file. put delegates the buffer upload to ssh2-sftp-client.

streamChunkBytes and streamConcurrency tune both streaming directions. Values must be integers within the documented bounds; invalid values throw a RangeError naming the option. The transfer window uses at most their product in bytes (up to 64 MiB). This excludes caller-owned source buffers and SSH transport buffers: supply a backpressure-aware source rather than a preloaded whole-file buffer. The SDK creates no client-side temporary transfer files; Temper-hosted uploads use the server-side spool covered in the linked capacity documentation. Downloads use the size reported at the start; premature EOF is an error.

Run the local 10 MiB benchmark with a 75 ms delay per SFTP request:

pnpm --filter @ontemper/sftp exec tsx test/streaming-benchmark.ts

It reports elapsed upload/download times and observed request concurrency for streamConcurrency: 1 versus defaults, without a timing-based pass/fail threshold. One representative run on 2026-09-06 produced:

| Direction | streamConcurrency: 1 | Default window | Observed in-flight requests | | --------- | ---------------------: | -------------: | --------------------------: | | Upload | 36.8 s | 1.25 s | 32 | | Download | 27.7 s | 2.11 s | 16 |

This benchmark reports elapsed time and observed request concurrency in a local latency simulation. It does not verify byte order, define hosted endpoint capacity, or define an SLA. The independent test/streaming.test.ts suite verifies ordered, byte-identical transfers with SHA-256. See the hosted server's deployment, scaling, and capacity evidence for server-side limits and deployed observations.

append(input: Buffer | NodeJS.ReadableStream, path: string): Promise<string>

Appends data to an existing file.

The client method works only when the remote server supports append. Temper hosted SFTP rejects append with SSH_FX_OP_UNSUPPORTED; use create or truncate writes there. See the server operation table.

const additionalData = Buffer.from('more content');
await sftp.append(additionalData, '/remote/path/file.txt');

Directory Operations

list(path: string, filter?: ListFilterFunction): Promise<FileInfo[]>

Lists files and directories in the specified path.

const files = await sftp.list('/remote/directory');
const onlyFiles = await sftp.list('/remote/directory', (item) => item.type === '-');

mkdir(path: string, recursive?: boolean): Promise<string>

Creates a directory.

await sftp.mkdir('/remote/new-directory');
await sftp.mkdir('/remote/nested/path', true); // recursive

rmdir(path: string, recursive?: boolean): Promise<string>

Removes a directory.

await sftp.rmdir('/remote/empty-directory');
await sftp.rmdir('/remote/directory-with-files', true); // recursive

uploadDir(sourcePath: string, destinationPath: string, options?: UploadDirOptions): Promise<string>

Uploads an entire directory. Files move with pipelined writes over up to four SFTP sessions, three files per session at a time; nested directories are created first. Set connections to change the session count. When the server refuses an extra session, the transfer continues on the sessions it has. { useFastput: false } streams files with put on one session, up to 10 at once. Use it for a server that mishandles parallel requests: ssh2-sftp-client warns that such servers can truncate or corrupt pipelined transfers.

await sftp.uploadDir('./local/directory', '/remote/destination');

downloadDir(sourcePath: string, destinationPath: string, options?: DownloadDirOptions): Promise<string>

Downloads an entire directory. Files move with pipelined reads over up to four SFTP sessions, three files per session at a time; a symlinked file downloads from its target. Set connections to change the session count. When the server refuses an extra session, the transfer continues on the sessions it has. { useFastget: false } streams files with get on one session, up to 10 at once. Use it for a server that mishandles parallel requests: ssh2-sftp-client warns that such servers can truncate or corrupt pipelined transfers.

await sftp.downloadDir('/remote/directory', './local/destination');

File System Operations

exists(path: string): Promise<false | FileInfoType>

Checks if a file or directory exists.

const exists = await sftp.exists('/remote/path/file.txt');
// Returns: false, 'd' (directory), '-' (file), or 'l' (symlink)

stat(path: string): Promise<FileStats>

Gets file or directory statistics.

const stats = await sftp.stat('/remote/path/file.txt');
console.log('Size:', stats.size);
console.log('Modified:', stats.modifyTime);

realPath(path: string): Promise<string>

Resolves the real path of a file or directory.

const realPath = await sftp.realPath('/remote/symlink');

cwd(): Promise<string>

Gets the current working directory.

const currentDir = await sftp.cwd();

delete(path: string, noErrorOK?: boolean): Promise<string>

Deletes a file.

await sftp.delete('/remote/path/file.txt');

rename(sourcePath: string, destinationPath: string): Promise<string>

Renames or moves a file or directory.

await sftp.rename('/remote/old-name.txt', '/remote/new-name.txt');

chmod(path: string, mode: number | string): Promise<string>

Changes file or directory permissions.

await sftp.chmod('/remote/path/file.txt', 0o644);
await sftp.chmod('/remote/path/file.txt', '644');

Retry and integrity guidance

An upload method resolves only after the remote file handle closes. A rejected promise can still follow a successful provider commit when the network fails after the server stores the object. Treat that result as unknown, not as proof that the object is absent.

Before you retry an upload:

  1. Reconnect.
  2. Call stat() for the destination.
  3. If the size matches, download the destination and compare its SHA-256 with the local file.
  4. Accept the first upload when the hash matches. Retry only when the destination is absent or the customer explicitly accepts replacement after a mismatch.

Do not retry every SSH_FX_FAILURE without inspection. The code can mean a generation conflict, an existing no-clobber destination, or an unknown provider result.

Hosted-SFTP rename is no-clobber and non-atomic. An error can leave the source, or it can temporarily leave both names after a provider copy. After a rename error, inspect both paths and compare bytes before you delete either path or retry the move.

Type Definitions

FileInfo

interface FileInfo {
  type: FileInfoType; // 'd' | '-' | 'l' (directory, file, symlink)
  name: string;
  size: number;
  modifyTime: number;
  accessTime: number;
  rights: {
    user: string;
    group: string;
    other: string;
  };
  owner: number;
  group: number;
}

FileStats

interface FileStats {
  mode: number;
  uid: number;
  gid: number;
  size: number;
  accessTime: number;
  modifyTime: number;
  isDirectory: boolean;
  isFile: boolean;
  isBlockDevice: boolean;
  isCharacterDevice: boolean;
  isSymbolicLink: boolean;
  isFIFO: boolean;
  isSocket: boolean;
}

Directory Options

interface DirOptions {
  filter?: DirFilterFunction;
}

interface UploadDirOptions extends DirOptions {
  useFastput?: boolean; // default true
  connections?: number; // default 4
}

interface DownloadDirOptions extends DirOptions {
  useFastget?: boolean; // default true
  connections?: number; // default 4
}

Logging and Tracing

The Temper SFTP Client creates spans for its operations. Spans include:

  • Operation type and timing
  • File paths
  • Host information
  • Error details (if any)
  • Byte counts for single-file transfers (get, put, append, downloadFile, uploadFile); downloadDir and uploadDir log paths only
  • Full file contents for get, put, and append (content), for every SFTP server including Temper-hosted endpoints; getStream, putStream, downloadFile, uploadFile, downloadDir, and uploadDir log no content

Synced file content is part of the observability product: get, put, and append spans carry the complete file body in sftp.content, the same contract as HTTP request/response body logging. Spans never include the connection password, private key, or passphrase. The file bodies that get, put, and append log are logged as they are, so a file that contains a secret puts that secret in the logs.

Custom Logger

You can provide a custom logger instance:

import { TemperSftpClient } from '@ontemper/sftp';
import { logger } from 'unnbound-logger-sdk';

const sftp = new TemperSftpClient({
  host: 'sftp.example.com',
  username: 'user',
  password: 'pass',
  logger: logger, // Custom logger instance
});

Example Log Output

{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"connect"},"message":"SFTP connect started."}
{"level":"info","spanId":"556c53e6-3bb8-4118-b3b8-570e41c27654","type":"sftp","sftp":{"host":"sftp.domain.com","operation":"put","path":"/uploads/file.txt","bytes":15,"content":"A-100,3,shipped"},"duration":150,"message":"SFTP put completed."}

Error Handling

All methods return promises that will reject with detailed error information:

try {
  await sftp.get('/non-existent-file.txt');
} catch (error) {
  console.error('SFTP operation failed:', error.message);
  // Error details are automatically logged with the span
}

Authentication Methods

Password Authentication

const sftp = new TemperSftpClient({
  host: 'sftp.example.com',
  username: 'myuser',
  password: 'mypassword',
});

Private Key Authentication

const sftp = new TemperSftpClient({
  host: 'sftp.example.com',
  username: 'myuser',
  privateKey: `-----BEGIN OPENSSH PRIVATE KEY-----
...your private key content...
-----END OPENSSH PRIVATE KEY-----`,
});

Private Key with Passphrase

const sftp = new TemperSftpClient({
  host: 'sftp.example.com',
  username: 'myuser',
  privateKey: 'your-private-key',
  passphrase: 'your-passphrase',
});

Private Key from Environment Variables

The client automatically normalizes private keys from environment variables by converting \n escape sequences to actual newlines:

// Environment variable: PRIVATE_KEY="-----BEGIN OPENSSH PRIVATE KEY-----\nMIIEpAIBAAKCAQEA...\n-----END OPENSSH PRIVATE KEY-----"

const sftp = new TemperSftpClient({
  host: 'sftp.example.com',
  username: 'myuser',
  privateKey: process.env.PRIVATE_KEY, // Automatically normalized
});

Requirements

  • Node.js >= 22.0.0
  • TypeScript (for TypeScript projects)

Dependencies

  • ssh2-sftp-client: Core SFTP functionality
  • unnbound-logger-sdk: Structured logging and tracing

Support

For support, please open an issue on the GitHub repository.