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

@pmx-trust/sdk

v1.0.1

Published

PMX TRUST SDK — universal (Node + browser) file proof submission, keccak-256 hashing, canonicalization, and evidence bundle handling

Readme

@pmx-trust/sdk

Universal TypeScript SDK for the PMX Trust platform — runs in Node.js and the browser from a single import.

Submit files for tamper-proof, blockchain-anchored proofs. The SDK hashes files locally with keccak-256, applies format-specific canonicalization (PDF metadata stripping, exc-c14n for XML, byte-level for images), and submits only the hash + metadata to the PMX API. Original files never leave your infrastructure.

Highlights

  • Universal — same PMXTrustClient works in Node, browsers, Next.js, edge runtimes
  • Zero native dependencies — pure JavaScript, no sharp, no native bindings
  • Format canonicalization — PDF, DOCX, XLSX, PPTX, XML, JPG, PNG produce byte-identical hashes regardless of metadata variation
  • Type-safe — full TypeScript types for every request/response
  • Tree-shakable — Node-only helpers (S3, file paths) split into a /node subpath
  • Small — ~40 KB ESM, ~22 KB gzipped

Install

npm install @pmx-trust/sdk
# or
pnpm add @pmx-trust/sdk
# or
yarn add @pmx-trust/sdk

Requires Node.js >=20 for server-side use. Browser builds work in any modern browser (uses fetch + Web Crypto, both standard since 2021).

Quick start

In a React app (browser)

import { PMXTrustClient } from "@pmx-trust/sdk";

const client = new PMXTrustClient({
  apiKey: import.meta.env.VITE_PMX_API_KEY,
});

async function onUpload(file: File) {
  // `submit` reads file.name, file.type, file.size automatically
  const proof = await client.submit(file);
  console.log("Proof submitted:", proof.id);

  // Poll for confirmation
  const confirmed = await client.waitForConfirmation(proof.id);
  console.log("Status:", confirmed.status);
}

In a Node.js script

import { PMXTrustClient } from "@pmx-trust/sdk";

const client = new PMXTrustClient({
  apiKey: process.env.PMX_API_KEY!,
});

// Path-based — Node only
const proof = await client.submitFile("./contract.pdf");
const confirmed = await client.waitForConfirmation(proof.id);
console.log("Confirmed at:", confirmed.confirmedAt);

The same import works in both. Path-based methods (submitFile, etc.) throw BrowserUnsupportedError if invoked from the browser; the universal submit(blob | uint8array | file) works everywhere.

Constructor options

new PMXTrustClient({ apiKey, timeout, s3Config });

| Option | Type | Default | Notes | | ---------- | ---------------- | -------- | ------------------------------------------------------- | | apiKey | string | — | Required. Format: pmx_<64-char-hex> | | timeout | number (ms) | 30_000 | Per-request timeout | | s3Config | S3ClientConfig | — | Defaults for S3 helpers (region, endpoint, credentials) |

Authentication is via the x-api-key header. The SDK sends it on every request; you don't manage headers yourself.

Internal options (environment, baseUrl, sdkVersion) exist for local dev / QA testing and are not part of the public API surface — see SDK source if you need them.

Methods

Universal — work in Node and browser

| Method | Signature | Returns | | --------------------- | ---------------------------------------------------------------------------------------------- | ----------------------------------- | | submit | (input: File \| Blob \| Uint8Array, options?: SubmitOptions) => Promise<SubmitProofResponse> | The submitted proof | | hash | (input: File \| Blob \| Uint8Array, options?: SubmitOptions) => Promise<HashResult> | Local hash, no API call | | verify | (input: File \| Blob \| Uint8Array, options?: SubmitOptions) => Promise<VerificationResult> | Verification result | | getProof | (proofId: string) => Promise<ProofDetail> | Current proof state | | verifyHash | (keccak256Hash: string) => Promise<VerificationResult> | Verify by hash only | | waitForConfirmation | (proofId: string, options?: WaitOptions) => Promise<ProofDetail> | Polls until CONFIRMED or FAILED |

SubmitOptions lets you override file metadata: { fileName?: string; mimeType?: string }. When you pass a File, the SDK auto-extracts name and type. For raw Blob or Uint8Array, pass fileName so the SDK can detect the format and apply canonicalization.

Node-only — throw BrowserUnsupportedError in the browser

