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

@shayc/open-board-format

v1.3.0

Published

A TypeScript toolkit for Open Board Format — the open standard for Augmentative and Alternative Communication (AAC) boards.

Readme

@shayc/open-board-format

npm version CI License: MIT

A TypeScript toolkit for Open Board Format — the open standard for Augmentative and Alternative Communication (AAC) boards. OBF (.obf) is a JSON file describing a single communication board: buttons, images, sounds, grid layout, metadata. OBZ (.obz) is a ZIP archive bundling one or more boards with their media and a manifest.json. This package parses, validates, and creates both.

my-board.obz
├── manifest.json        ← table of contents: root board + id-to-path maps
├── boards/
│   └── home.obf         ← one JSON board per file
├── images/
│   └── dog.png
└── sounds/
    └── hello.mp3
  • Typed end to end — every type is inferred from a Zod schema, and every schema is exported for safeParse or composing into your own contracts.
  • Browser and Node.js 22+ — pure ESM, works against File, Blob, ArrayBuffer, or any typed-array view (e.g. Node's Buffer).
  • One entry point for either formatloadBoard sniffs the bytes and tells you whether it found an .obf board or an .obz package.
  • Spec-faithful round trips — unknown fields are preserved rather than stripped, so vendor extensions allowed by the OBF spec survive parseOBFstringifyOBF.
  • Small footprint — one runtime dependency (fflate) plus Zod as a peer, tree-shakeable, no side effects.

Contents: Install · Quick start · Usage · API · Errors · Security · Scope · Versioning · Contributing · Related · License

Install

npm install @shayc/open-board-format zod

zod 4 is a peer dependency — npm 7+ installs it automatically, but pnpm and Yarn users should add it explicitly (as shown above).

ESM only — CommonJS (require) is not supported.

Quick start

import { loadBoard } from "@shayc/open-board-format";

// `file` came from drag-and-drop or <input type="file"> — could be .obf or .obz
const loaded = await loadBoard(file);

if (loaded.format === "obf") {
  console.log(loaded.board.buttons.length);
} else {
  console.log(loaded.archive.rootBoard.buttons.length);
}

loadBoard accepts a File, Blob, ArrayBuffer, or any typed-array view (e.g. a Node Buffer), and throws on invalid input (see Errors). Already holding a JSON string? parseOBF(json) returns a validated OBFBoard directly.

In Node.js, read the file first — there's no File API outside the browser, but readFile's Buffer works directly:

import { readFile } from "node:fs/promises";
import { loadBoard } from "@shayc/open-board-format";

const loaded = await loadBoard(await readFile("my-board.obz"));

Usage

Which function do I need?

  • You have a single board (OBF). Use parseOBF for a JSON string, validateOBF for an already-parsed object, loadOBF for a browser File. stringifyOBF serializes back out.
  • You have a package of boards plus media (OBZ). Use extractOBZ for a File, Blob, ArrayBuffer, or typed-array view (loadOBZ is the same thing, File-only); createOBZ to build a new one.
  • You don't know which you have. Use loadBoard — it sniffs the bytes and returns a { format, ... } union so you don't have to inspect the file extension yourself.

Read an OBZ package

import { loadOBZ, extractOBZ } from "@shayc/open-board-format";

// From a File (e.g. drag-and-drop)
const { rootBoard, boards, resources } = await loadOBZ(file);

// Or from anything else — ArrayBuffer, Blob, Node Buffer, etc.
const parsed = await extractOBZ(buffer);

// Untrusted input? Cap declared uncompressed sizes (see Security)
const guarded = await extractOBZ(buffer, {
  limits: { maxTotalOriginalSize: 500e6 },
});

rootBoard is the package's home board — the one manifest.root points at, already resolved. boards is keyed by board ID and resources by archive path; the manifest is also returned if you need the raw table of contents.

Resources are raw bytes. To display an image in the browser:

const bytes = resources.get("images/hello.png")!;
const url = URL.createObjectURL(new Blob([bytes]));

Create an OBZ package

Buttons reference media by ID (image_id, sound_id); the board's images/sounds entries carry the archive path; the resources map supplies the bytes for each path. Every path a board declares must have a matching resource entry, or createOBZ throws.

import { createOBZ } from "@shayc/open-board-format";
import type { OBFBoard } from "@shayc/open-board-format";

const board: OBFBoard = {
  format: "open-board-0.1",
  id: "board-1",
  buttons: [{ id: "btn-1", label: "Hello", image_id: "img-1" }],
  grid: { rows: 1, columns: 1, order: [["btn-1"]] },
  images: [{ id: "img-1", path: "images/hello.png" }],
};

const pngBytes = new Uint8Array(/* ... */);
const resources = new Map([["images/hello.png", pngBytes]]);

const blob = await createOBZ([board], "board-1", resources);

The manifest.json is generated for you — boards are written to boards/<id>.obf (the id is percent-encoded, so it's always a safe filename), and rootBoardId (the second argument) selects the home board.

Validate with Zod directly

import { OBFBoardSchema } from "@shayc/open-board-format";

const result = OBFBoardSchema.safeParse(data);

if (result.success) {
  console.log(result.data.buttons);
} else {
  console.error(result.error.issues);
}

API

One naming convention covers the whole surface: parse* takes a JSON string, validate* takes an already-parsed object, load* takes a browser File, stringify* returns a JSON string — and extractOBZ/loadBoard also accept a Blob, ArrayBuffer, or typed-array view (e.g. a Node Buffer), for use outside the browser.

OBF (single board)

| Function | Returns | Description | | --------------------- | ------------------- | ------------------------------------------------------------ | | parseOBF(json) | OBFBoard | Parse a JSON string into a validated OBFBoard | | validateOBF(data) | OBFBoard | Validate an unknown object as OBFBoard (throws on failure) | | stringifyOBF(board) | string | Serialize an OBFBoard to a JSON string | | loadOBF(file) | Promise<OBFBoard> | Load an OBFBoard from a browser File |

OBZ (board package)

| Function | Returns | Description | | -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------- | | loadOBZ(file, options?) | Promise<ParsedOBZ> | Load an OBZ package from a browser File | | extractOBZ(archive, options?) | Promise<ParsedOBZ> | Extract boards, manifest, root board, and resources from a File, Blob, ArrayBuffer, or typed-array view | | createOBZ(boards, rootBoardId, resources?) | Promise<Blob> | Create an OBZ package as a Blob | | parseManifest(json) | OBFManifest | Parse a manifest.json string into a validated OBFManifest |

Format detection

| Function | Returns | Description | | ---------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | | loadBoard(input, options?) | Promise<LoadedBoard> | Detect OBF vs OBZ from a File, Blob, ArrayBuffer, or typed-array view and load it; returns a LoadedBoard union |

Utilities

| Function | Returns | Description | | -------------------------- | ---------------------------------- | -------------------------------------------------------- | | isZip(archive) | boolean | Check if an ArrayBuffer starts with a ZIP magic number | | zip(entries) | Promise<Uint8Array> | Create a ZIP from a map of paths to buffers | | unzip(archive, options?) | Promise<Map<string, Uint8Array>> | Extract a ZIP into a map of paths to Uint8Array |

Types

| Type | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------- | | OBFBoard | A single communication board | | OBFGrid | Grid layout (rows, columns, order) | | OBFButton | A button on the board | | OBFButtonAction | Button action (spelling or specialty) | | OBFSpellingAction | Spelling action (e.g., +s) | | OBFSpecialtyAction | Specialty action (e.g., :clear) | | OBFLoadBoard | Reference to load another board | | OBFMedia | Common media properties (base for OBFImage and OBFSound) | | OBFImage | An image resource (extends OBFMedia) | | OBFSound | A sound resource (alias of OBFMedia) | | OBFSymbolInfo | Symbol set reference | | OBFManifest | OBZ package manifest | | ParsedOBZ | Return type of extractOBZ / loadOBZ{ manifest, boards, rootBoard, resources } | | LoadedBoard | Return type of loadBoard{ format: "obz", archive } \| { format: "obf", board } | | BinaryInput | Input type of loadBoard / extractOBZFile \| Blob \| ArrayBuffer \| ArrayBufferView | | UnzipLimits | Optional extraction caps — { maxEntrySize?, maxTotalOriginalSize?, maxEntries? } (see Security) | | UnzipOptions | Options for unzip / extractOBZ / loadOBZ / loadBoard{ limits?: UnzipLimits } | | OBFID | Unique identifier (string, coerced from number) | | OBFFormatVersion | Format version string (e.g., open-board-0.1) | | OBFLicense | Licensing information | | OBFLocaleCode | BCP 47 locale code | | OBFLocalizedStrings | Key-value string translations | | OBFStrings | Multi-locale string translations |

Schemas

Every type above except ParsedOBZ, LoadedBoard, BinaryInput, UnzipLimits, and UnzipOptions is exported alongside a matching Zod schema with a Schema suffix — OBFBoardOBFBoardSchema, OBFManifestOBFManifestSchema, and so on. Import any of them to validate with safeParse/parse or to compose into your own schemas:

import { OBFButtonSchema, OBFManifestSchema } from "@shayc/open-board-format";

Errors

Every failure throws an OBFError. Branch on error.info.code — a discriminated union where each code carries exactly the fields relevant to that failure. Don't match on error.message; the message is human-readable and may change between releases.

import { loadBoard, OBFError } from "@shayc/open-board-format";

try {
  await loadBoard(file);
} catch (error) {
  if (!(error instanceof OBFError)) throw error;

  switch (error.info.code) {
    case "missing-resource":
      // `kind`, `mediaId`, and `path` are all typed and present here
      console.warn(`Missing ${error.info.kind} at ${error.info.path}`);
      break;
    case "invalid-board":
      // `issues` is the Zod issue list — which field failed and why
      console.error(error.info.issues);
      break;
    default:
      console.error(error.message);
  }
}

The code values, grouped by what they describe:

| Group | info.code | Key fields (on info) | | ----------- | ------------------- | --------------------------------------------- | | Decoding | not-json | source | | | not-zip | — | | | unreadable-zip | — | | | archive-too-large | limit, path, and the tripped cap's fields | | Validation | invalid-board | issues, boardId? | | | invalid-manifest | issues | | Read (OBZ) | missing-manifest | — | | | missing-board | boardId, path | | | board-id-mismatch | path, declaredId, actualId | | Write (OBZ) | unknown-root | rootBoardId | | | duplicate-board | boardId | | | missing-resource | kind, mediaId, path | | | conflicting-paths | kind, mediaId, paths | | | path-collision | path | | | zip-failed | — | | Internal | internal | detail |

OBFErrorInfo and OBFErrorCode are exported for exhaustive handling.

  • The underlying error, when there is one, is always on the standard error.cause — never duplicated on info.
  • Validation failures (invalid-board, invalid-manifest) put the ZodError on error.cause, so z.treeifyError(error.cause) gives nested, UI-friendly output, while info.issues (Zod's issue type, re-exported as OBFIssue) gives you the flat list directly.
  • For not-json / *-zip failures, error.cause is the underlying parser or fflate error.
  • An internal code signals a bug in this library that callers can't recover from — please report it.

Re-zipping an extracted .obz by hand? Zip the folder's contents, not the folder itself. If manifest.json ends up nested inside a top-level folder in the archive, extraction fails with missing-manifest even though the file is right there.

Security

OBZ archives are untrusted input. To guard against zip bombs, pass limits (via UnzipOptions) to loadBoard / loadOBZ / extractOBZ / unzip:

const parsed = await extractOBZ(buffer, {
  limits: {
    maxEntrySize: 100e6, // any single entry, in bytes
    maxTotalOriginalSize: 500e6, // sum of all entries, in bytes
    maxEntries: 10_000, // entry count, including directory entries
  },
});

Size limits are checked per entry against the uncompressed size declared in the archive's ZIP metadata, before that entry is inflated; maxEntries caps how many entries are processed at all. Entries accepted before a later entry trips a limit have already been inflated, but total allocation stays bounded by the caps. Exceeding a limit aborts extraction and rejects with an OBFError whose code is archive-too-large (info carries limit, the entry path that tripped it, and maxBytes/declaredBytes for the size limits or maxEntries/entryCount for the count). No limits are applied by default. A lying header can't force larger allocations — fflate allocates output buffers at exactly the declared size — so the declared-size caps bound memory use.

Entry paths are not sanitized — if you write extracted resources to disk, validate paths yourself first to avoid directory traversal.

Found a security issue? Open a private advisory at github.com/shayc/open-board-format/security/advisories/new.

Scope

What this library deliberately does not do:

  • No network I/O — media referenced by url or data_url is not fetched; resolving external media is up to you.
  • No rendering — it parses and validates data; drawing boards and playing sounds belong to your app.
  • No default extraction limits, no path sanitization — size caps are opt-in via UnzipOptions; see Security before writing archive contents to disk.
  • No referential integrity checks — a grid.order id with no matching button, or an image_id/sound_id with no matching image/sound, is not flagged. Resolving references is up to your rendering layer.

Versioning

Semver. The public API — every exported function, type, and Zod schema — is stable; breaking changes ship as major releases. See CHANGELOG.md.

Contributing

See CONTRIBUTING.md for development setup (Node 22+, Vitest, the changeset workflow).

Related

License

MIT © Shay Cojocaru