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

@noxius/transferit

v0.1.3

Published

TypeScript client for transfer.it — upload and download via the MEGA backend (Node + browser)

Readme

@noxius/transferit

TypeScript client for transfer.it — upload and download via the MEGA backend (Node.js and browsers).

Port of transferit-py (MIT). Not affiliated with MEGA or transfer.it.

import {
  Transferit,
  MegaAPI,
  MegaAPIError,
  streamDecrypt,
  type UploadResult,
  type DownloadResult,
  type TransferInfo,
  type TransferNode,
} from "@noxius/transferit";

Install

npm install @noxius/transferit

| Runtime | Requirements | |---------|----------------| | Node | 20+ (filesystem upload/download) | | Browser | Secure context (HTTPS or localhost) for WebSocket uploads |

A service worker is not required. It is only used if you opt into downloadBrowser() / React useTransfer so decrypted files stream into the browser download shelf. Uploads, listing, metadata, Node downloads, and in-page decrypt via streamDecrypt work without one.

Package entry points

| Import | What you get | |--------|----------------| | @noxius/transferit | Core client + helpers | | @noxius/transferit/react | Headless React hooks (react peer, optional) | | @noxius/transferit/sw | Bundled service worker script (dist/sw-download.js) |


Browser

Upload

Same upload() API — pass a File, Blob, FileList (e.g. <input webkitdirectory>), or { path, blob }[] instead of a filesystem path. No service worker.

Upload chunk encryption and integrity checks use native WebCrypto AES in browsers and Node, with keys imported once per file. This avoids the throughput limit and main-thread blocking of JavaScript AES. Environments without WebCrypto use the original implementation; ciphertext and integrity checks are identical.

const tx = new Transferit();
await tx.upload(fileInput.files![0]!);
await tx.upload(fileInput.files!); // folder picker → preserves relative paths

Download without a service worker

MEGA’s CDN only serves ciphertext. Decrypt in the page with streamDecrypt, then save however you like (Blob URL, showSaveFilePicker, etc.).

Tradeoff: building a Blob holds the whole plaintext in memory. Prefer the service worker path for large files.

import { MegaAPI, Transferit, streamDecrypt } from "@noxius/transferit";

const tx = new Transferit();
const url = "https://transfer.it/t/xxxxxxxxxxxx";
const xh = MegaAPI.parseXh(url);
const password = null; // or "hunter2"

const nodes = await tx.info(url, { password });
const file = nodes.find((n) => n.isFile)!;

const { g, s } = await tx.api.getDownloadUrl(xh, file.handle, {
  pwToken: password ? await MegaAPI.derivePassword(xh, password) : null,
});
const size = Number(s) || file.size || 0;

const plain = await streamDecrypt(String(g), file.key, size);
const blob = await new Response(plain).blob();

const a = document.createElement("a");
a.href = URL.createObjectURL(blob);
a.download = file.name || file.handle;
a.click();
URL.revokeObjectURL(a.href);

tx.close();

Download with service worker (optional)

Register the bundled worker, then use downloadBrowser() so files land in the normal download shelf (streamed decrypt — not held as a full Blob):

# copy the worker to your site root (or configure serviceWorkerUrl)
cp node_modules/@noxius/transferit/dist/sw-download.js ./public/sw-download.js
import { Transferit } from "@noxius/transferit";
// script URL resolves via: import "@noxius/transferit/sw"

const tx = new Transferit();
await tx.downloadBrowser("https://transfer.it/t/xxxxxxxxxxxx", {
  serviceWorkerUrl: "/sw-download.js",
  scope: "/",
  // password: "hunter2",
});

| Option | Type | Notes | |--------|------|-------| | password | string | For protected transfers | | serviceWorkerUrl | string | Default /sw-download.js | | scope | string | SW registration scope (default /) | | onFileStart / onFileDone | (node) => void | Per-file hooks |

Returns { xh, started, nodes } — started is how many downloads were queued; the browser download shelf finishes them.

