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

stusign

v0.1.0

Published

Typed Wacom STU device control, WebHID and Web Serial capture, image transfer and signature exports.

Readme

StuSign

A TypeScript library for Wacom STU signature tablets, using WebHID or Web Serial directly in the browser. Capture pen input, send images, recall stored screens, configure the tablet, and export signatures as SVG, PNG or versioned JSON.

ESM, TypeScript declarations, tree-shakable entry points, and a tsdown build. No Wacom SDK installation or local relay is needed for supported browser connections. This is an independent project; there is no compatibility layer for older libraries.

STU-540 display, plaintext capture and stored-image restoration have been checked on physical hardware. Other model profiles and advanced workflows have software coverage only. See supported features and devices for model-specific capabilities and limitations.

Contents

Requirements

  • A supported STU tablet connected to the computer running the browser.
  • Desktop Chrome or Edge with navigator.hid or navigator.serial, on HTTPS or localhost. Feature-detect the API; Safari and Firefox are not supported browser targets.
  • HID mode for WebHID; serial mode for Web Serial. USB virtual COM uses 128000 baud; Wacom's physical RS-232 kit uses 115200.
  • Initial selection must run from a click/tap. Permissions are specific to the browser profile and origin. Close other applications or tabs holding the tablet.
  • An ESM-aware bundler such as Vite for the browser example. Node.js 22.18+ is supported for server-side core imports, recording processing and development; native Node hardware transports are not included.

Chrome's WebHID guide and Web Serial guide describe browser permissions and deployment restrictions. An RDP session needs the port to be visible to the remote browser; serial redirection alone is not a browser-compatibility guarantee. See terminal-server constraints.

Getting started

npm install stusign

The complete example below connects, clears the screen, enables tablet inking, records until Finish or Cancel, previews an SVG and closes the connection. It deliberately uses plaintext capture. Configure required encryption for protected capture.

Add these controls to your page, and load the TypeScript entry with your bundler:

<label><input id="serial" type="checkbox" /> Tablet is in USB COM mode</label>
<button id="connect">Connect and sign</button>
<button id="finish" disabled>Finish</button>
<button id="cancel" disabled>Cancel</button>
<output id="status" aria-live="polite"></output>
<img id="preview" alt="Captured signature" />
<a id="download" download="signature.svg" hidden>Download SVG</a>
<script type="module" src="/src/main.ts"></script>
import { StuDevice, type Recording, type Signature } from 'stusign';
import { createWebHidManager } from 'stusign/webhid';
import { createWebSerialManager } from 'stusign/webserial';
import { createObjectURL, toSVGBlob } from 'stusign/render';

const connect = document.querySelector<HTMLButtonElement>('#connect')!;
const finish = document.querySelector<HTMLButtonElement>('#finish')!;
const cancel = document.querySelector<HTMLButtonElement>('#cancel')!;
const serial = document.querySelector<HTMLInputElement>('#serial')!;
const status = document.querySelector<HTMLOutputElement>('#status')!;
const preview = document.querySelector<HTMLImageElement>('#preview')!;
const download = document.querySelector<HTMLAnchorElement>('#download')!;
let imageURL: ReturnType<typeof createObjectURL> | undefined;

function review(tablet: StuDevice): Promise<boolean> {
  return new Promise((resolve, reject) => {
    const settle = (accepted?: boolean, error?: unknown): void => {
      finish.disabled = cancel.disabled = true;
      finish.onclick = cancel.onclick = null;
      unsubscribe();
      if (error) reject(error);
      else resolve(accepted ?? false);
    };
    const unsubscribe = tablet.on((event) => {
      if (event.type === 'disconnect')
        settle(
          false,
          event.reason instanceof Error ? event.reason : new Error('Tablet disconnected'),
        );
      if (event.type === 'error') settle(false, event.error);
    });
    finish.onclick = () => settle(true);
    cancel.onclick = () => settle(false);
    finish.disabled = cancel.disabled = false;
  });
}

function showSignature(signature: Signature): void {
  imageURL?.dispose();
  imageURL = createObjectURL(toSVGBlob(signature, { background: '#ffffff' }));
  preview.src = download.href = imageURL.url;
  download.hidden = false;
  status.textContent = signature.complete ? 'Signature ready.' : 'Signature contains input gaps.';
}

