From 7f9239dc3d18ca21e15ec1458dc57d5fa60f3abf Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Tue, 16 Jun 2026 14:42:52 +0100 Subject: [PATCH 1/5] feat: implement Gradient Labs Node.js/TypeScript client Full client for the public Gradient Labs API per nodejs_CLIENT_PLAN.md: - Resource-namespaced API (client.conversations.start, client.tools.list, ...) covering all Integration and Management endpoints in the OpenAPI spec. - Zero runtime dependencies: built-in fetch + node:crypto. - Typed error hierarchy (GradientLabsError / ConfigurationError / ApiError) with traceId helper and typed ErrorCode constants. - Open string enums sourced from the Go source. - Cursor pagination with Page + listAll() async iterators. - WebhookVerifier: HMAC-SHA256 signature + leeway check, constant-time compare, typed discriminated-union events, X-GradientLabs-Token passthrough. - 36 offline tests (webhook, errors, http, types), examples for 7 groups, dual ESM/CJS build via tsup, CI + publish workflows. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 40 + .github/workflows/publish.yml | 45 + .gitignore | 8 + .prettierignore | 5 + .prettierrc.json | 6 + CHANGELOG.md | 12 + LICENSE | 21 + README.md | 170 + eslint.config.js | 16 + examples/README.md | 43 + examples/articles/index.ts | 46 + examples/back-office-tasks/index.ts | 34 + examples/conversations/index.ts | 47 + examples/procedures/index.ts | 41 + examples/resources/index.ts | 54 + examples/tools/index.ts | 51 + examples/webhooks/index.ts | 69 + package-lock.json | 3829 ++++++++++++++++++++ package.json | 60 + src/client.ts | 118 + src/errors.ts | 81 + src/index.ts | 74 + src/internal/http.ts | 146 + src/internal/pagination.ts | 33 + src/internal/user-agent.ts | 11 + src/internal/version.ts | 2 + src/models/articles.ts | 64 + src/models/back-office-tasks.ts | 54 + src/models/common.ts | 19 + src/models/conversations.ts | 140 + src/models/enums.ts | 247 ++ src/models/hand-off-targets.ts | 25 + src/models/notes.ts | 44 + src/models/procedures.ts | 50 + src/models/resources.ts | 129 + src/models/secrets.ts | 23 + src/models/terminology.ts | 30 + src/models/tools.ts | 139 + src/models/traffic-groups.ts | 27 + src/models/voice.ts | 18 + src/request-config.ts | 5 + src/resources/articles.ts | 34 + src/resources/back-office-tasks.ts | 25 + src/resources/conversations.ts | 129 + src/resources/hand-off-targets.ts | 61 + src/resources/ip-addresses.ts | 22 + src/resources/notes.ts | 43 + src/resources/outbound-conversations.ts | 24 + src/resources/procedures.ts | 119 + src/resources/resource-sources.ts | 71 + src/resources/resource-types.ts | 57 + src/resources/secrets.ts | 37 + src/resources/terminology-substitutions.ts | 63 + src/resources/tools.ts | 69 + src/resources/topics.ts | 41 + src/resources/traffic-groups.ts | 94 + src/resources/voice.ts | 32 + src/webhooks/events.ts | 115 + src/webhooks/verifier.ts | 206 ++ test/errors.test.ts | 46 + test/http.test.ts | 139 + test/types.test.ts | 56 + test/webhook-constant-time.test.ts | 39 + test/webhook.test.ts | 199 + tsconfig.json | 22 + tsup.config.ts | 14 + 66 files changed, 7833 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml create mode 100644 .gitignore create mode 100644 .prettierignore create mode 100644 .prettierrc.json create mode 100644 CHANGELOG.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 eslint.config.js create mode 100644 examples/README.md create mode 100644 examples/articles/index.ts create mode 100644 examples/back-office-tasks/index.ts create mode 100644 examples/conversations/index.ts create mode 100644 examples/procedures/index.ts create mode 100644 examples/resources/index.ts create mode 100644 examples/tools/index.ts create mode 100644 examples/webhooks/index.ts create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/client.ts create mode 100644 src/errors.ts create mode 100644 src/index.ts create mode 100644 src/internal/http.ts create mode 100644 src/internal/pagination.ts create mode 100644 src/internal/user-agent.ts create mode 100644 src/internal/version.ts create mode 100644 src/models/articles.ts create mode 100644 src/models/back-office-tasks.ts create mode 100644 src/models/common.ts create mode 100644 src/models/conversations.ts create mode 100644 src/models/enums.ts create mode 100644 src/models/hand-off-targets.ts create mode 100644 src/models/notes.ts create mode 100644 src/models/procedures.ts create mode 100644 src/models/resources.ts create mode 100644 src/models/secrets.ts create mode 100644 src/models/terminology.ts create mode 100644 src/models/tools.ts create mode 100644 src/models/traffic-groups.ts create mode 100644 src/models/voice.ts create mode 100644 src/request-config.ts create mode 100644 src/resources/articles.ts create mode 100644 src/resources/back-office-tasks.ts create mode 100644 src/resources/conversations.ts create mode 100644 src/resources/hand-off-targets.ts create mode 100644 src/resources/ip-addresses.ts create mode 100644 src/resources/notes.ts create mode 100644 src/resources/outbound-conversations.ts create mode 100644 src/resources/procedures.ts create mode 100644 src/resources/resource-sources.ts create mode 100644 src/resources/resource-types.ts create mode 100644 src/resources/secrets.ts create mode 100644 src/resources/terminology-substitutions.ts create mode 100644 src/resources/tools.ts create mode 100644 src/resources/topics.ts create mode 100644 src/resources/traffic-groups.ts create mode 100644 src/resources/voice.ts create mode 100644 src/webhooks/events.ts create mode 100644 src/webhooks/verifier.ts create mode 100644 test/errors.test.ts create mode 100644 test/http.test.ts create mode 100644 test/types.test.ts create mode 100644 test/webhook-constant-time.test.ts create mode 100644 test/webhook.test.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts 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..d2ac805 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +coverage/ +*.log +.DS_Store +.env +.env.local +*.tsbuildinfo diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..97b3c91 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,5 @@ +dist/ +node_modules/ +coverage/ +CHANGELOG.md +nodejs_CLIENT_PLAN.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..c0d3004 --- /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 @gradient-labs/client +``` + +## Quick start + +```ts +import { GradientLabs } from "@gradient-labs/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 "@gradient-labs/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 "@gradient-labs/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..8bcd975 --- /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 +> `@gradient-labs/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..b076aa4 --- /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 "@gradient-labs/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..2de87fc --- /dev/null +++ b/examples/back-office-tasks/index.ts @@ -0,0 +1,34 @@ +/** + * 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 "@gradient-labs/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-task-${Date.now()}`; + + const task = await client.backOfficeTasks.create({ + id, + agent_id: process.env.GL_BACK_OFFICE_AGENT_ID, + 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..5083f35 --- /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 "@gradient-labs/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..c7ca0cb --- /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 "@gradient-labs/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..29297a7 --- /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 "@gradient-labs/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..83f1b32 --- /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 "@gradient-labs/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..e726efc --- /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 "@gradient-labs/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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..b41a957 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3829 @@ +{ + "name": "@gradient-labs/client", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@gradient-labs/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..c744787 --- /dev/null +++ b/package.json @@ -0,0 +1,60 @@ +{ + "name": "@gradient-labs/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..245d7dd --- /dev/null +++ b/src/index.ts @@ -0,0 +1,74 @@ +// Public entry point for @gradient-labs/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 WebhookBackOfficeTask, + 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..1317554 --- /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. */ + 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..714df39 --- /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..3f5953f --- /dev/null +++ b/src/models/conversations.ts @@ -0,0 +1,140 @@ +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; + 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..93af4e4 --- /dev/null +++ b/src/models/enums.ts @@ -0,0 +1,247 @@ +// 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 — see +// nodejs_CLIENT_PLAN.md §2 for the per-enum source file. + +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..c34f2d7 --- /dev/null +++ b/src/models/procedures.ts @@ -0,0 +1,50 @@ +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; + 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..560abd3 --- /dev/null +++ b/src/webhooks/events.ts @@ -0,0 +1,115 @@ +import type { BackOfficeTaskResult } from "../models/back-office-tasks.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]; + +/** Details of the conversation a webhook event relates to. */ +export interface WebhookConversation { + id: string; + customer_id: string; + /** Metadata attached to the conversation when it was started. */ + metadata: unknown; +} + +/** Details of the back-office task a webhook event relates to. */ +export interface WebhookBackOfficeTask { + id: string; + agent_id: string; + metadata?: Record; +} + +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; +} + +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; + conversation: WebhookConversation; +} + +export interface ResourcePullEvent { + resource_type: string; + conversation: WebhookConversation; +} + +export interface BackOfficeTaskCompleteEvent { + task: WebhookBackOfficeTask; + result?: BackOfficeTaskResult; +} + +export interface BackOfficeTaskHandOffEvent { + task: WebhookBackOfficeTask; + hand_off_reason?: string; +} + +export interface BackOfficeTaskFailEvent { + task: WebhookBackOfficeTask; + failure_reasons?: string[]; +} + +/** 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/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..257b736 --- /dev/null +++ b/test/webhook.test.ts @@ -0,0 +1,199 @@ +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 eventFixtures: Array<[string, unknown]> = [ + ["agent.message", { conversation: { id: "c", customer_id: "u", metadata: null }, body: "hi" }], + [ + "conversation.hand_off", + { + conversation: { id: "c", customer_id: "u", metadata: null }, + reason_code: "complex", + reason: "Too complex", + }, + ], + ["conversation.finished", { conversation: { id: "c", customer_id: "u", metadata: null } }], + [ + "action.execute", + { + action: "create_ticket", + params: { foo: 1 }, + conversation: { id: "c", customer_id: "u", metadata: null }, + }, + ], + [ + "resource.pull", + { resource_type: "order", conversation: { id: "c", customer_id: "u", metadata: null } }, + ], + [ + "back-office-task.complete", + { task: { id: "t1", agent_id: "a1" }, result: { result_type: "custom" } }, + ], + [ + "back-office-task.hand-off", + { task: { id: "t1", agent_id: "a1" }, hand_off_reason: "needs human" }, + ], + ["back-office-task.fail", { task: { id: "t1", agent_id: "a1" }, 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" }; + }, +}); From 725bc1ce954d5750acb2be95a104a3e3394cccbe Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Tue, 16 Jun 2026 15:10:58 +0100 Subject: [PATCH 2/5] fix: correct webhook payloads and request field requirements per backend review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cross-checked against the Go backend webhook publishers and request validators: - Webhooks: back-office events carry the full task under a single 'back_office_task' key (was a wrong '{task, ...siblings}' shape); the three back-office events now reuse the full BackOfficeTask model. - Webhooks: split conversation payload types — agent.message/hand_off/finished carry {id, customer_id} only (no metadata); action.execute/resource.pull use ActionWebhookConversation ({id, customer_id, customer_source, metadata}) and an optional back_office_task ref; both are optional. Added last_customer_message_id to AgentMessageEvent. - Required fields: ReturnAsyncToolResultParams.payload and CreateBackOfficeTaskParams.agent_id are required server-side (validation.Required). - Optional fields: ResumeConversationParams.resources, Attachment.summary/ description, and ProcedureVersion.GatedConfig are optional/nullable in the API. - Tests: added pagination.test.ts (paginate + procedures.listAll) and corrected the webhook event fixtures to the real shapes. 40 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 2 + examples/back-office-tasks/index.ts | 7 ++- src/index.ts | 3 +- src/models/back-office-tasks.ts | 4 +- src/models/common.ts | 4 +- src/models/conversations.ts | 5 +- src/models/procedures.ts | 3 +- src/webhooks/events.ts | 53 +++++++++++++++------ test/pagination.test.ts | 73 +++++++++++++++++++++++++++++ test/webhook.test.ts | 29 ++++++++---- 10 files changed, 152 insertions(+), 31 deletions(-) create mode 100644 test/pagination.test.ts diff --git a/.gitignore b/.gitignore index d2ac805..8517940 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ coverage/ .env .env.local *.tsbuildinfo +.idea/ +.vscode/ diff --git a/examples/back-office-tasks/index.ts b/examples/back-office-tasks/index.ts index 2de87fc..4c531cb 100644 --- a/examples/back-office-tasks/index.ts +++ b/examples/back-office-tasks/index.ts @@ -11,6 +11,11 @@ if (!apiKey) { throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); } +const agentId = process.env.GL_BACK_OFFICE_AGENT_ID ?? ""; +if (!agentId) { + throw new Error("GL_BACK_OFFICE_AGENT_ID environment variable is required"); +} + const client = new GradientLabs({ apiKey }); async function main(): Promise { @@ -18,7 +23,7 @@ async function main(): Promise { const task = await client.backOfficeTasks.create({ id, - agent_id: process.env.GL_BACK_OFFICE_AGENT_ID, + agent_id: agentId, input: { order_id: "order-123", reason: "refund_request" }, metadata: { source: "nodejs-example" }, }); diff --git a/src/index.ts b/src/index.ts index 245d7dd..9b317c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,7 +26,8 @@ export { type WebhookEvent, type ParsedWebhook, type WebhookConversation, - type WebhookBackOfficeTask, + type ActionWebhookConversation, + type ActionWebhookBackOfficeTask, type AgentMessageEvent, type ConversationHandOffEvent, type ConversationFinishedEvent, diff --git a/src/models/back-office-tasks.ts b/src/models/back-office-tasks.ts index 1317554..6b18ca9 100644 --- a/src/models/back-office-tasks.ts +++ b/src/models/back-office-tasks.ts @@ -44,8 +44,8 @@ export interface CreateBackOfficeTaskParams { 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. */ - agent_id?: string; + /** 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[]; diff --git a/src/models/common.ts b/src/models/common.ts index 714df39..3a6dc99 100644 --- a/src/models/common.ts +++ b/src/models/common.ts @@ -13,7 +13,7 @@ export interface Attachment { /** Publicly accessible URL where the attachment can be downloaded. */ url: string; /** Optional short summary of what the attachment is. */ - summary: string; + summary?: string; /** Optional full textual extract of the attachment's contents. */ - description: string; + description?: string; } diff --git a/src/models/conversations.ts b/src/models/conversations.ts index 3f5953f..344fe63 100644 --- a/src/models/conversations.ts +++ b/src/models/conversations.ts @@ -78,7 +78,7 @@ export interface AssignConversationParams { export interface ResumeConversationParams { assignee_type: ParticipantType; - resources: Record; + resources?: Record; assignee_id?: string; reason?: string; timestamp?: string; @@ -115,7 +115,8 @@ export interface RateConversationParams { export interface ReturnAsyncToolResultParams { async_tool_execution_id: string; - payload?: Record; + /** Required by the API: the JSON result of the async tool execution. */ + payload: Record; timestamp?: string; } diff --git a/src/models/procedures.ts b/src/models/procedures.ts index c34f2d7..ed1bcd1 100644 --- a/src/models/procedures.ts +++ b/src/models/procedures.ts @@ -27,7 +27,8 @@ export interface ProcedureVersion { Author: string; Created: string; Gated: boolean; - GatedConfig: GatedConfig; + /** Present only when the version is gated. */ + GatedConfig?: GatedConfig; Live: boolean; } diff --git a/src/webhooks/events.ts b/src/webhooks/events.ts index 560abd3..f9fc83b 100644 --- a/src/webhooks/events.ts +++ b/src/webhooks/events.ts @@ -1,4 +1,5 @@ -import type { BackOfficeTaskResult } from "../models/back-office-tasks.js"; +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 = { @@ -13,19 +14,33 @@ export const WebhookType = { } as const; export type WebhookType = (typeof WebhookType)[keyof typeof WebhookType]; -/** Details of the conversation a webhook event relates to. */ +/** + * 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; } -/** Details of the back-office task a webhook event relates to. */ -export interface WebhookBackOfficeTask { +/** + * 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; - agent_id: string; - metadata?: Record; } export interface AgentMessageEvent { @@ -36,6 +51,8 @@ export interface AgentMessageEvent { 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 { @@ -59,27 +76,35 @@ export interface ActionExecuteEvent { action: string; /** Arguments to execute the action with. */ params: unknown; - conversation: WebhookConversation; + /** 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; - conversation: WebhookConversation; + /** 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 { - task: WebhookBackOfficeTask; - result?: BackOfficeTaskResult; + back_office_task: BackOfficeTask; } export interface BackOfficeTaskHandOffEvent { - task: WebhookBackOfficeTask; - hand_off_reason?: string; + back_office_task: BackOfficeTask; } export interface BackOfficeTaskFailEvent { - task: WebhookBackOfficeTask; - failure_reasons?: string[]; + back_office_task: BackOfficeTask; } /** Fields common to every webhook envelope. */ 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/webhook.test.ts b/test/webhook.test.ts index 257b736..ce42c53 100644 --- a/test/webhook.test.ts +++ b/test/webhook.test.ts @@ -145,38 +145,51 @@ describe("WebhookVerifier.parse", () => { ).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", metadata: null }, body: "hi" }], + ["agent.message", { conversation: { id: "c", customer_id: "u" }, body: "hi" }], [ "conversation.hand_off", { - conversation: { id: "c", customer_id: "u", metadata: null }, + conversation: { id: "c", customer_id: "u" }, reason_code: "complex", reason: "Too complex", }, ], - ["conversation.finished", { conversation: { id: "c", customer_id: "u", metadata: null } }], + ["conversation.finished", { conversation: { id: "c", customer_id: "u" } }], [ "action.execute", { action: "create_ticket", params: { foo: 1 }, - conversation: { id: "c", customer_id: "u", metadata: null }, + conversation: { id: "c", customer_id: "u", customer_source: "public-api", metadata: null }, }, ], [ "resource.pull", - { resource_type: "order", conversation: { id: "c", customer_id: "u", metadata: null } }, + { + 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", - { task: { id: "t1", agent_id: "a1" }, result: { result_type: "custom" } }, + { back_office_task: { ...baseTask, status: "completed", result: { result_type: "custom" } } }, ], [ "back-office-task.hand-off", - { task: { id: "t1", agent_id: "a1" }, hand_off_reason: "needs human" }, + { 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"] } }, ], - ["back-office-task.fail", { task: { id: "t1", agent_id: "a1" }, failure_reasons: ["boom"] }], ]; it.each(eventFixtures)("deserializes a %s event", (type, data) => { From 531c16a0898c13bc152cb791ac3cdb93cd78a85a Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Tue, 16 Jun 2026 15:12:15 +0100 Subject: [PATCH 3/5] docs(examples): pass back-office agent_id as a literal, not an env var agent_id is an ordinary parameter (e.g. "boagent_12345"), not an environment concern; use a literal placeholder in the example. Co-Authored-By: Claude Opus 4.8 (1M context) --- examples/back-office-tasks/index.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/examples/back-office-tasks/index.ts b/examples/back-office-tasks/index.ts index 4c531cb..f4db84d 100644 --- a/examples/back-office-tasks/index.ts +++ b/examples/back-office-tasks/index.ts @@ -11,13 +11,12 @@ if (!apiKey) { throw new Error("GRADIENT_LABS_API_KEY environment variable is required"); } -const agentId = process.env.GL_BACK_OFFICE_AGENT_ID ?? ""; -if (!agentId) { - throw new Error("GL_BACK_OFFICE_AGENT_ID 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()}`; From 52447e83cce159d1f9609bf0e3edd4cadb35a3c5 Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Tue, 16 Jun 2026 15:25:04 +0100 Subject: [PATCH 4/5] fix: use @gradientlabs npm scope (not @gradient-labs) The npm organization is 'gradientlabs', so the package is @gradientlabs/client. Updated package.json/lockfile, README, examples, plan, and source references. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 8 ++++---- examples/README.md | 2 +- examples/articles/index.ts | 2 +- examples/back-office-tasks/index.ts | 2 +- examples/conversations/index.ts | 2 +- examples/procedures/index.ts | 2 +- examples/resources/index.ts | 2 +- examples/tools/index.ts | 2 +- examples/webhooks/index.ts | 2 +- nodejs_CLIENT_PLAN.md | 6 +++--- package-lock.json | 4 ++-- package.json | 2 +- src/index.ts | 2 +- 13 files changed, 19 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c0d3004..804430d 100644 --- a/README.md +++ b/README.md @@ -9,13 +9,13 @@ Requires **Node.js 20** or newer. ## Installation ```bash -npm install @gradient-labs/client +npm install @gradientlabs/client ``` ## Quick start ```ts -import { GradientLabs } from "@gradient-labs/client"; +import { GradientLabs } from "@gradientlabs/client"; const client = new GradientLabs({ apiKey: process.env.GRADIENT_LABS_API_KEY! }); @@ -65,7 +65,7 @@ Non-2xx responses throw an `ApiError`; client misconfiguration throws a `ConfigurationError`. Both extend `GradientLabsError`. ```ts -import { ApiError, ErrorCode } from "@gradient-labs/client"; +import { ApiError, ErrorCode } from "@gradientlabs/client"; try { await client.conversations.get("missing"); @@ -104,7 +104,7 @@ incoming requests. Pass the **raw** request body — the signature is computed o the exact bytes received. ```ts -import { GradientLabs, InvalidWebhookSignatureError } from "@gradient-labs/client"; +import { GradientLabs, InvalidWebhookSignatureError } from "@gradientlabs/client"; const client = new GradientLabs({ apiKey: process.env.GRADIENT_LABS_API_KEY!, diff --git a/examples/README.md b/examples/README.md index 8bcd975..f4bb37f 100644 --- a/examples/README.md +++ b/examples/README.md @@ -5,7 +5,7 @@ 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 -> `@gradient-labs/client` instead. +> `@gradientlabs/client` instead. ## Prerequisites diff --git a/examples/articles/index.ts b/examples/articles/index.ts index b076aa4..ba11b5a 100644 --- a/examples/articles/index.ts +++ b/examples/articles/index.ts @@ -2,7 +2,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/back-office-tasks/index.ts b/examples/back-office-tasks/index.ts index f4db84d..653c96f 100644 --- a/examples/back-office-tasks/index.ts +++ b/examples/back-office-tasks/index.ts @@ -2,7 +2,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/conversations/index.ts b/examples/conversations/index.ts index 5083f35..0cbb9c4 100644 --- a/examples/conversations/index.ts +++ b/examples/conversations/index.ts @@ -3,7 +3,7 @@ * it back, then finishes it. * * In your own project, import from the published package: - * import { GradientLabs } from "@gradient-labs/client"; + * import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/procedures/index.ts b/examples/procedures/index.ts index c7ca0cb..f873a28 100644 --- a/examples/procedures/index.ts +++ b/examples/procedures/index.ts @@ -2,7 +2,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/resources/index.ts b/examples/resources/index.ts index 29297a7..8a051c1 100644 --- a/examples/resources/index.ts +++ b/examples/resources/index.ts @@ -3,7 +3,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/tools/index.ts b/examples/tools/index.ts index 83f1b32..80b411d 100644 --- a/examples/tools/index.ts +++ b/examples/tools/index.ts @@ -2,7 +2,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { GradientLabs } from "../../src/index.js"; diff --git a/examples/webhooks/index.ts b/examples/webhooks/index.ts index e726efc..05540ae 100644 --- a/examples/webhooks/index.ts +++ b/examples/webhooks/index.ts @@ -5,7 +5,7 @@ * 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 "@gradient-labs/client"; + * In your own project: import { GradientLabs } from "@gradientlabs/client"; */ import { createServer } from "node:http"; diff --git a/nodejs_CLIENT_PLAN.md b/nodejs_CLIENT_PLAN.md index 26ff9aa..4fe7f16 100644 --- a/nodejs_CLIENT_PLAN.md +++ b/nodejs_CLIENT_PLAN.md @@ -8,7 +8,7 @@ | Decision | Choice | |----------|--------| -| Package name | `@gradient-labs/client` (npm) | +| Package name | `@gradientlabs/client` (npm) | | Registry | npm (public) | | Repo name | `gradientlabs-ai/nodejs-client` | | Language / build | TypeScript → compiled JS, ships `.d.ts`, **dual ESM + CommonJS** | @@ -241,7 +241,7 @@ from the Go source (paths below), **not** invented: Instantiated with a single options object (idiomatic TS; avoids Go's functional-options ceremony): ```ts -import { GradientLabs } from "@gradient-labs/client"; +import { GradientLabs } from "@gradientlabs/client"; const client = new GradientLabs({ apiKey: process.env.GRADIENT_LABS_API_KEY!, // required @@ -450,7 +450,7 @@ and `node:crypto` cover HTTP, cancellation, and HMAC — no `axios`/`node-fetch` **`package.json` essentials:** ```jsonc { - "name": "@gradient-labs/client", + "name": "@gradientlabs/client", "version": "0.1.0", "type": "module", "engines": { "node": ">=20" }, diff --git a/package-lock.json b/package-lock.json index b41a957..21708a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "@gradient-labs/client", + "name": "@gradientlabs/client", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "@gradient-labs/client", + "name": "@gradientlabs/client", "version": "0.1.0", "license": "MIT", "devDependencies": { diff --git a/package.json b/package.json index c744787..a93354b 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@gradient-labs/client", + "name": "@gradientlabs/client", "version": "0.1.0", "description": "Official Node.js / TypeScript client for the Gradient Labs API", "type": "module", diff --git a/src/index.ts b/src/index.ts index 9b317c1..cb50c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,4 @@ -// Public entry point for @gradient-labs/client. +// Public entry point for @gradientlabs/client. export { GradientLabs, type GradientLabsConfig } from "./client.js"; export { type RequestConfig } from "./request-config.js"; From a4c99a8de884ed146d10d3121e87fab7e2982869 Mon Sep 17 00:00:00 2001 From: Arthur Ceccotti Date: Tue, 16 Jun 2026 15:51:21 +0100 Subject: [PATCH 5/5] Remove nodejs_CLIENT_PLAN.md and its references The plan file has served its purpose now the client is implemented. Drop it along with the .prettierignore entry and the enums.ts comment that pointed at it. Co-Authored-By: Claude Opus 4.8 (1M context) --- .prettierignore | 1 - nodejs_CLIENT_PLAN.md | 531 ------------------------------------------ src/models/enums.ts | 3 +- 3 files changed, 1 insertion(+), 534 deletions(-) delete mode 100644 nodejs_CLIENT_PLAN.md diff --git a/.prettierignore b/.prettierignore index 97b3c91..e88c14f 100644 --- a/.prettierignore +++ b/.prettierignore @@ -2,4 +2,3 @@ dist/ node_modules/ coverage/ CHANGELOG.md -nodejs_CLIENT_PLAN.md diff --git a/nodejs_CLIENT_PLAN.md b/nodejs_CLIENT_PLAN.md deleted file mode 100644 index 4fe7f16..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 | `@gradientlabs/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 "@gradientlabs/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": "@gradientlabs/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/src/models/enums.ts b/src/models/enums.ts index 93af4e4..cfbf444 100644 --- a/src/models/enums.ts +++ b/src/models/enums.ts @@ -2,8 +2,7 @@ // 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 — see -// nodejs_CLIENT_PLAN.md §2 for the per-enum source file. +// Values are sourced verbatim from the wearegradient Go source. export const ArticleStatus = { Draft: "draft",