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

pcloud-kit

v0.3.0

Published

Unofficial pCloud SDK

Readme

pcloud-kit

Unofficial, zero-dependency, type-safe pCloud SDK for modern JavaScript runtimes.

A modern alternative to pcloud-sdk-js with full TypeScript support, and no runtime dependencies.

Why

| | pcloud-sdk-js | pcloud-kit | | -------------- | -------------------------------------------- | --------------------------------------------- | | Runtime deps | isomorphic-fetch, form-data, invariant | none | | TypeScript | untyped JS | strict, exactOptionalPropertyTypes | | Module format | CJS | ESM only, sideEffects: false | | Progress | XHR events | stream-based byte counts | | Errors | raw rejected JSON | typed PcloudApiError / PcloudNetworkError | | API style | callback or promise | promise only | | Environments | Node + browser | Node + browser (platform-neutral build) | | EU/US failover | manual | automatic on 5xx |

Requirements

  • Node >=22.18.0 — relies on global fetch, Writable.toWeb, openAsBlob, Array.prototype.toSorted
  • Browser — any modern browser with fetch, FormData, and ReadableStream

Install

npm install pcloud-kit
# pnpm add pcloud-kit / yarn add pcloud-kit / bun add pcloud-kit

Quick start

import { createClient } from 'pcloud-kit'

const client = createClient({ token: process.env.PCLOUD_TOKEN! })

const root = await client.listfolder(0)
for (const entry of root.contents ?? []) {
	// TypeScript narrows entry to FolderMetadata | FileMetadata via isfolder
	console.log(entry.isfolder ? '[dir]' : '[file]', entry.name)
}

Authentication

OAuth — server-side code flow (Node)

import { buildAuthorizeUrl, getTokenFromCode } from 'pcloud-kit/oauth'

// Step 1: redirect the user to pCloud's consent screen
const url = buildAuthorizeUrl({
	clientId: process.env.PCLOUD_CLIENT_ID!,
	redirectUri: 'https://example.com/callback',
	responseType: 'code',
})

// Step 2: in your callback handler, exchange the code for an access token
const { access_token, locationid } = await getTokenFromCode(
	code,
	process.env.PCLOUD_CLIENT_ID!,
	process.env.PCLOUD_APP_SECRET!,
)

const client = createClient({ token: access_token })

OAuth — browser popup flow

import { initOauthToken, popup } from 'pcloud-kit/oauth-browser'

// On the main page: opens a popup and wires the callback
initOauthToken({
	clientId: 'YOUR_CLIENT_ID',
	redirectUri: 'https://example.com/oauth-callback',
	receiveToken: (token, locationid) => {
		const client = createClient({ token })
	},
})

// On the redirectUri page: reads the token from the URL and passes it back
popup()

For environments where a redirect URI is impractical, initOauthPollToken uses the poll_token response type and simultaneously polls both EU and US servers:

import { initOauthPollToken } from 'pcloud-kit/oauth-browser'

initOauthPollToken({
	clientId: 'YOUR_CLIENT_ID',
	receiveToken: (token, locationid) => {
		/* … */
	},
	onError: (err) => {
		/* … */
	},
})

Username/password session token

If you have a session token from userinfo (the auth field), use type: 'pcloud' — this switches the auth header from access_token= to auth=:

const client = createClient({ token: sessionToken, type: 'pcloud' })

type defaults to 'oauth'.

Convenience methods

These are fully typed and bound directly on the client:

| Method | Signature | Returns | | ------------------ | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | userinfo | () | Promise<UserInfo> | | listfolder | (folderid?: number, options?: ListFolderOptions) | Promise<FolderMetadata> | | createfolder | (name: string, parentfolderid?: number) | Promise<FolderMetadata> | | deletefile | (fileid: number) | Promise<FileMetadata> | | deletefolder | (folderid: number, recursive?: boolean) | Promise<FolderMetadata> | | movefile | (fileid: number, tofolderid: number) | Promise<FileMetadata> | | movefolder | (folderid: number, tofolderid: number) | Promise<FolderMetadata> | | renamefile | (fileid: number, toname: string) | Promise<FileMetadata> | | renamefolder | (folderid: number, toname: string) | Promise<FolderMetadata> | | getfilelink | (fileid: number) | Promise<string> | | getthumblink | (fileid: number, options?: ThumbOptions) | Promise<string> | | sharefolder | (folderid: number, mail: string, permissions: 'view' \| 'edit', message?: string) | Promise<ShareInfo> | | appshare | (folderid: number, userid: number, clientid: string) | Promise<AppShareInfo> | | login | (params?: Record<string, string>) | Promise<UserInfo> | | register | (email: string, password: string, options?: RegisterOptions) | Promise<number> | | upload | (source: string \| Blob \| File, folderid?: number, options?: UploadOptions) | Promise<{ metadata: FileMetadata; checksums: Checksums }> | | download | (url: string, options?: DownloadOptions) | Promise<ReadableStream<Uint8Array>> | | downloadfile | (fileid: number, destination: string \| WritableStream<Uint8Array>, options?: DownloadOptions) | Promise<FileLocal> | | remoteupload | (url: string, folderid?: number, options?: RemoteUploadOptions) | Promise<{ metadata: FileMetadata }> | | getthumbsfileids | (fileids: number[], receiveThumb: (thumb: ThumbResult) => void, options?: ThumbOptions) | Promise<ThumbResult[]> | | getthumbslinks | (fileids: number[], options?: ThumbOptions) | Promise<ThumbResult[]> |

