@shayc/open-board-format 1.3.2 → 1.3.4
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 +12 -0
- package/README.md +216 -215
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @shayc/open-board-format
|
|
2
2
|
|
|
3
|
+
## 1.3.4
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- ea8b3f6: Clarify the README introduction and `loadBoard` usage example.
|
|
8
|
+
|
|
9
|
+
## 1.3.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- a58a344: Rewrite the README as a decision-first package guide with accurate API, validation, error, and security documentation.
|
|
14
|
+
|
|
3
15
|
## 1.3.2
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,28 +2,19 @@
|
|
|
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
|
-
A TypeScript
|
|
6
|
+
A TypeScript/JavaScript library for parsing, validating, and creating [Open Board Format](https://www.openboardformat.org/) (OBF) communication boards (`.obf`) and archives (`.obz`) for AAC applications.
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
├── boards/
|
|
13
|
-
│ └── home.obf ← one JSON board per file
|
|
14
|
-
├── images/
|
|
15
|
-
│ └── dog.png
|
|
16
|
-
└── sounds/
|
|
17
|
-
└── hello.mp3
|
|
18
|
-
```
|
|
8
|
+
Add Open Board Format import and export without implementing schemas, manifests, or archive handling yourself.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
19
11
|
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
-
|
|
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.
|
|
12
|
+
- Load OBF or OBZ through one byte-based format detection API.
|
|
13
|
+
- Create OBZ archives with generated manifests and validated media resources.
|
|
14
|
+
- Use exported [Zod](https://zod.dev/) schemas and inferred TypeScript types.
|
|
15
|
+
- Preserve unknown fields, including vendor extensions.
|
|
25
16
|
|
|
26
|
-
|
|
17
|
+
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
18
|
|
|
28
19
|
## Install
|
|
29
20
|
|
|
@@ -31,183 +22,179 @@ my-board.obz
|
|
|
31
22
|
npm install @shayc/open-board-format zod
|
|
32
23
|
```
|
|
33
24
|
|
|
34
|
-
`zod
|
|
25
|
+
`zod ^4.4.3` is a required peer dependency.
|
|
35
26
|
|
|
36
|
-
|
|
27
|
+
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.
|
|
37
28
|
|
|
38
29
|
## Quick start
|
|
39
30
|
|
|
40
31
|
```ts
|
|
41
32
|
import { loadBoard } from "@shayc/open-board-format";
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
|
|
34
|
+
const loaded = await loadBoard(input);
|
|
35
|
+
```
|
|
45
36
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
}
|
|
37
|
+
`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.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
const board = loaded.format === "obf" ? loaded.board : loaded.archive.rootBoard;
|
|
51
41
|
```
|
|
52
42
|
|
|
53
|
-
|
|
43
|
+
## Formats
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
- **OBF (`.obf`)** is one JSON communication board.
|
|
46
|
+
- **OBZ (`.obz`)** is a ZIP archive containing one or more boards and optional media.
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
my-board.obz
|
|
50
|
+
├── manifest.json
|
|
51
|
+
├── boards/
|
|
52
|
+
│ └── home.obf
|
|
53
|
+
├── images/
|
|
54
|
+
│ └── dog.png
|
|
55
|
+
└── sounds/
|
|
56
|
+
└── hello.mp3
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Every OBZ archive requires `manifest.json` at its root, even when it contains only one board.
|
|
60
|
+
|
|
61
|
+
### Which function should I call?
|
|
62
|
+
|
|
63
|
+
- Unknown file: `loadBoard(input)`
|
|
64
|
+
- Known `.obf` file: `loadOBF(file)`
|
|
65
|
+
- Known `.obz` input: `extractOBZ(input)`
|
|
66
|
+
- Creating an archive: `createOBZ(...)`
|
|
67
|
+
|
|
68
|
+
See the [API reference](#api-reference) for the complete function list. Here, `File` means the Web Platform object, not a filesystem path.
|
|
69
|
+
|
|
70
|
+
## Examples
|
|
71
|
+
|
|
72
|
+
### Read an OBZ archive
|
|
56
73
|
|
|
57
74
|
```ts
|
|
58
|
-
import {
|
|
59
|
-
import { loadBoard } from "@shayc/open-board-format";
|
|
75
|
+
import { extractOBZ } from "@shayc/open-board-format";
|
|
60
76
|
|
|
61
|
-
const
|
|
77
|
+
const archive = await extractOBZ(obzBytes);
|
|
62
78
|
```
|
|
63
79
|
|
|
64
|
-
|
|
80
|
+
The returned `ParsedOBZ` contains:
|
|
65
81
|
|
|
66
|
-
|
|
82
|
+
- `manifest`: the validated OBZ manifest.
|
|
83
|
+
- `rootBoard`: the board referenced by `manifest.root`.
|
|
84
|
+
- `boards`: a `Map` keyed by board ID.
|
|
85
|
+
- `resources`: a `Map` containing the raw bytes of every file entry.
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
- **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.
|
|
70
|
-
- **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.
|
|
87
|
+
`resources` includes the manifest, board files, media, and unrelated extra files. Directory-marker entries are omitted.
|
|
71
88
|
|
|
72
|
-
|
|
89
|
+
For untrusted archives, configure [extraction limits](#extraction-limits).
|
|
73
90
|
|
|
74
|
-
|
|
75
|
-
import { loadOBZ, extractOBZ } from "@shayc/open-board-format";
|
|
91
|
+
### Create an OBZ archive
|
|
76
92
|
|
|
77
|
-
|
|
78
|
-
const { rootBoard, boards, resources } = await loadOBZ(file);
|
|
93
|
+
Given an existing board and its media resources:
|
|
79
94
|
|
|
80
|
-
|
|
81
|
-
|
|
95
|
+
```ts
|
|
96
|
+
import { createOBZ } from "@shayc/open-board-format";
|
|
82
97
|
|
|
83
|
-
|
|
84
|
-
const guarded = await extractOBZ(buffer, {
|
|
85
|
-
limits: { maxTotalOriginalSize: 500e6 },
|
|
86
|
-
});
|
|
98
|
+
const blob = await createOBZ([existingBoard], existingBoard.id, resources);
|
|
87
99
|
```
|
|
88
100
|
|
|
89
|
-
`
|
|
101
|
+
`createOBZ` generates the manifest automatically, writes boards to `boards/<encoded-id>.obf`, and uses `rootBoardId` as the archive's entry board.
|
|
90
102
|
|
|
91
|
-
|
|
103
|
+
### Validate a board
|
|
92
104
|
|
|
93
105
|
```ts
|
|
94
|
-
|
|
95
|
-
|
|
106
|
+
import { OBFBoardSchema } from "@shayc/open-board-format";
|
|
107
|
+
|
|
108
|
+
export const validateBoard = (value: unknown) =>
|
|
109
|
+
OBFBoardSchema.safeParse(value);
|
|
96
110
|
```
|
|
97
111
|
|
|
98
|
-
|
|
112
|
+
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()`.
|
|
99
113
|
|
|
100
|
-
|
|
114
|
+
## Validation details
|
|
101
115
|
|
|
102
|
-
|
|
103
|
-
import { createOBZ } from "@shayc/open-board-format";
|
|
104
|
-
import type { OBFBoard } from "@shayc/open-board-format";
|
|
116
|
+
Validation returns a parsed copy of the input. Known fields may be normalized during parsing:
|
|
105
117
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
buttons: [{ id: "btn-1", label: "Hello", image_id: "img-1" }],
|
|
110
|
-
grid: { rows: 1, columns: 1, order: [["btn-1"]] },
|
|
111
|
-
images: [{ id: "img-1", path: "images/hello.png" }],
|
|
112
|
-
};
|
|
118
|
+
- Numeric IDs become strings.
|
|
119
|
+
- Empty optional IDs, URLs, and email addresses become `undefined`.
|
|
120
|
+
- Unknown properties are preserved at every loose-object level, with or without an `ext_` prefix.
|
|
113
121
|
|
|
114
|
-
|
|
115
|
-
const resources = new Map([["images/hello.png", pngBytes]]);
|
|
122
|
+
Structural validation checks:
|
|
116
123
|
|
|
117
|
-
|
|
118
|
-
|
|
124
|
+
- URL and email fields are syntax-checked.
|
|
125
|
+
- Grid dimensions must be integers from 1 through 100.
|
|
126
|
+
- `grid.order` must exactly match the declared row and column counts.
|
|
127
|
+
- Positioned buttons must provide `top`, `left`, `width`, and `height`, each between 0 and 1.
|
|
128
|
+
- Format versions must match `open-board-*`; they are not restricted to `open-board-0.1`.
|
|
129
|
+
- An OBZ manifest root must appear in `paths.boards`.
|
|
119
130
|
|
|
120
|
-
|
|
131
|
+
Validation is not a complete OBF conformance or graph-integrity check. It does not enforce:
|
|
121
132
|
|
|
122
|
-
|
|
133
|
+
- Unique button, image, or sound IDs.
|
|
134
|
+
- Resolution of `grid.order`, `image_id`, `sound_id`, or `load_board` references.
|
|
135
|
+
- A consistent positioning mode across every button on a board.
|
|
136
|
+
- BCP 47 locale syntax, color syntax, MIME correctness, or safe HTML.
|
|
137
|
+
- During extraction, the existence of manifest-declared media files or their agreement with board media records.
|
|
123
138
|
|
|
124
|
-
|
|
125
|
-
import { OBFBoardSchema } from "@shayc/open-board-format";
|
|
139
|
+
Add application-specific checks after parsing when those guarantees matter.
|
|
126
140
|
|
|
127
|
-
|
|
141
|
+
## API reference
|
|
128
142
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
143
|
+
### High-level API
|
|
144
|
+
|
|
145
|
+
#### Board data
|
|
146
|
+
|
|
147
|
+
| Function | Returns | Behavior |
|
|
148
|
+
| --------------------- | ------------------- | ----------------------------------------------------------- |
|
|
149
|
+
| `parseOBF(json)` | `OBFBoard` | Parse JSON and validate a board; strips a leading UTF-8 BOM |
|
|
150
|
+
| `validateOBF(value)` | `OBFBoard` | Validate and normalize an unknown value |
|
|
151
|
+
| `stringifyOBF(board)` | `string` | Serialize as two-space JSON without revalidating |
|
|
152
|
+
| `loadOBF(file)` | `Promise<OBFBoard>` | Read a `File`, then parse and validate it |
|
|
153
|
+
|
|
154
|
+
#### Archives and format detection
|
|
155
|
+
|
|
156
|
+
| Function | Returns | Behavior |
|
|
157
|
+
| -------------------------------------------- | ---------------------- | ------------------------------------------------------------------- |
|
|
158
|
+
| `loadBoard(input, options?)` | `Promise<LoadedBoard>` | Detect OBF or OBZ from the bytes, then load it |
|
|
159
|
+
| `loadOBZ(file, options?)` | `Promise<ParsedOBZ>` | `File` convenience wrapper around `extractOBZ` |
|
|
160
|
+
| `extractOBZ(input, options?)` | `Promise<ParsedOBZ>` | Extract and validate the manifest and every manifest-declared board |
|
|
161
|
+
| `createOBZ(boards, rootBoardId, resources?)` | `Promise<Blob>` | Validate and package boards and resources with a generated manifest |
|
|
162
|
+
| `parseManifest(json)` | `OBFManifest` | Parse and validate manifest JSON |
|
|
163
|
+
|
|
164
|
+
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.
|
|
165
|
+
|
|
166
|
+
### Types and schemas
|
|
167
|
+
|
|
168
|
+
`LoadedBoard` is a discriminated union:
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
{ format: "obf", board: OBFBoard }
|
|
172
|
+
| { format: "obz", archive: ParsedOBZ }
|
|
134
173
|
```
|
|
135
174
|
|
|
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:
|
|
175
|
+
`ParsedOBZ` provides the validated archive contents:
|
|
203
176
|
|
|
204
177
|
```ts
|
|
205
|
-
|
|
178
|
+
interface ParsedOBZ {
|
|
179
|
+
manifest: OBFManifest;
|
|
180
|
+
boards: Map<string, OBFBoard>;
|
|
181
|
+
rootBoard: OBFBoard;
|
|
182
|
+
resources: Map<string, Uint8Array>;
|
|
183
|
+
}
|
|
206
184
|
```
|
|
207
185
|
|
|
208
|
-
|
|
186
|
+
Main exports include:
|
|
209
187
|
|
|
210
|
-
|
|
188
|
+
- Board, action, media, metadata, and manifest types.
|
|
189
|
+
- Matching Zod schemas, including `OBFBoardSchema` and `OBFManifestSchema`.
|
|
190
|
+
- Input and archive types: `BinaryInput`, `ParsedOBZ`, and `LoadedBoard`.
|
|
191
|
+
- Structured errors through `OBFError` and its related types.
|
|
192
|
+
|
|
193
|
+
### Errors
|
|
194
|
+
|
|
195
|
+
Expected parsing, validation, and archive-domain failures from the high-level APIs use `OBFError`.
|
|
196
|
+
|
|
197
|
+
Branch on `error.info.code`, not `error.message`.
|
|
211
198
|
|
|
212
199
|
```ts
|
|
213
200
|
import { loadBoard, OBFError } from "@shayc/open-board-format";
|
|
@@ -215,94 +202,108 @@ import { loadBoard, OBFError } from "@shayc/open-board-format";
|
|
|
215
202
|
try {
|
|
216
203
|
await loadBoard(file);
|
|
217
204
|
} catch (error) {
|
|
218
|
-
if (
|
|
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
|
|
227
|
-
console.error(error.info.issues);
|
|
228
|
-
break;
|
|
229
|
-
default:
|
|
230
|
-
console.error(error.message);
|
|
205
|
+
if (error instanceof OBFError) {
|
|
206
|
+
console.error(error.info.code);
|
|
231
207
|
}
|
|
208
|
+
|
|
209
|
+
throw error;
|
|
232
210
|
}
|
|
233
211
|
```
|
|
234
212
|
|
|
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
|
-
|
|
213
|
+
<details>
|
|
214
|
+
<summary><strong>Error codes</strong></summary>
|
|
215
|
+
|
|
216
|
+
| Area | `info.code` | Additional fields |
|
|
217
|
+
| -------------- | ------------------- | -------------------------------------------------- |
|
|
218
|
+
| Decoding | `not-json` | `source` |
|
|
219
|
+
| Decoding | `not-zip` | — |
|
|
220
|
+
| Decoding | `unreadable-zip` | — |
|
|
221
|
+
| Limits | `archive-too-large` | `limit`, `path`, and fields for the exceeded limit |
|
|
222
|
+
| Validation | `invalid-board` | `issues`, `boardId?` |
|
|
223
|
+
| Validation | `invalid-manifest` | `issues` |
|
|
224
|
+
| OBZ extraction | `missing-manifest` | — |
|
|
225
|
+
| OBZ extraction | `missing-board` | `boardId`, `path` |
|
|
226
|
+
| OBZ extraction | `board-id-mismatch` | `path`, `declaredId`, `actualId` |
|
|
227
|
+
| OBZ creation | `unknown-root` | `rootBoardId` |
|
|
228
|
+
| OBZ creation | `duplicate-board` | `boardId` |
|
|
229
|
+
| OBZ creation | `missing-resource` | `kind`, `mediaId`, `path` |
|
|
230
|
+
| OBZ creation | `conflicting-paths` | `kind`, `mediaId`, `paths` |
|
|
231
|
+
| OBZ creation | `path-collision` | `path` |
|
|
232
|
+
| OBZ creation | `zip-failed` | — |
|
|
233
|
+
| Internal | `internal` | `detail` |
|
|
234
|
+
|
|
235
|
+
</details>
|
|
236
|
+
|
|
237
|
+
Validation failures expose the underlying `ZodError` as `error.cause` and provide its flat issue list through `error.info.issues`.
|
|
238
|
+
|
|
239
|
+
`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.
|
|
240
|
+
|
|
241
|
+
Direct schema `.parse()` calls throw `ZodError` rather than `OBFError`.
|
|
242
|
+
|
|
243
|
+
<details>
|
|
244
|
+
<summary><strong>Low-level ZIP utilities</strong></summary>
|
|
245
|
+
|
|
246
|
+
The following exports are available for advanced archive workflows:
|
|
247
|
+
|
|
248
|
+
| Function | Returns | Behavior |
|
|
249
|
+
| ------------------------- | ---------------------------------- | -------------------------------------------------------- |
|
|
250
|
+
| `isZip(buffer)` | `boolean` | Check whether an `ArrayBuffer` has a ZIP signature |
|
|
251
|
+
| `zip(entries)` | `Promise<Uint8Array>` | Compress a map of paths to `Uint8Array` or `ArrayBuffer` |
|
|
252
|
+
| `unzip(buffer, options?)` | `Promise<Map<string, Uint8Array>>` | Extract an `ArrayBuffer` and omit directory markers |
|
|
253
|
+
|
|
254
|
+
</details>
|
|
264
255
|
|
|
265
256
|
## Security
|
|
266
257
|
|
|
267
|
-
OBZ archives
|
|
258
|
+
Treat OBZ archives and their contents as untrusted input.
|
|
259
|
+
|
|
260
|
+
### Extraction limits
|
|
268
261
|
|
|
269
262
|
```ts
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
263
|
+
import { extractOBZ } from "@shayc/open-board-format";
|
|
264
|
+
import type { BinaryInput } from "@shayc/open-board-format";
|
|
265
|
+
|
|
266
|
+
export function extractUntrusted(input: BinaryInput) {
|
|
267
|
+
return extractOBZ(input, {
|
|
268
|
+
limits: {
|
|
269
|
+
// Examples only—choose limits appropriate for your application.
|
|
270
|
+
maxEntrySize: 100 * 1024 ** 2, // 100 MiB
|
|
271
|
+
maxTotalOriginalSize: 500 * 1024 ** 2, // 500 MiB
|
|
272
|
+
maxEntries: 10_000,
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
}
|
|
277
276
|
```
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
Entry paths are not sanitized — if you write extracted resources to disk, validate paths yourself first to avoid directory traversal.
|
|
278
|
+
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
279
|
|
|
283
|
-
|
|
280
|
+
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
281
|
|
|
285
|
-
|
|
282
|
+
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
283
|
|
|
287
|
-
|
|
284
|
+
### Other boundaries
|
|
288
285
|
|
|
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.
|
|
286
|
+
- Archive entry paths are not sanitized. Validate them before writing files to disk to prevent directory traversal.
|
|
287
|
+
- `description_html` is not sanitized. Sanitize it before inserting it into the DOM.
|
|
288
|
+
- URLs and `data_url` values are validated syntactically but are never fetched.
|
|
293
289
|
|
|
294
|
-
|
|
290
|
+
Found a vulnerability? Email [shayc@outlook.com](mailto:shayc@outlook.com) rather than opening a public issue.
|
|
295
291
|
|
|
296
|
-
|
|
292
|
+
## Runtime
|
|
297
293
|
|
|
298
|
-
|
|
294
|
+
- Pure ESM for Node.js `>=22` and modern browsers; CommonJS is unsupported.
|
|
295
|
+
- Browser environments must provide `Blob`, `File`, `TextEncoder`, and `TextDecoder`.
|
|
296
|
+
- `fflate` is the only runtime dependency; `zod ^4.4.3` is a peer dependency.
|
|
297
|
+
- CI covers Node.js 22, 24, and 26. Browser engines are not currently tested in CI.
|
|
299
298
|
|
|
300
|
-
|
|
299
|
+
## Project
|
|
301
300
|
|
|
302
|
-
|
|
301
|
+
The public API follows semantic versioning. Breaking changes to exported APIs, schemas, or documented behavior ship as major releases.
|
|
303
302
|
|
|
304
|
-
-
|
|
305
|
-
- [
|
|
303
|
+
- **Changelog:** See [CHANGELOG.md](CHANGELOG.md).
|
|
304
|
+
- **Support:** [Open an issue](https://github.com/shayc/open-board-format/issues) with a minimal reproduction, package version, runtime, and bundler where applicable.
|
|
305
|
+
- **Contributing:** See [CONTRIBUTING.md](CONTRIBUTING.md) for development commands, tests, and the changeset workflow.
|
|
306
|
+
- **Specification:** See the [official OBF documentation](https://www.openboardformat.org/docs) or the included [offline mirror](docs/external/open-board-format.md).
|
|
306
307
|
|
|
307
308
|
## License
|
|
308
309
|
|
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.4",
|
|
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>",
|