Lower-level helpers (also exported): ensureServiceWorker, downloadViaServiceWorker, downloadNodeViaServiceWorker, triggerServiceWorkerDownload.

React (@noxius/transferit/react)

Headless hooks — bring your own UI. react is an optional peer (>=18).

useTransfer downloads through the service worker (same optional path as downloadBrowser). Upload and listing do not need a worker.

import {
  TransferitProvider,
  useUpload,
  useTransfer,
} from "@noxius/transferit/react";

function App() {
  return (
    <TransferitProvider>
      <Downloader url="https://transfer.it/t/xxxxxxxxxxxx" />
    </TransferitProvider>
  );
}

function Downloader({ url }: { url: string }) {
  const transfer = useTransfer(url);

  return (
    <button
      type="button"
      onClick={async () => {
        await transfer.begin();
        await transfer.download(); // or transfer.download(transfer.nodes[0]!)
      }}
    >
      {transfer.status}
    </button>
  );
}

| Export | Role | |--------|------| | TransferitProvider / useTransferit | Shared client | | useUpload | Upload + progress state (no SW) | | useTransfer | List + optional SW download (begin / download / refresh) |

useTransfer(url, { password?, serviceWorkerUrl?, scope? }).


Client setup

One Transferit instance owns the MEGA bt7 API client and (lazily) an anonymous ephemeral account reused across uploads.

const tx = new Transferit({
  defaultSender: "[email protected]",   // used when upload omits sender
  defaultExpiry: "7d",               // seconds or duration string
  defaultConcurrency: 8,             // WebSocket connections per file
  defaultParallel: null,             // files uploaded at once (null = auto)
});

// …use tx…

tx.close();

| Option | Type | Default | Notes | |--------|------|---------|-------| | api | MegaAPI | new instance | Inject for tests or a custom API base | | defaultSender | string \| null | null | Fallback for upload({ sender }) | | defaultExpiry | number \| string \| null | null | Fallback for upload({ expiry }) | | defaultConcurrency | number | 8 | Per-file WebSocket fan-out | | defaultParallel | number \| null | null | Concurrent files; null → max(2, poolCount) |

tx.api exposes the underlying MegaAPI for advanced calls.


upload

Upload a file or folder into a new transfer and get a share URL.

async function upload(
  source: string | File | Blob | FileList | UploadEntry[],
  opts?: UploadOptions,
): Promise<UploadResult>
  • Node: source is a filesystem path (file or directory).
  • Browser: source is a File / Blob / FileList / { path, blob }[].

Creates an ephemeral MEGA session on first write, builds a transfer container, streams AES-encrypted chunks over WebSockets, then returns https://transfer.it/t/<xh>.

Minimal

const tx = new Transferit();
const result = await tx.upload("./report.pdf");

console.log(result.url);         // https://transfer.it/t/xxxxxxxxxxxx
console.log(result.xh);          // xxxxxxxxxxxx
console.log(result.totalBytes);
console.log(result.fileCount);   // 1
tx.close();

Folder

Uploads recursively and recreates the directory tree on the transfer. Empty subfolders are preserved. Symlinks are not followed.

const result = await tx.upload("./project", {
  title: "project dump",
  exclude: [".git", "node_modules", "*.log"],
});

console.log(result.fileCount, result.folderCount);

Create link (title only)

await tx.upload("./demo.mp4", {
  title: "Q1 demo",
});

Override the uploaded filename

Use filename to rename the file stored in the transfer, for example to add a missing extension:

await tx.upload(file, { filename: "rename.mp4" });
// Node: await tx.upload("./video", { filename: "rename.mp4" });

title sets the transfer title separately. filename requires exactly one file, also works with a Blob, FileList, or entry list, and preserves the source file and any parent folders. Supply a filename, not a path. Without filename, the original naming behavior is preserved.

Send files (recipients + schedule)

sender is required whenever you set message, password, expiry, notify-on-expiry, or recipients.

