# @ferret/assets: API

Loading and caching of glTF, KTX2 and packed assets, `.ferret` packages and their security checks.

160 exports. Generated from the source (`packages/assets/src`), so it matches the code.

## Classes

### AssetCache

*class* · `packages/assets/src/cache.ts:30`

```ts
class AssetCache
```

Loads glTF/GLB models once and hands out instances. The source stays in memory (not in the scene) while anything uses it;
each instance is its own copy of the nodes, skeletons and animation groups, with materials and geometry shared.
A URL, a `data:` URL, a blob URL or a File all work. Reference counting: `release` the url when the level that asked for it goes.

- `onProgress?: (p: LoadProgress) => void` — Called with the bytes loaded so far, for a loading screen.
- `constructor(readonly scene: BABYLON.Scene)`
- `get size(): number`
- `has(url: string): boolean`
- `refs(url: string): number`
- `async load(url: string, ext = guessExt(url)): Promise<void>` — Load (or reuse) a model and take a reference to it.
- `async preload(urls: string[]): Promise<void>` — Load several models; resolves when all are in.
- `async instantiate(url: string, name?: string): Promise<ModelInstance>` — A new copy of a loaded model in the scene. The model must have been loaded (`load`).
- `release(url: string): void` — Drop one reference; at zero the source and its instances are freed.
- `dispose(): void`

### AssetStore

*class* · `packages/assets/src/store.ts:68`

```ts
class AssetStore
```

