diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a6dd3b5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,40 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20, 22] + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: npm + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Check formatting + run: npm run format:check + + - name: Type-check + run: npm run typecheck + + - name: Build + run: npm run build + + - name: Test + run: npm test diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..05cde4e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,45 @@ +name: Publish + +on: + push: + tags: + - "v*.*.*" + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write # required for npm provenance + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + registry-url: "https://registry.npmjs.org" + + - name: Install dependencies + run: npm ci + + - name: Verify tag matches package version + run: | + TAG="${GITHUB_REF_NAME#v}" + PKG="$(node -p "require('./package.json').version")" + if [ "$TAG" != "$PKG" ]; then + echo "Tag $TAG does not match package.json version $PKG" >&2 + exit 1 + fi + + - name: Build + run: npm run build + + - name: Test + run: npm test + + - name: Publish to npm + run: npm publish --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8517940 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules/ +dist/ +coverage/ +*.log +.DS_Store +.env +.env.local +*.tsbuildinfo +.idea/ +.vscode/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..e88c14f --- /dev/null +++ b/.prettierignore @@ -0,0 +1,4 @@ +dist/ +node_modules/ +coverage/ +CHANGELOG.md diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..fded98e --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": false, + "printWidth": 100 +} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..aa87326 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added + +- Initial release of the Gradient Labs Node.js / TypeScript client. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a871df6 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Gradient Labs + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..804430d --- /dev/null +++ b/README.md @@ -0,0 +1,170 @@ +# Gradient Labs Node.js / TypeScript client + +Official client for the [Gradient Labs API](https://api.gradient-labs.ai). Written +in TypeScript, ships full type declarations and both ESM and CommonJS builds, and +has **zero runtime dependencies** (it uses Node's built-in `fetch` and `crypto`). + +Requires **Node.js 20** or newer. + +## Installation + +```bash +npm install @gradientlabs/client +``` + +## Quick start + +```ts +import { GradientLabs } from "@gradientlabs/client"; + +const client = new GradientLabs({ apiKey: process.env.GRADIENT_LABS_API_KEY! }); + +const conversation = await client.conversations.start({ + id: "ticket-12345", + customer_id: "customer-678", + channel: "web", + assignee_type: "AI Agent", +}); + +console.log(conversation.status); +``` + +The client is organised into resource namespaces, e.g. `client.conversations`, +`client.tools`, `client.procedures`. There are two API key roles: + +- **Integration** — conversation runtime endpoints (`conversations`, + `outboundConversations`, `backOfficeTasks`, `voice`). +- **Management** — configuration endpoints (`tools`, `articles`, `topics`, + `procedures`, `handOffTargets`, `resourceSources`, `resourceTypes`, `secrets`, + `notes`, `terminologySubstitutions`, `trafficGroups`, `ipAddresses`). + +## Configuration + +```ts +const client = new GradientLabs({ + apiKey: "sk_live_...", // required + baseUrl: "https://api.gradient-labs.ai", // optional (this is the default) + webhookSigningKey: "whsec_...", // optional, required to verify webhooks + webhookLeewayMs: 5 * 60 * 1000, // optional, default 5 minutes + timeoutMs: 30_000, // optional per-request timeout + fetch: myFetch, // optional, inject a custom fetch (tests, proxies, instrumentation) +}); +``` + +Every method accepts an optional final argument carrying an `AbortSignal` for +cancellation: + +```ts +const controller = new AbortController(); +const tools = await client.tools.list({ signal: controller.signal }); +``` + +## Error handling + +Non-2xx responses throw an `ApiError`; client misconfiguration throws a +`ConfigurationError`. Both extend `GradientLabsError`. + +```ts +import { ApiError, ErrorCode } from "@gradientlabs/client"; + +try { + await client.conversations.get("missing"); +} catch (err) { + if (err instanceof ApiError) { + console.error(err.statusCode, err.code, err.message); + if (err.code === ErrorCode.NotFound) { + // handle 404 + } + console.error("trace id:", err.traceId); // give this to support + } +} +``` + +The client never retries failed requests — retry policy is left to you. + +## Pagination + +List endpoints that paginate return a `Page` with opaque `next`/`prev` +cursors. Use `listAll()` to iterate every page automatically: + +```ts +for await (const procedure of client.procedures.listAll()) { + console.log(procedure.name); +} + +// or page manually: +const page = await client.procedures.list(); +const next = await client.procedures.list({ cursor: page.pageInfo.next }); +``` + +## Webhook verification + +Construct the client with your `webhookSigningKey`, then verify and parse +incoming requests. Pass the **raw** request body — the signature is computed over +the exact bytes received. + +```ts +import { GradientLabs, InvalidWebhookSignatureError } from "@gradientlabs/client"; + +const client = new GradientLabs({ + apiKey: process.env.GRADIENT_LABS_API_KEY!, + webhookSigningKey: process.env.GL_WEBHOOK_SIGNING_KEY!, +}); + +// Express example (use express.raw() so req.body is the raw payload): +app.post("/webhooks", express.raw({ type: "*/*" }), (req, res) => { + try { + const { event, token } = client.webhooks.parse({ + body: req.body, // Buffer + headers: req.headers, + }); + + switch (event.type) { + case "agent.message": + console.log(event.data.body); + break; + case "conversation.hand_off": + console.log(event.data.reason_code); + break; + // ...other event types + } + + res.sendStatus(200); + } catch (err) { + if (err instanceof InvalidWebhookSignatureError) { + res.sendStatus(401); + } else { + res.sendStatus(500); + } + } +}); +``` + +`event` is a discriminated union on `event.type`, so narrowing gives you a +fully-typed `event.data`. The optional `X-GradientLabs-Token` header is returned +as `token`. + +Supported event types: `agent.message`, `conversation.hand_off`, +`conversation.finished`, `action.execute`, `resource.pull`, +`back-office-task.complete`, `back-office-task.hand-off`, `back-office-task.fail`. + +## Examples + +See the [`examples/`](./examples) directory for runnable examples covering +conversations, tools, articles, procedures, resources, back-office tasks, and a +webhook server. + +## Development + +```bash +npm install +npm run build # dual ESM/CJS + type declarations +npm test # vitest (no network required) +npm run lint +npm run typecheck +npm run format +``` + +## License + +[MIT](./LICENSE) diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..1033637 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,16 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["dist/**", "node_modules/**"], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/no-empty-object-type": "off", + }, + }, +); diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..f4bb37f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,43 @@ +# Examples + +Runnable smoke tests against the real Gradient Labs API. Each example reads your +API key from the `GRADIENT_LABS_API_KEY` environment variable. + +> These examples import the client from `../../src` so they run straight from a +> checkout. In your own project, install the package and import from +> `@gradientlabs/client` instead. + +## Prerequisites + +```bash +npm install +export GRADIENT_LABS_API_KEY="sk_live_..." +``` + +Some examples need a **Management** API key (tools, articles, procedures, +resources); the conversation, back-office-task, and voice examples need an +**Integration** key. + +## Running + +Use [`tsx`](https://github.com/privatenumber/tsx) to run the TypeScript directly: + +```bash +npx tsx examples/conversations/index.ts +npx tsx examples/tools/index.ts +npx tsx examples/articles/index.ts +npx tsx examples/procedures/index.ts +npx tsx examples/resources/index.ts +npx tsx examples/back-office-tasks/index.ts +``` + +### Webhooks + +The webhooks example starts a local HTTP server that verifies and dispatches +incoming webhooks: + +```bash +export GL_WEBHOOK_SIGNING_KEY="whsec_..." +npx tsx examples/webhooks/index.ts +# then point your workspace's webhook URL at http://localhost:3000/ +``` diff --git a/examples/articles/index.ts b/examples/articles/index.ts new file mode 100644 index 0000000..ba11b5a --- /dev/null +++ b/examples/articles/index.ts @@ -0,0 +1,46 @@ +/** + * Articles example: upserts a help article, toggles whether the agent may use + * it, then deletes it. Requires a Management API key. + * + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +async function main(): Promise { + const id = `example-article-${Date.now()}`; + const now = new Date().toISOString(); + + await client.articles.upsert({ + id, + author_id: "author-1", + title: "How to reset your password", + description: "Step-by-step password reset guide.", + body: "1. Go to settings. 2. Click reset password. 3. Follow the email link.", + visibility: "public", + topic_id: "", + status: "published", + data: {}, + created: now, + last_edited: now, + public_url: "https://help.example.com/reset-password", + }); + console.log("Upserted article:", id); + + await client.articles.setUsageStatus(id, { usage_status: "on" }); + console.log("Enabled article for the agent"); + + await client.articles.delete(id); + console.log("Deleted article"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/back-office-tasks/index.ts b/examples/back-office-tasks/index.ts new file mode 100644 index 0000000..653c96f --- /dev/null +++ b/examples/back-office-tasks/index.ts @@ -0,0 +1,38 @@ +/** + * Back-office tasks example: creates a back-office task and reads its status. + * Requires an Integration API key and a configured back-office agent. + * + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +// The ID of the back-office agent to run the task against, e.g. "boagent_12345". +// Replace with one of your configured agents. +const agentId = "boagent_12345"; + +async function main(): Promise { + const id = `example-task-${Date.now()}`; + + const task = await client.backOfficeTasks.create({ + id, + agent_id: agentId, + input: { order_id: "order-123", reason: "refund_request" }, + metadata: { source: "nodejs-example" }, + }); + console.log("Created back-office task:", task.id, "status:", task.status ?? "(pending)"); + + const fetched = await client.backOfficeTasks.get(id); + console.log("Read task, status:", fetched.status ?? "(pending)"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/conversations/index.ts b/examples/conversations/index.ts new file mode 100644 index 0000000..0cbb9c4 --- /dev/null +++ b/examples/conversations/index.ts @@ -0,0 +1,47 @@ +/** + * Conversations example: starts a conversation, sends a customer message, reads + * it back, then finishes it. + * + * In your own project, import from the published package: + * import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +async function main(): Promise { + const id = `example-${Date.now()}`; + + const conversation = await client.conversations.start({ + id, + customer_id: "customer-123", + channel: "web", + assignee_type: "AI Agent", + metadata: { source: "nodejs-example" }, + }); + console.log("Started conversation:", conversation.id, conversation.status); + + await client.conversations.addMessage(id, { + id: `msg-${Date.now()}`, + body: "Hi, I need help with my order.", + participant_id: "customer-123", + participant_type: "Customer", + }); + console.log("Sent customer message"); + + const fetched = await client.conversations.get(id); + console.log("Read conversation, latest intent:", fetched.latest_intent || "(none yet)"); + + await client.conversations.finish(id, { reason: "example complete" }); + console.log("Finished conversation"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/procedures/index.ts b/examples/procedures/index.ts new file mode 100644 index 0000000..f873a28 --- /dev/null +++ b/examples/procedures/index.ts @@ -0,0 +1,41 @@ +/** + * Procedures example: lists procedures (auto-following pagination), reads one, + * and lists its versions. Requires a Management API key. + * + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +async function main(): Promise { + let count = 0; + let firstId: string | undefined; + + // listAll transparently follows pagination cursors. + for await (const procedure of client.procedures.listAll()) { + if (count === 0) { + firstId = procedure.id; + } + count += 1; + } + console.log(`Found ${count} procedure(s).`); + + if (firstId) { + const procedure = await client.procedures.get(firstId); + console.log("First procedure:", procedure.name, `(${procedure.status})`); + + const versions = await client.procedures.listVersions(firstId); + console.log(`It has ${versions.length} version(s).`); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/resources/index.ts b/examples/resources/index.ts new file mode 100644 index 0000000..8a051c1 --- /dev/null +++ b/examples/resources/index.ts @@ -0,0 +1,54 @@ +/** + * Resources example: the full ResourceType → ResourceSource loop. Creates a + * resource source, infers its schema from example payloads, creates a resource + * type backed by that source, then cleans both up. Requires a Management API key. + * + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +async function main(): Promise { + const source = await client.resourceSources.create({ + display_name: `Example source ${Date.now()}`, + description: "Customer profile lookup, created by the Node.js example.", + source_type: "http", + http_config: { + method: "GET", + url_template: "https://example.com/customers/${params.customer_id}", + }, + }); + console.log("Created resource source:", source.id); + + await client.resourceSources.updateSchemaByExamples(source.id, { + examples: [{ name: "Ada Lovelace", tier: "premium", lifetime_value: 4200 }], + schema_update_strategy: "replace", + }); + console.log("Inferred schema from examples"); + + const type = await client.resourceTypes.create({ + display_name: `Example type ${Date.now()}`, + description: "Per-customer profile.", + scope: "local", + refresh_strategy: "dynamic", + is_enabled: true, + source_config: { source_id: source.id, attributes: [], cache: "1h" }, + }); + console.log("Created resource type:", type.id); + + // Clean up. + await client.resourceTypes.delete(type.id); + await client.resourceSources.delete(source.id); + console.log("Cleaned up resource type and source"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/tools/index.ts b/examples/tools/index.ts new file mode 100644 index 0000000..80b411d --- /dev/null +++ b/examples/tools/index.ts @@ -0,0 +1,51 @@ +/** + * Tools example: lists existing tools, creates a simple HTTP tool, reads it + * back, then deletes it. Requires a Management API key. + * + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { GradientLabs } from "../../src/index.js"; + +const apiKey = process.env.GRADIENT_LABS_API_KEY; +if (!apiKey) { + throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); +} + +const client = new GradientLabs({ apiKey }); + +async function main(): Promise { + const tools = await client.tools.list(); + console.log(`You have ${tools.length} tool(s).`); + + const id = `example-tool-${Date.now()}`; + const created = await client.tools.create({ + id, + name: "Example weather lookup", + description: "Looks up the weather for a city. Created by the Node.js example.", + parameters: [ + { + name: "city", + description: "The city to look up the weather for.", + type: "string", + allowed_sources: ["llm"], + required: true, + }, + ], + http: { + method: "GET", + url_template: "https://example.com/weather?city=${params.city}", + }, + }); + console.log("Created tool:", created.id); + + const fetched = await client.tools.get(id); + console.log("Read tool:", fetched.name); + + await client.tools.delete(id); + console.log("Deleted tool"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/examples/webhooks/index.ts b/examples/webhooks/index.ts new file mode 100644 index 0000000..05540ae --- /dev/null +++ b/examples/webhooks/index.ts @@ -0,0 +1,69 @@ +/** + * Webhooks example: a minimal HTTP server that receives Gradient Labs webhooks, + * verifies their signature, and dispatches on the event type. + * + * Run it, then point your workspace's webhook URL at http://localhost:3000/. + * + * Requires GL_WEBHOOK_SIGNING_KEY (the signing key from your workspace). + * In your own project: import { GradientLabs } from "@gradientlabs/client"; + */ +import { createServer } from "node:http"; + +import { + GradientLabs, + InvalidWebhookSignatureError, + UnknownWebhookTypeError, +} from "../../src/index.js"; + +const signingKey = process.env.GL_WEBHOOK_SIGNING_KEY; +if (!signingKey) { + throw new Error("GL_WEBHOOK_SIGNING_KEY environment variable is required"); +} + +// An API key is required to construct the client, even though webhook +// verification itself does not call the API. +const client = new GradientLabs({ + apiKey: process.env.GRADIENT_LABS_API_KEY ?? "unused-for-webhooks", + webhookSigningKey: signingKey, +}); + +const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", (chunk) => chunks.push(chunk as Buffer)); + req.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + try { + const { event, token } = client.webhooks.parse({ body, headers: req.headers }); + console.log(`Received ${event.type} (token present: ${token !== undefined})`); + + switch (event.type) { + case "agent.message": + console.log(" agent says:", event.data.body); + break; + case "conversation.hand_off": + console.log(" handing off:", event.data.reason_code); + break; + case "conversation.finished": + console.log(" conversation finished"); + break; + default: + console.log(" (no specific handler)"); + } + + res.writeHead(200).end("ok"); + } catch (err) { + if (err instanceof InvalidWebhookSignatureError) { + res.writeHead(401).end("invalid signature"); + } else if (err instanceof UnknownWebhookTypeError) { + // Log and acknowledge so Gradient Labs does not retry. + console.warn("unknown webhook type:", err.type); + res.writeHead(200).end("ok"); + } else { + console.error(err); + res.writeHead(500).end("error"); + } + } + }); +}); + +server.listen(3000, () => console.log("Listening for webhooks on http://localhost:3000/")); diff --git a/nodejs_CLIENT_PLAN.md b/nodejs_CLIENT_PLAN.md deleted file mode 100644 index 26ff9aa..0000000 --- a/nodejs_CLIENT_PLAN.md +++ /dev/null @@ -1,531 +0,0 @@ -# Gradient Labs Node.js / TypeScript Client — Implementation Plan - -> Status: **ready for engineering review**. No client code has been written yet. -> Once approved, this file is committed to the new `gradientlabs-ai/nodejs-client` repo and -> `/wearegradient-dev:gl-implement-api-client` builds the client from it. - -## Decisions (locked) - -| Decision | Choice | -|----------|--------| -| Package name | `@gradient-labs/client` (npm) | -| Registry | npm (public) | -| Repo name | `gradientlabs-ai/nodejs-client` | -| Language / build | TypeScript → compiled JS, ships `.d.ts`, **dual ESM + CommonJS** | -| Minimum runtime | Node.js 20 LTS | -| Concurrency model | Promise-based `async`/`await`; cancellation via `AbortSignal` | -| DI / framework package | **Deferred** to a follow-up — core client only for now | -| HTTP layer | Built-in global `fetch` (stable since Node 18, no dependency) | -| Webhook crypto | Built-in `node:crypto` (`createHmac`, `timingSafeEqual`) | -| Runtime dependencies | **Zero** | - ---- - -## 1. Goal - -Provide an idiomatic, dependency-free TypeScript client for the **public** Gradient Labs API -(`https://api.gradient-labs.ai`) that mirrors the design of the canonical Go client: a single -configurable `GradientLabs` client exposing resource-namespaced methods (`client.conversations.start(...)`, -`client.tools.list(...)`), strongly-typed request/response models, a typed error hierarchy, cursor -pagination helpers, and a webhook verifier. It targets Node 20+, ships full type declarations and both -ESM and CommonJS builds, and exposes **only** the public API surface defined in -`platform/openapi/spec.json` (the `publicapi` Integration role and the `publicmanagementapi` Management -role) — no internal services, Encore package prefixes, or implementation detail leak into the public types. - ---- - -## 2. API surface - -The client is organised into **resource namespaces** hung off the root client (idiomatic for TS SDKs, -cf. Stripe/Octokit), e.g. `client.conversations.start(params)`. Method names are `camelCase`. Each -namespace is tagged with the API key role it requires: **Integration** (conversation/runtime endpoints, -`publicapi`) or **Management** (configuration endpoints, `publicmanagementapi`). A single API key carries -a role; calling a Management method with an Integration key yields `permission_denied`. - -> The flat reference Go method name is shown in parentheses so reviewers can cross-check against -> `go-client`. - -### `client.conversations` — Integration -| Method | HTTP | Notes | -|--------|------|-------| -| `start(params)` | `POST /conversations` | returns `Conversation` | -| `get(id)` | `GET /conversations/{id}/read` | canonical read (`ConversationRead`) | -| `addMessage(id, params)` | `POST /conversations/{id}/messages` | | -| `addEvent(id, params)` | `POST /conversations/{id}/events` | typing / delivered / read etc. (`ConversationEventType`) | -| `assign(id, params)` | `PUT /conversations/{id}/assignee` | | -| `rate(id, params)` | `PUT /conversations/{id}/rate` | | -| `cancel(id)` | `PUT /conversations/{id}/cancel` | | -| `finish(id)` | `PUT /conversations/{id}/finish` | | -| `resume(id, params)` | `PUT /conversations/{id}/resume` | | -| `returnAsyncToolResult(id, params)` | `PUT /conversations/{id}/return-async-tool-result` | for async tools | - -> `GET /conversations/{id}` (`ConversationReadDeprecated`) is **deprecated** — not exposed; `get()` uses -> the `/read` variant. - -### `client.outboundConversations` — Integration -| Method | HTTP | -|--------|------| -| `start(params)` | `POST /outbound/conversations` | - -### `client.backOfficeTasks` — Integration -| Method | HTTP | -|--------|------| -| `create(params)` | `POST /back-office-tasks` | -| `get(id)` | `GET /back-office-tasks/{id}/read` | - -### `client.voice` — Integration -| Method | HTTP | -|--------|------| -| `getLatestCallContext(phoneNumber)` | `GET /voice/latest-call-context/{phoneNumber}` | - -### `client.tools` — Management -| Method | HTTP | -|--------|------| -| `list()` | `GET /tools` | -| `create(params)` | `POST /tools` | -| `get(id)` | `GET /tools/{id}` | -| `update(id, params)` | `PUT /tools/{id}` | -| `delete(id)` | `DELETE /tools/{id}` | -| `execute(id, params)` | `POST /tools/{toolID}/execute` | - -### `client.articles` — Management -| Method | HTTP | -|--------|------| -| `upsert(params)` | `POST /articles` | -| `setUsageStatus(id, params)` | `POST /articles/{articleID}/usage-status` | -| `delete(id)` | `DELETE /articles/{id}` | - -### `client.topics` — Management -| Method | HTTP | -|--------|------| -| `list(params?)` | `GET /topics` | -| `upsert(params)` | `POST /topics` (`ArticleTopicUpsert`) | -| `get(id)` | `GET /topic/{id}` | - -### `client.procedures` — Management -| Method | HTTP | -|--------|------| -| `list(params?)` | `GET /procedures` | -| `get(id)` | `GET /procedure/{procedureID}` | -| `setLimit(id, params)` | `POST /procedure/{procedureID}/limit` | -| `listVersions(id, params?)` | `GET /procedures/{procedureID}/versions` | -| `setLiveVersion(id, version)` | `POST /procedures/{procedureID}/versions/{version}/set-live` | -| `unsetLiveVersion(id, version)` | `POST /procedures/{procedureID}/versions/{version}/unset-live` | -| `setGatedVersion(id, version, params)` | `POST /procedures/{procedureID}/versions/{version}/set-gated` | -| `unsetGatedVersion(id, version)` | `POST /procedures/{procedureID}/versions/{version}/unset-gated` | - -### `client.handOffTargets` — Management -| Method | HTTP | -|--------|------| -| `list()` | `GET /hand-off-targets` | -| `upsert(params)` | `POST /hand-off-targets` | -| `delete(params)` | `DELETE /hand-off-targets` | -| `getDefault()` | `GET /hand-off-targets/default` | -| `setDefault(params)` | `PUT /hand-off-targets/default` | - -### `client.resourceSources` — Management -| Method | HTTP | -|--------|------| -| `list(params?)` | `GET /resource-sources` | -| `create(params)` | `POST /resource-sources` | -| `get(id)` | `GET /resource-sources/{id}` | -| `update(id, params)` | `PUT /resource-sources/{id}` | -| `delete(id)` | `DELETE /resource-sources/{id}` | -| `updateSchemaByExamples(id, params)` | `POST /resource-sources/{id}/schema-by-examples` | - -### `client.resourceTypes` — Management -| Method | HTTP | -|--------|------| -| `list(params?)` | `GET /resource-types` | -| `create(params)` | `POST /resource-types` | -| `get(id)` | `GET /resource-types/{id}` | -| `update(id, params)` | `PUT /resource-types/{id}` | -| `delete(id)` | `DELETE /resource-types/{id}` | - -### `client.secrets` — Management -| Method | HTTP | -|--------|------| -| `list()` | `GET /secrets` | -| `write(name, params)` | `PUT /secrets/{name}` | -| `revoke(name)` | `DELETE /secrets/{name}` | - -### `client.notes` — Management -| Method | HTTP | -|--------|------| -| `create(params)` | `POST /notes` | -| `update(id, params)` | `POST /notes/{id}` | -| `setStatus(id, params)` | `POST /notes/{noteID}/status` | -| `delete(id)` | `DELETE /notes/{id}` | - -### `client.terminologySubstitutions` — Management -| Method | HTTP | -|--------|------| -| `list(params?)` | `GET /terminology-substitutions` | -| `create(params)` | `POST /terminology-substitutions` | -| `get(id)` | `GET /terminology-substitutions/{id}` | -| `update(id, params)` | `PUT /terminology-substitutions/{id}` | -| `delete(id)` | `DELETE /terminology-substitutions/{id}` | - -### `client.trafficGroups` — Management -| Method | HTTP | -|--------|------| -| `list()` | `GET /traffic-groups` | -| `create(params)` | `POST /traffic-groups` | -| `update(id, params)` | `PUT /traffic-groups/{id}` | -| `delete(id)` | `DELETE /traffic-groups/{id}` | -| `addTarget(id, params)` | `POST /traffic-groups/{id}/targets` | -| `removeTarget(id, targetId)` | `DELETE /traffic-groups/{id}/targets/{targetId}` | -| `addExclusion(id, params)` | `POST /traffic-groups/{id}/exclusions` | -| `removeExclusion(id, targetId)` | `DELETE /traffic-groups/{id}/exclusions/{targetId}` | - -### `client.ipAddresses` — Management -| Method | HTTP | -|--------|------| -| `list()` | `GET /ip-addresses` | egress IPs to allow-list | - -> `GET /spec.json` (`Spec`) is a meta endpoint that returns the OpenAPI document. **Not exposed** as a -> client method — consumers fetch the docs directly if needed. - -### Enum strategy — open string unions + typed constants - -Every named string enum is modelled as an **open** union so a future server-side value never breaks a -consumer at runtime, while still giving autocomplete on known values: - -```ts -export const Channel = { - Web: "web", - Email: "email", - Voice: "voice", -} as const; -export type Channel = (typeof Channel)[keyof typeof Channel] | (string & {}); -``` - -The `(string & {})` keeps the type open (accepts any string) without collapsing the literal autocomplete. -Each enum gets one such `const` object + type pair in `src/models/enums.ts`. Values are sourced verbatim -from the Go source (paths below), **not** invented: - -| Enum (client name) | Values | Go source | -|--------------------|--------|-----------| -| `ArticleStatus` | `draft`, `published`, `deleted`, `excluded`, `unknown` | `common/article/article.go` | -| `ArticleUsageStatus` | `on`, `off` | `common/article/article.go` | -| `ArticleVisibility` | `public`, `users`, `internal`, `unknown` | `common/article/article.go` | -| `AttachmentType` | `image`, `file` | `common/conversation/attachment_event.go` | -| `Channel` | `web`, `email`, `voice`, `unmapped` | `common/conversation/channel.go` | -| `CustomerSource` | `dixa`, `intercom`, `freshchat`, `freshdesk`, `public-api`, `chat-sdk`, `salesforce`, `zendesk`, `livekit`, `twilio`, `talkdesk`, `intercom-voice`, `livechat`, `web-app`, `gmail`, `file` | `common/conversation/customer_source.go` | -| `ParticipantType` | `Customer`, `Agent`, `AI Agent`, `Bot` | `common/conversation/participant.go` | -| `ConversationEventType` | `assigned`, `cancelled`, `finished`, `resumed`, `internal-note`, `message`, `delivered`, `read`, `rated`, `started`, `typing`, `async-tool-result` | `support-platforms/public-api/events/event.go` | -| `ProcedureStatus` | `unsaved`, `draft`, `live`, `archived` | `common/procedure/procedure.go` | -| `NoteStatus` | `draft`, `live`, `deleted` | `common/note/note.go` | -| `BackOfficeTaskStatus` | `pending`, `in-progress`, `completed`, `failed`, `handed-off` | `back-office/back-office-tasks/types/public-api/task.go` | -| `BackOfficeTaskResultType` | `custom` | `common/back-office/result.go` | -| `AttributeCardinality` | `one`, `many` | `resources/libraries/resource-schema/attribute.go` | -| `AttributeType` | `string`, `date`, `timestamp`, `boolean`, `number`, `array`, `complex` | `resources/libraries/resource-schema/attribute.go` | -| `ResourceSourceRefreshStrategy` | `dynamic`, `static` | `resources/resource-sources/resource-source/source.go` | -| `ResourceSourceScope` | `global`, `local` | `resources/resource-sources/resource-source/source.go` | -| `ResourceSourceType` | `http`, `internal`, `webhook` | `resources/resource-sources/resource-source/source.go` | -| `SchemaUpdateStrategy` | `replace`, `merge` | `resources/resource-sources/update.go` | -| `ResourceTypeRefreshStrategy` | `dynamic`, `static` | `resources/resource-types/resource-type/type.go` | -| `ResourceTypeScope` | `global`, `local` | `resources/resource-types/resource-type/type.go` | -| `SupportPlatform` | `dixa`, `freshchat`, `freshdesk`, `gmail`, `intercom`, `livechat`, `public-api`, `chat-sdk`, `salesforce`, `zendesk`, `livekit`, `twilio`, `talkdesk`, `intercom-voice`, `conversation-synthesizor`, `web-app` | `common/support-platforms/names.go` | -| `BodyEncoding` | `application/json`, `application/x-www-form-urlencoded` | `tools/customer-tools/types/http.go` | -| `ParameterSource` | `llm`, `literal`, `resource` | `tools/tool-registry/types/parameter.go` | -| `ParameterType` | `string`, `string_array`, `integer`, `float`, `boolean`, `date`, `timestamp`, `duration` | `tools/tool-registry/types/parameter.go` | - -> Note from the spec: `ToolParameter.type` currently only accepts `string` server-side; the full -> `ParameterType` set is modelled for forward-compatibility, matching the Go source. - ---- - -## 3. Client configuration - -Instantiated with a single options object (idiomatic TS; avoids Go's functional-options ceremony): - -```ts -import { GradientLabs } from "@gradient-labs/client"; - -const client = new GradientLabs({ - apiKey: process.env.GRADIENT_LABS_API_KEY!, // required - // Optional: - baseUrl: "https://api.gradient-labs.ai", // default - webhookSigningKey: process.env.GL_WEBHOOK_KEY, - webhookLeewayMs: 5 * 60 * 1000, // default 5 min - fetch: customFetch, // override (tests, proxies, instrumentation) - timeoutMs: 30_000, // optional per-client request timeout -}); -``` - -- **`apiKey`** is required and validated at construction (throws `ConfigurationError` if missing/empty). -- **`baseUrl`** defaults to `https://api.gradient-labs.ai`. -- Auth header `Authorization: Bearer ` and `User-Agent: - Gradient-Labs-Node/ (node/)` are set on every request. -- **`fetch`** injection is the equivalent of Go's `WithTransport` — used for tests and instrumentation. -- Every method accepts an optional final `{ signal?: AbortSignal }` argument for cancellation/timeout - (the Node equivalent of Go's `context.Context`). A per-client `timeoutMs` wires an internal - `AbortSignal.timeout()` merged with any caller-supplied signal. - ---- - -## 4. Error handling - -Single base error class with typed subclasses, all extending the native `Error`: - -``` -GradientLabsError // base — anything thrown by the client -├── ConfigurationError // bad/missing config (no network) -├── ApiError // non-2xx HTTP response -│ .statusCode: number -│ .code: ErrorCode // parsed from envelope `code` -│ .message: string // envelope `message` -│ .details: Record -│ .traceId: string | undefined // getter over details.trace_id -└── WebhookVerificationError // signature/leeway failure (respond 401) -``` - -- Parsed from the API error envelope `{ code, message, details }` (see `components.responses.APIError`). -- `traceId` mirrors the Go `TraceID()` helper (reads `details.trace_id`), surfaced for support tickets. -- **Typed error codes** so callers `switch` instead of string-comparing. Sourced from - `encore.dev/beta/errs#ErrCode`: - -```ts -export const ErrorCode = { - NotFound: "not_found", - Unauthenticated: "unauthenticated", - PermissionDenied: "permission_denied", - InvalidArgument: "invalid_argument", - FailedPrecondition: "failed_precondition", - ResourceExhausted: "resource_exhausted", - AlreadyExists: "already_exists", - Unavailable: "unavailable", - DeadlineExceeded: "deadline_exceeded", - Internal: "internal", -} as const; -export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode] | (string & {}); -``` - -Network/abort failures from `fetch` are wrapped in `GradientLabsError` (preserving `cause`) so callers -catch one error family. - ---- - -## 5. Webhook support - -Mirrors the Go client exactly (HMAC-SHA256 over `.`). - -```ts -const result = client.webhooks.parse({ - body: rawBodyString, // RAW request body string/Buffer — not pre-parsed JSON - headers: req.headers, // reads X-GradientLabs-Signature + X-GradientLabs-Token -}); -// result: { event: WebhookEvent; token?: string } -``` - -- **`client.webhooks.verify({ body, signature })`** — lower-level boolean/throw check. Validates the - `X-GradientLabs-Signature` header (`t=,v1=` format), recomputes - `HMAC_SHA256(signingKey, ".")`, compares with `crypto.timingSafeEqual`, and enforces the - configurable leeway (default 5 min). Throws `WebhookVerificationError` on failure. -- **`client.webhooks.parse(...)`** verifies, then decodes `data` into the matching typed event and also - returns the optional **`X-GradientLabs-Token`** passthrough (the per-conversation sensitive token), - exactly like the Go client's `(webhook, token, err)` return. -- A **discriminated union** `WebhookEvent` on the `type` field gives exhaustive `switch` narrowing: - -```ts -type WebhookEvent = - | { type: "agent.message"; data: AgentMessageEvent; id: string; sequenceNumber: number; timestamp: string } - | { type: "conversation.hand_off"; data: ConversationHandOffEvent; /* ... */ } - | { type: "conversation.finished"; data: ConversationFinishedEvent; /* ... */ } - | { type: "action.execute"; data: ActionExecuteEvent; /* ... */ } - | { type: "resource.pull"; data: ResourcePullEvent; /* ... */ }; -``` - -- Currently supported event types: **`agent.message`**, **`conversation.hand_off`**, - **`conversation.finished`**, **`action.execute`**, **`resource.pull`**. Unknown types throw a typed - `UnknownWebhookTypeError` (subclass of `GradientLabsError`) so callers can log + return HTTP 200. -- Helper accepts raw body as `string | Buffer | Uint8Array` (Express/Fastify/raw `http`). Docs will note - the body must be the **raw** payload (signature is computed over bytes, so frameworks must not - re-serialise). - ---- - -## 6. Pagination - -Cursor-based, matching `PaginationInfo` (`next`/`prev` opaque strings; `after`/`before` query params). - -- List endpoints that paginate return a typed `Page`: - -```ts -interface Page { - data: T[]; - pageInfo: { next?: string; prev?: string }; -} -``` - -- Cursors are passed via params: `client.tools.list({ after: page.pageInfo.next })`. -- An **async-iterator** convenience auto-follows `next` so callers can `for await`: - -```ts -for await (const tool of client.tools.listAll()) { /* ... */ } -``` - - `listAll()` is generated only for genuinely paginated list endpoints; small fixed lists (e.g. - hand-off targets, IP addresses, secrets) return a plain array as the Go client does. The implementer - will confirm per-endpoint which responses carry `PaginationInfo` from the spec before adding - `listAll()`. - ---- - -## 7. Repo structure - -``` -nodejs-client/ -├── src/ -│ ├── index.ts # public barrel: GradientLabs, types, errors, webhooks -│ ├── client.ts # GradientLabs root client + config + namespace wiring -│ ├── internal/ -│ │ ├── http.ts # fetch wrapper: auth, UA, JSON, error mapping, AbortSignal -│ │ ├── pagination.ts # Page + async-iterator helper -│ │ └── user-agent.ts # UA string builder -│ ├── resources/ # one file per namespace -│ │ ├── conversations.ts -│ │ ├── outbound-conversations.ts -│ │ ├── back-office-tasks.ts -│ │ ├── voice.ts -│ │ ├── tools.ts -│ │ ├── articles.ts -│ │ ├── topics.ts -│ │ ├── procedures.ts -│ │ ├── hand-off-targets.ts -│ │ ├── resource-sources.ts -│ │ ├── resource-types.ts -│ │ ├── secrets.ts -│ │ ├── notes.ts -│ │ ├── terminology-substitutions.ts -│ │ ├── traffic-groups.ts -│ │ └── ip-addresses.ts -│ ├── models/ # response types + enums (one module per domain + enums.ts) -│ ├── requests/ # *Params request types (co-located or per-domain) -│ ├── webhooks/ -│ │ ├── verifier.ts # WebhookVerifier (HMAC, leeway) -│ │ └── events.ts # WebhookEvent union + event payload types -│ └── errors.ts # GradientLabsError hierarchy + ErrorCode -├── examples/ -│ ├── conversations/ -│ ├── tools/ -│ ├── articles/ -│ ├── webhooks/ -│ ├── procedures/ -│ ├── resources/ -│ └── back-office-tasks/ -├── test/ -│ ├── webhook.test.ts # signature verify (valid/expired/tampered) + token passthrough -│ ├── errors.test.ts # envelope → ApiError mapping, traceId, code constants -│ ├── pagination.test.ts # cursor following / async iterator -│ ├── client.test.ts # config, headers (auth + UA), AbortSignal, base URL -│ └── types.test.ts # round-trip (de)serialization of representative models -├── .github/workflows/ -│ ├── ci.yml # install → lint → typecheck → test (Node 20 + 22 matrix) -│ └── publish.yml # build + npm publish on version tag (provenance) -├── tsconfig.json # base (strict) -├── tsconfig.build.json # emit config -├── tsup.config.ts # dual ESM/CJS bundling + .d.ts -├── package.json -├── README.md -├── LICENSE # MIT (match go-client) -└── nodejs_CLIENT_PLAN.md # this file -``` - ---- - -## 8. Build and dependency plan - -**Runtime dependencies: none.** Node 20's built-in global `fetch`, `AbortSignal`/`AbortSignal.timeout()`, -and `node:crypto` cover HTTP, cancellation, and HMAC — no `axios`/`node-fetch`/`undici` needed. - -**Build tooling (devDependencies only):** -- `typescript` — `strict: true`, `target: ES2022`, `moduleResolution: NodeNext`. -- `tsup` (esbuild-based) — emits **ESM (`.mjs`) + CJS (`.cjs`)** bundles and `.d.ts` from one config. Chosen - over hand-rolled `tsc` dual builds for simplicity and correct `exports` wiring. -- `vitest` — fast TS-native test runner (no separate transpile step). -- `eslint` + `@typescript-eslint` + `prettier` — lint/format. - -**`package.json` essentials:** -```jsonc -{ - "name": "@gradient-labs/client", - "version": "0.1.0", - "type": "module", - "engines": { "node": ">=20" }, - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" - } - }, - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "types": "./dist/index.d.ts", - "files": ["dist"], - "sideEffects": false, - "scripts": { - "build": "tsup", - "test": "vitest run", - "lint": "eslint .", - "typecheck": "tsc --noEmit", - "format": "prettier --write ." - }, - "license": "MIT", - "repository": "github:gradientlabs-ai/nodejs-client" -} -``` - -The package version feeds the `User-Agent` (imported from a generated `version.ts`, kept in sync with -`package.json` at build time — mirrors Go's `version.go`). - ---- - -## 9. Testing plan - -All tests are **offline** (no network); HTTP is exercised by injecting a fake `fetch`. - -| Test file | Verifies | -|-----------|----------| -| `webhook.test.ts` | valid signature passes; tampered body fails; expired (outside leeway) fails; multiple `v1=` signatures; `X-GradientLabs-Token` returned; each event type decodes to correct payload; unknown type throws `UnknownWebhookTypeError`. Uses known key + body + precomputed HMAC fixtures. | -| `errors.test.ts` | non-2xx → `ApiError` with `statusCode`/`code`/`message`/`details`; `traceId` extracted from `details.trace_id`; `ErrorCode` constants match spec; malformed error body still yields a usable `ApiError`. | -| `pagination.test.ts` | `Page` shape; `listAll()` async-iterator follows `next` until exhausted; stops when `next` absent. | -| `client.test.ts` | required-`apiKey` throws `ConfigurationError`; `Authorization` + `User-Agent` headers set; `baseUrl` override respected; `AbortSignal` cancels in-flight request; `timeoutMs` aborts. | -| `types.test.ts` | representative models (Conversation, Tool, Procedure, ResourceType, BackOfficeTask) round-trip through (de)serialization; optional/`omitempty` fields handled. | - -Run with `npm test` (`vitest run`). Coverage reported via vitest's built-in c8. - ---- - -## 10. CI plan - -**`ci.yml`** (push + PR): matrix on Node **20** and **22** → `npm ci` → `npm run lint` → -`npm run typecheck` → `npm run build` → `npm test`. Fails the PR on any step. - -**`publish.yml`** (on `v*` tag): checkout → `npm ci` → `npm run build` → `npm publish --access public` -with **npm provenance** (`id-token: write`), authed via `NPM_TOKEN` org secret. A guard step asserts the -git tag matches `package.json` version before publishing. - ---- - -## 11. Implementation order - -1. **Infrastructure** — `package.json`, `tsconfig*`, `tsup.config.ts`, eslint/prettier; `errors.ts`; - `internal/user-agent.ts` + `version.ts`; `internal/http.ts` (fetch wrapper: auth, UA, JSON body, - error mapping, `AbortSignal`); `client.ts` skeleton with config validation. -2. **One resource group end-to-end** — `conversations` (richest Integration surface): models, request - types, all methods, an example, and tests. Locks the patterns (path/query/body handling, response - decoding) before scaling out. -3. **Pagination + webhooks** — `internal/pagination.ts` (`Page` + `listAll()`); `webhooks/verifier.ts` - + `webhooks/events.ts` with full test suite. These are the highest-risk-to-get-wrong pieces, done - early. -4. **Remaining Integration namespaces** — `outboundConversations`, `backOfficeTasks`, `voice`. -5. **Management namespaces** — `tools`, `articles`, `topics`, `procedures`, `handOffTargets`, - `resourceSources`, `resourceTypes`, `secrets`, `notes`, `terminologySubstitutions`, `trafficGroups`, - `ipAddresses`, plus `models/enums.ts`. -6. **Examples** — one runnable example per group listed in the tree. -7. **Docs + CI** — `README.md` (install, quickstart, auth roles, webhook handling, pagination), wire - `ci.yml` + `publish.yml`, final lint/typecheck/test green. -``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..21708a7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3829 @@ +{ + "name": "@gradientlabs/client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gradientlabs/client", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.7.0", + "@types/node": "^20.14.0", + "eslint": "^9.7.0", + "prettier": "^3.3.0", + "tsup": "^8.2.0", + "typescript": "^5.5.0", + "typescript-eslint": "^8.0.0", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.61.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.61.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", + "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", + "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.12" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", + "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.9", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/snapshot": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", + "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "magic-string": "^0.30.12", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vitest/spy": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", + "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", + "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.1.9", + "loupe": "^3.1.2", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "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" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.4.tgz", + "integrity": "sha512-N2MylSdi48+5N/6S5j+maeHbUSIzzZ5uOcX5Hm4QpV8Dkb1HFjfAKTKX6yNPJQD9AhcT3ifHNB66tWTTJDi11Q==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/semver": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", + "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", + "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.7", + "es-module-lexer": "^1.5.4", + "pathe": "^1.1.2", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-node/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitest": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", + "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "2.1.9", + "@vitest/mocker": "2.1.9", + "@vitest/pretty-format": "^2.1.9", + "@vitest/runner": "2.1.9", + "@vitest/snapshot": "2.1.9", + "@vitest/spy": "2.1.9", + "@vitest/utils": "2.1.9", + "chai": "^5.1.2", + "debug": "^4.3.7", + "expect-type": "^1.1.0", + "magic-string": "^0.30.12", + "pathe": "^1.1.2", + "std-env": "^3.8.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.1", + "tinypool": "^1.0.1", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.1.9", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.1.9", + "@vitest/ui": "2.1.9", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a93354b --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "@gradientlabs/client", + "version": "0.1.0", + "description": "Official Node.js / TypeScript client for the Gradient Labs API", + "type": "module", + "engines": { + "node": ">=20" + }, + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "dist" + ], + "sideEffects": false, + "scripts": { + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint .", + "typecheck": "tsc --noEmit", + "format": "prettier --write .", + "format:check": "prettier --check ." + }, + "keywords": [ + "gradient-labs", + "ai", + "customer-support", + "api", + "client", + "sdk" + ], + "author": "Gradient Labs", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/gradientlabs-ai/nodejs-client.git" + }, + "homepage": "https://github.com/gradientlabs-ai/nodejs-client#readme", + "bugs": { + "url": "https://github.com/gradientlabs-ai/nodejs-client/issues" + }, + "devDependencies": { + "@eslint/js": "^9.7.0", + "@types/node": "^20.14.0", + "eslint": "^9.7.0", + "prettier": "^3.3.0", + "tsup": "^8.2.0", + "typescript": "^5.5.0", + "typescript-eslint": "^8.0.0", + "vitest": "^2.0.0" + } +} diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..ea4582c --- /dev/null +++ b/src/client.ts @@ -0,0 +1,118 @@ +import { ConfigurationError } from "./errors.js"; +import { HttpClient, type FetchLike } from "./internal/http.js"; +import { Articles } from "./resources/articles.js"; +import { BackOfficeTasks } from "./resources/back-office-tasks.js"; +import { Conversations } from "./resources/conversations.js"; +import { HandOffTargets } from "./resources/hand-off-targets.js"; +import { IpAddressesResource } from "./resources/ip-addresses.js"; +import { Notes } from "./resources/notes.js"; +import { OutboundConversations } from "./resources/outbound-conversations.js"; +import { Procedures } from "./resources/procedures.js"; +import { ResourceSources } from "./resources/resource-sources.js"; +import { ResourceTypes } from "./resources/resource-types.js"; +import { Secrets } from "./resources/secrets.js"; +import { TerminologySubstitutions } from "./resources/terminology-substitutions.js"; +import { Tools } from "./resources/tools.js"; +import { Topics } from "./resources/topics.js"; +import { TrafficGroups } from "./resources/traffic-groups.js"; +import { Voice } from "./resources/voice.js"; +import { WebhookVerifier } from "./webhooks/verifier.js"; + +const DEFAULT_BASE_URL = "https://api.gradient-labs.ai"; + +export interface GradientLabsConfig { + /** Your Gradient Labs API key. Required. */ + apiKey: string; + /** Override the base URL. Defaults to https://api.gradient-labs.ai. */ + baseUrl?: string; + /** The webhook signing key, required to verify incoming webhooks. */ + webhookSigningKey?: string; + /** Maximum accepted age of a webhook, in milliseconds. Defaults to 5 minutes. */ + webhookLeewayMs?: number; + /** Inject a custom fetch implementation (tests, proxies, instrumentation). */ + fetch?: FetchLike; + /** Per-request timeout in milliseconds. No timeout by default. */ + timeoutMs?: number; +} + +const defaultFetch: FetchLike = (input, init) => + fetch(input, init as RequestInit) as unknown as ReturnType; + +/** + * The Gradient Labs API client. Construct one with your API key, then access + * endpoints through the resource namespaces (e.g. `client.conversations.start`). + */ +export class GradientLabs { + // Integration role (publicapi) + readonly conversations: Conversations; + readonly outboundConversations: OutboundConversations; + readonly backOfficeTasks: BackOfficeTasks; + readonly voice: Voice; + + // Management role (publicmanagementapi) + readonly tools: Tools; + readonly articles: Articles; + readonly topics: Topics; + readonly procedures: Procedures; + readonly handOffTargets: HandOffTargets; + readonly resourceSources: ResourceSources; + readonly resourceTypes: ResourceTypes; + readonly secrets: Secrets; + readonly notes: Notes; + readonly terminologySubstitutions: TerminologySubstitutions; + readonly trafficGroups: TrafficGroups; + readonly ipAddresses: IpAddressesResource; + + private readonly webhookVerifier?: WebhookVerifier; + + constructor(config: GradientLabsConfig) { + if (!config.apiKey) { + throw new ConfigurationError("apiKey is required"); + } + + const http = new HttpClient({ + baseUrl: config.baseUrl ?? DEFAULT_BASE_URL, + apiKey: config.apiKey, + fetch: config.fetch ?? defaultFetch, + timeoutMs: config.timeoutMs, + }); + + this.conversations = new Conversations(http); + this.outboundConversations = new OutboundConversations(http); + this.backOfficeTasks = new BackOfficeTasks(http); + this.voice = new Voice(http); + + this.tools = new Tools(http); + this.articles = new Articles(http); + this.topics = new Topics(http); + this.procedures = new Procedures(http); + this.handOffTargets = new HandOffTargets(http); + this.resourceSources = new ResourceSources(http); + this.resourceTypes = new ResourceTypes(http); + this.secrets = new Secrets(http); + this.notes = new Notes(http); + this.terminologySubstitutions = new TerminologySubstitutions(http); + this.trafficGroups = new TrafficGroups(http); + this.ipAddresses = new IpAddressesResource(http); + + if (config.webhookSigningKey) { + this.webhookVerifier = new WebhookVerifier({ + signingKey: config.webhookSigningKey, + leewayMs: config.webhookLeewayMs, + }); + } + } + + /** + * The webhook verifier. Throws {@link ConfigurationError} if the client was + * created without a `webhookSigningKey`. + */ + get webhooks(): WebhookVerifier { + if (!this.webhookVerifier) { + throw new ConfigurationError( + "webhookSigningKey is required to verify webhooks; pass it to the GradientLabs constructor", + ); + } + return this.webhookVerifier; + } +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..e165271 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,81 @@ +/** + * Well-known error codes returned by the Gradient Labs API in the `code` field + * of an error response. Callers can switch on {@link ApiError.code} rather than + * comparing message strings. + * + * The union is intentionally open (`| (string & {})`) so that a future + * server-side code never breaks a consumer at compile time. + */ +export const ErrorCode = { + NotFound: "not_found", + Unauthenticated: "unauthenticated", + PermissionDenied: "permission_denied", + InvalidArgument: "invalid_argument", + FailedPrecondition: "failed_precondition", + ResourceExhausted: "resource_exhausted", + AlreadyExists: "already_exists", + Unavailable: "unavailable", + DeadlineExceeded: "deadline_exceeded", + Internal: "internal", +} as const; + +export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode] | (string & {}); + +/** + * Base class for every error thrown by this client. Catch this to handle any + * failure originating from the library. + */ +export class GradientLabsError extends Error { + constructor(message: string, options?: { cause?: unknown }) { + super(message, options); + this.name = "GradientLabsError"; + } +} + +/** + * Thrown when the client is misconfigured (e.g. a missing API key). These are + * raised before any network request is made. + */ +export class ConfigurationError extends GradientLabsError { + constructor(message: string) { + super(message); + this.name = "ConfigurationError"; + } +} + +/** + * Thrown when the API returns a non-2xx response. It carries the HTTP status + * code along with the parsed error envelope (`code`, `message`, `details`). + */ +export class ApiError extends GradientLabsError { + /** HTTP status code of the response. */ + readonly statusCode: number; + + /** Machine-readable error code from the response envelope. */ + readonly code: ErrorCode; + + /** Arbitrary structured details returned with the error. */ + readonly details: Record; + + constructor(args: { + statusCode: number; + code: ErrorCode; + message: string; + details?: Record; + }) { + super(args.message || `unexpected response status: ${args.statusCode}`); + this.name = "ApiError"; + this.statusCode = args.statusCode; + this.code = args.code; + this.details = args.details ?? {}; + } + + /** + * The identifier that can be given to Gradient Labs technical support to + * investigate an error, if present in the error details. + */ + get traceId(): string | undefined { + const traceId = this.details["trace_id"]; + return typeof traceId === "string" ? traceId : undefined; + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cb50c36 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,75 @@ +// Public entry point for @gradientlabs/client. + +export { GradientLabs, type GradientLabsConfig } from "./client.js"; +export { type RequestConfig } from "./request-config.js"; + +// Errors +export { GradientLabsError, ConfigurationError, ApiError, ErrorCode } from "./errors.js"; + +// HTTP injection point +export { type FetchLike } from "./internal/http.js"; + +// Pagination +export { type Page, type PageInfo } from "./internal/pagination.js"; + +// Webhooks +export { + WebhookVerifier, + InvalidWebhookSignatureError, + UnknownWebhookTypeError, + type WebhookVerifierConfig, + type HeadersLike, + type WebhookBody, +} from "./webhooks/verifier.js"; +export { + WebhookType, + type WebhookEvent, + type ParsedWebhook, + type WebhookConversation, + type ActionWebhookConversation, + type ActionWebhookBackOfficeTask, + type AgentMessageEvent, + type ConversationHandOffEvent, + type ConversationFinishedEvent, + type ActionExecuteEvent, + type ResourcePullEvent, + type BackOfficeTaskCompleteEvent, + type BackOfficeTaskHandOffEvent, + type BackOfficeTaskFailEvent, +} from "./webhooks/events.js"; + +// Resource classes (for typing/advanced usage) +export { Conversations } from "./resources/conversations.js"; +export { OutboundConversations } from "./resources/outbound-conversations.js"; +export { BackOfficeTasks } from "./resources/back-office-tasks.js"; +export { Voice } from "./resources/voice.js"; +export { Tools } from "./resources/tools.js"; +export { Articles } from "./resources/articles.js"; +export { Topics } from "./resources/topics.js"; +export { Procedures } from "./resources/procedures.js"; +export { HandOffTargets } from "./resources/hand-off-targets.js"; +export { ResourceSources } from "./resources/resource-sources.js"; +export { ResourceTypes } from "./resources/resource-types.js"; +export { Secrets } from "./resources/secrets.js"; +export { Notes } from "./resources/notes.js"; +export { TerminologySubstitutions } from "./resources/terminology-substitutions.js"; +export { TrafficGroups } from "./resources/traffic-groups.js"; +export { IpAddressesResource, type IpAddresses } from "./resources/ip-addresses.js"; + +// Enums +export * from "./models/enums.js"; + +// Models +export * from "./models/common.js"; +export * from "./models/conversations.js"; +export * from "./models/back-office-tasks.js"; +export * from "./models/voice.js"; +export * from "./models/tools.js"; +export * from "./models/articles.js"; +export * from "./models/procedures.js"; +export * from "./models/hand-off-targets.js"; +export * from "./models/resources.js"; +export * from "./models/secrets.js"; +export * from "./models/notes.js"; +export * from "./models/terminology.js"; +export * from "./models/traffic-groups.js"; diff --git a/src/internal/http.ts b/src/internal/http.ts new file mode 100644 index 0000000..d56a4b4 --- /dev/null +++ b/src/internal/http.ts @@ -0,0 +1,146 @@ +import { ApiError, GradientLabsError, type ErrorCode } from "../errors.js"; +import { userAgent } from "./user-agent.js"; + +/** A subset of the global `fetch` signature, so callers can inject their own. */ +export type FetchLike = ( + input: string, + init: { + method: string; + headers: Record; + body?: string; + signal?: AbortSignal; + }, +) => Promise<{ + status: number; + text(): Promise; +}>; + +export interface HttpClientConfig { + baseUrl: string; + apiKey: string; + fetch: FetchLike; + timeoutMs?: number; +} + +export interface RequestOptions { + /** Query parameters. Undefined/null values are omitted. */ + query?: Record; + /** JSON request body. Serialised with JSON.stringify. */ + body?: unknown; + /** Caller-supplied cancellation signal. */ + signal?: AbortSignal; +} + +/** + * Thin wrapper around `fetch` that handles auth, headers, JSON + * (de)serialisation, cancellation, and error mapping. It never retries — retry + * policy is left entirely to the caller. + */ +export class HttpClient { + constructor(private readonly config: HttpClientConfig) {} + + async request(method: string, path: string, options: RequestOptions = {}): Promise { + const url = this.buildUrl(path, options.query); + + const headers: Record = { + Authorization: `Bearer ${this.config.apiKey}`, + Accept: "application/json", + "User-Agent": userAgent(), + }; + + let body: string | undefined; + if (options.body !== undefined) { + body = JSON.stringify(options.body); + headers["Content-Type"] = "application/json"; + } + + const signal = this.buildSignal(options.signal); + + let response: { status: number; text(): Promise }; + try { + response = await this.config.fetch(url, { method, headers, body, signal }); + } catch (cause) { + throw new GradientLabsError(`request to ${method} ${path} failed: ${errorMessage(cause)}`, { + cause, + }); + } + + const rawBody = await response.text(); + + if (response.status < 200 || response.status > 299) { + throw toApiError(response.status, rawBody); + } + + if (!rawBody) { + return undefined as T; + } + + try { + return JSON.parse(rawBody) as T; + } catch (cause) { + throw new GradientLabsError(`failed to parse response body: ${errorMessage(cause)}`, { + cause, + }); + } + } + + private buildUrl( + path: string, + query?: Record, + ): string { + const base = this.config.baseUrl.replace(/\/+$/, ""); + const cleanPath = path.replace(/^\/+/, ""); + const url = new URL(`${base}/${cleanPath}`); + if (query) { + for (const [key, value] of Object.entries(query)) { + if (value !== undefined && value !== null) { + url.searchParams.set(key, String(value)); + } + } + } + return url.toString(); + } + + private buildSignal(callerSignal?: AbortSignal): AbortSignal | undefined { + const { timeoutMs } = this.config; + if (timeoutMs === undefined) { + return callerSignal; + } + const timeoutSignal = AbortSignal.timeout(timeoutMs); + if (!callerSignal) { + return timeoutSignal; + } + // Node 20.3+ has AbortSignal.any; fall back to the caller signal otherwise. + if (typeof AbortSignal.any === "function") { + return AbortSignal.any([callerSignal, timeoutSignal]); + } + return callerSignal; + } +} + +function toApiError(statusCode: number, rawBody: string): ApiError { + let code: ErrorCode = ""; + let message = ""; + let details: Record = {}; + + if (rawBody) { + try { + const parsed = JSON.parse(rawBody) as { + code?: string; + message?: string; + details?: Record; + }; + code = parsed.code ?? ""; + message = parsed.message ?? ""; + details = parsed.details ?? {}; + } catch { + // Leave defaults; the status code still produces a usable error. + } + } + + return new ApiError({ statusCode, code, message, details }); +} + +function errorMessage(cause: unknown): string { + return cause instanceof Error ? cause.message : String(cause); +} diff --git a/src/internal/pagination.ts b/src/internal/pagination.ts new file mode 100644 index 0000000..bfb5653 --- /dev/null +++ b/src/internal/pagination.ts @@ -0,0 +1,33 @@ +/** + * Cursor-based pagination metadata returned by list endpoints. Cursors are + * opaque strings; pass `next`/`prev` back via the `after`/`before` parameters + * to page through results. + */ +export interface PageInfo { + next?: string; + prev?: string; +} + +/** A single page of a paginated list response. */ +export interface Page { + data: T[]; + pageInfo: PageInfo; +} + +/** + * Drives an async iterator that auto-follows `next` cursors, yielding each item + * across all pages. `fetchPage` is called once per page with the current + * `after` cursor (undefined for the first page). + */ +export async function* paginate( + fetchPage: (after: string | undefined) => Promise>, +): AsyncGenerator { + let after: string | undefined; + do { + const page = await fetchPage(after); + for (const item of page.data) { + yield item; + } + after = page.pageInfo.next; + } while (after); +} diff --git a/src/internal/user-agent.ts b/src/internal/user-agent.ts new file mode 100644 index 0000000..9d52472 --- /dev/null +++ b/src/internal/user-agent.ts @@ -0,0 +1,11 @@ +import { VERSION } from "./version.js"; + +/** + * Builds the User-Agent header identifying this client library, in the format + * `Gradient-Labs-Node/ (node/)`. + */ +export function userAgent(): string { + const runtimeVersion = + typeof process !== "undefined" && process.version ? process.version : "unknown"; + return `Gradient-Labs-Node/${VERSION} (node/${runtimeVersion})`; +} diff --git a/src/internal/version.ts b/src/internal/version.ts new file mode 100644 index 0000000..f13a8a0 --- /dev/null +++ b/src/internal/version.ts @@ -0,0 +1,2 @@ +// Kept in sync with the "version" field in package.json. +export const VERSION = "0.1.0"; diff --git a/src/models/articles.ts b/src/models/articles.ts new file mode 100644 index 0000000..8b04229 --- /dev/null +++ b/src/models/articles.ts @@ -0,0 +1,64 @@ +import type { ArticleStatus, ArticleUsageStatus, ArticleVisibility } from "./enums.js"; + +export interface UpsertArticleParams { + /** External identifier the company uses for this article. */ + id: string; + author_id: string; + title: string; + description: string; + body: string; + visibility: ArticleVisibility; + /** External identifier of the topic this article belongs to. */ + topic_id: string; + status: ArticleStatus; + /** Additional meta-data about the article. */ + data: Record; + created: string; + last_edited: string; + public_url: string; +} + +export interface SetArticleUsageStatusParams { + usage_status: ArticleUsageStatus; +} + +/** An article topic. */ +export interface Topic { + Source: string; + ExternalID: string; + Name: string; + Description: string; + Visibility: ArticleVisibility; + ParentExternalID: string; + Created: string; + LastEdited: string; + LastSeen: string; + /** Base64-encoded raw representation of the topic from the support platform. */ + Data: string; + PublicURL: string; +} + +export interface ListTopicsParams { + /** Which platform's topics to read. Defaults to "public-api". */ + support_platform?: string; +} + +export interface ReadTopicParams { + /** Which platform's topic to read. Defaults to "public-api". */ + support_platform?: string; +} + +export interface UpsertTopicParams { + /** External identifier the company uses for this topic. */ + id: string; + /** External identifier of this topic's parent topic. */ + parent_id: string; + name: string; + description: string; + visibility: ArticleVisibility; + status: ArticleStatus; + data: Record; + created: string; + last_edited: string; + public_url: string; +} diff --git a/src/models/back-office-tasks.ts b/src/models/back-office-tasks.ts new file mode 100644 index 0000000..6b18ca9 --- /dev/null +++ b/src/models/back-office-tasks.ts @@ -0,0 +1,54 @@ +import type { BackOfficeTaskResultType, BackOfficeTaskStatus } from "./enums.js"; + +/** An attachment uploaded with a back-office task. URL and base64 are mutually exclusive. */ +export interface BackOfficeTaskAttachmentInput { + file_name: string; + url?: string; + base_64_contents?: string; +} + +/** An attachment as returned on a back-office task. */ +export interface BackOfficeTaskAttachment { + idempotency_key: string; + file_name: string; + external_url?: string; + raw_contents?: string; +} + +/** The result of a back-office task, validated against the procedure's result schema. */ +export interface BackOfficeTaskResult { + result_type: BackOfficeTaskResultType; + custom?: Record; +} + +/** A back-office (Tier 2) task processed by a configurable agent. */ +export interface BackOfficeTask { + id: string; + agent_id: string; + input: Record; + created: string; + updated?: string; + status?: BackOfficeTaskStatus; + result?: BackOfficeTaskResult; + metadata?: Record; + attachments?: BackOfficeTaskAttachment[]; + completed?: string; + failed?: string; + failure_reasons?: string[]; + handed_off?: string; + hand_off_reason?: string; +} + +export interface CreateBackOfficeTaskParams { + /** Unique external identifier for the task. */ + id: string; + /** Input data for the task; shape depends on the task type. */ + input: Record; + /** Identifies the configurable back-office agent to run the task against. Required. */ + agent_id: string; + /** Optional free-format metadata the agent can read. */ + metadata?: Record; + attachments?: BackOfficeTaskAttachmentInput[]; + /** Optional creation timestamp (RFC3339). Defaults to now. */ + created?: string; +} diff --git a/src/models/common.ts b/src/models/common.ts new file mode 100644 index 0000000..3a6dc99 --- /dev/null +++ b/src/models/common.ts @@ -0,0 +1,19 @@ +import type { AttachmentType } from "./enums.js"; + +/** Basic information about a user, e.g. the author of a procedure. */ +export interface UserDetails { + email: string; +} + +/** A file or media item attached to a conversation message. */ +export interface Attachment { + type: AttachmentType; + /** Original file name including extension. */ + file_name: string; + /** Publicly accessible URL where the attachment can be downloaded. */ + url: string; + /** Optional short summary of what the attachment is. */ + summary?: string; + /** Optional full textual extract of the attachment's contents. */ + description?: string; +} diff --git a/src/models/conversations.ts b/src/models/conversations.ts new file mode 100644 index 0000000..344fe63 --- /dev/null +++ b/src/models/conversations.ts @@ -0,0 +1,141 @@ +import type { Attachment } from "./common.js"; +import type { Channel, ConversationEventType, CustomerSource, ParticipantType } from "./enums.js"; + +/** Agent-derived metadata about how a conversation was processed. */ +export interface AgentMetadata { + intent: string; + intent_handoff_target: string; + handoff_reason: string; + handoff_note: string; +} + +/** A series of messages between a customer, human agent, and the AI agent. */ +export interface Conversation { + id: string; + customer_id: string; + channel: Channel; + created: string; + updated: string; + status: string; + /** Whether an AI agent is currently actively handling this conversation. */ + agent_is_active: boolean; + latest_intent: string; + latest_handoff_target: string; + latest_agent_metadata?: AgentMetadata; +} + +/** A single message within a conversation. */ +export interface Message { + id: string; + body: string; + participant_id: string; + participant_type: ParticipantType; + subject?: string; + attachments?: Attachment[]; + created?: string; + conversation_token?: string; +} + +export interface StartConversationParams { + /** Unique external identifier for the conversation. */ + id: string; + /** Unique external identifier for the customer. */ + customer_id: string; + channel: Channel; + /** Optional identifier of the participant the conversation is assigned to. */ + assignee_id?: string; + /** Set to "AI Agent" to assign the conversation to the Gradient Labs AI. */ + assignee_type?: ParticipantType; + /** Arbitrary metadata attached to the conversation; echoed back in webhooks. */ + metadata?: unknown; + /** Optional creation timestamp (RFC3339). Defaults to now. */ + created?: string; + /** Structured context data the AI agent can use, keyed by resource type. */ + resources?: Record; + /** Optional traffic group to scope which procedures the conversation can access. */ + traffic_group_id?: string; + /** Raw sensitive token echoed back in future tool/webhook calls. */ + conversation_token?: string; +} + +export interface AddMessageParams { + id: string; + body?: string; + participant_id: string; + participant_type: ParticipantType; + subject?: string; + attachments?: Attachment[]; + created?: string; + conversation_token?: string; +} + +export interface AssignConversationParams { + assignee_type: ParticipantType; + assignee_id?: string; + reason?: string; + timestamp?: string; +} + +export interface ResumeConversationParams { + assignee_type: ParticipantType; + resources?: Record; + assignee_id?: string; + reason?: string; + timestamp?: string; +} + +export interface CancelConversationParams { + reason?: string; + timestamp?: string; +} + +export interface FinishConversationParams { + reason?: string; + timestamp?: string; +} + +export interface ConversationEventParams { + type: ConversationEventType; + participant_id: string; + participant_type: ParticipantType; + body?: string; + message_id?: string; + idempotency_key?: string; + timestamp?: string; +} + +export interface RateConversationParams { + type: string; + value: number; + max_value: number; + min_value: number; + comments?: string; + timestamp?: string; +} + +export interface ReturnAsyncToolResultParams { + async_tool_execution_id: string; + /** Required by the API: the JSON result of the async tool execution. */ + payload: Record; + timestamp?: string; +} + +export interface ReadConversationParams { + /** Which platform's conversation to read. Defaults to "public-api". */ + support_platform?: string; +} + +export interface StartOutboundConversationParams { + customer_id: string; + customer_source: CustomerSource; + procedure_id: string; + channel?: Channel; + support_platform?: string; + body?: string; + subject?: string; + resources?: Record; +} + +export interface OutboundConversation { + conversation_id: string; +} diff --git a/src/models/enums.ts b/src/models/enums.ts new file mode 100644 index 0000000..cfbf444 --- /dev/null +++ b/src/models/enums.ts @@ -0,0 +1,246 @@ +// Open string enums. Each is modelled as a `const` object of known values plus +// an open union type (`| (string & {})`) so that a future server-side value +// never breaks a consumer at compile time while retaining autocomplete. +// +// Values are sourced verbatim from the wearegradient Go source. + +export const ArticleStatus = { + Draft: "draft", + Published: "published", + Deleted: "deleted", + Excluded: "excluded", + Unknown: "unknown", +} as const; +export type ArticleStatus = (typeof ArticleStatus)[keyof typeof ArticleStatus] | (string & {}); + +export const ArticleUsageStatus = { + On: "on", + Off: "off", +} as const; +export type ArticleUsageStatus = + | (typeof ArticleUsageStatus)[keyof typeof ArticleUsageStatus] + | (string & {}); + +export const ArticleVisibility = { + Public: "public", + Users: "users", + Internal: "internal", + Unknown: "unknown", +} as const; +export type ArticleVisibility = + | (typeof ArticleVisibility)[keyof typeof ArticleVisibility] + | (string & {}); + +export const AttachmentType = { + Image: "image", + File: "file", +} as const; +export type AttachmentType = (typeof AttachmentType)[keyof typeof AttachmentType] | (string & {}); + +export const Channel = { + Web: "web", + Email: "email", + Voice: "voice", + Unmapped: "unmapped", +} as const; +export type Channel = (typeof Channel)[keyof typeof Channel] | (string & {}); + +export const CustomerSource = { + Dixa: "dixa", + Intercom: "intercom", + Freshchat: "freshchat", + Freshdesk: "freshdesk", + PublicApi: "public-api", + ChatSdk: "chat-sdk", + Salesforce: "salesforce", + Zendesk: "zendesk", + Livekit: "livekit", + Twilio: "twilio", + Talkdesk: "talkdesk", + IntercomVoice: "intercom-voice", + Livechat: "livechat", + WebApp: "web-app", + Gmail: "gmail", + File: "file", +} as const; +export type CustomerSource = (typeof CustomerSource)[keyof typeof CustomerSource] | (string & {}); + +export const ParticipantType = { + Customer: "Customer", + Agent: "Agent", + AIAgent: "AI Agent", + Bot: "Bot", +} as const; +export type ParticipantType = + | (typeof ParticipantType)[keyof typeof ParticipantType] + | (string & {}); + +export const ConversationEventType = { + Assigned: "assigned", + Cancelled: "cancelled", + Finished: "finished", + Resumed: "resumed", + InternalNote: "internal-note", + Message: "message", + Delivered: "delivered", + Read: "read", + Rated: "rated", + Started: "started", + Typing: "typing", + AsyncToolResult: "async-tool-result", +} as const; +export type ConversationEventType = + | (typeof ConversationEventType)[keyof typeof ConversationEventType] + | (string & {}); + +export const ProcedureStatus = { + Unsaved: "unsaved", + Draft: "draft", + Live: "live", + Archived: "archived", +} as const; +export type ProcedureStatus = + | (typeof ProcedureStatus)[keyof typeof ProcedureStatus] + | (string & {}); + +export const NoteStatus = { + Draft: "draft", + Live: "live", + Deleted: "deleted", +} as const; +export type NoteStatus = (typeof NoteStatus)[keyof typeof NoteStatus] | (string & {}); + +export const BackOfficeTaskStatus = { + Pending: "pending", + InProgress: "in-progress", + Completed: "completed", + Failed: "failed", + HandedOff: "handed-off", +} as const; +export type BackOfficeTaskStatus = + | (typeof BackOfficeTaskStatus)[keyof typeof BackOfficeTaskStatus] + | (string & {}); + +export const BackOfficeTaskResultType = { + Custom: "custom", +} as const; +export type BackOfficeTaskResultType = + | (typeof BackOfficeTaskResultType)[keyof typeof BackOfficeTaskResultType] + | (string & {}); + +export const AttributeCardinality = { + One: "one", + Many: "many", +} as const; +export type AttributeCardinality = + | (typeof AttributeCardinality)[keyof typeof AttributeCardinality] + | (string & {}); + +export const AttributeType = { + String: "string", + Date: "date", + Timestamp: "timestamp", + Boolean: "boolean", + Number: "number", + Array: "array", + Complex: "complex", +} as const; +export type AttributeType = (typeof AttributeType)[keyof typeof AttributeType] | (string & {}); + +export const ResourceSourceRefreshStrategy = { + Dynamic: "dynamic", + Static: "static", +} as const; +export type ResourceSourceRefreshStrategy = + | (typeof ResourceSourceRefreshStrategy)[keyof typeof ResourceSourceRefreshStrategy] + | (string & {}); + +export const ResourceSourceScope = { + Global: "global", + Local: "local", +} as const; +export type ResourceSourceScope = + | (typeof ResourceSourceScope)[keyof typeof ResourceSourceScope] + | (string & {}); + +export const ResourceSourceType = { + Http: "http", + Internal: "internal", + Webhook: "webhook", +} as const; +export type ResourceSourceType = + | (typeof ResourceSourceType)[keyof typeof ResourceSourceType] + | (string & {}); + +export const SchemaUpdateStrategy = { + Replace: "replace", + Merge: "merge", +} as const; +export type SchemaUpdateStrategy = + | (typeof SchemaUpdateStrategy)[keyof typeof SchemaUpdateStrategy] + | (string & {}); + +export const ResourceTypeRefreshStrategy = { + Dynamic: "dynamic", + Static: "static", +} as const; +export type ResourceTypeRefreshStrategy = + | (typeof ResourceTypeRefreshStrategy)[keyof typeof ResourceTypeRefreshStrategy] + | (string & {}); + +export const ResourceTypeScope = { + Global: "global", + Local: "local", +} as const; +export type ResourceTypeScope = + | (typeof ResourceTypeScope)[keyof typeof ResourceTypeScope] + | (string & {}); + +export const SupportPlatform = { + Dixa: "dixa", + Freshchat: "freshchat", + Freshdesk: "freshdesk", + Gmail: "gmail", + Intercom: "intercom", + Livechat: "livechat", + PublicApi: "public-api", + ChatSdk: "chat-sdk", + Salesforce: "salesforce", + Zendesk: "zendesk", + Livekit: "livekit", + Twilio: "twilio", + Talkdesk: "talkdesk", + IntercomVoice: "intercom-voice", + ConversationSynthesizor: "conversation-synthesizor", + WebApp: "web-app", +} as const; +export type SupportPlatform = + | (typeof SupportPlatform)[keyof typeof SupportPlatform] + | (string & {}); + +export const BodyEncoding = { + Json: "application/json", + Form: "application/x-www-form-urlencoded", +} as const; +export type BodyEncoding = (typeof BodyEncoding)[keyof typeof BodyEncoding] | (string & {}); + +export const ParameterSource = { + Llm: "llm", + Literal: "literal", + Resource: "resource", +} as const; +export type ParameterSource = + | (typeof ParameterSource)[keyof typeof ParameterSource] + | (string & {}); + +export const ParameterType = { + String: "string", + StringArray: "string_array", + Integer: "integer", + Float: "float", + Boolean: "boolean", + Date: "date", + Timestamp: "timestamp", + Duration: "duration", +} as const; +export type ParameterType = (typeof ParameterType)[keyof typeof ParameterType] | (string & {}); diff --git a/src/models/hand-off-targets.ts b/src/models/hand-off-targets.ts new file mode 100644 index 0000000..0ff764e --- /dev/null +++ b/src/models/hand-off-targets.ts @@ -0,0 +1,25 @@ +/** A hand-off target (team, agent, or queue) a conversation can be routed to. */ +export interface HandOffTarget { + id: string; + name: string; +} + +export interface UpsertHandOffTargetParams { + id: string; + name: string; +} + +export interface DeleteHandOffTargetParams { + id: string; +} + +export interface GetDefaultHandOffTargetParams { + /** The conversation channel to get the default for. */ + channel: string; +} + +export interface SetDefaultHandOffTargetParams { + /** Target ID to set as default. Empty string clears the default. */ + id: string; + channel: string; +} diff --git a/src/models/notes.ts b/src/models/notes.ts new file mode 100644 index 0000000..f4058fe --- /dev/null +++ b/src/models/notes.ts @@ -0,0 +1,44 @@ +import type { NoteStatus } from "./enums.js"; + +/** A note that provides the AI agent with time-bound context. */ +export interface Note { + /** Gradient Labs' internal ID for this note. */ + gradient_labs_id: string; + /** The company's external ID for this note. */ + id: string; + title: string; + body: string; + url: string; + valid_from: string; + valid_to: string; + last_modified_by: string; + created: string; + updated: string; + status: NoteStatus; +} + +export interface CreateNoteParams { + /** External identifier the company uses for this note. */ + id: string; + title: string; + /** Main contents of the note. Mutually exclusive with webpage_url. */ + body: string; + /** Webpage to use as the note body. Mutually exclusive with body. */ + webpage_url: string; + /** When the note becomes relevant. */ + start_time: string; + /** When the note is no longer relevant. */ + end_time: string; +} + +export interface UpdateNoteParams { + title: string; + body: string; + webpage_url: string; + start_time: string; + end_time: string; +} + +export interface SetNoteStatusParams { + status: NoteStatus; +} diff --git a/src/models/procedures.ts b/src/models/procedures.ts new file mode 100644 index 0000000..ed1bcd1 --- /dev/null +++ b/src/models/procedures.ts @@ -0,0 +1,51 @@ +import type { UserDetails } from "./common.js"; +import type { ProcedureStatus } from "./enums.js"; + +/** An AI agent procedure that defines how the agent should handle conversations. */ +export interface Procedure { + id: string; + name: string; + description: string; + status: ProcedureStatus; + author: UserDetails; + created: string; + updated: string; + has_daily_limit: boolean; + max_daily_conversations: number; +} + +/** Configuration limiting a gated procedure version. */ +export interface GatedConfig { + MaxDailyConversations: number; +} + +/** A specific saved version of a procedure. */ +export interface ProcedureVersion { + Name: string; + Description: string; + Version: number; + Author: string; + Created: string; + Gated: boolean; + /** Present only when the version is gated. */ + GatedConfig?: GatedConfig; + Live: boolean; +} + +export interface ListProceduresParams { + /** Opaque pagination cursor from a previous response. */ + cursor?: string; + /** Filter by status, e.g. "live" or "draft". */ + status?: ProcedureStatus; +} + +export interface SetProcedureLimitParams { + has_daily_limit?: boolean; + max_daily_conversations?: number; +} + +export interface SetGatedVersionParams { + max_daily_conversations: number; + /** Replace an existing gated version if one exists. */ + replace: boolean; +} diff --git a/src/models/resources.ts b/src/models/resources.ts new file mode 100644 index 0000000..f4d3cd7 --- /dev/null +++ b/src/models/resources.ts @@ -0,0 +1,129 @@ +import type { + AttributeCardinality, + AttributeType, + ResourceSourceRefreshStrategy, + ResourceSourceScope, + ResourceSourceType, + ResourceTypeRefreshStrategy, + ResourceTypeScope, + SchemaUpdateStrategy, +} from "./enums.js"; + +/** A single data field within a resource schema. */ +export interface Attribute { + path: string; + type: AttributeType; + cardinality: AttributeCardinality; + description: string; + is_root: boolean; + name: string; +} + +/** The structure of data provided by a resource source or type. */ +export interface Schema { + /** The complete JSON schema definition in its original format. */ + raw: Record; + attributes?: Attribute[]; +} + +/** HTTP body configuration for a resource source. */ +export interface ResourceSourceHttpBodyDefinition { + encoding: string; + json_template?: string; + form_field_templates?: Record; +} + +/** HTTP configuration for a resource source. */ +export interface ResourceSourceHttpConfig { + method: string; + url_template: string; + header_templates?: Record; + body?: ResourceSourceHttpBodyDefinition; +} + +/** Webhook configuration for a resource source. */ +export interface ResourceSourceWebhookConfig { + name: string; +} + +/** A data source that provides structured information to AI agents. */ +export interface ResourceSource { + id: string; + display_name: string; + description: string; + source_type: ResourceSourceType; + available_refresh_strategies: ResourceSourceRefreshStrategy[]; + available_scopes: ResourceSourceScope[]; + created: string; + updated: string; + attribute_descriptions?: Record; + schema?: Schema; + http_config?: ResourceSourceHttpConfig; + webhook_config?: ResourceSourceWebhookConfig; +} + +export interface CreateResourceSourceParams { + display_name: string; + source_type: ResourceSourceType; + description?: string; + attribute_descriptions?: Record; + http_config?: ResourceSourceHttpConfig; + webhook_config?: ResourceSourceWebhookConfig; +} + +export interface UpdateResourceSourceParams { + display_name?: string; + description?: string; + source_type?: ResourceSourceType; + attribute_descriptions?: Record; + schema?: Schema; + http_config?: ResourceSourceHttpConfig; + webhook_config?: ResourceSourceWebhookConfig; +} + +export interface UpdateSchemaByExamplesParams { + /** Resource payload examples to infer the schema from. */ + examples: Record[]; + schema_update_strategy?: SchemaUpdateStrategy; +} + +/** How a resource type connects to and retrieves data from a source. */ +export interface SourceConfig { + source_id: string; + /** Which attributes to include. Empty means all. */ + attributes: string[]; + /** Cache duration, e.g. "1h", "30m", "never". */ + cache: string; +} + +/** A specific type of structured data accessible by AI agents. */ +export interface ResourceType { + id: string; + display_name: string; + description: string; + scope: ResourceTypeScope; + refresh_strategy: ResourceTypeRefreshStrategy; + is_enabled: boolean; + created: string; + updated: string; + schema?: Schema; + source_config?: SourceConfig; +} + +export interface CreateResourceTypeParams { + display_name: string; + scope: ResourceTypeScope; + refresh_strategy: ResourceTypeRefreshStrategy; + description?: string; + is_enabled?: boolean; + source_config?: SourceConfig; +} + +export interface UpdateResourceTypeParams { + display_name?: string; + description?: string; + scope?: ResourceTypeScope; + refresh_strategy?: ResourceTypeRefreshStrategy; + is_enabled?: boolean; + source_config?: SourceConfig; +} diff --git a/src/models/secrets.ts b/src/models/secrets.ts new file mode 100644 index 0000000..f0f8439 --- /dev/null +++ b/src/models/secrets.ts @@ -0,0 +1,23 @@ +import type { HttpDefinition } from "./tools.js"; + +/** HTTP mechanism for refreshing a secret's value. */ +export interface RefreshMechanismHttp { + request_definition: HttpDefinition; + /** Name of the parameter in the (JSON) response body. */ + response_param_name: string; +} + +/** A configured secret. The value itself is never returned. */ +export interface Secret { + name: string; + created: string; + updated: string; + expiry?: string; + refresh_mechanism_http?: RefreshMechanismHttp; +} + +export interface WriteSecretParams { + value: string; + expiry?: string; + refresh_mechanism_http?: RefreshMechanismHttp; +} diff --git a/src/models/terminology.ts b/src/models/terminology.ts new file mode 100644 index 0000000..3f9a50c --- /dev/null +++ b/src/models/terminology.ts @@ -0,0 +1,30 @@ +/** A term the agent should avoid and its replacement, optionally resource-scoped. */ +export interface TerminologySubstitution { + id: string; + blocked: string; + blocked_description: string; + replacement: string; + resource_type_id: string; + resource_attribute_json_path: string; + resource_value_to_match: string; + created: string; + updated: string; +} + +export interface CreateTerminologySubstitutionParams { + blocked: string; + blocked_description: string; + replacement: string; + resource_type_id?: string; + resource_attribute_json_path?: string; + resource_value_to_match?: string; +} + +export interface UpdateTerminologySubstitutionParams { + blocked: string; + blocked_description: string; + replacement: string; + resource_type_id?: string; + resource_attribute_json_path?: string; + resource_value_to_match?: string; +} diff --git a/src/models/tools.ts b/src/models/tools.ts new file mode 100644 index 0000000..7b4745b --- /dev/null +++ b/src/models/tools.ts @@ -0,0 +1,139 @@ +import type { BodyEncoding, ParameterSource, ParameterType } from "./enums.js"; + +/** Request body configuration for an HTTP tool. */ +export interface HttpBodyDefinition { + encoding: BodyEncoding; + /** JSON body template when encoding is "application/json". */ + json_template?: string; + /** Form fields when encoding is form-encoded. */ + form_field_templates?: Record; +} + +/** Configures a tool to make direct HTTP requests to external APIs. */ +export interface HttpDefinition { + method: string; + /** URL with `${params.name}` substitution. */ + url_template: string; + header_templates?: Record; + body?: HttpBodyDefinition; +} + +/** Configures a tool to call a webhook. */ +export interface ToolWebhookConfiguration { + name: string; +} + +/** Configures a tool to execute via a Temporal workflow. */ +export interface WorkflowConfiguration { + workflow_type: string; + task_queue: string; +} + +/** Configures a tool to execute via a Model Context Protocol server. */ +export interface McpConfiguration { + server_id: string; + external_tool_name: string; +} + +/** A tool configuration embedded within another tool (e.g. async start/cancel). */ +export interface ChildTool { + http?: HttpDefinition; + webhook?: ToolWebhookConfiguration; + workflow?: WorkflowConfiguration; +} + +/** + * Configures a tool for long-running asynchronous operations. + * + * `timeout` is a duration in nanoseconds (the API's native representation). + */ +export interface AsyncDefinition { + start_execution_tool: ChildTool; + /** Maximum duration to wait, in nanoseconds. */ + timeout: number; + cancel_execution_tool?: ChildTool; +} + +/** A predefined choice for a tool parameter. */ +export interface ParameterOption { + value: string; + text: string; +} + +/** An input parameter a tool accepts when invoked. */ +export interface ToolParameter { + name: string; + description: string; + type: ParameterType; + allowed_sources: ParameterSource[]; + /** If nil, defaults to true (required). */ + required?: boolean; + options?: ParameterOption[]; +} + +/** A group of parameters that become active for a discriminator value. */ +export interface ToolParameterSet { + discriminator_parameter_name: string; + discriminator_value: string; + parameters: ToolParameter[]; +} + +/** A custom tool the AI agent can use during conversations. */ +export interface Tool { + id: string; + name: string; + description: string; + parameters: ToolParameter[]; + parameter_sets?: ToolParameterSet[]; + draft?: boolean; + http?: HttpDefinition; + webhook?: ToolWebhookConfiguration; + async?: AsyncDefinition; + mcp?: McpConfiguration; +} + +export interface CreateToolParams { + id: string; + name: string; + description: string; + parameters: ToolParameter[]; + parameter_sets?: ToolParameterSet[]; + draft?: boolean; + http?: HttpDefinition; + webhook?: ToolWebhookConfiguration; + async?: AsyncDefinition; + mcp?: McpConfiguration; +} + +export interface UpdateToolParams { + description: string; + parameters: ToolParameter[]; + http?: HttpDefinition; + webhook?: ToolWebhookConfiguration; + async?: AsyncDefinition; +} + +export interface ReadToolParams { + /** Read a specific tool version. */ + version?: number; +} + +/** A name/value argument passed to a tool execution. */ +export interface ToolArgument { + name: string; + value: string; +} + +export interface ExecuteToolParams { + arguments: ToolArgument[]; + /** Optional conversation-scoped token, if the tool requires one. */ + token?: string; +} + +export interface ToolExecutionResult { + id: string; + /** JSON result of the execution, if it succeeded. */ + result?: Record; + /** Error that occurred during execution, if it failed. */ + error?: string; +} diff --git a/src/models/traffic-groups.ts b/src/models/traffic-groups.ts new file mode 100644 index 0000000..346567a --- /dev/null +++ b/src/models/traffic-groups.ts @@ -0,0 +1,27 @@ +/** An assigned or excluded target within a traffic group. */ +export interface TrafficGroupTarget { + target_type: string; + target_id: string; +} + +/** A traffic group scoping which procedures conversations can access. */ +export interface TrafficGroup { + id: string; + name: string; + targets: TrafficGroupTarget[]; + excluded_targets: TrafficGroupTarget[]; +} + +export interface CreateTrafficGroupParams { + name: string; +} + +export interface UpdateTrafficGroupParams { + name: string; +} + +export interface TrafficGroupTargetParams { + /** Type of target, e.g. "procedure". */ + target_type: string; + target_id: string; +} diff --git a/src/models/voice.ts b/src/models/voice.ts new file mode 100644 index 0000000..6c877c7 --- /dev/null +++ b/src/models/voice.ts @@ -0,0 +1,18 @@ +/** The most recent voice call context for a phone number. */ +export interface VoiceCallContext { + /** Timestamp (RFC3339) when the call was received by the AI voice agent. */ + started_at: string; + summary?: string; + transcript?: string; + handoff_reason?: string; + last_executed_procedure?: string; + last_executed_procedure_url?: string; + gradient_labs_url?: string; +} + +export interface ReadVoiceCallContextParams { + /** Time window (seconds) to look back for recent call events. Default 60, min 5. */ + lookback_seconds?: number; + /** Include large fields (full transcript and untruncated summary). */ + include_large_fields?: boolean; +} diff --git a/src/request-config.ts b/src/request-config.ts new file mode 100644 index 0000000..ebb6181 --- /dev/null +++ b/src/request-config.ts @@ -0,0 +1,5 @@ +/** Per-request options accepted by every client method. */ +export interface RequestConfig { + /** Cancellation signal, the Node equivalent of Go's context.Context. */ + signal?: AbortSignal; +} diff --git a/src/resources/articles.ts b/src/resources/articles.ts new file mode 100644 index 0000000..bd9466b --- /dev/null +++ b/src/resources/articles.ts @@ -0,0 +1,34 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { SetArticleUsageStatusParams, UpsertArticleParams } from "../models/articles.js"; + +/** + * Article management endpoints. Requires a Management API key. + */ +export class Articles { + constructor(private readonly http: HttpClient) {} + + /** Creates or updates a help article. */ + upsert(params: UpsertArticleParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "articles", { body: params, signal: config.signal }); + } + + /** Updates whether the AI agent can use an article or not. */ + setUsageStatus( + id: string, + params: SetArticleUsageStatusParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", `articles/${encodeURIComponent(id)}/usage-status`, { + body: params, + signal: config.signal, + }); + } + + /** Marks an article as deleted. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `articles/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/back-office-tasks.ts b/src/resources/back-office-tasks.ts new file mode 100644 index 0000000..29e2c0e --- /dev/null +++ b/src/resources/back-office-tasks.ts @@ -0,0 +1,25 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { BackOfficeTask, CreateBackOfficeTaskParams } from "../models/back-office-tasks.js"; + +/** + * Back-office task endpoints. Requires an Integration API key. + */ +export class BackOfficeTasks { + constructor(private readonly http: HttpClient) {} + + /** Creates a new back-office task. */ + create(params: CreateBackOfficeTaskParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "back-office-tasks", { + body: params, + signal: config.signal, + }); + } + + /** Retrieves detailed information about a back-office task. */ + get(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `back-office-tasks/${encodeURIComponent(id)}/read`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/conversations.ts b/src/resources/conversations.ts new file mode 100644 index 0000000..a5eeff6 --- /dev/null +++ b/src/resources/conversations.ts @@ -0,0 +1,129 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + AddMessageParams, + AssignConversationParams, + CancelConversationParams, + Conversation, + ConversationEventParams, + FinishConversationParams, + Message, + ReadConversationParams, + RateConversationParams, + ResumeConversationParams, + ReturnAsyncToolResultParams, + StartConversationParams, +} from "../models/conversations.js"; + +/** + * Conversation endpoints. Requires an Integration API key. + */ +export class Conversations { + constructor(private readonly http: HttpClient) {} + + /** Begins a conversation. */ + start(params: StartConversationParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "conversations", { body: params, signal: config.signal }); + } + + /** Retrieves a conversation, including the latest AI agent metadata. */ + get( + id: string, + params: ReadConversationParams = {}, + config: RequestConfig = {}, + ): Promise { + return this.http.request("GET", `conversations/${encodeURIComponent(id)}/read`, { + query: { support_platform: params.support_platform }, + signal: config.signal, + }); + } + + /** + * Retrieves a conversation. + * + * @deprecated Use {@link Conversations.get} instead, which reads from the + * canonical `/read` endpoint. + */ + getDeprecated(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `conversations/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Adds a new message to an existing conversation. */ + addMessage(id: string, params: AddMessageParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", `conversations/${encodeURIComponent(id)}/messages`, { + body: params, + signal: config.signal, + }); + } + + /** Transfers responsibility for handling a conversation to a participant. */ + assign(id: string, params: AssignConversationParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", `conversations/${encodeURIComponent(id)}/assignee`, { + body: params, + signal: config.signal, + }); + } + + /** Logs an event against the conversation (e.g. typing, delivered, read). */ + addEvent(id: string, params: ConversationEventParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", `conversations/${encodeURIComponent(id)}/events`, { + body: params, + signal: config.signal, + }); + } + + /** Adds the result of a customer survey (e.g. CSAT) to a conversation. */ + rate(id: string, params: RateConversationParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", `conversations/${encodeURIComponent(id)}/rate`, { + body: params, + signal: config.signal, + }); + } + + /** Cancels a conversation (e.g. because a human has taken it over). */ + cancel( + id: string, + params: CancelConversationParams = {}, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `conversations/${encodeURIComponent(id)}/cancel`, { + body: params, + signal: config.signal, + }); + } + + /** Finishes a conversation that has reached a natural end state. */ + finish( + id: string, + params: FinishConversationParams = {}, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `conversations/${encodeURIComponent(id)}/finish`, { + body: params, + signal: config.signal, + }); + } + + /** Re-opens a conversation that was previously finished. */ + resume(id: string, params: ResumeConversationParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", `conversations/${encodeURIComponent(id)}/resume`, { + body: params, + signal: config.signal, + }); + } + + /** Returns the result of an async tool execution. */ + returnAsyncToolResult( + id: string, + params: ReturnAsyncToolResultParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request( + "PUT", + `conversations/${encodeURIComponent(id)}/return-async-tool-result`, + { body: params, signal: config.signal }, + ); + } +} diff --git a/src/resources/hand-off-targets.ts b/src/resources/hand-off-targets.ts new file mode 100644 index 0000000..919a554 --- /dev/null +++ b/src/resources/hand-off-targets.ts @@ -0,0 +1,61 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + DeleteHandOffTargetParams, + GetDefaultHandOffTargetParams, + HandOffTarget, + SetDefaultHandOffTargetParams, + UpsertHandOffTargetParams, +} from "../models/hand-off-targets.js"; + +interface ListHandOffTargetsResponse { + targets: HandOffTarget[]; +} + +/** + * Hand-off target management endpoints. Requires a Management API key. + */ +export class HandOffTargets { + constructor(private readonly http: HttpClient) {} + + /** Lists the available hand-off targets. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "hand-off-targets", { + signal: config.signal, + }); + return rsp.targets; + } + + /** Creates or updates a hand-off target. */ + upsert(params: UpsertHandOffTargetParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "hand-off-targets", { body: params, signal: config.signal }); + } + + /** Deletes a hand-off target. */ + delete(params: DeleteHandOffTargetParams, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", "hand-off-targets", { + query: { id: params.id }, + signal: config.signal, + }); + } + + /** Gets the current default hand-off target for a channel. Returns "" if unset. */ + async getDefault( + params: GetDefaultHandOffTargetParams, + config: RequestConfig = {}, + ): Promise { + const rsp = await this.http.request<{ id: string }>("GET", "hand-off-targets/default", { + query: { channel: params.channel }, + signal: config.signal, + }); + return rsp.id; + } + + /** Sets the default hand-off target for a channel. */ + setDefault(params: SetDefaultHandOffTargetParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", "hand-off-targets/default", { + body: params, + signal: config.signal, + }); + } +} diff --git a/src/resources/ip-addresses.ts b/src/resources/ip-addresses.ts new file mode 100644 index 0000000..740b808 --- /dev/null +++ b/src/resources/ip-addresses.ts @@ -0,0 +1,22 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; + +/** The IP addresses Gradient Labs uses, in CIDR format. */ +export interface IpAddresses { + /** Addresses the public API is served from. */ + api: string[]; + /** Addresses outbound (egress) requests originate from. */ + egress: string[]; +} + +/** + * IP address endpoints. Requires a Management API key. + */ +export class IpAddressesResource { + constructor(private readonly http: HttpClient) {} + + /** Returns the list of IP addresses Gradient Labs uses, in CIDR format. */ + list(config: RequestConfig = {}): Promise { + return this.http.request("GET", "ip-addresses", { signal: config.signal }); + } +} diff --git a/src/resources/notes.ts b/src/resources/notes.ts new file mode 100644 index 0000000..1004450 --- /dev/null +++ b/src/resources/notes.ts @@ -0,0 +1,43 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateNoteParams, + Note, + SetNoteStatusParams, + UpdateNoteParams, +} from "../models/notes.js"; + +/** + * Note management endpoints. Requires a Management API key. + */ +export class Notes { + constructor(private readonly http: HttpClient) {} + + /** Creates a new note. */ + create(params: CreateNoteParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "notes", { body: params, signal: config.signal }); + } + + /** Updates an existing note's contents. */ + update(id: string, params: UpdateNoteParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", `notes/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Updates a note's status. */ + setStatus(id: string, params: SetNoteStatusParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", `notes/${encodeURIComponent(id)}/status`, { + body: params, + signal: config.signal, + }); + } + + /** Marks a note as deleted. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `notes/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/outbound-conversations.ts b/src/resources/outbound-conversations.ts new file mode 100644 index 0000000..aa55815 --- /dev/null +++ b/src/resources/outbound-conversations.ts @@ -0,0 +1,24 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + OutboundConversation, + StartOutboundConversationParams, +} from "../models/conversations.js"; + +/** + * Outbound conversation endpoints. Requires an Integration API key. + */ +export class OutboundConversations { + constructor(private readonly http: HttpClient) {} + + /** Creates and starts a new outbound conversation initiated by the AI agent. */ + start( + params: StartOutboundConversationParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", "outbound/conversations", { + body: params, + signal: config.signal, + }); + } +} diff --git a/src/resources/procedures.ts b/src/resources/procedures.ts new file mode 100644 index 0000000..5795563 --- /dev/null +++ b/src/resources/procedures.ts @@ -0,0 +1,119 @@ +import type { HttpClient } from "../internal/http.js"; +import type { Page } from "../internal/pagination.js"; +import { paginate } from "../internal/pagination.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + ListProceduresParams, + Procedure, + ProcedureVersion, + SetGatedVersionParams, + SetProcedureLimitParams, +} from "../models/procedures.js"; + +interface ListProceduresResponse { + procedures: Procedure[]; + pagination: { next?: string; prev?: string }; +} + +interface ListVersionsResponse { + Versions: ProcedureVersion[]; +} + +/** + * Procedure management endpoints. Requires a Management API key. + */ +export class Procedures { + constructor(private readonly http: HttpClient) {} + + /** Retrieves one page of procedures. */ + async list( + params: ListProceduresParams = {}, + config: RequestConfig = {}, + ): Promise> { + const rsp = await this.http.request("GET", "procedures", { + query: { cursor: params.cursor, status: params.status }, + signal: config.signal, + }); + return { data: rsp.procedures, pageInfo: rsp.pagination }; + } + + /** Iterates over all procedures, transparently following pagination cursors. */ + listAll( + params: Omit = {}, + config: RequestConfig = {}, + ): AsyncGenerator { + return paginate((cursor) => this.list({ ...params, cursor }, config)); + } + + /** Retrieves a specific procedure by ID. */ + get(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `procedure/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Configures daily usage limits for a procedure. Returns the updated procedure. */ + async setLimit( + id: string, + params: SetProcedureLimitParams, + config: RequestConfig = {}, + ): Promise { + const rsp = await this.http.request<{ procedure: Procedure }>( + "POST", + `procedure/${encodeURIComponent(id)}/limit`, + { body: params, signal: config.signal }, + ); + return rsp.procedure; + } + + /** Lists the non-ephemeral versions of a procedure. */ + async listVersions(id: string, config: RequestConfig = {}): Promise { + const rsp = await this.http.request( + "GET", + `procedures/${encodeURIComponent(id)}/versions`, + { signal: config.signal }, + ); + return rsp.Versions; + } + + /** Promotes a procedure version to be the live (production) version. */ + setLiveVersion(id: string, version: number, config: RequestConfig = {}): Promise { + return this.http.request( + "POST", + `procedures/${encodeURIComponent(id)}/versions/${version}/set-live`, + { signal: config.signal }, + ); + } + + /** Removes a procedure version from being the live revision. */ + unsetLiveVersion(id: string, version: number, config: RequestConfig = {}): Promise { + return this.http.request( + "POST", + `procedures/${encodeURIComponent(id)}/versions/${version}/unset-live`, + { signal: config.signal }, + ); + } + + /** Marks a procedure version as gated for A/B testing. */ + setGatedVersion( + id: string, + version: number, + params: SetGatedVersionParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request( + "POST", + `procedures/${encodeURIComponent(id)}/versions/${version}/set-gated`, + { body: params, signal: config.signal }, + ); + } + + /** Removes the gated marking from a procedure version. */ + unsetGatedVersion(id: string, version: number, config: RequestConfig = {}): Promise { + return this.http.request( + "POST", + `procedures/${encodeURIComponent(id)}/versions/${version}/unset-gated`, + { signal: config.signal }, + ); + } +} diff --git a/src/resources/resource-sources.ts b/src/resources/resource-sources.ts new file mode 100644 index 0000000..1af80ed --- /dev/null +++ b/src/resources/resource-sources.ts @@ -0,0 +1,71 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateResourceSourceParams, + ResourceSource, + UpdateResourceSourceParams, + UpdateSchemaByExamplesParams, +} from "../models/resources.js"; + +interface ListResourceSourcesResponse { + resource_sources: ResourceSource[]; +} + +/** + * Resource source management endpoints. Requires a Management API key. + */ +export class ResourceSources { + constructor(private readonly http: HttpClient) {} + + /** Lists all resource sources accessible to the company. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "resource-sources", { + signal: config.signal, + }); + return rsp.resource_sources; + } + + /** Creates a new resource source. */ + create(params: CreateResourceSourceParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "resource-sources", { body: params, signal: config.signal }); + } + + /** Retrieves a specific resource source by ID. */ + get(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `resource-sources/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Updates an existing resource source. */ + update( + id: string, + params: UpdateResourceSourceParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `resource-sources/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Deletes a resource source. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `resource-sources/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Modifies the source schema based on the provided payload examples. */ + updateSchemaByExamples( + id: string, + params: UpdateSchemaByExamplesParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request( + "POST", + `resource-sources/${encodeURIComponent(id)}/schema-by-examples`, + { body: params, signal: config.signal }, + ); + } +} diff --git a/src/resources/resource-types.ts b/src/resources/resource-types.ts new file mode 100644 index 0000000..6d5fade --- /dev/null +++ b/src/resources/resource-types.ts @@ -0,0 +1,57 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateResourceTypeParams, + ResourceType, + UpdateResourceTypeParams, +} from "../models/resources.js"; + +interface ListResourceTypesResponse { + resource_types: ResourceType[]; +} + +/** + * Resource type management endpoints. Requires a Management API key. + */ +export class ResourceTypes { + constructor(private readonly http: HttpClient) {} + + /** Lists all resource types accessible to the company. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "resource-types", { + signal: config.signal, + }); + return rsp.resource_types; + } + + /** Creates a new resource type. */ + create(params: CreateResourceTypeParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "resource-types", { body: params, signal: config.signal }); + } + + /** Retrieves a specific resource type by ID. */ + get(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `resource-types/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Updates an existing resource type. */ + update( + id: string, + params: UpdateResourceTypeParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `resource-types/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Deletes a resource type. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `resource-types/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/secrets.ts b/src/resources/secrets.ts new file mode 100644 index 0000000..30dd655 --- /dev/null +++ b/src/resources/secrets.ts @@ -0,0 +1,37 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { Secret, WriteSecretParams } from "../models/secrets.js"; + +interface ListSecretsResponse { + secrets: Secret[]; +} + +/** + * Secret management endpoints. Requires a Management API key. + */ +export class Secrets { + constructor(private readonly http: HttpClient) {} + + /** Lists the company's configured secrets. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "secrets", { + signal: config.signal, + }); + return rsp.secrets; + } + + /** Creates or updates a secret. */ + write(name: string, params: WriteSecretParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", `secrets/${encodeURIComponent(name)}`, { + body: params, + signal: config.signal, + }); + } + + /** Revokes a secret so it can no longer be used. */ + revoke(name: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `secrets/${encodeURIComponent(name)}`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/terminology-substitutions.ts b/src/resources/terminology-substitutions.ts new file mode 100644 index 0000000..c4d4931 --- /dev/null +++ b/src/resources/terminology-substitutions.ts @@ -0,0 +1,63 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateTerminologySubstitutionParams, + TerminologySubstitution, + UpdateTerminologySubstitutionParams, +} from "../models/terminology.js"; + +interface ListResponse { + substitutions: TerminologySubstitution[]; +} + +/** + * Terminology substitution management endpoints. Requires a Management API key. + */ +export class TerminologySubstitutions { + constructor(private readonly http: HttpClient) {} + + /** Returns all terminology substitutions configured for the organization. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "terminology-substitutions", { + signal: config.signal, + }); + return rsp.substitutions; + } + + /** Creates a new terminology substitution. */ + create( + params: CreateTerminologySubstitutionParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", "terminology-substitutions", { + body: params, + signal: config.signal, + }); + } + + /** Returns a single terminology substitution by ID. */ + get(id: string, config: RequestConfig = {}): Promise { + return this.http.request("GET", `terminology-substitutions/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Updates an existing terminology substitution. */ + update( + id: string, + params: UpdateTerminologySubstitutionParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `terminology-substitutions/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Deletes a terminology substitution by ID. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `terminology-substitutions/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } +} diff --git a/src/resources/tools.ts b/src/resources/tools.ts new file mode 100644 index 0000000..fabe643 --- /dev/null +++ b/src/resources/tools.ts @@ -0,0 +1,69 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateToolParams, + ExecuteToolParams, + ReadToolParams, + Tool, + ToolExecutionResult, + UpdateToolParams, +} from "../models/tools.js"; + +interface ToolListResponse { + tools: Tool[]; +} + +/** + * Tool management endpoints. Requires a Management API key. + */ +export class Tools { + constructor(private readonly http: HttpClient) {} + + /** Returns all tools created by the company. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "tools", { + signal: config.signal, + }); + return rsp.tools; + } + + /** Creates a new custom tool. */ + create(params: CreateToolParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "tools", { body: params, signal: config.signal }); + } + + /** Reads a tool by ID and optional version. */ + get(id: string, params: ReadToolParams = {}, config: RequestConfig = {}): Promise { + return this.http.request("GET", `tools/${encodeURIComponent(id)}`, { + query: { version: params.version }, + signal: config.signal, + }); + } + + /** Creates a new revision of a tool. The name and type cannot be changed. */ + update(id: string, params: UpdateToolParams, config: RequestConfig = {}): Promise { + return this.http.request("PUT", `tools/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Deletes a tool. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `tools/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Executes a tool, enabling you to test it without an actual conversation. */ + execute( + id: string, + params: ExecuteToolParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", `tools/${encodeURIComponent(id)}/execute`, { + body: { arguments: params.arguments, token: params.token ?? "" }, + signal: config.signal, + }); + } +} diff --git a/src/resources/topics.ts b/src/resources/topics.ts new file mode 100644 index 0000000..cfbcbc2 --- /dev/null +++ b/src/resources/topics.ts @@ -0,0 +1,41 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + ListTopicsParams, + ReadTopicParams, + Topic, + UpsertTopicParams, +} from "../models/articles.js"; + +interface ListTopicsResponse { + Topics: Topic[]; +} + +/** + * Article topic management endpoints. Requires a Management API key. + */ +export class Topics { + constructor(private readonly http: HttpClient) {} + + /** Lists the company's topics, optionally filtered by support platform. */ + async list(params: ListTopicsParams = {}, config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "topics", { + query: { support_platform: params.support_platform }, + signal: config.signal, + }); + return rsp.Topics; + } + + /** Reads a single article topic. */ + get(id: string, params: ReadTopicParams = {}, config: RequestConfig = {}): Promise { + return this.http.request("GET", `topic/${encodeURIComponent(id)}`, { + query: { support_platform: params.support_platform }, + signal: config.signal, + }); + } + + /** Creates or updates an article topic. */ + upsert(params: UpsertTopicParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "topics", { body: params, signal: config.signal }); + } +} diff --git a/src/resources/traffic-groups.ts b/src/resources/traffic-groups.ts new file mode 100644 index 0000000..aab1c8d --- /dev/null +++ b/src/resources/traffic-groups.ts @@ -0,0 +1,94 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { + CreateTrafficGroupParams, + TrafficGroup, + TrafficGroupTarget, + TrafficGroupTargetParams, + UpdateTrafficGroupParams, +} from "../models/traffic-groups.js"; + +interface ListResponse { + traffic_groups: TrafficGroup[]; +} + +/** + * Traffic group management endpoints. Requires a Management API key. + */ +export class TrafficGroups { + constructor(private readonly http: HttpClient) {} + + /** Lists all traffic groups for the company. */ + async list(config: RequestConfig = {}): Promise { + const rsp = await this.http.request("GET", "traffic-groups", { + signal: config.signal, + }); + return rsp.traffic_groups; + } + + /** Creates a new traffic group. */ + create(params: CreateTrafficGroupParams, config: RequestConfig = {}): Promise { + return this.http.request("POST", "traffic-groups", { body: params, signal: config.signal }); + } + + /** Updates an existing traffic group. */ + update( + id: string, + params: UpdateTrafficGroupParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("PUT", `traffic-groups/${encodeURIComponent(id)}`, { + body: params, + signal: config.signal, + }); + } + + /** Deletes a traffic group and all associated targets. */ + delete(id: string, config: RequestConfig = {}): Promise { + return this.http.request("DELETE", `traffic-groups/${encodeURIComponent(id)}`, { + signal: config.signal, + }); + } + + /** Adds a target to a traffic group. */ + addTarget( + id: string, + params: TrafficGroupTargetParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", `traffic-groups/${encodeURIComponent(id)}/targets`, { + body: params, + signal: config.signal, + }); + } + + /** Removes a target from a traffic group. */ + removeTarget(id: string, targetId: string, config: RequestConfig = {}): Promise { + return this.http.request( + "DELETE", + `traffic-groups/${encodeURIComponent(id)}/targets/${encodeURIComponent(targetId)}`, + { signal: config.signal }, + ); + } + + /** Excludes a target (e.g. a procedure) from a traffic group. */ + addExclusion( + id: string, + params: TrafficGroupTargetParams, + config: RequestConfig = {}, + ): Promise { + return this.http.request("POST", `traffic-groups/${encodeURIComponent(id)}/exclusions`, { + body: params, + signal: config.signal, + }); + } + + /** Removes a target exclusion from a traffic group. */ + removeExclusion(id: string, targetId: string, config: RequestConfig = {}): Promise { + return this.http.request( + "DELETE", + `traffic-groups/${encodeURIComponent(id)}/exclusions/${encodeURIComponent(targetId)}`, + { signal: config.signal }, + ); + } +} diff --git a/src/resources/voice.ts b/src/resources/voice.ts new file mode 100644 index 0000000..b371ae7 --- /dev/null +++ b/src/resources/voice.ts @@ -0,0 +1,32 @@ +import type { HttpClient } from "../internal/http.js"; +import type { RequestConfig } from "../request-config.js"; +import type { ReadVoiceCallContextParams, VoiceCallContext } from "../models/voice.js"; + +/** + * Voice endpoints. Requires an Integration API key. + */ +export class Voice { + constructor(private readonly http: HttpClient) {} + + /** + * Retrieves the most recent call context for a given phone number. Throws an + * {@link ApiError} with status 404 if there have been no recent call events. + */ + getLatestCallContext( + phoneNumber: string, + params: ReadVoiceCallContextParams = {}, + config: RequestConfig = {}, + ): Promise { + return this.http.request( + "GET", + `voice/latest-call-context/${encodeURIComponent(phoneNumber)}`, + { + query: { + lookback_seconds: params.lookback_seconds, + include_large_fields: params.include_large_fields, + }, + signal: config.signal, + }, + ); + } +} diff --git a/src/webhooks/events.ts b/src/webhooks/events.ts new file mode 100644 index 0000000..f9fc83b --- /dev/null +++ b/src/webhooks/events.ts @@ -0,0 +1,140 @@ +import type { BackOfficeTask } from "../models/back-office-tasks.js"; +import type { CustomerSource } from "../models/enums.js"; + +/** The set of webhook event types currently delivered by Gradient Labs. */ +export const WebhookType = { + AgentMessage: "agent.message", + ConversationHandOff: "conversation.hand_off", + ConversationFinished: "conversation.finished", + ActionExecute: "action.execute", + ResourcePull: "resource.pull", + BackOfficeTaskComplete: "back-office-task.complete", + BackOfficeTaskHandOff: "back-office-task.hand-off", + BackOfficeTaskFail: "back-office-task.fail", +} as const; +export type WebhookType = (typeof WebhookType)[keyof typeof WebhookType]; + +/** + * The conversation an `agent.message`, `conversation.hand_off`, or + * `conversation.finished` event relates to. + */ +export interface WebhookConversation { + id: string; + customer_id: string; +} + +/** + * The conversation an `action.execute` or `resource.pull` event relates to. + * Present only when the action ran in a conversation context. + */ +export interface ActionWebhookConversation { + id: string; + customer_id: string; + customer_source: CustomerSource; + /** Metadata attached to the conversation when it was started. */ + metadata: unknown; +} + +/** + * The back-office task an `action.execute` or `resource.pull` event relates to. + * Present only when the action ran in a back-office task context. + */ +export interface ActionWebhookBackOfficeTask { + id: string; +} + +export interface AgentMessageEvent { + conversation: WebhookConversation; + body: string; + total?: number; + sequence?: number; + intent?: string; + /** Whether this is a holding response sent while the agent works. */ + is_holding?: boolean; + /** External ID of the customer message this turn is responding to. */ + last_customer_message_id?: string; +} + +export interface ConversationHandOffEvent { + conversation: WebhookConversation; + target?: string; + /** Coded reason the agent wants to hand off. */ + reason_code: string; + /** Human-legible description of the reason code. */ + reason: string; + note?: string; + intent?: string; +} + +export interface ConversationFinishedEvent { + conversation: WebhookConversation; + reason_code?: string; + intent?: string; +} + +export interface ActionExecuteEvent { + action: string; + /** Arguments to execute the action with. */ + params: unknown; + /** Set when the action ran in a conversation context. */ + conversation?: ActionWebhookConversation; + /** Set when the action ran in a back-office task context. */ + back_office_task?: ActionWebhookBackOfficeTask; +} + +export interface ResourcePullEvent { + resource_type: string; + /** Set when the pull ran in a conversation context. */ + conversation?: ActionWebhookConversation; + /** Set when the pull ran in a back-office task context. */ + back_office_task?: ActionWebhookBackOfficeTask; +} + +/** + * The three back-office task events all carry the full task under a single + * `back_office_task` key. The outcome (result / failure reasons / hand-off + * reason) lives inside that task object. + */ +export interface BackOfficeTaskCompleteEvent { + back_office_task: BackOfficeTask; +} + +export interface BackOfficeTaskHandOffEvent { + back_office_task: BackOfficeTask; +} + +export interface BackOfficeTaskFailEvent { + back_office_task: BackOfficeTask; +} + +/** Fields common to every webhook envelope. */ +interface WebhookBase { + id: string; + sequence_number: number; + /** RFC3339 timestamp of when the event was generated. */ + timestamp: string; +} + +/** + * A parsed, verified webhook event. Discriminate on `type` for exhaustive, + * type-safe handling of the `data` payload. + */ +export type WebhookEvent = + | (WebhookBase & { type: "agent.message"; data: AgentMessageEvent }) + | (WebhookBase & { type: "conversation.hand_off"; data: ConversationHandOffEvent }) + | (WebhookBase & { type: "conversation.finished"; data: ConversationFinishedEvent }) + | (WebhookBase & { type: "action.execute"; data: ActionExecuteEvent }) + | (WebhookBase & { type: "resource.pull"; data: ResourcePullEvent }) + | (WebhookBase & { type: "back-office-task.complete"; data: BackOfficeTaskCompleteEvent }) + | (WebhookBase & { type: "back-office-task.hand-off"; data: BackOfficeTaskHandOffEvent }) + | (WebhookBase & { type: "back-office-task.fail"; data: BackOfficeTaskFailEvent }); + +/** The result of parsing a webhook request. */ +export interface ParsedWebhook { + event: WebhookEvent; + /** + * The optional sensitive conversation token from the `X-GradientLabs-Token` + * header, if present. + */ + token?: string; +} diff --git a/src/webhooks/verifier.ts b/src/webhooks/verifier.ts new file mode 100644 index 0000000..054c1a7 --- /dev/null +++ b/src/webhooks/verifier.ts @@ -0,0 +1,206 @@ +import { createHmac, timingSafeEqual } from "node:crypto"; + +import { GradientLabsError } from "../errors.js"; +import { WebhookType, type ParsedWebhook, type WebhookEvent } from "./events.js"; + +const SIGNATURE_HEADER = "x-gradientlabs-signature"; +const TOKEN_HEADER = "x-gradientlabs-token"; +const DEFAULT_LEEWAY_MS = 5 * 60 * 1000; + +/** Thrown when a webhook's signature or timestamp cannot be verified. Respond 401. */ +export class InvalidWebhookSignatureError extends GradientLabsError { + constructor(message = "webhook signature is invalid") { + super(message); + this.name = "InvalidWebhookSignatureError"; + } +} + +/** Thrown when a webhook of an unrecognised type is received. Log it and respond 200. */ +export class UnknownWebhookTypeError extends GradientLabsError { + readonly type: string; + constructor(type: string) { + super(`unknown webhook event type received: ${type}`); + this.name = "UnknownWebhookTypeError"; + this.type = type; + } +} + +/** A source of request headers: a Fetch `Headers`, a Node headers object, or a plain map. */ +export type HeadersLike = + | { get(name: string): string | null } + | Record; + +/** The raw request body. Signatures are computed over the exact bytes received. */ +export type WebhookBody = string | Uint8Array; + +export interface WebhookVerifierConfig { + /** The webhook signing key configured for your workspace. */ + signingKey: string; + /** Maximum accepted age of a webhook, in milliseconds. Defaults to 5 minutes. */ + leewayMs?: number; + /** Injectable clock for testing. Defaults to Date.now. */ + now?: () => number; +} + +/** + * Verifies the authenticity of requests to your webhook endpoint using the + * `X-GradientLabs-Signature` header (format `t=,v1=`). + */ +export class WebhookVerifier { + private readonly signingKey: string; + private readonly leewayMs: number; + private readonly now: () => number; + + constructor(config: WebhookVerifierConfig) { + this.signingKey = config.signingKey; + this.leewayMs = config.leewayMs ?? DEFAULT_LEEWAY_MS; + this.now = config.now ?? Date.now; + } + + /** + * Verifies a webhook's signature and timestamp. Throws + * {@link InvalidWebhookSignatureError} if verification fails. + */ + verify(args: { body: WebhookBody; signature: string | null | undefined }): void { + const body = toBuffer(args.body); + const { timestamp, signatures } = parseSignatureHeader(args.signature); + + if (Math.abs(this.now() - timestamp * 1000) > this.leewayMs) { + throw new InvalidWebhookSignatureError("webhook timestamp is outside the allowed leeway"); + } + + const expected = this.computeSignature(timestamp, body); + for (const candidate of signatures) { + if (constantTimeEqual(expected, candidate)) { + return; + } + } + throw new InvalidWebhookSignatureError(); + } + + /** + * Verifies a webhook request, then parses it into a typed event. Returns the + * event along with the optional `X-GradientLabs-Token` passthrough. + * + * Verification always happens before the event type is inspected. + */ + parse(args: { body: WebhookBody; headers: HeadersLike }): ParsedWebhook { + const signature = getHeader(args.headers, SIGNATURE_HEADER); + this.verify({ body: args.body, signature }); + + const text = + typeof args.body === "string" ? args.body : Buffer.from(args.body).toString("utf8"); + const payload = JSON.parse(text) as { + id: string; + type: string; + sequence_number: number; + timestamp: string; + data: unknown; + }; + + if (!isKnownType(payload.type)) { + throw new UnknownWebhookTypeError(payload.type); + } + + const event = { + id: payload.id, + type: payload.type, + sequence_number: payload.sequence_number, + timestamp: payload.timestamp, + data: payload.data, + } as WebhookEvent; + + const token = getHeader(args.headers, TOKEN_HEADER) ?? undefined; + return { event, token }; + } + + private computeSignature(timestamp: number, body: Buffer): Buffer { + return createHmac("sha256", this.signingKey).update(`${timestamp}.`).update(body).digest(); + } +} + +function isKnownType(type: string): type is WebhookEvent["type"] { + return (Object.values(WebhookType) as string[]).includes(type); +} + +function parseSignatureHeader(header: string | null | undefined): { + timestamp: number; + signatures: Buffer[]; +} { + if (!header) { + throw new InvalidWebhookSignatureError("missing signature header"); + } + + let timestamp: number | undefined; + const signatures: Buffer[] = []; + + for (const pair of header.split(",")) { + const idx = pair.indexOf("="); + if (idx === -1) { + throw new InvalidWebhookSignatureError("malformed signature header"); + } + const key = pair.slice(0, idx); + const value = pair.slice(idx + 1); + if (key === "t") { + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed)) { + throw new InvalidWebhookSignatureError("invalid timestamp component"); + } + timestamp = parsed; + } else if (key === "v1") { + try { + signatures.push(Buffer.from(value, "hex")); + } catch { + throw new InvalidWebhookSignatureError("invalid signature component"); + } + } + } + + if (timestamp === undefined) { + throw new InvalidWebhookSignatureError("signature header contains no timestamp component"); + } + if (signatures.length === 0) { + throw new InvalidWebhookSignatureError("signature header contains no v1 signature"); + } + + return { timestamp, signatures }; +} + +function constantTimeEqual(a: Buffer, b: Buffer): boolean { + if (a.length !== b.length) { + return false; + } + return timingSafeEqual(a, b); +} + +function toBuffer(body: WebhookBody): Buffer { + return typeof body === "string" ? Buffer.from(body, "utf8") : Buffer.from(body); +} + +function getHeader(headers: HeadersLike, name: string): string | null { + if (typeof (headers as { get?: unknown }).get === "function") { + return (headers as { get(n: string): string | null }).get(name); + } + const record = headers as Record; + // Node lower-cases header keys; check the canonical lower-case form plus a + // case-insensitive fallback. + const direct = record[name]; + const value = direct ?? findCaseInsensitive(record, name); + if (value === undefined) { + return null; + } + return Array.isArray(value) ? (value[0] ?? null) : value; +} + +function findCaseInsensitive( + record: Record, + name: string, +): string | string[] | undefined { + const lower = name.toLowerCase(); + for (const key of Object.keys(record)) { + if (key.toLowerCase() === lower) { + return record[key]; + } + } + return undefined; +} diff --git a/test/errors.test.ts b/test/errors.test.ts new file mode 100644 index 0000000..d465069 --- /dev/null +++ b/test/errors.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import { + ApiError, + ConfigurationError, + ErrorCode, + GradientLabs, + GradientLabsError, +} from "../src/index.js"; + +describe("errors", () => { + it("requires an apiKey", () => { + expect(() => new GradientLabs({ apiKey: "" })).toThrow(ConfigurationError); + }); + + it("exposes typed well-known error codes", () => { + expect(ErrorCode.NotFound).toBe("not_found"); + expect(ErrorCode.PermissionDenied).toBe("permission_denied"); + expect(ErrorCode.FailedPrecondition).toBe("failed_precondition"); + }); + + it("ApiError carries status, code, message, and details", () => { + const err = new ApiError({ + statusCode: 403, + code: "permission_denied", + message: "nope", + details: { trace_id: "t-1", extra: 5 }, + }); + expect(err).toBeInstanceOf(GradientLabsError); + expect(err.statusCode).toBe(403); + expect(err.code).toBe("permission_denied"); + expect(err.message).toBe("nope"); + expect(err.details["extra"]).toBe(5); + expect(err.traceId).toBe("t-1"); + }); + + it("ApiError.traceId is undefined when absent", () => { + const err = new ApiError({ statusCode: 500, code: "internal", message: "boom" }); + expect(err.traceId).toBeUndefined(); + }); + + it("ApiError falls back to a status message when message is empty", () => { + const err = new ApiError({ statusCode: 502, code: "unavailable", message: "" }); + expect(err.message).toContain("502"); + }); +}); diff --git a/test/http.test.ts b/test/http.test.ts new file mode 100644 index 0000000..2a76d01 --- /dev/null +++ b/test/http.test.ts @@ -0,0 +1,139 @@ +import { describe, expect, it } from "vitest"; + +import { ApiError, GradientLabs, type FetchLike } from "../src/index.js"; + +interface RecordedRequest { + input: string; + method: string; + headers: Record; + body?: string; +} + +function fakeFetch( + response: { status: number; body: string }, + record: RecordedRequest[], +): FetchLike { + return (input, init) => { + record.push({ input, method: init.method, headers: init.headers, body: init.body }); + return Promise.resolve({ + status: response.status, + text: () => Promise.resolve(response.body), + }); + }; +} + +const conversationJson = JSON.stringify({ + id: "conv_1", + customer_id: "cust_1", + channel: "web", + created: "2026-01-01T00:00:00Z", + updated: "2026-01-01T00:00:00Z", + status: "active", + agent_is_active: true, + latest_intent: "", + latest_handoff_target: "", +}); + +describe("HttpClient", () => { + it("sets the Authorization bearer header on every request", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: conversationJson }, record), + }); + + await client.conversations.get("conv_1"); + + expect(record).toHaveLength(1); + expect(record[0]!.headers["Authorization"]).toBe("Bearer sk_test_123"); + }); + + it("sets a User-Agent header in the expected format", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: conversationJson }, record), + }); + + await client.conversations.get("conv_1"); + + expect(record[0]!.headers["User-Agent"]).toMatch( + /^Gradient-Labs-Node\/\d+\.\d+\.\d+ \(node\/.+\)$/, + ); + }); + + it("sets Accept and Content-Type appropriately", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 200, body: conversationJson }, record), + }); + + await client.conversations.start({ + id: "conv_1", + customer_id: "cust_1", + channel: "web", + }); + + expect(record[0]!.headers["Accept"]).toBe("application/json"); + expect(record[0]!.headers["Content-Type"]).toBe("application/json"); + expect(record[0]!.body).toContain('"id":"conv_1"'); + }); + + it("respects a custom base URL", async () => { + const record: RecordedRequest[] = []; + const client = new GradientLabs({ + apiKey: "sk_test_123", + baseUrl: "https://example.test/api", + fetch: fakeFetch({ status: 200, body: conversationJson }, record), + }); + + await client.conversations.get("conv_1"); + + expect(record[0]!.input).toBe("https://example.test/api/conversations/conv_1/read"); + }); + + it("maps a non-2xx response to an ApiError with status, code, message, and trace id", async () => { + const record: RecordedRequest[] = []; + const errorBody = JSON.stringify({ + code: "not_found", + message: "conversation not found", + details: { trace_id: "trace-abc" }, + }); + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: fakeFetch({ status: 404, body: errorBody }, record), + }); + + await expect(client.conversations.get("missing")).rejects.toMatchObject({ + statusCode: 404, + code: "not_found", + message: "conversation not found", + }); + + try { + await client.conversations.get("missing"); + } catch (err) { + expect(err).toBeInstanceOf(ApiError); + expect((err as ApiError).traceId).toBe("trace-abc"); + } + }); + + it("passes through an AbortSignal", async () => { + const controller = new AbortController(); + controller.abort(); + const client = new GradientLabs({ + apiKey: "sk_test_123", + fetch: (_input, init) => { + if (init.signal?.aborted) { + return Promise.reject(new Error("aborted")); + } + return Promise.resolve({ status: 200, text: () => Promise.resolve(conversationJson) }); + }, + }); + + await expect( + client.conversations.get("conv_1", {}, { signal: controller.signal }), + ).rejects.toThrow(); + }); +}); diff --git a/test/pagination.test.ts b/test/pagination.test.ts new file mode 100644 index 0000000..e7ceb11 --- /dev/null +++ b/test/pagination.test.ts @@ -0,0 +1,73 @@ +import { describe, expect, it } from "vitest"; + +import { GradientLabs, type FetchLike } from "../src/index.js"; +import { paginate } from "../src/internal/pagination.js"; + +describe("paginate", () => { + it("follows next cursors until exhausted", async () => { + const pages = [ + { data: [1, 2], pageInfo: { next: "c1" } }, + { data: [3, 4], pageInfo: { next: "c2" } }, + { data: [5], pageInfo: {} }, + ]; + const seenCursors: Array = []; + + const collected: number[] = []; + for await (const n of paginate((cursor) => { + seenCursors.push(cursor); + return Promise.resolve(pages[seenCursors.length - 1]!); + })) { + collected.push(n); + } + + expect(collected).toEqual([1, 2, 3, 4, 5]); + expect(seenCursors).toEqual([undefined, "c1", "c2"]); + }); + + it("stops after a single page when there is no next cursor", async () => { + let calls = 0; + const collected: string[] = []; + for await (const item of paginate(() => { + calls += 1; + return Promise.resolve({ data: ["only"], pageInfo: {} }); + })) { + collected.push(item); + } + expect(collected).toEqual(["only"]); + expect(calls).toBe(1); + }); +}); + +describe("procedures.listAll", () => { + it("transparently pages through the procedures endpoint", async () => { + const page1 = JSON.stringify({ + procedures: [{ id: "p1" }, { id: "p2" }], + pagination: { next: "cursor-2" }, + }); + const page2 = JSON.stringify({ + procedures: [{ id: "p3" }], + pagination: {}, + }); + const requestedUrls: string[] = []; + const responses = [page1, page2]; + + const fetchImpl: FetchLike = (input) => { + requestedUrls.push(input); + const body = responses[requestedUrls.length - 1]!; + return Promise.resolve({ status: 200, text: () => Promise.resolve(body) }); + }; + + const client = new GradientLabs({ apiKey: "sk_test", fetch: fetchImpl }); + + const ids: string[] = []; + for await (const procedure of client.procedures.listAll()) { + ids.push(procedure.id); + } + + expect(ids).toEqual(["p1", "p2", "p3"]); + expect(requestedUrls).toHaveLength(2); + // First request has no cursor; second carries the cursor from page one. + expect(requestedUrls[0]).not.toContain("cursor="); + expect(requestedUrls[1]).toContain("cursor=cursor-2"); + }); +}); diff --git a/test/types.test.ts b/test/types.test.ts new file mode 100644 index 0000000..fb82cb0 --- /dev/null +++ b/test/types.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; + +import type { Conversation, Page } from "../src/index.js"; + +describe("type deserialization", () => { + it("deserializes a Conversation including optional agent metadata", () => { + const raw = JSON.stringify({ + id: "conv_1", + customer_id: "cust_1", + channel: "email", + created: "2026-01-01T00:00:00Z", + updated: "2026-01-02T00:00:00Z", + status: "finished", + agent_is_active: false, + latest_intent: "refund", + latest_handoff_target: "team_billing", + latest_agent_metadata: { + intent: "refund", + intent_handoff_target: "team_billing", + handoff_reason: "complex_case", + handoff_note: "customer wants a refund", + }, + }); + + const conv = JSON.parse(raw) as Conversation; + + expect(conv.id).toBe("conv_1"); + expect(conv.channel).toBe("email"); + expect(conv.agent_is_active).toBe(false); + expect(conv.latest_agent_metadata?.handoff_reason).toBe("complex_case"); + }); + + it("treats omitted optional fields as undefined", () => { + const conv = JSON.parse( + JSON.stringify({ + id: "conv_2", + customer_id: "cust_2", + channel: "web", + created: "2026-01-01T00:00:00Z", + updated: "2026-01-01T00:00:00Z", + status: "active", + agent_is_active: true, + latest_intent: "", + latest_handoff_target: "", + }), + ) as Conversation; + + expect(conv.latest_agent_metadata).toBeUndefined(); + }); + + it("models a Page with null cursors when there are no further pages", () => { + const page: Page = { data: ["a", "b"], pageInfo: {} }; + expect(page.pageInfo.next).toBeUndefined(); + expect(page.pageInfo.prev).toBeUndefined(); + }); +}); diff --git a/test/webhook-constant-time.test.ts b/test/webhook-constant-time.test.ts new file mode 100644 index 0000000..4c3909d --- /dev/null +++ b/test/webhook-constant-time.test.ts @@ -0,0 +1,39 @@ +import { describe, expect, it, vi } from "vitest"; + +// Assert that signature comparison goes through crypto.timingSafeEqual (a +// constant-time comparison), not a plain string/buffer equality check. +const { timingSafeEqualSpy } = vi.hoisted(() => ({ timingSafeEqualSpy: vi.fn() })); + +vi.mock("node:crypto", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + timingSafeEqual: (a: NodeJS.ArrayBufferView, b: NodeJS.ArrayBufferView) => { + timingSafeEqualSpy(); + return actual.timingSafeEqual(a, b); + }, + }; +}); + +const { createHmac } = await import("node:crypto"); +const { WebhookVerifier } = await import("../src/index.js"); + +describe("constant-time comparison", () => { + it("uses crypto.timingSafeEqual to compare signatures", () => { + const key = "whsec_key"; + const body = JSON.stringify({ + id: "evt", + type: "agent.message", + sequence_number: 1, + timestamp: "2026-01-01T00:00:00Z", + data: { conversation: { id: "c", customer_id: "u", metadata: null }, body: "hi" }, + }); + const ts = 1_700_000_000; + const sig = createHmac("sha256", key).update(`${ts}.`).update(body).digest("hex"); + + const verifier = new WebhookVerifier({ signingKey: key, now: () => ts * 1000 }); + verifier.verify({ body, signature: `t=${ts},v1=${sig}` }); + + expect(timingSafeEqualSpy).toHaveBeenCalled(); + }); +}); diff --git a/test/webhook.test.ts b/test/webhook.test.ts new file mode 100644 index 0000000..ce42c53 --- /dev/null +++ b/test/webhook.test.ts @@ -0,0 +1,212 @@ +import { createHmac } from "node:crypto"; + +import { describe, expect, it } from "vitest"; + +import { + InvalidWebhookSignatureError, + UnknownWebhookTypeError, + WebhookVerifier, +} from "../src/index.js"; + +const SIGNING_KEY = "whsec_test_key"; +const FIXED_NOW = 1_700_000_000_000; // ms +const FIXED_TS = Math.floor(FIXED_NOW / 1000); // s + +function sign(body: string, key: string, ts: number): string { + const sig = createHmac("sha256", key).update(`${ts}.`).update(body).digest("hex"); + return `t=${ts},v1=${sig}`; +} + +function newVerifier(overrides: { key?: string; leewayMs?: number } = {}): WebhookVerifier { + return new WebhookVerifier({ + signingKey: overrides.key ?? SIGNING_KEY, + leewayMs: overrides.leewayMs, + now: () => FIXED_NOW, + }); +} + +const agentMessageBody = JSON.stringify({ + id: "evt_1", + type: "agent.message", + sequence_number: 1, + timestamp: "2026-01-01T00:00:00Z", + data: { + conversation: { id: "conv_1", customer_id: "cust_1", metadata: { foo: "bar" } }, + body: "Hello!", + is_holding: false, + }, +}); + +describe("WebhookVerifier.verify", () => { + it("accepts a valid signature", () => { + const verifier = newVerifier(); + const signature = sign(agentMessageBody, SIGNING_KEY, FIXED_TS); + expect(() => verifier.verify({ body: agentMessageBody, signature })).not.toThrow(); + }); + + it("rejects a signature made with the wrong key", () => { + const verifier = newVerifier(); + const signature = sign(agentMessageBody, "whsec_wrong_key", FIXED_TS); + expect(() => verifier.verify({ body: agentMessageBody, signature })).toThrow( + InvalidWebhookSignatureError, + ); + }); + + it("rejects a tampered body", () => { + const verifier = newVerifier(); + const signature = sign(agentMessageBody, SIGNING_KEY, FIXED_TS); + expect(() => verifier.verify({ body: agentMessageBody + " ", signature })).toThrow( + InvalidWebhookSignatureError, + ); + }); + + it.each([ + ["empty", ""], + ["missing t", `v1=${"a".repeat(64)}`], + ["missing v1", `t=${FIXED_TS}`], + ["malformed pair", "not-a-pair"], + ])("rejects a malformed header (%s)", (_name, header) => { + const verifier = newVerifier(); + expect(() => verifier.verify({ body: agentMessageBody, signature: header })).toThrow( + InvalidWebhookSignatureError, + ); + }); + + it("rejects a timestamp that is too old", () => { + const verifier = newVerifier({ leewayMs: 5 * 60 * 1000 }); + const oldTs = FIXED_TS - 10 * 60; // 10 minutes ago + const signature = sign(agentMessageBody, SIGNING_KEY, oldTs); + expect(() => verifier.verify({ body: agentMessageBody, signature })).toThrow( + InvalidWebhookSignatureError, + ); + }); + + it("rejects a timestamp too far in the future", () => { + const verifier = newVerifier({ leewayMs: 5 * 60 * 1000 }); + const futureTs = FIXED_TS + 10 * 60; + const signature = sign(agentMessageBody, SIGNING_KEY, futureTs); + expect(() => verifier.verify({ body: agentMessageBody, signature })).toThrow( + InvalidWebhookSignatureError, + ); + }); + + it("accepts when one of several v1 signatures matches", () => { + const verifier = newVerifier(); + const good = createHmac("sha256", SIGNING_KEY) + .update(`${FIXED_TS}.`) + .update(agentMessageBody) + .digest("hex"); + const signature = `t=${FIXED_TS},v1=${"0".repeat(64)},v1=${good}`; + expect(() => verifier.verify({ body: agentMessageBody, signature })).not.toThrow(); + }); +}); + +describe("WebhookVerifier.parse", () => { + it("verifies, then returns the typed event and token passthrough", () => { + const verifier = newVerifier(); + const signature = sign(agentMessageBody, SIGNING_KEY, FIXED_TS); + const { event, token } = verifier.parse({ + body: agentMessageBody, + headers: { + "x-gradientlabs-signature": signature, + "x-gradientlabs-token": "secret-token", + }, + }); + + expect(token).toBe("secret-token"); + expect(event.type).toBe("agent.message"); + if (event.type === "agent.message") { + expect(event.data.body).toBe("Hello!"); + expect(event.data.conversation.customer_id).toBe("cust_1"); + } + }); + + it("works with a Fetch-style Headers object", () => { + const verifier = newVerifier(); + const signature = sign(agentMessageBody, SIGNING_KEY, FIXED_TS); + const headers = new Headers({ "X-GradientLabs-Signature": signature }); + const { event, token } = verifier.parse({ body: agentMessageBody, headers }); + expect(event.type).toBe("agent.message"); + expect(token).toBeUndefined(); + }); + + it("throws UnknownWebhookTypeError for an unrecognised type", () => { + const verifier = newVerifier(); + const body = JSON.stringify({ + id: "evt_x", + type: "something.new", + sequence_number: 1, + timestamp: "2026-01-01T00:00:00Z", + data: {}, + }); + const signature = sign(body, SIGNING_KEY, FIXED_TS); + expect(() => + verifier.parse({ body, headers: { "x-gradientlabs-signature": signature } }), + ).toThrow(UnknownWebhookTypeError); + }); + + const baseTask = { + id: "t1", + agent_id: "a1", + input: { foo: "bar" }, + created: "2026-01-01T00:00:00Z", + }; + const eventFixtures: Array<[string, unknown]> = [ + ["agent.message", { conversation: { id: "c", customer_id: "u" }, body: "hi" }], + [ + "conversation.hand_off", + { + conversation: { id: "c", customer_id: "u" }, + reason_code: "complex", + reason: "Too complex", + }, + ], + ["conversation.finished", { conversation: { id: "c", customer_id: "u" } }], + [ + "action.execute", + { + action: "create_ticket", + params: { foo: 1 }, + conversation: { id: "c", customer_id: "u", customer_source: "public-api", metadata: null }, + }, + ], + [ + "resource.pull", + { + resource_type: "order", + conversation: { id: "c", customer_id: "u", customer_source: "public-api", metadata: null }, + }, + ], + ["action.execute", { action: "lookup", params: {}, back_office_task: { id: "t1" } }], + [ + "back-office-task.complete", + { back_office_task: { ...baseTask, status: "completed", result: { result_type: "custom" } } }, + ], + [ + "back-office-task.hand-off", + { back_office_task: { ...baseTask, status: "handed-off", hand_off_reason: "needs human" } }, + ], + [ + "back-office-task.fail", + { back_office_task: { ...baseTask, status: "failed", failure_reasons: ["boom"] } }, + ], + ]; + + it.each(eventFixtures)("deserializes a %s event", (type, data) => { + const verifier = newVerifier(); + const body = JSON.stringify({ + id: "evt", + type, + sequence_number: 2, + timestamp: "2026-01-01T00:00:00Z", + data, + }); + const signature = sign(body, SIGNING_KEY, FIXED_TS); + const { event } = verifier.parse({ + body, + headers: { "x-gradientlabs-signature": signature }, + }); + expect(event.type).toBe(type); + expect(event.data).toEqual(data); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1e377bb --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": false, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "outDir": "dist", + "rootDir": "." + }, + "include": ["src", "test", "examples"], + "exclude": ["dist", "node_modules"] +} diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..8f122ea --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm", "cjs"], + dts: true, + sourcemap: true, + clean: true, + treeshake: true, + target: "node20", + outExtension({ format }) { + return { js: format === "esm" ? ".mjs" : ".cjs" }; + }, +});