From f9d518a420e451456e9a7e0ac907113b517d3840 Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Wed, 13 May 2026 00:50:14 -0400 Subject: [PATCH 1/4] feat(node-sdk): Init and implement node SDK --- beava-js/.gitignore | 31 + beava-js/.prettierignore | 5 + beava-js/README.md | 70 + beava-js/package.json | 20 + beava-js/packages/beava-node/README.md | 16 + .../packages/beava-node/eslint.config.mjs | 12 + beava-js/packages/beava-node/package.json | 53 + .../packages/beava-node/src/beava-error.ts | 39 + .../beava-node/src/create-beava-client.ts | 248 +++ beava-js/packages/beava-node/src/index.ts | 39 + .../packages/beava-node/src/wire-schemas.ts | 131 ++ .../test/create-beava-client.unit.test.ts | 202 ++ .../beava-node/test/helpers/repo-root.ts | 23 + .../test/helpers/spawn-beava-server.ts | 117 ++ .../test/transport-http.integration.test.ts | 98 + beava-js/packages/beava-node/tsconfig.json | 14 + beava-js/packages/beava-node/vitest.config.ts | 10 + beava-js/pnpm-lock.yaml | 1816 +++++++++++++++++ beava-js/pnpm-workspace.yaml | 2 + beava-js/tsconfig.node-library.json | 20 + beava-js/turbo.json | 20 + 21 files changed, 2986 insertions(+) create mode 100644 beava-js/.gitignore create mode 100644 beava-js/.prettierignore create mode 100644 beava-js/README.md create mode 100644 beava-js/package.json create mode 100644 beava-js/packages/beava-node/README.md create mode 100644 beava-js/packages/beava-node/eslint.config.mjs create mode 100644 beava-js/packages/beava-node/package.json create mode 100644 beava-js/packages/beava-node/src/beava-error.ts create mode 100644 beava-js/packages/beava-node/src/create-beava-client.ts create mode 100644 beava-js/packages/beava-node/src/index.ts create mode 100644 beava-js/packages/beava-node/src/wire-schemas.ts create mode 100644 beava-js/packages/beava-node/test/create-beava-client.unit.test.ts create mode 100644 beava-js/packages/beava-node/test/helpers/repo-root.ts create mode 100644 beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts create mode 100644 beava-js/packages/beava-node/test/transport-http.integration.test.ts create mode 100644 beava-js/packages/beava-node/tsconfig.json create mode 100644 beava-js/packages/beava-node/vitest.config.ts create mode 100644 beava-js/pnpm-lock.yaml create mode 100644 beava-js/pnpm-workspace.yaml create mode 100644 beava-js/tsconfig.node-library.json create mode 100644 beava-js/turbo.json diff --git a/beava-js/.gitignore b/beava-js/.gitignore new file mode 100644 index 00000000..22478255 --- /dev/null +++ b/beava-js/.gitignore @@ -0,0 +1,31 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# Dependencies +node_modules +.pnp +.pnp.js + +# Local env files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Testing +coverage + +# Turbo +.turbo + +# Build outputs +dist + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Misc +.DS_Store +*.pem diff --git a/beava-js/.prettierignore b/beava-js/.prettierignore new file mode 100644 index 00000000..c3cd7045 --- /dev/null +++ b/beava-js/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +coverage +.turbo +pnpm-lock.yaml diff --git a/beava-js/README.md b/beava-js/README.md new file mode 100644 index 00000000..d06e8bdc --- /dev/null +++ b/beava-js/README.md @@ -0,0 +1,70 @@ +# beava-js + +[Turborepo](https://turbo.build/repo) monorepo for the official Beava TypeScript packages: **`@beava/node`** (server-side HTTP client) and **`@beava/client`** (browser entry that re-exports the same fetch-based API). + +## Layout + +| Path | Package | Role | +|------|---------|------| +| `packages/beava-node` | `@beava/node` | `createBeavaClient`, Zod wire schemas, Vitest unit + optional HTTP integration tests | +| `packages/beava-client` | `@beava/client` | Re-exports `@beava/node` for app bundles that want a browser-scoped package name | + +Workspace members are defined in **`pnpm-workspace.yaml`** (currently `packages/beava-node`; add `packages/beava-client` when that package lands). Library packages extend **`tsconfig.node-library.json`** at this directory root (no separate TypeScript config package). + +## Prerequisites + +- **Node** `>=18` (see root **`package.json`** `engines`) +- **pnpm** `9.x` via [Corepack](https://nodejs.org/api/corepack.html): `corepack enable pnpm` + +## Commands + +From **`beava-js/`**: + +```sh +pnpm install +pnpm run build # tsc emit for publishable packages +pnpm run lint # eslint across workspace +pnpm run check-types # tsc --noEmit +pnpm run test # Vitest (see below) +``` + +Scoped examples: + +```sh +pnpm exec turbo run build test --filter=@beava/node +pnpm exec turbo run lint check-types --filter=@beava/client +``` + +## Tests + +**`@beava/node`** uses [Vitest](https://vitest.dev/). Default **`pnpm run test`** runs **unit tests** only (mocked `fetch`). + +**HTTP integration tests** (real `beava` subprocess, same idea as `python/tests/test_transport_http.py`) run when: + +1. The **`beava`** binary exists at **`target/debug/beava`** (repo root: run **`cargo build --bin beava`** from the Beava repo root), and +2. You set **`BEAVA_INTEGRATION=1`**. Optionally set **`BEAVA_REPO_ROOT`** to the Beava git root if discovery fails. + +```sh +# from beava-js/ +BEAVA_INTEGRATION=1 BEAVA_REPO_ROOT=/path/to/beava pnpm exec turbo run test --filter=@beava/node +``` + +CI sets these when running the **`beava-js`** job in **`.github/workflows/ci.yml`**. + +## Repo checks + +From the **Beava repo root**, **`bash .github/scripts/check.sh`** can run Rust, Python, and this tree together. **`bash .github/scripts/check.sh --js`** runs **`pnpm install`** and **`turbo run lint check-types test`** under **`beava-js/`** only. If **`target/debug/beava`** exists (from **`cargo build --bin beava`**), **`BEAVA_INTEGRATION=1`** is set so all Vitest tests run; otherwise three HTTP integration tests are skipped and the log notes why. + +## npm publish + +1. Bump **`version`** in **`packages/beava-node/package.json`** and **`packages/beava-client/package.json`**, and set **`@beava/client`** `dependencies["@beava/node"]` to **`workspace:^`** (same major/minor/patch as **`@beava/node`**). **`pnpm publish`** rewrites that to a normal **`^`** range on the tarball. +2. From **`beava-js/`**: **`pnpm install`**, **`pnpm run build`**, **`pnpm run test`** (with integration if you use **`BEAVA_INTEGRATION=1`**). +3. **`pnpm publish --filter @beava/node --access public`** (uses **`prepack`** to run **`tsc`**; add **`--dry-run`** or **`pnpm pack --filter @beava/node`** to inspect the tarball). +4. After **`@beava/node`** is on the registry, **`pnpm publish --filter @beava/client --access public`**. + +Use **`npm whoami`** / **`npm login`** (or **`pnpm config set //registry.npmjs.org/:_authToken`**). Scoped **`@beava/*`** packages need **`publishConfig.access`** (already **`public`**). + +## Links + +- [Turborepo tasks and filters](https://turbo.build/repo/docs/crafting-your-repository/running-tasks) +- Beava repo: [github.com/beava-dev/beava](https://github.com/beava-dev/beava) diff --git a/beava-js/package.json b/beava-js/package.json new file mode 100644 index 00000000..3508b75f --- /dev/null +++ b/beava-js/package.json @@ -0,0 +1,20 @@ +{ + "name": "beava-js", + "private": true, + "scripts": { + "build": "turbo run build", + "lint": "turbo run lint", + "format": "prettier --write \"**/*.{ts,md}\"", + "check-types": "turbo run check-types", + "test": "turbo run test" + }, + "devDependencies": { + "prettier": "^3.7.4", + "turbo": "^2.9.12", + "typescript": "5.9.2" + }, + "packageManager": "pnpm@9.0.0", + "engines": { + "node": ">=18" + } +} diff --git a/beava-js/packages/beava-node/README.md b/beava-js/packages/beava-node/README.md new file mode 100644 index 00000000..b787685e --- /dev/null +++ b/beava-js/packages/beava-node/README.md @@ -0,0 +1,16 @@ +# @beava/node + +TypeScript HTTP client for the [Beava](https://beava.dev) feature server data plane (`POST /ping`, `/register`, `/push`, `/get`, `/batch_get`, `/reset`). Uses `fetch` only (Node 18+, Bun, Deno with npm compatibility). + +```ts +import { createBeavaClient } from "@beava/node"; + +const client = createBeavaClient({ baseUrl: "http://127.0.0.1:8080" }); +await client.ping(); +``` + +See the [beava-js README](https://github.com/beava-dev/beava/tree/main/beava-js) for tests and integration. + +## License + +Apache-2.0 diff --git a/beava-js/packages/beava-node/eslint.config.mjs b/beava-js/packages/beava-node/eslint.config.mjs new file mode 100644 index 00000000..266513ed --- /dev/null +++ b/beava-js/packages/beava-node/eslint.config.mjs @@ -0,0 +1,12 @@ +import eslint from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + eslintConfigPrettier, + ...tseslint.configs.recommended, + { + ignores: ["dist/**"], + }, +); diff --git a/beava-js/packages/beava-node/package.json b/beava-js/packages/beava-node/package.json new file mode 100644 index 00000000..cfcf4d72 --- /dev/null +++ b/beava-js/packages/beava-node/package.json @@ -0,0 +1,53 @@ +{ + "name": "@beava/node", + "version": "0.1.0", + "description": "Beava feature server HTTP client for Node.js (async fetch).", + "type": "module", + "sideEffects": false, + "engines": { + "node": ">=18.18.0" + }, + "files": ["dist", "README.md"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "rimraf dist && tsc", + "prepack": "pnpm run build", + "test": "vitest run", + "lint": "eslint . --max-warnings 0", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "zod": "^3.24.2" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^22.15.3", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.1", + "rimraf": "^6.0.1", + "typescript": "5.9.2", + "vitest": "^3.1.3" + }, + "homepage": "https://beava.dev", + "bugs": { + "url": "https://github.com/beava-dev/beava/issues" + }, + "keywords": ["beava", "features", "fetch", "http", "real-time"], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/beava-dev/beava.git", + "directory": "beava-js/packages/beava-node" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/beava-js/packages/beava-node/src/beava-error.ts b/beava-js/packages/beava-node/src/beava-error.ts new file mode 100644 index 00000000..84f18a70 --- /dev/null +++ b/beava-js/packages/beava-node/src/beava-error.ts @@ -0,0 +1,39 @@ +import type { ZodError } from "zod"; + +import type { BeavaWireErrorBody } from "./wire-schemas.js"; + +export type { BeavaWireErrorBody } from "./wire-schemas.js"; + +export class BeavaResponseValidationError extends Error { + readonly zodError: ZodError; + + constructor(zodError: ZodError) { + super(`Invalid beava success response: ${zodError.message}`); + this.name = "BeavaResponseValidationError"; + this.zodError = zodError; + } +} + +export class BeavaError extends Error { + readonly status: number; + readonly code: string; + readonly path: string; + readonly errors: unknown[]; + + constructor( + status: number, + wire: BeavaWireErrorBody, + options?: ErrorOptions, + ) { + const msg = + wire.message ?? + wire.reason ?? + `Beava request failed with HTTP ${String(status)}`; + super(msg, options); + this.name = "BeavaError"; + this.status = status; + this.code = wire.code; + this.path = wire.path ?? ""; + this.errors = wire.errors ?? []; + } +} diff --git a/beava-js/packages/beava-node/src/create-beava-client.ts b/beava-js/packages/beava-node/src/create-beava-client.ts new file mode 100644 index 00000000..e3694fc6 --- /dev/null +++ b/beava-js/packages/beava-node/src/create-beava-client.ts @@ -0,0 +1,248 @@ +import { BeavaError, BeavaResponseValidationError } from "./beava-error.js"; +import { + batchGetRequestSchema, + batchGetResponseSchema, + beavaErrorEnvelopeSchema, + getRequestSchema, + jsonObjectSchema, + pingResponseSchema, + pushRequestSchema, + pushResponseSchema, + registerRequestSchema, + registerResponseSchema, +} from "./wire-schemas.js"; +import type { + BatchGetEntry, + BatchGetRequest, + GetRequest, + JsonObject, + JsonValue, + PingResponse, + PushRequest, + PushResponse, + RegisterRequest, + RegisterResponse, +} from "./wire-schemas.js"; +import type { ZodType } from "zod"; + +const JSON_HEADERS = { + "Content-Type": "application/json", +} as const; + +export type BeavaClientOptions = { + /** Base URL of the data plane, e.g. `http://127.0.0.1:8080` (no trailing path). */ + baseUrl: string; + /** Per-request I/O timeout in seconds (default 30, matches Python `HttpTransport`). */ + timeoutSeconds?: number; + /** Optional `fetch` override (tests or custom runtimes). */ + fetch?: typeof fetch; + /** Extra headers merged on every request after defaults. */ + headers?: HeadersInit; +}; + +export type BeavaClient = { + readonly ping: (signal?: AbortSignal) => Promise; + readonly register: ( + body: RegisterRequest, + signal?: AbortSignal, + ) => Promise; + readonly push: ( + body: PushRequest, + signal?: AbortSignal, + ) => Promise; + readonly get: (body: GetRequest, signal?: AbortSignal) => Promise; + readonly batchGet: ( + body: BatchGetRequest, + signal?: AbortSignal, + ) => Promise; + readonly reset: (signal?: AbortSignal) => Promise; +}; + +function joinBasePath(baseUrl: string, path: string): string { + const trimmed = baseUrl.replace(/\/+$/, ""); + const p = path.startsWith("/") ? path : `/${path}`; + return `${trimmed}${p}`; +} + +function combineAbortSignals( + a: AbortSignal | undefined, + b: AbortSignal | undefined, +): AbortSignal | undefined { + if (!a && !b) return undefined; + if (!a) return b; + if (!b) return a; + const c = new AbortController(); + const onAbort = (): void => { + c.abort(); + }; + a.addEventListener("abort", onAbort, { once: true }); + b.addEventListener("abort", onAbort, { once: true }); + return c.signal; +} + +function requestSignal( + timeoutSeconds: number | undefined, +): AbortSignal | undefined { + if (timeoutSeconds === undefined) return undefined; + const ms = Math.max(1, Math.ceil(timeoutSeconds * 1000)); + return AbortSignal.timeout(ms); +} + +function serialiseKey(key: GetRequest["key"]): JsonValue { + return key as JsonValue; +} + +function toWireBatchEntry(e: BatchGetEntry): Record { + if ("features" in e) { + const row: Record = { + table: e.table, + key: serialiseKey(e.key), + }; + if (e.features !== null) { + row.features = [...e.features]; + } + return row; + } + return { table: e.table, key: serialiseKey(e.key) }; +} + +async function readJsonValue(res: Response): Promise { + const text = await res.text(); + if (text.length === 0) return {}; + try { + return JSON.parse(text) as unknown; + } catch { + throw new BeavaError(res.status, { + code: "unparseable_body", + message: text.slice(0, 200), + }); + } +} + +function throwBeavaError(status: number, body: unknown): never { + const envelope = beavaErrorEnvelopeSchema.safeParse(body); + if (envelope.success) { + throw new BeavaError(status, envelope.data.error); + } + throw new BeavaError(status, { + code: "unknown", + message: typeof body === "string" ? body : JSON.stringify(body), + }); +} + +function parseSuccess(schema: ZodType, data: unknown): T { + const r = schema.safeParse(data); + if (!r.success) { + throw new BeavaResponseValidationError(r.error); + } + return r.data; +} + +async function postBeava( + fetchImpl: typeof fetch, + url: string, + bodyBytes: string, + signal: AbortSignal | undefined, + extraHeaders: HeadersInit | undefined, +): Promise { + const headers = new Headers(JSON_HEADERS); + if (extraHeaders) { + new Headers(extraHeaders).forEach((v, k) => { + headers.set(k, v); + }); + } + const res = await fetchImpl(url, { + method: "POST", + headers, + body: bodyBytes, + signal, + }); + const parsed = await readJsonValue(res); + if (res.status === 200) { + return parsed; + } + throwBeavaError(res.status, parsed); +} + +export function createBeavaClient(options: BeavaClientOptions): BeavaClient { + const fetchImpl = options.fetch ?? globalThis.fetch; + const timeoutSeconds = options.timeoutSeconds ?? 30; + const extraHeaders = options.headers; + const baseUrl = options.baseUrl; + + const run = async ( + path: string, + bodyJson: string, + perCallSignal?: AbortSignal, + ): Promise => { + const url = joinBasePath(baseUrl, path); + const signal = combineAbortSignals( + perCallSignal, + requestSignal(timeoutSeconds), + ); + return postBeava(fetchImpl, url, bodyJson, signal, extraHeaders); + }; + + return { + async ping(signal?: AbortSignal): Promise { + const out = await run("/ping", "{}", signal); + return parseSuccess(pingResponseSchema, out); + }, + + async register( + body: RegisterRequest, + signal?: AbortSignal, + ): Promise { + const parsed = registerRequestSchema.parse(body); + const payload: Record = { + nodes: parsed.nodes.map((n) => ({ ...n })), + }; + if (parsed.force === true) payload.force = true; + if (parsed.dry_run === true) payload.dry_run = true; + const out = await run("/register", JSON.stringify(payload), signal); + return parseSuccess(registerResponseSchema, out); + }, + + async push(body: PushRequest, signal?: AbortSignal): Promise { + const parsed = pushRequestSchema.parse(body); + const payload: Record = { + event: parsed.event, + data: { ...parsed.data }, + }; + const out = await run("/push", JSON.stringify(payload), signal); + return parseSuccess(pushResponseSchema, out); + }, + + async get(body: GetRequest, signal?: AbortSignal): Promise { + const parsed = getRequestSchema.parse(body); + const payload: Record = { + table: parsed.table, + key: serialiseKey(parsed.key), + }; + if (parsed.features != null) { + payload.features = [...parsed.features]; + } + const out = await run("/get", JSON.stringify(payload), signal); + return parseSuccess(jsonObjectSchema, out); + }, + + async batchGet( + body: BatchGetRequest, + signal?: AbortSignal, + ): Promise { + const parsed = batchGetRequestSchema.parse(body); + const wireRequests = parsed.requests.map((r) => toWireBatchEntry(r)); + const out = await run( + "/batch_get", + JSON.stringify({ requests: wireRequests }), + signal, + ); + const envelope = parseSuccess(batchGetResponseSchema, out); + return envelope.results ?? []; + }, + + async reset(signal?: AbortSignal): Promise { + await run("/reset", "{}", signal); + }, + }; +} diff --git a/beava-js/packages/beava-node/src/index.ts b/beava-js/packages/beava-node/src/index.ts new file mode 100644 index 00000000..aef5297c --- /dev/null +++ b/beava-js/packages/beava-node/src/index.ts @@ -0,0 +1,39 @@ +export { + BeavaError, + BeavaResponseValidationError, + type BeavaWireErrorBody, +} from "./beava-error.js"; +export { + createBeavaClient, + type BeavaClient, + type BeavaClientOptions, +} from "./create-beava-client.js"; +export { + batchGetEntrySchema, + batchGetRequestSchema, + batchGetResponseSchema, + beavaErrorEnvelopeSchema, + beavaWireErrorBodySchema, + entityKeySchema, + getRequestSchema, + jsonObjectSchema, + jsonValueSchema, + pingResponseSchema, + pushRequestSchema, + pushResponseSchema, + registerRequestSchema, + registerResponseSchema, +} from "./wire-schemas.js"; +export type { + BatchGetEntry, + BatchGetRequest, + EntityKey, + GetRequest, + JsonObject, + JsonValue, + PingResponse, + PushRequest, + PushResponse, + RegisterRequest, + RegisterResponse, +} from "./wire-schemas.js"; diff --git a/beava-js/packages/beava-node/src/wire-schemas.ts b/beava-js/packages/beava-node/src/wire-schemas.ts new file mode 100644 index 00000000..44e01742 --- /dev/null +++ b/beava-js/packages/beava-node/src/wire-schemas.ts @@ -0,0 +1,131 @@ +import { z } from "zod"; + +export type JsonValue = + | null + | boolean + | number + | string + | JsonValue[] + | { [key: string]: JsonValue }; + +export const jsonValueSchema: z.ZodType = z.lazy(() => + z.union([ + z.null(), + z.boolean(), + z.number(), + z.string(), + z.array(jsonValueSchema), + z.record(z.string(), jsonValueSchema), + ]), +); + +export const jsonObjectSchema = z.record(z.string(), jsonValueSchema); +export type JsonObject = z.infer; + +export const entityKeySchema = z.union([ + z.string(), + z.array(jsonValueSchema), +]); + +export type EntityKey = z.infer; + +export const registerRequestSchema = z + .object({ + nodes: z.array(jsonObjectSchema), + force: z.boolean().optional(), + dry_run: z.boolean().optional(), + }) + .strict(); + +export type RegisterRequest = z.infer; + +export const registerResponseSchema = jsonObjectSchema; +export type RegisterResponse = z.infer; + +export const pushRequestSchema = z + .object({ + event: z.string().min(1), + data: jsonObjectSchema, + }) + .strict(); + +export type PushRequest = z.infer; + +export const pushResponseSchema = z + .object({ + ack_lsn: z.number(), + idempotent_replay: z.boolean(), + registry_version: z.number(), + }) + .passthrough(); + +export type PushResponse = z.infer; + +export const getRequestSchema = z + .object({ + table: z.string().min(1), + key: entityKeySchema, + features: z.array(z.string()).nullable().optional(), + }) + .strict(); + +export type GetRequest = z.infer; + +export const batchGetEntrySchema = z.union([ + z + .object({ + table: z.string().min(1), + key: entityKeySchema, + }) + .strict(), + z + .object({ + table: z.string().min(1), + key: entityKeySchema, + features: z.array(z.string()).nullable(), + }) + .strict(), +]); + +export type BatchGetEntry = z.infer; + +export const batchGetRequestSchema = z + .object({ + requests: z.array(batchGetEntrySchema), + }) + .strict(); + +export type BatchGetRequest = z.infer; + +export const batchGetResponseSchema = z + .object({ + results: z.array(jsonObjectSchema).optional(), + }) + .passthrough(); + +export const pingResponseSchema = z + .object({ + pong: z.literal(true), + registry_version: z.number(), + }) + .strict(); + +export type PingResponse = z.infer; + +export const beavaWireErrorBodySchema = z + .object({ + code: z.string(), + path: z.string().optional(), + message: z.string().optional(), + reason: z.string().optional(), + errors: z.array(z.unknown()).optional(), + }) + .passthrough(); + +export type BeavaWireErrorBody = z.infer; + +export const beavaErrorEnvelopeSchema = z + .object({ + error: beavaWireErrorBodySchema, + }) + .passthrough(); diff --git a/beava-js/packages/beava-node/test/create-beava-client.unit.test.ts b/beava-js/packages/beava-node/test/create-beava-client.unit.test.ts new file mode 100644 index 00000000..a730a30c --- /dev/null +++ b/beava-js/packages/beava-node/test/create-beava-client.unit.test.ts @@ -0,0 +1,202 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + BeavaError, + BeavaResponseValidationError, +} from "../src/beava-error.js"; +import { createBeavaClient } from "../src/create-beava-client.js"; +import { registerRequestSchema } from "../src/wire-schemas.js"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +describe("createBeavaClient (unit, mocked fetch)", () => { + let fetchMock: ReturnType; + + beforeEach(() => { + fetchMock = vi.fn(); + }); + + it("POST /ping with Content-Type and empty JSON body", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ pong: true, registry_version: 7 }), + ); + const client = createBeavaClient({ + baseUrl: "http://127.0.0.1:9999/", + fetch: fetchMock as typeof fetch, + }); + const out = await client.ping(); + expect(out).toEqual({ pong: true, registry_version: 7 }); + expect(fetchMock).toHaveBeenCalledTimes(1); + const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(url).toBe("http://127.0.0.1:9999/ping"); + expect(init.method).toBe("POST"); + expect((init.headers as Headers).get("Content-Type")).toBe( + "application/json", + ); + expect(init.body).toBe("{}"); + }); + + it("merges custom headers", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ pong: true, registry_version: 1 }), + ); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + headers: { "X-Test": "1" }, + }); + await client.ping(); + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + const h = new Headers(init.headers as HeadersInit); + expect(h.get("Content-Type")).toBe("application/json"); + expect(h.get("X-Test")).toBe("1"); + }); + + it("push sends { event, data }", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse({ + ack_lsn: 1, + idempotent_replay: false, + registry_version: 2, + }), + ); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await client.push({ event: "Ev", data: { x: 1 } }); + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(JSON.parse(init.body as string)).toEqual({ + event: "Ev", + data: { x: 1 }, + }); + }); + + it("get omits features when null", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ visits: 2 })); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await client.get({ + table: "t", + key: "alice", + features: null, + }); + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(JSON.parse(init.body as string)).toEqual({ + table: "t", + key: "alice", + }); + }); + + it("batch_get serialises tuple-style entries", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ results: [{ a: 1 }] })); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await client.batchGet({ + requests: [ + { table: "t1", key: "k1" }, + { table: "t2", key: "k2", features: null }, + { table: "t3", key: "k3", features: ["f"] }, + ], + }); + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(JSON.parse(init.body as string)).toEqual({ + requests: [ + { table: "t1", key: "k1" }, + { table: "t2", key: "k2" }, + { table: "t3", key: "k3", features: ["f"] }, + ], + }); + }); + + it("reset sends {}", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ reset: true })); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await client.reset(); + const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]; + expect(init.body).toBe("{}"); + }); + + it("maps 4xx JSON error envelope to BeavaError", async () => { + fetchMock.mockResolvedValueOnce( + jsonResponse( + { + error: { + code: "invalid_registration", + path: "/register", + message: "bad", + }, + }, + 400, + ), + ); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await expect( + client.register({ nodes: [{ kind: "event", name: "x" }] }), + ).rejects.toMatchObject({ + name: "BeavaError", + code: "invalid_registration", + status: 400, + }); + }); + + it("throws BeavaResponseValidationError on malformed success ping body", async () => { + fetchMock.mockResolvedValueOnce(jsonResponse({ pong: false })); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await expect(client.ping()).rejects.toBeInstanceOf( + BeavaResponseValidationError, + ); + }); + + it("throws BeavaError on invalid JSON error body", async () => { + fetchMock.mockResolvedValueOnce( + new Response("not-json", { status: 500, statusText: "ERR" }), + ); + const client = createBeavaClient({ + baseUrl: "http://h.test", + fetch: fetchMock as typeof fetch, + }); + await expect(client.ping()).rejects.toMatchObject({ + name: "BeavaError", + code: "unparseable_body", + }); + }); +}); + +describe("registerRequestSchema", () => { + it("rejects unknown keys (strict)", () => { + const r = registerRequestSchema.safeParse({ + nodes: [], + extra: 1, + }); + expect(r.success).toBe(false); + }); +}); + +describe("BeavaError", () => { + it("prefers message over reason in constructor", () => { + const e = new BeavaError(400, { + code: "x", + message: "m", + reason: "r", + }); + expect(e.message).toContain("m"); + }); +}); diff --git a/beava-js/packages/beava-node/test/helpers/repo-root.ts b/beava-js/packages/beava-node/test/helpers/repo-root.ts new file mode 100644 index 00000000..ca3c0ae8 --- /dev/null +++ b/beava-js/packages/beava-node/test/helpers/repo-root.ts @@ -0,0 +1,23 @@ +import { existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +/** Repo root (directory containing `Cargo.toml`). */ +export function findRepoRoot(): string { + const fromEnv = process.env.BEAVA_REPO_ROOT; + if (fromEnv && existsSync(join(fromEnv, "Cargo.toml"))) { + return fromEnv; + } + let dir = dirname(fileURLToPath(import.meta.url)); + for (let i = 0; i < 12; i++) { + if (existsSync(join(dir, "Cargo.toml"))) { + return dir; + } + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error( + "Could not locate repo root (Cargo.toml). Set BEAVA_REPO_ROOT if needed.", + ); +} diff --git a/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts b/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts new file mode 100644 index 00000000..4a4f7f35 --- /dev/null +++ b/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts @@ -0,0 +1,117 @@ +import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createInterface } from "node:readline"; +import { findRepoRoot } from "./repo-root.js"; + +export type BeavaTestServer = { + readonly httpUrl: string; + readonly tcpUrl: string; + close: () => Promise; +}; + +function binaryPath(repoRoot: string): string { + const name = process.platform === "win32" ? "beava.exe" : "beava"; + return join(repoRoot, "target", "debug", name); +} + +/** + * Spawns `target/debug/beava` with ephemeral ports (mirrors `python/tests/conftest.py::beava_server`). + */ +export async function spawnBeavaServer(): Promise { + const repoRoot = findRepoRoot(); + const bin = binaryPath(repoRoot); + const walDir = mkdtempSync(join(tmpdir(), "beava-js-wal-")); + const snapDir = mkdtempSync(join(tmpdir(), "beava-js-snap-")); + + const proc: ChildProcessWithoutNullStreams = spawn( + bin, + ["--config", process.platform === "win32" ? "NUL" : "/dev/null"], + { + env: { + ...process.env, + BEAVA_LISTEN_ADDR: "127.0.0.1:0", + BEAVA_TCP_PORT: "0", + BEAVA_WAL_DIR: walDir, + BEAVA_SNAPSHOT_DIR: snapDir, + BEAVA_DEV_ENDPOINTS: "1", + }, + stdio: ["ignore", "pipe", "ignore"], + }, + ); + + const httpAddrs: string[] = []; + const tcpAddrs: string[] = []; + + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + proc.kill("SIGKILL"); + reject( + new Error( + `beava bind timeout (http=${httpAddrs.join()}, tcp=${tcpAddrs.join()})`, + ), + ); + }, 5_000); + + const rl = createInterface({ input: proc.stdout }); + + proc.once("error", (err) => { + clearTimeout(timeout); + rl.close(); + reject(err); + }); + + proc.once("exit", (code, signal) => { + if (httpAddrs.length > 0 && tcpAddrs.length > 0) return; + clearTimeout(timeout); + rl.close(); + reject( + new Error( + `beava exited before bind (code=${String(code)} signal=${String(signal)})`, + ), + ); + }); + + rl.on("line", (line) => { + try { + const rec: unknown = JSON.parse(line); + if (typeof rec !== "object" || rec === null) return; + const o = rec as { kind?: string; addr?: string }; + if (o.kind === "server.http_bound" && o.addr) httpAddrs.push(o.addr); + if (o.kind === "server.tcp_bound" && o.addr) tcpAddrs.push(o.addr); + if (httpAddrs.length > 0 && tcpAddrs.length > 0) { + clearTimeout(timeout); + rl.close(); + resolve(); + } + } catch { + /* ignore non-JSON lines */ + } + }); + }); + + const httpUrl = `http://${httpAddrs[0]}`; + const tcpUrl = `tcp://${tcpAddrs[0]}`; + + return { + httpUrl, + tcpUrl, + close() { + return new Promise((resolve) => { + if (proc.exitCode !== null || proc.signalCode !== null) { + resolve(); + return; + } + const killTimer = setTimeout(() => { + proc.kill("SIGKILL"); + }, 5_000); + proc.once("exit", () => { + clearTimeout(killTimer); + resolve(); + }); + proc.kill("SIGTERM"); + }); + }, + }; +} diff --git a/beava-js/packages/beava-node/test/transport-http.integration.test.ts b/beava-js/packages/beava-node/test/transport-http.integration.test.ts new file mode 100644 index 00000000..1fdde664 --- /dev/null +++ b/beava-js/packages/beava-node/test/transport-http.integration.test.ts @@ -0,0 +1,98 @@ +import { + describe, + it, + expect, + beforeEach, + afterEach, +} from "vitest"; +import { createBeavaClient } from "../src/create-beava-client.js"; +import { spawnBeavaServer } from "./helpers/spawn-beava-server.js"; + +const integration = process.env.BEAVA_INTEGRATION === "1"; + +/** Mirrors `python/tests/test_transport_http.py::VALID_REGISTER_PAYLOAD`. */ +const validRegister = { + nodes: [ + { + kind: "event", + name: "TestEvent", + schema: { + fields: { event_time: "i64", amount: "f64" }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], +} as const; + +const invalidRegister = { + nodes: [ + { + kind: "event", + name: "_beava_reserved", + schema: { + fields: { x: "f64" }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], +} as const; + +describe.skipIf(!integration)("HTTP transport (integration, real beava)", () => { + let server: Awaited>; + + beforeEach(async () => { + server = await spawnBeavaServer(); + }, 120_000); + + afterEach(async () => { + await server?.close(); + }); + + it("register returns status ok and registry_version >= 1", async () => { + const client = createBeavaClient({ baseUrl: server.httpUrl }); + const result = await client.register(validRegister); + expect(result).toMatchObject({ + status: "ok", + }); + expect(Number(result.registry_version)).toBeGreaterThanOrEqual(1); + }); + + it("invalid register raises BeavaError with invalid_registration", async () => { + const client = createBeavaClient({ baseUrl: server.httpUrl }); + await expect(client.register(invalidRegister)).rejects.toMatchObject({ + name: "BeavaError", + code: "invalid_registration", + }); + }); + + it("ping bumps registry_version after register", async () => { + const client = createBeavaClient({ baseUrl: server.httpUrl }); + const pre = await client.ping(); + expect(pre.pong).toBe(true); + const preV = pre.registry_version; + await client.register({ + nodes: [ + { + kind: "event", + name: "PingBumpEvent", + schema: { + fields: { n: "i64" }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], + }); + const post = await client.ping(); + expect(post.pong).toBe(true); + expect(post.registry_version).toBe(preV + 1); + }); +}); diff --git a/beava-js/packages/beava-node/tsconfig.json b/beava-js/packages/beava-node/tsconfig.json new file mode 100644 index 00000000..88a061c2 --- /dev/null +++ b/beava-js/packages/beava-node/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.node-library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/beava-js/packages/beava-node/vitest.config.ts b/beava-js/packages/beava-node/vitest.config.ts new file mode 100644 index 00000000..5eb1166f --- /dev/null +++ b/beava-js/packages/beava-node/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["test/**/*.test.ts"], + hookTimeout: 120_000, + testTimeout: 30_000, + }, +}); diff --git a/beava-js/pnpm-lock.yaml b/beava-js/pnpm-lock.yaml new file mode 100644 index 00000000..79587af6 --- /dev/null +++ b/beava-js/pnpm-lock.yaml @@ -0,0 +1,1816 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + prettier: + specifier: ^3.7.4 + version: 3.7.4 + turbo: + specifier: ^2.9.12 + version: 2.9.12 + typescript: + specifier: 5.9.2 + version: 5.9.2 + + packages/beava-node: + dependencies: + zod: + specifier: ^3.24.2 + version: 3.25.76 + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@types/node': + specifier: ^22.15.3 + version: 22.15.3 + eslint: + specifier: ^9.39.1 + version: 9.39.1 + eslint-config-prettier: + specifier: ^10.1.1 + version: 10.1.1(eslint@9.39.1) + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: 5.9.2 + version: 5.9.2 + vitest: + specifier: ^3.1.3 + version: 3.2.4(@types/node@22.15.3) + +packages: + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + 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/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.1': + resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@turbo/darwin-64@2.9.12': + resolution: {integrity: sha512-eu3eFRmE9NjgZ0wPdRJ44l+LGSeIky+tz5ZQd8zQkw/Yqi+BM7wq+8nbabeoiVUcICi/IZweMOKl/MCmkrd1+g==} + cpu: [x64] + os: [darwin] + + '@turbo/darwin-arm64@2.9.12': + resolution: {integrity: sha512-RUkAE404z/J8NsyrUosMcBaXT6M4bRFxTQrmkDQBLQVXaC8Jl0e9bMvYDSX0GW7Ffm2m3j9y7RXgR1foeUAM9w==} + cpu: [arm64] + os: [darwin] + + '@turbo/linux-64@2.9.12': + resolution: {integrity: sha512-InIUtH7cw/vqXNX1Gr7QgWfmw3ct08pV5CpfdEOR48z2u2rzdmpIuk00B/Q2xCb0PMWtKgiMQynfuphmEuUyTQ==} + cpu: [x64] + os: [linux] + + '@turbo/linux-arm64@2.9.12': + resolution: {integrity: sha512-lC6nD//Xh67fmJM0LKaLsg74Wry0aYrgMklpiNgCbUaMdPIOqj0A00iri3NU7Lb7pZHx8ViisgpeDKlpSgFUCA==} + cpu: [arm64] + os: [linux] + + '@turbo/windows-64@2.9.12': + resolution: {integrity: sha512-conYri8VUl72JOdYnLDPYwzqbPcY5ECoHmo9FWoKznemhaAIilj4maHqs9Uar0aKfNoZIULniy+6iWaLtLO34A==} + cpu: [x64] + os: [win32] + + '@turbo/windows-arm64@2.9.12': + resolution: {integrity: sha512-XoR4bsg62/L/esRVcmoMESEiNZ36+YmyjYGLpoqk8nwMgXzzVjNOgX0lRSz5w/U/ajLGv3nhMsS0Q2QOdvp2AQ==} + cpu: [arm64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@22.15.3': + resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.1: + resolution: {integrity: sha512-4EQQr6wXwS+ZJSzaR5ZCrYgLxqvUjdXctaEtBqHcbkW944B1NQyO4qpdHQbXBONfwxXdkAY81HH4+LUfrg+zPw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.1: + resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} + engines: {node: '>=14'} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + rimraf@6.1.3: + resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==} + engines: {node: 20 || >=22} + hasBin: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + turbo@2.9.12: + resolution: {integrity: sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw==} + hasBin: true + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typescript@5.9.2: + resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.3: + resolution: {integrity: sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==} + 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@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + 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'} + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.1': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@turbo/darwin-64@2.9.12': + optional: true + + '@turbo/darwin-arm64@2.9.12': + optional: true + + '@turbo/linux-64@2.9.12': + optional: true + + '@turbo/linux-arm64@2.9.12': + optional: true + + '@turbo/windows-64@2.9.12': + optional: true + + '@turbo/windows-arm64@2.9.12': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@22.15.3': + dependencies: + undici-types: 6.21.0 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.3(@types/node@22.15.3))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.3(@types/node@22.15.3) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + assertion-error@2.0.1: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.4 + + cac@6.7.14: {} + + callsites@3.1.0: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@2.1.3: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + concat-map@0.0.1: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + deep-is@0.1.4: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.1(eslint@9.39.1): + dependencies: + eslint: 9.39.1 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.39.1 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + fsevents@2.3.3: + optional: true + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + has-flag@4.0.0: {} + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + isexe@2.0.0: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@3.2.1: {} + + lru-cache@11.3.6: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.6 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minipass@7.1.3: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-json-from-dist@1.0.1: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.6 + minipass: 7.1.3 + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.14: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.7.4: {} + + punycode@2.3.1: {} + + resolve-from@4.0.0: {} + + rimraf@6.1.3: + dependencies: + glob: 13.0.6 + package-json-from-dist: 1.0.1 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + turbo@2.9.12: + optionalDependencies: + '@turbo/darwin-64': 2.9.12 + '@turbo/darwin-arm64': 2.9.12 + '@turbo/linux-64': 2.9.12 + '@turbo/linux-arm64': 2.9.12 + '@turbo/windows-64': 2.9.12 + '@turbo/windows-arm64': 2.9.12 + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typescript@5.9.2: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@3.2.4(@types/node@22.15.3): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.3(@types/node@22.15.3) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.3(@types/node@22.15.3): + dependencies: + esbuild: 0.27.7 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.14 + rollup: 4.60.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 22.15.3 + fsevents: 2.3.3 + + vitest@3.2.4(@types/node@22.15.3): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.3(@types/node@22.15.3)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.3(@types/node@22.15.3) + vite-node: 3.2.4(@types/node@22.15.3) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 22.15.3 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} diff --git a/beava-js/pnpm-workspace.yaml b/beava-js/pnpm-workspace.yaml new file mode 100644 index 00000000..fae81352 --- /dev/null +++ b/beava-js/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - "packages/beava-node" diff --git a/beava-js/tsconfig.node-library.json b/beava-js/tsconfig.node-library.json new file mode 100644 index 00000000..9dd1dc4d --- /dev/null +++ b/beava-js/tsconfig.node-library.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "NodeNext", + "moduleDetection": "force", + "moduleResolution": "NodeNext", + "noEmit": false, + "noUncheckedIndexedAccess": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022" + } +} diff --git a/beava-js/turbo.json b/beava-js/turbo.json new file mode 100644 index 00000000..cb422397 --- /dev/null +++ b/beava-js/turbo.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://turborepo.dev/schema.json", + "tasks": { + "build": { + "dependsOn": ["^build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "lint": { + "dependsOn": ["^lint"] + }, + "check-types": { + "dependsOn": ["^check-types"] + }, + "test": { + "outputs": [], + "env": ["BEAVA_INTEGRATION", "BEAVA_REPO_ROOT"] + } + } +} From 4502576eb0a3a53c7220fb60d5f2200290c00b88 Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Thu, 14 May 2026 21:53:01 -0400 Subject: [PATCH 2/4] feat(beava-js): add @beava/client and wire workspace CI. Ship the browser package in the pnpm workspace, expand beava-js gitignore for local tool caches, add missing eslint deps, and align check.sh plus GitHub workflows and SOURCE-OF-TRUTH with the JS monorepo. --- .github/scripts/check.sh | 32 ++- .github/workflows/ci.yml | 31 +++ .github/workflows/pr.yml | 25 ++ SOURCE-OF-TRUTH.md | 5 +- beava-js/.gitignore | 9 + beava-js/README.md | 2 +- beava-js/packages/beava-client/README.md | 14 + .../packages/beava-client/eslint.config.mjs | 12 + beava-js/packages/beava-client/package.json | 49 ++++ beava-js/packages/beava-client/src/index.ts | 37 +++ beava-js/packages/beava-client/tsconfig.json | 14 + beava-js/packages/beava-node/package.json | 1 + beava-js/pnpm-lock.yaml | 239 ++++++++++++++++++ beava-js/pnpm-workspace.yaml | 1 + 14 files changed, 461 insertions(+), 10 deletions(-) create mode 100644 beava-js/packages/beava-client/README.md create mode 100644 beava-js/packages/beava-client/eslint.config.mjs create mode 100644 beava-js/packages/beava-client/package.json create mode 100644 beava-js/packages/beava-client/src/index.ts create mode 100644 beava-js/packages/beava-client/tsconfig.json diff --git a/.github/scripts/check.sh b/.github/scripts/check.sh index dde3fed1..8d11a049 100755 --- a/.github/scripts/check.sh +++ b/.github/scripts/check.sh @@ -1,16 +1,20 @@ #!/usr/bin/env bash # Local pre-PR check — runs the same gates CI runs (cargo fmt + clippy + -# tests + Python tests) and writes a one-line PASS/FAIL summary you can -# paste into the PR description as proof. +# tests + Python tests + beava-js Vitest) and writes a one-line PASS/FAIL +# summary you can paste into the PR description as proof. # # Usage: # bash .github/scripts/check.sh # run everything, print summary # bash .github/scripts/check.sh --fast # skip cargo test (~10× faster) # bash .github/scripts/check.sh --rust # rust gates only (cargo fmt/clippy/test) # bash .github/scripts/check.sh --python # python gates only (ruff + mypy + pytest) +# bash .github/scripts/check.sh --js # beava-js only (pnpm install + turbo lint/check-types/test) # bash .github/scripts/check.sh --output= # path for full log (default ~/.beava-check.log) # -# --rust and --python are mutually exclusive; pass neither to run both. +# When target/debug/beava exists, beava-js Vitest also runs HTTP integration tests (BEAVA_INTEGRATION=1). +# Otherwise three integration tests are skipped (same as CI after cargo build). +# +# --rust, --python, and --js are mutually exclusive; pass none to run rust + python + beava-js. # # Exit code: 0 if all checks pass, non-zero otherwise. set -uo pipefail @@ -18,12 +22,14 @@ set -uo pipefail FAST=0 RUN_RUST=1 RUN_PYTHON=1 +RUN_JS=1 OUT="$HOME/.beava-check.log" for arg in "$@"; do case "$arg" in --fast) FAST=1 ;; - --rust) RUN_PYTHON=0 ;; - --python) RUN_RUST=0 ;; + --rust) RUN_PYTHON=0; RUN_JS=0 ;; + --python) RUN_RUST=0; RUN_JS=0 ;; + --js) RUN_RUST=0; RUN_PYTHON=0; RUN_JS=1 ;; --output=*) OUT="${arg#--output=}" ;; -h|--help) sed -n '2,/^set/p' "$0" | sed 's/^# \{0,1\}//; /^set /d' @@ -32,8 +38,8 @@ for arg in "$@"; do esac done -if [[ "$RUN_RUST" -eq 0 && "$RUN_PYTHON" -eq 0 ]]; then - echo "error: --rust and --python are mutually exclusive" >&2 +if [[ "$RUN_RUST" -eq 0 && "$RUN_PYTHON" -eq 0 && "$RUN_JS" -eq 0 ]]; then + echo "error: no check targets enabled (use --rust, --python, or --js)" >&2 exit 2 fi @@ -100,6 +106,18 @@ if [[ "$RUN_PYTHON" -eq 1 && -d python && -f python/pyproject.toml ]]; then bash -c 'cd python && python -m pytest -q --no-header' fi +if [[ "$RUN_JS" -eq 1 && -f "$REPO_ROOT/beava-js/package.json" ]]; then + js_cmd='cd beava-js && (command -v corepack >/dev/null 2>&1 && corepack enable pnpm || true) && pnpm install && pnpm exec turbo run lint check-types test' + if [[ -f "$REPO_ROOT/target/debug/beava" || -f "$REPO_ROOT/target/debug/beava.exe" ]]; then + run "pnpm turbo lint/check-types/test (beava-js, +HTTP integration)" \ + env BEAVA_INTEGRATION=1 BEAVA_REPO_ROOT="$REPO_ROOT" bash -c "$js_cmd" + else + printf '\n(note: target/debug/beava missing; 3 HTTP integration Vitest tests skipped — run: cargo build --bin beava)\n' | tee -a "$OUT" >&2 + run "pnpm turbo lint/check-types/test (beava-js, unit Vitest only)" \ + bash -c "$js_cmd" + fi +fi + # Tail-of-log + summary. echo echo "─── Summary ────────────────────────────────────────────────" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 087ad92e..6bfa171d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,3 +114,34 @@ jobs: - name: Run pytest (v0 acceptance suite via pyproject testpaths) working-directory: python run: python -m pytest -q --timeout=60 --no-header + + beava-js: + name: beava-js (lint, types, Vitest + HTTP integration) + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + with: + key: beava-js-${{ runner.os }} + + - name: Build beava binary (Vitest integration harness) + run: cargo build --bin beava + + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Enable pnpm + run: corepack enable pnpm + + - name: pnpm install + turbo (lint, check-types, test) + working-directory: beava-js + env: + BEAVA_INTEGRATION: "1" + BEAVA_REPO_ROOT: ${{ github.workspace }} + run: | + pnpm install + pnpm exec turbo run lint check-types test diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 323c2175..9fc7ae59 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -131,6 +131,23 @@ jobs: --cov-report=term-missing:skip-covered \ 2>&1 | tee /tmp/python-test-output.txt + - uses: actions/setup-node@v4 + with: + node-version: "22" + + - name: Enable pnpm + run: corepack enable pnpm + + - name: beava-js (lint, types, Vitest + integration) + working-directory: beava-js + env: + BEAVA_INTEGRATION: "1" + BEAVA_REPO_ROOT: ${{ github.workspace }} + id: beava_js_tests + run: | + pnpm install + pnpm exec turbo run lint check-types test 2>&1 | tee /tmp/beava-js-test-output.txt + - name: Add Python summary if: always() run: | @@ -149,6 +166,14 @@ jobs: || echo "No coverage block captured" >> "$GITHUB_STEP_SUMMARY" echo '```' >> "$GITHUB_STEP_SUMMARY" + - name: Add beava-js summary + if: always() + run: | + echo "## beava-js Test Results" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + tail -15 /tmp/beava-js-test-output.txt >> "$GITHUB_STEP_SUMMARY" 2>/dev/null || echo "No beava-js output captured" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + coverage-rust: name: Rust coverage (informational) runs-on: ubuntu-latest diff --git a/SOURCE-OF-TRUTH.md b/SOURCE-OF-TRUTH.md index 36952766..4ce632ae 100644 --- a/SOURCE-OF-TRUTH.md +++ b/SOURCE-OF-TRUTH.md @@ -8,6 +8,7 @@ left column wins; the right column is downstream and must be regenerated. | Domain | Canonical source | Downstream | |--------|-----------------|------------| | Python SDK API | `python/beava/__init__.py` (`__all__`) + `python/beava/_*.py` | `beava-website/project/sdk/python/*/index.html` | +| TypeScript / npm HTTP SDK (`@beava/node`, `@beava/client`) | `beava-js/packages/beava-node/src/*.ts` + `wire-schemas.ts` | npm package READMEs; wire parity with Python `HttpTransport` | | Server CLI flags | `crates/beava-server/src/cli.rs` | `beava-website/project/sdk/server/index.html` | | Server config (env vars, YAML) | `crates/beava-server/src/{config,wal_config,main}.rs` + `beava.example.yaml` | `beava-website/project/sdk/server/index.html` | | Wire format (TCP frames + opcodes) | `crates/beava-core/src/wire.rs` | `beava-website/project/sdk/http/wire-spec/index.html` | @@ -22,7 +23,7 @@ left column wins; the right column is downstream and must be regenerated. | Surface | Canonical source | Notes | |---------|------------------|-------| | SDK sidebar nav | `beava-website/project/js/sdk/SdkSidebar.jsx` (`SDK_NAV` const) | Single source for all 12 SDK pages | -| SDK shell CSS | `beava-website/project/styles/sdk-shell.css` | Hero CSS stays inline per page | +| SDK shell CSS | `beava-website/project/styles/sdk-shell.css` (bundled via `styles/beava.entry.css` → `project/styles/beava.css`) | Hero CSS stays inline per page | | Site header / footer | `beava-website/project/js/_shared/{SiteHeader,SiteFooter}.jsx` | Mounted via Babel on every page | | LLM agent text dump | regenerated by `npm run build:llms` | `beava-website/project/sdk/llms.txt` (index) + `llms-full.txt` (concat) | | Pagefind search index | regenerated by `npm run build:search` | `beava-website/project/_pagefind/` | @@ -57,7 +58,7 @@ cd beava-website && npm run build:search The 12 SDK pages under `beava-website/project/sdk/` are hand-written HTML (~6,500 LOC total). They mount shared chrome via React+Babel (SiteHeader / -SiteFooter / SdkSidebar / SdkTOC / SdkPager) and load `sdk-shell.css` for +SiteFooter / SdkSidebar / SdkTOC / SdkPager) and load the compiled `beava.css` (includes sdk-shell rules) for prose styling. Authoring is 80% prose + 20% chrome boilerplate. A markdown-based pipeline was considered for this PR. **Findings:** diff --git a/beava-js/.gitignore b/beava-js/.gitignore index 22478255..0dee86f3 100644 --- a/beava-js/.gitignore +++ b/beava-js/.gitignore @@ -26,6 +26,15 @@ npm-debug.log* yarn-debug.log* yarn-error.log* +# pnpm +.pnpm-debug.log* + +# TypeScript incremental (if enabled in a package tsconfig) +*.tsbuildinfo + +# ESLint +.eslintcache + # Misc .DS_Store *.pem diff --git a/beava-js/README.md b/beava-js/README.md index d06e8bdc..f952b11a 100644 --- a/beava-js/README.md +++ b/beava-js/README.md @@ -9,7 +9,7 @@ | `packages/beava-node` | `@beava/node` | `createBeavaClient`, Zod wire schemas, Vitest unit + optional HTTP integration tests | | `packages/beava-client` | `@beava/client` | Re-exports `@beava/node` for app bundles that want a browser-scoped package name | -Workspace members are defined in **`pnpm-workspace.yaml`** (currently `packages/beava-node`; add `packages/beava-client` when that package lands). Library packages extend **`tsconfig.node-library.json`** at this directory root (no separate TypeScript config package). +Workspace members are defined in **`pnpm-workspace.yaml`** (`packages/beava-node`, `packages/beava-client`). Library packages extend **`tsconfig.node-library.json`** at this directory root (no separate TypeScript config package). ## Prerequisites diff --git a/beava-js/packages/beava-client/README.md b/beava-js/packages/beava-client/README.md new file mode 100644 index 00000000..748a68d6 --- /dev/null +++ b/beava-js/packages/beava-client/README.md @@ -0,0 +1,14 @@ +# @beava/client + +Browser-oriented package name for the same Beava HTTP client as [`@beava/node`](https://www.npmjs.com/package/@beava/node) (global `fetch`, no Node-only APIs in the client path). Depends on `@beava/node`. + +```ts +import { createBeavaClient } from "@beava/client"; + +const client = createBeavaClient({ baseUrl: "https://api.example.com" }); +await client.ping(); +``` + +## License + +Apache-2.0 diff --git a/beava-js/packages/beava-client/eslint.config.mjs b/beava-js/packages/beava-client/eslint.config.mjs new file mode 100644 index 00000000..266513ed --- /dev/null +++ b/beava-js/packages/beava-client/eslint.config.mjs @@ -0,0 +1,12 @@ +import eslint from "@eslint/js"; +import eslintConfigPrettier from "eslint-config-prettier"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + eslintConfigPrettier, + ...tseslint.configs.recommended, + { + ignores: ["dist/**"], + }, +); diff --git a/beava-js/packages/beava-client/package.json b/beava-js/packages/beava-client/package.json new file mode 100644 index 00000000..f396a8e9 --- /dev/null +++ b/beava-js/packages/beava-client/package.json @@ -0,0 +1,49 @@ +{ + "name": "@beava/client", + "version": "0.1.0", + "description": "Beava feature server HTTP client for browsers (async fetch).", + "type": "module", + "sideEffects": false, + "files": ["dist", "README.md"], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "rimraf dist && tsc", + "prepack": "pnpm run build", + "lint": "eslint . --max-warnings 0", + "check-types": "tsc --noEmit" + }, + "dependencies": { + "@beava/node": "workspace:^0.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.39.1", + "@types/node": "^22.15.3", + "eslint": "^9.39.1", + "eslint-config-prettier": "^10.1.1", + "rimraf": "^6.0.1", + "typescript": "5.9.2", + "typescript-eslint": "^8.49.0" + }, + "homepage": "https://beava.dev", + "bugs": { + "url": "https://github.com/beava-dev/beava/issues" + }, + "keywords": ["beava", "features", "fetch", "browser", "http"], + "license": "Apache-2.0", + "repository": { + "type": "git", + "url": "https://github.com/beava-dev/beava.git", + "directory": "beava-js/packages/beava-client" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/beava-js/packages/beava-client/src/index.ts b/beava-js/packages/beava-client/src/index.ts new file mode 100644 index 00000000..7bef3133 --- /dev/null +++ b/beava-js/packages/beava-client/src/index.ts @@ -0,0 +1,37 @@ +/** + * Browser-oriented entry: same implementation as `@beava/node` (global `fetch` only). + * Depend on `@beava/client` in front-end apps; depend on `@beava/node` in Node services. + */ +export { + BeavaError, + BeavaResponseValidationError, + createBeavaClient, + batchGetEntrySchema, + batchGetRequestSchema, + batchGetResponseSchema, + beavaErrorEnvelopeSchema, + beavaWireErrorBodySchema, + entityKeySchema, + getRequestSchema, + jsonObjectSchema, + jsonValueSchema, + pingResponseSchema, + pushRequestSchema, + pushResponseSchema, + registerRequestSchema, + registerResponseSchema, + type BeavaClient, + type BeavaClientOptions, + type BeavaWireErrorBody, + type BatchGetEntry, + type BatchGetRequest, + type EntityKey, + type GetRequest, + type JsonObject, + type JsonValue, + type PingResponse, + type PushRequest, + type PushResponse, + type RegisterRequest, + type RegisterResponse, +} from "@beava/node"; diff --git a/beava-js/packages/beava-client/tsconfig.json b/beava-js/packages/beava-client/tsconfig.json new file mode 100644 index 00000000..88a061c2 --- /dev/null +++ b/beava-js/packages/beava-client/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.node-library.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src" + }, + "include": [ + "src/**/*.ts" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/beava-js/packages/beava-node/package.json b/beava-js/packages/beava-node/package.json index cfcf4d72..c2c1059f 100644 --- a/beava-js/packages/beava-node/package.json +++ b/beava-js/packages/beava-node/package.json @@ -34,6 +34,7 @@ "eslint-config-prettier": "^10.1.1", "rimraf": "^6.0.1", "typescript": "5.9.2", + "typescript-eslint": "^8.49.0", "vitest": "^3.1.3" }, "homepage": "https://beava.dev", diff --git a/beava-js/pnpm-lock.yaml b/beava-js/pnpm-lock.yaml index 79587af6..49db3201 100644 --- a/beava-js/pnpm-lock.yaml +++ b/beava-js/pnpm-lock.yaml @@ -18,6 +18,34 @@ importers: specifier: 5.9.2 version: 5.9.2 + packages/beava-client: + dependencies: + '@beava/node': + specifier: workspace:^0.1.0 + version: link:../beava-node + devDependencies: + '@eslint/js': + specifier: ^9.39.1 + version: 9.39.1 + '@types/node': + specifier: ^22.15.3 + version: 22.15.3 + eslint: + specifier: ^9.39.1 + version: 9.39.1 + eslint-config-prettier: + specifier: ^10.1.1 + version: 10.1.1(eslint@9.39.1) + rimraf: + specifier: ^6.0.1 + version: 6.1.3 + typescript: + specifier: 5.9.2 + version: 5.9.2 + typescript-eslint: + specifier: ^8.49.0 + version: 8.59.3(eslint@9.39.1)(typescript@5.9.2) + packages/beava-node: dependencies: zod: @@ -42,6 +70,9 @@ importers: typescript: specifier: 5.9.2 version: 5.9.2 + typescript-eslint: + specifier: ^8.49.0 + version: 8.59.3(eslint@9.39.1)(typescript@5.9.2) vitest: specifier: ^3.1.3 version: 3.2.4(@types/node@22.15.3) @@ -210,6 +241,12 @@ packages: peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@eslint-community/regexpp@4.12.2': resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -431,6 +468,65 @@ packages: '@types/node@22.15.3': resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} + '@typescript-eslint/eslint-plugin@8.59.3': + resolution: {integrity: sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.3 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.3': + resolution: {integrity: sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.3': + resolution: {integrity: sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.3': + resolution: {integrity: sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.3': + resolution: {integrity: sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.3': + resolution: {integrity: sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.3': + resolution: {integrity: sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.3': + resolution: {integrity: sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.3': + resolution: {integrity: sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.3': + resolution: {integrity: sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@vitest/expect@3.2.4': resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} @@ -577,6 +673,10 @@ packages: resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + eslint@9.39.1: resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -672,6 +772,10 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -826,6 +930,11 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} + engines: {node: '>=10'} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -880,6 +989,12 @@ packages: resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} engines: {node: '>=14.0.0'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + turbo@2.9.12: resolution: {integrity: sha512-lCPgus1NuTiBdaITWqzSH/Ff6HVL8HHGBtOXHg1dHRfcshN79XkygSdh0M6g8b0td91ILLG5MTkLOkp5UvyPJw==} hasBin: true @@ -888,6 +1003,13 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} + typescript-eslint@8.59.3: + resolution: {integrity: sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + typescript@5.9.2: resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==} engines: {node: '>=14.17'} @@ -1077,6 +1199,11 @@ snapshots: eslint: 9.39.1 eslint-visitor-keys: 3.4.3 + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.1)': + dependencies: + eslint: 9.39.1 + eslint-visitor-keys: 3.4.3 + '@eslint-community/regexpp@4.12.2': {} '@eslint/config-array@0.21.1': @@ -1239,6 +1366,97 @@ snapshots: dependencies: undici-types: 6.21.0 + '@typescript-eslint/eslint-plugin@8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.1)(typescript@5.9.2))(eslint@9.39.1)(typescript@5.9.2)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/type-utils': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + '@typescript-eslint/utils': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.59.3 + eslint: 9.39.1 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.3(eslint@9.39.1)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.2) + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + eslint: 9.39.1 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.3(typescript@5.9.2)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.2) + '@typescript-eslint/types': 8.59.3 + debug: 4.4.3 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + + '@typescript-eslint/tsconfig-utils@8.59.3(typescript@5.9.2)': + dependencies: + typescript: 5.9.2 + + '@typescript-eslint/type-utils@8.59.3(eslint@9.39.1)(typescript@5.9.2)': + dependencies: + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.2) + '@typescript-eslint/utils': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + debug: 4.4.3 + eslint: 9.39.1 + ts-api-utils: 2.5.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.3': {} + + '@typescript-eslint/typescript-estree@8.59.3(typescript@5.9.2)': + dependencies: + '@typescript-eslint/project-service': 8.59.3(typescript@5.9.2) + '@typescript-eslint/tsconfig-utils': 8.59.3(typescript@5.9.2) + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/visitor-keys': 8.59.3 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.15 + ts-api-utils: 2.5.0(typescript@5.9.2) + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.3(eslint@9.39.1)(typescript@5.9.2)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.1) + '@typescript-eslint/scope-manager': 8.59.3 + '@typescript-eslint/types': 8.59.3 + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.2) + eslint: 9.39.1 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.3': + dependencies: + '@typescript-eslint/types': 8.59.3 + eslint-visitor-keys: 5.0.1 + '@vitest/expect@3.2.4': dependencies: '@types/chai': 5.2.3 @@ -1402,6 +1620,8 @@ snapshots: eslint-visitor-keys@4.2.1: {} + eslint-visitor-keys@5.0.1: {} + eslint@9.39.1: dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1) @@ -1510,6 +1730,8 @@ snapshots: ignore@5.3.2: {} + ignore@7.0.5: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -1666,6 +1888,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.60.3 fsevents: 2.3.3 + semver@7.8.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -1705,6 +1929,10 @@ snapshots: tinyspy@4.0.4: {} + ts-api-utils@2.5.0(typescript@5.9.2): + dependencies: + typescript: 5.9.2 + turbo@2.9.12: optionalDependencies: '@turbo/darwin-64': 2.9.12 @@ -1718,6 +1946,17 @@ snapshots: dependencies: prelude-ls: 1.2.1 + typescript-eslint@8.59.3(eslint@9.39.1)(typescript@5.9.2): + dependencies: + '@typescript-eslint/eslint-plugin': 8.59.3(@typescript-eslint/parser@8.59.3(eslint@9.39.1)(typescript@5.9.2))(eslint@9.39.1)(typescript@5.9.2) + '@typescript-eslint/parser': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + '@typescript-eslint/typescript-estree': 8.59.3(typescript@5.9.2) + '@typescript-eslint/utils': 8.59.3(eslint@9.39.1)(typescript@5.9.2) + eslint: 9.39.1 + typescript: 5.9.2 + transitivePeerDependencies: + - supports-color + typescript@5.9.2: {} undici-types@6.21.0: {} diff --git a/beava-js/pnpm-workspace.yaml b/beava-js/pnpm-workspace.yaml index fae81352..f12ac409 100644 --- a/beava-js/pnpm-workspace.yaml +++ b/beava-js/pnpm-workspace.yaml @@ -1,2 +1,3 @@ packages: - "packages/beava-node" + - "packages/beava-client" From 700cd6390e7b79fc335dcab6088adcdd148a7713 Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Fri, 29 May 2026 21:44:33 -0400 Subject: [PATCH 3/4] docs(beava-js): improve npm package usage docs Expand the package READMEs and add a local Node example so npm users can install, connect, handle errors, and smoke-test the client against a Beava server. --- beava-js/README.md | 105 ++++++++++++++++- beava-js/examples/node-basic.mjs | 58 ++++++++++ beava-js/packages/beava-client/README.md | 75 ++++++++++++- beava-js/packages/beava-node/README.md | 137 ++++++++++++++++++++++- 4 files changed, 360 insertions(+), 15 deletions(-) create mode 100644 beava-js/examples/node-basic.mjs diff --git a/beava-js/README.md b/beava-js/README.md index f952b11a..72b6f05c 100644 --- a/beava-js/README.md +++ b/beava-js/README.md @@ -1,13 +1,106 @@ # beava-js -[Turborepo](https://turbo.build/repo) monorepo for the official Beava TypeScript packages: **`@beava/node`** (server-side HTTP client) and **`@beava/client`** (browser entry that re-exports the same fetch-based API). +[Turborepo](https://turbo.build/repo) workspace for the official Beava TypeScript HTTP clients. + +- **`@beava/node`**: Node.js service and script client. +- **`@beava/client`**: browser-oriented package name that re-exports the same fetch-based API. + +Both packages expose `createBeavaClient`, typed request and response shapes, Beava wire error classes, and Zod schemas for the HTTP data plane. + +## Install + +```sh +npm install @beava/node +# or, in browser bundles: +npm install @beava/client +``` + +Run a local server first: + +```sh +beava +# default HTTP URL: http://127.0.0.1:8080 +``` + +## Example + +```ts +import { BeavaError, createBeavaClient } from "@beava/node"; + +const beava = createBeavaClient({ + baseUrl: "http://127.0.0.1:8080", + timeoutSeconds: 10, +}); + +await beava.ping(); + +await beava.register({ + nodes: [ + { + kind: "event", + name: "Purchase", + schema: { + fields: { + user_id: "str", + amount: "f64", + }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], +}); + +await beava.push({ + event: "Purchase", + data: { + user_id: "alice", + amount: 42.5, + }, +}); +``` + +Read from a feature table that your deployed Beava pipeline has registered: + +```ts +try { + const row = await beava.get({ table: "UserSpend", key: "alice" }); + console.log(row); +} catch (error) { + if (error instanceof BeavaError) { + console.error(error.status, error.code, error.message); + } +} +``` + +A runnable Node example lives at `examples/node-basic.mjs`. After `pnpm install` and `pnpm run build`, run it with: + +```sh +BEAVA_URL=http://127.0.0.1:8080 node examples/node-basic.mjs +``` + +## API Surface + +| Method | Wire route | Notes | +| ------------------------------------- | ----------------- | -------------------------------------- | +| `ping()` | `POST /ping` | Liveness and registry version. | +| `register({ nodes, force, dry_run })` | `POST /register` | Registers event and table descriptors. | +| `push({ event, data })` | `POST /push` | Sends one event payload. | +| `get({ table, key, features })` | `POST /get` | Reads one feature row. | +| `batchGet({ requests })` | `POST /batch_get` | Reads many feature rows. | +| `reset()` | `POST /reset` | Test-mode only state reset. | + +Server error envelopes throw `BeavaError`. Malformed success responses throw `BeavaResponseValidationError`, which usually means the client and server versions disagree about the wire shape. ## Layout -| Path | Package | Role | -|------|---------|------| -| `packages/beava-node` | `@beava/node` | `createBeavaClient`, Zod wire schemas, Vitest unit + optional HTTP integration tests | -| `packages/beava-client` | `@beava/client` | Re-exports `@beava/node` for app bundles that want a browser-scoped package name | +| Path | Package | Role | +| ------------------------- | --------------- | ------------------------------------------------------------------------------------ | +| `packages/beava-node` | `@beava/node` | `createBeavaClient`, Zod wire schemas, Vitest unit + optional HTTP integration tests | +| `packages/beava-client` | `@beava/client` | Re-exports `@beava/node` for app bundles that want a browser-scoped package name | +| `examples/node-basic.mjs` | example | Connects to a running Beava server, registers an event, and pushes one row | Workspace members are defined in **`pnpm-workspace.yaml`** (`packages/beava-node`, `packages/beava-client`). Library packages extend **`tsconfig.node-library.json`** at this directory root (no separate TypeScript config package). @@ -41,7 +134,7 @@ pnpm exec turbo run lint check-types --filter=@beava/client **HTTP integration tests** (real `beava` subprocess, same idea as `python/tests/test_transport_http.py`) run when: -1. The **`beava`** binary exists at **`target/debug/beava`** (repo root: run **`cargo build --bin beava`** from the Beava repo root), and +1. The **`beava`** binary exists at **`target/debug/beava`** (repo root: run **`cargo build --bin beava`** from the Beava repo root), and 2. You set **`BEAVA_INTEGRATION=1`**. Optionally set **`BEAVA_REPO_ROOT`** to the Beava git root if discovery fails. ```sh diff --git a/beava-js/examples/node-basic.mjs b/beava-js/examples/node-basic.mjs new file mode 100644 index 00000000..bc22c7b9 --- /dev/null +++ b/beava-js/examples/node-basic.mjs @@ -0,0 +1,58 @@ +import { + BeavaError, + createBeavaClient, +} from "../packages/beava-node/dist/index.js"; + +const baseUrl = process.env.BEAVA_URL ?? "http://127.0.0.1:8080"; + +const beava = createBeavaClient({ + baseUrl, + timeoutSeconds: 10, +}); + +try { + const ping = await beava.ping(); + console.log("connected", ping); + + await beava.register({ + nodes: [ + { + kind: "event", + name: "Purchase", + schema: { + fields: { + user_id: "str", + amount: "f64", + }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], + }); + + const ack = await beava.push({ + event: "Purchase", + data: { + user_id: "alice", + amount: 42.5, + }, + }); + + console.log("pushed", ack); +} catch (error) { + if (error instanceof BeavaError) { + console.error("beava error", { + status: error.status, + code: error.code, + message: error.message, + path: error.path, + errors: error.errors, + }); + } else { + console.error(error); + } + process.exitCode = 1; +} diff --git a/beava-js/packages/beava-client/README.md b/beava-js/packages/beava-client/README.md index 748a68d6..f40641e9 100644 --- a/beava-js/packages/beava-client/README.md +++ b/beava-js/packages/beava-client/README.md @@ -1,12 +1,79 @@ # @beava/client -Browser-oriented package name for the same Beava HTTP client as [`@beava/node`](https://www.npmjs.com/package/@beava/node) (global `fetch`, no Node-only APIs in the client path). Depends on `@beava/node`. +Browser-oriented package name for the Beava TypeScript HTTP client. It re-exports the same fetch-based API as [`@beava/node`](https://www.npmjs.com/package/@beava/node), but lets front-end apps depend on a browser-scoped package name. + +Use this package when your app talks to a Beava HTTP endpoint from a browser bundle. Use `@beava/node` for Node.js services and scripts. + +## Install + +```sh +npm install @beava/client +# or: pnpm add @beava/client +``` + +## Example ```ts -import { createBeavaClient } from "@beava/client"; +import { BeavaError, createBeavaClient } from "@beava/client"; + +const beava = createBeavaClient({ + baseUrl: "https://features.example.com", + timeoutSeconds: 10, +}); + +await beava.push({ + event: "PageView", + data: { + user_id: "alice", + path: "/pricing", + }, +}); + +try { + const row = await beava.get({ + table: "UserActivity", + key: "alice", + }); + console.log(row); +} catch (error) { + if (error instanceof BeavaError) { + console.error(error.status, error.code, error.message); + } +} +``` -const client = createBeavaClient({ baseUrl: "https://api.example.com" }); -await client.ping(); +## API + +`@beava/client` exports: + +- `createBeavaClient` +- `BeavaError` +- `BeavaResponseValidationError` +- request and response types for `ping`, `register`, `push`, `get`, and `batchGet` +- Zod schemas for validating Beava wire payloads + +The methods are the same as `@beava/node`: + +| Method | Wire route | Notes | +| ------------------------------------- | ----------------- | ------------------------------ | +| `ping()` | `POST /ping` | Liveness and registry version. | +| `register({ nodes, force, dry_run })` | `POST /register` | Registers descriptors. | +| `push({ event, data })` | `POST /push` | Sends one event. | +| `get({ table, key, features })` | `POST /get` | Reads one feature row. | +| `batchGet({ requests })` | `POST /batch_get` | Reads many feature rows. | +| `reset()` | `POST /reset` | Test-mode only state reset. | + +## Browser Notes + +This package does not include auth, retries, or transport fallbacks. Pass any app-specific headers through `createBeavaClient({ headers })`, and keep Beava endpoints behind the same auth and CORS policy you use for the rest of your API. + +```ts +const beava = createBeavaClient({ + baseUrl: "/api/beava", + headers: { + Authorization: `Bearer ${token}`, + }, +}); ``` ## License diff --git a/beava-js/packages/beava-node/README.md b/beava-js/packages/beava-node/README.md index b787685e..0d10ecad 100644 --- a/beava-js/packages/beava-node/README.md +++ b/beava-js/packages/beava-node/README.md @@ -1,15 +1,142 @@ # @beava/node -TypeScript HTTP client for the [Beava](https://beava.dev) feature server data plane (`POST /ping`, `/register`, `/push`, `/get`, `/batch_get`, `/reset`). Uses `fetch` only (Node 18+, Bun, Deno with npm compatibility). +TypeScript HTTP client for the [Beava](https://beava.dev) feature server data plane. It maps directly to the JSON routes exposed by a running `beava` server: `POST /ping`, `/register`, `/push`, `/get`, `/batch_get`, and `/reset`. + +The package is ESM-only, typed, and uses standard `fetch`. It works in Node.js 18.18+ and other runtimes with npm compatibility and global `fetch`. + +## Install + +```sh +npm install @beava/node +# or: pnpm add @beava/node +``` + +Start a local Beava server before running client code: + +```sh +beava +# default HTTP URL: http://127.0.0.1:8080 +``` + +## Quickstart ```ts -import { createBeavaClient } from "@beava/node"; +import { BeavaError, createBeavaClient } from "@beava/node"; + +const beava = createBeavaClient({ + baseUrl: "http://127.0.0.1:8080", + timeoutSeconds: 10, +}); + +await beava.ping(); + +await beava.register({ + nodes: [ + { + kind: "event", + name: "Purchase", + schema: { + fields: { + user_id: "str", + amount: "f64", + }, + optional_fields: [], + }, + dedupe_key: null, + dedupe_window_ms: null, + keep_events_for_ms: null, + }, + ], +}); + +await beava.push({ + event: "Purchase", + data: { + user_id: "alice", + amount: 42.5, + }, +}); +``` + +Read from a feature table that your deployed Beava pipeline has registered: + +```ts +try { + const row = await beava.get({ + table: "UserSpend", + key: "alice", + features: ["total_spend"], + }); + console.log(row); +} catch (error) { + if (error instanceof BeavaError) { + console.error(error.status, error.code, error.message); + } +} +``` + +`register` accepts the same wire descriptor JSON used by `POST /register`. Most applications generate or copy that payload from the Beava pipeline definition they are deploying. + +## API + +```ts +const beava = createBeavaClient({ + baseUrl: "http://127.0.0.1:8080", + timeoutSeconds: 30, + headers: { Authorization: "Bearer ..." }, +}); +``` + +| Method | Wire route | Notes | +| ------------------------------------- | ----------------- | ----------------------------------------------------- | +| `ping()` | `POST /ping` | Returns `{ pong: true, registry_version }`. | +| `register({ nodes, force, dry_run })` | `POST /register` | Registers event and table descriptors. | +| `push({ event, data })` | `POST /push` | Sends one event payload. | +| `get({ table, key, features })` | `POST /get` | Reads one feature row. `features` is optional. | +| `batchGet({ requests })` | `POST /batch_get` | Reads many feature rows and returns `results`. | +| `reset()` | `POST /reset` | Clears state when the server is running in test mode. | + +Every method accepts an optional `AbortSignal` as its final argument: + +```ts +const controller = new AbortController(); +await beava.get({ table: "UserSpend", key: "alice" }, controller.signal); +``` + +## Errors + +Server error envelopes become `BeavaError`: + +```ts +try { + await beava.register({ nodes: [] }); +} catch (error) { + if (error instanceof BeavaError) { + console.log(error.status); // HTTP status + console.log(error.code); // Beava wire error code + console.log(error.path); // wire path, when present + console.log(error.errors); // structured details, when present + } +} +``` + +Malformed success responses throw `BeavaResponseValidationError`. This usually means the client and server versions disagree about the wire shape. + +## Browser Apps + +Use [`@beava/client`](https://www.npmjs.com/package/@beava/client) in browser bundles if you prefer a browser-scoped package name. It re-exports this same fetch-based API. + +## Development + +The source lives in the Beava monorepo under `beava-js/packages/beava-node`. -const client = createBeavaClient({ baseUrl: "http://127.0.0.1:8080" }); -await client.ping(); +```sh +cd beava-js +pnpm install +pnpm exec turbo run lint check-types test --filter=@beava/node ``` -See the [beava-js README](https://github.com/beava-dev/beava/tree/main/beava-js) for tests and integration. +See the [beava-js README](https://github.com/beava-dev/beava/tree/main/beava-js) for workspace commands, integration tests, and npm publish steps. ## License From bf0b051e2ad9035831442c4a9b3639c2753474ce Mon Sep 17 00:00:00 2001 From: Anh Nguyen Date: Fri, 29 May 2026 21:44:37 -0400 Subject: [PATCH 4/4] test(beava-js): use ephemeral admin port in integration helper Avoid local admin sidecar port collisions when the HTTP integration tests spawn temporary Beava servers. --- beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts b/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts index 4a4f7f35..10af21c8 100644 --- a/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts +++ b/beava-js/packages/beava-node/test/helpers/spawn-beava-server.ts @@ -32,6 +32,7 @@ export async function spawnBeavaServer(): Promise { env: { ...process.env, BEAVA_LISTEN_ADDR: "127.0.0.1:0", + BEAVA_ADMIN_ADDR: "127.0.0.1:0", BEAVA_TCP_PORT: "0", BEAVA_WAL_DIR: walDir, BEAVA_SNAPSHOT_DIR: snapDir,