@shayc/open-board-format 1.3.2 → 1.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +223 -204
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -2,28 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@shayc/open-board-format)
|
|
4
4
|
[](https://github.com/shayc/open-board-format/actions/workflows/ci.yml)
|
|
5
|
-
[](LICENSE)
|
|
6
5
|
|
|
7
|
-
|
|
6
|
+
Parse, validate, and create [Open Board Format](https://www.openboardformat.org/) (OBF) communication boards (`.obf`) and archives (`.obz`) for augmentative and alternative communication (AAC) applications in TypeScript or JavaScript.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
my-board.obz
|
|
11
|
-
├── manifest.json ← table of contents: root board + id-to-path maps
|
|
12
|
-
├── boards/
|
|
13
|
-
│ └── home.obf ← one JSON board per file
|
|
14
|
-
├── images/
|
|
15
|
-
│ └── dog.png
|
|
16
|
-
└── sounds/
|
|
17
|
-
└── hello.mp3
|
|
18
|
-
```
|
|
8
|
+
Add AAC board import and export without implementing schemas, manifests, or archive handling yourself.
|
|
19
9
|
|
|
20
|
-
|
|
21
|
-
- **Browser and Node.js 22+** — pure ESM, works against `File`, `Blob`, `ArrayBuffer`, or any typed-array view (e.g. Node's `Buffer`).
|
|
22
|
-
- **One entry point for either format** — `loadBoard` sniffs the bytes and tells you whether it found an `.obf` board or an `.obz` package.
|
|
23
|
-
- **Spec-faithful round trips** — unknown fields are preserved rather than stripped, so vendor extensions allowed by the OBF spec survive `parseOBF` → `stringifyOBF`.
|
|
24
|
-
- **Small footprint** — ~11 kB min+gzip including the single runtime dependency ([fflate](https://github.com/101arrowz/fflate)); Zod is a peer, and the package is tree-shakeable with no side effects.
|
|
10
|
+
The package handles format detection, validation, archive creation, and schema access so applications can focus on board experiences instead of file handling.
|
|
25
11
|
|
|
26
|
-
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Load OBF or OBZ through one byte-based format detection API.
|
|
15
|
+
- Create OBZ archives with generated manifests and validated media resources.
|
|
16
|
+
- Use exported [Zod](https://zod.dev/) schemas and inferred TypeScript types.
|
|
17
|
+
- Preserve unknown fields, including vendor extensions.
|
|
18
|
+
- Run as pure ESM in Node.js 22+ and modern browsers.
|
|
19
|
+
|
|
20
|
+
It focuses on board data and archives only. It does not render boards, play media, fetch remote resources, or resolve navigation and media references.
|
|
27
21
|
|
|
28
22
|
## Install
|
|
29
23
|
|
|
@@ -31,75 +25,77 @@ my-board.obz
|
|
|
31
25
|
npm install @shayc/open-board-format zod
|
|
32
26
|
```
|
|
33
27
|
|
|
34
|
-
`zod
|
|
35
|
-
|
|
36
|
-
ESM only — CommonJS (`require`) is not supported.
|
|
28
|
+
`zod ^4.4.3` is a required peer dependency.
|
|
37
29
|
|
|
38
30
|
## Quick start
|
|
39
31
|
|
|
40
32
|
```ts
|
|
41
33
|
import { loadBoard } from "@shayc/open-board-format";
|
|
34
|
+
import type { BinaryInput, OBFBoard } from "@shayc/open-board-format";
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
const loaded = await loadBoard(
|
|
36
|
+
export async function loadRootBoard(input: BinaryInput): Promise<OBFBoard> {
|
|
37
|
+
const loaded = await loadBoard(input);
|
|
45
38
|
|
|
46
|
-
|
|
47
|
-
console.log(loaded.board.buttons.length);
|
|
48
|
-
} else {
|
|
49
|
-
console.log(loaded.archive.rootBoard.buttons.length);
|
|
39
|
+
return loaded.format === "obf" ? loaded.board : loaded.archive.rootBoard;
|
|
50
40
|
}
|
|
51
41
|
```
|
|
52
42
|
|
|
53
|
-
`
|
|
43
|
+
`BinaryInput` accepts `File`, `Blob`, `ArrayBuffer`, and `ArrayBufferView` values, including browser files, fetched blobs, typed arrays, and Node.js `Buffer` values. `loadBoard` detects the format from the bytes, not the filename.
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
## Formats
|
|
56
46
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
import { loadBoard } from "@shayc/open-board-format";
|
|
47
|
+
- **OBF (`.obf`)** is one JSON communication board.
|
|
48
|
+
- **OBZ (`.obz`)** is a ZIP archive containing one or more boards and optional media.
|
|
60
49
|
|
|
61
|
-
|
|
50
|
+
```text
|
|
51
|
+
my-board.obz
|
|
52
|
+
├── manifest.json
|
|
53
|
+
├── boards/
|
|
54
|
+
│ └── home.obf
|
|
55
|
+
├── images/
|
|
56
|
+
│ └── dog.png
|
|
57
|
+
└── sounds/
|
|
58
|
+
└── hello.mp3
|
|
62
59
|
```
|
|
63
60
|
|
|
64
|
-
|
|
61
|
+
Every OBZ archive requires `manifest.json` at its root, even when it contains only one board.
|
|
65
62
|
|
|
66
|
-
### Which function
|
|
63
|
+
### Which function should I call?
|
|
67
64
|
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
65
|
+
- Unknown file: `loadBoard(input)`
|
|
66
|
+
- Known `.obf` file: `loadOBF(file)`
|
|
67
|
+
- Known `.obz` input: `extractOBZ(input)`
|
|
68
|
+
- Creating an archive: `createOBZ(...)`
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
See the [API reference](#api-reference) for the complete function list. Here, `File` means the Web Platform object, not a filesystem path.
|
|
73
71
|
|
|
74
|
-
|
|
75
|
-
import { loadOBZ, extractOBZ } from "@shayc/open-board-format";
|
|
72
|
+
## Examples
|
|
76
73
|
|
|
77
|
-
|
|
78
|
-
const { rootBoard, boards, resources } = await loadOBZ(file);
|
|
74
|
+
### Read an OBZ archive
|
|
79
75
|
|
|
80
|
-
|
|
81
|
-
|
|
76
|
+
```ts
|
|
77
|
+
import { extractOBZ } from "@shayc/open-board-format";
|
|
82
78
|
|
|
83
|
-
|
|
84
|
-
const guarded = await extractOBZ(buffer, {
|
|
85
|
-
limits: { maxTotalOriginalSize: 500e6 },
|
|
86
|
-
});
|
|
79
|
+
const archive = await extractOBZ(obzBytes);
|
|
87
80
|
```
|
|
88
81
|
|
|
89
|
-
|
|
82
|
+
The returned `ParsedOBZ` contains:
|
|
90
83
|
|
|
91
|
-
|
|
84
|
+
- `manifest`: the validated OBZ manifest.
|
|
85
|
+
- `rootBoard`: the board referenced by `manifest.root`.
|
|
86
|
+
- `boards`: a `Map` keyed by board ID.
|
|
87
|
+
- `resources`: a `Map` containing the raw bytes of every file entry.
|
|
92
88
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```
|
|
89
|
+
`resources` includes the manifest, board files, media, and unrelated extra files. Directory-marker entries are omitted.
|
|
90
|
+
|
|
91
|
+
For untrusted archives, configure [extraction limits](#extraction-limits).
|
|
97
92
|
|
|
98
|
-
### Create an OBZ
|
|
93
|
+
### Create an OBZ archive
|
|
99
94
|
|
|
100
|
-
Buttons reference media by ID
|
|
95
|
+
Buttons reference media by ID. Image and sound records declare archive paths, while the `resources` map supplies the bytes stored at those paths.
|
|
101
96
|
|
|
102
97
|
```ts
|
|
98
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
103
99
|
import { createOBZ } from "@shayc/open-board-format";
|
|
104
100
|
import type { OBFBoard } from "@shayc/open-board-format";
|
|
105
101
|
|
|
@@ -111,198 +107,221 @@ const board: OBFBoard = {
|
|
|
111
107
|
images: [{ id: "img-1", path: "images/hello.png" }],
|
|
112
108
|
};
|
|
113
109
|
|
|
114
|
-
const pngBytes =
|
|
110
|
+
const pngBytes = await readFile("hello.png");
|
|
115
111
|
const resources = new Map([["images/hello.png", pngBytes]]);
|
|
116
112
|
|
|
117
113
|
const blob = await createOBZ([board], "board-1", resources);
|
|
114
|
+
await writeFile("my-board.obz", new Uint8Array(await blob.arrayBuffer()));
|
|
118
115
|
```
|
|
119
116
|
|
|
120
|
-
|
|
117
|
+
`createOBZ` generates the manifest automatically, writes boards to `boards/<encoded-id>.obf`, and uses `rootBoardId` as the archive's entry board.
|
|
121
118
|
|
|
122
|
-
|
|
119
|
+
Before writing the archive, it 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.
|
|
120
|
+
|
|
121
|
+
### Validate a board
|
|
123
122
|
|
|
124
123
|
```ts
|
|
125
124
|
import { OBFBoardSchema } from "@shayc/open-board-format";
|
|
126
125
|
|
|
127
|
-
const
|
|
126
|
+
export const validateBoard = (value: unknown) =>
|
|
127
|
+
OBFBoardSchema.safeParse(value);
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
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()`.
|
|
128
131
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
## Validation behavior
|
|
133
|
+
|
|
134
|
+
Validation returns a parsed copy of the input. Known fields may be normalized during parsing:
|
|
135
|
+
|
|
136
|
+
- Numeric IDs become strings.
|
|
137
|
+
- Empty optional IDs, URLs, and email addresses become `undefined`.
|
|
138
|
+
- Unknown properties are preserved at every loose-object level, with or without an `ext_` prefix.
|
|
139
|
+
|
|
140
|
+
Structural validation checks:
|
|
141
|
+
|
|
142
|
+
- URL and email fields are syntax-checked.
|
|
143
|
+
- Grid dimensions must be integers from 1 through 100.
|
|
144
|
+
- `grid.order` must exactly match the declared row and column counts.
|
|
145
|
+
- Positioned buttons must provide `top`, `left`, `width`, and `height`, each between 0 and 1.
|
|
146
|
+
- Format versions must match `open-board-*`; they are not restricted to `open-board-0.1`.
|
|
147
|
+
- An OBZ manifest root must appear in `paths.boards`.
|
|
148
|
+
|
|
149
|
+
Validation is not a complete OBF conformance or graph-integrity check. It does not enforce:
|
|
150
|
+
|
|
151
|
+
- Unique button, image, or sound IDs.
|
|
152
|
+
- Resolution of `grid.order`, `image_id`, `sound_id`, or `load_board` references.
|
|
153
|
+
- A consistent positioning mode across every button on a board.
|
|
154
|
+
- BCP 47 locale syntax, color syntax, MIME correctness, or safe HTML.
|
|
155
|
+
- During extraction, the existence of manifest-declared media files or their agreement with board media records.
|
|
156
|
+
|
|
157
|
+
Add application-specific checks after parsing when those guarantees matter.
|
|
158
|
+
|
|
159
|
+
## API reference
|
|
160
|
+
|
|
161
|
+
### Functions
|
|
162
|
+
|
|
163
|
+
#### Board data
|
|
164
|
+
|
|
165
|
+
| Function | Returns | Behavior |
|
|
166
|
+
| --------------------- | ------------------- | ----------------------------------------------------------- |
|
|
167
|
+
| `parseOBF(json)` | `OBFBoard` | Parse JSON and validate a board; strips a leading UTF-8 BOM |
|
|
168
|
+
| `validateOBF(value)` | `OBFBoard` | Validate and normalize an unknown value |
|
|
169
|
+
| `stringifyOBF(board)` | `string` | Serialize as two-space JSON without revalidating |
|
|
170
|
+
| `loadOBF(file)` | `Promise<OBFBoard>` | Read a `File`, then parse and validate it |
|
|
171
|
+
|
|
172
|
+
#### Archives and format detection
|
|
173
|
+
|
|
174
|
+
| Function | Returns | Behavior |
|
|
175
|
+
| -------------------------------------------- | ---------------------- | ------------------------------------------------------------------- |
|
|
176
|
+
| `loadBoard(input, options?)` | `Promise<LoadedBoard>` | Detect OBF or OBZ from the bytes, then load it |
|
|
177
|
+
| `loadOBZ(file, options?)` | `Promise<ParsedOBZ>` | `File` convenience wrapper around `extractOBZ` |
|
|
178
|
+
| `extractOBZ(input, options?)` | `Promise<ParsedOBZ>` | Extract and validate the manifest and every manifest-declared board |
|
|
179
|
+
| `createOBZ(boards, rootBoardId, resources?)` | `Promise<Blob>` | Validate and package boards and resources with a generated manifest |
|
|
180
|
+
| `parseManifest(json)` | `OBFManifest` | Parse and validate manifest JSON |
|
|
181
|
+
|
|
182
|
+
#### ZIP utilities
|
|
183
|
+
|
|
184
|
+
| Function | Returns | Behavior |
|
|
185
|
+
| ------------------------- | ---------------------------------- | -------------------------------------------------------- |
|
|
186
|
+
| `isZip(buffer)` | `boolean` | Check whether an `ArrayBuffer` has a ZIP signature |
|
|
187
|
+
| `zip(entries)` | `Promise<Uint8Array>` | Compress a map of paths to `Uint8Array` or `ArrayBuffer` |
|
|
188
|
+
| `unzip(buffer, options?)` | `Promise<Map<string, Uint8Array>>` | Extract an `ArrayBuffer` and omit directory markers |
|
|
189
|
+
|
|
190
|
+
### Types and schemas
|
|
191
|
+
|
|
192
|
+
`LoadedBoard` is a discriminated union:
|
|
193
|
+
|
|
194
|
+
```ts
|
|
195
|
+
{ format: "obf", board: OBFBoard }
|
|
196
|
+
| { format: "obz", archive: ParsedOBZ }
|
|
134
197
|
```
|
|
135
198
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
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.
|
|
139
|
-
|
|
140
|
-
### OBF (single board)
|
|
141
|
-
|
|
142
|
-
| Function | Returns | Description |
|
|
143
|
-
| --------------------- | ------------------- | ------------------------------------------------------------ |
|
|
144
|
-
| `parseOBF(json)` | `OBFBoard` | Parse a JSON string into a validated `OBFBoard` |
|
|
145
|
-
| `validateOBF(data)` | `OBFBoard` | Validate an unknown object as `OBFBoard` (throws on failure) |
|
|
146
|
-
| `stringifyOBF(board)` | `string` | Serialize an `OBFBoard` to a JSON string |
|
|
147
|
-
| `loadOBF(file)` | `Promise<OBFBoard>` | Load an `OBFBoard` from a browser `File` |
|
|
148
|
-
|
|
149
|
-
### OBZ (board package)
|
|
150
|
-
|
|
151
|
-
| Function | Returns | Description |
|
|
152
|
-
| -------------------------------------------- | -------------------- | ------------------------------------------------------------------------------------------------------------- |
|
|
153
|
-
| `loadOBZ(file, options?)` | `Promise<ParsedOBZ>` | Load an OBZ package from a browser `File` |
|
|
154
|
-
| `extractOBZ(archive, options?)` | `Promise<ParsedOBZ>` | Extract boards, manifest, root board, and resources from a `File`, `Blob`, `ArrayBuffer`, or typed-array view |
|
|
155
|
-
| `createOBZ(boards, rootBoardId, resources?)` | `Promise<Blob>` | Create an OBZ package as a `Blob` |
|
|
156
|
-
| `parseManifest(json)` | `OBFManifest` | Parse a `manifest.json` string into a validated `OBFManifest` |
|
|
157
|
-
|
|
158
|
-
### Format detection
|
|
159
|
-
|
|
160
|
-
| Function | Returns | Description |
|
|
161
|
-
| ---------------------------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
162
|
-
| `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 |
|
|
163
|
-
|
|
164
|
-
### Utilities
|
|
165
|
-
|
|
166
|
-
| Function | Returns | Description |
|
|
167
|
-
| -------------------------- | ---------------------------------- | -------------------------------------------------------- |
|
|
168
|
-
| `isZip(archive)` | `boolean` | Check if an `ArrayBuffer` starts with a ZIP magic number |
|
|
169
|
-
| `zip(entries)` | `Promise<Uint8Array>` | Create a ZIP from a map of paths to buffers |
|
|
170
|
-
| `unzip(archive, options?)` | `Promise<Map<string, Uint8Array>>` | Extract a ZIP into a map of paths to `Uint8Array` |
|
|
171
|
-
|
|
172
|
-
### Types
|
|
173
|
-
|
|
174
|
-
| Type | Description |
|
|
175
|
-
| --------------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
176
|
-
| `OBFBoard` | A single communication board |
|
|
177
|
-
| `OBFGrid` | Grid layout (rows, columns, order) |
|
|
178
|
-
| `OBFButton` | A button on the board |
|
|
179
|
-
| `OBFButtonAction` | Button action (spelling or specialty) |
|
|
180
|
-
| `OBFSpellingAction` | Spelling action (e.g., `+s`) |
|
|
181
|
-
| `OBFSpecialtyAction` | Specialty action (e.g., `:clear`) |
|
|
182
|
-
| `OBFLoadBoard` | Reference to load another board |
|
|
183
|
-
| `OBFMedia` | Common media properties (base for `OBFImage` and `OBFSound`) |
|
|
184
|
-
| `OBFImage` | An image resource (extends `OBFMedia`) |
|
|
185
|
-
| `OBFSound` | A sound resource (alias of `OBFMedia`) |
|
|
186
|
-
| `OBFSymbolInfo` | Symbol set reference |
|
|
187
|
-
| `OBFManifest` | OBZ package manifest |
|
|
188
|
-
| `ParsedOBZ` | Return type of `extractOBZ` / `loadOBZ` — `{ manifest, boards, rootBoard, resources }` |
|
|
189
|
-
| `LoadedBoard` | Return type of `loadBoard` — `{ format: "obz", archive } \| { format: "obf", board }` |
|
|
190
|
-
| `BinaryInput` | Input type of `loadBoard` / `extractOBZ` — `File \| Blob \| ArrayBuffer \| ArrayBufferView` |
|
|
191
|
-
| `UnzipLimits` | Optional extraction caps — `{ maxEntrySize?, maxTotalOriginalSize?, maxEntries? }` (see [Security](#security)) |
|
|
192
|
-
| `UnzipOptions` | Options for `unzip` / `extractOBZ` / `loadOBZ` / `loadBoard` — `{ limits?: UnzipLimits }` |
|
|
193
|
-
| `OBFID` | Unique identifier (string, coerced from number) |
|
|
194
|
-
| `OBFFormatVersion` | Format version string (e.g., `open-board-0.1`) |
|
|
195
|
-
| `OBFLicense` | Licensing information |
|
|
196
|
-
| `OBFLocaleCode` | BCP 47 locale code |
|
|
197
|
-
| `OBFLocalizedStrings` | Key-value string translations |
|
|
198
|
-
| `OBFStrings` | Multi-locale string translations |
|
|
199
|
-
|
|
200
|
-
### Schemas
|
|
201
|
-
|
|
202
|
-
Every type above except `ParsedOBZ`, `LoadedBoard`, `BinaryInput`, `UnzipLimits`, and `UnzipOptions` is exported alongside a matching Zod schema with a `Schema` suffix — `OBFBoard` → `OBFBoardSchema`, `OBFManifest` → `OBFManifestSchema`, and so on. Import any of them to validate with `safeParse`/`parse` or to compose into your own schemas:
|
|
199
|
+
`ParsedOBZ` provides the validated archive contents:
|
|
203
200
|
|
|
204
201
|
```ts
|
|
205
|
-
|
|
202
|
+
interface ParsedOBZ {
|
|
203
|
+
manifest: OBFManifest;
|
|
204
|
+
boards: Map<string, OBFBoard>;
|
|
205
|
+
rootBoard: OBFBoard;
|
|
206
|
+
resources: Map<string, Uint8Array>;
|
|
207
|
+
}
|
|
206
208
|
```
|
|
207
209
|
|
|
208
|
-
|
|
210
|
+
Main exports include:
|
|
211
|
+
|
|
212
|
+
- Board, action, media, metadata, and manifest types.
|
|
213
|
+
- Matching Zod schemas, including `OBFBoardSchema` and `OBFManifestSchema`.
|
|
214
|
+
- Input and archive types: `BinaryInput`, `ParsedOBZ`, and `LoadedBoard`.
|
|
215
|
+
- Structured errors through `OBFError` and its related types.
|
|
216
|
+
|
|
217
|
+
### Errors
|
|
209
218
|
|
|
210
|
-
|
|
219
|
+
Expected parsing, validation, and archive-domain failures from the high-level APIs use `OBFError`.
|
|
220
|
+
|
|
221
|
+
Branch on `error.info.code`, not `error.message`.
|
|
211
222
|
|
|
212
223
|
```ts
|
|
213
224
|
import { loadBoard, OBFError } from "@shayc/open-board-format";
|
|
225
|
+
import type { BinaryInput } from "@shayc/open-board-format";
|
|
226
|
+
|
|
227
|
+
export async function openBoard(input: BinaryInput) {
|
|
228
|
+
try {
|
|
229
|
+
return await loadBoard(input);
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (!(error instanceof OBFError)) throw error;
|
|
214
232
|
|
|
215
|
-
|
|
216
|
-
await loadBoard(file);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
if (!(error instanceof OBFError)) throw error;
|
|
219
|
-
|
|
220
|
-
switch (error.info.code) {
|
|
221
|
-
case "missing-resource":
|
|
222
|
-
// `kind`, `mediaId`, and `path` are all typed and present here
|
|
223
|
-
console.warn(`Missing ${error.info.kind} at ${error.info.path}`);
|
|
224
|
-
break;
|
|
225
|
-
case "invalid-board":
|
|
226
|
-
// `issues` is the Zod issue list — which field failed and why
|
|
233
|
+
if (error.info.code === "invalid-board") {
|
|
227
234
|
console.error(error.info.issues);
|
|
228
|
-
|
|
229
|
-
default:
|
|
235
|
+
} else {
|
|
230
236
|
console.error(error.message);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
throw error;
|
|
231
240
|
}
|
|
232
241
|
}
|
|
233
242
|
```
|
|
234
243
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
243
|
-
|
|
|
244
|
-
|
|
|
245
|
-
|
|
|
246
|
-
|
|
|
247
|
-
|
|
|
248
|
-
|
|
|
249
|
-
|
|
|
250
|
-
|
|
|
251
|
-
|
|
|
252
|
-
|
|
|
253
|
-
|
|
|
254
|
-
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
-
|
|
262
|
-
|
|
263
|
-
|
|
244
|
+
<details>
|
|
245
|
+
<summary><strong>Error codes</strong></summary>
|
|
246
|
+
|
|
247
|
+
| Area | `info.code` | Additional fields |
|
|
248
|
+
| -------------- | ------------------- | -------------------------------------------------- |
|
|
249
|
+
| Decoding | `not-json` | `source` |
|
|
250
|
+
| Decoding | `not-zip` | — |
|
|
251
|
+
| Decoding | `unreadable-zip` | — |
|
|
252
|
+
| Limits | `archive-too-large` | `limit`, `path`, and fields for the exceeded limit |
|
|
253
|
+
| Validation | `invalid-board` | `issues`, `boardId?` |
|
|
254
|
+
| Validation | `invalid-manifest` | `issues` |
|
|
255
|
+
| OBZ extraction | `missing-manifest` | — |
|
|
256
|
+
| OBZ extraction | `missing-board` | `boardId`, `path` |
|
|
257
|
+
| OBZ extraction | `board-id-mismatch` | `path`, `declaredId`, `actualId` |
|
|
258
|
+
| OBZ creation | `unknown-root` | `rootBoardId` |
|
|
259
|
+
| OBZ creation | `duplicate-board` | `boardId` |
|
|
260
|
+
| OBZ creation | `missing-resource` | `kind`, `mediaId`, `path` |
|
|
261
|
+
| OBZ creation | `conflicting-paths` | `kind`, `mediaId`, `paths` |
|
|
262
|
+
| OBZ creation | `path-collision` | `path` |
|
|
263
|
+
| OBZ creation | `zip-failed` | — |
|
|
264
|
+
| Internal | `internal` | `detail` |
|
|
265
|
+
|
|
266
|
+
</details>
|
|
267
|
+
|
|
268
|
+
Validation failures expose the underlying `ZodError` as `error.cause` and provide its flat issue list through `error.info.issues`.
|
|
269
|
+
|
|
270
|
+
`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.
|
|
271
|
+
|
|
272
|
+
Direct schema `.parse()` calls throw `ZodError` rather than `OBFError`.
|
|
264
273
|
|
|
265
274
|
## Security
|
|
266
275
|
|
|
267
|
-
OBZ archives
|
|
276
|
+
Treat OBZ archives and their contents as untrusted input.
|
|
277
|
+
|
|
278
|
+
### Extraction limits
|
|
268
279
|
|
|
269
280
|
```ts
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
281
|
+
import { extractOBZ } from "@shayc/open-board-format";
|
|
282
|
+
import type { BinaryInput } from "@shayc/open-board-format";
|
|
283
|
+
|
|
284
|
+
export function extractUntrusted(input: BinaryInput) {
|
|
285
|
+
return extractOBZ(input, {
|
|
286
|
+
limits: {
|
|
287
|
+
// Examples only—choose limits appropriate for your application.
|
|
288
|
+
maxEntrySize: 100 * 1024 ** 2, // 100 MiB
|
|
289
|
+
maxTotalOriginalSize: 500 * 1024 ** 2, // 500 MiB
|
|
290
|
+
maxEntries: 10_000,
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
}
|
|
277
294
|
```
|
|
278
295
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
Entry paths are not sanitized — if you write extracted resources to disk, validate paths yourself first to avoid directory traversal.
|
|
296
|
+
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.
|
|
282
297
|
|
|
283
|
-
|
|
298
|
+
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.
|
|
284
299
|
|
|
285
|
-
|
|
300
|
+
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.
|
|
286
301
|
|
|
287
|
-
|
|
302
|
+
### Other boundaries
|
|
288
303
|
|
|
289
|
-
-
|
|
290
|
-
-
|
|
291
|
-
-
|
|
292
|
-
- **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.
|
|
304
|
+
- Archive entry paths are not sanitized. Validate them before writing files to disk to prevent directory traversal.
|
|
305
|
+
- `description_html` is not sanitized. Sanitize it before inserting it into the DOM.
|
|
306
|
+
- URLs and `data_url` values are validated syntactically but are never fetched.
|
|
293
307
|
|
|
294
|
-
|
|
308
|
+
Found a vulnerability? Email [shayc@outlook.com](mailto:shayc@outlook.com) rather than opening a public issue.
|
|
295
309
|
|
|
296
|
-
|
|
310
|
+
## Runtime
|
|
297
311
|
|
|
298
|
-
|
|
312
|
+
- Pure ESM for Node.js `>=22` and modern browsers; CommonJS is unsupported.
|
|
313
|
+
- Browser environments must provide `Blob`, `File`, `TextEncoder`, and `TextDecoder`.
|
|
314
|
+
- `fflate` is the only runtime dependency; `zod ^4.4.3` is a peer dependency.
|
|
315
|
+
- CI covers Node.js 22, 24, and 26. Browser engines are not currently tested in CI.
|
|
299
316
|
|
|
300
|
-
|
|
317
|
+
## Project
|
|
301
318
|
|
|
302
|
-
|
|
319
|
+
The public API follows semantic versioning. Breaking changes to exported APIs, schemas, or documented behavior ship as major releases.
|
|
303
320
|
|
|
304
|
-
-
|
|
305
|
-
- [
|
|
321
|
+
- **Changelog:** See [CHANGELOG.md](CHANGELOG.md).
|
|
322
|
+
- **Support:** [Open an issue](https://github.com/shayc/open-board-format/issues) with a minimal reproduction, package version, runtime, and bundler where applicable.
|
|
323
|
+
- **Contributing:** See [CONTRIBUTING.md](CONTRIBUTING.md) for development commands, tests, and the changeset workflow.
|
|
324
|
+
- **Specification:** See the [official OBF documentation](https://www.openboardformat.org/docs) or the included [offline mirror](docs/external/open-board-format.md).
|
|
306
325
|
|
|
307
326
|
## License
|
|
308
327
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shayc/open-board-format",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.3",
|
|
4
4
|
"description": "Parse, validate, and create Open Board Format (.obf/.obz) files — the open standard for Augmentative and Alternative Communication (AAC) boards. TypeScript, browser and Node.js.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Shay Cojocaru <shayc@outlook.com>",
|