Uploads

// From a Blob or File (browser or Node)
const { metadata, checksums } = await client.upload(new Blob(['hello']), folderId, {
	onBegin: () => console.log('starting'),
	onProgress: ({ loaded, total }) => console.log(loaded, '/', total),
	onFinish: () => console.log('done'),
})

// From a filesystem path (Node only)
await client.upload('/path/to/video.mp4', folderId)

// Remote URL — pCloud fetches it server-side, progress is polled automatically
await client.remoteupload('https://example.com/archive.zip', folderId, {
	onProgress: ({ all }) => console.log(all.downloaded, '/', all.size),
})

Chunked/resumable upload primitives (upload_create, upload_write, upload_save) are available via client.call() — there is no high-level wrapper yet.

Downloads and streaming

// Download a file to disk (Node)
const { path, bytes } = await client.downloadfile(fileId, '/tmp/out.bin')

// Download to any WritableStream (browser or Node)
await client.downloadfile(fileId, writableStream, {
	onProgress: ({ loaded, total }) => {
		/* … */
	},
})

// Get a raw ReadableStream to pipe or consume yourself
const url = await client.getfilelink(fileId)
const stream = await client.download(url)

Error handling

import { PcloudApiError, PcloudNetworkError } from 'pcloud-kit'

try {
	await client.listfolder(0)
} catch (err) {
	if (err instanceof PcloudApiError) {
		// pCloud returned a non-zero result code.
		// `err.params` echoes the method's input params with
		// known secret/sensitive keys stripped, so it's safe to log.
		console.error(err.result, err.method, err.params)
	} else if (err instanceof PcloudNetworkError) {
		// fetch itself failed (timeout, DNS, etc.). The underlying fetch URL
		// may include the auth token as a query param, so `err.message` and
		// `err.cause.message` are scrubbed: values of known secret keys are
		// replaced with `***`. `err.cause` is a plain `{ name, message }`
		// object (not the raw fetch error) to prevent incidental leaks.
		console.error(err.status, err.cause)
	}
}

The error classes can also be imported from pcloud-kit/errors to avoid pulling in the full client.

Logging and proxies

pCloud's HTTP/JSON API requires authentication parameters to travel as query-string values: ?access_token=, ?auth=, and — for register / login?password=. The SDK keeps those values out of its own thrown errors (see Error handling), but anything sitting between your app and *.pcloud.com will see the full URL.

If you operate or pass through any of the following, disable query-string capture (or scrub these keys) so tokens and passwords don't end up in stored logs:

  • reverse proxies / load balancers (nginx $request, Envoy access logs, ALB/CloudFront request logs)
  • APM and tracing (OpenTelemetry HTTP spans, Datadog/New Relic/Sentry breadcrumbs that record outbound URLs)
  • CI logs that print fetch URLs on failure

The server-side OAuth code exchange (getTokenFromCode) is exempt: client_id, code, and client_secret travel in the POST body (per RFC 6749 §3.2), so they don't appear in URLs.

Low-level client.call() — 160+ endpoints

The convenience methods cover the most common operations. For everything else, call() is constrained to the PcloudMethodName literal union:

// Typed against ~160 known pCloud method names
const stat = await client.call<{ metadata: FileMetadata }>('stat', { fileid: 12345 })

// Escape hatch for unlisted or future methods
const res = await client.callRaw('some_new_method', { param: 'value' })

call() also accepts an AbortSignal:

const controller = new AbortController()
await client.call('stat', { fileid: 12345 }, { signal: controller.signal })

See the pCloud HTTP API docs for the full method list and parameters.

Advanced client options

const client = createClient({
	token,
	type: 'oauth', // 'oauth' (default) | 'pcloud'
	apiServer: 'eapi.pcloud.com', // default EU server; 'api.pcloud.com' for US
	useProxy: true, // auto-detect nearest server via getapiserver on init
	coalesceReads: false, // disable in-flight deduplication of identical GET reads
})