await tx.upload("./deck.pdf", {
  title: "Review please",
  message: "Draft for Friday",
  sender: "[email protected]",
  recipients: ["[email protected]", "[email protected]"],
  schedule: Math.floor(Date.now() / 1000) + 3600, // unix seconds — delay invite email
  expiry: "7d",
  notifyExpiry: true, // needs expiry + sender
});

Password-protected transfer

await tx.upload("./secrets.zip", {
  title: "Confidential",
  sender: "[email protected]",
  password: "hunter2",
  expiry: "30d",
  maxDownloads: 10,
});

Progress callbacks

await tx.upload("./big.bin", {
  onStart(totalBytes, fileCount) {
    console.log(`starting ${fileCount} file(s), ${totalBytes} bytes`);
  },
  onProgress(sent, total) {
    console.log(`${((sent / total) * 100).toFixed(1)}%`);
  },
  onFileStart(fileno, filePath, size) {
    console.log(`#${fileno} ${filePath} (${size})`);
  },
  onFileProgress(fileno, filePath, sent, size) {
    // per-file progress
  },
  onFileDone(fileno, filePath, size) {
    console.log(`done #${fileno}`);
  },
});

Upload options

| Option | Type | Notes | |--------|------|-------| | title | string | Transfer title; defaults to basename of path | | filename | string | Stored filename override; requires exactly one file; cannot contain /, \, or NUL | | message | string | Landing-page message (requires sender) | | password | string | Protects the transfer (requires sender) | | sender | string | Sender email; required for most extras | | expiry | number \| string | Seconds, or duration like "7d", "2h30m", "1y" | | notifyExpiry | boolean | Expiry reminder; needs expiry > 0 and sender | | maxDownloads | number | Cap downloads (API-supported; not on web UI) | | recipients | string[] | Invite emails; one API call each (requires sender) | | schedule | number | Unix timestamp — when to send invite emails | | concurrency | number | WebSockets per file (default 8) | | parallel | number | Files uploaded at once | | exclude | string[] | Glob patterns (fnmatch-style) for folder uploads |

UploadResult

interface UploadResult {
  xh: string;          // 12-char handle
  url: string;         // https://transfer.it/t/<xh>
  title: string;
  totalBytes: number;
  fileCount: number;
  folderCount: number; // subfolders created (excludes transfer root)
  toString(): string;  // same as url
  toJSON(): object;
}
const result = await tx.upload("./a.pdf");
console.log(String(result));           // share URL
console.log(JSON.stringify(result.toJSON(), null, 2));

download (Node)

Mirror a transfer into a local directory. Recreates folder hierarchy. No session and no service worker required.

In the browser there is no filesystem outputDir — use streamDecrypt or optional downloadBrowser.

async function download(
  urlOrXh: string,
  outputDir: string,
  opts?: DownloadOptions,
): Promise<DownloadResult>