| Method | Signature | | ---------------- | ----------------------------------------------------------------------------------------- | | submitFile | (filePath: string) => Promise<SubmitProofResponse> | | hashFile | (filePath: string) => Promise<HashResult> | | verifyFile | (filePath: string) => Promise<VerificationResult> | | submitStream | (stream: Readable, mimeType?: string) => Promise<SubmitProofResponse> | | submitBatch | (filePaths: string[], options?: { concurrency?: number }) => Promise<BatchSubmitResult> | | submitS3Object | (ref: S3ObjectRef) => Promise<SubmitProofResponse> | | hashS3Object | (ref: S3ObjectRef) => Promise<HashResult> | | verifyS3Object | (ref: S3ObjectRef) => Promise<VerificationResult> |

These methods exist on the same PMXTrustClient class but require Node.js. Calling them from a browser throws BrowserUnsupportedError with a message pointing at the universal alternative.

S3 helpers depend on @aws-sdk/client-s3, which is declared as an optionalDependencies entry — npm installs it alongside the SDK by default, so submitS3Object / hashS3Object / verifyS3Object work out of the box. If you installed with --no-optional (or your registry couldn't fetch it), the SDK throws at the first S3 call with a message pointing at npm install @aws-sdk/client-s3 as the fix.

Listing objects (universal — Node + browser)

listS3Objects is the one S3 method that doesn't need streaming, so it works from both Node and a browser bundle. The other S3 methods (submit/hash/verify) stay Node-only.

listS3Objects(options: S3ListOptions): Promise<S3ListResult>

Folder navigation is the default — delimiter: '/' returns files directly under prefix in objects, and sub-folders in commonPrefixes. Pass delimiter: '' for a recursive flat listing. Caller drives pagination via continuationToken (one S3 call per invocation).

Node example — uses the AWS default credential chain (AWS_* env vars, ~/.aws/credentials, IAM role):

import { PMXTrustClient } from '@pmx-trust/sdk';

const client = new PMXTrustClient({ apiKey: 'pmx_...' });

const result = await client.listS3Objects({
  bucket: 'my-bucket',
  prefix: 'contracts/',
  delimiter: '/',
});
console.log(result.commonPrefixes); // ['contracts/2025/', 'contracts/2026/']
console.log(result.objects);        // files directly under contracts/

Browser example — credentials MUST come from Cognito / STS / your backend. Never ship long-lived accessKeyId/secretAccessKey to a browser bundle:

import { PMXTrustClient } from '@pmx-trust/sdk';
import { fromCognitoIdentityPool } from '@aws-sdk/credential-providers';

const client = new PMXTrustClient({
  apiKey: 'pmx_...',
  s3Config: {
    region: 'us-east-1',
    credentials: fromCognitoIdentityPool({
      identityPoolId: 'us-east-1:xxx-xxx-xxx',
      clientConfig: { region: 'us-east-1' },
    }),
  },
});

const result = await client.listS3Objects({ bucket: 'my-bucket', delimiter: '/' });

The browser bundle includes a hard guard: calling listS3Objects in a browser environment without s3Config.credentials throws a PMXError before any AWS-SDK code loads — no network call is even prepared.

Required bucket CORS for browser usage:

[
  {
    "AllowedOrigins": ["https://your-app.example.com"],
    "AllowedMethods": ["GET"],
    "AllowedHeaders": ["*"],
    "ExposeHeaders": []
  }
]

If the bucket isn't configured for CORS the SDK wraps the resulting browser TypeError("Failed to fetch") into a friendly S3AccessError with code NetworkOrCors and a pointer at this snippet.

Pagination looplistS3Objects returns one page per call. Walk pages via the returned token:

let token: string | undefined;
do {
  const page = await client.listS3Objects({
    bucket: 'my-bucket',
    prefix: 'archive/',
    continuationToken: token,
  });
  // process page.objects, page.commonPrefixes
  token = page.nextContinuationToken ?? undefined;
} while (token);

Imports

// Universal entry — everything browser-safe + the typed Node-only methods
import {
  PMXTrustClient,
  canonicalizeBuffer,
  detectFormat,
  hashBytes,
  blobToBytes,
  CANON_VERSION,
  SDK_VERSION,
  IS_BROWSER,
  // Errors
  PMXError,
  ApiError,
  TimeoutError,
  FileValidationError,
  S3AccessError,
  BrowserUnsupportedError,
} from "@pmx-trust/sdk";

// Node-only subpath — standalone helpers that take filesystem paths or AWS refs
import {
  hashFilePath,
  hashStream,
  hashStreamWithMeta,
  hashS3Object,
  resolveFileName,
  canonicalize, // file-path version (uses fs.readFileSync)
} from "@pmx-trust/sdk/node";

The /node subpath is for callers who prefer the functional style or need direct access to lower-level helpers. The class methods on PMXTrustClient cover all the same operations.

CLI (pmx)

The package ships a pmx binary so you can submit, verify, and inspect proofs from a shell without writing code. Installing the SDK makes pmx available; npx works without a global install.

# One-off, no install
npx @pmx-trust/sdk submit ./contract.pdf

# Global install
npm install -g @pmx-trust/sdk
pmx --help

First-time setup

pmx login

Prompts for your API key and environment (production or qa), then writes them to ~/.pmxrc (mode 0600). After this every command picks the credentials up automatically — no --api-key flag needed.

pmx config get          # show active profile (apiKey + AWS secret masked)
pmx config set environment qa
pmx logout              # clear stored apiKey

Optional: S3 credentials

If you don't already have AWS CLI configured (~/.aws/credentials) and don't want to juggle AWS_* env vars, store S3 creds in ~/.pmxrc:

pmx login s3
# Prompts for: default bucket (optional), region, access key id,
# secret access key, session token (blank for long-lived IAM keys).
pmx logout s3            # clear them

Safety rule: the AWS default credential chain always wins. If AWS_ACCESS_KEY_ID is in your env, or ~/.aws/credentials exists, the CLI uses those and ignores what's in ~/.pmxrc. Stored creds are only consulted as a fallback. For SSO, IAM roles, AssumeRole chains, or anything else that rotates automatically, leave pmx login s3 alone and use the AWS chain.

With a default bucket stored, the --s3 flag accepts shorthand:

pmx submit --s3 contracts/acme.pdf            # → s3://<default-bucket>/contracts/acme.pdf
pmx submit --s3 s3://other-bucket/k.pdf       # full URI always overrides

Examples

# Single file
pmx submit ./contract.pdf

# Batch — default concurrency 5
pmx submit ./a.pdf ./b.pdf ./c.pdf --concurrency 3

# S3 — works out of the box (@aws-sdk/client-s3 ships as an
# optional dependency). Requires AWS credentials resolvable by the
# AWS SDK (env vars, ~/.aws/credentials, instance role, etc.).
pmx submit --s3 s3://my-bucket/contracts/acme.pdf

# Lineage — same id across submissions versions them
pmx submit ./contract-v1.pdf --lineage contract:acme:2026-q2
pmx submit ./contract-v2.pdf --lineage contract:acme:2026-q2

# Verify
pmx verify ./contract.pdf
pmx verify --hash 0x3a7f...
pmx verify --s3 s3://my-bucket/contracts/acme.pdf

# Fetch a proof
pmx proof <proof-id> --json

Output modes

| Flag | Behaviour | | ----------- | ---------------------------------------------------------------------- | | (default) | Coloured human summary; batch results in a table | | --json | Single JSON object (or array for batch) to stdout — machine-readable | | --quiet | Proof ids only, one per line — pipe-friendly (xargs, jq, etc.) |

Logs and progress go to stderr; results go to stdout. pmx submit ./*.pdf --json > results.json works cleanly.

Auth resolution order

For every command, pmx resolves the API key, environment, and base URL in this order — first non-empty value wins:

  1. CLI flag (--api-key, --environment, --base-url)
  2. Process env (PMX_API_KEY, PMX_ENVIRONMENT, PMX_BASE_URL)
  3. ./.env in the current directory
  4. ~/.pmxrc (active profile)

| Env var | Notes | | ----------------- | ----------------------------------------------------------- | | PMX_API_KEY | pmx_<64-hex> form preferred; raw 64-char hex is accepted | | PMX_ENVIRONMENT | production or qa | | PMX_BASE_URL | Overrides PMX_ENVIRONMENT; use for self-host or local dev |

Exit codes

| Code | Condition | | ---- | -------------------------------------------------------------------- | | 0 | Success | | 1 | Unknown / unhandled error | | 2 | FileValidationError (unsupported MIME, file too large, etc.) | | 3 | ApiError (non-2xx response from the backend) | | 4 | TimeoutError (waitForConfirmation deadline exceeded) | | 5 | S3AccessError — or @aws-sdk/client-s3 missing on a --s3 call | | 6 | Missing / invalid auth |

Supported formats

The SDK canonicalizes these formats before hashing — files with identical content produce identical hashes regardless of authoring metadata, timestamps, or compression variation:

| Format | MIME | Canonicalization | | ---------------- | ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- | | PDF | application/pdf | Strip metadata (Title, Author, Producer, Creator, dates, XMP), re-serialize via pdf-lib | | DOCX, XLSX, PPTX | application/vnd.openxmlformats-… | Zero out core.xml values, remove app.xml/custom.xml/thumbnails, exc-c14n every XML part, deterministic ZIP | | XML | application/xml | W3C Exclusive XML Canonicalization (exc-c14n) without comments | | JPEG | image/jpeg | Pure-JS byte stripping: remove APP1–APP15 (EXIF, XMP, ICC) and COM markers, keep image data | | PNG | image/png | Pure-JS byte stripping: keep critical chunks (IHDR, PLTE, IDAT, IEND), drop all ancillary chunks |

Other file types (text, archives, video, etc.) are hashed as raw bytes — submission still works; just no canonicalization step.

Errors

All errors extend PMXError. Catch by type to handle distinct failure modes:

import {
  PMXError,
  ApiError,
  TimeoutError,
  BrowserUnsupportedError,
} from "@pmx-trust/sdk";

try {
  await client.submit(file);
} catch (err) {
  if (err instanceof ApiError) {
    console.error("Backend rejected:", err.statusCode, err.responseBody);
  } else if (err instanceof TimeoutError) {
    console.error("Took longer than the configured timeout");
  } else if (err instanceof BrowserUnsupportedError) {
    console.error(`${err.method}() requires Node.js`);
  } else if (err instanceof PMXError) {
    console.error("SDK error:", err.message);
  }
}

| Error | Thrown when | | ------------------------- | --------------------------------------------------------------------- | | ApiError | Backend returned a non-2xx response | | TimeoutError | waitForConfirmation exceeded its deadline | | FileValidationError | File too large, empty, unreadable, or stream exceeded MAX_FILE_SIZE | | S3AccessError | S3 fetch failed (NoSuchKey, AccessDenied, etc.) | | BrowserUnsupportedError | Path / stream / S3 method invoked in a browser | | PMXError | Base class |

Bundling notes

When bundling for the browser (Vite, webpack 5, esbuild), you may see warnings about node:fs, node:stream, or node:path being externalized. These are harmless. The Node-only code paths are guarded by assertNode() and never execute in the browser — the imports just sit in the bundle as stubs. Modern bundlers handle this correctly.

The browser bundle has zero native dependencies. There is no sharp, no xml-crypto, no mime-db — those have all been removed or vendored in.

TypeScript

All public types are exported from the main entry. Common ones:

import type {
  PMXTrustClientOptions,
  PMXEnvironment,
  SubmitProofResponse,
  ProofDetail,
  VerificationResult,
  WaitOptions,
  BatchSubmitResult,
  HashResult,
  SupportedFormat,
  S3ObjectRef,
  S3ClientConfig,
  SubmitOptions,
} from "@pmx-trust/sdk";

HashResult is identical whether returned from a universal hash() or a Node hashFilePath() call.

Local development

Working on the SDK itself or against an unreleased version?

# Inside the SDK repo
pnpm install
pnpm build      # tsup — produces dist/index.{js,cjs,d.ts} + dist/node/index.{js,cjs,d.ts}
pnpm test       # vitest — runs both Node and jsdom suites
pnpm typecheck

To consume an unreleased build in another app, pack and install:

# In this repo
pnpm pack
# Produces pmx-trust-sdk-<version>.tgz

# In your consumer app
pnpm add /path/to/pmx-trust-sdk-<version>.tgz

License

Proprietary. See package.json for the canonical license declaration.

The XML canonicalization implementation under src/vendor/c14n/ is derived from xml-crypto (MIT) — see src/vendor/c14n/ATTRIBUTION.md for full attribution.