connect.addEventListener('click', async () => {
  connect.disabled = true;
  imageURL?.dispose();
  imageURL = undefined;
  preview.removeAttribute('src');
  download.removeAttribute('href');
  download.hidden = true;
  let tablet: StuDevice | undefined;
  let recording: Recording | undefined;
  try {
    const manager = serial.checked
      ? createWebSerialManager({ baudRate: 128000 })
      : createWebHidManager();
    // Keep this call in the click handler: the browser requires a user gesture.
    const transport = await manager.requestDevice();
    if (!transport) {
      status.textContent = 'Selection cancelled.';
      return;
    }
    tablet = await StuDevice.open(transport);
    await tablet.display.clear();
    await tablet.settings.setInking(true);
    // Deliberate plaintext example. See the encryption section for required protection.
    recording = tablet.capture.create({ encryption: 'none' });
    await recording.start();
    status.textContent = 'Sign on the tablet, then choose Finish or Cancel.';
    if (await review(tablet)) {
      const signature = await recording.finish();
      if (signature.hasInk) showSignature(signature);
      else status.textContent = 'No ink captured.';
    } else {
      await recording.cancel();
      status.textContent = 'Cancelled.';
    }
  } catch (error) {
    status.textContent = error instanceof Error ? error.message : 'Capture failed.';
  } finally {
    try {
      await recording?.dispose();
    } catch (error) {
      status.textContent = error instanceof Error ? error.message : 'Capture cleanup failed.';
    }
    try {
      await tablet?.close();
    } catch (error) {
      status.textContent = error instanceof Error ? error.message : 'Connection cleanup failed.';
    }
    connect.disabled = false;
  }
});

window.addEventListener('pagehide', () => imageURL?.dispose());

The runnable basic example is also available from this checkout with pnpm demo. Check Tablet is in USB COM mode for a serial-configured STU-540. Keep the library's canonical recording separate from your preview; drawing or clearing the LCD does not erase recorded samples.

Encryption

For RSA-generation tablets such as STU-430, STU-530 and STU-540, supply a crypto provider when opening and choose encryption: 'required':

import { StuDevice, type ReportTransport } from 'stusign';
import { createRsaCryptoProvider } from 'stusign/crypto';

async function startProtectedCapture(transport: ReportTransport) {
  const tablet = await StuDevice.open(transport, {
    cryptoProvider: createRsaCryptoProvider(),
  });
  const recording = tablet.capture.create({ encryption: 'required' });
  try {
    await recording.start();
    return { tablet, recording }; // Caller finishes/cancels and closes in finally.
  } catch (error) {
    try {
      await recording.dispose();
    } finally {
      await tablet.close();
    }
    throw error;
  }
}

Required encryption fails instead of falling back to plaintext. RSA-OAEP/SHA-1 and AES are device protocol requirements. Older devices need an injected LegacyDhBackend; STU-541 needs a separate TLS backend, which this package does not provide. Hardware qualification and an independent security review of encrypted workflows remain outstanding. See security semantics.

Authorized reopening

getAuthorizedDevices() lists handles with existing permission; it never shows a chooser. Use it on page load and listen to manager.onConnection() for USB attachment. Serialize attempts, open only when idle, and ask the user to select if multiple handles match. A serial manager lists permitted serial adapters, which may include unrelated hardware: use known adapter filters or a previously selected port, rather than probing every port.

For a single known HID tablet, the connection guide includes a complete event-driven example with disposal. Use readyStabilityMs: 0 for an immediate page-refresh check; the default 500 ms stable-ready interval is useful after USB power-up. Transient busy states still wait. Never reuse a faulted StuDevice; close it and open a fresh instance. Recordings do not resume across disconnects.

Images and stored screens

import type { StuDevice } from 'stusign';
import { imageFromCanvas } from 'stusign/render';

async function showCanvas(tablet: StuDevice, canvas: HTMLCanvasElement) {
  // Canvas dimensions must equal the tablet's screenWidth and screenHeight.
  await tablet.display.writeImage(imageFromCanvas(canvas), {
    format: 'auto',
    timeoutMs: 300_000, // Full-color serial uploads can be slow.
    onProgress: ({ sent, total }) => console.log(Math.round((100 * sent) / total)),
  });
}

Supported encodings depend on the device: mono, zlib-compressed mono, RGB565 or BGR24. RGBA input is composited against white by default. Use area: { x, y, width, height } for partial uploads; image dimensions must equal the target rectangle. Upload the background before starting encrypted capture.

Store a full-screen image once, then recall it without retransferring pixels:

import { StuDevice, type ReportTransport, type RgbaImage } from 'stusign';

async function provisionWelcome(tablet: StuDevice, image: RgbaImage) {
  // Choose a slot owned by this application. Occupied slots reject by default.
  return tablet.rom.storeImage({ kind: 'slideshow', number: 10 }, image, {
    timeoutMs: 300_000,
  });
}

async function reopenWithWelcome(
  transport: ReportTransport,
  saved: Awaited<ReturnType<typeof provisionWelcome>>,
) {
  return StuDevice.open(transport, {
    startupBackground: 0x17212b,
    startupImage: { source: 'stored', slot: saved.slot, expectedHash: saved.hash },
  });
}

Keep the returned slot and hash in your application; serialize Uint8Array as a number array or hex and restore it before use. overwrite: true explicitly replaces an occupied slot. ROM operations change persistent tablet storage.