urlOrXh may be a full URL (https://transfer.it/t/…) or the bare 12-character handle.

Minimal

const dl = await tx.download(
  "https://transfer.it/t/xxxxxxxxxxxx",
  "./downloads",
);

console.log(dl.paths);       // absolute paths written (or skipped)
console.log(dl.skipped);     // already existed, left alone
console.log(dl.totalBytes);

Password-protected

await tx.download(url, "./out", {
  password: "hunter2",
});

Overwrite existing files

By default, existing files are skipped. Pass force: true to replace them.

await tx.download(url, "./out", { force: true });

Progress callbacks

await tx.download(url, "./out", {
  onStart(files, totalBytes) {
    console.log(`${files.length} files, ${totalBytes} bytes`);
  },
  onFileStart(node, outPath) {
    console.log(`↓ ${node.name} → ${outPath}`);
  },
  onFileProgress(node, done, total) {
    console.log(`${node.name}: ${done}/${total}`);
  },
  onFileDone(node, outPath) {
    console.log(`✓ ${outPath}`);
  },
  onSkip(node, outPath) {
    console.log(`skip ${outPath}`);
  },
});

Download options

| Option | Type | Notes | |--------|------|-------| | password | string | Plain password for protected transfers | | force | boolean | Overwrite existing files (default false) | | onStart | (files, totalBytes) => void | After listing, before downloads | | onFileStart | (node, outPath) => void | | | onFileProgress | (node, done, total) => void | | | onFileDone | (node, outPath) => void | | | onSkip | (node, outPath) => void | Fired when skipping an existing file |

DownloadResult

interface DownloadResult {
  xh: string;
  outputDir: string;   // absolute
  paths: string[];     // every file path (including skipped)
  skipped: string[];
  totalBytes: number;
  toJSON(): object;
}

info

List every file and folder node in a transfer. No session required.

async function info(
  urlOrXh: string,
  opts?: { password?: string | null },
): Promise<TransferNode[]>

List contents

const nodes = await tx.info("https://transfer.it/t/xxxxxxxxxxxx");

for (const n of nodes) {
  if (n.isFolder) {
    console.log(`[dir]  ${n.name}  (${n.handle})`);
  } else {
    console.log(`[file] ${n.name}  ${n.size} bytes`);
  }
}

Password-protected

const nodes = await tx.info(url, { password: "hunter2" });
const files = nodes.filter((n) => n.isFile);

Tree-ish print

const nodes = await tx.info(url);
const byHandle = new Map(nodes.map((n) => [n.handle, n]));

for (const n of nodes.filter((n) => n.isFile)) {
  const parts: string[] = [n.name ?? n.handle];
  let p = n.parent;
  while (p) {
    const parent = byHandle.get(p);
    if (!parent || !parent.parent) break; // stop at transfer root
    parts.unshift(parent.name ?? parent.handle);
    p = parent.parent;
  }
  console.log(parts.join("/") + ` (${n.size})`);
}

TransferNode

interface TransferNode {
  handle: string;
  parent: string;          // "" for transfer root
  kind: number;            // 0 = file, 1 = folder
  name: string | null;
  size: number | null;     // null for folders
  timestamp: number | null;
  key: number[];           // a32 key (for advanced decrypt)
  raw: Record<string, unknown>;
  readonly isFile: boolean;
  readonly isFolder: boolean;
  toJSON(): object;
}

metadata

Fetch transfer-level metadata (xi). No session required.

async function metadata(
  urlOrXh: string,
  opts?: { password?: string | null },
): Promise<TransferInfo>

password is accepted for API symmetry with info / download but is ignored — xi returns title, sender, size totals, and the password flag even for protected transfers (without unlocking file listing).

Read panel info

const meta = await tx.metadata("https://transfer.it/t/xxxxxxxxxxxx");

console.log(meta.title);
console.log(meta.sender);
console.log(meta.message);
console.log(meta.passwordProtected);
console.log(meta.totalBytes, meta.fileCount, meta.folderCount);
console.log(meta.url);

Gate before download

const meta = await tx.metadata(url);

if (meta.passwordProtected) {
  const password = process.env.TRANSFER_PASSWORD;
  if (!password) throw new Error("password required");
  await tx.download(url, "./out", { password });
} else {
  await tx.download(url, "./out");
}

TransferInfo

interface TransferInfo {
  xh: string;
  url: string;
  rootHandle: string | null;
  title: string | null;
  sender: string | null;
  message: string | null;
  passwordProtected: boolean;
  zipHandle: string | null;
  zipPending: boolean;
  totalBytes: number;
  fileCount: number;
  folderCount: number;
  raw: Record<string, unknown>;
  toJSON(): object;
}

Errors

API failures throw MegaAPIError. Numeric MEGA codes are on .code; short labels on .codeName.

import { MegaAPIError } from "@noxius/transferit";

try {
  await tx.info(url, { password: "wrong" });
} catch (err) {
  if (err instanceof MegaAPIError) {
    if (err.code === -14) {
      // EKEY — missing or wrong password
      console.error(err.message, err.codeName);
    } else if (err.code === -9) {
      // ENOENT — deleted / expired / bad handle
      console.error("transfer not found");
    } else {
      console.error(err.code, err.message);
    }
  } else {
    throw err;
  }
}

Common codes: -3 EAGAIN (auto-retried), -8 EEXPIRED, -9 ENOENT, -11 EACCESS, -14 EKEY, -15 ESID.


End-to-end example

import { Transferit } from "@noxius/transferit";

const tx = new Transferit({ defaultSender: "[email protected]" });

const uploaded = await tx.upload("./hello.txt", {
  title: "hello",
  expiry: "7d",
  onProgress: (sent, total) => {
    process.stdout.write(`\r${sent}/${total}`);
  },
});
console.log("\nshare:", uploaded.url);

const meta = await tx.metadata(uploaded.url);
console.log("title:", meta.title, "bytes:", meta.totalBytes);

const nodes = await tx.info(uploaded.url);
console.log(
  "files:",
  nodes.filter((n) => n.isFile).map((n) => n.name),
);

const dl = await tx.download(uploaded.url, "./out");
console.log("wrote:", dl.paths);

tx.close();

Low-level MegaAPI

Most apps only need Transferit. Use MegaAPI directly when you need individual bt7 verbs or a custom base URL.

import { MegaAPI, Transferit } from "@noxius/transferit";

const api = new MegaAPI(); // default https://bt7.api.mega.co.nz/
await api.createEphemeralSession();

const { xh, rootH } = await api.createTransfer("manual");
// …upload chunks yourself, then:
// await api.finaliseFile(rootH, token, ulKey, macs, "file.bin");
await api.closeTransfer(xh);

const xh2 = MegaAPI.parseXh("https://transfer.it/t/xxxxxxxxxxxx");
const pw = MegaAPI.derivePassword(xh2, "hunter2");

// Inject into the high-level client:
const tx = new Transferit({ api });

| Method | Purpose | |--------|---------| | MegaAPI.parseXh(urlOrXh) | Extract 12-char handle | | MegaAPI.derivePassword(xh, password) | PBKDF2 token for protected transfers | | api.createEphemeralSession() | Anonymous up + us | | api.createTransfer(name) | xn → { xh, rootH, folderKey } | | api.closeTransfer(xh) | xc | | api.deleteTransfer(xh) | Delete transfer | | api.fetchTransfer(xh, { password? }) | f listing (+ pwToken when unlocked) | | api.fetchTransferInfo(xh) | xi metadata | | api.getDownloadUrl(xh, nodeHandle, { pwToken? }) | g | | api.uploadPools() | usc WebSocket pool directory | | api.validatePassword(xh, pwToken) | Check password token | | api.setTransferAttributes(…) / setTransferRecipient(…) | Transfer extras |

Streaming decrypt helpers

| Export | Purpose | |--------|---------| | streamDecrypt(cdnUrl, keyA32, size) | Fetch CDN ciphertext → decrypted ReadableStream (browser + Node) | | createDecryptTransform(keyA32, size) | Web TransformStream for AES-CTR plaintext | | computeFolderPaths(nodes, rootHandle) | Relative paths for nested folder downloads |


Duration strings

expiry accepts integers (seconds) or human durations:

| Input | Seconds | |-------|---------| | "30s" | 30 | | "5m" | 300 | | "2h" | 7200 | | "7d" | 604800 | | "1w" | 604800 | | "1y" | 31536000 | | "2h30m" | 9000 | | "3600" | 3600 |

Valid range: 1 second … 10 years. 0 / omit = no expiry.


Development

npm install
npm test
npm run build
TRANSFERIT_LIVE=1 npm test   # live upload/download smoke

License

MIT — see LICENSE. Protocol implementation adapted from transferit-py © Adnan Ahmad.

Use of this library must comply with transfer.it and MEGA terms. Provided as-is, without warranty.