diff --git a/.changeset/add-png-cobs-streams.md b/.changeset/add-png-cobs-streams.md new file mode 100644 index 0000000..7385ca2 --- /dev/null +++ b/.changeset/add-png-cobs-streams.md @@ -0,0 +1,12 @@ +--- +"@hsblabs/web-stream-extras": minor +--- + +Add PNG text chunk and COBS stream APIs + +- `createPNGTextChunkWriter` — embed arbitrary binary payloads into PNG tEXt chunks as a transform stream +- `extractPNGTextChunk` — extract a payload from a PNG tEXt chunk (all-at-once) +- `streamPNGTextChunk` — stream-extract a payload from a PNG tEXt chunk segment by segment +- `encodeCOBSFrame` / `decodeCOBSFrame` — frame-level COBS encode/decode +- `createCOBSEncoderStream` / `createCOBSDecoderStream` — COBS transform streams +- `writeCOBS` / `readCOBS` — high-level COBS pipe helpers diff --git a/.gitignore b/.gitignore index 283cccd..27d97af 100755 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ dist-ssr # tmp LESSONS.md TODO.md +docs/superpowers/ +.context/ diff --git a/.npmignore b/.npmignore index 2c5878e..0368744 100644 --- a/.npmignore +++ b/.npmignore @@ -7,6 +7,7 @@ .serena/ .vscode/ example/ +examples/ packages/ src/ bench/ diff --git a/README.md b/README.md index 8cdf5b5..63dcf32 100644 --- a/README.md +++ b/README.md @@ -11,9 +11,11 @@ Use this package when you need to: - build or consume `ReadableStream` pipelines - collect byte streams into a single `Uint8Array` - convert between strings and `Uint8Array` +- encode or decode delimiter-separated COBS frames - encode and decode base64 or base64url streams - decode, encode, split, and join text streams - encrypt or decrypt byte streams with the Web Streams API +- embed or extract binary payloads inside PNG `tEXt` chunks This makes it a good fit for: @@ -87,6 +89,47 @@ const result = await readAllBytes(decrypted); console.log(new TextDecoder().decode(result)); // "secret payload" ``` +### Encode and decode COBS frames + +```ts +import { + createCOBSDecoderStream, + createCOBSEncoderStream, +} from "@hsblabs/web-stream-extras/cobs"; +import { readableFromChunks } from "@hsblabs/web-stream-extras"; + +const rawFrames = readableFromChunks([ + Uint8Array.of(0x11, 0x22, 0x00, 0x33), + Uint8Array.of(0xff, 0xee), +]); + +const encoded = rawFrames.pipeThrough(createCOBSEncoderStream()); +const decoded = encoded.pipeThrough(createCOBSDecoderStream()); +``` + +### Embed and extract a binary payload in PNG + +```ts +import { + createPNGTextChunkWriter, + extractPNGTextChunk, + streamPNGTextChunk, +} from "@hsblabs/web-stream-extras/png"; +import { readAllBytes, readableFromChunks } from "@hsblabs/web-stream-extras"; + +const sourcePNG = readableFromChunks(pngBytes); +const payloadWriter = createPNGTextChunkWriter(sourcePNG); + +await readableFromChunks(fileBytes).pipeTo(payloadWriter.writable); +const rebuiltPNG = await readAllBytes(payloadWriter.readable); + +const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(rebuiltPNG)), +); + +const streamed = streamPNGTextChunk(readableFromChunks(rebuiltPNG)); +``` + ## Why Use It This package focuses on a small set of utilities that are useful in real byte-stream pipelines: @@ -94,7 +137,9 @@ This package focuses on a small set of utilities that are useful in real byte-st - `readableFromChunks()` for quickly creating a `ReadableStream` - `readAllChunks()` and `readAllBytes()` for consuming a stream - binary conversion helpers for strings and random byte generation +- `cobs` helpers for delimiter-separated frame encoding and decoding - `encryption` helpers for stream encryption without changing your stream-first API style +- `png` helpers for embedding and extracting binary payloads in PNG metadata The goal is to keep Web Streams code simple, predictable, and easy to compose. @@ -132,6 +177,36 @@ The `encryption` subpath provides stream encryption utilities for binary streams `webCryptoStream(masterKey)` is a higher-level helper for applications that manage stream keys with the Web Crypto API. It uses an `AES-GCM` master key to create encrypted 32-byte stream keys, then unwraps those keys before delegating to `encryptStream()` and `decryptStream()`. +### `@hsblabs/web-stream-extras/cobs` + +The `cobs` subpath provides Consistent Overhead Byte Stuffing helpers: + +- `encodeCOBSFrame` +- `decodeCOBSFrame` +- `createCOBSEncoderStream` +- `createCOBSDecoderStream` +- `readCOBS` +- `writeCOBS` + +`encodeCOBSFrame()` and `decodeCOBSFrame()` work on a single frame without the trailing delimiter. + +The stream helpers use `0x00` as the frame delimiter. Each input chunk to `createCOBSEncoderStream()` is treated as one raw frame, and `createCOBSDecoderStream()` emits one decoded frame for each delimiter-terminated encoded frame. + +### `@hsblabs/web-stream-extras/png` + +The `png` subpath provides binary payload helpers for PNG files: + +- `createPNGTextChunkWriter` +- `extractPNGTextChunk` +- `streamPNGTextChunk` + +`createPNGTextChunkWriter()` accepts a source PNG stream and returns a `{ writable, readable }` pair. Write arbitrary `Uint8Array` payload bytes into `writable`, then read the rebuilt PNG from `readable`. + +`createPNGTextChunkWriter()` forwards validated source chunks as they are read, keeps `IEND` pending, then appends internal payload chunks and the final `IEND` after `writable` closes. + +`extractPNGTextChunk()` reads a PNG stream, validates the embedded payload, and then returns it as `ReadableStream`. The extractor keeps all-or-nothing semantics rather than emitting payload bytes before the final manifest and CRC are checked. + +`streamPNGTextChunk()` is the late-error variant. It emits payload data segments as they arrive and only rejects at the end if the terminal manifest or payload CRC is invalid. ### `@hsblabs/web-stream-extras/text` The `text` subpath provides small convenience wrappers for text-oriented streams: @@ -210,11 +285,17 @@ console.log(new TextDecoder().decode(result)); // "secret payload" - Browsers: works in modern browsers with Web Streams support - Runtime APIs: - root utilities depend on the WHATWG Streams API + - `cobs` depends on the WHATWG Streams API - `encryption` depends on both the Web Streams API and Web Crypto + - `png` depends on the WHATWG Streams API ## Notes +- `cobs` is intentionally a subpath export. The root package stays focused on generic stream helpers. - `encryption` is intentionally a subpath export. The root package is not encryption-only. +- `png` is intentionally a subpath export. The root package stays focused on generic stream helpers. - This package does not handle authentication, password-based key derivation, user management, or key storage. - For `encryption`, you are expected to provide the raw encryption key (`Uint8Array`) yourself. - `webCryptoStream()` is optional. It is a convenience wrapper when you already manage a separate `AES-GCM` master key and want encrypted per-stream keys as strings. +- `png` stores payload bytes in internal `tEXt` chunks and does not expose low-level PNG metadata knobs in the public API. +- `png` stores payload bytes as multiple internal data segments plus one terminal manifest segment before `IEND`. diff --git a/docs/superpowers/plans/2026-03-22-png-text-chunk-implementation.md b/docs/superpowers/plans/2026-03-22-png-text-chunk-implementation.md deleted file mode 100644 index 5672f48..0000000 --- a/docs/superpowers/plans/2026-03-22-png-text-chunk-implementation.md +++ /dev/null @@ -1,257 +0,0 @@ -# PNG Text Chunk Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a new `@hsblabs/web-stream-extras/png` subpath that can embed arbitrary binary data into PNG `tEXt` chunks and extract it back as `ReadableStream`. - -**Architecture:** Keep the root package unchanged and add a dedicated `png` public entry mirroring the existing `encryption` subpath pattern. Implement PNG-specific helpers locally in `src/png/` for COBS, CRC32, and chunk framing, then build two public stream APIs on top: a coupled writer pair for embedding and a readable extractor for extraction. - -**Tech Stack:** TypeScript, WHATWG Streams, Vitest, pnpm, tsdown - ---- - -### Task 1: Add Public Surface And Failing API Tests - -**Files:** -- Create: `src/png.ts` -- Create: `src/png/public.ts` -- Modify: `package.json` -- Modify: `tsdown.config.ts` -- Test: `src/png.test.ts` - -- [ ] **Step 1: Write the failing public API tests** - -```ts -import { describe, expect, it } from "vitest"; -import * as pngApi from "./png"; - -describe("png public API", () => { - it("exports the supported png helpers", () => { - expect(Object.keys(pngApi).sort()).toEqual([ - "createPNGTextChunkWriter", - "extractPNGTextChunk", - ]); - }); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: FAIL because `./png` does not exist or exports are missing - -- [ ] **Step 3: Write minimal public entry implementation** - -```ts -export type { PNGTextChunkWriteOptions, PNGTextChunkWriter } from "./png/public"; -export { createPNGTextChunkWriter, extractPNGTextChunk } from "./png/public"; -``` - -- [ ] **Step 4: Wire packaging for the new subpath** - -Update `package.json` exports and `tsdown.config.ts` entries to include `./png` and `src/png.ts`. - -- [ ] **Step 5: Run test to verify it passes** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: PASS for the export surface assertion - -### Task 2: Build Low-Level PNG Helpers With Red-Green Tests - -**Files:** -- Create: `src/png/constants.ts` -- Create: `src/png/cobs.ts` -- Create: `src/png/crc32.ts` -- Create: `src/png/framing.ts` -- Modify: `src/png.test.ts` - -- [ ] **Step 1: Write failing tests for COBS, CRC32, and PNG framing** - -```ts -it("round-trips arbitrary bytes through COBS", () => { - expect(decodeCOBS(encodeCOBS(new Uint8Array([0, 1, 0, 2])))).toEqual( - new Uint8Array([0, 1, 0, 2]), - ); -}); - -it("builds a valid tEXt chunk with matching CRC", () => { - const chunk = createTextChunk(new Uint8Array([1, 2, 3])); - expect(parseChunk(chunk).type).toBe("tEXt"); -}); - -it("encodes and validates a versioned payload segment header", () => { - const encoded = createPayloadSegment({ - segmentData: new Uint8Array([1, 2, 3]), - segmentIndex: 0, - segmentCount: 2, - payloadCrc32: 123, - isFirst: true, - isLast: false, - }); - expect(parsePayloadSegment(encoded).segmentIndex).toBe(0); -}); -``` - -- [ ] **Step 2: Run test to verify it fails** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: FAIL because helper modules/functions are missing - -- [ ] **Step 3: Implement minimal helper modules** - -```ts -// cobs.ts -export function encodeCOBS(input: Uint8Array): Uint8Array {} -export function decodeCOBS(input: Uint8Array): Uint8Array {} - -// crc32.ts -export function crc32(input: Uint8Array): number {} - -// framing.ts -export function parsePNGSignature(...) {} -export function parseChunkHeader(...) {} -export function createTextChunk(...) {} -``` - -- [ ] **Step 4: Run helper tests to verify they pass** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: PASS for helper-level tests - -- [ ] **Step 5: Refactor helper boundaries without changing behavior** - -Keep PNG-specific logic under `src/png/` and avoid moving it into `src/shared/` yet. - -### Task 3: Implement `extractPNGTextChunk()` With TDD - -**Files:** -- Modify: `src/png/public.ts` -- Modify: `src/png/framing.ts` -- Modify: `src/png/constants.ts` -- Modify: `src/png.test.ts` - -- [ ] **Step 1: Write failing extraction tests** - -```ts -it("extracts an embedded payload", async () => { - const payload = await readAllBytes(extractPNGTextChunk(readableFromChunks(png))); - expect(payload).toEqual(expectedPayload); -}); - -it("extracts payloads split across multiple tEXt chunks", async () => { - const payload = await readAllBytes(extractPNGTextChunk(readableFromChunks(png))); - expect(payload).toEqual(expectedPayload); -}); - -it("fails when no embedded payload exists", async () => { - await expect(readAllBytes(extractPNGTextChunk(readableFromChunks(png)))).rejects.toThrow(); -}); -``` - -- [ ] **Step 2: Run extraction tests to verify they fail** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: FAIL because extraction behavior is not implemented - -- [ ] **Step 3: Implement the minimal extractor** - -```ts -export function extractPNGTextChunk( - png: ReadableStream, -): ReadableStream { - // parse PNG, collect matching tEXt chunks, validate segment headers, - // stream segment data, and throw on malformed input -} -``` - -- [ ] **Step 4: Run extraction tests to verify they pass** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: PASS for extraction happy-path and error-path tests - -- [ ] **Step 5: Add edge-case tests and keep green** - -Cover empty payload sentinel, bad signature, invalid non-ASCII chunk type, missing `IEND`, trailing bytes after `IEND`, truncated chunks, CRC mismatch, COBS decode failure, invalid `magic`/`version`, `segmentCount = 0`, inconsistent `segmentCount` / `payloadCrc32`, missing or duplicate or reverse-order segment indexes, payload CRC mismatch, and PNG input split 1 byte at a time. - -### Task 4: Implement `createPNGTextChunkWriter()` With TDD - -**Files:** -- Modify: `src/png/public.ts` -- Create: `src/png/transformers.ts` -- Modify: `src/png/framing.ts` -- Modify: `src/png.test.ts` - -- [ ] **Step 1: Write failing writer tests** - -```ts -it("embeds payload chunks before IEND and round-trips with extractor", async () => { - const writer = createPNGTextChunkWriter(readableFromChunks(sourcePng)); - await readableFromChunks(payloadChunks).pipeTo(writer.writable); - const rebuilt = await readAllBytes(writer.readable); - expect(await readAllBytes(extractPNGTextChunk(readableFromChunks(rebuilt)))).toEqual(payload); -}); - -it("replaces an existing embedded payload when onExisting is replace", async () => { - // create PNG with existing embedded chunks and verify replacement -}); - -it("waits at IEND until payload close", async () => { - // start reading output before closing the payload side and verify final bytes are withheld -}); -``` - -- [ ] **Step 2: Run writer tests to verify they fail** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: FAIL because writer behavior and lifecycle coupling are not implemented - -- [ ] **Step 3: Implement the minimal writer pair** - -```ts -export function createPNGTextChunkWriter( - png: ReadableStream, - options?: PNGTextChunkWriteOptions, -): PNGTextChunkWriter { - // accept payload writes, validate source PNG, buffer payload segments, - // insert them before IEND, and couple close/error propagation -} -``` - -- [ ] **Step 4: Run writer tests to verify they pass** - -Run: `pnpm exec vitest run src/png.test.ts` -Expected: PASS for round-trip, replace/error, and lifecycle tests - -- [ ] **Step 5: Add cancellation and abort propagation tests** - -Verify `readable.cancel(reason)` cancels the source PNG reader and propagates the same `reason` to the writer, `writable.abort(reason)` cancels the source PNG reader and propagates the same `reason` to the output, the default `onExisting` behavior is `"error"`, late source PNG failures reject pending `close()`/`pipeTo()` promises, subsequent `write()` / `close()` calls reject after source-side failures, `close()` / `pipeTo()` resolve only after segment insertion and output close, payload written 1 byte at a time still succeeds, `0x00`-heavy payloads still succeed, empty payload emits the sentinel segment, embedded chunks are inserted immediately before `IEND`, output waits at `IEND` until payload close, and the fixed internal keyword is used consistently. - -### Task 5: Finish Documentation And Full Verification - -**Files:** -- Modify: `README.md` -- Modify: `TODO.md` -- Modify: `LESSONS.md` - -- [ ] **Step 1: Update README with a focused png example** - -```ts -import { createPNGTextChunkWriter, extractPNGTextChunk } from "@hsblabs/web-stream-extras/png"; -``` - -- [ ] **Step 2: Run targeted tests** - -Run: `pnpm exec vitest run src/png.test.ts src/public-api.test.ts` -Expected: PASS - -- [ ] **Step 3: Run repository verification** - -Run: `pnpm test` -Expected: PASS - -Run: `pnpm build` -Expected: PASS - -- [ ] **Step 4: Review diffs and prepare handoff** - -Confirm the new subpath is documented, buildable, and covered by tests before handing back results. diff --git a/docs/superpowers/specs/2026-03-22-png-text-chunk-design.md b/docs/superpowers/specs/2026-03-22-png-text-chunk-design.md deleted file mode 100644 index 263a0a5..0000000 --- a/docs/superpowers/specs/2026-03-22-png-text-chunk-design.md +++ /dev/null @@ -1,252 +0,0 @@ -# PNG Text Chunk Binary Stream Design - -**Date:** 2026-03-22 - -**Status:** Proposed - -## Goal - -PNG の `tEXt` チャンクを使って任意バイナリを埋め込み・取り出しできる Web Streams API ベースの API を `@hsblabs/web-stream-extras/png` として追加する。 - -利用者が欲しいものは次の 2 つに絞る。 - -- バイナリを書き込むと、元の PNG を再構成した `ReadableStream` を得られる `WritableStream` -- PNG を読むと、埋め込まれた任意バイナリを `ReadableStream` として取り出せる API - -## Non-Goals - -- PNG 画像のデコード、描画、圧縮率最適化 -- `zTXt` や `iTXt` の初期対応 -- 利用者が任意の PNG `keyword` を指定する API -- 複数種類の埋め込み payload を同じ PNG に共存させる汎用メタデータ層 - -## Public API - -新しい subpath を追加する。 - -```ts -import { - createPNGTextChunkWriter, - extractPNGTextChunk, -} from "@hsblabs/web-stream-extras/png"; -``` - -```ts -export interface PNGTextChunkWriteOptions { - onExisting?: "error" | "replace"; -} - -export interface PNGTextChunkWriter { - writable: WritableStream; - readable: ReadableStream; -} - -export function createPNGTextChunkWriter( - png: ReadableStream, - options?: PNGTextChunkWriteOptions, -): PNGTextChunkWriter; - -export function extractPNGTextChunk( - png: ReadableStream, -): ReadableStream; -``` - -### API rationale - -- `createPNGTextChunkWriter()` は、利用者が任意のファイルを `WritableStream` に流し込めるようにするための低レベル API である -- 再構成 PNG は `writer.readable` から読む -- `extractPNGTextChunk()` は埋め込まれた payload だけを返す。PNG 自体の再出力はしない -- `keyword` は公開 options に出さない。PNG 上の識別子は内部詳細とし、外部 API から隠す - -## PNG Storage Format - -### Why `tEXt` - -`tEXt` は `keyword + NUL + text` の構造を持つ。`text` には `NUL` を含められないため、生バイナリはそのまま書けない。 - -`base64` は約 33% 膨らむため、初期設計では採用しない。代わりに COBS を使い、`0x00` を含まない byte 列へ変換して `tEXt` の text に載せる。 - -### Internal chunk identification - -- `keyword` はライブラリ内部の固定値を使う -- 利用者はこの値を指定しない -- extractor はこの固定 `keyword` を持つ `tEXt` チャンク列を payload として扱う - -### Payload segmentation - -payload 全体を 1 個の `tEXt` にまとめると、chunk length の都合で書き込み側が全 payload を事前に保持しやすい。初期実装では payload を複数の `tEXt` chunk に分割する。 - -各 payload segment は次の順で格納する。 - -1. 元の payload の一部を固定上限まで読む -2. segment header を先頭に付ける -3. segment payload と header をまとめて COBS encode する -4. `tEXt` chunk の text field に encode 結果を書き込む - -extract 側は各 `tEXt` chunk の text を個別に COBS decode し、segment header を検証した上で segment payload を連結して出力する。 - -### Segment wire format - -COBS decode 後の各 segment は次の binary header を持つ。 - -```text -magic[4] = ASCII "HSBP" -version[1] = 0x01 -flags[1] = bit0:first, bit1:last -segmentIndex[4] = uint32 big-endian -segmentCount[4] = uint32 big-endian -payloadCrc32[4] = uint32 big-endian -segmentData[n] -``` - -header の意図は次の通り。 - -- `magic` と `version` で将来の format 変更を識別する -- `segmentIndex` と `segmentCount` で欠落や並び替えを検出する -- `payloadCrc32` で payload 全体の整合性を検証する -- empty payload は `segmentCount = 1`, `segmentIndex = 0`, `flags = first|last`, `segmentData = empty` の 1 segment で表現する - -### Segment ordering - -- payload segment 用 `tEXt` chunks はすべて `IEND` 直前に挿入する -- replace 時は既存の内部 `keyword` の `tEXt` chunks を全て除去してから、新しい segment 群を `IEND` 直前に挿入する -- extractor は PNG 内に現れる順に segment を読み、`segmentIndex` が `0..segmentCount-1` で単調増加していることを要求する - -### Integrity - -- parser は PNG signature を検証する -- 各 chunk の length, type, data, CRC を検証する -- writer が新規に生成する `tEXt` chunks は CRC を再計算して書く -- 既存 chunk の CRC 不整合は error にする - -## Behavior Rules - -### `createPNGTextChunkWriter()` - -- `png` は元画像の byte stream -- 返却された `writable` に payload bytes を書き込む -- payload の受け取りと PNG 再構成は並行して進むが、payload segments は `IEND` 直前に挿入するため、writer は payload close まで segment 情報を保持する -- `readable` は入力 PNG をそのまま流しつつ、`IEND` 直前で payload `tEXt` chunks を差し込む -- source PNG が先に `IEND` まで到達しても、`writable.close()` されるまでは `readable` はその位置で待機する - -#### Lifecycle coupling - -- `writable.close()` は payload の終端を表す -- `writable.close()` が返す promise は、payload 受付だけでは resolve しない。source PNG の検証、`IEND` 直前への segment 挿入、`readable` への終端反映まで成功した時点で resolve する -- source PNG の parse / CRC / `onExisting` 失敗が payload close 後に発覚した場合でも、その失敗は保留中の `writable.close()` promise を reject する -- `WritableStreamDefaultWriter.close()` の代わりに `payload.pipeTo(writer.writable)` を使う場合も、返却 promise は同じ完了条件に従う -- `readable` は source PNG の検証完了と payload 埋め込み完了の両方がそろった時点で close する -- `readable.cancel(reason)` されたら source PNG reader を cancel し、`writable` は同じ reason で error 状態にする -- `writable.abort(reason)` されたら `readable` は同じ reason で error にし、source PNG reader を cancel する -- source PNG の read error、PNG parse error、CRC error、`onExisting: "error"` の衝突は `readable` を error にし、以後の `writable.write()` / `close()` も同じ reason で reject する - -#### Memory model - -- 初期実装は constant-memory ではない -- `IEND` 直前へ挿入する設計上、最悪ケースでは payload segment 群を `readable` 完了まで保持するため、追加メモリは `O(payload)` になる -- segment 分割の目的は 1 個の巨大 contiguous buffer を避け、wire format と処理単位を明確にすることにある - -#### `onExisting` - -- default は `"error"` -- `"error"`: - - 内部 `keyword` の既存 `tEXt` chunk を 1 個でも見つけたら失敗 -- `"replace"`: - - 内部 `keyword` の既存 `tEXt` chunks をすべて除去し、新しい payload segments に置き換える - -### `extractPNGTextChunk()` - -- 固定 `keyword` を持つ `tEXt` chunks をすべて検出する -- text field を順番に COBS decode し、segment header を検証してから `segmentData` を `ReadableStream` に流す -- 対象 chunk が 1 個もなければ error -- 対象 chunk の COBS decode に失敗したら error -- `magic`, `version`, `segmentCount`, `payloadCrc32` が segment 間で一致しなければ error -- 最終 segment 読了時に payload CRC32 を検証し、不一致なら error - -### Empty payload - -- writer は empty payload でも 1 つの sentinel segment を出力する -- extractor は sentinel segment を空の payload として扱い、空 stream を正常終了する - -## Error Handling - -次は明示的に error とする。 - -- PNG signature が不正 -- chunk header が途中で切れている -- chunk body または CRC が途中で切れている -- chunk type が ASCII 英字 4 byte でない -- CRC mismatch -- `IEND` が存在しない -- `IEND` 後に余剰データがある -- `"error"` 指定時に既存埋め込み chunk が見つかる -- extractor で対象 chunk が存在しない -- COBS decode 不能 -- segment `magic` または `version` が不正 -- segment index が欠落・重複・逆順 -- segment count が 0 -- payload CRC32 mismatch - -## Implementation Outline - -### File layout - -- `src/png.ts` - - public entry re-export -- `src/png/public.ts` - - 公開型と公開関数 -- `src/png/constants.ts` - - PNG signature, internal keyword, segment size 上限 -- `src/png/cobs.ts` - - COBS encode/decode -- `src/png/crc32.ts` - - CRC32 計算 -- `src/png/framing.ts` - - PNG chunk parse / build helper -- `src/png/transformers.ts` - - writer / extractor の stream 実装 -- `src/png.test.ts` - - public API ベースの統合テスト - -### Stream architecture - -- PNG parser は `ByteQueue` を使って signature / chunk header / CRC を段階的に読む -- 既存 PNG の chunk body は必要以上に保持せず、そのまま出力へ流す -- payload 側は writer 内部で独立した queue に蓄積し、close 時に segment 列と payload CRC32 を確定させる -- `IEND` を読むまでは payload chunks を出さず、payload close 後に `IEND` 直前でまとめて排出する -- source PNG が `IEND` まで先に進んだ場合は、payload close までそこで待機する - -## Testing Strategy - -最低限、次を cover する。 - -- writer + extractor の round-trip -- payload を 1 byte 分割で書き込んでも round-trip する -- PNG 入力を 1 byte 分割で渡しても動く -- payload に `0x00` を大量に含んでも round-trip する -- empty payload を round-trip する -- `onExisting: "error"` で既存埋め込みがあると失敗する -- `onExisting: "replace"` で既存埋め込みを置き換える -- 埋め込み chunk 群が `IEND` 直前に入る -- source PNG が先に `IEND` へ到達しても payload close 待ちをする -- `readable.cancel()` と `writable.abort()` の error 伝播が期待通りに動く -- malformed PNG signature を reject する -- truncated chunk header / body / CRC を reject する -- CRC mismatch を reject する -- extractor が対象 chunk 不在で失敗する -- segment index 欠落・逆順・重複を reject する -- payload CRC mismatch を reject する - -## Documentation Updates - -- `package.json.exports` に `./png` を追加する -- `tsdown.config.ts` の entry に `src/png.ts` を追加する -- `README.md` に `png` subpath の概要とサンプルを追加する -- root public API テストは不変のまま維持し、`png` 用の public API テストを新設する - -## Open Questions Resolved - -- payload は `string` ではなく任意バイナリとして扱う -- `keyword` は公開 options にしない -- サイズ効率は `base64` ではなく COBS を優先する -- payload は 1 chunk ではなく複数 `tEXt` chunks に分割し、wire format version と整合性検証を持たせる diff --git a/examples/cobs/index.ts b/examples/cobs/index.ts new file mode 100644 index 0000000..d877e2b --- /dev/null +++ b/examples/cobs/index.ts @@ -0,0 +1,124 @@ +/** + * COBS (Consistent Overhead Byte Stuffing) usage examples + * + * Run: node --experimental-strip-types ./cobs/index.ts + * or: pnpm --filter examples run cobs (from the repo root) + */ + +import { + createCOBSDecoderStream, + createCOBSEncoderStream, + decodeCOBSFrame, + encodeCOBSFrame, + readCOBS, + writeCOBS, +} from "@hsblabs/web-stream-extras/cobs"; + +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +// ── 1. Frame-level API ──────────────────────────────────────────────────────── +// +// COBS replaces every 0x00 byte so that the resulting encoded frame contains no +// null bytes. A 0x00 delimiter can then be appended to mark the end of the +// frame on a byte-stream transport. + +console.log("=== 1. Frame-level encode / decode ==="); + +const message = enc.encode("hello\x00world"); // contains a null byte +const encoded = encodeCOBSFrame(message); +const decoded = decodeCOBSFrame(encoded); + +console.log("Input bytes :", [...message]); +console.log("Encoded (no 0x00):", [...encoded]); +console.log( + "Contains 0x00? :", + encoded.includes(0), + "(should be false)", +); +console.log( + "Round-trip match:", + decoded.length === message.length && decoded.every((b, i) => b === message[i]), +); + +// ── 2. Transform stream API ─────────────────────────────────────────────────── +// +// When frames arrive as a stream, pipe through createCOBSEncoderStream() and +// createCOBSDecoderStream(). The encoder appends a 0x00 delimiter after each +// encoded frame; the decoder strips delimiters and reconstructs original frames. + +console.log("\n=== 2. Transform stream API ==="); + +const frames: Uint8Array[] = [ + enc.encode("first frame"), + enc.encode("second\x00frame"), // null byte in the middle + Uint8Array.of(0x01, 0x00, 0x02, 0x03), // binary data with null +]; + +const encodedStream = new ReadableStream({ + start(controller) { + for (const f of frames) controller.enqueue(f); + controller.close(); + }, +}).pipeThrough(createCOBSEncoderStream()); + +const roundTripStream = encodedStream.pipeThrough(createCOBSDecoderStream()); + +const reader = roundTripStream.getReader(); +let idx = 0; +while (true) { + const { done, value } = await reader.read(); + if (done) break; + const original = frames[idx]!; + const ok = + value.length === original.length && value.every((b, i) => b === original[i]); + console.log(`Frame ${idx} round-trip: ${ok ? "✓ OK" : "✗ MISMATCH"}`); + idx++; +} + +// ── 3. writeCOBS / readCOBS ─────────────────────────────────────────────────── +// +// writeCOBS(sink) wraps a WritableStream so that each written chunk is +// automatically COBS-encoded with a trailing 0x00 before being forwarded. +// readCOBS(source) wraps a ReadableStream and yields decoded frames. +// +// Typical pattern: connect them via a pass-through or actual transport stream. +// Drive both ends concurrently to avoid backpressure deadlocks. + +console.log("\n=== 3. writeCOBS / readCOBS ==="); + +const { readable, writable } = new TransformStream(); +const encSink = writeCOBS(writable); +const decSource = readCOBS(readable); + +// Read all decoded frames concurrently with the writes. +const readAllDecodedPromise = (async (): Promise => { + const chunks: Uint8Array[] = []; + const r = decSource.getReader(); + while (true) { + const { done, value } = await r.read(); + if (done) break; + chunks.push(value); + } + return chunks; +})(); + +const writeAllPromise = (async () => { + const writer = encSink.getWriter(); + await writer.write(enc.encode("high-level COBS API")); + await writer.write(Uint8Array.of(0xde, 0xad, 0x00, 0xbe, 0xef)); // binary with null + await writer.close(); +})(); + +const [decodedFrames] = await Promise.all([readAllDecodedPromise, writeAllPromise]); + +for (const value of decodedFrames) { + if ([...value].every((b) => b >= 0x20 && b < 0x80)) { + console.log("Decoded (text) :", dec.decode(value)); + } else { + console.log( + "Decoded (hex) :", + [...value].map((b) => b.toString(16).padStart(2, "0")).join(" "), + ); + } +} diff --git a/examples/package.json b/examples/package.json new file mode 100644 index 0000000..723cb3f --- /dev/null +++ b/examples/package.json @@ -0,0 +1,18 @@ +{ + "name": "examples", + "private": true, + "type": "module", + "scripts": { + "cobs": "node --experimental-strip-types ./cobs/index.ts", + "png": "node --experimental-strip-types ./png/index.ts" + }, + "dependencies": { + "@hsblabs/web-stream-extras": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:" + }, + "engines": { + "node": ">=22" + } +} diff --git a/examples/png/index.ts b/examples/png/index.ts new file mode 100644 index 0000000..b9b8d16 --- /dev/null +++ b/examples/png/index.ts @@ -0,0 +1,141 @@ +/** + * PNG text-chunk embed / extract usage examples + * + * Run: node --experimental-strip-types ./png/index.ts + * or: pnpm --filter examples run png (from the repo root) + * + * Requires the library to be built first: + * pnpm build (from the repo root) + */ + +import { readAllBytes, readableFromChunks } from "@hsblabs/web-stream-extras"; +import { + createPNGTextChunkWriter, + extractPNGTextChunk, + streamPNGTextChunk, +} from "@hsblabs/web-stream-extras/png"; + + +// A minimal valid 1×1 pixel PNG used as the base image in these examples. +// Replace this with a real PNG read from disk for real-world use. +const MINIMAL_PNG = Buffer.from( + "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", + "base64", +); +const enc = new TextEncoder(); +const dec = new TextDecoder(); + +// ── Helper: embed a payload into a PNG ──────────────────────────────────────── +// +// createPNGTextChunkWriter returns a { readable, writable } pair. +// Both sides must be driven concurrently: pipe the payload into writable while +// consuming readable to get the output PNG bytes. + +async function embed( + pngBytes: Uint8Array, + payload: Uint8Array, +): Promise { + const writer = createPNGTextChunkWriter(readableFromChunks(pngBytes)); + + const [, result] = await Promise.all([ + readableFromChunks(payload).pipeTo(writer.writable), + readAllBytes(writer.readable), + ]); + + return result; +} + +// ── 1. Embed a text payload into a PNG ─────────────────────────────────────── + +console.log("=== 1. Embed payload ==="); + +const payload = enc.encode("Hello from PNG payload!"); +const embeddedPng = await embed(MINIMAL_PNG, payload); + +console.log(`Source PNG size : ${MINIMAL_PNG.byteLength} bytes`); +console.log(`Payload size : ${payload.byteLength} bytes`); +console.log(`Embedded PNG size: ${embeddedPng.byteLength} bytes`); +console.log( + "Still valid PNG? :", + embeddedPng[0] === 0x89 && embeddedPng[1] === 0x50, +); + +// ── 2. Extract with all-or-nothing semantics ────────────────────────────────── +// +// extractPNGTextChunk buffers everything and only emits after the manifest +// chunk has been validated. Use this when you need guaranteed integrity before +// processing the payload. + +console.log("\n=== 2. extractPNGTextChunk (all-or-nothing) ==="); + +const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(embeddedPng)), +); + +console.log("Extracted payload:", dec.decode(extracted)); +console.log( + "Matches original :", + extracted.length === payload.length && + extracted.every((b, i) => b === payload[i]), +); + +// ── 3. Stream with early emission ───────────────────────────────────────────── +// +// streamPNGTextChunk yields payload segments as they arrive, before the +// terminal manifest has been validated. Useful for large payloads where you +// want to start processing immediately. Any integrity error is reported on a +// subsequent read. + +console.log("\n=== 3. streamPNGTextChunk (streaming) ==="); + +// Embed a larger payload so multiple segments are emitted. +const largePayload = enc.encode("A".repeat(64 * 1024)); // 64 KiB, spans two segments +const largePng = await embed(MINIMAL_PNG, largePayload); + +const segmentReader = streamPNGTextChunk( + readableFromChunks(largePng), +).getReader(); + +const segments: Uint8Array[] = []; +while (true) { + const { done, value } = await segmentReader.read(); + if (done) break; + segments.push(value); + console.log(` Received segment: ${value.byteLength} bytes`); +} + +const reassembled = new Uint8Array( + segments.reduce((s, c) => s + c.byteLength, 0), +); +let offset = 0; +for (const seg of segments) { + reassembled.set(seg, offset); + offset += seg.byteLength; +} +console.log( + "Reassembled matches original:", + reassembled.length === largePayload.length && + reassembled.every((b, i) => b === largePayload[i]), +); + +// ── 4. Re-embed with { onExisting: "replace" } ─────────────────────────────── +// +// By default the writer throws if the source PNG already contains an embedded +// payload. Pass { onExisting: "replace" } to overwrite it silently. + +console.log("\n=== 4. Replace existing payload ==="); + +const replacement = enc.encode("Updated payload"); +const replaceWriter = createPNGTextChunkWriter( + readableFromChunks(embeddedPng), + { onExisting: "replace" }, +); +const [, replacedPng] = await Promise.all([ + readableFromChunks(replacement).pipeTo(replaceWriter.writable), + readAllBytes(replaceWriter.readable), +]); + +const replacedExtracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(replacedPng)), +); +console.log("Replaced payload:", dec.decode(replacedExtracted)); diff --git a/examples/tsconfig.json b/examples/tsconfig.json new file mode 100644 index 0000000..e75ad2e --- /dev/null +++ b/examples/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "allowImportingTsExtensions": false, + "noEmit": true, + "isolatedModules": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "strictFunctionTypes": true, + "skipLibCheck": true, + "lib": ["ESNext"], + "types": ["node"] + }, + "include": ["./**/*.ts"] +} diff --git a/package.json b/package.json index 4649243..ca68fe3 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,14 @@ "changeset": "changeset", "changeset:version": "changeset version", "changeset:publish": "changeset publish", - "publish:check": "biome check ./src ./bench && pnpm test && pnpm build", + "typecheck": "tsc -p tsconfig.typecheck.json", + "publish:check": "biome check ./src ./bench && pnpm typecheck && pnpm test && pnpm build", "prepublishOnly": "pnpm publish:check", "test": "vitest run ./src", "test:watch": "vitest ./src", - "test:static": "pnpm lint && pnpm fmt", - "lint": "biome check ./src ./bench --write", - "fmt": "biome format --write ./src ./bench" + "lint": "biome check ./src ./bench ./scripts", + "lint:fix": "biome check ./src ./bench ./scripts --write", + "format": "biome format --write ./src ./bench ./scripts" }, "files": [ "dist", @@ -26,8 +27,10 @@ "exports": { ".": "./dist/index.js", "./base64": "./dist/base64.js", + "./cobs": "./dist/cobs.js", "./encryption": "./dist/encryption.js", "./jsonl": "./dist/jsonl.js", + "./png": "./dist/png.js", "./text": "./dist/text.js", "./package.json": "./package.json" }, @@ -55,10 +58,10 @@ "devDependencies": { "@biomejs/biome": "2.4.5", "@changesets/cli": "^2.31.0", - "@types/node": "^25.3.3", - "tsdown": "^0.21.0-beta.2", + "@types/node": "catalog:", + "tsdown": "^0.21.10", "typescript": "6.0.0-beta", - "vitest": "^4.0.18" + "vitest": "^4.1.5" }, "packageManager": "pnpm@10.33.2" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2224ba5..1a5da51 100755 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,12 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + '@types/node': + specifier: ^25.6.0 + version: 25.6.0 + importers: .: @@ -13,36 +19,46 @@ importers: version: 2.4.5 '@changesets/cli': specifier: ^2.31.0 - version: 2.31.0(@types/node@25.3.3) + version: 2.31.0(@types/node@25.6.0) '@types/node': - specifier: ^25.3.3 - version: 25.3.3 + specifier: 'catalog:' + version: 25.6.0 tsdown: - specifier: ^0.21.0-beta.2 - version: 0.21.0-beta.2(typescript@6.0.0-beta) + specifier: ^0.21.10 + version: 0.21.10(typescript@6.0.0-beta) typescript: specifier: 6.0.0-beta version: 6.0.0-beta vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.3)(yaml@2.4.2) + specifier: ^4.1.5 + version: 4.1.5(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(yaml@2.4.2)) + + examples: + dependencies: + '@hsblabs/web-stream-extras': + specifier: workspace:* + version: link:.. + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 25.3.3 packages: - '@babel/generator@8.0.0-rc.1': - resolution: {integrity: sha512-3ypWOOiC4AYHKr8vYRVtWtWmyvcoItHtVqF8paFax+ydpmUdPsJpLBkBBs5ItmhdrwC3a0ZSqqFAdzls4ODP3w==} + '@babel/generator@8.0.0-rc.3': + resolution: {integrity: sha512-em37/13/nR320G4jab/nIIHZgc2Wz2y/D39lxnTyxB4/D/omPQncl/lSdlnJY1OhQcRGugTSIF2l/69o31C9dA==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-string-parser@8.0.0-rc.2': - resolution: {integrity: sha512-noLx87RwlBEMrTzncWd/FvTxoJ9+ycHNg0n8yyYydIoDsLZuxknKgWRJUqcrVkNrJ74uGyhWQzQaS3q8xfGAhQ==} + '@babel/helper-string-parser@8.0.0-rc.3': + resolution: {integrity: sha512-AmwWFx1m8G/a5cXkxLxTiWl+YEoWuoFLUCwqMlNuWO1tqAYITQAbCRPUkyBHv1VOFgfjVOqEj6L3u15J5ZCzTA==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/helper-validator-identifier@8.0.0-rc.1': - resolution: {integrity: sha512-I4YnARytXC2RzkLNVnf5qFNFMzp679qZpmtw/V3Jt2uGnWiIxyJtaukjG7R8pSx8nG2NamICpGfljQsogj+FbQ==} + '@babel/helper-validator-identifier@8.0.0-rc.3': + resolution: {integrity: sha512-8AWCJ2VJJyDFlGBep5GpaaQ9AAaE/FjAcrqI7jyssYhtL7WGV0DOKpJsQqM037xDbpRLHXsY8TwU7zDma7coOw==} engines: {node: ^20.19.0 || >=22.12.0} - '@babel/parser@8.0.0-rc.1': - resolution: {integrity: sha512-6HyyU5l1yK/7h9Ki52i5h6mDAx4qJdiLQO4FdCyJNoB/gy3T3GGJdhQzzbZgvgZCugYBvwtQiWRt94QKedHnkA==} + '@babel/parser@8.0.0-rc.3': + resolution: {integrity: sha512-B20dvP3MfNc/XS5KKCHy/oyWl5IA6Cn9YjXRdDlCjNmUFrjvLXMNUfQq/QUy9fnG2gYkKKcrto2YaF9B32ToOQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -50,8 +66,8 @@ packages: resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/types@8.0.0-rc.1': - resolution: {integrity: sha512-ubmJ6TShyaD69VE9DQrlXcdkvJbmwWPB8qYj0H2kaJi29O7vJT9ajSdBd2W8CG34pwL9pYA74fi7RHC1qbLoVQ==} + '@babel/types@8.0.0-rc.3': + resolution: {integrity: sha512-mOm5ZrYmphGfqVWoH5YYMTITb3cDXsFgmvFlvkvWDMsR9X8RFnt7a0Wb6yNIdoFsiMO9WjYLq+U/FMtqIYAF8Q==} engines: {node: ^20.19.0 || >=22.12.0} '@biomejs/biome@2.4.5': @@ -166,167 +182,167 @@ packages: '@changesets/write@0.4.0': resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@esbuild/aix-ppc64@0.27.3': - resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.27.3': - resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.27.3': - resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.27.3': - resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.27.3': - resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.27.3': - resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.27.3': - resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.3': - resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.27.3': - resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.27.3': - resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.27.3': - resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.27.3': - resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.27.3': - resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.27.3': - resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.27.3': - resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.27.3': - resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.27.3': - resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.27.3': - resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.3': - resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.27.3': - resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.3': - resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.27.3': - resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.27.3': - resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.27.3': - resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.27.3': - resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.27.3': - resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -359,8 +375,11 @@ packages: '@manypkg/get-packages@1.1.3': resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} - '@napi-rs/wasm-runtime@1.1.1': - resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -374,231 +393,245 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@oxc-project/types@0.114.0': - resolution: {integrity: sha512-//nBfbzHQHvJs8oFIjv6coZ6uxQ4alLfiPe6D5vit6c4pmxATHHlVwgB1k+Hv4yoAMyncdxgRBF5K4BYWUCzvA==} + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} '@quansync/fs@1.0.0': resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==} - '@rolldown/binding-android-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-zCEmUrt1bggwgBgeKLxNj217J1OrChrp3jJt24VK9jAharSTeVaHODNL+LpcQVhRz+FktYWfT9cjo5oZ99ZLpg==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-ZP9xb9lPAex36pvkNWCjSEJW/Gfdm9I3ssiqOFLmpZ/vosPXgpoGxCmh+dX1Qs+/bWQE6toNFXWWL8vYoKoK9Q==} + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.0.0-rc.5': - resolution: {integrity: sha512-7IdrPunf6dp9mywMgTOKMMGDnMHQ6+h5gRl6LW8rhD8WK2kXX0IwzcM5Zc0B5J7xQs8QWOlKjv8BJsU/1CD3pg==} + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.0.0-rc.5': - resolution: {integrity: sha512-o/JCk+dL0IN68EBhZ4DqfsfvxPfMeoM6cJtxORC1YYoxGHZyth2Kb2maXDb4oddw2wu8iIbnYXYPEzBtAF5CAg==} + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': - resolution: {integrity: sha512-IIBwTtA6VwxQLcEgq2mfrUgam7VvPZjhd/jxmeS1npM+edWsrrpRLHUdze+sk4rhb8/xpP3flemgcZXXUW6ukw==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': - resolution: {integrity: sha512-KSol1De1spMZL+Xg7K5IBWXIvRWv7+pveaxFWXpezezAG7CS6ojzRjtCGCiLxQricutTAi/LkNWKMsd2wNhMKQ==} + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': - resolution: {integrity: sha512-WFljyDkxtXRlWxMjxeegf7xMYXxUr8u7JdXlOEWKYgDqEgxUnSEsVDxBiNWQ1D5kQKwf8Wo4sVKEYPRhCdsjwA==} + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': - resolution: {integrity: sha512-CUlplTujmbDWp2gamvrqVKi2Or8lmngXT1WxsizJfts7JrvfGhZObciaY/+CbdbS9qNnskvwMZNEhTPrn7b+WA==} + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': - resolution: {integrity: sha512-wdf7g9NbVZCeAo2iGhsjJb7I8ZFfs6X8bumfrWg82VK+8P6AlLXwk48a1ASiJQDTS7Svq2xVzZg3sGO2aXpHRA==} + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': - resolution: {integrity: sha512-0CWY7ubu12nhzz+tkpHjoG3IRSTlWYe0wrfJRf4qqjqQSGtAYgoL9kwzdvlhaFdZ5ffVeyYw9qLsChcjUMEloQ==} + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': - resolution: {integrity: sha512-LztXnGzv6t2u830mnZrFLRVqT/DPJ9DL4ZTz/y93rqUVkeHjMMYIYaFj+BUthiYxbVH9dH0SZYufETspKY/NhA==} - engines: {node: '>=14.0.0'} + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': - resolution: {integrity: sha512-jUct1XVeGtyjqJXEAfvdFa8xoigYZ2rge7nYEm70ppQxpfH9ze2fbIrpHmP2tNM2vL/F6Dd0CpXhpjPbC6bSxQ==} + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': - resolution: {integrity: sha512-VQ8F9ld5gw29epjnVGdrx8ugiLTe8BMqmhDYy7nGbdeDo4HAt4bgdZvLbViEhg7DZyHLpiEUlO5/jPSUrIuxRQ==} + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] - '@rolldown/pluginutils@1.0.0-rc.5': - resolution: {integrity: sha512-RxlLX/DPoarZ9PtxVrQgZhPoor987YtKQqCo5zkjX+0S0yLJ7Vv515Wk6+xtTL67VONKJKxETWZwuZjss2idYw==} + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} - '@rollup/rollup-android-arm-eabi@4.59.0': - resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} + '@rollup/rollup-android-arm-eabi@4.60.2': + resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] os: [android] - '@rollup/rollup-android-arm64@4.59.0': - resolution: {integrity: sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==} + '@rollup/rollup-android-arm64@4.60.2': + resolution: {integrity: sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.59.0': - resolution: {integrity: sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==} + '@rollup/rollup-darwin-arm64@4.60.2': + resolution: {integrity: sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.59.0': - resolution: {integrity: sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==} + '@rollup/rollup-darwin-x64@4.60.2': + resolution: {integrity: sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==} cpu: [x64] os: [darwin] - '@rollup/rollup-freebsd-arm64@4.59.0': - resolution: {integrity: sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==} + '@rollup/rollup-freebsd-arm64@4.60.2': + resolution: {integrity: sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==} cpu: [arm64] os: [freebsd] - '@rollup/rollup-freebsd-x64@4.59.0': - resolution: {integrity: sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==} + '@rollup/rollup-freebsd-x64@4.60.2': + resolution: {integrity: sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==} cpu: [x64] os: [freebsd] - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': - resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': + resolution: {integrity: sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==} cpu: [arm] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm-musleabihf@4.59.0': - resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} + '@rollup/rollup-linux-arm-musleabihf@4.60.2': + resolution: {integrity: sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==} cpu: [arm] os: [linux] libc: [musl] - '@rollup/rollup-linux-arm64-gnu@4.59.0': - resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} + '@rollup/rollup-linux-arm64-gnu@4.60.2': + resolution: {integrity: sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==} cpu: [arm64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.59.0': - resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} + '@rollup/rollup-linux-arm64-musl@4.60.2': + resolution: {integrity: sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==} cpu: [arm64] os: [linux] libc: [musl] - '@rollup/rollup-linux-loong64-gnu@4.59.0': - resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} + '@rollup/rollup-linux-loong64-gnu@4.60.2': + resolution: {integrity: sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==} cpu: [loong64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-loong64-musl@4.59.0': - resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} + '@rollup/rollup-linux-loong64-musl@4.60.2': + resolution: {integrity: sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==} cpu: [loong64] os: [linux] libc: [musl] - '@rollup/rollup-linux-ppc64-gnu@4.59.0': - resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} + '@rollup/rollup-linux-ppc64-gnu@4.60.2': + resolution: {integrity: sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==} cpu: [ppc64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-ppc64-musl@4.59.0': - resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} + '@rollup/rollup-linux-ppc64-musl@4.60.2': + resolution: {integrity: sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==} cpu: [ppc64] os: [linux] libc: [musl] - '@rollup/rollup-linux-riscv64-gnu@4.59.0': - resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} + '@rollup/rollup-linux-riscv64-gnu@4.60.2': + resolution: {integrity: sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==} cpu: [riscv64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-riscv64-musl@4.59.0': - resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} + '@rollup/rollup-linux-riscv64-musl@4.60.2': + resolution: {integrity: sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==} cpu: [riscv64] os: [linux] libc: [musl] - '@rollup/rollup-linux-s390x-gnu@4.59.0': - resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} + '@rollup/rollup-linux-s390x-gnu@4.60.2': + resolution: {integrity: sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==} cpu: [s390x] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.59.0': - resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} + '@rollup/rollup-linux-x64-gnu@4.60.2': + resolution: {integrity: sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==} cpu: [x64] os: [linux] libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.59.0': - resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} + '@rollup/rollup-linux-x64-musl@4.60.2': + resolution: {integrity: sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==} cpu: [x64] os: [linux] libc: [musl] - '@rollup/rollup-openbsd-x64@4.59.0': - resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} + '@rollup/rollup-openbsd-x64@4.60.2': + resolution: {integrity: sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==} cpu: [x64] os: [openbsd] - '@rollup/rollup-openharmony-arm64@4.59.0': - resolution: {integrity: sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==} + '@rollup/rollup-openharmony-arm64@4.60.2': + resolution: {integrity: sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==} cpu: [arm64] os: [openharmony] - '@rollup/rollup-win32-arm64-msvc@4.59.0': - resolution: {integrity: sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==} + '@rollup/rollup-win32-arm64-msvc@4.60.2': + resolution: {integrity: sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==} cpu: [arm64] os: [win32] - '@rollup/rollup-win32-ia32-msvc@4.59.0': - resolution: {integrity: sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==} + '@rollup/rollup-win32-ia32-msvc@4.60.2': + resolution: {integrity: sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==} cpu: [ia32] os: [win32] - '@rollup/rollup-win32-x64-gnu@4.59.0': - resolution: {integrity: sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==} + '@rollup/rollup-win32-x64-gnu@4.60.2': + resolution: {integrity: sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==} cpu: [x64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.59.0': - resolution: {integrity: sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==} + '@rollup/rollup-win32-x64-msvc@4.60.2': + resolution: {integrity: sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==} cpu: [x64] os: [win32] @@ -626,34 +659,37 @@ packages: '@types/node@25.3.3': resolution: {integrity: sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==} - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} @@ -696,9 +732,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} + cac@7.0.0: + resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==} + engines: {node: '>=20.19.0'} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -707,12 +743,15 @@ packages: chardet@2.1.1: resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - defu@6.1.4: - resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} detect-indent@6.1.0: resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} @@ -739,11 +778,11 @@ packages: resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} engines: {node: '>=8.6'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} - esbuild@0.27.3: - resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} engines: {node: '>=18'} hasBin: true @@ -799,8 +838,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - get-tsconfig@4.13.6: - resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-tsconfig@4.14.0: + resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} @@ -813,8 +852,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - hookable@6.0.1: - resolution: {integrity: sha512-uKGyY8BuzN/a5gvzvA+3FVWo0+wUjgtfSdnmjtrOVwQCZPHpHDH2WRO3VZSOeluYrHoDCiXFffZXs8Dj1ULWtw==} + hookable@6.1.1: + resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==} human-id@4.1.3: resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==} @@ -828,8 +867,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - import-without-cache@0.2.5: - resolution: {integrity: sha512-B6Lc2s6yApwnD2/pMzFh/d5AVjdsDXjgkeJ766FmFuJELIGHNycKRj+l3A39yZPM4CchqNCB4RITEAYB1KUM6A==} + import-without-cache@0.3.3: + resolution: {integrity: sha512-bDxwDdF04gm550DfZHgffvlX+9kUlcz32UD0AeBTmVPFiWkrexF2XVmiuFFbDhiFuP8fQkrkvI2KdSNPYWAXkQ==} engines: {node: '>=20.19.0'} is-extglob@2.1.1: @@ -949,16 +988,16 @@ packages: resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} engines: {node: '>=8.6'} - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} pify@4.0.1: resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} engines: {node: '>=6'} - postcss@8.5.6: - resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} engines: {node: ^10 || ^12 || >=14} prettier@2.8.8: @@ -990,14 +1029,14 @@ packages: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rolldown-plugin-dts@0.22.2: - resolution: {integrity: sha512-Ge+XF962Kobjr0hRPx1neVnLU2jpKkD2zevZTfPKf/0el4eYo9SyGPm0stiHDG2JQuL0Q3HLD0Kn+ST8esvVdA==} + rolldown-plugin-dts@0.23.2: + resolution: {integrity: sha512-PbSqLawLgZBGcOGT3yqWBGn4cX+wh2nt5FuBGdcMHyOhoukmjbhYAl8NT9sE4U38Cm9tqLOIQeOrvzeayM0DLQ==} engines: {node: '>=20.19.0'} peerDependencies: '@ts-macro/tsc': ^0.3.6 - '@typescript/native-preview': '>=7.0.0-dev.20250601.1' - rolldown: ^1.0.0-rc.3 - typescript: ^5.0.0 || ^6.0.0-beta + '@typescript/native-preview': '>=7.0.0-dev.20260325.1' + rolldown: ^1.0.0-rc.12 + typescript: ^5.0.0 || ^6.0.0 vue-tsc: ~3.2.0 peerDependenciesMeta: '@ts-macro/tsc': @@ -1009,13 +1048,13 @@ packages: vue-tsc: optional: true - rolldown@1.0.0-rc.5: - resolution: {integrity: sha512-0AdalTs6hNTioaCYIkAa7+xsmHBfU5hCNclZnM/lp7lGGDuUOb6N4BVNtwiomybbencDjq/waKjTImqiGCs5sw==} + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - rollup@4.59.0: - resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} + rollup@4.60.2: + resolution: {integrity: sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true @@ -1062,8 +1101,8 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} @@ -1080,16 +1119,16 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.1.1: + resolution: {integrity: sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg==} engines: {node: '>=18'} - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} to-regex-range@5.0.1: @@ -1100,28 +1139,31 @@ packages: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true - tsdown@0.21.0-beta.2: - resolution: {integrity: sha512-OKj8mKf0ws1ucxuEi3mO/OGyfRQxO9MY2D6SoIE/7RZcbojsZSBhJr4xC4MNivMqrQvi3Ke2e+aRZDemPBWPCw==} + tsdown@0.21.10: + resolution: {integrity: sha512-3wk73yBhZe/wX7REqSdivNQ84TDs1mJ+IlnzrrEREP70xlJ/AEIzqaI04l/TzMKVIdkTdC3CPaADn2Lk/0SkdA==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: '@arethetypeswrong/core': ^0.18.1 + '@tsdown/css': 0.21.10 + '@tsdown/exe': 0.21.10 '@vitejs/devtools': '*' publint: ^0.3.0 - typescript: ^5.0.0 - unplugin-lightningcss: ^0.4.0 + typescript: ^5.0.0 || ^6.0.0 unplugin-unused: ^0.5.0 peerDependenciesMeta: '@arethetypeswrong/core': optional: true + '@tsdown/css': + optional: true + '@tsdown/exe': + optional: true '@vitejs/devtools': optional: true publint: optional: true typescript: optional: true - unplugin-lightningcss: - optional: true unplugin-unused: optional: true @@ -1139,12 +1181,15 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} - unrun@0.2.28: - resolution: {integrity: sha512-LqMrI3ZEUMZ2476aCsbUTfy95CHByqez05nju4AQv4XFPkxh5yai7Di1/Qb0FoELHEEPDWhQi23EJeFyrBV0Og==} + unrun@0.2.37: + resolution: {integrity: sha512-AA7vDuYsgeSYVzJMm16UKA+aXFKhy7nFqW9z5l7q44K4ppFWZAMqYS58ePRZbugMLPH0fwwMzD5A8nP0avxwZQ==} engines: {node: '>=20.19.0'} hasBin: true peerDependencies: @@ -1193,20 +1238,23 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' '@opentelemetry/api': ^1.9.0 '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 - '@vitest/browser-playwright': 4.0.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -1220,6 +1268,10 @@ packages: optional: true '@vitest/browser-webdriverio': optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true '@vitest/ui': optional: true happy-dom: @@ -1244,29 +1296,29 @@ packages: snapshots: - '@babel/generator@8.0.0-rc.1': + '@babel/generator@8.0.0-rc.3': dependencies: - '@babel/parser': 8.0.0-rc.1 - '@babel/types': 8.0.0-rc.1 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 '@types/jsesc': 2.5.1 jsesc: 3.1.0 - '@babel/helper-string-parser@8.0.0-rc.2': {} + '@babel/helper-string-parser@8.0.0-rc.3': {} - '@babel/helper-validator-identifier@8.0.0-rc.1': {} + '@babel/helper-validator-identifier@8.0.0-rc.3': {} - '@babel/parser@8.0.0-rc.1': + '@babel/parser@8.0.0-rc.3': dependencies: - '@babel/types': 8.0.0-rc.1 + '@babel/types': 8.0.0-rc.3 '@babel/runtime@7.29.2': {} - '@babel/types@8.0.0-rc.1': + '@babel/types@8.0.0-rc.3': dependencies: - '@babel/helper-string-parser': 8.0.0-rc.2 - '@babel/helper-validator-identifier': 8.0.0-rc.1 + '@babel/helper-string-parser': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 '@biomejs/biome@2.4.5': optionalDependencies: @@ -1332,7 +1384,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.31.0(@types/node@25.3.3)': + '@changesets/cli@2.31.0(@types/node@25.6.0)': dependencies: '@changesets/apply-release-plan': 7.1.1 '@changesets/assemble-release-plan': 6.0.10 @@ -1348,7 +1400,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.3.3) + '@inquirer/external-editor': 1.0.3(@types/node@25.6.0) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 enquirer: 2.4.1 @@ -1446,106 +1498,106 @@ snapshots: human-id: 4.1.3 prettier: 2.8.8 - '@emnapi/core@1.8.1': + '@emnapi/core@1.10.0': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 optional: true - '@esbuild/aix-ppc64@0.27.3': + '@esbuild/aix-ppc64@0.27.7': optional: true - '@esbuild/android-arm64@0.27.3': + '@esbuild/android-arm64@0.27.7': optional: true - '@esbuild/android-arm@0.27.3': + '@esbuild/android-arm@0.27.7': optional: true - '@esbuild/android-x64@0.27.3': + '@esbuild/android-x64@0.27.7': optional: true - '@esbuild/darwin-arm64@0.27.3': + '@esbuild/darwin-arm64@0.27.7': optional: true - '@esbuild/darwin-x64@0.27.3': + '@esbuild/darwin-x64@0.27.7': optional: true - '@esbuild/freebsd-arm64@0.27.3': + '@esbuild/freebsd-arm64@0.27.7': optional: true - '@esbuild/freebsd-x64@0.27.3': + '@esbuild/freebsd-x64@0.27.7': optional: true - '@esbuild/linux-arm64@0.27.3': + '@esbuild/linux-arm64@0.27.7': optional: true - '@esbuild/linux-arm@0.27.3': + '@esbuild/linux-arm@0.27.7': optional: true - '@esbuild/linux-ia32@0.27.3': + '@esbuild/linux-ia32@0.27.7': optional: true - '@esbuild/linux-loong64@0.27.3': + '@esbuild/linux-loong64@0.27.7': optional: true - '@esbuild/linux-mips64el@0.27.3': + '@esbuild/linux-mips64el@0.27.7': optional: true - '@esbuild/linux-ppc64@0.27.3': + '@esbuild/linux-ppc64@0.27.7': optional: true - '@esbuild/linux-riscv64@0.27.3': + '@esbuild/linux-riscv64@0.27.7': optional: true - '@esbuild/linux-s390x@0.27.3': + '@esbuild/linux-s390x@0.27.7': optional: true - '@esbuild/linux-x64@0.27.3': + '@esbuild/linux-x64@0.27.7': optional: true - '@esbuild/netbsd-arm64@0.27.3': + '@esbuild/netbsd-arm64@0.27.7': optional: true - '@esbuild/netbsd-x64@0.27.3': + '@esbuild/netbsd-x64@0.27.7': optional: true - '@esbuild/openbsd-arm64@0.27.3': + '@esbuild/openbsd-arm64@0.27.7': optional: true - '@esbuild/openbsd-x64@0.27.3': + '@esbuild/openbsd-x64@0.27.7': optional: true - '@esbuild/openharmony-arm64@0.27.3': + '@esbuild/openharmony-arm64@0.27.7': optional: true - '@esbuild/sunos-x64@0.27.3': + '@esbuild/sunos-x64@0.27.7': optional: true - '@esbuild/win32-arm64@0.27.3': + '@esbuild/win32-arm64@0.27.7': optional: true - '@esbuild/win32-ia32@0.27.3': + '@esbuild/win32-ia32@0.27.7': optional: true - '@esbuild/win32-x64@0.27.3': + '@esbuild/win32-x64@0.27.7': optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.3.3)': + '@inquirer/external-editor@1.0.3(@types/node@25.6.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.3.3 + '@types/node': 25.6.0 '@jridgewell/gen-mapping@0.3.13': dependencies: @@ -1577,10 +1629,10 @@ snapshots: globby: 11.1.0 read-yaml-file: 1.1.0 - '@napi-rs/wasm-runtime@1.1.1': + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.10.1 optional: true @@ -1596,128 +1648,136 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.20.1 - '@oxc-project/types@0.114.0': {} + '@oxc-project/types@0.127.0': {} '@quansync/fs@1.0.0': dependencies: quansync: 1.0.0 - '@rolldown/binding-android-arm64@1.0.0-rc.5': + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-arm64@1.0.0-rc.5': + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true - '@rolldown/binding-darwin-x64@1.0.0-rc.5': + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true - '@rolldown/binding-freebsd-x64@1.0.0-rc.5': + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.5': + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.5': + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-arm64-musl@1.0.0-rc.5': + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-gnu@1.0.0-rc.5': + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true - '@rolldown/binding-linux-x64-musl@1.0.0-rc.5': + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true - '@rolldown/binding-openharmony-arm64@1.0.0-rc.5': + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true - '@rolldown/binding-wasm32-wasi@1.0.0-rc.5': + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: - '@napi-rs/wasm-runtime': 1.1.1 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.5': + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true - '@rolldown/binding-win32-x64-msvc@1.0.0-rc.5': + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true - '@rolldown/pluginutils@1.0.0-rc.5': {} + '@rolldown/pluginutils@1.0.0-rc.17': {} - '@rollup/rollup-android-arm-eabi@4.59.0': + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true - '@rollup/rollup-android-arm64@4.59.0': + '@rollup/rollup-android-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-arm64@4.59.0': + '@rollup/rollup-darwin-arm64@4.60.2': optional: true - '@rollup/rollup-darwin-x64@4.59.0': + '@rollup/rollup-darwin-x64@4.60.2': optional: true - '@rollup/rollup-freebsd-arm64@4.59.0': + '@rollup/rollup-freebsd-arm64@4.60.2': optional: true - '@rollup/rollup-freebsd-x64@4.59.0': + '@rollup/rollup-freebsd-x64@4.60.2': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.59.0': + '@rollup/rollup-linux-arm-gnueabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.59.0': + '@rollup/rollup-linux-arm-musleabihf@4.60.2': optional: true - '@rollup/rollup-linux-arm64-gnu@4.59.0': + '@rollup/rollup-linux-arm64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.59.0': + '@rollup/rollup-linux-arm64-musl@4.60.2': optional: true - '@rollup/rollup-linux-loong64-gnu@4.59.0': + '@rollup/rollup-linux-loong64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-loong64-musl@4.59.0': + '@rollup/rollup-linux-loong64-musl@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-gnu@4.59.0': + '@rollup/rollup-linux-ppc64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-ppc64-musl@4.59.0': + '@rollup/rollup-linux-ppc64-musl@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.59.0': + '@rollup/rollup-linux-riscv64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-riscv64-musl@4.59.0': + '@rollup/rollup-linux-riscv64-musl@4.60.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.59.0': + '@rollup/rollup-linux-s390x-gnu@4.60.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.59.0': + '@rollup/rollup-linux-x64-gnu@4.60.2': optional: true - '@rollup/rollup-linux-x64-musl@4.59.0': + '@rollup/rollup-linux-x64-musl@4.60.2': optional: true - '@rollup/rollup-openbsd-x64@4.59.0': + '@rollup/rollup-openbsd-x64@4.60.2': optional: true - '@rollup/rollup-openharmony-arm64@4.59.0': + '@rollup/rollup-openharmony-arm64@4.60.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.59.0': + '@rollup/rollup-win32-arm64-msvc@4.60.2': optional: true - '@rollup/rollup-win32-ia32-msvc@4.59.0': + '@rollup/rollup-win32-ia32-msvc@4.60.2': optional: true - '@rollup/rollup-win32-x64-gnu@4.59.0': + '@rollup/rollup-win32-x64-gnu@4.60.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.59.0': + '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true '@standard-schema/spec@1.1.0': {} @@ -1744,44 +1804,50 @@ snapshots: dependencies: undici-types: 7.18.2 - '@vitest/expect@4.0.18': + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@vitest/expect@4.1.5': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.3)(yaml@2.4.2))': + '@vitest/mocker@4.1.5(vite@7.3.1(@types/node@25.6.0)(yaml@2.4.2))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.5 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.3)(yaml@2.4.2) + vite: 7.3.1(@types/node@25.6.0)(yaml@2.4.2) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.5': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.5': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.5 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.5': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.5': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.5': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 ansi-colors@4.1.3: {} @@ -1801,7 +1867,7 @@ snapshots: ast-kit@3.0.0-beta.1: dependencies: - '@babel/parser': 8.0.0-rc.1 + '@babel/parser': 8.0.0-rc.3 estree-walker: 3.0.3 pathe: 2.0.3 @@ -1815,19 +1881,21 @@ snapshots: dependencies: fill-range: 7.1.1 - cac@6.7.14: {} + cac@7.0.0: {} chai@6.2.2: {} chardet@2.1.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - defu@6.1.4: {} + defu@6.1.7: {} detect-indent@6.1.0: {} @@ -1844,36 +1912,36 @@ snapshots: ansi-colors: 4.1.3 strip-ansi: 6.0.1 - es-module-lexer@1.7.0: {} + es-module-lexer@2.1.0: {} - esbuild@0.27.3: + esbuild@0.27.7: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.3 - '@esbuild/android-arm': 0.27.3 - '@esbuild/android-arm64': 0.27.3 - '@esbuild/android-x64': 0.27.3 - '@esbuild/darwin-arm64': 0.27.3 - '@esbuild/darwin-x64': 0.27.3 - '@esbuild/freebsd-arm64': 0.27.3 - '@esbuild/freebsd-x64': 0.27.3 - '@esbuild/linux-arm': 0.27.3 - '@esbuild/linux-arm64': 0.27.3 - '@esbuild/linux-ia32': 0.27.3 - '@esbuild/linux-loong64': 0.27.3 - '@esbuild/linux-mips64el': 0.27.3 - '@esbuild/linux-ppc64': 0.27.3 - '@esbuild/linux-riscv64': 0.27.3 - '@esbuild/linux-s390x': 0.27.3 - '@esbuild/linux-x64': 0.27.3 - '@esbuild/netbsd-arm64': 0.27.3 - '@esbuild/netbsd-x64': 0.27.3 - '@esbuild/openbsd-arm64': 0.27.3 - '@esbuild/openbsd-x64': 0.27.3 - '@esbuild/openharmony-arm64': 0.27.3 - '@esbuild/sunos-x64': 0.27.3 - '@esbuild/win32-arm64': 0.27.3 - '@esbuild/win32-ia32': 0.27.3 - '@esbuild/win32-x64': 0.27.3 + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 esprima@4.0.1: {} @@ -1897,9 +1965,9 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): + fdir@6.5.0(picomatch@4.0.4): optionalDependencies: - picomatch: 4.0.3 + picomatch: 4.0.4 fill-range@7.1.1: dependencies: @@ -1925,7 +1993,7 @@ snapshots: fsevents@2.3.3: optional: true - get-tsconfig@4.13.6: + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 @@ -1944,7 +2012,7 @@ snapshots: graceful-fs@4.2.11: {} - hookable@6.0.1: {} + hookable@6.1.1: {} human-id@4.1.3: {} @@ -1954,7 +2022,7 @@ snapshots: ignore@5.3.2: {} - import-without-cache@0.2.5: {} + import-without-cache@0.3.3: {} is-extglob@2.1.1: {} @@ -2044,11 +2112,11 @@ snapshots: picomatch@2.3.2: {} - picomatch@4.0.3: {} + picomatch@4.0.4: {} pify@4.0.1: {} - postcss@8.5.6: + postcss@8.5.12: dependencies: nanoid: 3.3.11 picocolors: 1.1.1 @@ -2075,71 +2143,74 @@ snapshots: reusify@1.1.0: {} - rolldown-plugin-dts@0.22.2(rolldown@1.0.0-rc.5)(typescript@6.0.0-beta): + rolldown-plugin-dts@0.23.2(rolldown@1.0.0-rc.17)(typescript@6.0.0-beta): dependencies: - '@babel/generator': 8.0.0-rc.1 - '@babel/helper-validator-identifier': 8.0.0-rc.1 - '@babel/parser': 8.0.0-rc.1 - '@babel/types': 8.0.0-rc.1 + '@babel/generator': 8.0.0-rc.3 + '@babel/helper-validator-identifier': 8.0.0-rc.3 + '@babel/parser': 8.0.0-rc.3 + '@babel/types': 8.0.0-rc.3 ast-kit: 3.0.0-beta.1 birpc: 4.0.0 dts-resolver: 2.1.3 - get-tsconfig: 4.13.6 + get-tsconfig: 4.14.0 obug: 2.1.1 - rolldown: 1.0.0-rc.5 + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 optionalDependencies: typescript: 6.0.0-beta transitivePeerDependencies: - oxc-resolver - rolldown@1.0.0-rc.5: + rolldown@1.0.0-rc.17: dependencies: - '@oxc-project/types': 0.114.0 - '@rolldown/pluginutils': 1.0.0-rc.5 + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 optionalDependencies: - '@rolldown/binding-android-arm64': 1.0.0-rc.5 - '@rolldown/binding-darwin-arm64': 1.0.0-rc.5 - '@rolldown/binding-darwin-x64': 1.0.0-rc.5 - '@rolldown/binding-freebsd-x64': 1.0.0-rc.5 - '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.5 - '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.5 - '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.5 - '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.5 - '@rolldown/binding-linux-x64-musl': 1.0.0-rc.5 - '@rolldown/binding-openharmony-arm64': 1.0.0-rc.5 - '@rolldown/binding-wasm32-wasi': 1.0.0-rc.5 - '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.5 - '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.5 - - rollup@4.59.0: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + rollup@4.60.2: dependencies: '@types/estree': 1.0.8 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.59.0 - '@rollup/rollup-android-arm64': 4.59.0 - '@rollup/rollup-darwin-arm64': 4.59.0 - '@rollup/rollup-darwin-x64': 4.59.0 - '@rollup/rollup-freebsd-arm64': 4.59.0 - '@rollup/rollup-freebsd-x64': 4.59.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.59.0 - '@rollup/rollup-linux-arm-musleabihf': 4.59.0 - '@rollup/rollup-linux-arm64-gnu': 4.59.0 - '@rollup/rollup-linux-arm64-musl': 4.59.0 - '@rollup/rollup-linux-loong64-gnu': 4.59.0 - '@rollup/rollup-linux-loong64-musl': 4.59.0 - '@rollup/rollup-linux-ppc64-gnu': 4.59.0 - '@rollup/rollup-linux-ppc64-musl': 4.59.0 - '@rollup/rollup-linux-riscv64-gnu': 4.59.0 - '@rollup/rollup-linux-riscv64-musl': 4.59.0 - '@rollup/rollup-linux-s390x-gnu': 4.59.0 - '@rollup/rollup-linux-x64-gnu': 4.59.0 - '@rollup/rollup-linux-x64-musl': 4.59.0 - '@rollup/rollup-openbsd-x64': 4.59.0 - '@rollup/rollup-openharmony-arm64': 4.59.0 - '@rollup/rollup-win32-arm64-msvc': 4.59.0 - '@rollup/rollup-win32-ia32-msvc': 4.59.0 - '@rollup/rollup-win32-x64-gnu': 4.59.0 - '@rollup/rollup-win32-x64-msvc': 4.59.0 + '@rollup/rollup-android-arm-eabi': 4.60.2 + '@rollup/rollup-android-arm64': 4.60.2 + '@rollup/rollup-darwin-arm64': 4.60.2 + '@rollup/rollup-darwin-x64': 4.60.2 + '@rollup/rollup-freebsd-arm64': 4.60.2 + '@rollup/rollup-freebsd-x64': 4.60.2 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.2 + '@rollup/rollup-linux-arm-musleabihf': 4.60.2 + '@rollup/rollup-linux-arm64-gnu': 4.60.2 + '@rollup/rollup-linux-arm64-musl': 4.60.2 + '@rollup/rollup-linux-loong64-gnu': 4.60.2 + '@rollup/rollup-linux-loong64-musl': 4.60.2 + '@rollup/rollup-linux-ppc64-gnu': 4.60.2 + '@rollup/rollup-linux-ppc64-musl': 4.60.2 + '@rollup/rollup-linux-riscv64-gnu': 4.60.2 + '@rollup/rollup-linux-riscv64-musl': 4.60.2 + '@rollup/rollup-linux-s390x-gnu': 4.60.2 + '@rollup/rollup-linux-x64-gnu': 4.60.2 + '@rollup/rollup-linux-x64-musl': 4.60.2 + '@rollup/rollup-openbsd-x64': 4.60.2 + '@rollup/rollup-openharmony-arm64': 4.60.2 + '@rollup/rollup-win32-arm64-msvc': 4.60.2 + '@rollup/rollup-win32-ia32-msvc': 4.60.2 + '@rollup/rollup-win32-x64-gnu': 4.60.2 + '@rollup/rollup-win32-x64-msvc': 4.60.2 fsevents: 2.3.3 run-parallel@1.2.0: @@ -2173,7 +2244,7 @@ snapshots: stackback@0.0.2: {} - std-env@3.10.0: {} + std-env@4.1.0: {} strip-ansi@6.0.1: dependencies: @@ -2185,14 +2256,14 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.1.1: {} - tinyglobby@0.2.15: + tinyglobby@0.2.16: dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} to-regex-range@5.0.1: dependencies: @@ -2200,24 +2271,24 @@ snapshots: tree-kill@1.2.2: {} - tsdown@0.21.0-beta.2(typescript@6.0.0-beta): + tsdown@0.21.10(typescript@6.0.0-beta): dependencies: ansis: 4.2.0 - cac: 6.7.14 - defu: 6.1.4 + cac: 7.0.0 + defu: 6.1.7 empathic: 2.0.0 - hookable: 6.0.1 - import-without-cache: 0.2.5 + hookable: 6.1.1 + import-without-cache: 0.3.3 obug: 2.1.1 - picomatch: 4.0.3 - rolldown: 1.0.0-rc.5 - rolldown-plugin-dts: 0.22.2(rolldown@1.0.0-rc.5)(typescript@6.0.0-beta) + picomatch: 4.0.4 + rolldown: 1.0.0-rc.17 + rolldown-plugin-dts: 0.23.2(rolldown@1.0.0-rc.17)(typescript@6.0.0-beta) semver: 7.7.4 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 + tinyexec: 1.1.1 + tinyglobby: 0.2.16 tree-kill: 1.2.2 unconfig-core: 7.5.0 - unrun: 0.2.28 + unrun: 0.2.37 optionalDependencies: typescript: 6.0.0-beta transitivePeerDependencies: @@ -2239,61 +2310,53 @@ snapshots: undici-types@7.18.2: {} + undici-types@7.19.2: {} + universalify@0.1.2: {} - unrun@0.2.28: + unrun@0.2.37: dependencies: - rolldown: 1.0.0-rc.5 + rolldown: 1.0.0-rc.17 - vite@7.3.1(@types/node@25.3.3)(yaml@2.4.2): + vite@7.3.1(@types/node@25.6.0)(yaml@2.4.2): dependencies: - esbuild: 0.27.3 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.6 - rollup: 4.59.0 - tinyglobby: 0.2.15 + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.12 + rollup: 4.60.2 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 25.3.3 + '@types/node': 25.6.0 fsevents: 2.3.3 yaml: 2.4.2 - vitest@4.0.18(@types/node@25.3.3)(yaml@2.4.2): + vitest@4.1.5(@types/node@25.6.0)(vite@7.3.1(@types/node@25.6.0)(yaml@2.4.2)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.3)(yaml@2.4.2)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@7.3.1(@types/node@25.6.0)(yaml@2.4.2)) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 + picomatch: 4.0.4 + std-env: 4.1.0 tinybench: 2.9.0 - tinyexec: 1.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.3)(yaml@2.4.2) + tinyexec: 1.1.1 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@25.6.0)(yaml@2.4.2) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.3 + '@types/node': 25.6.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml which@2.0.2: dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index c5739b7..62b491e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,2 +1,8 @@ +packages: + - "examples" + +catalog: + "@types/node": ^25.6.0 + ignoredBuiltDependencies: - esbuild diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs new file mode 100644 index 0000000..3896024 --- /dev/null +++ b/scripts/package-smoke.mjs @@ -0,0 +1,54 @@ +import assert from "node:assert/strict"; + +import * as rootApi from "@hsblabs/web-stream-extras"; +import * as cobsApi from "@hsblabs/web-stream-extras/cobs"; +import * as encryptionApi from "@hsblabs/web-stream-extras/encryption"; +import * as pngApi from "@hsblabs/web-stream-extras/png"; + +assert.deepEqual(Object.keys(rootApi).sort(), [ + "binaryToString", + "randomBytes", + "readAllBytes", + "readAllChunks", + "readableFromChunks", + "stringToBinary", +]); + +assert.deepEqual(Object.keys(cobsApi).sort(), [ + "createCOBSDecoderStream", + "createCOBSEncoderStream", + "decodeCOBSFrame", + "encodeCOBSFrame", + "readCOBS", + "writeCOBS", +]); + +assert.deepEqual(Object.keys(encryptionApi).sort(), [ + "DecryptionStream", + "EncryptionStream", + "decryptStream", + "encryptStream", + "webCryptoStream", +]); + +assert.deepEqual(Object.keys(pngApi).sort(), [ + "createPNGTextChunkWriter", + "extractPNGTextChunk", + "streamPNGTextChunk", +]); + +const frame = Uint8Array.of(0x11, 0x00, 0x22); +const encoded = cobsApi.encodeCOBSFrame(frame); + +assert.deepEqual( + Array.from(cobsApi.decodeCOBSFrame(encoded)), + Array.from(frame), +); +assert.equal( + typeof rootApi.readableFromChunks([Uint8Array.of(1)]).getReader, + "function", +); +assert.equal(typeof encryptionApi.encryptStream, "function"); +assert.equal(typeof pngApi.streamPNGTextChunk, "function"); + +process.stdout.write("package smoke test passed\n"); diff --git a/src/byte-queue.test.ts b/src/byte-queue.test.ts index 16ce15d..a932ab9 100644 --- a/src/byte-queue.test.ts +++ b/src/byte-queue.test.ts @@ -39,6 +39,26 @@ describe("ByteQueue", () => { expect(() => queue.read(2)).toThrow(); }); + it("finds a byte across chunk boundaries without flattening the queue", () => { + const queue = new ByteQueue(); + queue.append(toU8Array([1, 2])); + queue.append(toU8Array([3, 0, 4])); + + expect(queue.indexOf(0)).toBe(3); + expect(queue.indexOf(9)).toBe(-1); + }); + + it("discards buffered bytes without allocating a read result", () => { + const queue = new ByteQueue(); + queue.append(toU8Array([1, 2])); + queue.append(toU8Array([3, 4, 5])); + + queue.discard(3); + + expect(queue.byteLength).toBe(2); + expect(queue.read(2)).toEqual(toU8Array([4, 5])); + }); + it("preserves ordering after repeated reads trigger internal compaction", () => { const queue = new ByteQueue(); const source = toU8Array(96); diff --git a/src/byte-queue.ts b/src/byte-queue.ts index 68108a3..e983cee 100644 --- a/src/byte-queue.ts +++ b/src/byte-queue.ts @@ -19,6 +19,59 @@ export class ByteQueue { this.#byteLength += chunk.byteLength; } + discard(length: number): void { + if (length < 0) { + throwError("Length must be non-negative"); + } + if (length > this.#byteLength) { + throwError("Length exceeds buffered data"); + } + if (length === 0) { + return; + } + + let remaining = length; + + while (remaining > 0) { + const chunk = this.#chunks[this.#head]; + if (!chunk) { + throwError("Buffered data is inconsistent"); + } + + if (chunk.byteLength <= remaining) { + this.#head++; + remaining -= chunk.byteLength; + continue; + } + + this.#chunks[this.#head] = chunk.subarray(remaining); + remaining = 0; + } + + this.#byteLength -= length; + this.#compact(); + } + + indexOf(value: number): number { + let offset = 0; + + for (let index = this.#head; index < this.#chunks.length; index++) { + const chunk = this.#chunks[index]; + if (!chunk) { + break; + } + + const foundIndex = chunk.indexOf(value); + if (foundIndex !== -1) { + return offset + foundIndex; + } + + offset += chunk.byteLength; + } + + return -1; + } + read(length: number): Uint8Array { if (length < 0) { throwError("Length must be non-negative"); diff --git a/src/cobs.test.ts b/src/cobs.test.ts new file mode 100644 index 0000000..c642ccf --- /dev/null +++ b/src/cobs.test.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from "vitest"; +import * as cobsApi from "./cobs"; +import { + createCOBSDecoderStream, + createCOBSEncoderStream, + decodeCOBSFrame, + encodeCOBSFrame, + readCOBS, + writeCOBS, +} from "./cobs"; +import { readAllBytes, readAllChunks, readableFromChunks } from "./readable"; + +describe("cobs public API", () => { + it("exports the supported cobs helpers", () => { + expect(Object.keys(cobsApi).sort()).toEqual([ + "createCOBSDecoderStream", + "createCOBSEncoderStream", + "decodeCOBSFrame", + "encodeCOBSFrame", + "readCOBS", + "writeCOBS", + ]); + }); +}); + +describe("cobs", () => { + it("round-trips a raw frame with embedded zero bytes", () => { + const raw = Uint8Array.of(0x11, 0x22, 0x00, 0x33); + + expect(decodeCOBSFrame(encodeCOBSFrame(raw))).toEqual(raw); + }); + + it("encodes each incoming chunk as a delimiter-terminated frame", async () => { + const encoded = await readAllChunks( + readableFromChunks([ + Uint8Array.of(0x01, 0x02, 0x00), + Uint8Array.of(0xff, 0xee), + ]).pipeThrough(createCOBSEncoderStream()), + ); + + expect(encoded).toEqual([ + Uint8Array.of(0x03, 0x01, 0x02, 0x01, 0x00), + Uint8Array.of(0x03, 0xff, 0xee, 0x00), + ]); + }); + + it("decodes delimiter-separated frames across arbitrary chunk boundaries", async () => { + const decoded = await readAllChunks( + readableFromChunks([ + Uint8Array.of(0x03, 0x11), + Uint8Array.of(0x22, 0x02, 0x33, 0x00, 0x01, 0x00), + ]).pipeThrough(createCOBSDecoderStream()), + ); + + expect(decoded).toEqual([ + Uint8Array.of(0x11, 0x22, 0x00, 0x33), + new Uint8Array(0), + ]); + }); + + it("exposes readCOBS as a decoding convenience", async () => { + const decoded = await readAllChunks( + readCOBS( + readableFromChunks([ + Uint8Array.of(0x03, 0x11, 0x22), + Uint8Array.of(0x02, 0x33, 0x00), + ]), + ), + ); + + expect(decoded).toEqual([Uint8Array.of(0x11, 0x22, 0x00, 0x33)]); + }); + + it("exposes writeCOBS as an encoding convenience", async () => { + const written: Uint8Array[] = []; + const sink = new WritableStream({ + write(chunk) { + written.push(chunk.slice()); + }, + }); + const writer = writeCOBS(sink).getWriter(); + + await writer.write(Uint8Array.of(0x11, 0x22, 0x00, 0x33)); + await writer.write(new Uint8Array(0)); + await writer.close(); + + await expect(readAllBytes(readableFromChunks(written))).resolves.toEqual( + Uint8Array.of(0x03, 0x11, 0x22, 0x02, 0x33, 0x00, 0x01, 0x00), + ); + }); + + it("rejects unterminated frames at the end of the stream", async () => { + await expect( + readAllChunks( + readableFromChunks(Uint8Array.of(0x03, 0x11, 0x22)).pipeThrough( + createCOBSDecoderStream(), + ), + ), + ).rejects.toThrow(); + }); +}); diff --git a/src/cobs.ts b/src/cobs.ts new file mode 100644 index 0000000..6be67b3 --- /dev/null +++ b/src/cobs.ts @@ -0,0 +1,8 @@ +export { + createCOBSDecoderStream, + createCOBSEncoderStream, + decodeCOBSFrame, + encodeCOBSFrame, + readCOBS, + writeCOBS, +} from "./cobs/public"; diff --git a/src/cobs/public.ts b/src/cobs/public.ts new file mode 100644 index 0000000..a3eec21 --- /dev/null +++ b/src/cobs/public.ts @@ -0,0 +1,136 @@ +import { ByteQueue } from "../byte-queue"; +import { throwError } from "../shared/error"; + +const COBS_DELIMITER = 0x00; + +const withDelimiter = (encodedFrame: Uint8Array): Uint8Array => { + const result = new Uint8Array(encodedFrame.byteLength + 1); + result.set(encodedFrame, 0); + result[encodedFrame.byteLength] = COBS_DELIMITER; + return result; +}; + +export const encodeCOBSFrame = (frame: Uint8Array): Uint8Array => { + const output: number[] = [0]; + let codeIndex = 0; + let code = 1; + + for (const value of frame) { + if (value === COBS_DELIMITER) { + output[codeIndex] = code; + codeIndex = output.length; + output.push(0); + code = 1; + continue; + } + + output.push(value); + code++; + + if (code === 0xff) { + output[codeIndex] = code; + codeIndex = output.length; + output.push(0); + code = 1; + } + } + + output[codeIndex] = code; + return Uint8Array.from(output); +}; + +export const decodeCOBSFrame = (encodedFrame: Uint8Array): Uint8Array => { + if (encodedFrame.byteLength === 0) { + throwError("Invalid COBS frame: empty encoded payload."); + } + + const output: number[] = []; + let index = 0; + + while (index < encodedFrame.byteLength) { + const code = encodedFrame[index]; + if (code === COBS_DELIMITER) { + throwError("Invalid COBS frame: code byte must not be 0x00."); + } + + const next = index + 1; + const endExclusive = next + (code - 1); + + if (endExclusive > encodedFrame.byteLength) { + throwError("Invalid COBS frame: truncated block."); + } + + for (let cursor = next; cursor < endExclusive; cursor++) { + output.push(encodedFrame[cursor] ?? 0); + } + + index = endExclusive; + + if (code !== 0xff && index < encodedFrame.byteLength) { + output.push(COBS_DELIMITER); + } + } + + return Uint8Array.from(output); +}; + +export const createCOBSEncoderStream = (): TransformStream< + Uint8Array, + Uint8Array +> => + new TransformStream({ + transform(chunk, controller) { + controller.enqueue(withDelimiter(encodeCOBSFrame(chunk))); + }, + }); + +export const createCOBSDecoderStream = (): TransformStream< + Uint8Array, + Uint8Array +> => { + const buffer = new ByteQueue(); + + return new TransformStream({ + flush() { + if (buffer.byteLength !== 0) { + throwError("Invalid COBS stream: unterminated frame at end of stream."); + } + }, + transform(chunk, controller) { + buffer.append(chunk); + + while (true) { + const delimiterIndex = buffer.indexOf(COBS_DELIMITER); + if (delimiterIndex === -1) { + return; + } + + const frame = buffer.read(delimiterIndex); + buffer.discard(1); + controller.enqueue(decodeCOBSFrame(frame)); + } + }, + }); +}; + +export const readCOBS = ( + source: ReadableStream, +): ReadableStream => source.pipeThrough(createCOBSDecoderStream()); + +export const writeCOBS = ( + sink: WritableStream, +): WritableStream => { + const writer = sink.getWriter(); + + return new WritableStream({ + abort(reason) { + return writer.abort(reason); + }, + close() { + return writer.close(); + }, + write(chunk) { + return writer.write(withDelimiter(encodeCOBSFrame(chunk))); + }, + }); +}; diff --git a/src/png.test.ts b/src/png.test.ts new file mode 100644 index 0000000..c979e66 --- /dev/null +++ b/src/png.test.ts @@ -0,0 +1,953 @@ +import { describe, expect, it } from "vitest"; +import { decodeCOBSFrame, encodeCOBSFrame } from "./cobs"; +import * as pngApi from "./png"; +import { + PNG_INTERNAL_TEXT_CHUNK_KEYWORD, + PNG_PAYLOAD_MAGIC, + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + PNG_PAYLOAD_VERSION, + PNG_SIGNATURE, +} from "./png/constants"; +import { crc32 } from "./png/crc32"; +import { + createChunk, + createPayloadDataSegment, + createPayloadManifestSegment, + createTextChunk, + getInternalTextChunkPayload, + isInternalTextChunk, + parsePayloadSegment, + parsePNGBytes, +} from "./png/framing"; +import { + createPNGTextChunkWriter, + extractPNGTextChunk, + streamPNGTextChunk, +} from "./png/public"; +import { readAllBytes, readableFromChunks } from "./readable"; + +const MINIMAL_PNG = concatBytes( + PNG_SIGNATURE, + createChunk("IHDR", Uint8Array.of(0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0)), + createChunk( + "IDAT", + Uint8Array.of(0x78, 0x9c, 0x63, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01), + ), + createChunk("IEND", new Uint8Array(0)), +); + +function splitBytes(data: Uint8Array, sizes: number[]): Uint8Array[] { + const chunks: Uint8Array[] = []; + let offset = 0; + + for (const size of sizes) { + if (offset >= data.byteLength) { + break; + } + + chunks.push(data.slice(offset, offset + size)); + offset += size; + } + + if (offset < data.byteLength) { + chunks.push(data.slice(offset)); + } + + return chunks; +} + +function splitIntoSingleBytes(data: Uint8Array): Uint8Array[] { + return Array.from(data, (value) => new Uint8Array([value])); +} + +function concatBytes(...arrays: Uint8Array[]): Uint8Array { + const totalLength = arrays.reduce((sum, array) => sum + array.byteLength, 0); + const result = new Uint8Array(totalLength); + let offset = 0; + + for (const array of arrays) { + result.set(array, offset); + offset += array.byteLength; + } + + return result; +} + +function delayed(value: T, ms: number): Promise { + return new Promise((resolve) => { + setTimeout(() => resolve(value), ms); + }); +} + +function timeoutFail(ms: number, message: string): Promise { + return delayed(undefined, ms).then(() => { + throw new Error(message); + }); +} + +function expectReadValue( + result: ReadableStreamReadResult, + message: string, +): T { + if (result.done) { + throw new Error(message); + } + return result.value; +} + +function readableFromDelayedChunks( + chunks: Uint8Array[], + delayMs: number, +): ReadableStream { + let index = 0; + + return new ReadableStream({ + async pull(controller) { + if (index >= chunks.length) { + controller.close(); + return; + } + + const chunk = chunks[index]; + index += 1; + await delayed(undefined, delayMs); + controller.enqueue(chunk); + }, + }); +} + +function reverseSegmentOrder(png: Uint8Array): Uint8Array { + const chunks = parsePNGBytes(png); + const internalChunks = chunks.filter(isInternalTextChunk).reverse(); + const nextChunks = chunks.flatMap((chunk) => + isInternalTextChunk(chunk) ? internalChunks.splice(0, 1) : [chunk], + ); + + return concatBytes(PNG_SIGNATURE, ...nextChunks.map((chunk) => chunk.raw)); +} + +function mutateFirstInternalChunk( + png: Uint8Array, + mutate: (segmentBytes: Uint8Array) => Uint8Array, +): Uint8Array { + const chunks = parsePNGBytes(png); + const nextChunks = chunks.map((chunk) => { + if (!isInternalTextChunk(chunk)) { + return chunk.raw; + } + + const segment = parsePayloadSegment( + chunk.data.subarray(PNG_INTERNAL_TEXT_CHUNK_KEYWORD.length + 1), + ); + const mutated = mutate(segment.raw); + return createTextChunk(mutated, PNG_INTERNAL_TEXT_CHUNK_KEYWORD); + }); + + return concatBytes(PNG_SIGNATURE, ...nextChunks); +} + +function replaceInternalChunks( + png: Uint8Array, + replace: (chunks: ReturnType) => Uint8Array[], +): Uint8Array { + const chunks = parsePNGBytes(png); + const iendChunk = chunks.find((chunk) => chunk.type === "IEND"); + const nextInternalChunks = replace(chunks.filter(isInternalTextChunk)); + const nextChunks = chunks + .filter((chunk) => chunk.type !== "IEND" && !isInternalTextChunk(chunk)) + .map((chunk) => chunk.raw); + + if (!iendChunk) { + throw new Error("Expected IEND chunk"); + } + + return concatBytes( + PNG_SIGNATURE, + ...nextChunks, + ...nextInternalChunks, + iendChunk.raw, + ); +} + +async function embedPayload( + payload: Uint8Array, + options?: { onExisting?: "error" | "replace" }, + source = MINIMAL_PNG, +): Promise { + const writer = createPNGTextChunkWriter(readableFromChunks(source), options); + await readableFromChunks(payload).pipeTo(writer.writable); + return readAllBytes(writer.readable); +} + +describe("png public API", () => { + it("exports the supported png helpers", () => { + expect(Object.keys(pngApi).sort()).toEqual([ + "createPNGTextChunkWriter", + "extractPNGTextChunk", + "streamPNGTextChunk", + ]); + }); +}); + +describe("png helpers", () => { + it("round-trips arbitrary bytes through COBS", () => { + const input = new Uint8Array([0, 1, 0, 2, 3, 0, 4, 0]); + + expect(decodeCOBSFrame(encodeCOBSFrame(input))).toEqual(input); + }); + + it("builds a valid tEXt chunk with matching CRC", () => { + const chunk = createTextChunk(new Uint8Array([1, 2, 3]), "keyword"); + const parsed = parsePNGBytes(concatBytes(PNG_SIGNATURE, chunk)); + + expect(parsed).toHaveLength(1); + expect(parsed[0]?.type).toBe("tEXt"); + expect(parsed[0]?.crc).toBe( + crc32(concatBytes(new TextEncoder().encode("tEXt"), parsed[0].data)), + ); + }); + + it("encodes and validates payload data segments", () => { + const encoded = createPayloadDataSegment({ + segmentData: new Uint8Array([1, 2, 3]), + segmentIndex: 0, + }); + const parsed = parsePayloadSegment(encoded); + + expect(Array.from(parsed.magic)).toEqual(Array.from(PNG_PAYLOAD_MAGIC)); + expect(parsed.version).toBe(PNG_PAYLOAD_VERSION); + expect(parsed.kind).toBe("data"); + if (parsed.kind !== "data") { + throw new Error("Expected a data segment"); + } + expect(parsed.segmentIndex).toBe(0); + expect(parsed.segmentData).toEqual(new Uint8Array([1, 2, 3])); + }); + + it("encodes and validates payload manifest segments", () => { + const encoded = createPayloadManifestSegment({ + payloadCrc32: 123, + segmentCount: 2, + }); + const parsed = parsePayloadSegment(encoded); + + expect(Array.from(parsed.magic)).toEqual(Array.from(PNG_PAYLOAD_MAGIC)); + expect(parsed.version).toBe(PNG_PAYLOAD_VERSION); + expect(parsed.kind).toBe("manifest"); + if (parsed.kind !== "manifest") { + throw new Error("Expected a manifest segment"); + } + expect(parsed.segmentCount).toBe(2); + expect(parsed.payloadCrc32).toBe(123); + }); +}); + +describe("extractPNGTextChunk", () => { + it("extracts payloads split across multiple tEXt chunks", async () => { + const payload = new Uint8Array( + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 32, + ).map((_, index) => index & 0xff); + const embedded = await embedPayload(payload); + + const extracted = await readAllBytes( + extractPNGTextChunk( + readableFromChunks(splitBytes(embedded, [5, 3, 2, 1])), + ), + ); + + expect(extracted).toEqual(payload); + }); + + it("round-trips empty payloads", async () => { + const embedded = await embedPayload(new Uint8Array(0)); + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(embedded)), + ); + + expect(extracted).toEqual(new Uint8Array(0)); + }); + + it("propagates cancel reasons to the source png reader", async () => { + const embedded = await embedPayload(new Uint8Array([1, 2, 3])); + let canceledReason: unknown; + let index = 0; + const source = new ReadableStream({ + cancel(reason) { + canceledReason = reason; + }, + async pull(controller) { + if (index >= embedded.byteLength) { + controller.close(); + return; + } + + const next = embedded.slice(index, index + 1); + index += 1; + await delayed(undefined, 5); + controller.enqueue(next); + }, + }); + const reason = new Error("cancel extract"); + + await extractPNGTextChunk(source).cancel(reason); + + expect(canceledReason).toBe(reason); + }); + + it("rejects pngs without embedded payloads", async () => { + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(MINIMAL_PNG))), + ).rejects.toThrow(); + }); + + it("rejects malformed signatures", async () => { + const mutated = MINIMAL_PNG.slice(); + mutated[0] ^= 0xff; + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects missing IEND chunks", async () => { + const mutated = MINIMAL_PNG.slice(0, -12); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects trailing bytes after IEND", async () => { + const mutated = concatBytes(MINIMAL_PNG, new Uint8Array([0xff])); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects invalid non-ascii chunk types", async () => { + const mutated = MINIMAL_PNG.slice(); + mutated[12] = 0x80; + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects invalid payload versions", async () => { + const embedded = await embedPayload(new Uint8Array([1, 2, 3])); + const mutated = mutateFirstInternalChunk(embedded, (segmentRaw) => { + const next = segmentRaw.slice(); + next[4] = 0x7f; + return encodeCOBSFrame(next); + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects reverse-order segment indexes", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 4).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + + await expect( + readAllBytes( + extractPNGTextChunk(readableFromChunks(reverseSegmentOrder(embedded))), + ), + ).rejects.toThrow(); + }); + + it("rejects payload data that appears after the manifest", async () => { + const payload = new Uint8Array( + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH * 2 + 4, + ).map((_, index) => index & 0xff); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + const dataChunks = internalChunks.slice(0, -1); + const trailingDataChunk = dataChunks.at(-1); + + if (!manifestChunk || !trailingDataChunk) { + throw new Error("Expected manifest and trailing data chunk"); + } + + return [ + ...dataChunks.slice(0, -1).map((chunk) => chunk.raw), + manifestChunk.raw, + trailingDataChunk.raw, + ]; + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects manifest segment-count mismatches", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 4).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + if (!manifestChunk) { + throw new Error("Expected manifest chunk"); + } + + const manifest = parsePayloadSegment( + getInternalTextChunkPayload(manifestChunk), + ); + if (manifest.kind !== "manifest") { + throw new Error("Expected a manifest segment"); + } + + return [ + ...internalChunks.slice(0, -1).map((chunk) => chunk.raw), + createTextChunk( + createPayloadManifestSegment({ + payloadCrc32: manifest.payloadCrc32, + segmentCount: manifest.segmentCount + 1, + }), + ), + ]; + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects manifest crc mismatches", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 4).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + if (!manifestChunk) { + throw new Error("Expected manifest chunk"); + } + + const manifest = parsePayloadSegment( + getInternalTextChunkPayload(manifestChunk), + ); + if (manifest.kind !== "manifest") { + throw new Error("Expected a manifest segment"); + } + + return [ + ...internalChunks.slice(0, -1).map((chunk) => chunk.raw), + createTextChunk( + createPayloadManifestSegment({ + payloadCrc32: manifest.payloadCrc32 ^ 0xffffffff, + segmentCount: manifest.segmentCount, + }), + ), + ]; + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects duplicate manifests", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 4).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + if (!manifestChunk) { + throw new Error("Expected manifest chunk"); + } + + return [ + ...internalChunks.slice(0, -1).map((chunk) => chunk.raw), + manifestChunk.raw, + manifestChunk.raw, + ]; + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); + + it("rejects manifest-first ordering", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 4).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + if (!manifestChunk) { + throw new Error("Expected manifest chunk"); + } + + return [ + manifestChunk.raw, + ...internalChunks.slice(0, -1).map((chunk) => chunk.raw), + ]; + }); + + await expect( + readAllBytes(extractPNGTextChunk(readableFromChunks(mutated))), + ).rejects.toThrow(); + }); +}); + +describe("streamPNGTextChunk", () => { + it("closes cleanly for empty payloads", async () => { + const embedded = await embedPayload(new Uint8Array(0)); + const reader = streamPNGTextChunk(readableFromChunks(embedded)).getReader(); + + await expect(reader.read()).resolves.toEqual({ + done: true, + value: undefined, + }); + }); + + it("supports single-byte source chunking", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 8).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const reader = streamPNGTextChunk( + readableFromChunks(splitIntoSingleBytes(embedded)), + ).getReader(); + const first = await reader.read(); + const firstChunk = expectReadValue(first, "Expected payload bytes"); + + expect(first.done).toBe(false); + expect(firstChunk).toEqual( + payload.subarray(0, PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + const remaining: Uint8Array[] = []; + while (true) { + const next = await reader.read(); + if (next.done) break; + remaining.push(next.value); + } + + expect(concatBytes(firstChunk, ...remaining)).toEqual(payload); + }); + + it("emits payload bytes before the terminal manifest arrives", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 8).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const chunks = parsePNGBytes(embedded); + const stream = streamPNGTextChunk( + readableFromDelayedChunks( + [PNG_SIGNATURE, ...chunks.map((chunk) => chunk.raw)], + 10, + ), + ); + const reader = stream.getReader(); + + const first = await Promise.race([ + reader.read(), + timeoutFail(80, "Expected payload bytes before the manifest arrived"), + ]); + const firstChunk = expectReadValue(first, "Expected payload bytes"); + + expect(first.done).toBe(false); + expect(firstChunk).toEqual( + payload.subarray(0, PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + const remaining: Uint8Array[] = []; + while (true) { + const next = await reader.read(); + if (next.done) break; + remaining.push(next.value); + } + + expect(concatBytes(firstChunk, ...remaining)).toEqual(payload); + }); + + it("does not drain the source png past one upstream prefetch window without demand", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 8).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const sourceChunks = [ + PNG_SIGNATURE, + ...parsePNGBytes(embedded).map((chunk) => chunk.raw), + ]; + const firstInternalIndex = + parsePNGBytes(embedded).findIndex(isInternalTextChunk); + let pullCount = 0; + let index = 0; + const source = new ReadableStream({ + async pull(controller) { + pullCount += 1; + if (index >= sourceChunks.length) { + controller.close(); + return; + } + + const chunk = sourceChunks[index]; + index += 1; + await delayed(undefined, 5); + controller.enqueue(chunk); + }, + }); + const stream = streamPNGTextChunk(source); + + await delayed(undefined, 80); + + expect(pullCount).toBeLessThanOrEqual(firstInternalIndex + 3); + await stream.cancel(new Error("stop stream extract")); + }); + + it("allows partial output before reporting missing manifests", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 8).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => + internalChunks.slice(0, -1).map((chunk) => chunk.raw), + ); + const reader = streamPNGTextChunk(readableFromChunks(mutated)).getReader(); + const first = await reader.read(); + + expect(first.done).toBe(false); + expect(first.value).toEqual( + payload.subarray(0, PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + const second = await reader.read(); + expect(second.done).toBe(false); + expect(second.value).toEqual( + payload.subarray(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + await expect(reader.read()).rejects.toThrow( + "PNG does not contain an embedded payload", + ); + }); + + it("allows partial output before reporting crc mismatches", async () => { + const payload = new Uint8Array(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + 8).map( + (_, index) => index & 0xff, + ); + const embedded = await embedPayload(payload); + const mutated = replaceInternalChunks(embedded, (internalChunks) => { + const manifestChunk = internalChunks.at(-1); + if (!manifestChunk) { + throw new Error("Expected manifest chunk"); + } + + const manifest = parsePayloadSegment( + getInternalTextChunkPayload(manifestChunk), + ); + if (manifest.kind !== "manifest") { + throw new Error("Expected a manifest segment"); + } + + return [ + ...internalChunks.slice(0, -1).map((chunk) => chunk.raw), + createTextChunk( + createPayloadManifestSegment({ + payloadCrc32: manifest.payloadCrc32 ^ 0xffffffff, + segmentCount: manifest.segmentCount, + }), + ), + ]; + }); + const reader = streamPNGTextChunk(readableFromChunks(mutated)).getReader(); + const first = await reader.read(); + + expect(first.done).toBe(false); + expect(first.value).toEqual( + payload.subarray(0, PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + const second = await reader.read(); + expect(second.done).toBe(false); + expect(second.value).toEqual( + payload.subarray(PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH), + ); + + await expect(reader.read()).rejects.toThrow("PNG payload CRC mismatch"); + }); +}); + +describe("createPNGTextChunkWriter", () => { + it("embeds payload chunks before IEND and round-trips with extractor", async () => { + const payload = new Uint8Array([0, 1, 2, 3, 0, 4, 5, 0, 6]); + const writer = createPNGTextChunkWriter( + readableFromChunks(splitBytes(MINIMAL_PNG, [8, 7, 5, 3, 2, 1])), + ); + + await readableFromChunks(splitIntoSingleBytes(payload)).pipeTo( + writer.writable, + ); + const rebuilt = await readAllBytes(writer.readable); + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(splitIntoSingleBytes(rebuilt))), + ); + const chunks = parsePNGBytes(rebuilt); + const iendIndex = chunks.findIndex((chunk) => chunk.type === "IEND"); + const internalChunks = chunks.filter(isInternalTextChunk); + const internalIndex = chunks.findIndex(isInternalTextChunk); + const previousChunk = chunks[iendIndex - 1]; + const manifestChunk = internalChunks.at(-1); + const manifest = manifestChunk + ? parsePayloadSegment(getInternalTextChunkPayload(manifestChunk)) + : null; + + expect(extracted).toEqual(payload); + expect(internalIndex).toBeGreaterThanOrEqual(0); + expect(iendIndex).toBeGreaterThan(internalIndex); + expect(previousChunk ? isInternalTextChunk(previousChunk) : false).toBe( + true, + ); + expect(manifest?.kind).toBe("manifest"); + expect(internalChunks.slice(0, -1).length).toBeGreaterThan(0); + expect( + internalChunks.slice(0, -1).every((chunk, index) => { + const parsed = parsePayloadSegment(getInternalTextChunkPayload(chunk)); + return parsed.kind === "data" && parsed.segmentIndex === index; + }), + ).toBe(true); + if (manifest?.kind !== "manifest") { + throw new Error("Expected a manifest segment"); + } + expect(manifest.segmentCount).toBe(internalChunks.length - 1); + expect(manifest.payloadCrc32).toBe(crc32(payload)); + }); + + it("emits validated source chunks before payload close and keeps IEND pending", async () => { + const sourceChunks = parsePNGBytes(MINIMAL_PNG); + const writer = createPNGTextChunkWriter( + readableFromDelayedChunks( + [PNG_SIGNATURE, ...sourceChunks.map((chunk) => chunk.raw)], + 10, + ), + ); + const reader = writer.readable.getReader(); + const payloadWriter = writer.writable.getWriter(); + const writePromise = payloadWriter.write(new Uint8Array([1, 2, 3])); + + const first = await Promise.race([ + reader.read(), + timeoutFail(40, "Expected the PNG signature before payload close"), + ]); + const firstChunk = expectReadValue(first, "Expected PNG signature"); + const second = await reader.read(); + const third = await reader.read(); + const pendingRead = reader.read(); + + expect(first).toEqual({ done: false, value: PNG_SIGNATURE }); + expect(second).toEqual({ done: false, value: sourceChunks[0]?.raw }); + expect(third).toEqual({ done: false, value: sourceChunks[1]?.raw }); + await expect( + Promise.race([pendingRead.then(() => "done"), delayed("pending", 5)]), + ).resolves.toBe("pending"); + + await writePromise; + await payloadWriter.close(); + + const remaining: Uint8Array[] = []; + const fourth = await pendingRead; + if (!fourth.done) { + remaining.push(fourth.value); + } + while (true) { + const next = await reader.read(); + if (next.done) break; + remaining.push(next.value); + } + + const rebuilt = concatBytes( + firstChunk, + second.value as Uint8Array, + third.value as Uint8Array, + ...remaining, + ); + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(rebuilt)), + ); + + expect(extracted).toEqual(new Uint8Array([1, 2, 3])); + }); + + it("does not drain the source png past one upstream prefetch window before downstream reads demand more output", async () => { + const sourceChunks = [ + PNG_SIGNATURE, + ...parsePNGBytes(MINIMAL_PNG).map((chunk) => chunk.raw), + ]; + let pullCount = 0; + let index = 0; + const source = new ReadableStream({ + async pull(controller) { + pullCount += 1; + if (index >= sourceChunks.length) { + controller.close(); + return; + } + + const chunk = sourceChunks[index]; + index += 1; + await delayed(undefined, 5); + controller.enqueue(chunk); + }, + }); + const writer = createPNGTextChunkWriter(source); + + await delayed(undefined, 40); + + expect(pullCount).toBeLessThanOrEqual(2); + await writer.readable.cancel(new Error("stop writer output")); + }); + + it("uses error as the default existing-chunk policy", async () => { + const embedded = await embedPayload(new Uint8Array([1, 2, 3])); + const writer = createPNGTextChunkWriter(readableFromChunks(embedded)); + const writePromise = readableFromChunks(new Uint8Array([4, 5, 6])).pipeTo( + writer.writable, + ); + + await expect(writePromise).rejects.toThrow(); + await expect(readAllBytes(writer.readable)).rejects.toThrow(); + }); + + it("replaces an existing embedded payload when onExisting is replace", async () => { + const embedded = await embedPayload(new Uint8Array([1, 2, 3])); + const rewritten = await embedPayload( + new Uint8Array([4, 5, 6, 7]), + { onExisting: "replace" }, + embedded, + ); + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(rewritten)), + ); + const chunks = parsePNGBytes(rewritten).filter(isInternalTextChunk); + + expect(extracted).toEqual(new Uint8Array([4, 5, 6, 7])); + expect(chunks.length).toBeGreaterThan(0); + }); + + it("preserves unrelated tEXt chunks byte-for-byte when onExisting is replace", async () => { + const unrelatedTextChunk = createTextChunk( + new TextEncoder().encode("keep-me"), + "note", + ); + const sourceWithText = concatBytes( + PNG_SIGNATURE, + createChunk("IHDR", Uint8Array.of(0, 0, 0, 1, 0, 0, 0, 1, 8, 6, 0, 0, 0)), + unrelatedTextChunk, + createChunk( + "IDAT", + Uint8Array.of(0x78, 0x9c, 0x63, 0x00, 0x00, 0x00, 0x02, 0x00, 0x01), + ), + createChunk("IEND", new Uint8Array(0)), + ); + const embedded = await embedPayload( + new Uint8Array([1, 2, 3]), + undefined, + sourceWithText, + ); + const rewritten = await embedPayload( + new Uint8Array([4, 5, 6, 7]), + { onExisting: "replace" }, + embedded, + ); + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(rewritten)), + ); + const textChunks = parsePNGBytes(rewritten).filter( + (chunk) => chunk.type === "tEXt", + ); + const unrelatedChunks = textChunks.filter( + (chunk) => !isInternalTextChunk(chunk), + ); + const internalChunks = textChunks.filter(isInternalTextChunk); + + expect(extracted).toEqual(new Uint8Array([4, 5, 6, 7])); + expect(unrelatedChunks).toHaveLength(1); + expect(unrelatedChunks[0]?.raw).toEqual(unrelatedTextChunk); + expect(internalChunks).toHaveLength(2); + }); + + it("waits for payload close before completing the output", async () => { + const writer = createPNGTextChunkWriter(readableFromChunks(MINIMAL_PNG)); + const outputPromise = readAllBytes(writer.readable); + const payloadWriter = writer.writable.getWriter(); + + await payloadWriter.write(new Uint8Array([1, 2, 3])); + + await expect( + Promise.race([outputPromise.then(() => "done"), delayed("pending", 20)]), + ).resolves.toBe("pending"); + + await payloadWriter.close(); + const rebuilt = await outputPromise; + const extracted = await readAllBytes( + extractPNGTextChunk(readableFromChunks(rebuilt)), + ); + + expect(extracted).toEqual(new Uint8Array([1, 2, 3])); + }); + + it("rejects pending close when the source png is invalid", async () => { + const mutated = MINIMAL_PNG.slice(); + mutated[20] ^= 0xff; + const writer = createPNGTextChunkWriter(readableFromChunks(mutated)); + const payloadWriter = writer.writable.getWriter(); + + await expect( + payloadWriter.write(new Uint8Array([1, 2, 3])), + ).rejects.toThrow(); + await expect(payloadWriter.close()).rejects.toThrow(); + await expect(readAllBytes(writer.readable)).rejects.toThrow(); + }); + + it("propagates readable cancel reasons and cancels the source png reader", async () => { + let canceledReason: unknown; + const source = new ReadableStream({ + cancel(reason) { + canceledReason = reason; + }, + start(controller) { + controller.enqueue(MINIMAL_PNG); + }, + }); + const writer = createPNGTextChunkWriter(source); + const reason = new Error("cancel output"); + + await writer.readable.cancel(reason); + + await expect( + writer.writable.getWriter().write(new Uint8Array([1])), + ).rejects.toBe(reason); + expect(canceledReason).toBe(reason); + }); + + it("propagates writable abort reasons and cancels the source png reader", async () => { + let canceledReason: unknown; + const source = new ReadableStream({ + cancel(reason) { + canceledReason = reason; + }, + start(controller) { + controller.enqueue(MINIMAL_PNG); + }, + }); + const writer = createPNGTextChunkWriter(source); + const payloadWriter = writer.writable.getWriter(); + const reason = new Error("abort payload"); + + await expect(payloadWriter.abort(reason)).resolves.toBeUndefined(); + await expect(readAllBytes(writer.readable)).rejects.toBe(reason); + expect(canceledReason).toBe(reason); + }); +}); diff --git a/src/png.ts b/src/png.ts new file mode 100644 index 0000000..e5d60f3 --- /dev/null +++ b/src/png.ts @@ -0,0 +1,9 @@ +export type { + PNGTextChunkWriteOptions, + PNGTextChunkWriter, +} from "./png/public"; +export { + createPNGTextChunkWriter, + extractPNGTextChunk, + streamPNGTextChunk, +} from "./png/public"; diff --git a/src/png/constants.ts b/src/png/constants.ts new file mode 100644 index 0000000..8b72ff5 --- /dev/null +++ b/src/png/constants.ts @@ -0,0 +1,22 @@ +export const PNG_SIGNATURE = Uint8Array.of( + 0x89, + 0x50, + 0x4e, + 0x47, + 0x0d, + 0x0a, + 0x1a, + 0x0a, +); + +export const PNG_TEXT_CHUNK_TYPE = "tEXt"; +export const PNG_IEND_CHUNK_TYPE = "IEND"; +export const PNG_INTERNAL_TEXT_CHUNK_KEYWORD = "hsblabs-web-stream-extras-bin"; + +export const PNG_PAYLOAD_MAGIC = new TextEncoder().encode("HSBP"); +export const PNG_PAYLOAD_VERSION = 2; +export const PNG_PAYLOAD_SEGMENT_KIND_DATA = 0; +export const PNG_PAYLOAD_SEGMENT_KIND_MANIFEST = 1; +export const PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH = 32 * 1024; +export const PNG_PAYLOAD_DATA_HEADER_LENGTH = 10; +export const PNG_PAYLOAD_MANIFEST_HEADER_LENGTH = 14; diff --git a/src/png/crc32.ts b/src/png/crc32.ts new file mode 100644 index 0000000..f16747f --- /dev/null +++ b/src/png/crc32.ts @@ -0,0 +1,37 @@ +const CRC32_TABLE = (() => { + const table = new Uint32Array(256); + + for (let index = 0; index < table.length; index++) { + let value = index; + + for (let bit = 0; bit < 8; bit++) { + value = (value & 1) === 0 ? value >>> 1 : (value >>> 1) ^ 0xedb8_8320; + } + + table[index] = value >>> 0; + } + + return table; +})(); + +export function createCRC32State(): number { + return 0xffff_ffff; +} + +export function updateCRC32(state: number, input: Uint8Array): number { + let value = state >>> 0; + + for (const byte of input) { + value = (CRC32_TABLE[(value ^ byte) & 0xff] ?? 0) ^ (value >>> 8); + } + + return value >>> 0; +} + +export function finalizeCRC32(state: number): number { + return (state ^ 0xffff_ffff) >>> 0; +} + +export function crc32(input: Uint8Array): number { + return finalizeCRC32(updateCRC32(createCRC32State(), input)); +} diff --git a/src/png/framing.ts b/src/png/framing.ts new file mode 100644 index 0000000..93a3022 --- /dev/null +++ b/src/png/framing.ts @@ -0,0 +1,386 @@ +import { decodeCOBSFrame, encodeCOBSFrame } from "../cobs/public"; +import { throwError } from "../shared/error"; +import { concatU8Arrays } from "../shared/uint8array"; +import { + PNG_IEND_CHUNK_TYPE, + PNG_INTERNAL_TEXT_CHUNK_KEYWORD, + PNG_PAYLOAD_DATA_HEADER_LENGTH, + PNG_PAYLOAD_MAGIC, + PNG_PAYLOAD_MANIFEST_HEADER_LENGTH, + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + PNG_PAYLOAD_SEGMENT_KIND_DATA, + PNG_PAYLOAD_SEGMENT_KIND_MANIFEST, + PNG_PAYLOAD_VERSION, + PNG_SIGNATURE, + PNG_TEXT_CHUNK_TYPE, +} from "./constants"; +import { crc32 } from "./crc32"; + +const textEncoder = new TextEncoder(); +const ASCII_LETTER = /[A-Za-z]/; + +function latin1BytesToString(bytes: Uint8Array): string { + return Array.from(bytes, (value) => String.fromCharCode(value)).join(""); +} + +export interface PNGChunk { + crc: number; + data: Uint8Array; + length: number; + raw: Uint8Array; + type: string; +} + +export interface CreatePayloadDataSegmentInput { + segmentData: Uint8Array; + segmentIndex: number; +} + +export interface CreatePayloadManifestSegmentInput { + payloadCrc32: number; + segmentCount: number; +} + +interface ParsedPayloadSegmentBase { + kind: "data" | "manifest"; + magic: Uint8Array; + raw: Uint8Array; + version: number; +} + +export interface ParsedPayloadDataSegment extends ParsedPayloadSegmentBase { + kind: "data"; + segmentData: Uint8Array; + segmentIndex: number; +} + +export interface ParsedPayloadManifestSegment extends ParsedPayloadSegmentBase { + kind: "manifest"; + payloadCrc32: number; + segmentCount: number; +} + +export type ParsedPayloadSegment = + | ParsedPayloadDataSegment + | ParsedPayloadManifestSegment; + +interface ParsePNGBytesOptions { + requireIEND?: boolean; +} + +function readUint32BE(data: Uint8Array, offset: number): number { + return new DataView(data.buffer, data.byteOffset, data.byteLength).getUint32( + offset, + false, + ); +} + +function writeUint32BE(value: number): Uint8Array { + const output = new Uint8Array(4); + new DataView(output.buffer).setUint32(0, value >>> 0, false); + return output; +} + +function assertChunkType(type: string): void { + if (type.length !== 4 || [...type].some((char) => !ASCII_LETTER.test(char))) { + throwError("PNG chunk type must be 4 ASCII letters"); + } +} + +function assertChunkTypeBytes(typeBytes: Uint8Array): void { + if ( + typeBytes.byteLength !== 4 || + Array.from(typeBytes).some( + (value) => + !((value >= 0x41 && value <= 0x5a) || (value >= 0x61 && value <= 0x7a)), + ) + ) { + throwError("PNG chunk type must be 4 ASCII letters"); + } +} + +function assertKeyword(keyword: string): Uint8Array { + if (keyword.length === 0 || keyword.length >= 80 || keyword.includes("\0")) { + throwError("PNG tEXt keyword must be 1-79 bytes without NUL"); + } + + return textEncoder.encode(keyword); +} + +function parseTextChunkKeyword(data: Uint8Array): string | null { + const separatorIndex = data.indexOf(0); + if (separatorIndex <= 0) { + return null; + } + + return latin1BytesToString(data.subarray(0, separatorIndex)); +} + +export function assertPNGSignature(signature: Uint8Array): void { + if (signature.byteLength < PNG_SIGNATURE.byteLength) { + throwError("PNG signature is truncated"); + } + + if (!PNG_SIGNATURE.every((value, index) => signature[index] === value)) { + throwError("PNG signature is invalid"); + } +} + +export function createChunk(type: string, data: Uint8Array): Uint8Array { + assertChunkType(type); + const typeBytes = textEncoder.encode(type); + const lengthBytes = writeUint32BE(data.byteLength); + const crcBytes = writeUint32BE(crc32(concatU8Arrays(typeBytes, data))); + + return concatU8Arrays(lengthBytes, typeBytes, data, crcBytes); +} + +export function createTextChunk( + text: Uint8Array, + keyword = PNG_INTERNAL_TEXT_CHUNK_KEYWORD, +): Uint8Array { + if (text.includes(0)) { + throwError("PNG tEXt payload must not contain NUL"); + } + + return createChunk( + PNG_TEXT_CHUNK_TYPE, + concatU8Arrays(assertKeyword(keyword), Uint8Array.of(0), text), + ); +} + +export function createPayloadDataSegment( + input: CreatePayloadDataSegmentInput, +): Uint8Array { + const header = new Uint8Array(PNG_PAYLOAD_DATA_HEADER_LENGTH); + header.set(PNG_PAYLOAD_MAGIC, 0); + header[4] = PNG_PAYLOAD_VERSION; + header[5] = PNG_PAYLOAD_SEGMENT_KIND_DATA; + header.set(writeUint32BE(input.segmentIndex), 6); + + return encodeCOBSFrame(concatU8Arrays(header, input.segmentData)); +} + +export function createPayloadManifestSegment( + input: CreatePayloadManifestSegmentInput, +): Uint8Array { + const header = new Uint8Array(PNG_PAYLOAD_MANIFEST_HEADER_LENGTH); + header.set(PNG_PAYLOAD_MAGIC, 0); + header[4] = PNG_PAYLOAD_VERSION; + header[5] = PNG_PAYLOAD_SEGMENT_KIND_MANIFEST; + header.set(writeUint32BE(input.segmentCount), 6); + header.set(writeUint32BE(input.payloadCrc32), 10); + + return encodeCOBSFrame(header); +} + +export function parsePayloadSegment(encoded: Uint8Array): ParsedPayloadSegment { + const raw = decodeCOBSFrame(encoded); + + if (raw.byteLength < PNG_PAYLOAD_DATA_HEADER_LENGTH) { + throwError("PNG payload segment header is truncated"); + } + + const magic = raw.subarray(0, 4); + if (!magic.every((value, index) => value === PNG_PAYLOAD_MAGIC[index])) { + throwError("PNG payload segment magic is invalid"); + } + + const version = raw[4] ?? -1; + if (version !== PNG_PAYLOAD_VERSION) { + throwError("PNG payload segment version is invalid"); + } + + const kind = raw[5]; + if (kind === PNG_PAYLOAD_SEGMENT_KIND_DATA) { + return { + kind: "data", + magic: magic.slice(), + raw, + segmentData: raw.subarray(PNG_PAYLOAD_DATA_HEADER_LENGTH), + segmentIndex: readUint32BE(raw, 6), + version, + }; + } + + if (kind === PNG_PAYLOAD_SEGMENT_KIND_MANIFEST) { + if (raw.byteLength !== PNG_PAYLOAD_MANIFEST_HEADER_LENGTH) { + throwError("PNG payload manifest header is invalid"); + } + + return { + kind: "manifest", + magic: magic.slice(), + payloadCrc32: readUint32BE(raw, 10), + raw, + segmentCount: readUint32BE(raw, 6), + version, + }; + } + + throwError("PNG payload segment kind is invalid"); +} + +export function isInternalTextChunk(chunk: PNGChunk): boolean { + return ( + chunk.type === PNG_TEXT_CHUNK_TYPE && + parseTextChunkKeyword(chunk.data) === PNG_INTERNAL_TEXT_CHUNK_KEYWORD + ); +} + +export function buildPayloadTextChunks(payload: Uint8Array): Uint8Array[] { + const payloadCrc32 = crc32(payload); + const segmentCount = Math.ceil( + payload.byteLength / PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + ); + const chunks: Uint8Array[] = []; + + for (let index = 0; index < segmentCount; index++) { + const start = index * PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH; + const end = Math.min( + start + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + payload.byteLength, + ); + const segmentData = payload.slice(start, end); + + chunks.push( + createTextChunk( + createPayloadDataSegment({ + segmentData, + segmentIndex: index, + }), + ), + ); + } + + chunks.push( + createTextChunk( + createPayloadManifestSegment({ + payloadCrc32, + segmentCount, + }), + ), + ); + + return chunks; +} + +export interface PNGChunkHeader { + length: number; + type: string; +} + +export function parsePNGChunkHeader(headerBytes: Uint8Array): PNGChunkHeader { + if (headerBytes.byteLength < 8) { + throwError("PNG chunk header is truncated"); + } + + const typeBytes = headerBytes.subarray(4, 8); + assertChunkTypeBytes(typeBytes); + return { + length: readUint32BE(headerBytes, 0), + type: latin1BytesToString(typeBytes), + }; +} + +export function parsePNGChunk(raw: Uint8Array): PNGChunk { + if (raw.byteLength < 12) { + throwError("PNG chunk header is truncated"); + } + + const { length, type } = parsePNGChunkHeader(raw.subarray(0, 8)); + const dataStart = 8; + const dataEnd = dataStart + length; + const crcStart = dataEnd; + const nextOffset = crcStart + 4; + + if (nextOffset !== raw.byteLength) { + throwError("PNG chunk data is truncated"); + } + + const typeBytes = raw.subarray(4, 8); + const data = raw.slice(dataStart, dataEnd); + const crc = readUint32BE(raw, crcStart); + const expectedCrc = crc32(concatU8Arrays(typeBytes, data)); + + if (crc !== expectedCrc) { + throwError("PNG chunk CRC mismatch"); + } + + return { crc, data, length, raw, type }; +} + +export function parsePNGBytes( + bytes: Uint8Array, + { requireIEND = false }: ParsePNGBytesOptions = {}, +): PNGChunk[] { + assertPNGSignature(bytes); + + const chunks: PNGChunk[] = []; + let offset = PNG_SIGNATURE.byteLength; + let seenIEND = false; + + while (offset < bytes.byteLength) { + const header = parsePNGChunkHeader(bytes.subarray(offset, offset + 8)); + const nextOffset = offset + 8 + header.length + 4; + if (nextOffset > bytes.byteLength) { + throwError("PNG chunk data is truncated"); + } + + const chunk = parsePNGChunk(bytes.slice(offset, nextOffset)); + chunks.push(chunk); + offset = nextOffset; + + if (chunk.type === PNG_IEND_CHUNK_TYPE) { + if (chunk.length !== 0) { + throwError("PNG IEND chunk must be empty"); + } + if (offset !== bytes.byteLength) { + throwError("PNG has trailing bytes after IEND"); + } + seenIEND = true; + break; + } + } + + if (requireIEND && !seenIEND) { + throwError("PNG is missing IEND"); + } + + return chunks; +} + +export function rebuildPNGWithPayload( + sourcePNG: Uint8Array, + payload: Uint8Array, + onExisting: "error" | "replace", +): Uint8Array { + const chunks = parsePNGBytes(sourcePNG, { requireIEND: true }); + const output: Uint8Array[] = [PNG_SIGNATURE]; + + for (const chunk of chunks) { + if (chunk.type === PNG_IEND_CHUNK_TYPE) { + output.push(...buildPayloadTextChunks(payload), chunk.raw); + continue; + } + + if (!isInternalTextChunk(chunk)) { + output.push(chunk.raw); + continue; + } + + if (onExisting === "error") { + throwError("PNG already contains an embedded payload"); + } + } + + return concatU8Arrays(...output); +} + +export function getInternalTextChunkPayload(chunk: PNGChunk): Uint8Array { + if (!isInternalTextChunk(chunk)) { + throwError("PNG chunk is not an internal tEXt chunk"); + } + + return chunk.data.subarray(PNG_INTERNAL_TEXT_CHUNK_KEYWORD.length + 1); +} diff --git a/src/png/public.ts b/src/png/public.ts new file mode 100644 index 0000000..94d700c --- /dev/null +++ b/src/png/public.ts @@ -0,0 +1,49 @@ +import { + createPNGTextChunkWriterImpl, + extractPNGTextChunkImpl, + streamPNGTextChunkImpl, +} from "./transformers"; + +export interface PNGTextChunkWriteOptions { + onExisting?: "error" | "replace"; +} + +export interface PNGTextChunkWriter { + writable: WritableStream; + readable: ReadableStream; +} + +/** + * Rebuilds a PNG by forwarding validated source chunks, appending internal + * payload chunks immediately before the final `IEND`. + */ +export function createPNGTextChunkWriter( + png: ReadableStream, + options?: PNGTextChunkWriteOptions, +): PNGTextChunkWriter { + return createPNGTextChunkWriterImpl(png, options); +} + +/** + * Extracts the embedded payload with all-or-nothing semantics. + * + * The returned stream emits only after the terminal manifest and payload CRC + * have been validated. + */ +export function extractPNGTextChunk( + png: ReadableStream, +): ReadableStream { + return extractPNGTextChunkImpl(png); +} + +/** + * Extracts the embedded payload as soon as data segments arrive. + * + * This late-error variant may emit partial payload bytes before rejecting if + * the terminal manifest or payload CRC is invalid. + */ +export function streamPNGTextChunk( + png: ReadableStream, +): ReadableStream { + return streamPNGTextChunkImpl(png); +} diff --git a/src/png/transformers.ts b/src/png/transformers.ts new file mode 100644 index 0000000..40201a2 --- /dev/null +++ b/src/png/transformers.ts @@ -0,0 +1,527 @@ +import { ByteQueue } from "../byte-queue"; +import { throwError } from "../shared/error"; +import { concatU8Arrays } from "../shared/uint8array"; +import { + PNG_IEND_CHUNK_TYPE, + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + PNG_SIGNATURE, +} from "./constants"; +import { createCRC32State, finalizeCRC32, updateCRC32 } from "./crc32"; +import { + assertPNGSignature, + createPayloadDataSegment, + createPayloadManifestSegment, + createTextChunk, + getInternalTextChunkPayload, + isInternalTextChunk, + parsePayloadSegment, + parsePNGChunk, + parsePNGChunkHeader, +} from "./framing"; +import type { PNGTextChunkWriteOptions, PNGTextChunkWriter } from "./public"; + +interface Deferred { + promise: Promise; + reject(reason?: unknown): void; + resolve(value: T | PromiseLike): void; +} + +function createDeferred(): Deferred { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((innerResolve, innerReject) => { + resolve = innerResolve; + reject = innerReject; + }); + void promise.catch(() => {}); + + return { promise, reject, resolve }; +} + +function normalizeReason(reason: unknown): unknown { + return reason ?? new Error("PNG stream was canceled"); +} + +interface PayloadWriteRequest { + ack: Deferred; + chunk: Uint8Array; +} + +async function cancelSourceReader( + sourceReader: ReadableStreamDefaultReader, + reason: unknown, +): Promise { + try { + await sourceReader.cancel(reason); + } catch {} +} + +async function readPNGSignature( + sourceReader: ReadableStreamDefaultReader, + queue: ByteQueue, +): Promise { + while (queue.byteLength < PNG_SIGNATURE.byteLength) { + const { done, value } = await sourceReader.read(); + if (done) { + throwError("PNG signature is truncated"); + } + + queue.append(value); + } + + const signature = queue.read(PNG_SIGNATURE.byteLength); + assertPNGSignature(signature); + return signature; +} + +async function readSourceChunkBytes( + sourceReader: ReadableStreamDefaultReader, + queue: ByteQueue, +): Promise { + while (queue.byteLength < 8) { + const { done, value } = await sourceReader.read(); + if (done) { + if (queue.byteLength === 0) { + return null; + } + throwError("PNG chunk header is truncated"); + } + + queue.append(value); + } + + const header = queue.read(8); + const { length } = parsePNGChunkHeader(header); + + while (queue.byteLength < length + 4) { + const { done, value } = await sourceReader.read(); + if (done) { + throwError("PNG chunk data is truncated"); + } + + queue.append(value); + } + + const tail = queue.read(length + 4); + const raw = new Uint8Array(8 + tail.byteLength); + raw.set(header, 0); + raw.set(tail, 8); + return raw; +} + +async function assertNoTrailingBytes( + sourceReader: ReadableStreamDefaultReader, + queue: ByteQueue, +): Promise { + if (queue.byteLength !== 0) { + throwError("PNG has trailing bytes after IEND"); + } + + while (true) { + const { done, value } = await sourceReader.read(); + if (done) { + return; + } + if (value.byteLength !== 0) { + throwError("PNG has trailing bytes after IEND"); + } + } +} + +export function createPNGTextChunkWriterImpl( + png: ReadableStream, + { onExisting = "error" }: PNGTextChunkWriteOptions = {}, +): PNGTextChunkWriter { + const sourceReader = png.getReader(); + const completion = createDeferred(); + const sourceValidated = createDeferred(); + const payloadWrites: PayloadWriteRequest[] = []; + let payloadClosed = false; + let payloadNotifier = createDeferred(); + let outputNotifier = createDeferred(); + const outputChunks: Uint8Array[] = []; + const sourceQueue = new ByteQueue(); + let sourcePump: Promise | null = null; + let iendChunkRaw: Uint8Array | null = null; + let outputClosed = false; + let failure: unknown; + + function fail(reason: unknown): void { + if (failure !== undefined) { + return; + } + + failure = normalizeReason(reason); + for (const request of payloadWrites.splice(0)) { + request.ack.reject(failure); + } + sourceValidated.reject(failure); + completion.reject(failure); + outputNotifier.resolve(); + payloadNotifier.resolve(); + } + + function notifyPayload(): void { + payloadNotifier.resolve(); + } + + function enqueueOutput(chunk: Uint8Array): void { + outputChunks.push(chunk); + outputNotifier.resolve(); + } + + async function waitForPayloadActivity(): Promise { + const current = payloadNotifier; + await current.promise; + if (payloadNotifier === current) { + payloadNotifier = createDeferred(); + } + } + + async function waitForOutputActivity(): Promise { + const current = outputNotifier; + await current.promise; + if (outputNotifier === current) { + outputNotifier = createDeferred(); + } + } + + const initializePromise = (async () => { + try { + enqueueOutput(await readPNGSignature(sourceReader, sourceQueue)); + } catch (error) { + const reason = failure ?? error; + fail(reason); + await cancelSourceReader(sourceReader, reason); + } + })(); + void initializePromise.catch(() => {}); + + function startSourcePump(): void { + if (sourcePump) { + return; + } + + sourcePump = (async () => { + try { + await initializePromise; + if (failure !== undefined) { + throw failure; + } + + while (true) { + const raw = await readSourceChunkBytes(sourceReader, sourceQueue); + if (!raw) { + throwError("PNG is missing IEND"); + } + + const chunk = parsePNGChunk(raw); + if (chunk.type === PNG_IEND_CHUNK_TYPE) { + if (chunk.length !== 0) { + throwError("PNG IEND chunk must be empty"); + } + + iendChunkRaw = chunk.raw; + break; + } + + if (isInternalTextChunk(chunk)) { + if (onExisting === "error") { + throwError("PNG already contains an embedded payload"); + } + + continue; + } + + enqueueOutput(chunk.raw); + } + + await assertNoTrailingBytes(sourceReader, sourceQueue); + sourceValidated.resolve(); + + let payloadCrcState = createCRC32State(); + let segmentCount = 0; + + while (true) { + while (payloadWrites.length > 0) { + const request = payloadWrites.shift(); + if (!request) { + continue; + } + + payloadCrcState = updateCRC32(payloadCrcState, request.chunk); + + for ( + let offset = 0; + offset < request.chunk.byteLength; + offset += PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH + ) { + const next = Math.min( + offset + PNG_PAYLOAD_SEGMENT_DATA_MAX_LENGTH, + request.chunk.byteLength, + ); + enqueueOutput( + createTextChunk( + createPayloadDataSegment({ + segmentData: request.chunk.subarray(offset, next), + segmentIndex: segmentCount, + }), + ), + ); + segmentCount++; + } + } + + if (payloadClosed) { + break; + } + + await waitForPayloadActivity(); + if (failure !== undefined) { + throw failure; + } + } + + enqueueOutput( + createTextChunk( + createPayloadManifestSegment({ + payloadCrc32: finalizeCRC32(payloadCrcState), + segmentCount, + }), + ), + ); + if (!iendChunkRaw) { + throwError("PNG is missing IEND"); + } + enqueueOutput(iendChunkRaw); + outputClosed = true; + outputNotifier.resolve(); + completion.resolve(); + } catch (error) { + const reason = failure ?? error; + fail(reason); + await cancelSourceReader(sourceReader, reason); + } + })(); + void sourcePump.catch(() => {}); + } + + const readable = new ReadableStream({ + async cancel(reason) { + fail(reason); + await cancelSourceReader(sourceReader, failure); + }, + async pull(controller) { + await initializePromise; + + while (outputChunks.length === 0) { + if (failure !== undefined) { + controller.error(failure); + return; + } + if (outputClosed) { + controller.close(); + return; + } + + startSourcePump(); + await waitForOutputActivity(); + } + + if (failure !== undefined) { + controller.error(failure); + return; + } + + const next = outputChunks.shift(); + if (!next) { + controller.close(); + return; + } + + controller.enqueue(next); + }, + }); + + const writable = new WritableStream({ + async abort(reason) { + fail(reason); + await cancelSourceReader(sourceReader, failure); + }, + close() { + if (failure !== undefined) { + return Promise.reject(failure); + } + + payloadClosed = true; + startSourcePump(); + notifyPayload(); + return completion.promise; + }, + write(chunk) { + if (failure !== undefined) { + return Promise.reject(failure); + } + + const request = { + ack: createDeferred(), + chunk: chunk.slice(), + }; + void sourceValidated.promise.then( + () => request.ack.resolve(), + (reason) => request.ack.reject(reason), + ); + + payloadWrites.push(request); + startSourcePump(); + notifyPayload(); + return request.ack.promise; + }, + }); + + return { readable, writable }; +} + +export function extractPNGTextChunkImpl( + png: ReadableStream, +): ReadableStream { + const sourceReader = streamPNGTextChunkImpl(png).getReader(); + let canceled = false; + + return new ReadableStream({ + async cancel(reason) { + canceled = true; + await sourceReader.cancel(reason); + }, + async start(controller) { + try { + const parts: Uint8Array[] = []; + + while (true) { + const { done, value } = await sourceReader.read(); + if (done) { + break; + } + + parts.push(value); + } + + if (canceled) { + return; + } + + controller.enqueue(concatU8Arrays(...parts)); + controller.close(); + } catch (error) { + if (canceled) { + return; + } + + controller.error(error); + } + }, + }); +} + +export function streamPNGTextChunkImpl( + png: ReadableStream, +): ReadableStream { + const sourceReader = png.getReader(); + let cancelReason: unknown; + const iterator = (async function* (): AsyncGenerator { + let completed = false; + + try { + const sourceQueue = new ByteQueue(); + await readPNGSignature(sourceReader, sourceQueue); + + let expectedIndex = 0; + let manifest: ReturnType | null = null; + let payloadCrcState = createCRC32State(); + + while (true) { + const raw = await readSourceChunkBytes(sourceReader, sourceQueue); + if (!raw) { + throwError("PNG is missing IEND"); + } + + const chunk = parsePNGChunk(raw); + if (chunk.type === PNG_IEND_CHUNK_TYPE) { + if (chunk.length !== 0) { + throwError("PNG IEND chunk must be empty"); + } + + break; + } + + if (!isInternalTextChunk(chunk)) { + continue; + } + + const segment = parsePayloadSegment(getInternalTextChunkPayload(chunk)); + + if (segment.kind === "manifest") { + if (manifest) { + throwError("PNG payload manifest is duplicated"); + } + + manifest = segment; + continue; + } + + if (manifest) { + throwError("PNG payload data appears after manifest"); + } + + if (segment.segmentIndex !== expectedIndex) { + throwError("PNG payload segment index is invalid"); + } + + payloadCrcState = updateCRC32(payloadCrcState, segment.segmentData); + expectedIndex++; + yield segment.segmentData.slice(); + } + + await assertNoTrailingBytes(sourceReader, sourceQueue); + + if (!manifest || manifest.kind !== "manifest") { + throwError("PNG does not contain an embedded payload"); + } + if (expectedIndex !== manifest.segmentCount) { + throwError("PNG payload segment count is incomplete"); + } + if (finalizeCRC32(payloadCrcState) !== manifest.payloadCrc32) { + throwError("PNG payload CRC mismatch"); + } + + completed = true; + } finally { + if (!completed) { + await cancelSourceReader(sourceReader, cancelReason); + } + } + })(); + + return new ReadableStream({ + async cancel(reason) { + cancelReason = reason; + await Promise.allSettled([ + cancelSourceReader(sourceReader, reason), + iterator.return(undefined), + ]); + }, + async pull(controller) { + try { + const { done, value } = await iterator.next(); + if (done) { + controller.close(); + return; + } + + controller.enqueue(value); + } catch (error) { + controller.error(error); + } + }, + }); +} diff --git a/tsconfig.json b/tsconfig.json index 41ed82d..4c145a7 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,6 @@ "module": "ESNext", "moduleResolution": "Bundler", "allowImportingTsExtensions": false, - "baseUrl": ".", "rootDir": "./src", "declaration": true, "sourceMap": true, diff --git a/tsconfig.typecheck.json b/tsconfig.typecheck.json new file mode 100644 index 0000000..92f54db --- /dev/null +++ b/tsconfig.typecheck.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "ignoreDeprecations": "6.0" + }, + "include": ["src/**/*.ts"], + "exclude": ["dist"] +} diff --git a/tsdown.config.ts b/tsdown.config.ts index 5f08113..237da2d 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -7,6 +7,7 @@ export default defineConfig({ "src/encryption.ts", "src/jsonl.ts", "src/text.ts", + "src/png.ts", "src/cobs.ts" ], outDir: "dist", target: "es2022",