- `constructor(o: StoreOptions = {})`
- `resolve(ref: string): string` — Resolve a ref to an absolute URL and check the rules of section 6. Throws StreamError('origin').
- `require(ref: string, opts: RequireOptions = {}): Promise<Handle>`
- `prefetch(refs: string[], opts: RequireOptions = {}): void` — Ask for files nobody waits for. Failures are kept in `status`, not thrown.
- `stats(): { queued: number; loading: number; done: number; failed: number; bytes: number }` — What the store holds and is doing now: files by state and the bytes in them (for `stream.status`).
- `status(ref: string): StoreStatus`
- `plan(refs: { ref: string; size?: number }[]): { bytes: number; files: number; cached: boolean }` — What asking for these refs would download now: files not yet held, their bytes (sizes given by the caller).
- `release(ref: string): void`
- `progress(group: string): StreamProgress`
- `onProgress(fn: (group: string, p: StreamProgress) => void): () => void`
- `cancelGroup(group: string): void` — Abort everything queued or running that belongs to a group (a travel to another level).
- `async byteSource(ref: string, o: { block?: number; maxBlocks?: number; signal?: AbortSignal } = {}): Promise<ByteSource>` — A ByteSource over a URL read through the store: blocks of `block` bytes (read-ahead merges neighbouring ranges), kept in memory, retried like any file. `FerretPackage.open(source)` then costs the bytes of the entries it reads, not the package.
- `get cache(): PersistentCache | undefined` — The persistent cache this store writes to, if any.
- `async fetchFresh(ref: string, o: { signal?: AbortSignal } = {}): Promise<Uint8Array>` — A mutable file (a catalog, an index): always asked of the server, never from the cache, retried like any file. Not queued, because what a travel needs next must not wait behind downloads.
- `keep(key: string, bytes: Uint8Array, meta: { pkg?: string; version?: string; url?: string } = {}): void` — Keep verified bytes under their hash (or another key) in the persistent cache. Never fails the caller: a full or broken cache is only a missed saving.
- `async flushCache(): Promise<void>` — Resolves when every cache write started so far has finished (tests, and before the page is suspended).
- `cacheInfo(): Promise<CacheInfo | null>`
- `pin(pkg: string): void`
- `unpin(pkg: string): void`
- `async pinPack(name: string, files: { ref: string; sha256: string; size?: number }[], o: { priority?: StreamPriority; signal?: AbortSignal } = {}): Promise<{ ok: number; missing: string[] }>` — A pack of files the game wants always on the device (a transition's images, videos, sounds): fetched at low priority, kept in the cache under the pack's name and pinned there, so the wait of a travel never depends on the network for them. Returns what could not be fetched; `status(ref)` tells later whether one is there.
- `async clear(pkg?: string): Promise<void>` — Forget downloaded content: one package, or everything.

### CacheStorageBackend

*class* · `packages/assets/src/persist.ts:23`

```ts
class CacheStorageBackend implements CacheBackend
```

Cache Storage (`caches`): browsers and the Android WebView. A synthetic URL per key.

- `readonly name: "cache-storage"`
- `constructor(private readonly cache: Cache, private readonly origin = 'https://ferret-cache.invalid/')`
- `static async open(caches: CacheStorage, name = 'ferret-assets'): Promise<CacheStorageBackend>`
- `async get(k: string)`
- `async put(k: string, b: Uint8Array)`
- `async delete(k: string)`
- `async keys()`

### CatalogError

*class* · `packages/assets/src/catalog.ts:58`

```ts
class CatalogError extends Error
```

- `constructor(public readonly code: CatalogFailure, message: string, public readonly issues: PackageIssue[] = [])`

### ChunkedBackend

*class* · `packages/assets/src/persist.ts:63`

```ts
class ChunkedBackend implements CacheBackend
```

Splits big values into parts: Chromium refused a single Cache Storage entry above 128 MB (S0). A value of at most `chunk` bytes is
stored as is; a bigger one as parts `key~0..n` and then a small record `key!m`. The record is written last, so a cut write leaves
no half value visible (the orphan parts are removed by `sweep`).

- `constructor(private readonly inner: CacheBackend, private readonly chunk = 32 * 1048576)`
- `get name()`
- `async get(k: string)`
- `async put(k: string, b: Uint8Array)`
- `async delete(k: string)`
- `async keys()` — Logical keys only.
- `async sweep()` — Remove parts whose record is missing (a write that was cut).

### FerretPackage

*class* · `packages/assets/src/package.ts:402`

```ts
class FerretPackage
```

- `get source(): PackageSource` — the files as they can be read now: decrypted once the package is unlocked
- `get encrypted(): boolean`
- `get dirty(): boolean` — the changes made through `draft()` that are not packed yet
- `draft(): PackageDraft` — Start editing: from now on this package reads through a draft, so writes (`draft().set(path, data)`) show in everything that reads the package (a mounted level reloads with them). `pack` writes the result.
- `async appendSave(base: ByteSource): Promise<{ at: number; tail: Uint8Array; dead: number; changed: string[]; commit(): void }>` — Save a project (`.fprj`) by appending: the files changed through `draft()` since the last save are written after the last byte of the file `base` (the package as it stands on disk, the bytes it was opened from plus the tails appended since), then a new directory and end record. Write `tail` at offset `at`, flush, then call `commit()`; a save cut off anywhere leaves the previous content. `dead` is the share of the file the saves left unused, so a tool can `compactZip` above a half. A project holds no hashes or signature, so nothing else changes. Only a project may be saved this way; any other package is written whole by `pack`.
- `async pack(o: PackOptions = {}): Promise<{ bytes: Uint8Array; manifest: PackageManifest }>` — Build the `.ferret` bytes of the package as it is now. An encrypted package is sealed again with the password or key it was unlocked with (pass `decrypt` for plain files, `encrypt` for another key). The old signature does not survive a change: pass `sign` to sign the new package.
- `get locked(): boolean` — encrypted and the key not given yet: the names can be listed, the files cannot be read
- `static async open(input: PackageInput, o: { fetch?: typeof fetch; /** the file's name (a Blob has none; a URL string is its own): `.fex` must be a game and `.fprj` a project */ fileName?: string } & Partial<KeyInput> = {}): Promise<FerretPackage>` — Open a package: a `.ferret` file (a Blob or File, bytes, or a URL — read by range requests when the server allows), or a folder (a URL ending in `/`, or a PackageSource). Fails when `ferret.json` is missing or invalid.
- `async unlock(input: KeyInput): Promise<void>` — Give the password or key of an encrypted package. Throws WrongKeyError when it does not fit.
- `get id(): string`
- `get version(): string`
- `has(path: string): boolean`
- `read(path: string): Promise<Uint8Array>`
- `async readText(path: string): Promise<string>`
- `async readJson<T = unknown>(path: string): Promise<T>`
- `levels(): string[]` — Ids of the levels: those `provides` names, else every `levels/<id>/manifest.json`.
- `get hasCode(): boolean` — Does the package hold code? (kinds `assets` and `levels` must not.)
- `get codeEntry(): string | undefined` — The module the code loader runs: `code.entry`, else a game's `entry.code`; undefined when the package names none.
- `info(): PackageInfo`
- `async verify(o: { trusted?: Iterable<string>; checksums?: boolean } = {}): Promise<VerifyResult>` — Is the signature there, does it match, and is the key one the reader trusts? (default true) also reads every file and compares it with the manifest, which needs the package unlocked. Unsigned is not a failure here: says so, and what to do about it is the reader's decision.
- `async codeAllowed(policy: CodePolicy = {}): Promise<{ allowed: boolean; reason?: string }>` — May this package's code run? Packages without code always can. Otherwise the code must come from the game's own origin, or the package must carry a valid signature (from a trusted key, if the reader keeps a list). Returns the reason when it may not.
- `async validate(o: { checksums?: boolean; engine?: EngineVersions; engineRequired?: boolean } = {}): Promise<PackageIssue[]>` — Check the package: the manifest, that the files it names exist, that kinds without code hold none, and (`checksums`, default true when the manifest has them) that every file has the sha256 the manifest gives. References between levels and assets are checked by `@ferret/levels` (`validatePackage`), which knows the level manifest.

### IndexedDbBackend

*class* · `packages/assets/src/persist.ts:35`

```ts
class IndexedDbBackend implements CacheBackend
```

IndexedDB blobs: one record per key.

- `readonly name: "indexeddb"`
- `static open(idb: IDBFactory, name = 'ferret-assets'): Promise<IndexedDbBackend>`
- `async get(k: string)`
- `async put(k: string, b: Uint8Array)`
- `async delete(k: string)`
- `async keys()`

### LockedError

*class* · `packages/assets/src/package.ts:383`

```ts
class LockedError extends Error
```

Opening an encrypted package without its key, or reading a file of it.

- `constructor(m = 'the package is encrypted: give its password or key (unlock)')`

### MemoryBackend

*class* · `packages/assets/src/persist.ts:13`

```ts
class MemoryBackend implements CacheBackend
```

- `readonly name: "memory"`
- `async get(k: string)`
- `async put(k: string, b: Uint8Array)`
- `async delete(k: string)`
- `async keys()`

### PackageDraft

*class* · `packages/assets/src/package.ts:336`

```ts
class PackageDraft implements PackageSource
```

A package kept in memory, with changes on top of another source: what the editor works on and `pack` writes.

- `readonly kind: "memory"`
- `constructor(readonly base: PackageSource | null = null)`
- `get changed(): string[]` — the paths changed (written or deleted) since the last `commit`
- `get dirty(): boolean`
- `commit(): void` — Remember that everything now in the draft is kept somewhere (appended to the file): `changed` and `dirty` start again from nothing. The files stay readable.
- `list(): string[]` — every file path (the manifest included)
- `has(p: string): boolean`
- `size(p: string): number` — bytes of the file in the package (uncompressed); 0 when unknown
- `async read(p: string): Promise<Uint8Array>`
- `set(path: string, data: Uint8Array | string): void`
- `delete(path: string): void`

### PackageSet

*class* · `packages/assets/src/package.ts:684`

```ts
class PackageSet
```

The packages that are open together, so `ferret://<id>/<path>` and `dependencies` resolve.

- `add(pkg: FerretPackage): this`
- `remove(id: string): void`
- `get(id: string): FerretPackage | undefined`
- `get all(): FerretPackage[]`
- `resolve(ref: string, from: FerretPackage, base = ''): { pkg: FerretPackage; path: string } | null` — Where a reference points: `ferret://id/path` in another package (which must be a declared dependency of `from`), or a path inside `from`.
- `deps(pkg: FerretPackage, seen = new Set<string>()): DependencyReport[]` — `pkg.deps`: every dependency of a package, found or not, and whether its version fits (and, down the tree, theirs).

### PersistentCache

*class* · `packages/assets/src/persist.ts:132`

```ts
class PersistentCache
```

- `constructor(readonly backend: CacheBackend, private readonly o: PersistentCacheOptions = {})`
- `async flush(): Promise<void>`
- `async has(key: string): Promise<boolean>`
- `async get(key: string): Promise<Uint8Array | null>`
- `async put(key: string, bytes: Uint8Array, meta: { pkg?: string; version?: string; url?: string } = {}): Promise<void>` — Keep verified bytes. The blob goes in first and the index after it, so an interrupted write leaves a blob the index does not know (swept later) and never an entry without bytes. Makes room by evicting the least recently used unpinned packages once; throws QuotaError when that is not enough.
- `pin(pkg: string): void`
- `unpin(pkg: string): void`
- `async clear(pkg?: string): Promise<void>`
- `async info(): Promise<CacheInfo>`
- `async putPartial(key: string, bytes: Uint8Array, etag: string): Promise<void>` — Keep what arrived of a file the network cut, with the validator the server gave, so a later session can resume it.
- `async getPartial(key: string): Promise<{ bytes: Uint8Array; etag: string } | null>`
- `async dropPartial(key: string): Promise<void>`
- `async cleanPartials(): Promise<void>` — Remove old partial files and blobs that no index knows (a write cut between the blob and the index).
- `static async requestPersistence(storage: Pick<StorageManager, 'persist' | 'persisted'> | undefined = (globalThis as { navigator?: Navigator }).navigator?.storage): Promise<boolean>` — Ask the browser not to drop the cache under pressure; call it after the first level loaded.

### QuotaError

*class* · `packages/assets/src/persist.ts:118`

```ts
class QuotaError extends Error
```

- `constructor(public readonly needed: number)`

### Sha256

*class* · `packages/assets/src/sha256.ts:10`

```ts
class Sha256
```

- `update(data: Uint8Array): this`
- `hex(): string` — The digest as hex. The hasher is spent afterwards.

### StreamError

*class* · `packages/assets/src/stream-error.ts:5`

```ts
class StreamError extends Error
```

- `constructor(public readonly ref: string, public readonly reason: StreamFailure, message: string, public readonly status?: number)`

### WrongKeyError

*class* · `packages/assets/src/security.ts:135`

```ts
class WrongKeyError extends Error
```

- `constructor(m = 'wrong password or key')`

## Functions

### appendToZip

*function* · `packages/assets/src/zip.ts:256`

```ts
async function appendToZip(src: ByteSource, changes: { set?: ZipFile[]; remove?: string[] }): Promise<{ at: number; tail: Uint8Array; entries: Map<string, ZipEntryInfo>; dead: number }>
```

Change a zip by appending: the new and changed files are written after the last byte, then a new directory that
lists every live file, then a new end record. Nothing before them is touched, so the cost of a save is the size of the change, not of the
project, and a save cut off at any byte leaves the previous directory the last complete one (`readZipDirectory` finds it). Replaced and
removed files stay in the file as dead bytes until `compactZip`. Write `tail` at offset `at` (the size of the file now), then flush.
`dead` is the share of the file that no directory entry uses.

### blobSource

*function* · `packages/assets/src/zip.ts:14`

```ts
function blobSource(blob: Blob): ByteSource
```

### buildCatalog

*function* · `packages/assets/src/catalog.ts:262`

```ts
async function buildCatalog(i: BuildCatalogInput): Promise<{ catalog: Catalog; files: Record<string, Uint8Array> }>
```

`pkg.catalog` + `pkg.publish`: the catalog, and the files to put on the server, as { path: bytes }.

### bytesSource

*function* · `packages/assets/src/zip.ts:17`

```ts
function bytesSource(bytes: Uint8Array): ByteSource
```

### canonicalManifest

*function* · `packages/assets/src/security.ts:70`

```ts
function canonicalManifest(m: object): Uint8Array
```

The bytes that are signed: the manifest without its signature, as JSON with sorted keys.

### checkCatalog

*function* · `packages/assets/src/catalog.ts:126`

```ts
async function checkCatalog(raw: unknown, policy: CatalogPolicy = {}): Promise<{ catalog: Catalog; unsigned: boolean; keyId?: string }>
```

Validate, check the signature against the keys the game trusts, and the engine range. Throws CatalogError.

### checkEngine

*function* · `packages/assets/src/package.ts:261`

```ts
function checkEngine(engine: PackageManifest['engine'], running: EngineVersions, o: { required?: boolean } = {}): PackageIssue[]
```

Compare the `engine` field of a manifest with the engine that is running. The ranges are those of
`dependencies`; Babylon is usually an exact version. Only the numbers count: a build stamp after `+` is ignored. A field that
is absent is no problem unless `required` is set (a finished game needs both). Returns the issues; empty means it fits.

### checkFileName

*function* · `packages/assets/src/package.ts:117`

```ts
function checkFileName(name: string, kind: PackageKind): PackageIssue[]
```

Does the file name's extension agree with the package kind? `.fex` is a game and `.fprj` a project; other names are free.

### cleanPath

*function* · `packages/assets/src/package.ts:210`

```ts
function cleanPath(p: unknown): string | null
```

A path inside a package: `a/b/c.ext`, no `..`, no leading slash. Returns it normalised, or null.

### compactZip

*function* · `packages/assets/src/zip.ts:285`

```ts
async function compactZip(src: ByteSource): Promise<Uint8Array>
```

A zip with only its live files, copied as they are (no recompression), in the order they were first written: drops what appends left behind.

### configureKtx2

*function* · `packages/assets/src/ktx2.ts:55`

```ts
function configureKtx2(o: Ktx2Options = {}): { base: string; workers: number }
```

Point Babylon's KTX2 decoder at the files of `ktx2/`. Idempotent: the first call wins (the worker pool starts with the first KTX2 texture).

### crc32

*function* · `packages/assets/src/zip.ts:183`

```ts
function crc32(data: Uint8Array): number
```

### createEncryption

*function* · `packages/assets/src/security.ts:116`

```ts
async function createEncryption(input: KeyInput, iterations = DEFAULT_ITERATIONS): Promise<{ encryption: PackageEncryption; key: CryptoKey }>
```

Make the encryption block (and the key) for a new encrypted package.

### deflate

*function* · `packages/assets/src/zip.ts:180`

```ts
function deflate: (b: Uint8Array<ArrayBufferLike>) => Promise<Uint8Array<ArrayBufferLike>>
```

Deterministic (see deflate.ts): the platform's CompressionStream gives different bytes in each engine.

### detectBackend

*function* · `packages/assets/src/persist.ts:100`

```ts
async function detectBackend(env: CacheEnv = globalThis as unknown as CacheEnv, chunk?: number): Promise<CacheBackend>
```

Pick the first backend that really writes, reads and deletes (a private window may offer the API and refuse the write).

### downloadedContentItem

*function* · `packages/assets/src/cache-menu.ts:17`

```ts
function downloadedContentItem(store: AssetStore, o: { onChange?: () => void; close?: () => void } = {}): CacheMenuItem & { refresh(): Promise<void> }
```

`value()` is read by a menu on every draw, so it shows the last size seen; `refresh()` reads the cache and calls `onChange`.

### etc1sTarget

*function* · `packages/assets/src/ktx2.ts:83`

```ts
function etc1sTarget(caps: Ktx2Caps, alpha = true): Ktx2Target
```

the block format Babylon picks for ETC1S data (the colour maps of `--ktx2`)

### folderSource

*function* · `packages/assets/src/package.ts:301`

```ts
async function folderSource(io: FolderIO): Promise<PackageSource>
```

### formatBytes

*function* · `packages/assets/src/cache-menu.ts:10`

```ts
function formatBytes(n: number): string
```

### fromBase64

*function* · `packages/assets/src/security.ts:46`

```ts
function fromBase64(b: string): Uint8Array
```

### generateEncryptionKey

*function* · `packages/assets/src/security.ts:105`

```ts
function generateEncryptionKey(): string
```

### generateSigningKey

*function* · `packages/assets/src/security.ts:62`

```ts
async function generateSigningKey(): Promise<SigningKey>
```

### httpFolderSource

*function* · `packages/assets/src/package.ts:317`

```ts
async function httpFolderSource(base: string, fetchFn: typeof fetch = (u, i) => fetch(u, i)): Promise<PackageSource>
```

A folder package on a server. The file names come from `index` (a list of paths) or, when the folder is served with an index
file named `ferret-index.json`, from that; otherwise from the `files` of its `ferret.json`.

### inflate

*function* · `packages/assets/src/zip.ts:178`

```ts
function inflate: (b: Uint8Array<ArrayBufferLike>, max?: number) => Promise<Uint8Array<ArrayBufferLike>>
```

### isCompressedFormat

*function* · `packages/assets/src/zip.ts:193`

```ts
function isCompressedFormat(path: string): boolean
```

Files that are compressed already are stored, so a package is read by slices without inflating (PLAN.md 4.9).

### itemPath

*function* · `packages/assets/src/library.ts:319`

```ts
function itemPath: (id: string) => string
```

Path of the item file of an id inside a package.

### joinPath

*function* · `packages/assets/src/package.ts:221`

```ts
function joinPath(base: string, ref: string): string | null
```

Resolve `ref` against the folder `base` (`levels/yard/` + `../../assets/a.glb` = `assets/a.glb`).

### keyIdOf

*function* · `packages/assets/src/security.ts:58`

```ts
async function keyIdOf(spki: Uint8Array): Promise<string>
```

### ktx2Base

*function* · `packages/assets/src/ktx2.ts:46`

```ts
function ktx2Base(o: Ktx2Options = {}): string
```

where the transcoder files are for this page (absolute: Babylon's worker is a blob, which resolves nothing relative)

### ktx2GpuBytes

*function* · `packages/assets/src/ktx2.ts:100`

```ts
function ktx2GpuBytes(width: number, height: number, target: Ktx2Target, mips = true): number
```

Video memory of a texture of `width` x `height` with a full mip chain in `target` (4/3 of the top level, rounded up by the blocks).

### ktx2Targets

*function* · `packages/assets/src/ktx2.ts:93`

```ts
function ktx2Targets(engine: { getCaps(): Ktx2Caps }): { caps: Required<Ktx2Caps>; uastc: Ktx2Target; uastcNonColour: Ktx2Target; etc1s: Ktx2Target }
```

The formats a running engine will transcode to: for logs, tests and the memory estimate (`Engine.getCaps()` is read).

### levelIds

*function* · `packages/assets/src/package.ts:611`

```ts
function levelIds(files: string[]): string[]
```

### loadCatalog

*function* · `packages/assets/src/catalog.ts:150`

```ts
async function loadCatalog(store: AssetStore, url: string, policy: CatalogPolicy = {}, o: { signal?: AbortSignal } = {}): Promise<LoadedCatalog>
```

Fetch a catalog (never from the HTTP cache) and check it. When the network fails, the last good copy kept in the persistent
cache is used and marked `stale`; with none, the failure is thrown. A good fresh copy replaces the kept one.

### openCatalogPackage

*function* · `packages/assets/src/catalog.ts:186`

```ts
async function openCatalogPackage(store: AssetStore, c: Catalog, pkgId: string, o: { tier?: string; signal?: AbortSignal } = {}): Promise<CatalogPackageSource>
```

Open a package of the catalog. `ferret.json` is checked against the catalog's `manifest` hash, and every file read afterwards
against the hash that manifest holds, so nothing the signed catalog does not vouch for is handed on. Files are looked up in the
persistent cache by their hash first; a miss reads the package by range requests (only then is the network used) and keeps the
file. The package's index is cached under the manifest hash, so a package seen once can be opened, and its cached files read,
with the network gone.

### openEncryption

*function* · `packages/assets/src/security.ts:138`

```ts
async function openEncryption(e: PackageEncryption, input: KeyInput): Promise<CryptoKey>
```

The key for an existing encrypted package; throws WrongKeyError when it does not open the package's check value.

### packageUrl

*function* · `packages/assets/src/catalog.ts:169`

```ts
function packageUrl(c: Catalog, pkgId: string, tier?: string): string
```

Absolute URL of a package file for a tier (falls back to the plain one), relative to the catalog's base.

### packFiles

*function* · `packages/assets/src/package.ts:743`

```ts
async function packFiles(files: { path: string; data: Uint8Array | string }[], o: PackOptions = {}): Promise<{ bytes: Uint8Array; manifest: PackageManifest }>
```

A folder's files (read from disk by the caller) packed into a package (`ferret pack`). The folder must hold `ferret.json`.

### packPackage

*function* · `packages/assets/src/package.ts:644`

```ts
async function packPackage(source: PackageSource, o: PackOptions = {}): Promise<{ bytes: Uint8Array; manifest: PackageManifest }>
```

### parseFerretRef

*function* · `packages/assets/src/library.ts:143`

```ts
function parseFerretRef(ref: string): { pkg: string; path: string } | null
```

`ferret://<package>/<path>` -> its parts, or null

### publishedPath

*function* · `packages/assets/src/catalog.ts:259`

```ts
function publishedPath: (hash: string, ext?: string) => string
```

Where a published package file lives: content addressed, so it never changes and can be cached forever.

### readZipDirectory

*function* · `packages/assets/src/zip.ts:122`

```ts
async function readZipDirectory(src: ByteSource): Promise<Map<string, ZipEntryInfo>>
```

### readZipEntry

*function* · `packages/assets/src/zip.ts:150`

```ts
async function readZipEntry(src: ByteSource, info: ZipEntryInfo): Promise<Uint8Array>
```

Read one file out of a zip: slice, inflate when needed, check the length and the CRC.

### satisfies

*function* · `packages/assets/src/package.ts:248`

```ts
function satisfies(version: string, range: string): boolean
```

Does `version` fit `range`?

### seal

*function* · `packages/assets/src/security.ts:153`

```ts
async function seal(key: CryptoKey, path: string, plain: Uint8Array, compress: boolean): Promise<Uint8Array>
```

`flag(1) iv(12) ciphertext+tag`: flag 1 = the plaintext was deflated first. The path is authenticated, not secret.

### segmentSource

*function* · `packages/assets/src/zip.ts:24`

```ts
function segmentSource(parts: Uint8Array[]): ByteSource
```

One file kept as pieces: the bytes it was opened from and the tails appended to it since. A save then costs the size of its tail, not a copy of
the file; `slice` joins only the pieces the range touches.

### sha256

*function* · `packages/assets/src/package.ts:617`

```ts
async function sha256(data: Uint8Array): Promise<string>
```

### signCatalog

*function* · `packages/assets/src/catalog.ts:111`

```ts
async function signCatalog(catalog: Omit<Catalog, 'signature'> | Catalog, privateKey: string): Promise<Catalog>
```

### signManifest

*function* · `packages/assets/src/security.ts:76`

```ts
async function signManifest(manifest: object, privateKey: string): Promise<PackageSignature>
```

### tierSource

*function* · `packages/assets/src/package.ts:760`

```ts
function tierSource(source: PackageSource, game: GameBlock | undefined, tier: 'desktop' | 'mobile' | 'low'): PackageSource
```

A package seen through one quality tier: the files under the tier's folder (`game.tiers.<tier>`, ending in `/`) replace
the files of the same path in the root, and the tier folders themselves disappear from the listing. A tier the game does not name is the root.

### toBase64

*function* · `packages/assets/src/security.ts:41`

```ts
function toBase64(u: Uint8Array): string
```

### uastcTarget

*function* · `packages/assets/src/ktx2.ts:75`

```ts
function uastcTarget(caps: Ktx2Caps, nonColour = false): Ktx2Target
```

the block format Babylon picks for UASTC data (`nonColour`: a normal / roughness map, which skips the RGBA-less fallbacks)

### unpackFiles

*function* · `packages/assets/src/package.ts:735`

```ts
async function unpackFiles(pkg: FerretPackage): Promise<{ path: string; data: Uint8Array }[]>
```

The files of a package as a plain list, the manifest first, for writing out as a folder (`ferret unpack`). An encrypted package must be unlocked.

### unseal

*function* · `packages/assets/src/security.ts:163`

```ts
async function unseal(key: CryptoKey, path: string, sealed: Uint8Array): Promise<Uint8Array>
```

### urlSource

*function* · `packages/assets/src/zip.ts:50`

```ts
async function urlSource(url: string, fetchFn: typeof fetch = (u, i) => fetch(u, i)): Promise<ByteSource>
```

A package on a server. Uses range requests when the server has them (only the pieces asked for travel); otherwise the whole file
is downloaded once. `fetchFn` replaces `fetch` (tests, a platform file API).

### validateCatalog

*function* · `packages/assets/src/catalog.ts:67`

```ts
function validateCatalog(c: unknown): PackageIssue[]
```

The problems of a catalog, each with a JSON path and a message (like the level manifest's). Empty when it is valid.

### validateCatalogAssets

*function* · `packages/assets/src/library.ts:338`

```ts
function validateCatalogAssets(assets: unknown, packages: Record<string, unknown> | undefined): PackageIssue[]
```

Problems of the catalog's `assets` section. `packages` is the catalog's own list.

### validateGameBlock

*function* · `packages/assets/src/package.ts:88`

```ts
function validateGameBlock(g: unknown, base = '/game'): PackageIssue[]
```

The game block against its schema; issue paths start at `base` (`/game`).

### validateLibraryItem

*function* · `packages/assets/src/library.ts:189`

```ts
function validateLibraryItem(item: unknown, o: ItemCheckOptions = {}): PackageIssue[]
```

The problems of an item file (a JSON path and a message each). Empty when it is usable.

### validatePackageManifest

*function* · `packages/assets/src/package.ts:124`

```ts
function validatePackageManifest(m: unknown): PackageIssue[]
```

Check a `ferret.json` against the schema. An empty list means it is usable.

### verifyManifestSignature

*function* · `packages/assets/src/security.ts:87`

```ts
async function verifyManifestSignature(manifest: { signature?: PackageSignature }): Promise<{ valid: boolean; keyId?: string; reason?: string }>
```

Does the signature in the manifest match its content? (Says nothing about whether the key is one to trust.)

### writeZip

*function* · `packages/assets/src/zip.ts:232`

```ts
async function writeZip(files: ZipFile[]): Promise<Uint8Array>
```

Write a zip. Paths use `/`; files are written in the order given.

### zipDeadShare

*function* · `packages/assets/src/zip.ts:277`

```ts
async function zipDeadShare(src: ByteSource): Promise<number>
```

The share (0..1) of a zip that no live file and no current directory uses: what `compactZip` would drop.

### zipSource

*function* · `packages/assets/src/package.ts:287`

```ts
async function zipSource(src: ByteSource): Promise<PackageSource>
```

## Interfaces

### BodyPart

*interface* · `packages/assets/src/library.ts:82`

```ts
interface BodyPart
```

A part of a body split out into a node of its own, so it can be hidden alone (the head from its own first-person camera).

- `node: string`
- `triangles?: number`

### BuildCatalogInput

*interface* · `packages/assets/src/catalog.ts:246`

```ts
interface BuildCatalogInput
```

- `id: string`
- `version: string`
- `base: string`
- `engine?: string`
- `packages: PackedPackage[]`
- `levels: Record<string, CatalogLevel>`
- `assets?: Record<string, CatalogAsset>` — library items (see library.ts): the section is built in before the catalog is validated and signed
- `signKey?: string`

### ByteSource

*interface* · `packages/assets/src/zip.ts:9`

```ts
interface ByteSource
```

Where bytes come from. A Blob (a File, a download), a byte array, or a URL read by HTTP range requests.

- `readonly size: number`
- `slice(start: number, end: number): Promise<Uint8Array>`

### CacheBackend

*interface* · `packages/assets/src/persist.ts:5`

```ts
interface CacheBackend
```

Byte storage keyed by string. A key is a content hash, or `u:<url>`; the cache above decides what is kept.

- `readonly name: string`
- `get(key: string): Promise<Uint8Array | null>`
- `put(key: string, bytes: Uint8Array): Promise<void>`
- `delete(key: string): Promise<void>`
- `keys(): Promise<string[]>`

### CacheEntry

*interface* · `packages/assets/src/persist.ts:116`

```ts
interface CacheEntry
```

- `size: number`
- `used: number`
- `pkg: string`
- `version?: string`
- `url?: string`

### CacheEnv

*interface* · `packages/assets/src/persist.ts:97`

```ts
interface CacheEnv
```

- `caches?: CacheStorage`
- `indexedDB?: IDBFactory`

### CacheInfo

*interface* · `packages/assets/src/persist.ts:117`

```ts
interface CacheInfo
```

- `backend: string`
- `used: number`
- `cap: number`
- `entries: number`
- `packages: Record<string, { bytes: number; files: number; pinned: boolean }>`

### CacheMenuItem

*interface* · `packages/assets/src/cache-menu.ts:5`

```ts
interface CacheMenuItem
```

- `id: string`
- `label: string`
- `value: () => string`
- `disabled?: boolean`
- `submenu: { title: string; items: { id: string; label: string; action?: () => void }[] }`

### Catalog

*interface* · `packages/assets/src/catalog.ts:42`

```ts
interface Catalog
```

- `format: number`
- `id: string`
- `version: string` — changes with every publish
- `engine?: string` — the Ferret versions this content needs (a range)
- `base: string`
- `packages: Record<string, CatalogPackage>`
- `levels: Record<string, CatalogLevel>`
- `assets?: Record<string, CatalogAsset>` — library items: where to find a texture, material, character or clip set without opening a package
- `signature?: PackageSignature`

### CatalogAsset

*interface* · `packages/assets/src/library.ts:322`

```ts
interface CatalogAsset
```

The `assets` section of a catalog: where to find an item without opening any package.

- `package: string`
- `type: LibraryItemType`
- `name: string`
- `tags?: string[]`
- `preview?: string` — path of the small preview, relative to the catalog's `base`
- `bytes?: Partial<Record<LibraryTier | 'all', number>>` — bytes per tier (characters), or one `all` entry
- `skeleton?: string` — characters and clip sets: the skeleton id, so a clip set that fits a body is found without opening any package
- `compatible?: string[]` — clip sets: other skeletons whose bone names these clips bind to

### CatalogLevel

*interface* · `packages/assets/src/catalog.ts:29`

```ts
interface CatalogLevel
```

- `package: string`
- `level: string`
- `name?: string`
- `tags?: string[]`
- `data?: Record<string, unknown>` — free: handed to the game, never interpreted by the engine
- `prefetch?: string[]` — started at idle priority once this level is running
- `bundled?: boolean` — inside the app: never fetched

### CatalogPackage

*interface* · `packages/assets/src/catalog.ts:13`

```ts
interface CatalogPackage
```

- `url: string` — relative to `base`, or absolute
- `version: string`
- `size: number` — bytes of the whole file: the progress total
- `sha256?: string` — sha256 of the whole file (the publisher's check, `pkg.verify` of a downloaded copy)
- `manifest: string` — sha256 of the package's `ferret.json`: it holds the hash of every file, so this one hash vouches for all of them (packages are read by range, not whole)
- `tiers?: Record<string, string>` — another file per device tier, by tier name (`low`, `high`)
- `mirrors?: string[]` — the same file elsewhere, tried when the first copy fails its hash

### CatalogPolicy

*interface* · `packages/assets/src/catalog.ts:116`

```ts
interface CatalogPolicy
```

- `trustedKeys?: Iterable<string>` — key ids the game ships. A signed catalog from another key is refused.
- `allowUnsigned?: boolean` — development builds only: accept an unsigned catalog (the UI shows a badge: `unsigned` in the result)
- `engineVersion?: string` — the engine's own version, checked against the catalog's `engine` range

### CharacterItem

*interface* · `packages/assets/src/library.ts:84`

```ts
interface CharacterItem extends ItemBase
```

- `type: 'character'`
- `kind: CharacterKind`
- `tiers: Partial<Record<LibraryTier, TierFile>>`
- `skeleton?: string` — skeleton id (a clip set is for one skeleton)
- `clips?: string[]` — clip set item ids, resolved by the skeleton's bone names
- `graph?: string`
- `movement?: string`
- `morphs?: string`
- `height?: number` — metres, measured from the bind pose
- `facing?: '+Z' | '-Z' | '+X' | '-X'` — the axis the toes point along in the model
- `bodyParts?: Record<string, BodyPart>` — body parts that sit in nodes of their own in the model file (`head`): same node name in every tier
- `sockets?: Record<string, Socket>` — named places to hold things: `grip_r`, `grip_l`, `saddle` (`attach` of the library)
- `parts?: { id: string; tint?: [number, number, number, number] }[]` — an assembled character: the ids of the parts it was made of

### ClipsItem

*interface* · `packages/assets/src/library.ts:107`

```ts
interface ClipsItem extends ItemBase
```

- `type: 'clips'`
- `skeleton: string`
- `compatible?: string[]` — other skeleton ids whose bone names make these clips bind without a retarget
- `file: string`
- `bytes: number`
- `clips: { name: string; duration: number; additive?: boolean; loop?: boolean; notifies?: { name: string; time: number }[] }[]` — per clip: its length, whether it loops, and the moments named in it (`hit-start`, `hit-end`, `parry-start`...; clips made in Ferret carry them, D4 of the fight demo)

### CodePolicy

*interface* · `packages/assets/src/package.ts:385`

```ts
interface CodePolicy
```

- `origin?: boolean` — the package comes from where the game itself does (its own site, or inside the signed OS Beroun app): its code is the game's
- `trusted?: Iterable<string>` — key ids the reader trusts; without it any valid signature is enough

### DependencyReport

*interface* · `packages/assets/src/package.ts:677`

```ts
interface DependencyReport
```

- `id: string`
- `deps: { id: string; range: string; found?: string; ok: boolean; message?: string }[]` — direct dependencies, in order, each with what was found

### EngineVersions

*interface* · `packages/assets/src/package.ts:254`

```ts
interface EngineVersions
```

The versions of the engine that is running (`FERRET_VERSION` and `BABYLON_VERSION` of `@ferret/core`).

- `ferret: string`
- `babylon: string`

### FolderIO

*interface* · `packages/assets/src/package.ts:299`

```ts
interface FolderIO
```

What a folder package needs: its file names and a way to read one. (Node's fs, a server's index, a platform file API.)

- `list(): Promise<string[]> | string[]`
- `read(path: string): Promise<Uint8Array>`

### GameBlock

*interface* · `packages/assets/src/package.ts:20`

```ts
interface GameBlock
```

What `createGame` is given, as data in the manifest of a game or a project.

- `input?: { actions?: Record<string, unknown>; bindings?: Record<string, unknown> }`
- `fov?: { baseVertical?: number; minHorizontal?: number }`
- `physicsLayers?: string[]`
- `clearColor?: [number, number, number]`
- `touchControls?: boolean`
- `tiers?: Partial<Record<'desktop' | 'mobile' | 'low', string>>` — quality variants of the levels in one package: a folder per tier, chosen by the player (`desktop`, `mobile`, `low`)

### Handle

*interface* · `packages/assets/src/store.ts:50`

```ts
interface Handle
```

- `readonly ref: string`
- `readonly bytes: Uint8Array`
- `readonly size: number`
- `blobUrl(): string`

### ItemCheckOptions

*interface* · `packages/assets/src/library.ts:180`

```ts
interface ItemCheckOptions
```

- `dependencies?: Iterable<string>` — ids of the packages this one depends on (and its own id as `own`): `ferret://` references outside them are refused
- `own?: string`
- `has?: (path: string) => boolean` — does a path exist in the package? When given, files an item names are checked

### Ktx2Caps

*interface* · `packages/assets/src/ktx2.ts:33`

```ts
interface Ktx2Caps
```

- `astc?: boolean`
- `bptc?: boolean`
- `s3tc?: boolean`
- `pvrtc?: boolean`
- `etc2?: boolean`
- `etc1?: boolean`

### Ktx2Options

*interface* · `packages/assets/src/ktx2.ts:24`

```ts
interface Ktx2Options
```

- `base?: string` — the folder of KTX2_FILES (default: `ktx2/` under `globalThis.FERRET_LIB`, else `lib/ferret/`); relative to the page
- `workers?: number` — decoder workers (default: Babylon's: half the cores, at most 4); 0 decodes on the page's own thread

### LibrarySource

*interface* · `packages/assets/src/library.ts:14`

```ts
interface LibrarySource
```

Where an item came from: informational, nothing reads it at run time.

- `project?: string`
- `ue?: string`
- `master?: string`
- `exported?: string`
- `params?: Record<string, unknown>`

### LoadedCatalog

*interface* · `packages/assets/src/catalog.ts:144`

```ts
interface LoadedCatalog
```

- `catalog: Catalog`
- `url: string`
- `stale: boolean`
- `unsigned: boolean`
- `keyId?: string`

### LoadProgress

*interface* · `packages/assets/src/cache.ts:14`

```ts
interface LoadProgress
```

- `url: string`
- `loaded: number`
- `total: number`

### MaterialDef

*interface* · `packages/assets/src/library.ts:48`

```ts
interface MaterialDef
```

A superset of glTF's PBR material that maps to Babylon's `PBRMaterial` one to one.

- `model?: 'pbr' | 'unlit'`
- `baseColor?: [number, number, number, number]`
- `metallic?: number`
- `roughness?: number`
- `maps?: Partial<Record<MapSlot, string>>` — `ferret://<package>/<path>` references to the maps; the package must be a dependency of the one holding the material
- `normalScale?: number`
- `uv?: { scale?: [number, number]; offset?: [number, number]; rotation?: number }`
- `worldAligned?: { size: number }` — projected by world position (triplanar), one repeat per `size` metres
- `alpha?: { mode: AlphaMode; cutoff?: number }`
- `doubleSided?: boolean`
- `specular?: number`
- `ior?: number`
- `emissive?: [number, number, number]`
- `emissiveStrength?: number`

### MaterialItem

*interface* · `packages/assets/src/library.ts:67`

```ts
interface MaterialItem extends ItemBase, MaterialDef
```

- `type: 'material'`
- `variants?: Record<string, MaterialDef>` — values that differ per variant, over the base values

### ModelCollision

*interface* · `packages/assets/src/library.ts:119`

```ts
interface ModelCollision
```

How a model collides when a game does not want its triangles: a box or a cylinder from the model's own base, in metres.

- `shape: 'box' | 'cylinder'`
- `size: [number, number, number]`
- `center: [number, number, number]`

### ModelInstance

*interface* · `packages/assets/src/cache.ts:3`

```ts
interface ModelInstance
```

What a loaded model gives a game.

- `root: BABYLON.TransformNode` — the model's top node(s): move this to place the model
- `meshes: BABYLON.AbstractMesh[]`
- `skeletons: BABYLON.Skeleton[]`
- `animations: Record<string, BABYLON.AnimationGroup>` — animation groups by name (stopped: a graph or a viewer plays them)
- `dispose(): void` — remove this instance from the scene; the cached source stays until released

### ModelItem

*interface* · `packages/assets/src/library.ts:122`

```ts
interface ModelItem extends ItemBase
```

A static mesh with its materials and textures in one glTF per tier (trees, rocks, props): placed by a game as many times as it likes (thin instances).

- `type: 'model'`
- `tiers: Partial<Record<LibraryTier, TierFile>>`
- `bounds: { min: [number, number, number]; max: [number, number, number] }` — bounding box in metres of the model at its origin
- `materials?: { name: string; alpha?: AlphaMode; doubleSided?: boolean }[]` — the materials in the file and how they draw (`mask` for leaves)
- `collision?: ModelCollision`
- `up?: '+Y'` — the axis that is up in the file (always `+Y`; written so a reader need not guess)