startupImage: { source: 'upload', image } transfers an image on every connection. Stored recall is faster. Both run when the application connects. The background color is volatile, so pass startupBackground to reapply it after a device restart. See stored images and connection appearance.

Tablet settings

import type { StuDevice } from 'stusign';
import { ReportId } from 'stusign/protocol';

async function configure(tablet: StuDevice) {
  await tablet.settings.setInking(true);
  if (tablet.support(ReportId.BackgroundColor24).state === 'supported') {
    await tablet.settings.setBackground(0x17212b);
    await tablet.display.clear(); // The new background is used when clearing.
  }
  if (tablet.support(ReportId.BootScreen).state === 'supported') {
    await tablet.settings.setBootScreen(false); // Persistent provisioning.
  }
}

Other settings include ink color/thickness, handwriting area, pressure thresholds, report mode/rate, rendering mode, brightness, contrast, UID and default HID/serial mode. Availability and persistence differ by model. support() indicates descriptor presence, not successful hardware qualification. See the complete API reference.

Autonomous signature, PIN, keypad and slideshow modes use tablet.modes with images provisioned through tablet.rom. Their typed interfaces are available, but advanced mode workflows still need physical qualification.

Events, exports and errors

import type { StuDevice, Signature } from 'stusign';
import { toPNGBlob } from 'stusign/render';

function observe(tablet: StuDevice) {
  return tablet.on((event) => {
    if (event.type === 'pen') console.log(event.sample.pressureNormalized);
    if (event.type === 'error') console.error(event.error.message);
    if (event.type === 'disconnect') console.info('Reconnect the tablet.');
  }); // Call the returned unsubscribe function when the view is disposed.
}

async function exportSignature(signature: Signature) {
  return {
    svg: signature.toSVG({ color: '#17212b', background: '#ffffff' }),
    png: await toPNGBlob(signature), // Browser canvas required.
    recording: signature.toJSON(), // Raw samples; device identity omitted by default.
  };
}
  • signature.hasInk distinguishes a blank recording. complete and loss report detected input gaps, not a guarantee that every physical sample arrived.
  • samples, strokes, bounds, timeline and replay() support custom rendering and analysis. Signature.fromJSON() validates versioned recordings.
  • tablet.stream({ types: ['pen'], maxBuffered: 1024, signal }) provides bounded async iteration. A slow stream consumer cannot silently truncate the canonical recording.
  • Operations accept signal and timeoutMs. A HID timeout rejects promptly but keeps the command queue occupied until its platform call settles. Serial protocol faults close the transport.
  • Catch StuError and use its code: UNSUPPORTED_BROWSER, PERMISSION_DENIED, DEVICE_BUSY, DISCONNECTED, UNSUPPORTED_FEATURE, INVALID_ARGUMENT, MALFORMED_REPORT, DEVICE_STATUS, TIMEOUT, ABORTED, ENCRYPTION, CAPTURE_OVERFLOW, INVALID_STATE, or TRANSPORT.
  • Malformed input and encryption failures fault the connection and fail the recording. Close and reconnect. Do not retry persistent writes blindly.
  • Raw signature/PIN data is sensitive. The library does not send it anywhere. Application retention, authentication and storage protection are your responsibility. Export integrity supports detached P-256 signatures with application-owned keys.

Modules and documentation

| Import | Contents | | ------------------- | -------------------------------------------------------------------------------- | | stusign | Device services, recordings, signatures, errors, shared types and model profiles | | stusign/webhid | Device chooser, authorized handles, HID descriptors and transport | | stusign/webserial | COM chooser, framed serial transport and report-size discovery | | stusign/protocol | Report catalogue, enums, codecs, image encoders, mode and ROM layouts | | stusign/capture | Recorder, immutable signature, transforms, SVG and replay | | stusign/render | Canvas, PNG/SVG blobs, RGBA extraction and disposable object URLs | | stusign/crypto | RSA/AES, injected DH adapter and detached export signing | | stusign/testing | Synthetic transport, pen fixtures and deterministic replay for consumer tests |

The core and transport declarations work without lib.dom; browser rendering and Web Crypto types are confined to optional modules. All entry points can be imported without opening hardware. The package is ESM-only.

Read the connection and usage guide, API reference, serial guide, support matrix, protocol evidence, and security guide.

Contributing

From a repository checkout:

pnpm install --frozen-lockfile
pnpm check
pnpm demo

pnpm check verifies formatting, strict types, regression tests, the tsdown build, examples, README TypeScript examples, documentation links in both the repository and npm archive, and packed consumers. pnpm hardware starts the optional guided hardware harness; see hardware qualification. pnpm test:coverage creates a local coverage report.

See CONTRIBUTING.md for development, hardware testing and publishing commands.

MIT. Wacom and STU are Wacom trademarks. StuSign is not affiliated with or endorsed by Wacom.