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

@openinc/smb-client

v1.1.5

Published

SMB1/2/3 client for Node.js, backed by native libsmbclient (Samba) via koffi FFI. Usable as a library or as a standalone HTTP container.

Readme

@openinc/smb-client

An SMB client for Node.js backed by Samba's native libsmbclient, exposed through koffi (FFI). Because it wraps libsmbclient, it speaks every SMB dialect the installed Samba supports — SMB1/CIFS, SMB2, and SMB3 including SMB3.1.1 encryption — instead of reimplementing the protocol in JavaScript.

Use it two ways:

  1. As a library in a Node.js application: const smb = new SMB({ ... }).
  2. As a standalone Docker container that exposes a share over a small HTTP API, so other containers can read and write files without speaking SMB themselves.

Why libsmbclient

Pure-JavaScript SMB packages (@marsaud/smb2 and friends) are stuck at SMB2 and do not implement SMB3 encryption. Rather than ship an incomplete, security-sensitive protocol reimplementation, this package binds the battle-tested libsmbclient. The trade-off is a native dependency: the libsmbclient.so runtime library must be present in the environment (a package install on normal images, a multi-stage copy on hardened images — see below).

Requirements

  • Node.js ≥ 18
  • libsmbclient shared library available at runtime
    • Alpine: apk add libsmbclient
    • Debian/Ubuntu: apt install libsmbclient
  • Linux on x64 or arm64 (matches the koffi and libsmbclient distribution model)

Library usage

import { SMB } from "@openinc/smb-client";

const smb = new SMB({
  host: "fileserver.example.com",
  share: "data",
  username: "svc-reports",
  password: process.env.SMB_PASS,
  domain: "CORP",        // optional workgroup / AD domain
  maxProtocol: "SMB3",   // "NT1" (SMB1) | "SMB2" | "SMB3"
  encrypt: true,         // require SMB3 encryption
});

// Whole-file helpers
await smb.writeFile("reports/2026-07.csv", Buffer.from("a;b;c\n"));
const buf = await smb.readFile("reports/2026-07.csv");

// Metadata
const st = await smb.stat("reports/2026-07.csv"); // { size, mtime, isDirectory }
if (await smb.exists("reports/2026-07.csv")) { /* ... */ }
const entries = await smb.readdir("reports");      // [{ name, type, size, mtime }, ...]

// Mutations
await smb.mkdir("reports/archive");                // recursive
await smb.rename("reports/2026-07.csv", "reports/archive/2026-07.csv");
await smb.unlink("reports/archive/2026-07.csv");
await smb.rmdir("reports/archive");

// Streaming (large files stay off the event loop; I/O runs on the libuv threadpool)
import { pipeline } from "node:stream/promises";
await pipeline(fs.createReadStream("big.bin"), smb.createWriteStream("uploads/big.bin"));
await pipeline(smb.createReadStream("uploads/big.bin"), fs.createWriteStream("copy.bin"));

Constructor options

| Option | Default | Description | |---|---|---| | host | — (required) | SMB server hostname or IP | | share | — (required) | Share name | | username | "" | User for authentication | | password | "" | Password | | domain | "" | Workgroup / AD domain | | port | 445 | SMB port | | minProtocol | "SMB2" | Lowest allowed dialect ("NT1" = SMB1/CIFS) | | maxProtocol | "SMB3" | Highest allowed dialect | | encrypt | false | Require SMB3 encryption | | useKerberos | false | Authenticate with Kerberos (with NTLM fallback) | | chunkSize | 131072 | Read/write chunk size in bytes | | debug | 0 | libsmbclient debug verbosity (0–10) |

Errors

Every method rejects with an SmbError exposing code (Node-style, e.g. ENOENT, EACCES, ECONNREFUSED), errno, and path. Metadata operations report an accurate code; for bulk read/write a mid-transfer failure falls back to a generic EIO (see Design notes).

Method reference

| Method | Returns | Notes | |---|---|---| | readFile(path) | Buffer | Reads the whole file | | writeFile(path, data, { mode }) | void | Truncates existing content | | createReadStream(path) | Readable | | | createWriteStream(path, { mode }) | Writable | Creates/truncates the file | | stat(path) | { size, mtime, isDirectory } | Throws if missing | | exists(path) | boolean | | | readdir(path, { stat }) | [{ name, type, size, mtime }] | stat: false skips per-entry stat | | mkdir(path) | void | Recursive | | rmdir(path) | void | Directory must be empty | | unlink(path) | void | | | rename(from, to) | void | Same share | | disconnect() | void | No-op (shared context); see SMB.shutdown() | | SMB.shutdown() | Promise | Frees the process-wide libsmbclient context |

HTTP gateway (standalone container)