// Swap the token on an existing client (e.g. after token refresh, or
// when reusing a long-lived client across multi-tenant requests).
// Resets the in-flight read coalesce cache so reads under the old
// token can't be served to a caller that now holds a different token.
client.setToken(newToken)

// Re-detect the nearest server
const server = await client.setupProxy()

The client automatically falls back from the EU to the US server (api.pcloud.com) on a 5xx or connection failure — no configuration needed.

Migrating from pcloud-sdk-js

Client creation

// Before
import pCloudSdk from 'pcloud-sdk-js'
const client = pCloudSdk.createClient(token)

// After
import { createClient } from 'pcloud-kit'
const client = createClient({ token })

Listing a folder

The method name and promise shape are identical. You now get discriminated union types for free:

const folder = await client.listfolder(0, { recursive: true })

for (const entry of folder.contents ?? []) {
	if (entry.isfolder) {
		entry.folderid // FolderMetadata — TypeScript knows this
	} else {
		entry.fileid // FileMetadata — TypeScript knows this
	}
}

Upload progress

// Before — XHR ProgressEvent
client.upload(file, folderId, { onProgress: (event) => event.loaded / event.total })

// After — stream-based byte counts
client.upload(file, folderId, {
	onProgress: ({ loaded, total }) => loaded / (total ?? Infinity),
})

Error handling

// Before — raw rejected JSON
.catch((err) => {
  if (err.result === 2000) { /* invalid token */ }
})

// After — typed error classes
.catch((err) => {
  if (err instanceof PcloudApiError && err.result === 2000) { /* invalid token */ }
})

Gaps

  • Chunked/resumable uploads — no high-level wrapper; use client.call('upload_create', …) etc. directly.
  • Upload progress on file paths (Node) — progress reflects bytes passing through the stream tee, not HTTP body-upload confirmation events.

Development

pnpm install
pnpm exec playwright install chromium   # one-time, for browser tests
pnpm test       # vitest — runs all three projects (see below)
pnpm build      # tsdown (ESM + .d.ts)

The project uses tsdown for bundling, vitest for testing, oxlint for linting, and oxfmt for formatting.

Test layout

pnpm test runs three Vitest projects:

| Project | Location | Environment | Mocking | | ------------------ | ---------------------------------------- | --------------------- | ------------------ | | unit | test/unit/**/*.test.ts | Node | fetch stub | | scenario-node | examples/node/**/*.scenario.test.ts | Node | MSW (msw/node) | | scenario-browser | examples/browser/**/*.scenario.test.ts | Chromium (Playwright) | MSW service worker |

Filter to one project with the matching script:

pnpm test:unit
pnpm test:scenario-node
pnpm test:browser

The unit tests stub fetch directly to assert URL shapes and error handling; the scenario tests drive the SDK end-to-end against MSW handlers shared between Node and the browser.

Examples

examples/ holds runnable scenarios that double as integration tests. They share fixtures and MSW handlers so the same flows are exercised from both Node and the browser.

examples/
  shared/      fixtures, MSW handler factory, scenario runner
  node/        Vitest tests + a tsx-runnable manual script
  browser/     Vitest browser-mode tests + a Vite + React demo page

Manual scenario — Node

Run the same flows the scenario-node tests cover from the command line:

pnpm scenario:node          # MSW-mocked, no network
pnpm scenario:node:real     # hits the real pCloud API (see env vars below)

For --real mode, set:

| Var | Required | Purpose | | ------------------- | -------- | -------------------------------------------------------- | | PCLOUD_TOKEN | yes | OAuth access token (sent as ?access_token=) | | PCLOUD_AUTH_TOKEN | optional | Session token (sent as ?auth=); skipped if unset | | PCLOUD_CLIENT_ID | optional | OAuth app client id; required for the code-exchange step | | PCLOUD_APP_SECRET | optional | OAuth app secret; required for the code-exchange step | | PCLOUD_CODE | optional | Authorization code; required for the code-exchange step |

Steps that lack their required env vars are skipped — you don't have to provide an app secret just to validate listfolder.

Manual scenario — browser

pnpm scenario:browser         # MSW-mocked, no network
pnpm scenario:browser:real    # hits the real pCloud API

Both commands open http://localhost:5173. A small React + Vite page with one button per flow (oauth listfolder, pcloud-mode listfolder, OAuth poll-token, error path).

In MSW mode the inputs are pre-filled with fixture tokens and every flow works against the mocked handlers.

In :real mode the worker is not started and requests go to the live API. Paste a real token into the input(s) — ?access_token= for the oauth-mode button, ?auth= for the pcloud-mode button. The OAuth poll-token and error-path buttons are disabled because they depend on MSW (the OAuth poll button uses a fake client_id and intercepts window.open; the error button overrides the worker handlers).

License

MIT