A TypeScript/JavaScript library for parsing, validating, and creating Open Board Format (OBF) communication boards (.obf) and archives (.obz) for AAC applications.
Add Open Board Format import and export without implementing schemas, manifests, or archive handling yourself.
- Load OBF or OBZ through one byte-based format detection API.
- Create OBZ archives with generated manifests and validated media resources.
- Use exported Zod schemas and inferred TypeScript types.
- Preserve unknown fields, including vendor extensions.
It focuses on board data and archives only. It does not render boards, play media, fetch remote resources, or resolve navigation and media references.
npm install @shayc/open-board-format zodzod ^4.4.3 is a required peer dependency.
Works in browsers and Node.js. Browser File uploads and Node.js Buffer values use the same loading API. Pure ESM; CommonJS is not supported.
import { loadBoard } from "@shayc/open-board-format";
const loaded = await loadBoard(input);loadBoard accepts a File, Blob, ArrayBuffer, or ArrayBufferView and detects the format from the bytes, not the filename. It returns a TypeScript discriminated union: OBF files contain a board directly, while OBZ files contain an archive whose rootBoard is the entry point.
const board = loaded.format === "obf" ? loaded.board : loaded.archive.rootBoard;- OBF (
.obf) is one JSON communication board. - OBZ (
.obz) is a ZIP archive containing one or more boards and optional media.
my-board.obz
├── manifest.json
├── boards/
│ └── home.obf
├── images/
│ └── dog.png
└── sounds/
└── hello.mp3
Every OBZ archive requires manifest.json at its root, even when it contains only one board.
- Unknown file:
loadBoard(input) - Known
.obffile:loadOBF(file) - Known
.obzinput:extractOBZ(input) - Creating an archive:
createOBZ(...)
See the API reference for the complete function list. Here, File means the Web Platform object, not a filesystem path.
import { extractOBZ } from "@shayc/open-board-format";
const archive = await extractOBZ(obzBytes);The returned ParsedOBZ contains:
manifest: the validated OBZ manifest.rootBoard: the board referenced bymanifest.root.boards: aMapkeyed by board ID.resources: aMapcontaining the raw bytes of every file entry.
resources includes the manifest, board files, media, and unrelated extra files. Directory-marker entries are omitted.
For untrusted archives, configure extraction limits.
Given an existing board and its media resources:
import { createOBZ } from "@shayc/open-board-format";
const blob = await createOBZ([existingBoard], existingBoard.id, resources);createOBZ generates the manifest automatically, writes boards to boards/<encoded-id>.obf, and uses rootBoardId as the archive's entry board.
import { OBFBoardSchema } from "@shayc/open-board-format";
export const validateBoard = (value: unknown) =>
OBFBoardSchema.safeParse(value);Every public OBF data model has a matching Zod schema export with a Schema suffix. The schemas can also be composed with Zod APIs such as .extend() and .pick().
Validation returns a parsed copy of the input. Known fields may be normalized during parsing:
- Numeric IDs become strings.
- Empty optional IDs, URLs, and email addresses become
undefined. - Unknown properties are preserved at every loose-object level, with or without an
ext_prefix.
Structural validation checks:
- URL and email fields are syntax-checked.
- Grid dimensions must be integers from 1 through 100.
grid.ordermust exactly match the declared row and column counts.- Positioned buttons must provide
top,left,width, andheight, each between 0 and 1. - Format versions must match
open-board-*; they are not restricted toopen-board-0.1. - An OBZ manifest root must appear in
paths.boards.
Validation is not a complete OBF conformance or graph-integrity check. It does not enforce:
- Unique button, image, or sound IDs.
- Resolution of
grid.order,image_id,sound_id, orload_boardreferences. - A consistent positioning mode across every button on a board.
- BCP 47 locale syntax, color syntax, MIME correctness, or safe HTML.
- During extraction, the existence of manifest-declared media files or their agreement with board media records.
Add application-specific checks after parsing when those guarantees matter.
| Function | Returns | Behavior |
|---|---|---|
parseOBF(json) |
OBFBoard |
Parse JSON and validate a board; strips a leading UTF-8 BOM |
validateOBF(value) |
OBFBoard |
Validate and normalize an unknown value |
stringifyOBF(board) |
string |
Serialize as two-space JSON without revalidating |
loadOBF(file) |
Promise<OBFBoard> |
Read a File, then parse and validate it |
| Function | Returns | Behavior |
|---|---|---|
loadBoard(input, options?) |
Promise<LoadedBoard> |
Detect OBF or OBZ from the bytes, then load it |
loadOBZ(file, options?) |
Promise<ParsedOBZ> |
File convenience wrapper around extractOBZ |
extractOBZ(input, options?) |
Promise<ParsedOBZ> |
Extract and validate the manifest and every manifest-declared board |
createOBZ(boards, rootBoardId, resources?) |
Promise<Blob> |
Validate and package boards and resources with a generated manifest |
parseManifest(json) |
OBFManifest |
Parse and validate manifest JSON |
Before writing an archive, createOBZ checks board IDs, the root board, generated paths, media-path conflicts, and declared media resources. It does not resolve load_board, image_id, or sound_id references.
LoadedBoard is a discriminated union:
{ format: "obf", board: OBFBoard }
| { format: "obz", archive: ParsedOBZ }ParsedOBZ provides the validated archive contents:
interface ParsedOBZ {
manifest: OBFManifest;
boards: Map<string, OBFBoard>;
rootBoard: OBFBoard;
resources: Map<string, Uint8Array>;
}Main exports include:
- Board, action, media, metadata, and manifest types.
- Matching Zod schemas, including
OBFBoardSchemaandOBFManifestSchema. - Input and archive types:
BinaryInput,ParsedOBZ, andLoadedBoard. - Structured errors through
OBFErrorand its related types.
Expected parsing, validation, and archive-domain failures from the high-level APIs use OBFError.
Branch on error.info.code, not error.message.
import { loadBoard, OBFError } from "@shayc/open-board-format";
try {
await loadBoard(file);
} catch (error) {
if (error instanceof OBFError) {
console.error(error.info.code);
}
throw error;
}Error codes
| Area | info.code |
Additional fields |
|---|---|---|
| Decoding | not-json |
source |
| Decoding | not-zip |
— |
| Decoding | unreadable-zip |
— |
| Limits | archive-too-large |
limit, path, and fields for the exceeded limit |
| Validation | invalid-board |
issues, boardId? |
| Validation | invalid-manifest |
issues |
| OBZ extraction | missing-manifest |
— |
| OBZ extraction | missing-board |
boardId, path |
| OBZ extraction | board-id-mismatch |
path, declaredId, actualId |
| OBZ creation | unknown-root |
rootBoardId |
| OBZ creation | duplicate-board |
boardId |
| OBZ creation | missing-resource |
kind, mediaId, path |
| OBZ creation | conflicting-paths |
kind, mediaId, paths |
| OBZ creation | path-collision |
path |
| OBZ creation | zip-failed |
— |
| Internal | internal |
detail |
Validation failures expose the underlying ZodError as error.cause and provide its flat issue list through error.info.issues.
not-json, unreadable-zip, and zip-failed expose the underlying parser or ZIP error as error.cause. An internal error indicates a library invariant failure and should be reported.
Direct schema .parse() calls throw ZodError rather than OBFError.
Low-level ZIP utilities
The following exports are available for advanced archive workflows:
| Function | Returns | Behavior |
|---|---|---|
isZip(buffer) |
boolean |
Check whether an ArrayBuffer has a ZIP signature |
zip(entries) |
Promise<Uint8Array> |
Compress a map of paths to Uint8Array or ArrayBuffer |
unzip(buffer, options?) |
Promise<Map<string, Uint8Array>> |
Extract an ArrayBuffer and omit directory markers |
Treat OBZ archives and their contents as untrusted input.
import { extractOBZ } from "@shayc/open-board-format";
import type { BinaryInput } from "@shayc/open-board-format";
export function extractUntrusted(input: BinaryInput) {
return extractOBZ(input, {
limits: {
// Examples only—choose limits appropriate for your application.
maxEntrySize: 100 * 1024 ** 2, // 100 MiB
maxTotalOriginalSize: 500 * 1024 ** 2, // 500 MiB
maxEntries: 10_000,
},
});
}Extraction limits are optional and disabled by default. Entry and total-size limits are checked against ZIP metadata before inflation, while maxEntries caps the number of entries processed.
These limits reduce risk, but they are not strict memory guarantees. ZIP metadata can be dishonest, and stored entries can produce more output than their declared uncompressed size.
Also enforce a limit on the compressed archive size before passing it to this package. Use process isolation or a streaming design when your threat model requires a strict memory boundary.
- Archive entry paths are not sanitized. Validate them before writing files to disk to prevent directory traversal.
description_htmlis not sanitized. Sanitize it before inserting it into the DOM.- URLs and
data_urlvalues are validated syntactically but are never fetched.
Found a vulnerability? Email shayc@outlook.com rather than opening a public issue.
- Pure ESM for Node.js
>=22and modern browsers; CommonJS is unsupported. - Browser environments must provide
Blob,File,TextEncoder, andTextDecoder. fflateis the only runtime dependency;zod ^4.4.3is a peer dependency.- CI covers Node.js 22, 24, and 26. Browser engines are not currently tested in CI.
The public API follows semantic versioning. Breaking changes to exported APIs, schemas, or documented behavior ship as major releases.
- Changelog: See CHANGELOG.md.
- Support: Open an issue with a minimal reproduction, package version, runtime, and bundler where applicable.
- Contributing: See CONTRIBUTING.md for development commands, tests, and the changeset workflow.
- Specification: See the official OBF documentation or the included offline mirror.
MIT © Shay Cojocaru