diff --git a/.changeset/record-adapter-do-sqlite.md b/.changeset/record-adapter-do-sqlite.md new file mode 100644 index 0000000..becfe43 --- /dev/null +++ b/.changeset/record-adapter-do-sqlite.md @@ -0,0 +1,25 @@ +--- +'@haverstack/record-adapter-do-sqlite': patch +'@haverstack/record-adapter-sqlite': patch +--- + +Add `@haverstack/record-adapter-do-sqlite` — a `StackRecordAdapter` over Cloudflare +Durable Objects' SQLite storage, for Workers deployments with no Node runtime +available. Reuses `SharedSqlRecordLogic`, the FTS5 schema and strategy, the query +builder, cursor codec, and row mappers from `@haverstack/sqlite-shared` — the same +shared layer `record-adapter-sqlite` is built on, now via its `./record` subpath +(the token-store and file-lock pieces stay Node-only and unreachable from this +adapter's bundle). No lock file: a Durable Object id maps to exactly one running +instance, so the platform itself is the single-writer guarantee. No persist/flush +step: every write through `ctx.storage.sql` is durable by the time the call returns. + +`@haverstack/sqlite-shared`'s `SqlExecutor` gained a `transaction(fn: () => T): T` +primitive, replacing the raw `BEGIN`/`COMMIT`/`ROLLBACK` statements `record-logic.ts` +used to issue directly. Durable Object SQLite storage rejects those statements +outright and does not roll back a write on a later exception the way an open SQL +transaction would (verified against the real Workers runtime) — its real primitive +is `ctx.storage.transactionSync(fn)`, a callback boundary that three independent +string-based `exec()` calls can't reach. `record-adapter-sqlite`'s executor +implements `transaction()` as literal `BEGIN`/`COMMIT`/`ROLLBACK` around `fn()`, +behavior-identical to what the inline code did before — its full test suite passes +unchanged. diff --git a/.gitignore b/.gitignore index 26eef9e..257bf6a 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,12 @@ dist/ *.js.map *.d.ts *.d.ts.map +# ...except hand-authored ambient declaration files, e.g. a Workers +# package's triple-slash reference to @cloudflare/vitest-pool-workers/types +# for `cloudflare:test` typings (wrangler's own generated +# worker-configuration.d.ts stays ignored — it's regenerated by a +# pretest/pretypecheck script, not committed). +!**/tests/support/*.d.ts # Test databases *.db diff --git a/README.md b/README.md index a534877..b0ed927 100644 --- a/README.md +++ b/README.md @@ -77,15 +77,16 @@ The delegation itself — "this app acts for Bob" — is asserted by you when th This is a monorepo. Packages are published to npm under the `@haverstack` scope. -| Package | Description | -| ----------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -| [`@haverstack/core`](./packages/core) | Stack class, types, schema, validation, ID generation | -| [`@haverstack/adapter-local`](./packages/adapter-local) | Local adapter (native SQLite + disk) — single-app/embedded or server use | -| [`@haverstack/record-adapter-sqlite`](./packages/record-adapter-sqlite) | Node native SQLite (`node:sqlite`) `StackRecordAdapter` — used by `adapter-local` | -| [`@haverstack/blob-adapter-disk`](./packages/blob-adapter-disk) | Disk filesystem `StackBlobAdapter` | -| [`@haverstack/blob-adapter-s3`](./packages/blob-adapter-s3) | S3 (and S3-compatible, e.g. Cloudflare R2) `StackBlobAdapter` | -| [`@haverstack/adapter-api`](./packages/adapter-api) | HTTP adapter for remote stack servers | -| [`@haverstack/commons`](./packages/commons) | Canonical Schema Commons type definitions (`note`, `task`, `contact`, ...) | +| Package | Description | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| [`@haverstack/core`](./packages/core) | Stack class, types, schema, validation, ID generation | +| [`@haverstack/adapter-local`](./packages/adapter-local) | Local adapter (native SQLite + disk) — single-app/embedded or server use | +| [`@haverstack/record-adapter-sqlite`](./packages/record-adapter-sqlite) | Node native SQLite (`node:sqlite`) `StackRecordAdapter` — used by `adapter-local` | +| [`@haverstack/record-adapter-do-sqlite`](./packages/record-adapter-do-sqlite) | Cloudflare Durable Objects (SQLite storage) `StackRecordAdapter` — Workers | +| [`@haverstack/blob-adapter-disk`](./packages/blob-adapter-disk) | Disk filesystem `StackBlobAdapter` | +| [`@haverstack/blob-adapter-s3`](./packages/blob-adapter-s3) | S3 (and S3-compatible, e.g. Cloudflare R2) `StackBlobAdapter` | +| [`@haverstack/adapter-api`](./packages/adapter-api) | HTTP adapter for remote stack servers | +| [`@haverstack/commons`](./packages/commons) | Canonical Schema Commons type definitions (`note`, `task`, `contact`, ...) | Planned: @@ -244,14 +245,15 @@ The adapter interface is split into `StackRecordAdapter` (structured records) an - **`record-adapter-*`** — `StackRecordAdapter` only - **`blob-adapter-*`** — `StackBlobAdapter` only -| Package | Type | Use case | -| ----------------------- | ------ | ------------------------------------------------------------------------------- | -| `adapter-local` | full | Single-app/embedded or server use — native SQLite records + disk blobs | -| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL — used by `adapter-local` | -| `blob-adapter-disk` | blob | Content-addressed blobs on the local filesystem | -| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | -| `adapter-api` | full | Hosted/shared stacks via HTTP | -| `adapter-json` | full | Portable JSON files _(planned)_ | +| Package | Type | Use case | +| -------------------------- | ------ | ------------------------------------------------------------------------------- | +| `adapter-local` | full | Single-app/embedded or server use — native SQLite records + disk blobs | +| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL — used by `adapter-local` | +| `record-adapter-do-sqlite` | record | Cloudflare Durable Objects (SQLite storage) records, FTS5 | +| `blob-adapter-disk` | blob | Content-addressed blobs on the local filesystem | +| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | +| `adapter-api` | full | Hosted/shared stacks via HTTP | +| `adapter-json` | full | Portable JSON files _(planned)_ | Use `combineAdapters({ record, blob })` from `@haverstack/core/adapter` to compose a record adapter with a different blob backend — for example, `NativeSQLiteRecordAdapter` with `S3BlobAdapter`. `adapter-local` wraps this pattern for the common case. @@ -314,6 +316,10 @@ packages/ index.ts # NativeSQLiteRecordAdapter (StackRecordAdapter), node:sqlite token-store.ts # NativeTokenStore (StackTokenStore), separate file from records tests/ + record-adapter-do-sqlite/ # @haverstack/record-adapter-do-sqlite + src/ + index.ts # DoSQLiteRecordAdapter (StackRecordAdapter), Cloudflare Durable Objects + tests/ blob-adapter-disk/ # @haverstack/blob-adapter-disk src/ index.ts # DiskBlobAdapter (StackBlobAdapter) diff --git a/docs/spec/adapters.md b/docs/spec/adapters.md index f075bc1..0158851 100644 --- a/docs/spec/adapters.md +++ b/docs/spec/adapters.md @@ -36,14 +36,15 @@ Packages follow a naming convention that makes the adapter type discoverable: ## Adapter backends -| Package | Type | Use case | -| ----------------------- | ------ | ---------------------------------------------------------------------------- | -| `adapter-local` | full | Local app storage — native SQLite + disk blobs | -| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL | -| `blob-adapter-disk` | blob | Content-addressed blobs on disk | -| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | -| `adapter-api` | full | Hosted/shared stacks via HTTP | -| `adapter-json` | full | Portable JSON files _(planned)_ | +| Package | Type | Use case | +| -------------------------- | ------ | ---------------------------------------------------------------------------- | +| `adapter-local` | full | Local app storage — native SQLite + disk blobs | +| `record-adapter-sqlite` | record | Node native SQLite (`node:sqlite`) records, FTS5, WAL | +| `record-adapter-do-sqlite` | record | Cloudflare Durable Objects (SQLite storage) records, FTS5 | +| `blob-adapter-disk` | blob | Content-addressed blobs on disk | +| `blob-adapter-s3` | blob | Content-addressed blobs on S3 or an S3-compatible store (e.g. Cloudflare R2) | +| `adapter-api` | full | Hosted/shared stacks via HTTP | +| `adapter-json` | full | Portable JSON files _(planned)_ | `adapter-local` is the batteries-included package for the common local case. It wraps `NativeSQLiteRecordAdapter` and `DiskBlobAdapter` and stores attachments in an `attachments/` subdirectory next to the database file. Bearer tokens, when used, live in a separate sibling file (`.tokens`, via `NativeTokenStore`) — never inside the portable stack database. @@ -64,11 +65,13 @@ const stack = await Stack.create(adapter); All adapters support the full Record API. Performance guarantees differ; correctness does not. -**`@haverstack/sqlite-shared`** is an internal, non-public package holding everything a SQLite-backed record adapter needs that isn't specific to one binding — schema DDL, `WHERE`/`ORDER` building, the cursor codec, row mappers, the FTS5 sanitizer and indexing strategy, the storage-ownership lock, and (via a small `SqlExecutor` interface normalizing a binding's call convention) the actual CRUD/query/version/type/association/token logic itself. An adapter implements only what's genuinely engine-specific: database construction, pragma/WAL setup, and lifecycle. `record-adapter-sqlite` is its only consumer today; the split exists so a second SQLite engine inherits the behavior rather than reimplementing it, and so a cursor minted by one is decodable by another. +**`@haverstack/sqlite-shared`** is an internal, non-public package holding everything a SQLite-backed record adapter needs that isn't specific to one binding — schema DDL, `WHERE`/`ORDER` building, the cursor codec, row mappers, the FTS5 sanitizer and indexing strategy, the storage-ownership lock, and (via a small `SqlExecutor` interface normalizing a binding's call convention) the actual CRUD/query/version/type/association/token logic itself. An adapter implements only what's genuinely engine-specific: database construction, pragma setup, transaction semantics, and lifecycle. `record-adapter-sqlite` and `record-adapter-do-sqlite` both consume it; the split exists so a second (and third) SQLite engine inherits the behavior rather than reimplementing it, and so a cursor minted by one is decodable by another. -**It is bundled into its consumers rather than published.** "Non-public" is enforced, not merely intended: the package is `private`, and `record-adapter-sqlite` inlines it at build time, so a consumer installing that adapter from the registry never resolves `@haverstack/sqlite-shared` and cannot depend on it. `SqlExecutor` and the `Shared*Logic` classes are therefore internal collaborators of the adapters in this repository, not an extension point — a second SQLite engine inherits them by living here, not by installing them. Reversing that (publishing it so third-party adapters can build on `SqlExecutor`) is a deliberate decision to make it public API with the stability obligations that implies, not a packaging tweak. +**It exposes two entry points.** The full barrel (`.`) includes the token-store logic (`SharedTokenLogic`, `TOKENS_SCHEMA_SQL`) and the file-lock helpers, both Node-specific (`node:crypto`, `node:fs`) — fine for `record-adapter-sqlite`, but a bare `import` of either survives tree-shaking as a dead-but-still-imported module in a bundle, which throws at load time in a Workers runtime without `nodejs_compat`. A record-only consumer with no token store and no lock file — `record-adapter-do-sqlite`, where the platform's single-writer-per-id model already is the lock — imports the `./record` subpath instead, which never reaches either. -`SqlExecutor` is synchronous. Every SQLite binding in scope executes queries in-process without yielding, and the shared logic's explicit `BEGIN`/`COMMIT` sequences depend on that — an engine reached over a network (D1, libsql over HTTP) does not fit this interface without making it async throughout. +**It is bundled into its consumers rather than published.** "Non-public" is enforced, not merely intended: the package is `private`, and its consumers inline it at build time, so installing one of them from the registry never resolves `@haverstack/sqlite-shared` and cannot depend on it. `SqlExecutor` and the `Shared*Logic` classes are therefore internal collaborators of the adapters in this repository, not an extension point — a second SQLite engine inherits them by living here, not by installing them. Reversing that (publishing it so third-party adapters can build on `SqlExecutor`) is a deliberate decision to make it public API with the stability obligations that implies, not a packaging tweak. + +`SqlExecutor` is synchronous. Every SQLite binding in scope executes queries in-process without yielding, and the shared logic's explicit transaction boundaries (`SqlExecutor.transaction(fn)`) depend on that — an engine reached over a network (D1, libsql over HTTP) does not fit this interface without making it async throughout. `transaction(fn)` — not raw `BEGIN`/`COMMIT`/`ROLLBACK` strings — is the interface's transaction primitive specifically because it isn't universal SQL text: `record-adapter-sqlite` implements it as literal `BEGIN`/`COMMIT`/`ROLLBACK` around `fn()`, while `record-adapter-do-sqlite` implements it as `ctx.storage.transactionSync(fn)`, because Durable Object SQLite storage rejects raw multi-statement transaction SQL outright and does not auto-commit-then-roll-back on a later exception — verified against the real Workers runtime, not assumed. A binding that only had the three raw statements to work with couldn't reach that primitive at all. SQLite-backed adapters enable foreign-key enforcement (`PRAGMA foreign_keys = ON`) so that operations like `associate()` against a nonexistent record fail loudly (`StackNotFoundError`) instead of silently creating an orphan row. @@ -90,7 +93,7 @@ type AdapterCapabilities = { `AdapterCapabilities` is the adapter-implementer-facing name. On the `StackClient` interface it is exposed as `features: StackFeatures` (a type alias for `AdapterCapabilities`). App and plugin code should read `stack.features` rather than going through the adapter directly. -**`contentFieldQuery` is required-`true` for local adapters, optional and discovery-driven for wire adapters.** "Local" means an adapter that reads/writes its storage in-process, with no network hop to a server that could have its own opinion — `record-adapter-sqlite`, any future JSON-file adapter, and first-party test doubles standing in for one. For storage a local adapter already owns and reads directly, filtering by `content` is just a linear scan over resident data — there's no architectural reason a local adapter can't support it, so declaring `false` is never legitimate there. A remote server reached through `adapter-api` is the one legitimate `false` case: native fields (`typeId`, `parentId`, `entityId`, dates) are a fixed, indexable schema every server needs anyway, but `content` is an arbitrary, app-defined JSON blob, and a server serving many stacks may reasonably decline to index or full-scan it. `fullTextSearch` has no such local-required rule — a local adapter may legitimately decline it (see the JSON adapter note below). +**`contentFieldQuery` is required-`true` for local adapters, optional and discovery-driven for wire adapters.** "Local" means an adapter that reads/writes its storage in-process, with no network hop to a server that could have its own opinion — `record-adapter-sqlite`, `record-adapter-do-sqlite` (a Durable Object's storage is in-process from that DO's own point of view, even though the DO itself is reached over the network), any future JSON-file adapter, and first-party test doubles standing in for one. For storage a local adapter already owns and reads directly, filtering by `content` is just a linear scan over resident data — there's no architectural reason a local adapter can't support it, so declaring `false` is never legitimate there. A remote server reached through `adapter-api` is the one legitimate `false` case: native fields (`typeId`, `parentId`, `entityId`, dates) are a fixed, indexable schema every server needs anyway, but `content` is an arbitrary, app-defined JSON blob, and a server serving many stacks may reasonably decline to index or full-scan it. `fullTextSearch` has no such local-required rule — a local adapter may legitimately decline it (see the JSON adapter note below). `Stack.query()` enforces this before dispatching — see [Capability-gated filters](./data-model.md#capability-gated-filters). That check is a backstop for the rule above, not a substitute for it: a local adapter that (incorrectly) declared `false` would otherwise return an unfiltered superset for every `content` query. @@ -98,9 +101,10 @@ type AdapterCapabilities = { - **JSON adapter** — supports all filter fields via O(n) scan; may maintain `_index.json` to speed up native field lookups; `fullTextSearch: false` in v1 (local adapters may decline `fullTextSearch`; only `contentFieldQuery` is required-`true`) - **Native SQLite adapter** (`record-adapter-sqlite`) — indexes all native fields and association labels; supports content field queries and full-text search via FTS5 +- **Durable Object SQLite adapter** (`record-adapter-do-sqlite`) — same capabilities as the native SQLite adapter (shares `SharedSqlRecordLogic`); DO's SQLite storage ships FTS5 - **API adapter** — capabilities determined by the server; declared in a discovery endpoint; the one adapter kind allowed to declare `contentFieldQuery: false` -Local, embedded adapters (JSON, native SQLite) declare `maxAttachmentBytes: null` — nothing at the storage layer imposes a ceiling. Only a server behind the API adapter enforces one, since it's the only adapter transporting attachment bytes over a connection with its own limits. +Local, embedded adapters (JSON, native SQLite, Durable Object SQLite) declare `maxAttachmentBytes: null` — nothing at the storage layer imposes a ceiling. Only a server behind the API adapter enforces one, since it's the only adapter transporting attachment bytes over a connection with its own limits. **`maxContentBytes` is the same field for the JSON side of a write** — the serialized size of a Record's `content` on create, or of a merge patch on update. Local adapters declare `null` for the same reason: a caller with in-process access to the database can spend its own memory however it likes, and nothing at the storage layer objects. A server declares its request-size limit here, and `Stack.create()`/`Stack.update()` pre-check against it and throw `StackPayloadTooLargeError` before sending — the same client-side courtesy `putAttachment()` extends for attachments, with the server's own limit still authoritative (see [Wire format § Request size limits](./wire-format.md#request-size-limits)). @@ -111,6 +115,7 @@ A stack's backing storage (a SQLite file, a JSON directory) has exactly one owni How each adapter honors the single-writer rule differs by what it actually is: - **`record-adapter-sqlite`** (Node, real files) writes through `node:sqlite` under WAL journaling — page-level writes and crash safety are properties of the storage engine itself. It still acquires a PID-stamped lock file beside the database on `open()`/`initialize()`, released on `close()`, so a second opener gets a clear, immediate error rather than discovering the trust-boundary problem the hard way. A stale lock (owning process no longer alive) is reclaimed automatically, and an explicit override is available for the rare case of PID reuse. +- **`record-adapter-do-sqlite`** (Cloudflare Durable Objects, SQLite storage) needs no lock file at all — a Durable Object id maps to exactly one running instance, enforced by the platform itself, so the single-writer rule is a property of the runtime rather than something this adapter has to implement. There is likewise no `persist`/flush step: every write through `ctx.storage.sql` is durable by the time the call returns. The one real engine-specific wrinkle is transactions — DO SQLite rejects raw `BEGIN`/`COMMIT`/`ROLLBACK` outright, and (confirmed against the real runtime, not assumed) does not roll back a write on a later exception the way an open SQL transaction would; the adapter reaches `ctx.storage.transactionSync()` instead, through `SqlExecutor.transaction()` (see above). - **The planned whole-file `adapter-json`** reads its entire store into memory on open and rewrites it whole on every persist, so it must supply both guarantees itself: a PID lock file (to fail loudly on double-open) and an atomic temp-file-and-`rename()` persist (so a crash mid-write can't leave a torn, unreadable file). `record-adapter-sqlite` gets both from WAL and real file locking instead. ## Lifecycle diff --git a/packages/record-adapter-do-sqlite/package.json b/packages/record-adapter-do-sqlite/package.json new file mode 100644 index 0000000..cd5c127 --- /dev/null +++ b/packages/record-adapter-do-sqlite/package.json @@ -0,0 +1,56 @@ +{ + "name": "@haverstack/record-adapter-do-sqlite", + "version": "0.1.0", + "description": "Cloudflare Durable Objects (SQLite storage) StackRecordAdapter for Haverstack — Workers, FTS5, single-writer by construction", + "type": "module", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/haverstack/core.git", + "directory": "packages/record-adapter-do-sqlite" + }, + "license": "CC0-1.0", + "keywords": [ + "haverstack", + "sqlite", + "cloudflare", + "durable-objects", + "workers", + "record", + "adapter", + "personal data", + "storage" + ], + "scripts": { + "prepublishOnly": "pnpm run build", + "build": "tsup", + "pretest": "wrangler types", + "test": "vitest run", + "pretypecheck": "wrangler types", + "typecheck": "tsc --noEmit", + "lint": "eslint src tests" + }, + "dependencies": { + "@haverstack/core": "workspace:^" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "^0.22.0", + "@cloudflare/workers-types": "^5.0.0", + "@haverstack/sqlite-shared": "workspace:*", + "tsup": "^8.5.1", + "typescript": "^5.5.0", + "vite": "^7.0.0", + "vitest": "^4.1.0", + "wrangler": "^4.127.1" + } +} diff --git a/packages/record-adapter-do-sqlite/src/executor.ts b/packages/record-adapter-do-sqlite/src/executor.ts new file mode 100644 index 0000000..13e9ef4 --- /dev/null +++ b/packages/record-adapter-do-sqlite/src/executor.ts @@ -0,0 +1,49 @@ +import type { SqlExecutor } from '@haverstack/sqlite-shared/record'; + +/** + * Normalizes a Durable Object's ctx.storage.sql to SqlExecutor. + * + * Two things don't map onto node:sqlite's shape the way the rest of this + * interface does: + * + * - There is no get/all/run split — sql.exec() always returns a cursor. + * get()/all() read it via .toArray(); run() reads .rowsWritten for the + * affected-row count run()'s contract promises. + * - Raw BEGIN/COMMIT/ROLLBACK are rejected outright by DO SQLite (verified + * against the real Workers runtime, not assumed) — its storage has no + * notion of a transaction left open across separate exec() calls, and + * each statement auto-commits the instant it runs, so no-op'ing those + * three strings would silently break atomicity, not preserve it. The + * platform's real primitive is ctx.storage.transactionSync(fn), which + * SqlExecutor.transaction() reaches directly — see docs/spec/adapters.md + * § Concurrency & storage ownership. + */ +export class DurableObjectSqliteExecutor implements SqlExecutor { + constructor(private readonly storage: DurableObjectStorage) {} + + private get sql(): SqlStorage { + return this.storage.sql; + } + + exec(sql: string): void { + this.sql.exec(sql); + } + + run(sql: string, params: readonly unknown[] = []): number { + const cursor = this.sql.exec(sql, ...(params as SqlStorageValue[])); + return cursor.rowsWritten; + } + + get>(sql: string, params: readonly unknown[] = []): T | undefined { + const rows = this.sql.exec(sql, ...(params as SqlStorageValue[])).toArray(); + return rows[0] as T | undefined; + } + + all>(sql: string, params: readonly unknown[] = []): T[] { + return this.sql.exec(sql, ...(params as SqlStorageValue[])).toArray() as T[]; + } + + transaction(fn: () => T): T { + return this.storage.transactionSync(fn); + } +} diff --git a/packages/record-adapter-do-sqlite/src/index.ts b/packages/record-adapter-do-sqlite/src/index.ts new file mode 100644 index 0000000..e58eb5b --- /dev/null +++ b/packages/record-adapter-do-sqlite/src/index.ts @@ -0,0 +1,255 @@ +/** + * Haverstack — Durable Object SQLite Record Adapter + * ------------------------------------------------------- + * Implements StackRecordAdapter over a Cloudflare Durable Object's SQLite + * storage (ctx.storage.sql). Full-text search uses FTS5, same as + * record-adapter-sqlite — DO's SQLite build ships it. + * + * Ownership and durability come from the platform, not from anything this + * class does: a Durable Object id maps to exactly one running instance, + * so there is no separate lock file the way record-adapter-sqlite needs + * one for real files (see docs/spec/adapters.md § Concurrency & storage + * ownership) — the DO *is* the lock. There is likewise no persist/flush + * step: every write through ctx.storage.sql is durable by the time the + * call returns, so flush()/close() are no-ops kept only to satisfy the + * optional StackRecordAdapter methods. + * + * This class itself is a thin binding: schema setup and the one piece of + * genuinely engine-specific wiring — SqlExecutor.transaction() reaching + * ctx.storage.transactionSync() instead of raw SQL BEGIN/COMMIT/ROLLBACK, + * which DO SQLite rejects outright — live here (see executor.ts). The + * actual StackRecordAdapter logic lives in @haverstack/sqlite-shared's + * SharedSqlRecordLogic, exactly as it does for record-adapter-sqlite. + */ + +import type { StackType, TypeId, FileId, RecordVersion, ActorOptions } from '@haverstack/core'; +import type { + StackRecord, + StackQuery, + QueryResult, + Association, + Permission, +} from '@haverstack/core'; +import type { StackRecordAdapter, AdapterCapabilities } from '@haverstack/core/adapter'; +import { + RECORD_SCHEMA_SQL, + FTS5_SCHEMA_SQL, + PRAGMA_FOREIGN_KEYS_ON, + insertConfigRecord, + readStackConfig, + SharedSqlRecordLogic, +} from '@haverstack/sqlite-shared/record'; +import { DurableObjectSqliteExecutor } from './executor.js'; + +// ------------------------------------------------------- +// Types +// ------------------------------------------------------- + +export type DoRecordCreateOptions = { + /** Entity ID of the stack owner. Ignored if the DO's storage already has a config record. */ + entityId: string; + /** IANA timezone string e.g. "America/New_York". Optional passthrough app metadata — no default. */ + timezone?: string; +}; + +// ------------------------------------------------------- +// DoSQLiteRecordAdapter +// ------------------------------------------------------- + +export class DoSQLiteRecordAdapter implements StackRecordAdapter { + readonly capabilities: AdapterCapabilities = { + fullTextSearch: true, + contentFieldQuery: true, + sortableFields: ['createdAt', 'updatedAt', 'version'], + maxAttachmentBytes: null, + maxContentBytes: null, + }; + + ownerEntityId!: string; + timezone: string | undefined; + + private readonly record: SharedSqlRecordLogic; + + private constructor(private readonly exec: DurableObjectSqliteExecutor) { + this.record = new SharedSqlRecordLogic({ exec }); + } + + /** + * Create (or reattach to) the adapter for a DO instance. There is no + * initialize()/open() split the way file-based adapters need one: a DO + * id either already has a config record (a previous call created it — + * reattach, opts.entityId/timezone ignored in favor of what's stored) + * or it doesn't (first call — opts.entityId/timezone become the config). + * Schema DDL is `CREATE TABLE IF NOT EXISTS`, so running it every call + * is idempotent and cheap. + */ + static async create( + storage: DurableObjectStorage, + opts: DoRecordCreateOptions, + ): Promise { + const exec = new DurableObjectSqliteExecutor(storage); + exec.exec(RECORD_SCHEMA_SQL); + exec.exec(FTS5_SCHEMA_SQL); + exec.exec(PRAGMA_FOREIGN_KEYS_ON); + // DO SQLite manages its own durability and rejects PRAGMA journal_mode + // outright ("not authorized") — verified against the real runtime, not + // assumed — so unlike record-adapter-sqlite, no WAL pragma runs here. + + const adapter = new DoSQLiteRecordAdapter(exec); + const existing = exec.get<{ content: string }>( + `SELECT content FROM records WHERE id = '_config'`, + ); + if (existing) { + const config = readStackConfig(exec); + adapter.ownerEntityId = config.entityId; + adapter.timezone = config.timezone; + } else { + insertConfigRecord(exec, opts.entityId, opts.timezone); + adapter.ownerEntityId = opts.entityId; + adapter.timezone = opts.timezone; + } + return adapter; + } + + // ------------------------------------------------------- + // Records + // ------------------------------------------------------- + + createRecord(record: StackRecord): Promise { + return this.record.createRecord(record); + } + + getRecord(id: string): Promise { + return this.record.getRecord(id); + } + + patchContent( + id: string, + patch: Record, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.patchContent(id, patch, opts); + } + + deleteRecord( + id: string, + opts?: { hard?: boolean; expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.deleteRecord(id, opts); + } + + undeleteRecord( + id: string, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.undeleteRecord(id, opts); + } + + setPermissions( + id: string, + permissions: Permission[], + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.setPermissions(id, permissions, opts); + } + + setUnlisted( + id: string, + unlisted: boolean, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.setUnlisted(id, unlisted, opts); + } + + restoreVersion( + id: string, + version: number, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.restoreVersion(id, version, opts); + } + + commitMigration( + id: string, + toTypeId: TypeId, + content: Record, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.commitMigration(id, toTypeId, content, opts); + } + + queryRecords(query: StackQuery): Promise { + return this.record.queryRecords(query); + } + + deleteUnreferencedAttachmentRecords( + fileId: FileId, + metadataTypeId: TypeId, + ): Promise { + return this.record.deleteUnreferencedAttachmentRecords(fileId, metadataTypeId); + } + + // ------------------------------------------------------- + // Versions + // ------------------------------------------------------- + + getVersions(id: string): Promise { + return this.record.getVersions(id); + } + + getVersion(id: string, version: number): Promise { + return this.record.getVersion(id, version); + } + + saveVersion(id: string, version: RecordVersion): Promise { + return this.record.saveVersion(id, version); + } + + // ------------------------------------------------------- + // Types + // ------------------------------------------------------- + + saveType(type: StackType): Promise { + return this.record.saveType(type); + } + + getType(id: TypeId): Promise { + return this.record.getType(id); + } + + listTypes(): Promise { + return this.record.listTypes(); + } + + // ------------------------------------------------------- + // Associations + // ------------------------------------------------------- + + associate( + recordId: string, + association: Association, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.associate(recordId, association, opts); + } + + dissociate( + recordId: string, + association: Association, + opts?: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions, + ): Promise { + return this.record.dissociate(recordId, association, opts); + } + + // ------------------------------------------------------- + // Lifecycle + // ------------------------------------------------------- + + /** No-op: every write through ctx.storage.sql is already durable. */ + async flush(): Promise {} + + /** No-op: no lock file, no connection to release — the DO's own lifecycle governs storage. */ + async close(): Promise {} +} + +export { DurableObjectSqliteExecutor } from './executor.js'; diff --git a/packages/record-adapter-do-sqlite/tests/record.test.ts b/packages/record-adapter-do-sqlite/tests/record.test.ts new file mode 100644 index 0000000..5b7f2ce --- /dev/null +++ b/packages/record-adapter-do-sqlite/tests/record.test.ts @@ -0,0 +1,299 @@ +/** + * Targeted subset, not a 1:1 port of record-adapter-sqlite's suite — that + * suite already proves SharedSqlRecordLogic's correctness once. What's + * unique to this adapter and worth proving again, against the real + * Workers runtime (@cloudflare/vitest-pool-workers), not `environment: + * 'node'`: the executor's translation of get/all/run onto SqlStorage's + * cursor API, FK/unique constraint mapping, FTS5, cursor-codec pagination, + * and — the one thing the #161 spike found couldn't be assumed — that + * exec.transaction() reaching ctx.storage.transactionSync() actually + * rolls back a rejected mutation's partial writes (see the FTS-consistency + * test below), since DO SQLite has no raw BEGIN/COMMIT/ROLLBACK to fall + * back on if that wiring were wrong. + */ +import { env } from 'cloudflare:test'; +import { describe, test, expect } from 'vitest'; +import type { StackRecord, StackQuery, QueryResult, Association } from '@haverstack/core'; +import type { AdapterCapabilities } from '@haverstack/core/adapter'; + +/** + * A Durable Object is a separate JS realm from the test file's own — even + * colocated, an RPC call across that boundary reconstructs a thrown Error + * as a generic object carrying the same enumerable properties (message, + * name, code, and any custom fields like recordId), but NOT the original + * class's prototype chain. `instanceof StackConflictError` fails on the + * far side of that boundary even though the error is genuinely a + * StackConflictError inside the DO; `.code` (StackError's discriminant, + * see packages/core/src/stack.ts) is what survives and what these tests + * assert on instead. record-adapter-sqlite's tests never hit this because + * everything there runs in one process. + */ + +/** + * Cloudflare's automatic RPC type inference for a DurableObjectStub + * collapses to `never` for several of TestRecordAdapterDO's methods — + * StackRecord/QueryResult/Association are ordinary data types and the + * runtime call works correctly (see the assertions below), but the + * recursive type transformation the RPC types apply to a class with this + * many methods and this much optional/union structure in their signatures + * doesn't resolve. Declaring the stub's shape explicitly sidesteps that + * inference rather than fighting it. + */ +type TestStub = { + getCapabilities(): Promise; + getOwnerEntityId(): Promise; + createRecord(record: StackRecord): Promise; + getRecord(id: string): Promise; + patchContent( + id: string, + patch: Record, + opts?: Record, + ): Promise; + deleteRecord( + id: string, + opts?: { hard?: boolean } & Record, + ): Promise; + queryRecords(query: StackQuery): Promise; + associate( + recordId: string, + association: Association, + opts?: Record, + ): Promise; + dissociate( + recordId: string, + association: Association, + opts?: Record, + ): Promise; + commitMigration( + id: string, + toTypeId: string, + content: Record, + opts?: Record, + ): Promise; +}; + +const getStub = (): TestStub => { + const id = env.TEST_DO.idFromName(`do-${Math.random().toString(36).slice(2)}`); + return env.TEST_DO.get(id) as unknown as TestStub; +}; + +const NOTE_TYPE_V1 = 'com.example.test/note@1'; + +const makeRecord = (overrides: Partial = {}): StackRecord => ({ + id: `rec-${Math.random().toString(36).slice(2)}`, + typeId: NOTE_TYPE_V1, + createdAt: new Date(), + updatedAt: new Date(), + content: { text: 'Hello world' }, + version: 1, + ...overrides, +}); + +describe('construction', () => { + test('declares capabilities matching record-adapter-sqlite', async () => { + const stub = getStub(); + const capabilities = await stub.getCapabilities(); + expect(capabilities).toEqual({ + fullTextSearch: true, + contentFieldQuery: true, + sortableFields: ['createdAt', 'updatedAt', 'version'], + maxAttachmentBytes: null, + maxContentBytes: null, + }); + }); + + test('sets ownerEntityId from create() options', async () => { + const stub = getStub(); + expect(await stub.getOwnerEntityId()).toBe('entity-test'); + }); +}); + +describe('records — CRUD', () => { + test('createRecord and getRecord roundtrip, with Date fields intact across the RPC boundary', async () => { + const stub = getStub(); + const record = makeRecord({ content: { text: 'Hello' } }); + await stub.createRecord(record); + const retrieved = await stub.getRecord(record.id); + expect(retrieved?.id).toBe(record.id); + expect(retrieved?.content).toEqual({ text: 'Hello' }); + expect(retrieved?.createdAt).toBeInstanceOf(Date); + expect(retrieved?.updatedAt).toBeInstanceOf(Date); + }); + + test('getRecord returns null for unknown id', async () => { + const stub = getStub(); + expect(await stub.getRecord('nonexistent')).toBeNull(); + }); + + test('createRecord throws StackConflictError on a duplicate id (unique constraint mapping)', async () => { + const stub = getStub(); + const record = makeRecord(); + await stub.createRecord(record); + const err = await stub + .createRecord({ ...record, content: { text: 'second' } }) + .catch((e: unknown) => e); + expect((err as { code?: string }).code).toBe('conflict'); + }); + + test('patchContent changes content and bumps version', async () => { + const stub = getStub(); + const record = makeRecord(); + await stub.createRecord(record); + const updated = await stub.patchContent(record.id, { text: 'Updated' }); + expect(updated.content).toEqual({ text: 'Updated' }); + expect(updated.version).toBe(2); + }); + + test('hard deleteRecord removes the record entirely', async () => { + const stub = getStub(); + const record = makeRecord(); + await stub.createRecord(record); + await stub.deleteRecord(record.id, { hard: true }); + expect(await stub.getRecord(record.id)).toBeNull(); + }); +}); + +describe('expectedVersion / transactional rollback', () => { + test('patchContent throws StackVersionConflictError and changes nothing when stale', async () => { + const stub = getStub(); + const record = await stub.createRecord(makeRecord()); + await stub.patchContent(record.id, { text: 'first' }); // -> v2 + + const err = await stub + .patchContent(record.id, { text: 'second' }, { expectedVersion: 1 }) + .catch((e: unknown) => e); + expect( + ( + err as { + code?: string; + recordId?: string; + expectedVersion?: number; + actualVersion?: number; + } + ).code, + ).toBe('version_conflict'); + expect((err as { recordId?: string }).recordId).toBe(record.id); + expect((err as { expectedVersion?: number }).expectedVersion).toBe(1); + expect((err as { actualVersion?: number }).actualVersion).toBe(2); + + const current = await stub.getRecord(record.id); + expect(current?.version).toBe(2); + expect(current?.content).toEqual({ text: 'first' }); + }); + + /** + * This is the load-bearing test for #161's central finding: patchContent + * removes the old FTS entry, then re-inserts it, inside one + * exec.transaction() block. If ctx.storage.transactionSync() did not + * actually roll back on throw — or if the executor had instead tried + * no-op'ing BEGIN/COMMIT/ROLLBACK, which the #161 spike found does NOT + * roll back on DO SQLite — a rejected patch here would leave the FTS + * index missing the original entry: searchable content would vanish + * even though the record's own content never changed. + */ + test('a rejected patchContent leaves the FTS index consistent with stored content', async () => { + const stub = getStub(); + const record = await stub.createRecord( + makeRecord({ content: { text: 'searchable original' } }), + ); + await stub + .patchContent(record.id, { text: 'rejected update' }, { expectedVersion: 999 }) + .catch(() => {}); + + const stillFindsOriginal = await stub.queryRecords({ filter: { search: 'original' } }); + expect(stillFindsOriginal.records.map((r) => r.id)).toEqual([record.id]); + const doesNotFindRejected = await stub.queryRecords({ filter: { search: 'rejected' } }); + expect(doesNotFindRejected.records).toEqual([]); + }); +}); + +describe('records — queries', () => { + test('filters by content field', async () => { + const stub = getStub(); + await stub.createRecord(makeRecord({ id: 'r1', content: { text: 'alpha', priority: 1 } })); + await stub.createRecord(makeRecord({ id: 'r2', content: { text: 'beta', priority: 2 } })); + const result = await stub.queryRecords({ filter: { content: { priority: 1 } } }); + expect(result.records.map((r) => r.id)).toEqual(['r1']); + }); + + test('full-text search (FTS5)', async () => { + const stub = getStub(); + await stub.createRecord(makeRecord({ id: 'r1', content: { text: 'SQLite is great' } })); + await stub.createRecord(makeRecord({ id: 'r2', content: { text: 'Postgres is also great' } })); + const result = await stub.queryRecords({ filter: { search: 'SQLite' } }); + expect(result.records.map((r) => r.id)).toEqual(['r1']); + }); + + test('cursor pagination returns correct pages', async () => { + const stub = getStub(); + for (let i = 0; i < 5; i++) { + await stub.createRecord( + makeRecord({ id: `r${i}`, createdAt: new Date(Date.now() + i * 1000) }), + ); + } + const page1 = await stub.queryRecords({ + sort: { field: 'createdAt', direction: 'asc' }, + limit: 3, + }); + expect(page1.records.length).toBe(3); + expect(page1.cursor).not.toBeNull(); + expect(page1.total).toBe(5); + + const page2 = await stub.queryRecords({ + sort: { field: 'createdAt', direction: 'asc' }, + limit: 3, + cursor: page1.cursor!, + }); + expect(page2.records.length).toBe(2); + expect(page2.cursor).toBeNull(); + }); + + test('malformed cursor throws StackQueryError', async () => { + const stub = getStub(); + await stub.createRecord(makeRecord({ id: 'r1' })); + const err = await stub.queryRecords({ cursor: '!!!not-a-cursor!!!' }).catch((e: unknown) => e); + expect((err as { code?: string }).code).toBe('bad_request'); + }); +}); + +describe('associations', () => { + test('associate adds a tag, dissociate removes it, both bump version', async () => { + const stub = getStub(); + const record = makeRecord(); + await stub.createRecord(record); + await stub.associate(record.id, { kind: 'tag', label: 'starred' }); + const withTag = await stub.getRecord(record.id); + expect(withTag?.associations?.some((a) => a.kind === 'tag' && a.label === 'starred')).toBe( + true, + ); + expect(withTag?.version).toBe(2); + + await stub.dissociate(record.id, { kind: 'tag', label: 'starred' }); + const withoutTag = await stub.getRecord(record.id); + expect(withoutTag?.associations).toBeUndefined(); + }); + + test('associate on a nonexistent record throws StackNotFoundError (FK constraint mapping) instead of creating an orphan row', async () => { + const stub = getStub(); + const err = await stub + .associate('nonexistent', { kind: 'tag', label: 'starred' }) + .catch((e: unknown) => e); + expect((err as { code?: string }).code).toBe('not_found'); + }); +}); + +describe('commitMigration', () => { + test('changes typeId and content together, and bumps version', async () => { + const stub = getStub(); + const record = makeRecord({ typeId: NOTE_TYPE_V1 }); + await stub.createRecord(record); + + const migrated = await stub.commitMigration(record.id, 'com.example.test/note@2', { + text: 'Hello world', + pinned: false, + }); + expect(migrated.typeId).toBe('com.example.test/note@2'); + expect(migrated.content).toEqual({ text: 'Hello world', pinned: false }); + expect(migrated.version).toBe(2); + }); +}); diff --git a/packages/record-adapter-do-sqlite/tests/support/env.d.ts b/packages/record-adapter-do-sqlite/tests/support/env.d.ts new file mode 100644 index 0000000..f2d39c1 --- /dev/null +++ b/packages/record-adapter-do-sqlite/tests/support/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/record-adapter-do-sqlite/tests/support/test-worker.ts b/packages/record-adapter-do-sqlite/tests/support/test-worker.ts new file mode 100644 index 0000000..50cda24 --- /dev/null +++ b/packages/record-adapter-do-sqlite/tests/support/test-worker.ts @@ -0,0 +1,154 @@ +/** + * Wraps DoSQLiteRecordAdapter in a real DurableObject subclass, the way a + * consuming Worker would — this IS the reference shape for that wiring, + * not test-only scaffolding around it. Every method is a thin RPC + * pass-through: Workers RPC serializes plain data (including Date, via + * structured clone) across the stub boundary, but not class instances, so + * each StackRecordAdapter method needs its own exposed method here. + */ +import { DurableObject } from 'cloudflare:workers'; +import { DoSQLiteRecordAdapter } from '../../src/index.js'; +import type { + StackRecord, + StackQuery, + Association, + Permission, + RecordVersion, + StackType, + TypeId, + FileId, + ActorOptions, +} from '@haverstack/core'; + +type MutationOpts = { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions; + +export class TestRecordAdapterDO extends DurableObject { + private adapter!: DoSQLiteRecordAdapter; + private readonly ready: Promise; + + constructor(ctx: DurableObjectState, env: Env) { + super(ctx, env); + this.ready = ctx.blockConcurrencyWhile(async () => { + this.adapter = await DoSQLiteRecordAdapter.create(ctx.storage, { + entityId: 'entity-test', + timezone: 'America/New_York', + }); + }); + } + + async getCapabilities() { + await this.ready; + return this.adapter.capabilities; + } + + async getOwnerEntityId() { + await this.ready; + return this.adapter.ownerEntityId; + } + + async createRecord(record: StackRecord) { + await this.ready; + return this.adapter.createRecord(record); + } + + async getRecord(id: string) { + await this.ready; + return this.adapter.getRecord(id); + } + + async patchContent(id: string, patch: Record, opts?: MutationOpts) { + await this.ready; + return this.adapter.patchContent(id, patch, opts); + } + + async deleteRecord(id: string, opts?: { hard?: boolean } & MutationOpts) { + await this.ready; + return this.adapter.deleteRecord(id, opts); + } + + async undeleteRecord(id: string, opts?: MutationOpts) { + await this.ready; + return this.adapter.undeleteRecord(id, opts); + } + + async setPermissions(id: string, permissions: Permission[], opts?: MutationOpts) { + await this.ready; + return this.adapter.setPermissions(id, permissions, opts); + } + + async setUnlisted(id: string, unlisted: boolean, opts?: MutationOpts) { + await this.ready; + return this.adapter.setUnlisted(id, unlisted, opts); + } + + async restoreVersion(id: string, version: number, opts?: MutationOpts) { + await this.ready; + return this.adapter.restoreVersion(id, version, opts); + } + + async commitMigration( + id: string, + toTypeId: TypeId, + content: Record, + opts?: MutationOpts, + ) { + await this.ready; + return this.adapter.commitMigration(id, toTypeId, content, opts); + } + + async queryRecords(query: StackQuery) { + await this.ready; + return this.adapter.queryRecords(query); + } + + async deleteUnreferencedAttachmentRecords(fileId: FileId, metadataTypeId: TypeId) { + await this.ready; + return this.adapter.deleteUnreferencedAttachmentRecords(fileId, metadataTypeId); + } + + async getVersions(id: string) { + await this.ready; + return this.adapter.getVersions(id); + } + + async getVersion(id: string, version: number) { + await this.ready; + return this.adapter.getVersion(id, version); + } + + async saveVersion(id: string, version: RecordVersion) { + await this.ready; + return this.adapter.saveVersion(id, version); + } + + async saveType(type: StackType) { + await this.ready; + return this.adapter.saveType(type); + } + + async getType(id: TypeId) { + await this.ready; + return this.adapter.getType(id); + } + + async listTypes() { + await this.ready; + return this.adapter.listTypes(); + } + + async associate(recordId: string, association: Association, opts?: MutationOpts) { + await this.ready; + return this.adapter.associate(recordId, association, opts); + } + + async dissociate(recordId: string, association: Association, opts?: MutationOpts) { + await this.ready; + return this.adapter.dissociate(recordId, association, opts); + } +} + +export default { + async fetch(): Promise { + return new Response('test worker: use RPC stub methods'); + }, +}; diff --git a/packages/record-adapter-do-sqlite/tsconfig.json b/packages/record-adapter-do-sqlite/tsconfig.json new file mode 100644 index 0000000..0ca8b2b --- /dev/null +++ b/packages/record-adapter-do-sqlite/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "noEmit": true, + "lib": ["ES2022"], + "types": [], + "skipLibCheck": true + }, + "include": ["src/**/*.ts", "tests/**/*.ts", "worker-configuration.d.ts"] +} diff --git a/packages/record-adapter-do-sqlite/tsup.config.ts b/packages/record-adapter-do-sqlite/tsup.config.ts new file mode 100644 index 0000000..dbf46dc --- /dev/null +++ b/packages/record-adapter-do-sqlite/tsup.config.ts @@ -0,0 +1,38 @@ +import { defineConfig } from 'tsup'; + +/** + * Mirrors record-adapter-sqlite's tsup config: @haverstack/sqlite-shared is + * internal (private, no stability promise, not published) and gets bundled + * into this package's output rather than resolved from the registry. + * @haverstack/core stays external — a real published peer, and inlining it + * would give this package its own private copy of the error classes, + * breaking `instanceof` against the caller's. + * + * target: 'es2022' rather than a Node target — this ships into a Workers + * bundle (via wrangler/esbuild in the consuming app), not run standalone + * under node. + * + * dts.compilerOptions.types is scoped to *this* isolated dts compilation + * only, not the package's shared tsconfig.json (which drives `tsc + * --noEmit` over src/** and tests/** together). src/executor.ts uses the + * ambient DurableObjectStorage/SqlStorage/SqlStorageValue globals with no + * import, so this build step — which follows the entry's module graph, + * not tsconfig's "include" — needs @cloudflare/workers-types to resolve + * them; the test tsconfig gets the same globals for free from wrangler's + * generated worker-configuration.d.ts, and adding workers-types there too + * conflicts with it over the ambient Env/Cloudflare.Env declaration (the + * exact clash wrangler's own "uninstall @cloudflare/workers-types" + * migration note warns about) without ever being needed. + */ +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + target: 'es2022', + dts: { compilerOptions: { types: ['@cloudflare/workers-types'] } }, + sourcemap: true, + clean: true, + splitting: false, + treeshake: true, + external: ['@haverstack/core', '@haverstack/core/adapter'], + noExternal: ['@haverstack/sqlite-shared'], +}); diff --git a/packages/record-adapter-do-sqlite/vitest.config.ts b/packages/record-adapter-do-sqlite/vitest.config.ts new file mode 100644 index 0000000..3d154aa --- /dev/null +++ b/packages/record-adapter-do-sqlite/vitest.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'vitest/config'; +import { cloudflareTest } from '@cloudflare/vitest-pool-workers'; +import { resolve } from 'path'; + +export default defineConfig({ + resolve: { + alias: { + '@haverstack/core/wire': resolve(__dirname, '../core/src/wire-entry.ts'), + '@haverstack/core/adapter': resolve(__dirname, '../core/src/adapter-entry.ts'), + '@haverstack/core': resolve(__dirname, '../core/src/index.ts'), + '@haverstack/sqlite-shared/record': resolve(__dirname, '../sqlite-shared/src/record.ts'), + }, + }, + plugins: [cloudflareTest({ wrangler: { configPath: './wrangler.jsonc' } })], +}); diff --git a/packages/record-adapter-do-sqlite/wrangler.jsonc b/packages/record-adapter-do-sqlite/wrangler.jsonc new file mode 100644 index 0000000..775d614 --- /dev/null +++ b/packages/record-adapter-do-sqlite/wrangler.jsonc @@ -0,0 +1,20 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "record-adapter-do-sqlite-tests", + "main": "tests/support/test-worker.ts", + "compatibility_date": "2026-08-01", + "durable_objects": { + "bindings": [ + { + "name": "TEST_DO", + "class_name": "TestRecordAdapterDO", + }, + ], + }, + "migrations": [ + { + "tag": "v1", + "new_sqlite_classes": ["TestRecordAdapterDO"], + }, + ], +} diff --git a/packages/record-adapter-sqlite/src/executor.ts b/packages/record-adapter-sqlite/src/executor.ts index 709b2af..035f057 100644 --- a/packages/record-adapter-sqlite/src/executor.ts +++ b/packages/record-adapter-sqlite/src/executor.ts @@ -21,4 +21,16 @@ export class NativeSqliteExecutor implements SqlExecutor { all>(sql: string, params: readonly unknown[] = []): T[] { return this.db.prepare(sql).all(...(params as (string | number | null)[])) as T[]; } + + transaction(fn: () => T): T { + this.db.exec('BEGIN'); + try { + const result = fn(); + this.db.exec('COMMIT'); + return result; + } catch (err) { + this.db.exec('ROLLBACK'); + throw err; + } + } } diff --git a/packages/sqlite-shared/package.json b/packages/sqlite-shared/package.json index 37659ff..b33756e 100644 --- a/packages/sqlite-shared/package.json +++ b/packages/sqlite-shared/package.json @@ -7,6 +7,10 @@ ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" + }, + "./record": { + "import": "./dist/record.js", + "types": "./dist/record.d.ts" } }, "main": "./dist/index.js", diff --git a/packages/sqlite-shared/src/executor.ts b/packages/sqlite-shared/src/executor.ts index 4d22d71..3e20f39 100644 --- a/packages/sqlite-shared/src/executor.ts +++ b/packages/sqlite-shared/src/executor.ts @@ -19,6 +19,17 @@ export interface SqlExecutor { get>(sql: string, params?: readonly unknown[]): T | undefined; /** Run a parameterized statement and return all result rows. */ all>(sql: string, params?: readonly unknown[]): T[]; + /** + * Run `fn` atomically: every statement it issues commits together, or + * none do if it throws. `fn` must be synchronous and call back into + * this executor only — not every engine's transaction primitive can + * straddle a suspended call. A binding with a real multi-statement + * transaction (BEGIN/COMMIT/ROLLBACK) implements this with one; a + * binding whose transaction primitive is itself a callback wrapper + * (e.g. a Durable Object's storage.transactionSync) can pass `fn` + * straight through to it. + */ + transaction(fn: () => T): T; } /** SQLite reports FK violations with this exact message; verify it holds when adding an engine. */ diff --git a/packages/sqlite-shared/src/record-logic.ts b/packages/sqlite-shared/src/record-logic.ts index c761975..4d97286 100644 --- a/packages/sqlite-shared/src/record-logic.ts +++ b/packages/sqlite-shared/src/record-logic.ts @@ -165,12 +165,13 @@ export class SharedSqlRecordLogic { } /** - * The synchronous read behind getRecord(). Callers inside a transaction - * use this one: an `await` between BEGIN and COMMIT yields the microtask - * queue mid-transaction, and the next operation to run would try to open - * one of its own. + * The synchronous read behind getRecord(). Callers inside exec.transaction() + * use this one: an `await` inside that callback yields the microtask queue + * mid-transaction, and the next operation to run would try to open one of + * its own — transaction() requires a synchronous callback for exactly this + * reason (see SqlExecutor.transaction). * - * The same synchrony is what lets the post-COMMIT reads below report the + * The same synchrony is what lets the post-commit reads below report the * version their own mutation produced: `getRecord()` runs its body before * the `await` yields, so nothing interleaves between the commit and the * read. A backend that made these reads genuinely asynchronous would open @@ -194,8 +195,7 @@ export class SharedSqlRecordLogic { this.checkExpectedVersion(existing, opts.expectedVersion); const merged = applyMergePatch(existing.content, patch); - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); fts5Strategy.remove(this.exec, id); this.exec.run( @@ -210,11 +210,7 @@ export class SharedSqlRecordLogic { ); fts5Strategy.insert(this.exec, id, JSON.stringify(merged)); this.syncFileRefs(id, existing.typeId, merged); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after patchContent: "${id}"`); @@ -230,18 +226,9 @@ export class SharedSqlRecordLogic { } & ActorOptions = {}, ): Promise { if (opts.hard) { - this.exec.exec('BEGIN'); - try { - const purged = this.hardDeleteRecord(id, opts.expectedVersion); - this.exec.exec('COMMIT'); - return purged; - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + return this.exec.transaction(() => this.hardDeleteRecord(id, opts.expectedVersion)); } else { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); const { clause, params: verParams } = this.versionGuard(opts.expectedVersion); const changed = this.exec.run( @@ -256,11 +243,7 @@ export class SharedSqlRecordLogic { ], ); if (changed === 0) this.throwVersionConflict(id, opts.expectedVersion); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); } const updated = await this.getRecord(id); @@ -303,8 +286,7 @@ export class SharedSqlRecordLogic { id: string, opts: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions = {}, ): Promise { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); const { clause, params: verParams } = this.versionGuard(opts.expectedVersion); const changed = this.exec.run( @@ -312,11 +294,7 @@ export class SharedSqlRecordLogic { [toMs(new Date()), opts.updatedBy ?? null, opts.updatedVia ?? null, id, ...verParams], ); if (changed === 0) this.throwVersionConflict(id, opts.expectedVersion); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after undelete: "${id}"`); @@ -328,8 +306,7 @@ export class SharedSqlRecordLogic { permissions: Permission[], opts: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions = {}, ): Promise { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); const { clause, params: verParams } = this.versionGuard(opts.expectedVersion); const changed = this.exec.run( @@ -344,11 +321,7 @@ export class SharedSqlRecordLogic { ], ); if (changed === 0) this.throwVersionConflict(id, opts.expectedVersion); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after setPermissions: "${id}"`); @@ -360,8 +333,7 @@ export class SharedSqlRecordLogic { unlisted: boolean, opts: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions = {}, ): Promise { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); const { clause, params: verParams } = this.versionGuard(opts.expectedVersion); const now = toMs(new Date()); @@ -377,11 +349,7 @@ export class SharedSqlRecordLogic { ], ); if (changed === 0) this.throwVersionConflict(id, opts.expectedVersion); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after setUnlisted: "${id}"`); @@ -400,8 +368,7 @@ export class SharedSqlRecordLogic { const target = await this.getVersion(id, version); if (!target) throw new Error(`Version not found: ${id}@${version}`); - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); fts5Strategy.remove(this.exec, id); this.exec.run( @@ -421,11 +388,7 @@ export class SharedSqlRecordLogic { } fts5Strategy.insert(this.exec, id, JSON.stringify(target.content)); this.syncFileRefs(id, target.typeId, target.content); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after restoreVersion: "${id}"`); @@ -446,8 +409,7 @@ export class SharedSqlRecordLogic { if (!existing) throw new Error(`Record not found: "${id}"`); this.checkExpectedVersion(existing, opts.expectedVersion); - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(id, opts.snapshot); fts5Strategy.remove(this.exec, id); this.exec.run( @@ -463,11 +425,7 @@ export class SharedSqlRecordLogic { ); fts5Strategy.insert(this.exec, id, JSON.stringify(content)); this.syncFileRefs(id, toTypeId, content); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(id); if (!updated) throw new Error(`Record not found after commitMigration: "${id}"`); @@ -515,8 +473,7 @@ export class SharedSqlRecordLogic { fileId: FileId, metadataTypeId: TypeId, ): Promise { - this.exec.exec('BEGIN'); - try { + return this.exec.transaction(() => { const referenced = this.exec.all<{ found: number }>( `SELECT 1 as found FROM associations WHERE kind = 'attachment' AND file_id = ? UNION ALL @@ -538,12 +495,8 @@ export class SharedSqlRecordLogic { if (purged) deleted.push(purged); } - this.exec.exec('COMMIT'); return deleted; - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); } // ------------------------------------------------------- @@ -699,18 +652,13 @@ export class SharedSqlRecordLogic { association: Association, opts: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions = {}, ): Promise { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(recordId, opts.snapshot); // Bump (and CAS-check) first, before the associations-table write, so // a lost race never partially applies. this.bumpVersion(recordId, opts); this.insertAssociations(recordId, [association]); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(recordId); if (!updated) throw new Error(`Record not found after associate: "${recordId}"`); @@ -722,8 +670,7 @@ export class SharedSqlRecordLogic { association: Association, opts: { expectedVersion?: number; snapshot?: RecordVersion } & ActorOptions = {}, ): Promise { - this.exec.exec('BEGIN'); - try { + this.exec.transaction(() => { if (opts.snapshot) this.snapshotBeforeMutation(recordId, opts.snapshot); this.bumpVersion(recordId, opts); this.exec.run( @@ -738,11 +685,7 @@ export class SharedSqlRecordLogic { AND related_stack = ?`, [recordId, association.kind, association.label, ...associationKeyColumns(association)], ); - this.exec.exec('COMMIT'); - } catch (err) { - this.exec.exec('ROLLBACK'); - throw err; - } + }); const updated = await this.getRecord(recordId); if (!updated) throw new Error(`Record not found after dissociate: "${recordId}"`); diff --git a/packages/sqlite-shared/src/record.ts b/packages/sqlite-shared/src/record.ts new file mode 100644 index 0000000..cb17f86 --- /dev/null +++ b/packages/sqlite-shared/src/record.ts @@ -0,0 +1,37 @@ +/** + * Same surface as index.ts, minus the token-store pieces + * (TOKENS_SCHEMA_SQL, SharedTokenLogic) and the file-lock helpers + * (acquireLock/releaseLock). A record-only SQLite engine — one with no + * separate token file and no lock file, e.g. a Durable Object, where the + * platform's single-writer-per-id model already is the lock — imports + * this instead of the full barrel so its bundle never reaches + * token-logic.ts's `node:crypto` import. That module is a real Node + * built-in outside a `nodejs_compat` Worker, and esbuild can't always + * fully eliminate an unused-but-reachable class export's module-level + * imports the way it does for lock.ts's plain functions — so avoiding the + * import (not just the unused export) has to happen at this file's level. + */ +export { + RECORD_SCHEMA_SQL, + FTS5_SCHEMA_SQL, + PRAGMA_FOREIGN_KEYS_ON, + PRAGMA_JOURNAL_MODE_WAL, +} from './schema.js'; +export { buildWhereClause, buildOrderClause, getSortField, getSortColumn } from './query.js'; +export { + encodeCursor, + decodeCursor, + makeCursor, + SORT_FIELDS, + type SortField, + type DecodedCursor, +} from './cursor.js'; +export { rowToRecord, rowToAssociation, rowToType, rowToVersion, toMs, fromMs } from './mappers.js'; +export { sanitizeFts5Query, fts5Strategy } from './fts5.js'; +export { + type SqlExecutor, + isForeignKeyViolation, + isUniqueConstraintViolation, +} from './executor.js'; +export { insertConfigRecord, readStackConfig, type StackConfig } from './config.js'; +export { SharedSqlRecordLogic, type SharedSqlRecordLogicDeps } from './record-logic.js'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e73a6e..13a0de0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + '@cloudflare/workers-types': 5.20260817.1 + importers: .: @@ -19,16 +22,16 @@ importers: version: 1.0.0 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.2.1) + version: 10.0.1(eslint@10.2.1(supports-color@10.2.2)) '@types/node': specifier: ^22.0.0 version: 22.19.17 eslint: specifier: ^10.2.1 - version: 10.2.1 + version: 10.2.1(supports-color@10.2.2) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.2.1) + version: 10.1.8(eslint@10.2.1(supports-color@10.2.2)) prettier: specifier: ^3.8.3 version: 3.8.3 @@ -37,10 +40,10 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.59.1 - version: 8.59.1(eslint@10.2.1)(typescript@5.9.3) + version: 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/adapter-api: dependencies: @@ -62,7 +65,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/adapter-local: dependencies: @@ -84,7 +87,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/blob-adapter-disk: dependencies: @@ -100,7 +103,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/blob-adapter-s3: dependencies: @@ -122,7 +125,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/commons: dependencies: @@ -138,7 +141,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/conformance-fixtures: dependencies: @@ -154,7 +157,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/core: devDependencies: @@ -166,7 +169,38 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) + + packages/record-adapter-do-sqlite: + dependencies: + '@haverstack/core': + specifier: workspace:^ + version: link:../core + devDependencies: + '@cloudflare/vitest-pool-workers': + specifier: ^0.22.0 + version: 0.22.0(@cloudflare/workers-types@5.20260817.1)(@vitest/runner@4.1.11)(@vitest/snapshot@4.1.11)(vitest@4.1.11(@types/node@22.19.17)(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0))) + '@cloudflare/workers-types': + specifier: 5.20260817.1 + version: 5.20260817.1 + '@haverstack/sqlite-shared': + specifier: workspace:* + version: link:../sqlite-shared + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.13)(supports-color@10.2.2)(typescript@5.9.3)(yaml@2.9.0) + typescript: + specifier: ^5.5.0 + version: 5.9.3 + vite: + specifier: ^7.0.0 + version: 7.3.6(@types/node@22.19.17)(yaml@2.9.0) + vitest: + specifier: ^4.1.0 + version: 4.1.11(@types/node@22.19.17)(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0)) + wrangler: + specifier: ^4.127.1 + version: 4.127.1(@cloudflare/workers-types@5.20260817.1) packages/record-adapter-sqlite: dependencies: @@ -182,13 +216,13 @@ importers: version: 22.19.17 tsup: specifier: ^8.5.1 - version: 8.5.1(postcss@8.5.13)(typescript@5.9.3)(yaml@2.9.0) + version: 8.5.1(postcss@8.5.13)(supports-color@10.2.2)(typescript@5.9.3)(yaml@2.9.0) typescript: specifier: ^5.5.0 version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/sqlite-shared: dependencies: @@ -204,7 +238,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages/wire-types: dependencies: @@ -220,7 +254,7 @@ importers: version: 5.9.3 vitest: specifier: ^2.0.0 - version: 2.1.9(@types/node@22.19.17) + version: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) packages: @@ -373,6 +407,96 @@ packages: resolution: {integrity: sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==} engines: {node: '>= 20.12.0'} + '@cloudflare/kv-asset-handler@0.5.0': + resolution: {integrity: sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg==} + engines: {node: '>=22.0.0'} + + '@cloudflare/unenv-preset@2.16.1': + resolution: {integrity: sha512-ECxObrMfyTl5bhQf/lZCXwo5G6xX9IAUo+nDMKK4SZ8m4Jvvxp52vilxyySSWh2YTZz8+HQ07qGH/2rEom1vDw==} + peerDependencies: + unenv: 2.0.0-rc.24 + workerd: '>1.20260305.0 <2.0.0-0' + peerDependenciesMeta: + workerd: + optional: true + + '@cloudflare/vitest-pool-workers@0.22.0': + resolution: {integrity: sha512-OJv/qikkOgxnKxJ5xrLS7zuOLZhc/6iziU+llqZm4tiQf2CJUYwlMuXN68VaWIQebORd1AUx4w6A0oy8XRbuaQ==} + peerDependencies: + '@vitest/runner': ^4.1.0 + '@vitest/snapshot': ^4.1.0 + vitest: ^4.1.0 + + '@cloudflare/workerd-darwin-64@1.20260815.1': + resolution: {integrity: sha512-7PsLdcz6pT9EMd1EJGZEgMyYRfs0CHxGs62PS2L1w3s6+xGmQcRXKm/zoMftmqZF45JBa4MzFeownRKbRt/x5g==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-64@1.20260828.1': + resolution: {integrity: sha512-CVd+xPhqUESg8Xhq09TZx0wl4FSirfJGOzvbPz2yHhBIvmNHFFQkSN3rkd7wEwnhQQk37Xi0/aD6ykPLJbmGiQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260815.1': + resolution: {integrity: sha512-60wtg8ng7FVWeOg/UMbZ9Ye0sslpRRAKoftPbdtuH2volq676quxVr6Zm2EjVULH/JFZeCn72dbLlrnbh0Mpcw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-darwin-arm64@1.20260828.1': + resolution: {integrity: sha512-5HDPXRM152vU5JveByGFk34X57TVyIsfp4cabepAf45DC0MKvm52ucJqAjW1h8bvW4X+zRw9GU35OHF9FEC9Ww==} + engines: {node: '>=16'} + cpu: [arm64] + os: [darwin] + + '@cloudflare/workerd-linux-64@1.20260815.1': + resolution: {integrity: sha512-MuqKIHPo0Qyo8MZMmy0lP2B5PeAL7f4T9Fu4Usk3QdbV4JIrKG/OoybN3Ign7m/Dff+L1Oo/ZHydB+hEg1ueFw==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-64@1.20260828.1': + resolution: {integrity: sha512-MQ1Ll9P7F72HHUKizbb7BlDfbY8fRoNMpbIpZoU6uKsSkneFICWSKv6UlgU9EQZ+w0i7TMa12iUgJ8l29eRI9A==} + engines: {node: '>=16'} + cpu: [x64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260815.1': + resolution: {integrity: sha512-XNFtJ5rIqJxnY6ISjkfbhT/ODiWJ6LcBvNbntuPD6I/F2k7aZeKgPaXrvWvKde66LXyzFKzc8Hn+Ydx4shevQg==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-linux-arm64@1.20260828.1': + resolution: {integrity: sha512-FBTaUQ1xcU9jcp4OyBPcH8x0QiFvc1iuZL2GkD8zp2q1WyTVHYOptRDQUU+cuHjt0rQ2EIKVPBjahPxfa0joBw==} + engines: {node: '>=16'} + cpu: [arm64] + os: [linux] + + '@cloudflare/workerd-windows-64@1.20260815.1': + resolution: {integrity: sha512-PiIUWrhbMg3quolwjgMvPOd75vKESjT4aDm7nL6mSjL5IOgmpO/zKstXnYfnEH3pq7sC0UCvKlF8ZPcfsh8NMw==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workerd-windows-64@1.20260828.1': + resolution: {integrity: sha512-yvr77hC7dUbvK5K+SCg062kkPq3sx+drV1PcgHslzHDYcJBtT0V3X80qLE49LW1vq2svaeNmsVQS+vHsqWu8cQ==} + engines: {node: '>=16'} + cpu: [x64] + os: [win32] + + '@cloudflare/workers-types@5.20260817.1': + resolution: {integrity: sha512-5Dv+cyjusBTPLMRedUCiLJu3zqeeupgyn1QcHmpHJV9k/TjQAxx79AO8RVtpjVaO2CB+Oh3ywAp1UuNpbyDjGQ==} + + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + + '@emnapi/runtime@1.11.3': + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -385,6 +509,12 @@ packages: cpu: [ppc64] os: [aix] + '@esbuild/aix-ppc64@0.28.1': + resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + '@esbuild/android-arm64@0.21.5': resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} engines: {node: '>=12'} @@ -397,6 +527,12 @@ packages: cpu: [arm64] os: [android] + '@esbuild/android-arm64@0.28.1': + resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + '@esbuild/android-arm@0.21.5': resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} engines: {node: '>=12'} @@ -409,6 +545,12 @@ packages: cpu: [arm] os: [android] + '@esbuild/android-arm@0.28.1': + resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + '@esbuild/android-x64@0.21.5': resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} engines: {node: '>=12'} @@ -421,6 +563,12 @@ packages: cpu: [x64] os: [android] + '@esbuild/android-x64@0.28.1': + resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + '@esbuild/darwin-arm64@0.21.5': resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} engines: {node: '>=12'} @@ -433,6 +581,12 @@ packages: cpu: [arm64] os: [darwin] + '@esbuild/darwin-arm64@0.28.1': + resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + '@esbuild/darwin-x64@0.21.5': resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} engines: {node: '>=12'} @@ -445,6 +599,12 @@ packages: cpu: [x64] os: [darwin] + '@esbuild/darwin-x64@0.28.1': + resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + '@esbuild/freebsd-arm64@0.21.5': resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} engines: {node: '>=12'} @@ -457,6 +617,12 @@ packages: cpu: [arm64] os: [freebsd] + '@esbuild/freebsd-arm64@0.28.1': + resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + '@esbuild/freebsd-x64@0.21.5': resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} engines: {node: '>=12'} @@ -469,6 +635,12 @@ packages: cpu: [x64] os: [freebsd] + '@esbuild/freebsd-x64@0.28.1': + resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + '@esbuild/linux-arm64@0.21.5': resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} engines: {node: '>=12'} @@ -481,6 +653,12 @@ packages: cpu: [arm64] os: [linux] + '@esbuild/linux-arm64@0.28.1': + resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + '@esbuild/linux-arm@0.21.5': resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} engines: {node: '>=12'} @@ -493,6 +671,12 @@ packages: cpu: [arm] os: [linux] + '@esbuild/linux-arm@0.28.1': + resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + '@esbuild/linux-ia32@0.21.5': resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} engines: {node: '>=12'} @@ -505,6 +689,12 @@ packages: cpu: [ia32] os: [linux] + '@esbuild/linux-ia32@0.28.1': + resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + '@esbuild/linux-loong64@0.21.5': resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} engines: {node: '>=12'} @@ -517,6 +707,12 @@ packages: cpu: [loong64] os: [linux] + '@esbuild/linux-loong64@0.28.1': + resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + '@esbuild/linux-mips64el@0.21.5': resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} engines: {node: '>=12'} @@ -529,6 +725,12 @@ packages: cpu: [mips64el] os: [linux] + '@esbuild/linux-mips64el@0.28.1': + resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + '@esbuild/linux-ppc64@0.21.5': resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} engines: {node: '>=12'} @@ -541,6 +743,12 @@ packages: cpu: [ppc64] os: [linux] + '@esbuild/linux-ppc64@0.28.1': + resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + '@esbuild/linux-riscv64@0.21.5': resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} engines: {node: '>=12'} @@ -553,6 +761,12 @@ packages: cpu: [riscv64] os: [linux] + '@esbuild/linux-riscv64@0.28.1': + resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + '@esbuild/linux-s390x@0.21.5': resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} engines: {node: '>=12'} @@ -565,6 +779,12 @@ packages: cpu: [s390x] os: [linux] + '@esbuild/linux-s390x@0.28.1': + resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + '@esbuild/linux-x64@0.21.5': resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} engines: {node: '>=12'} @@ -577,12 +797,24 @@ packages: cpu: [x64] os: [linux] + '@esbuild/linux-x64@0.28.1': + resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + '@esbuild/netbsd-arm64@0.27.7': resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] + '@esbuild/netbsd-arm64@0.28.1': + resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + '@esbuild/netbsd-x64@0.21.5': resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} engines: {node: '>=12'} @@ -595,12 +827,24 @@ packages: cpu: [x64] os: [netbsd] + '@esbuild/netbsd-x64@0.28.1': + resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + '@esbuild/openbsd-arm64@0.27.7': resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] + '@esbuild/openbsd-arm64@0.28.1': + resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + '@esbuild/openbsd-x64@0.21.5': resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} engines: {node: '>=12'} @@ -613,12 +857,24 @@ packages: cpu: [x64] os: [openbsd] + '@esbuild/openbsd-x64@0.28.1': + resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + '@esbuild/openharmony-arm64@0.27.7': resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] + '@esbuild/openharmony-arm64@0.28.1': + resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + '@esbuild/sunos-x64@0.21.5': resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} engines: {node: '>=12'} @@ -631,6 +887,12 @@ packages: cpu: [x64] os: [sunos] + '@esbuild/sunos-x64@0.28.1': + resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + '@esbuild/win32-arm64@0.21.5': resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} engines: {node: '>=12'} @@ -643,6 +905,12 @@ packages: cpu: [arm64] os: [win32] + '@esbuild/win32-arm64@0.28.1': + resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + '@esbuild/win32-ia32@0.21.5': resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} engines: {node: '>=12'} @@ -655,6 +923,12 @@ packages: cpu: [ia32] os: [win32] + '@esbuild/win32-ia32@0.28.1': + resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + '@esbuild/win32-x64@0.21.5': resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} engines: {node: '>=12'} @@ -667,6 +941,12 @@ packages: cpu: [x64] os: [win32] + '@esbuild/win32-x64@0.28.1': + resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + '@eslint-community/eslint-utils@4.9.1': resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -726,6 +1006,168 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@img/colour@1.1.0': + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} + + '@img/sharp-darwin-arm64@0.35.2': + resolution: {integrity: sha512-eEieHsMksAW4IiO5NzauESRl2D2qz3J/kwUxUrSfV06A93eEaRfMpHXyUb1mAqrR7i8U9A0GRqE9pjn6u1Jjpg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [darwin] + + '@img/sharp-darwin-x64@0.35.2': + resolution: {integrity: sha512-BaktuGPCeHJMARpodR8jK4uKiZrPAy9WrfQW0sdI37clracq8Bp01AYS3SZgi5FS/y5twa9t4+LIuuxQjqRrWw==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [darwin] + + '@img/sharp-freebsd-wasm32@0.35.2': + resolution: {integrity: sha512-YoAxdnd8hPUkvLHd3bWY+YA8nw3xM/RyRopYucNsWHVSan8NLVM3X2volsfoRDcXdUJPg6tXahSd7HXPK7lRnw==} + engines: {node: '>=20.9.0'} + os: [freebsd] + + '@img/sharp-libvips-darwin-arm64@1.3.1': + resolution: {integrity: sha512-4V/M3roRMTYjiwZY9IOVQOE8OyeCxFAkYmyZDrZl51uOKjibm3oeEJ4WAmLxutAfzFbC9jqUiPs2gbnGflH+7g==} + cpu: [arm64] + os: [darwin] + + '@img/sharp-libvips-darwin-x64@1.3.1': + resolution: {integrity: sha512-c0/DxItpJv2+dGhgycJBBgotdqruGYDvA79drdh0MD1dFpy7JzJ/PlXwi1H4rFf0eTy8tgbI91aHDnZIceY3jQ==} + cpu: [x64] + os: [darwin] + + '@img/sharp-libvips-linux-arm64@1.3.1': + resolution: {integrity: sha512-JznefmcK9j1JKPz8AkQDh89kjojubyfOasWBPKfzMIhPwsgDy9evpE/naJTXXXmghS1iFwR8u/kTwh/I2/+GCw==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-arm@1.3.1': + resolution: {integrity: sha512-aGGy9aWzXgHBG7HNyQPWorZthlp7+x6fDRoPAQbGO3ThcttuTyKIx3NuSHb6zb4gBNq6/yNn9f1cy9nFKS/Vmg==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-ppc64@1.3.1': + resolution: {integrity: sha512-1EkwGNCZk6iWNCMWqrvdJ+r1j0PT1zIz60CNPhYnJlK/zyeWqlsPZIe+ocBVqPF8k/Ssee/NCk+tE9Ryrko6ng==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-riscv64@1.3.1': + resolution: {integrity: sha512-Ilays+w2bXdnxzxtQdmXR62u8o8GYa3eL4+Gr+1KiE4xperMZUslRaVPJwwPkzlHEjGfXAfRVAa/7CYCtSqsBw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-s390x@1.3.1': + resolution: {integrity: sha512-VfBwVHQTbRoj4XlpA/KLZ7ltgMpz+4WSejFzQ+GnoImjo1PtEJ59QB2qR1xQEeRPYIkNrPIm2L4cICMvz4C2ew==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linux-x64@1.3.1': + resolution: {integrity: sha512-+c8ukgwU62DS54nCAjw7keOfHUkmr0B5QHEdcOqRnodF/MNXJbVI8Eopoj4B/0H8Asr65I+A4Amrn7a85/md6A==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + resolution: {integrity: sha512-qlKb/pwbkAi1WMsJrYHk7CuDrd12s27U2QnRhFYUoJNrRCmkosMTttuRFat/DDB3IlDm5qE1TJgZ4JDnHX8Ldw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + resolution: {integrity: sha512-yO21HwoUVLN8Qa+/SBjQLMYwBWAVJjeGPNe+hc0OUeMeifEtJqu5a1c4HayE1nNpDih9y3/KkoltfkDodmKAlg==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-linux-arm64@0.35.2': + resolution: {integrity: sha512-af12Pnd0ZGu2HfP8NayB0kk6eC/lrfbQE6HlR4jD+34wdJ1Vw9TF6TMn6ZvffT+WgqVsl0hRbmNvz2u/23VmwA==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-arm@0.35.2': + resolution: {integrity: sha512-SE4kzF2mepn6z+6E7L6lsV8FzuLL6IPQdyX8ZiwROAG/G8td+hP/m7FsFPwidtrF19gvajuC9l6TxAVcsA4S7A==} + engines: {node: '>=20.9.0'} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-ppc64@0.35.2': + resolution: {integrity: sha512-hYSBm7zcNtDCozCxQHYZJiu63b/bXsgRZuOxCIBZsStMM9Vap47iFHdbX4kCvQsblPB/k+clhELpdQJHQLSHvg==} + engines: {node: '>=20.9.0'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-riscv64@0.35.2': + resolution: {integrity: sha512-qQt0Kc13+Hoan/Awq/qMSQw3L+RI1NCRPgD5cUJ/1WSSmIoysLOc72jlRM3E0OHN9Yr313jgeQ2T+zW+F03QFA==} + engines: {node: '>=20.9.0'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-s390x@0.35.2': + resolution: {integrity: sha512-E4fLLfRPzDLlEeDaTzI98OFLcv++WL5ChLLMwPoVd0CIoZQqupBSNbOisPL5am9XsbQ9T84+iiMpUvbFtkunbA==} + engines: {node: '>=20.9.0'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@img/sharp-linux-x64@0.35.2': + resolution: {integrity: sha512-gi0zFJJRLswfCZmHtJdikXPOc5u7qamSOS3NHedLqLd4W8Q0NqjdBr6TTRIgsfFjqfTsHFgdfvJ9LwqSgcHiAA==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@img/sharp-linuxmusl-arm64@0.35.2': + resolution: {integrity: sha512-siWbOW1u6HFnFLrp0waKyW7VEf7jYvcDWdrXEFa8AkdAQgEvuu5Fz8/Y70w9EeqAdwDtfU012BhEHHaDqvQNzg==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@img/sharp-linuxmusl-x64@0.35.2': + resolution: {integrity: sha512-YBqMMcjDi4QGYiSn4vNOYBhmlC4z5AXqkOUUqI2e0AFA4urNv4ESgOgwNl3K+4etQhha0twXlzeF20bbULm9Yg==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@img/sharp-wasm32@0.35.2': + resolution: {integrity: sha512-Mrv4JQNYVQ94xH+jzZ9r+gowleN8mv2FTgKT+PI6bx5C0G8TdNYndu161pg2i7uoBwxy2ImPMHrJOM2LZef7Bw==} + engines: {node: '>=20.9.0'} + + '@img/sharp-webcontainers-wasm32@0.35.2': + resolution: {integrity: sha512-QNV27pxs9wpApEiCfvHM1RDoP1w1+2KrUWWDPEhEwg+latvOrfuhWrHWZKwdSFwU6jh3myjw/yOCRsUIuOft3g==} + engines: {node: '>=20.9.0'} + cpu: [wasm32] + + '@img/sharp-win32-arm64@0.35.2': + resolution: {integrity: sha512-BiVRYc/t6/Vl3e1hBx0hugG4oN9Pydf4fgMSpxTQJmwGUg/YoXTWHiFeRymHfCZzifxu4F4rpk/I67D0LQ20wQ==} + engines: {node: '>=20.9.0'} + cpu: [arm64] + os: [win32] + + '@img/sharp-win32-ia32@0.35.2': + resolution: {integrity: sha512-YYEhx9PImCC7T0tI8JDMi4DB9LwLCXCU5OWNYEXAxh5Q1ShKkyC6byxzoBJ3gEFDnH2lQckWuDe70G7mB2XJog==} + engines: {node: ^20.9.0} + cpu: [ia32] + os: [win32] + + '@img/sharp-win32-x64@0.35.2': + resolution: {integrity: sha512-imoOyBcoM/iiUr4J6VPpCNjPnjvP/Gks95898yB8YqoGGYmHYbOyCuNv9FMhFgtaiHFGbHW8bxKqRV6VjtXThQ==} + engines: {node: '>=20.9.0'} + cpu: [x64] + os: [win32] + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -739,6 +1181,9 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@3.1.0': resolution: {integrity: sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==} engines: {node: '>=20.0.0'} @@ -755,6 +1200,15 @@ packages: resolution: {integrity: sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==} engines: {node: '>=22.13'} + '@poppinss/colors@4.1.6': + resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==} + + '@poppinss/dumper@0.6.5': + resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==} + + '@poppinss/exception@1.2.3': + resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==} + '@rollup/rollup-android-arm-eabi@4.60.2': resolution: {integrity: sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==} cpu: [arm] @@ -893,6 +1347,10 @@ packages: cpu: [x64] os: [win32] + '@sindresorhus/is@7.2.0': + resolution: {integrity: sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==} + engines: {node: '>=18'} + '@sinonjs/commons@3.0.1': resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} @@ -929,6 +1387,18 @@ packages: resolution: {integrity: sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA==} engines: {node: '>=18.0.0'} + '@speed-highlight/core@1.2.24': + resolution: {integrity: sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/esrecurse@4.3.1': resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} @@ -1009,6 +1479,9 @@ packages: '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + '@vitest/expect@4.1.11': + resolution: {integrity: sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==} + '@vitest/mocker@2.1.9': resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} peerDependencies: @@ -1020,21 +1493,47 @@ packages: vite: optional: true + '@vitest/mocker@4.1.11': + resolution: {integrity: sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.9': resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + '@vitest/pretty-format@4.1.11': + resolution: {integrity: sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==} + '@vitest/runner@2.1.9': resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + '@vitest/runner@4.1.11': + resolution: {integrity: sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==} + '@vitest/snapshot@2.1.9': resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + '@vitest/snapshot@4.1.11': + resolution: {integrity: sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==} + '@vitest/spy@2.1.9': resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + '@vitest/spy@4.1.11': + resolution: {integrity: sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==} + '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@vitest/utils@4.1.11': + resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1062,6 +1561,9 @@ packages: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} + blake3-wasm@2.1.5: + resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -1087,6 +1589,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} @@ -1095,6 +1601,9 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + cjs-module-lexer@1.2.3: + resolution: {integrity: sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1106,6 +1615,13 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -1129,13 +1645,23 @@ packages: deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + diff@5.2.2: resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==} engines: {node: '>=0.3.1'} + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.3.2: + resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -1146,6 +1672,11 @@ packages: engines: {node: '>=18'} hasBin: true + esbuild@0.28.1: + resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} + engines: {node: '>=18'} + hasBin: true + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -1318,6 +1849,10 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + launch-editor@2.14.1: resolution: {integrity: sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==} @@ -1346,6 +1881,14 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + miniflare@5.20260815.0-alpha: + resolution: {integrity: sha512-YAaGj4Sh5f4fqHKiMQ8zRHDOOM5IGUVtMhnLIeyjuQfU+9P6hcOTrHUVtbfj/ZPay9Kzik4pWELB39pGgefjiQ==} + engines: {node: '>=22.0.0'} + + miniflare@5.20260828.0-alpha: + resolution: {integrity: sha512-6nbxhZEcz/UET3Y1OnYPsrAUjUmuFoib3ynUqteRdn1YnDxsLg8cwgZJZCk9QmtOmGzXwzXzgE/d/C0dJAPtVw==} + engines: {node: '>=22.0.0'} + minimatch@10.2.5: resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} @@ -1374,6 +1917,10 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} + engines: {node: '>=12.20.0'} + optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -1397,6 +1944,9 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-to-regexp@6.3.0: + resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} + path-to-regexp@8.4.2: resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} @@ -1482,6 +2032,10 @@ packages: engines: {node: '>=10'} hasBin: true + sharp@0.35.2: + resolution: {integrity: sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==} + engines: {node: '>=20.9.0'} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1517,11 +2071,18 @@ packages: std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.2.0: + resolution: {integrity: sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==} + sucrase@3.35.1: resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} engines: {node: '>=16 || 14 >=14.17'} hasBin: true + supports-color@10.2.2: + resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==} + engines: {node: '>=18'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -1555,6 +2116,10 @@ packages: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} + tinyrainbow@3.1.1: + resolution: {integrity: sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==} + engines: {node: '>=14.0.0'} + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} @@ -1624,6 +2189,13 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + unenv@2.0.0-rc.24: + resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -1663,6 +2235,46 @@ packages: terser: optional: true + vite@7.3.6: + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitest@2.1.9: resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1688,6 +2300,47 @@ packages: jsdom: optional: true + vitest@4.1.11: + resolution: {integrity: sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==} + 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.1.11 + '@vitest/browser-preview': 4.1.11 + '@vitest/browser-webdriverio': 4.1.11 + '@vitest/coverage-istanbul': 4.1.11 + '@vitest/coverage-v8': 4.1.11 + '@vitest/ui': 4.1.11 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1702,6 +2355,48 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + workerd@1.20260815.1: + resolution: {integrity: sha512-8bArFkHmlp7qFEKVPyNzDzHzS35gc2fg0PYBcDtaNLF7UCDryCX2BQnpkUkTHYIy824IRrHOTwOEoTj0sUO2Fg==} + engines: {node: '>=16'} + hasBin: true + + workerd@1.20260828.1: + resolution: {integrity: sha512-pB9yvt0kkwZDAGZHmpY59r0o3hM0DzdW6BJERqwZOhunZ3ssOyDSgQxOQer2cSZW4YCFeOTIQYN1qwhK5wv/Cw==} + engines: {node: '>=16'} + hasBin: true + + wrangler@4.124.0: + resolution: {integrity: sha512-75euoZKjVTJYFy+Xhctt/5JlZL4M6A4xmovZsUlep+6GHcCm14n9VtdGzNIybOW2t8wuNxRj5iMUwjT5E7Ctog==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': 5.20260817.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + wrangler@4.127.1: + resolution: {integrity: sha512-OzsiNgaI8i681L/+KnAKc+uEZ5D57xK5JuNvCOpRKICF4/5Q3Cu1oTGuUiT/f3GDUqQb3gzXNT0tfOHGMEtknw==} + engines: {node: '>=22.0.0'} + hasBin: true + peerDependencies: + '@cloudflare/workers-types': 5.20260817.1 + peerDependenciesMeta: + '@cloudflare/workers-types': + optional: true + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -1711,6 +2406,15 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + youch-core@0.3.3: + resolution: {integrity: sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA==} + + youch@4.1.0-beta.10: + resolution: {integrity: sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ==} + + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} + snapshots: '@aws-sdk/checksums@3.1000.29': @@ -1973,35 +2677,105 @@ snapshots: '@changesets/types': 7.0.0 '@manypkg/get-packages': 3.1.0 - '@changesets/read@1.0.0': - dependencies: - '@changesets/git': 4.0.0 - '@changesets/parse': 1.0.0 - '@changesets/types': 7.0.0 + '@changesets/read@1.0.0': + dependencies: + '@changesets/git': 4.0.0 + '@changesets/parse': 1.0.0 + '@changesets/types': 7.0.0 + + '@changesets/should-skip-package@1.0.0': + dependencies: + '@changesets/types': 7.0.0 + + '@changesets/types@7.0.0': {} + + '@changesets/write@1.0.1': + dependencies: + '@changesets/format': 0.1.2 + '@changesets/types': 7.0.0 + human-id: 4.2.1 + + '@clack/core@1.4.3': + dependencies: + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@clack/prompts@1.7.0': + dependencies: + '@clack/core': 1.4.3 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.2 + sisteransi: 1.0.5 + + '@cloudflare/kv-asset-handler@0.5.0': {} + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260815.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260815.1 + + '@cloudflare/unenv-preset@2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260828.1)': + dependencies: + unenv: 2.0.0-rc.24 + optionalDependencies: + workerd: 1.20260828.1 + + '@cloudflare/vitest-pool-workers@0.22.0(@cloudflare/workers-types@5.20260817.1)(@vitest/runner@4.1.11)(@vitest/snapshot@4.1.11)(vitest@4.1.11(@types/node@22.19.17)(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0)))': + dependencies: + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + cjs-module-lexer: 1.2.3 + esbuild: 0.28.1 + miniflare: 5.20260815.0-alpha + vitest: 4.1.11(@types/node@22.19.17)(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0)) + wrangler: 4.124.0(@cloudflare/workers-types@5.20260817.1) + zod: 4.4.3 + transitivePeerDependencies: + - '@cloudflare/workers-types' + - bufferutil + - utf-8-validate + + '@cloudflare/workerd-darwin-64@1.20260815.1': + optional: true + + '@cloudflare/workerd-darwin-64@1.20260828.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260815.1': + optional: true + + '@cloudflare/workerd-darwin-arm64@1.20260828.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260815.1': + optional: true + + '@cloudflare/workerd-linux-64@1.20260828.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260815.1': + optional: true + + '@cloudflare/workerd-linux-arm64@1.20260828.1': + optional: true - '@changesets/should-skip-package@1.0.0': - dependencies: - '@changesets/types': 7.0.0 + '@cloudflare/workerd-windows-64@1.20260815.1': + optional: true - '@changesets/types@7.0.0': {} + '@cloudflare/workerd-windows-64@1.20260828.1': + optional: true - '@changesets/write@1.0.1': - dependencies: - '@changesets/format': 0.1.2 - '@changesets/types': 7.0.0 - human-id: 4.2.1 + '@cloudflare/workers-types@5.20260817.1': {} - '@clack/core@1.4.3': + '@cspotcode/source-map-support@0.8.1': dependencies: - fast-wrap-ansi: 0.2.2 - sisteransi: 1.0.5 + '@jridgewell/trace-mapping': 0.3.9 - '@clack/prompts@1.7.0': + '@emnapi/runtime@1.11.3': dependencies: - '@clack/core': 1.4.3 - fast-string-width: 3.0.2 - fast-wrap-ansi: 0.2.2 - sisteransi: 1.0.5 + tslib: 2.8.1 + optional: true '@esbuild/aix-ppc64@0.21.5': optional: true @@ -2009,158 +2783,236 @@ snapshots: '@esbuild/aix-ppc64@0.27.7': optional: true + '@esbuild/aix-ppc64@0.28.1': + optional: true + '@esbuild/android-arm64@0.21.5': optional: true '@esbuild/android-arm64@0.27.7': optional: true + '@esbuild/android-arm64@0.28.1': + optional: true + '@esbuild/android-arm@0.21.5': optional: true '@esbuild/android-arm@0.27.7': optional: true + '@esbuild/android-arm@0.28.1': + optional: true + '@esbuild/android-x64@0.21.5': optional: true '@esbuild/android-x64@0.27.7': optional: true + '@esbuild/android-x64@0.28.1': + optional: true + '@esbuild/darwin-arm64@0.21.5': optional: true '@esbuild/darwin-arm64@0.27.7': optional: true + '@esbuild/darwin-arm64@0.28.1': + optional: true + '@esbuild/darwin-x64@0.21.5': optional: true '@esbuild/darwin-x64@0.27.7': optional: true + '@esbuild/darwin-x64@0.28.1': + optional: true + '@esbuild/freebsd-arm64@0.21.5': optional: true '@esbuild/freebsd-arm64@0.27.7': optional: true + '@esbuild/freebsd-arm64@0.28.1': + optional: true + '@esbuild/freebsd-x64@0.21.5': optional: true '@esbuild/freebsd-x64@0.27.7': optional: true + '@esbuild/freebsd-x64@0.28.1': + optional: true + '@esbuild/linux-arm64@0.21.5': optional: true '@esbuild/linux-arm64@0.27.7': optional: true + '@esbuild/linux-arm64@0.28.1': + optional: true + '@esbuild/linux-arm@0.21.5': optional: true '@esbuild/linux-arm@0.27.7': optional: true + '@esbuild/linux-arm@0.28.1': + optional: true + '@esbuild/linux-ia32@0.21.5': optional: true '@esbuild/linux-ia32@0.27.7': optional: true + '@esbuild/linux-ia32@0.28.1': + optional: true + '@esbuild/linux-loong64@0.21.5': optional: true '@esbuild/linux-loong64@0.27.7': optional: true + '@esbuild/linux-loong64@0.28.1': + optional: true + '@esbuild/linux-mips64el@0.21.5': optional: true '@esbuild/linux-mips64el@0.27.7': optional: true + '@esbuild/linux-mips64el@0.28.1': + optional: true + '@esbuild/linux-ppc64@0.21.5': optional: true '@esbuild/linux-ppc64@0.27.7': optional: true + '@esbuild/linux-ppc64@0.28.1': + optional: true + '@esbuild/linux-riscv64@0.21.5': optional: true '@esbuild/linux-riscv64@0.27.7': optional: true + '@esbuild/linux-riscv64@0.28.1': + optional: true + '@esbuild/linux-s390x@0.21.5': optional: true '@esbuild/linux-s390x@0.27.7': optional: true + '@esbuild/linux-s390x@0.28.1': + optional: true + '@esbuild/linux-x64@0.21.5': optional: true '@esbuild/linux-x64@0.27.7': optional: true + '@esbuild/linux-x64@0.28.1': + optional: true + '@esbuild/netbsd-arm64@0.27.7': optional: true + '@esbuild/netbsd-arm64@0.28.1': + optional: true + '@esbuild/netbsd-x64@0.21.5': optional: true '@esbuild/netbsd-x64@0.27.7': optional: true + '@esbuild/netbsd-x64@0.28.1': + optional: true + '@esbuild/openbsd-arm64@0.27.7': optional: true + '@esbuild/openbsd-arm64@0.28.1': + optional: true + '@esbuild/openbsd-x64@0.21.5': optional: true '@esbuild/openbsd-x64@0.27.7': optional: true + '@esbuild/openbsd-x64@0.28.1': + optional: true + '@esbuild/openharmony-arm64@0.27.7': optional: true + '@esbuild/openharmony-arm64@0.28.1': + optional: true + '@esbuild/sunos-x64@0.21.5': optional: true '@esbuild/sunos-x64@0.27.7': optional: true + '@esbuild/sunos-x64@0.28.1': + optional: true + '@esbuild/win32-arm64@0.21.5': optional: true '@esbuild/win32-arm64@0.27.7': optional: true + '@esbuild/win32-arm64@0.28.1': + optional: true + '@esbuild/win32-ia32@0.21.5': optional: true '@esbuild/win32-ia32@0.27.7': optional: true + '@esbuild/win32-ia32@0.28.1': + optional: true + '@esbuild/win32-x64@0.21.5': optional: true '@esbuild/win32-x64@0.27.7': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': + '@esbuild/win32-x64@0.28.1': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1(supports-color@10.2.2))': dependencies: - eslint: 10.2.1 + eslint: 10.2.1(supports-color@10.2.2) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@10.2.2)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -2173,9 +3025,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.2.1)': + '@eslint/js@10.0.1(eslint@10.2.1(supports-color@10.2.2))': optionalDependencies: - eslint: 10.2.1 + eslint: 10.2.1(supports-color@10.2.2) '@eslint/object-schema@3.0.5': {} @@ -2200,6 +3052,112 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@img/colour@1.1.0': {} + + '@img/sharp-darwin-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-arm64': 1.3.1 + optional: true + + '@img/sharp-darwin-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-darwin-x64': 1.3.1 + optional: true + + '@img/sharp-freebsd-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-libvips-darwin-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-darwin-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-arm@1.3.1': + optional: true + + '@img/sharp-libvips-linux-ppc64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-riscv64@1.3.1': + optional: true + + '@img/sharp-libvips-linux-s390x@1.3.1': + optional: true + + '@img/sharp-libvips-linux-x64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-arm64@1.3.1': + optional: true + + '@img/sharp-libvips-linuxmusl-x64@1.3.1': + optional: true + + '@img/sharp-linux-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm64': 1.3.1 + optional: true + + '@img/sharp-linux-arm@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-arm': 1.3.1 + optional: true + + '@img/sharp-linux-ppc64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-ppc64': 1.3.1 + optional: true + + '@img/sharp-linux-riscv64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-riscv64': 1.3.1 + optional: true + + '@img/sharp-linux-s390x@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-s390x': 1.3.1 + optional: true + + '@img/sharp-linux-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linux-x64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-arm64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + optional: true + + '@img/sharp-linuxmusl-x64@0.35.2': + optionalDependencies: + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + optional: true + + '@img/sharp-wasm32@0.35.2': + dependencies: + '@emnapi/runtime': 1.11.3 + optional: true + + '@img/sharp-webcontainers-wasm32@0.35.2': + dependencies: + '@img/sharp-wasm32': 0.35.2 + optional: true + + '@img/sharp-win32-arm64@0.35.2': + optional: true + + '@img/sharp-win32-ia32@0.35.2': + optional: true + + '@img/sharp-win32-x64@0.35.2': + optional: true + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2214,6 +3172,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + '@manypkg/find-root@3.1.0': dependencies: '@manypkg/tools': 2.1.2 @@ -2231,6 +3194,18 @@ snapshots: '@pnpm/deps.graph-sequencer@1100.0.1': {} + '@poppinss/colors@4.1.6': + dependencies: + kleur: 4.1.5 + + '@poppinss/dumper@0.6.5': + dependencies: + '@poppinss/colors': 4.1.6 + '@sindresorhus/is': 7.2.0 + supports-color: 10.2.2 + + '@poppinss/exception@1.2.3': {} + '@rollup/rollup-android-arm-eabi@4.60.2': optional: true @@ -2306,6 +3281,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.60.2': optional: true + '@sindresorhus/is@7.2.0': {} + '@sinonjs/commons@3.0.1': dependencies: type-detect: 4.0.8 @@ -2356,6 +3333,17 @@ snapshots: dependencies: tslib: 2.8.1 + '@speed-highlight/core@1.2.24': {} + + '@standard-schema/spec@1.1.0': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/esrecurse@4.3.1': {} '@types/estree@1.0.8': {} @@ -2372,15 +3360,15 @@ snapshots: '@types/sinonjs__fake-timers@15.0.1': {} - '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.59.1 - '@typescript-eslint/type-utils': 8.59.1(eslint@10.2.1)(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.1 - eslint: 10.2.1 + eslint: 10.2.1(supports-color@10.2.2) ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.5.0(typescript@5.9.3) @@ -2388,23 +3376,23 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.59.1(eslint@10.2.1)(typescript@5.9.3)': + '@typescript-eslint/parser@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.59.1 '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.59.1 - debug: 4.4.3 - eslint: 10.2.1 + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.2.1(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.59.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.59.1(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) '@typescript-eslint/types': 8.59.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2418,13 +3406,13 @@ snapshots: dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.59.1(eslint@10.2.1)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1)(typescript@5.9.3) - debug: 4.4.3 - eslint: 10.2.1 + '@typescript-eslint/typescript-estree': 8.59.1(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + debug: 4.4.3(supports-color@10.2.2) + eslint: 10.2.1(supports-color@10.2.2) ts-api-utils: 2.5.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -2432,13 +3420,13 @@ snapshots: '@typescript-eslint/types@8.59.1': {} - '@typescript-eslint/typescript-estree@8.59.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.59.1(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.59.1(typescript@5.9.3) + '@typescript-eslint/project-service': 8.59.1(supports-color@10.2.2)(typescript@5.9.3) '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) '@typescript-eslint/types': 8.59.1 '@typescript-eslint/visitor-keys': 8.59.1 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) minimatch: 10.2.5 semver: 7.7.4 tinyglobby: 0.2.16 @@ -2447,13 +3435,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.59.1(eslint@10.2.1)(typescript@5.9.3)': + '@typescript-eslint/utils@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(supports-color@10.2.2)) '@typescript-eslint/scope-manager': 8.59.1 '@typescript-eslint/types': 8.59.1 - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - eslint: 10.2.1 + '@typescript-eslint/typescript-estree': 8.59.1(supports-color@10.2.2)(typescript@5.9.3) + eslint: 10.2.1(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -2470,6 +3458,15 @@ snapshots: chai: 5.3.3 tinyrainbow: 1.2.0 + '@vitest/expect@4.1.11': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + chai: 6.2.2 + tinyrainbow: 3.1.1 + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@22.19.17))': dependencies: '@vitest/spy': 2.1.9 @@ -2478,31 +3475,63 @@ snapshots: optionalDependencies: vite: 5.4.21(@types/node@22.19.17) + '@vitest/mocker@4.1.11(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.11 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.6(@types/node@22.19.17)(yaml@2.9.0) + '@vitest/pretty-format@2.1.9': dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@4.1.11': + dependencies: + tinyrainbow: 3.1.1 + '@vitest/runner@2.1.9': dependencies: '@vitest/utils': 2.1.9 pathe: 1.1.2 + '@vitest/runner@4.1.11': + dependencies: + '@vitest/utils': 4.1.11 + pathe: 2.0.3 + '@vitest/snapshot@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 magic-string: 0.30.21 pathe: 1.1.2 + '@vitest/snapshot@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + '@vitest/utils': 4.1.11 + magic-string: 0.30.21 + pathe: 2.0.3 + '@vitest/spy@2.1.9': dependencies: tinyspy: 3.0.2 + '@vitest/spy@4.1.11': {} + '@vitest/utils@2.1.9': dependencies: '@vitest/pretty-format': 2.1.9 loupe: 3.2.1 tinyrainbow: 1.2.0 + '@vitest/utils@4.1.11': + dependencies: + '@vitest/pretty-format': 4.1.11 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.1 + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: acorn: 8.16.0 @@ -2528,6 +3557,8 @@ snapshots: balanced-match@4.0.4: {} + blake3-wasm@2.1.5: {} + bowser@2.14.1: {} brace-expansion@5.0.5: @@ -2551,18 +3582,26 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chai@6.2.2: {} + check-error@2.1.3: {} chokidar@4.0.3: dependencies: readdirp: 4.1.2 + cjs-module-lexer@1.2.3: {} + commander@4.1.1: {} confbox@0.1.8: {} consola@3.4.2: {} + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -2571,18 +3610,26 @@ snapshots: dataloader@2.2.3: {} - debug@4.4.3: + debug@4.4.3(supports-color@10.2.2): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 10.2.2 deep-eql@5.0.2: {} deep-is@0.1.4: {} + detect-libc@2.1.2: {} + diff@5.2.2: {} + error-stack-parser-es@1.0.5: {} + es-module-lexer@1.7.0: {} + es-module-lexer@2.3.2: {} + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -2638,11 +3685,40 @@ snapshots: '@esbuild/win32-ia32': 0.27.7 '@esbuild/win32-x64': 0.27.7 + esbuild@0.28.1: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.1 + '@esbuild/android-arm': 0.28.1 + '@esbuild/android-arm64': 0.28.1 + '@esbuild/android-x64': 0.28.1 + '@esbuild/darwin-arm64': 0.28.1 + '@esbuild/darwin-x64': 0.28.1 + '@esbuild/freebsd-arm64': 0.28.1 + '@esbuild/freebsd-x64': 0.28.1 + '@esbuild/linux-arm': 0.28.1 + '@esbuild/linux-arm64': 0.28.1 + '@esbuild/linux-ia32': 0.28.1 + '@esbuild/linux-loong64': 0.28.1 + '@esbuild/linux-mips64el': 0.28.1 + '@esbuild/linux-ppc64': 0.28.1 + '@esbuild/linux-riscv64': 0.28.1 + '@esbuild/linux-s390x': 0.28.1 + '@esbuild/linux-x64': 0.28.1 + '@esbuild/netbsd-arm64': 0.28.1 + '@esbuild/netbsd-x64': 0.28.1 + '@esbuild/openbsd-arm64': 0.28.1 + '@esbuild/openbsd-x64': 0.28.1 + '@esbuild/openharmony-arm64': 0.28.1 + '@esbuild/sunos-x64': 0.28.1 + '@esbuild/win32-arm64': 0.28.1 + '@esbuild/win32-ia32': 0.28.1 + '@esbuild/win32-x64': 0.28.1 + escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.2.1): + eslint-config-prettier@10.1.8(eslint@10.2.1(supports-color@10.2.2)): dependencies: - eslint: 10.2.1 + eslint: 10.2.1(supports-color@10.2.2) eslint-scope@9.1.2: dependencies: @@ -2655,11 +3731,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.2.1: + eslint@10.2.1(supports-color@10.2.2): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(supports-color@10.2.2)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 + '@eslint/config-array': 0.23.5(supports-color@10.2.2) '@eslint/config-helpers': 0.5.5 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.1 @@ -2669,7 +3745,7 @@ snapshots: '@types/estree': 1.0.8 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -2801,6 +3877,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + kleur@4.1.5: {} + launch-editor@2.14.1: dependencies: picocolors: 1.1.1 @@ -2827,6 +3905,30 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + miniflare@5.20260815.0-alpha: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.2 + undici: 7.29.0 + workerd: 1.20260815.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + miniflare@5.20260828.0-alpha: + dependencies: + '@cspotcode/source-map-support': 0.8.1 + sharp: 0.35.2 + undici: 7.29.0 + workerd: 1.20260828.1 + ws: 8.21.0 + youch: 4.1.0-beta.10 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + minimatch@10.2.5: dependencies: brace-expansion: 5.0.5 @@ -2859,6 +3961,8 @@ snapshots: object-assign@4.1.1: {} + obug@2.1.4: {} + optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -2882,6 +3986,8 @@ snapshots: path-key@3.1.1: {} + path-to-regexp@6.3.0: {} + path-to-regexp@8.4.2: {} pathe@1.1.2: {} @@ -2960,6 +4066,38 @@ snapshots: semver@7.8.5: {} + sharp@0.35.2: + dependencies: + '@img/colour': 1.1.0 + detect-libc: 2.1.2 + semver: 7.8.5 + optionalDependencies: + '@img/sharp-darwin-arm64': 0.35.2 + '@img/sharp-darwin-x64': 0.35.2 + '@img/sharp-freebsd-wasm32': 0.35.2 + '@img/sharp-libvips-darwin-arm64': 1.3.1 + '@img/sharp-libvips-darwin-x64': 1.3.1 + '@img/sharp-libvips-linux-arm': 1.3.1 + '@img/sharp-libvips-linux-arm64': 1.3.1 + '@img/sharp-libvips-linux-ppc64': 1.3.1 + '@img/sharp-libvips-linux-riscv64': 1.3.1 + '@img/sharp-libvips-linux-s390x': 1.3.1 + '@img/sharp-libvips-linux-x64': 1.3.1 + '@img/sharp-libvips-linuxmusl-arm64': 1.3.1 + '@img/sharp-libvips-linuxmusl-x64': 1.3.1 + '@img/sharp-linux-arm': 0.35.2 + '@img/sharp-linux-arm64': 0.35.2 + '@img/sharp-linux-ppc64': 0.35.2 + '@img/sharp-linux-riscv64': 0.35.2 + '@img/sharp-linux-s390x': 0.35.2 + '@img/sharp-linux-x64': 0.35.2 + '@img/sharp-linuxmusl-arm64': 0.35.2 + '@img/sharp-linuxmusl-x64': 0.35.2 + '@img/sharp-webcontainers-wasm32': 0.35.2 + '@img/sharp-win32-arm64': 0.35.2 + '@img/sharp-win32-ia32': 0.35.2 + '@img/sharp-win32-x64': 0.35.2 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -2989,6 +4127,8 @@ snapshots: std-env@3.10.0: {} + std-env@4.2.0: {} + sucrase@3.35.1: dependencies: '@jridgewell/gen-mapping': 0.3.13 @@ -2999,6 +4139,8 @@ snapshots: tinyglobby: 0.2.16 ts-interface-checker: 0.1.13 + supports-color@10.2.2: {} + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -3026,6 +4168,8 @@ snapshots: tinyrainbow@1.2.0: {} + tinyrainbow@3.1.1: {} + tinyspy@3.0.2: {} tree-kill@1.2.2: {} @@ -3038,13 +4182,13 @@ snapshots: tslib@2.8.1: {} - tsup@8.5.1(postcss@8.5.13)(typescript@5.9.3)(yaml@2.9.0): + tsup@8.5.1(postcss@8.5.13)(supports-color@10.2.2)(typescript@5.9.3)(yaml@2.9.0): dependencies: bundle-require: 5.1.0(esbuild@0.27.7) cac: 6.7.14 chokidar: 4.0.3 consola: 3.4.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) esbuild: 0.27.7 fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 @@ -3074,13 +4218,13 @@ snapshots: type-detect@4.1.0: {} - typescript-eslint@8.59.1(eslint@10.2.1)(typescript@5.9.3): + typescript-eslint@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1)(typescript@5.9.3))(eslint@10.2.1)(typescript@5.9.3) - '@typescript-eslint/parser': 8.59.1(eslint@10.2.1)(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.59.1(typescript@5.9.3) - '@typescript-eslint/utils': 8.59.1(eslint@10.2.1)(typescript@5.9.3) - eslint: 10.2.1 + '@typescript-eslint/eslint-plugin': 8.59.1(@typescript-eslint/parser@8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3))(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/parser': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.59.1(supports-color@10.2.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.59.1(eslint@10.2.1(supports-color@10.2.2))(supports-color@10.2.2)(typescript@5.9.3) + eslint: 10.2.1(supports-color@10.2.2) typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -3091,14 +4235,20 @@ snapshots: undici-types@6.21.0: {} + undici@7.29.0: {} + + unenv@2.0.0-rc.24: + dependencies: + pathe: 2.0.3 + uri-js@4.4.1: dependencies: punycode: 2.3.1 - vite-node@2.1.9(@types/node@22.19.17): + vite-node@2.1.9(@types/node@22.19.17)(supports-color@10.2.2): dependencies: cac: 6.7.14 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) es-module-lexer: 1.7.0 pathe: 1.1.2 vite: 5.4.21(@types/node@22.19.17) @@ -3122,7 +4272,20 @@ snapshots: '@types/node': 22.19.17 fsevents: 2.3.3 - vitest@2.1.9(@types/node@22.19.17): + vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0): + dependencies: + esbuild: 0.28.1 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + postcss: 8.5.13 + rollup: 4.60.2 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 22.19.17 + fsevents: 2.3.3 + yaml: 2.9.0 + + vitest@2.1.9(@types/node@22.19.17)(supports-color@10.2.2): dependencies: '@vitest/expect': 2.1.9 '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.17)) @@ -3132,7 +4295,7 @@ snapshots: '@vitest/spy': 2.1.9 '@vitest/utils': 2.1.9 chai: 5.3.3 - debug: 4.4.3 + debug: 4.4.3(supports-color@10.2.2) expect-type: 1.3.0 magic-string: 0.30.21 pathe: 1.1.2 @@ -3142,7 +4305,7 @@ snapshots: tinypool: 1.1.1 tinyrainbow: 1.2.0 vite: 5.4.21(@types/node@22.19.17) - vite-node: 2.1.9(@types/node@22.19.17) + vite-node: 2.1.9(@types/node@22.19.17)(supports-color@10.2.2) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.19.17 @@ -3157,6 +4320,33 @@ snapshots: - supports-color - terser + vitest@4.1.11(@types/node@22.19.17)(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.11 + '@vitest/mocker': 4.1.11(vite@7.3.6(@types/node@22.19.17)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.11 + '@vitest/runner': 4.1.11 + '@vitest/snapshot': 4.1.11 + '@vitest/spy': 4.1.11 + '@vitest/utils': 4.1.11 + es-module-lexer: 2.3.2 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.4 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.2.0 + tinybench: 2.9.0 + tinyexec: 1.3.0 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.1 + vite: 7.3.6(@types/node@22.19.17)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.19.17 + transitivePeerDependencies: + - msw + which@2.0.2: dependencies: isexe: 2.0.0 @@ -3168,6 +4358,73 @@ snapshots: word-wrap@1.2.5: {} + workerd@1.20260815.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260815.1 + '@cloudflare/workerd-darwin-arm64': 1.20260815.1 + '@cloudflare/workerd-linux-64': 1.20260815.1 + '@cloudflare/workerd-linux-arm64': 1.20260815.1 + '@cloudflare/workerd-windows-64': 1.20260815.1 + + workerd@1.20260828.1: + optionalDependencies: + '@cloudflare/workerd-darwin-64': 1.20260828.1 + '@cloudflare/workerd-darwin-arm64': 1.20260828.1 + '@cloudflare/workerd-linux-64': 1.20260828.1 + '@cloudflare/workerd-linux-arm64': 1.20260828.1 + '@cloudflare/workerd-windows-64': 1.20260828.1 + + wrangler@4.124.0(@cloudflare/workers-types@5.20260817.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260815.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 5.20260815.0-alpha + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260815.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20260817.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + wrangler@4.127.1(@cloudflare/workers-types@5.20260817.1): + dependencies: + '@cloudflare/kv-asset-handler': 0.5.0 + '@cloudflare/unenv-preset': 2.16.1(unenv@2.0.0-rc.24)(workerd@1.20260828.1) + blake3-wasm: 2.1.5 + esbuild: 0.28.1 + miniflare: 5.20260828.0-alpha + path-to-regexp: 6.3.0 + unenv: 2.0.0-rc.24 + workerd: 1.20260828.1 + optionalDependencies: + '@cloudflare/workers-types': 5.20260817.1 + fsevents: 2.3.3 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + ws@8.21.0: {} + yaml@2.9.0: {} yocto-queue@0.1.0: {} + + youch-core@0.3.3: + dependencies: + '@poppinss/exception': 1.2.3 + error-stack-parser-es: 1.0.5 + + youch@4.1.0-beta.10: + dependencies: + '@poppinss/colors': 4.1.6 + '@poppinss/dumper': 0.6.5 + '@speed-highlight/core': 1.2.24 + cookie: 1.1.1 + youch-core: 0.3.3 + + zod@4.4.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 578b40f..eea9d6f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,3 +2,23 @@ packages: - 'packages/*' allowBuilds: esbuild: true + workerd: true +# @cloudflare/workers-types publishes a new calendar-versioned release +# roughly every 24h, so resolving to "latest" almost always lands inside a +# minimumReleaseAge supply-chain policy window. Pinned to a safely-aged +# version — applies repo-wide since it's only a transitive dependency of +# wrangler / @cloudflare/vitest-pool-workers, nothing here depends on it +# directly for anything version-sensitive. +overrides: + '@cloudflare/workers-types': 5.20260817.1 +# @types/chai@5.2.3 (pulled in only by record-adapter-do-sqlite's vitest 4 / +# @cloudflare/vitest-pool-workers) collides with the chai types vendored +# inside @vitest/expect@2.1.9 (used by the rest of the repo on vitest 2) once +# both land in pnpm's shared @types hoist folder — "Duplicate identifier" +# across every package's typecheck, not just the new one. Keeping it +# unhoisted confines it to record-adapter-do-sqlite's own resolution chain, +# where it's actually needed. (Must live here, not .npmrc — pnpm 11 silently +# stops reading hoist-pattern from .npmrc.) +hoistPattern: + - '*' + - '!@types/chai' diff --git a/scripts/verify-pack.mjs b/scripts/verify-pack.mjs index 2647440..ab768cc 100644 --- a/scripts/verify-pack.mjs +++ b/scripts/verify-pack.mjs @@ -25,7 +25,18 @@ import { fileURLToPath } from 'node:url'; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..'); const packagesDir = join(repoRoot, 'packages'); -/** Entry points to import, and a symbol each must export. */ +/** + * Entry points to import, and a symbol each must export. + * + * @haverstack/record-adapter-do-sqlite is deliberately absent: it's a real + * publishable package (still packed and installed into the throwaway + * consumer below, so a missing `files` entry or an accidentally-unbundled + * @haverstack/sqlite-shared would still be caught), but its entry point + * imports ambient Durable Object globals that only exist inside the + * Workers runtime — `node -e "import(...)"` here would fail on that, not + * on anything wrong with the package. Its own vitest suite runs against + * the real Workers runtime (@cloudflare/vitest-pool-workers) instead. + */ const EXPECTATIONS = { '@haverstack/core': [ ['.', ['Stack', 'ScopedStack', 'StackError', 'SYSTEM_TYPES']],