The container in this directory runs a zero-dependency node:http server that fronts a single share.

| Method & route | Action | |---|---| | GET /healthz | Liveness probe | | GET /list/<path> | JSON directory listing | | HEAD /files/<path> | Stat (Content-Length, Last-Modified) | | GET /files/<path> | Download | | PUT /files/<path> | Upload (request body → file) | | DELETE /files/<path> | Delete |

Configuration is via environment variables:

| Variable | Default | Description | |---|---|---| | SMB_HOST | — (required) | SMB server host/IP | | SMB_SHARE | — (required) | Share name | | SMB_USER / SMB_PASS | "" | Credentials | | SMB_DOMAIN | "" | Workgroup / AD domain | | SMB_PORT | 445 | SMB port | | SMB_MIN_PROTOCOL / SMB_MAX_PROTOCOL | SMB2 / SMB3 | Dialect range | | SMB_ENCRYPT | — | 1/true to require SMB3 encryption | | PORT | 8080 | HTTP listen port | | HOST | 0.0.0.0 | HTTP bind address | | API_TOKEN | — | If set, require Authorization: Bearer <token> | | ACCESS_LOG | 1 | Log one line per request (method, path, status, bytes, duration); set 0 to disable | | STARTUP_CHECK | 1 | Probe the share once at boot and log whether it is reachable; set 0 to disable | | STARTUP_CHECK_FAIL_FAST | — | 1/true to exit the container if the share is unreachable at boot (otherwise it warns and keeps serving) | | ROUTE_DESTINATION | — | dhi-bootstrap: subnet to route via ROUTE_GATEWAY (e.g. a VPN-only SMB subnet). Route setup is skipped if unset | | ROUTE_GATEWAY | — | dhi-bootstrap: gateway IP for ROUTE_DESTINATION (e.g. the vpn-client's IP) | | DROP_TO_USER | node | dhi-bootstrap: user to drop privileges to before starting the server |

On startup the gateway logs a one-line diagnosis, e.g.:

[smb-client] Share smb://fileserver/data reachable (7 entries at root, 41ms).
[smb-client] Share smb://fileserver/data NOT reachable [ECONNREFUSED]: smbc_opendir failed: Connection refused (ECONNREFUSED)

Run it

docker build -t smb-client-gateway .

docker run --rm -p 8080:8080 \
  -e SMB_HOST=fileserver -e SMB_SHARE=data \
  -e SMB_USER=svc -e SMB_PASS=secret \
  smb-client-gateway

curl -T ./report.csv http://localhost:8080/files/reports/report.csv   # upload
curl http://localhost:8080/files/reports/report.csv                    # download
curl http://localhost:8080/list/reports                                # listing
curl -X DELETE http://localhost:8080/files/reports/report.csv          # delete

docker-compose

Minimal service definition to run the gateway in front of an existing SMB server and let other containers reach it over HTTP:

services:
  smb-gateway:
    image: openinc/smb-client-gateway:latest   # or: build: ./smb-client
    restart: unless-stopped
    environment:
      SMB_HOST: fileserver.example.com   # required — SMB server host/IP
      SMB_SHARE: data                    # required — share name
      SMB_USER: svc-reports
      SMB_PASS: ${SMB_PASS}              # from .env / your secret store, not hard-coded
      SMB_DOMAIN: CORP                   # optional workgroup / AD domain
      SMB_MAX_PROTOCOL: SMB3
      SMB_ENCRYPT: "1"                   # require SMB3 encryption
      API_TOKEN: ${GATEWAY_TOKEN}        # optional: require Bearer auth on the HTTP API
      PORT: "8080"
    ports:
      - "8080:8080"                       # drop this if only other compose services need it

  # Any other service talks to the share over HTTP via the compose network:
  my-app:
    image: my-app
    depends_on: [smb-gateway]
    environment:
      SMB_GATEWAY_URL: http://smb-gateway:8080

What each part is for:

  • image: / build: — use the published image, or build: ./smb-client to build locally.
  • environment: — the SMB_* variables configure the target share (see the table above); PORT/HOST/API_TOKEN configure the HTTP side. Keep SMB_PASS and API_TOKEN in a .env file or Docker/Swarm secrets rather than in the compose file.
  • ports: — only needed to reach the gateway from the host. Other services in the same compose project reach it as http://smb-gateway:8080 without any published port.
  • depends_on: — start ordering; the gateway itself connects to SMB lazily on the first request, so it starts fine even if the SMB server is not up yet.

Note: the server process runs as the unprivileged node user. Unless you need a VPN route (below), the SMB client needs no extra Linux capabilities — unlike an SMB server (port 445).

A full local stack (gateway + a throwaway Samba server for testing) is in docker-compose.example.yml.

Reaching a share that is only accessible over a VPN

When the SMB share sits behind a VPN reached by a vpn-client container, do not join that container's network stack with network_mode: "service:vpn-client" — Docker then refuses to publish the HTTP port:

Error response from daemon: conflicting options: port publishing and the container type network mode

Instead, keep the gateway on its own network and add a route to the SMB subnet via the vpn-client, exactly as the Parse server does. The image bundles dhi-bootstrap: on startup it runs as root, adds the route (ip route replace <ROUTE_DESTINATION> via <ROUTE_GATEWAY>), then drops to node before starting the server. Port publishing keeps working because the container is not sharing the vpn-client's netns.

services:
  smb-gateway:
    image: openinc/smb-client-gateway:latest
    cap_add: [NET_ADMIN]            # required for `ip route`
    environment:
      ROUTE_DESTINATION: 10.8.0.0/16 # SMB subnet (same as the Parse server uses)
      ROUTE_GATEWAY: 172.28.0.2      # the vpn-client's IP on the shared network
      DROP_TO_USER: node
      SMB_HOST: 10.8.0.10
      SMB_SHARE: data
      SMB_USER: svc
      SMB_PASS: ${SMB_PASS}
    ports:
      - "8080:8080"                  # works — no network_mode conflict
    networks: [vpn-network]

Give the vpn-client a static IP (ipv4_address) on the shared network and use it as ROUTE_GATEWAY. ROUTE_DESTINATION is the same subnet your Parse server already routes. When both are unset the route step is skipped, so the same image runs unchanged without a VPN. A full example (vpn-client + gateway) is in docker-compose.vpn.example.yml.

Docker Hardened Image

Hardened base images (dhi.io) have no package manager, so libsmbclient and its dependency chain must be copied in from a provider stage — the same pattern dhi-bootstrap uses for iproute2. libsmbclient additionally dlopens modules from /usr/lib/samba at runtime, so that directory is copied wholesale in addition to the ldd closure.

# Provider stage: resolve libsmbclient + its full dependency closure
FROM alpine:3.23 AS smb-libs
RUN apk add --no-cache libsmbclient
RUN mkdir -p /libs && \
    cp -aL /usr/lib/libsmbclient.so* /libs/ && \
    for so in $(ldd /usr/lib/libsmbclient.so.0 | awk '/=>/ {print $3}' | grep '^/'); do \
      cp -aL "$so" /libs/; \
    done

# Build stage: compile the TypeScript sources to dist/
FROM node:26-alpine3.24 AS build
RUN npm install -g [email protected]
WORKDIR /usr/src/app
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN pnpm install --frozen-lockfile
COPY tsconfig.json ./
COPY src ./src
RUN pnpm build

# Your hardened application image
FROM dhi.io/node:26-alpine3.24 AS app
WORKDIR /usr/src/app

# libsmbclient + dependency closure, plus the runtime-loaded samba modules
COPY --from=smb-libs /libs/ /usr/lib/
COPY --from=smb-libs /usr/lib/samba/ /usr/lib/samba/

COPY --chown=node:node package.json ./
RUN npm install --omit=dev
COPY --chown=node:node --from=build /usr/src/app/dist ./dist

USER node
CMD ["node", "dist/server.js"]

Verify the library chain in the provider stage with ldd /usr/lib/libsmbclient.so.0. If a dlopened module is missing at runtime you will see an SMB auth or negotiation failure — copying all of /usr/lib/samba (as above) covers the modules ldd does not report.

To embed the client as a library in a hardened image instead of the gateway, copy the same libs and import { SMB } from "@openinc/smb-client" from your own entrypoint.

Design notes

  • One process-wide libsmbclient context. Credentials are carried per-operation inside the smb:// URL, so multiple SMB instances with different credentials coexist. Protocol and encryption options, however, are set on the shared context: the first instance to initialize wins. Use separate processes if you need conflicting protocol settings.
  • Non-blocking I/O. libsmbclient is synchronous; koffi runs the calls on the libuv threadpool (UV_THREADPOOL_SIZE tunes concurrency, default 4). The event loop is never blocked by transfers.
  • errno handling. Failure is always detected via return values (reliable across threads). libsmbclient leaves a stale errno even after success, and errno set on a worker thread is not visible on the main thread — so on failure the call is re-run synchronously on the main thread to recover a trustworthy errno (and to absorb transient async failures). Bulk read/write skip this and report a generic EIO on the rare mid-transfer error.
  • Platform scope. Linux x64/arm64 containers. struct stat and smbc_dirent are decoded at fixed LP64 field offsets that are identical across musl and glibc on those architectures.

License

MIT © open.INC GmbH