From 4d48791f3cda0d9ef207be52613d77f4134de168 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Tue, 23 Dec 2025 23:24:38 +0800 Subject: [PATCH] refactor: nodejs sdk (#30036) Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- sdks/nodejs-client/.gitignore | 58 +- sdks/nodejs-client/LICENSE | 22 + sdks/nodejs-client/README.md | 118 +- sdks/nodejs-client/babel.config.cjs | 12 - sdks/nodejs-client/eslint.config.js | 45 + sdks/nodejs-client/index.d.ts | 107 - sdks/nodejs-client/index.js | 351 --- sdks/nodejs-client/index.test.js | 141 - sdks/nodejs-client/jest.config.cjs | 6 - sdks/nodejs-client/package.json | 62 +- sdks/nodejs-client/pnpm-lock.yaml | 2802 +++++++++++++++++ sdks/nodejs-client/scripts/publish.sh | 261 ++ sdks/nodejs-client/src/client/base.test.js | 175 + sdks/nodejs-client/src/client/base.ts | 284 ++ sdks/nodejs-client/src/client/chat.test.js | 239 ++ sdks/nodejs-client/src/client/chat.ts | 377 +++ .../src/client/completion.test.js | 83 + sdks/nodejs-client/src/client/completion.ts | 111 + .../src/client/knowledge-base.test.js | 249 ++ .../src/client/knowledge-base.ts | 706 +++++ .../src/client/validation.test.js | 91 + sdks/nodejs-client/src/client/validation.ts | 136 + .../nodejs-client/src/client/workflow.test.js | 119 + sdks/nodejs-client/src/client/workflow.ts | 165 + .../src/client/workspace.test.js | 21 + sdks/nodejs-client/src/client/workspace.ts | 16 + .../src/errors/dify-error.test.js | 37 + sdks/nodejs-client/src/errors/dify-error.ts | 75 + sdks/nodejs-client/src/http/client.test.js | 304 ++ sdks/nodejs-client/src/http/client.ts | 368 +++ sdks/nodejs-client/src/http/form-data.test.js | 23 + sdks/nodejs-client/src/http/form-data.ts | 31 + sdks/nodejs-client/src/http/retry.test.js | 38 + sdks/nodejs-client/src/http/retry.ts | 40 + sdks/nodejs-client/src/http/sse.test.js | 76 + sdks/nodejs-client/src/http/sse.ts | 133 + sdks/nodejs-client/src/index.test.js | 227 ++ sdks/nodejs-client/src/index.ts | 103 + sdks/nodejs-client/src/types/annotation.ts | 18 + sdks/nodejs-client/src/types/chat.ts | 17 + sdks/nodejs-client/src/types/common.ts | 71 + sdks/nodejs-client/src/types/completion.ts | 13 + .../nodejs-client/src/types/knowledge-base.ts | 184 ++ sdks/nodejs-client/src/types/workflow.ts | 12 + sdks/nodejs-client/src/types/workspace.ts | 2 + sdks/nodejs-client/tests/test-utils.js | 30 + sdks/nodejs-client/tsconfig.json | 17 + sdks/nodejs-client/tsup.config.ts | 12 + sdks/nodejs-client/vitest.config.ts | 14 + 49 files changed, 7901 insertions(+), 701 deletions(-) create mode 100644 sdks/nodejs-client/LICENSE delete mode 100644 sdks/nodejs-client/babel.config.cjs create mode 100644 sdks/nodejs-client/eslint.config.js delete mode 100644 sdks/nodejs-client/index.d.ts delete mode 100644 sdks/nodejs-client/index.js delete mode 100644 sdks/nodejs-client/index.test.js delete mode 100644 sdks/nodejs-client/jest.config.cjs create mode 100644 sdks/nodejs-client/pnpm-lock.yaml create mode 100755 sdks/nodejs-client/scripts/publish.sh create mode 100644 sdks/nodejs-client/src/client/base.test.js create mode 100644 sdks/nodejs-client/src/client/base.ts create mode 100644 sdks/nodejs-client/src/client/chat.test.js create mode 100644 sdks/nodejs-client/src/client/chat.ts create mode 100644 sdks/nodejs-client/src/client/completion.test.js create mode 100644 sdks/nodejs-client/src/client/completion.ts create mode 100644 sdks/nodejs-client/src/client/knowledge-base.test.js create mode 100644 sdks/nodejs-client/src/client/knowledge-base.ts create mode 100644 sdks/nodejs-client/src/client/validation.test.js create mode 100644 sdks/nodejs-client/src/client/validation.ts create mode 100644 sdks/nodejs-client/src/client/workflow.test.js create mode 100644 sdks/nodejs-client/src/client/workflow.ts create mode 100644 sdks/nodejs-client/src/client/workspace.test.js create mode 100644 sdks/nodejs-client/src/client/workspace.ts create mode 100644 sdks/nodejs-client/src/errors/dify-error.test.js create mode 100644 sdks/nodejs-client/src/errors/dify-error.ts create mode 100644 sdks/nodejs-client/src/http/client.test.js create mode 100644 sdks/nodejs-client/src/http/client.ts create mode 100644 sdks/nodejs-client/src/http/form-data.test.js create mode 100644 sdks/nodejs-client/src/http/form-data.ts create mode 100644 sdks/nodejs-client/src/http/retry.test.js create mode 100644 sdks/nodejs-client/src/http/retry.ts create mode 100644 sdks/nodejs-client/src/http/sse.test.js create mode 100644 sdks/nodejs-client/src/http/sse.ts create mode 100644 sdks/nodejs-client/src/index.test.js create mode 100644 sdks/nodejs-client/src/index.ts create mode 100644 sdks/nodejs-client/src/types/annotation.ts create mode 100644 sdks/nodejs-client/src/types/chat.ts create mode 100644 sdks/nodejs-client/src/types/common.ts create mode 100644 sdks/nodejs-client/src/types/completion.ts create mode 100644 sdks/nodejs-client/src/types/knowledge-base.ts create mode 100644 sdks/nodejs-client/src/types/workflow.ts create mode 100644 sdks/nodejs-client/src/types/workspace.ts create mode 100644 sdks/nodejs-client/tests/test-utils.js create mode 100644 sdks/nodejs-client/tsconfig.json create mode 100644 sdks/nodejs-client/tsup.config.ts create mode 100644 sdks/nodejs-client/vitest.config.ts diff --git a/sdks/nodejs-client/.gitignore b/sdks/nodejs-client/.gitignore index 1d40ff2ece..33cfbd3b18 100644 --- a/sdks/nodejs-client/.gitignore +++ b/sdks/nodejs-client/.gitignore @@ -1,48 +1,40 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. +# Dependencies +node_modules/ -# dependencies -/node_modules -/.pnp -.pnp.js +# Build output +dist/ -# testing -/coverage +# Testing +coverage/ -# next.js -/.next/ -/out/ +# IDE +.idea/ +.vscode/ +*.swp +*.swo -# production -/build - -# misc +# OS .DS_Store -*.pem +Thumbs.db -# debug +# Debug logs npm-debug.log* yarn-debug.log* yarn-error.log* -.pnpm-debug.log* +pnpm-debug.log* -# local env files -.env*.local +# Environment +.env +.env.local +.env.*.local -# vercel -.vercel - -# typescript +# TypeScript *.tsbuildinfo -next-env.d.ts -# npm +# Lock files (use pnpm-lock.yaml in CI if needed) package-lock.json +yarn.lock -# yarn -.pnp.cjs -.pnp.loader.mjs -.yarn/ -.yarnrc.yml - -# pmpm -pnpm-lock.yaml +# Misc +*.pem +*.tgz diff --git a/sdks/nodejs-client/LICENSE b/sdks/nodejs-client/LICENSE new file mode 100644 index 0000000000..ff17417e1f --- /dev/null +++ b/sdks/nodejs-client/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 LangGenius + +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/sdks/nodejs-client/README.md b/sdks/nodejs-client/README.md index 3a5688bcbe..f8c2803c08 100644 --- a/sdks/nodejs-client/README.md +++ b/sdks/nodejs-client/README.md @@ -13,54 +13,92 @@ npm install dify-client After installing the SDK, you can use it in your project like this: ```js -import { DifyClient, ChatClient, CompletionClient } from 'dify-client' +import { + DifyClient, + ChatClient, + CompletionClient, + WorkflowClient, + KnowledgeBaseClient, + WorkspaceClient +} from 'dify-client' -const API_KEY = 'your-api-key-here' -const user = `random-user-id` +const API_KEY = 'your-app-api-key' +const DATASET_API_KEY = 'your-dataset-api-key' +const user = 'random-user-id' const query = 'Please tell me a short story in 10 words or less.' -const remote_url_files = [{ - type: 'image', - transfer_method: 'remote_url', - url: 'your_url_address' -}] -// Create a completion client -const completionClient = new CompletionClient(API_KEY) -// Create a completion message -completionClient.createCompletionMessage({'query': query}, user) -// Create a completion message with vision model -completionClient.createCompletionMessage({'query': 'Describe the picture.'}, user, false, remote_url_files) - -// Create a chat client const chatClient = new ChatClient(API_KEY) -// Create a chat message in stream mode -const response = await chatClient.createChatMessage({}, query, user, true, null) -const stream = response.data; -stream.on('data', data => { - console.log(data); -}); -stream.on('end', () => { - console.log('stream done'); -}); -// Create a chat message with vision model -chatClient.createChatMessage({}, 'Describe the picture.', user, false, null, remote_url_files) -// Fetch conversations -chatClient.getConversations(user) -// Fetch conversation messages -chatClient.getConversationMessages(conversationId, user) -// Rename conversation -chatClient.renameConversation(conversationId, name, user) - - +const completionClient = new CompletionClient(API_KEY) +const workflowClient = new WorkflowClient(API_KEY) +const kbClient = new KnowledgeBaseClient(DATASET_API_KEY) +const workspaceClient = new WorkspaceClient(DATASET_API_KEY) const client = new DifyClient(API_KEY) -// Fetch application parameters -client.getApplicationParameters(user) -// Provide feedback for a message -client.messageFeedback(messageId, rating, user) + +// App core +await client.getApplicationParameters(user) +await client.messageFeedback('message-id', 'like', user) + +// Completion (blocking) +await completionClient.createCompletionMessage({ + inputs: { query }, + user, + response_mode: 'blocking' +}) + +// Chat (streaming) +const stream = await chatClient.createChatMessage({ + inputs: {}, + query, + user, + response_mode: 'streaming' +}) +for await (const event of stream) { + console.log(event.event, event.data) +} + +// Chatflow (advanced chat via workflow_id) +await chatClient.createChatMessage({ + inputs: {}, + query, + user, + workflow_id: 'workflow-id', + response_mode: 'blocking' +}) + +// Workflow run (blocking or streaming) +await workflowClient.run({ + inputs: { query }, + user, + response_mode: 'blocking' +}) + +// Knowledge base (dataset token required) +await kbClient.listDatasets({ page: 1, limit: 20 }) +await kbClient.createDataset({ name: 'KB', indexing_technique: 'economy' }) + +// RAG pipeline (may require service API route registration) +const pipelineStream = await kbClient.runPipeline('dataset-id', { + inputs: {}, + datasource_type: 'online_document', + datasource_info_list: [], + start_node_id: 'start-node-id', + is_published: true, + response_mode: 'streaming' +}) +for await (const event of pipelineStream) { + console.log(event.data) +} + +// Workspace models (dataset token required) +await workspaceClient.getModelsByType('text-embedding') ``` -Replace 'your-api-key-here' with your actual Dify API key.Replace 'your-app-id-here' with your actual Dify APP ID. +Notes: + +- App endpoints use an app API token; knowledge base and workspace endpoints use a dataset API token. +- Chat/completion require a stable `user` identifier in the request payload. +- For streaming responses, iterate the returned AsyncIterable. Use `stream.toText()` to collect text. ## License diff --git a/sdks/nodejs-client/babel.config.cjs b/sdks/nodejs-client/babel.config.cjs deleted file mode 100644 index 392abb66d8..0000000000 --- a/sdks/nodejs-client/babel.config.cjs +++ /dev/null @@ -1,12 +0,0 @@ -module.exports = { - presets: [ - [ - "@babel/preset-env", - { - targets: { - node: "current", - }, - }, - ], - ], -}; diff --git a/sdks/nodejs-client/eslint.config.js b/sdks/nodejs-client/eslint.config.js new file mode 100644 index 0000000000..9e659f5d28 --- /dev/null +++ b/sdks/nodejs-client/eslint.config.js @@ -0,0 +1,45 @@ +import js from "@eslint/js"; +import tsParser from "@typescript-eslint/parser"; +import tsPlugin from "@typescript-eslint/eslint-plugin"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; + +const tsconfigRootDir = path.dirname(fileURLToPath(import.meta.url)); +const typeCheckedRules = + tsPlugin.configs["recommended-type-checked"]?.rules ?? + tsPlugin.configs.recommendedTypeChecked?.rules ?? + {}; + +export default [ + { + ignores: ["dist", "node_modules", "scripts", "tests", "**/*.test.*", "**/*.spec.*"], + }, + js.configs.recommended, + { + files: ["src/**/*.ts"], + languageOptions: { + parser: tsParser, + ecmaVersion: "latest", + parserOptions: { + project: "./tsconfig.json", + tsconfigRootDir, + sourceType: "module", + }, + }, + plugins: { + "@typescript-eslint": tsPlugin, + }, + rules: { + ...tsPlugin.configs.recommended.rules, + ...typeCheckedRules, + "no-undef": "off", + "no-unused-vars": "off", + "@typescript-eslint/no-unsafe-call": "error", + "@typescript-eslint/no-unsafe-return": "error", + "@typescript-eslint/consistent-type-imports": [ + "error", + { prefer: "type-imports", fixStyle: "separate-type-imports" }, + ], + }, + }, +]; diff --git a/sdks/nodejs-client/index.d.ts b/sdks/nodejs-client/index.d.ts deleted file mode 100644 index 3ea4b9d153..0000000000 --- a/sdks/nodejs-client/index.d.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Types.d.ts -export const BASE_URL: string; - -export type RequestMethods = 'GET' | 'POST' | 'PATCH' | 'DELETE'; - -interface Params { - [key: string]: any; -} - -interface HeaderParams { - [key: string]: string; -} - -interface User { -} - -interface DifyFileBase { - type: "image" -} - -export interface DifyRemoteFile extends DifyFileBase { - transfer_method: "remote_url" - url: string -} - -export interface DifyLocalFile extends DifyFileBase { - transfer_method: "local_file" - upload_file_id: string -} - -export type DifyFile = DifyRemoteFile | DifyLocalFile; - -export declare class DifyClient { - constructor(apiKey: string, baseUrl?: string); - - updateApiKey(apiKey: string): void; - - sendRequest( - method: RequestMethods, - endpoint: string, - data?: any, - params?: Params, - stream?: boolean, - headerParams?: HeaderParams - ): Promise; - - messageFeedback(message_id: string, rating: number, user: User): Promise; - - getApplicationParameters(user: User): Promise; - - fileUpload(data: FormData): Promise; - - textToAudio(text: string ,user: string, streaming?: boolean): Promise; - - getMeta(user: User): Promise; -} - -export declare class CompletionClient extends DifyClient { - createCompletionMessage( - inputs: any, - user: User, - stream?: boolean, - files?: DifyFile[] | null - ): Promise; -} - -export declare class ChatClient extends DifyClient { - createChatMessage( - inputs: any, - query: string, - user: User, - stream?: boolean, - conversation_id?: string | null, - files?: DifyFile[] | null - ): Promise; - - getSuggested(message_id: string, user: User): Promise; - - stopMessage(task_id: string, user: User) : Promise; - - - getConversations( - user: User, - first_id?: string | null, - limit?: number | null, - pinned?: boolean | null - ): Promise; - - getConversationMessages( - user: User, - conversation_id?: string, - first_id?: string | null, - limit?: number | null - ): Promise; - - renameConversation(conversation_id: string, name: string, user: User,auto_generate:boolean): Promise; - - deleteConversation(conversation_id: string, user: User): Promise; - - audioToText(data: FormData): Promise; -} - -export declare class WorkflowClient extends DifyClient { - run(inputs: any, user: User, stream?: boolean,): Promise; - - stop(task_id: string, user: User): Promise; -} diff --git a/sdks/nodejs-client/index.js b/sdks/nodejs-client/index.js deleted file mode 100644 index 9743ae358c..0000000000 --- a/sdks/nodejs-client/index.js +++ /dev/null @@ -1,351 +0,0 @@ -import axios from "axios"; -export const BASE_URL = "https://api.dify.ai/v1"; - -export const routes = { - // app's - feedback: { - method: "POST", - url: (message_id) => `/messages/${message_id}/feedbacks`, - }, - application: { - method: "GET", - url: () => `/parameters`, - }, - fileUpload: { - method: "POST", - url: () => `/files/upload`, - }, - textToAudio: { - method: "POST", - url: () => `/text-to-audio`, - }, - getMeta: { - method: "GET", - url: () => `/meta`, - }, - - // completion's - createCompletionMessage: { - method: "POST", - url: () => `/completion-messages`, - }, - - // chat's - createChatMessage: { - method: "POST", - url: () => `/chat-messages`, - }, - getSuggested:{ - method: "GET", - url: (message_id) => `/messages/${message_id}/suggested`, - }, - stopChatMessage: { - method: "POST", - url: (task_id) => `/chat-messages/${task_id}/stop`, - }, - getConversations: { - method: "GET", - url: () => `/conversations`, - }, - getConversationMessages: { - method: "GET", - url: () => `/messages`, - }, - renameConversation: { - method: "POST", - url: (conversation_id) => `/conversations/${conversation_id}/name`, - }, - deleteConversation: { - method: "DELETE", - url: (conversation_id) => `/conversations/${conversation_id}`, - }, - audioToText: { - method: "POST", - url: () => `/audio-to-text`, - }, - - // workflow‘s - runWorkflow: { - method: "POST", - url: () => `/workflows/run`, - }, - stopWorkflow: { - method: "POST", - url: (task_id) => `/workflows/tasks/${task_id}/stop`, - } - -}; - -export class DifyClient { - constructor(apiKey, baseUrl = BASE_URL) { - this.apiKey = apiKey; - this.baseUrl = baseUrl; - } - - updateApiKey(apiKey) { - this.apiKey = apiKey; - } - - async sendRequest( - method, - endpoint, - data = null, - params = null, - stream = false, - headerParams = {} - ) { - const isFormData = - (typeof FormData !== "undefined" && data instanceof FormData) || - (data && data.constructor && data.constructor.name === "FormData"); - const headers = { - Authorization: `Bearer ${this.apiKey}`, - ...(isFormData ? {} : { "Content-Type": "application/json" }), - ...headerParams, - }; - - const url = `${this.baseUrl}${endpoint}`; - let response; - if (stream) { - response = await axios({ - method, - url, - data, - params, - headers, - responseType: "stream", - }); - } else { - response = await axios({ - method, - url, - ...(method !== "GET" && { data }), - params, - headers, - responseType: "json", - }); - } - - return response; - } - - messageFeedback(message_id, rating, user) { - const data = { - rating, - user, - }; - return this.sendRequest( - routes.feedback.method, - routes.feedback.url(message_id), - data - ); - } - - getApplicationParameters(user) { - const params = { user }; - return this.sendRequest( - routes.application.method, - routes.application.url(), - null, - params - ); - } - - fileUpload(data) { - return this.sendRequest( - routes.fileUpload.method, - routes.fileUpload.url(), - data - ); - } - - textToAudio(text, user, streaming = false) { - const data = { - text, - user, - streaming - }; - return this.sendRequest( - routes.textToAudio.method, - routes.textToAudio.url(), - data, - null, - streaming - ); - } - - getMeta(user) { - const params = { user }; - return this.sendRequest( - routes.getMeta.method, - routes.getMeta.url(), - null, - params - ); - } -} - -export class CompletionClient extends DifyClient { - createCompletionMessage(inputs, user, stream = false, files = null) { - const data = { - inputs, - user, - response_mode: stream ? "streaming" : "blocking", - files, - }; - return this.sendRequest( - routes.createCompletionMessage.method, - routes.createCompletionMessage.url(), - data, - null, - stream - ); - } - - runWorkflow(inputs, user, stream = false, files = null) { - const data = { - inputs, - user, - response_mode: stream ? "streaming" : "blocking", - }; - return this.sendRequest( - routes.runWorkflow.method, - routes.runWorkflow.url(), - data, - null, - stream - ); - } -} - -export class ChatClient extends DifyClient { - createChatMessage( - inputs, - query, - user, - stream = false, - conversation_id = null, - files = null - ) { - const data = { - inputs, - query, - user, - response_mode: stream ? "streaming" : "blocking", - files, - }; - if (conversation_id) data.conversation_id = conversation_id; - - return this.sendRequest( - routes.createChatMessage.method, - routes.createChatMessage.url(), - data, - null, - stream - ); - } - - getSuggested(message_id, user) { - const data = { user }; - return this.sendRequest( - routes.getSuggested.method, - routes.getSuggested.url(message_id), - data - ); - } - - stopMessage(task_id, user) { - const data = { user }; - return this.sendRequest( - routes.stopChatMessage.method, - routes.stopChatMessage.url(task_id), - data - ); - } - - getConversations(user, first_id = null, limit = null, pinned = null) { - const params = { user, first_id: first_id, limit, pinned }; - return this.sendRequest( - routes.getConversations.method, - routes.getConversations.url(), - null, - params - ); - } - - getConversationMessages( - user, - conversation_id = "", - first_id = null, - limit = null - ) { - const params = { user }; - - if (conversation_id) params.conversation_id = conversation_id; - - if (first_id) params.first_id = first_id; - - if (limit) params.limit = limit; - - return this.sendRequest( - routes.getConversationMessages.method, - routes.getConversationMessages.url(), - null, - params - ); - } - - renameConversation(conversation_id, name, user, auto_generate) { - const data = { name, user, auto_generate }; - return this.sendRequest( - routes.renameConversation.method, - routes.renameConversation.url(conversation_id), - data - ); - } - - deleteConversation(conversation_id, user) { - const data = { user }; - return this.sendRequest( - routes.deleteConversation.method, - routes.deleteConversation.url(conversation_id), - data - ); - } - - - audioToText(data) { - return this.sendRequest( - routes.audioToText.method, - routes.audioToText.url(), - data - ); - } - -} - -export class WorkflowClient extends DifyClient { - run(inputs,user,stream) { - const data = { - inputs, - response_mode: stream ? "streaming" : "blocking", - user - }; - - return this.sendRequest( - routes.runWorkflow.method, - routes.runWorkflow.url(), - data, - null, - stream - ); - } - - stop(task_id, user) { - const data = { user }; - return this.sendRequest( - routes.stopWorkflow.method, - routes.stopWorkflow.url(task_id), - data - ); - } -} diff --git a/sdks/nodejs-client/index.test.js b/sdks/nodejs-client/index.test.js deleted file mode 100644 index e3a1715238..0000000000 --- a/sdks/nodejs-client/index.test.js +++ /dev/null @@ -1,141 +0,0 @@ -import { DifyClient, WorkflowClient, BASE_URL, routes } from "."; - -import axios from 'axios' - -jest.mock('axios') - -afterEach(() => { - jest.resetAllMocks() -}) - -describe('Client', () => { - let difyClient - beforeEach(() => { - difyClient = new DifyClient('test') - }) - - test('should create a client', () => { - expect(difyClient).toBeDefined(); - }) - // test updateApiKey - test('should update the api key', () => { - difyClient.updateApiKey('test2'); - expect(difyClient.apiKey).toBe('test2'); - }) -}); - -describe('Send Requests', () => { - let difyClient - - beforeEach(() => { - difyClient = new DifyClient('test') - }) - - it('should make a successful request to the application parameter', async () => { - const method = 'GET' - const endpoint = routes.application.url() - const expectedResponse = { data: 'response' } - axios.mockResolvedValue(expectedResponse) - - await difyClient.sendRequest(method, endpoint) - - expect(axios).toHaveBeenCalledWith({ - method, - url: `${BASE_URL}${endpoint}`, - params: null, - headers: { - Authorization: `Bearer ${difyClient.apiKey}`, - 'Content-Type': 'application/json', - }, - responseType: 'json', - }) - - }) - - it('should handle errors from the API', async () => { - const method = 'GET' - const endpoint = '/test-endpoint' - const errorMessage = 'Request failed with status code 404' - axios.mockRejectedValue(new Error(errorMessage)) - - await expect(difyClient.sendRequest(method, endpoint)).rejects.toThrow( - errorMessage - ) - }) - - it('uses the getMeta route configuration', async () => { - axios.mockResolvedValue({ data: 'ok' }) - await difyClient.getMeta('end-user') - - expect(axios).toHaveBeenCalledWith({ - method: routes.getMeta.method, - url: `${BASE_URL}${routes.getMeta.url()}`, - params: { user: 'end-user' }, - headers: { - Authorization: `Bearer ${difyClient.apiKey}`, - 'Content-Type': 'application/json', - }, - responseType: 'json', - }) - }) -}) - -describe('File uploads', () => { - let difyClient - const OriginalFormData = global.FormData - - beforeAll(() => { - global.FormData = class FormDataMock {} - }) - - afterAll(() => { - global.FormData = OriginalFormData - }) - - beforeEach(() => { - difyClient = new DifyClient('test') - }) - - it('does not override multipart boundary headers for FormData', async () => { - const form = new FormData() - axios.mockResolvedValue({ data: 'ok' }) - - await difyClient.fileUpload(form) - - expect(axios).toHaveBeenCalledWith({ - method: routes.fileUpload.method, - url: `${BASE_URL}${routes.fileUpload.url()}`, - data: form, - params: null, - headers: { - Authorization: `Bearer ${difyClient.apiKey}`, - }, - responseType: 'json', - }) - }) -}) - -describe('Workflow client', () => { - let workflowClient - - beforeEach(() => { - workflowClient = new WorkflowClient('test') - }) - - it('uses tasks stop path for workflow stop', async () => { - axios.mockResolvedValue({ data: 'stopped' }) - await workflowClient.stop('task-1', 'end-user') - - expect(axios).toHaveBeenCalledWith({ - method: routes.stopWorkflow.method, - url: `${BASE_URL}${routes.stopWorkflow.url('task-1')}`, - data: { user: 'end-user' }, - params: null, - headers: { - Authorization: `Bearer ${workflowClient.apiKey}`, - 'Content-Type': 'application/json', - }, - responseType: 'json', - }) - }) -}) diff --git a/sdks/nodejs-client/jest.config.cjs b/sdks/nodejs-client/jest.config.cjs deleted file mode 100644 index ea0fb34ad1..0000000000 --- a/sdks/nodejs-client/jest.config.cjs +++ /dev/null @@ -1,6 +0,0 @@ -module.exports = { - testEnvironment: "node", - transform: { - "^.+\\.[tj]sx?$": "babel-jest", - }, -}; diff --git a/sdks/nodejs-client/package.json b/sdks/nodejs-client/package.json index c6bb0a9c1f..554cb909ef 100644 --- a/sdks/nodejs-client/package.json +++ b/sdks/nodejs-client/package.json @@ -1,30 +1,70 @@ { "name": "dify-client", - "version": "2.3.2", + "version": "3.0.0", "description": "This is the Node.js SDK for the Dify.AI API, which allows you to easily integrate Dify.AI into your Node.js applications.", - "main": "index.js", "type": "module", - "types":"index.d.ts", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "engines": { + "node": ">=18.0.0" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], "keywords": [ "Dify", "Dify.AI", - "LLM" + "LLM", + "AI", + "SDK", + "API" ], - "author": "Joel", + "author": "LangGenius", "contributors": [ - " <<427733928@qq.com>> (https://github.com/crazywoola)" + "Joel (https://github.com/iamjoel)", + "lyzno1 (https://github.com/lyzno1)", + "crazywoola <427733928@qq.com> (https://github.com/crazywoola)" ], + "repository": { + "type": "git", + "url": "https://github.com/langgenius/dify.git", + "directory": "sdks/nodejs-client" + }, + "bugs": { + "url": "https://github.com/langgenius/dify/issues" + }, + "homepage": "https://dify.ai", "license": "MIT", "scripts": { - "test": "jest" + "build": "tsup", + "lint": "eslint", + "lint:fix": "eslint --fix", + "type-check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:coverage": "vitest run --coverage", + "publish:check": "./scripts/publish.sh --dry-run", + "publish:npm": "./scripts/publish.sh" }, "dependencies": { "axios": "^1.3.5" }, "devDependencies": { - "@babel/core": "^7.21.8", - "@babel/preset-env": "^7.21.5", - "babel-jest": "^29.5.0", - "jest": "^29.5.0" + "@eslint/js": "^9.2.0", + "@types/node": "^20.11.30", + "@typescript-eslint/eslint-plugin": "^8.50.1", + "@typescript-eslint/parser": "^8.50.1", + "@vitest/coverage-v8": "1.6.1", + "eslint": "^9.2.0", + "tsup": "^8.5.1", + "typescript": "^5.4.5", + "vitest": "^1.5.0" } } diff --git a/sdks/nodejs-client/pnpm-lock.yaml b/sdks/nodejs-client/pnpm-lock.yaml new file mode 100644 index 0000000000..3e4011c580 --- /dev/null +++ b/sdks/nodejs-client/pnpm-lock.yaml @@ -0,0 +1,2802 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + axios: + specifier: ^1.3.5 + version: 1.13.2 + devDependencies: + '@eslint/js': + specifier: ^9.2.0 + version: 9.39.2 + '@types/node': + specifier: ^20.11.30 + version: 20.19.27 + '@typescript-eslint/eslint-plugin': + specifier: ^8.50.1 + version: 8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/parser': + specifier: ^8.50.1 + version: 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@vitest/coverage-v8': + specifier: 1.6.1 + version: 1.6.1(vitest@1.6.1(@types/node@20.19.27)) + eslint: + specifier: ^9.2.0 + version: 9.39.2 + tsup: + specifier: ^8.5.1 + version: 8.5.1(postcss@8.5.6)(typescript@5.9.3) + typescript: + specifier: ^5.4.5 + version: 5.9.3 + vitest: + specifier: ^1.5.0 + version: 1.6.1(@types/node@20.19.27) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.5': + resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.28.5': + resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + + '@esbuild/aix-ppc64@0.21.5': + resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [aix] + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.21.5': + resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.21.5': + resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.0': + resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.1': + resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.2': + resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.7': + resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + + '@jest/schemas@29.6.3': + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@rollup/rollup-android-arm-eabi@4.54.0': + resolution: {integrity: sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.54.0': + resolution: {integrity: sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.54.0': + resolution: {integrity: sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.54.0': + resolution: {integrity: sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.54.0': + resolution: {integrity: sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.54.0': + resolution: {integrity: sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.54.0': + resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.54.0': + resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.54.0': + resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.54.0': + resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + resolution: {integrity: sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + resolution: {integrity: sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.54.0': + resolution: {integrity: sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.54.0': + resolution: {integrity: sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==} + cpu: [x64] + os: [win32] + + '@sinclair/typebox@0.27.8': + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/node@20.19.27': + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + + '@typescript-eslint/eslint-plugin@8.50.1': + resolution: {integrity: sha512-PKhLGDq3JAg0Jk/aK890knnqduuI/Qj+udH7wCf0217IGi4gt+acgCyPVe79qoT+qKUvHMDQkwJeKW9fwl8Cyw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.50.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/parser@8.50.1': + resolution: {integrity: sha512-hM5faZwg7aVNa819m/5r7D0h0c9yC4DUlWAOvHAtISdFTc8xB86VmX5Xqabrama3wIPJ/q9RbGS1worb6JfnMg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/project-service@8.50.1': + resolution: {integrity: sha512-E1ur1MCVf+YiP89+o4Les/oBAVzmSbeRB0MQLfSlYtbWU17HPxZ6Bhs5iYmKZRALvEuBoXIZMOIRRc/P++Ortg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/scope-manager@8.50.1': + resolution: {integrity: sha512-mfRx06Myt3T4vuoHaKi8ZWNTPdzKPNBhiblze5N50//TSHOAQQevl/aolqA/BcqqbJ88GUnLqjjcBc8EWdBcVw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.50.1': + resolution: {integrity: sha512-ooHmotT/lCWLXi55G4mvaUF60aJa012QzvLK0Y+Mp4WdSt17QhMhWOaBWeGTFVkb2gDgBe19Cxy1elPXylslDw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/type-utils@8.50.1': + resolution: {integrity: sha512-7J3bf022QZE42tYMO6SL+6lTPKFk/WphhRPe9Tw/el+cEwzLz1Jjz2PX3GtGQVxooLDKeMVmMt7fWpYRdG5Etg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/types@8.50.1': + resolution: {integrity: sha512-v5lFIS2feTkNyMhd7AucE/9j/4V9v5iIbpVRncjk/K0sQ6Sb+Np9fgYS/63n6nwqahHQvbmujeBL7mp07Q9mlA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.50.1': + resolution: {integrity: sha512-woHPdW+0gj53aM+cxchymJCrh0cyS7BTIdcDxWUNsclr9VDkOSbqC13juHzxOmQ22dDkMZEpZB+3X1WpUvzgVQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/utils@8.50.1': + resolution: {integrity: sha512-lCLp8H1T9T7gPbEuJSnHwnSuO9mDf8mfK/Nion5mZmiEaQD9sWf9W4dfeFqRyqRjF06/kBuTmAqcs9sewM2NbQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.0.0' + + '@typescript-eslint/visitor-keys@8.50.1': + resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitest/coverage-v8@1.6.1': + resolution: {integrity: sha512-6YeRZwuO4oTGKxD3bijok756oktHSIm3eczVVzNe3scqzuhLwltIF3S9ZL/vwOVIpURmU6SnZhziXXAfw8/Qlw==} + peerDependencies: + vitest: 1.6.1 + + '@vitest/expect@1.6.1': + resolution: {integrity: sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==} + + '@vitest/runner@1.6.1': + resolution: {integrity: sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==} + + '@vitest/snapshot@1.6.1': + resolution: {integrity: sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==} + + '@vitest/spy@1.6.1': + resolution: {integrity: sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==} + + '@vitest/utils@1.6.1': + resolution: {integrity: sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn-walk@8.3.4: + resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==} + engines: {node: '>=0.4.0'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + assertion-error@1.1.0: + resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.13.2: + resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + bundle-require@5.1.0: + resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + peerDependencies: + esbuild: '>=0.18' + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + chai@4.5.0: + resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==} + engines: {node: '>=4'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@4.1.1: + resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@4.1.4: + resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==} + engines: {node: '>=6'} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + esbuild@0.21.5: + resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} + engines: {node: '>=12'} + hasBin: true + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.39.2: + resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + execa@8.0.1: + resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} + engines: {node: '>=16.17'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + fix-dts-default-cjs-exports@1.0.1: + resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@8.0.1: + resolution: {integrity: sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==} + engines: {node: '>=16'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + + human-signals@5.0.0: + resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} + engines: {node: '>=16.17.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-stream@3.0.0: + resolution: {integrity: sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lilconfig@3.1.3: + resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} + engines: {node: '>=14'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + load-tsconfig@0.2.5: + resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + local-pkg@0.5.1: + resolution: {integrity: sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + merge-stream@2.0.0: + resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-fn@4.0.0: + resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} + engines: {node: '>=12'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + npm-run-path@5.3.0: + resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + onetime@6.0.0: + resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==} + engines: {node: '>=12'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-limit@5.0.0: + resolution: {integrity: sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==} + engines: {node: '>=18'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@1.1.1: + resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pirates@4.0.7: + resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} + engines: {node: '>= 6'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + postcss-load-config@6.0.1: + resolution: {integrity: sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==} + 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 + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-is@18.3.1: + resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve-from@5.0.0: + resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} + engines: {node: '>=8'} + + rollup@4.54.0: + resolution: {integrity: sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-final-newline@3.0.0: + resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} + engines: {node: '>=12'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + strip-literal@2.1.1: + resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==} + + sucrase@3.35.1: + resolution: {integrity: sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==} + engines: {node: '>=16 || 14 >=14.17'} + hasBin: true + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + test-exclude@6.0.0: + resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} + engines: {node: '>=8'} + + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@0.8.4: + resolution: {integrity: sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==} + engines: {node: '>=14.0.0'} + + tinyspy@2.2.1: + resolution: {integrity: sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==} + engines: {node: '>=14.0.0'} + + tree-kill@1.2.2: + resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} + hasBin: true + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-interface-checker@0.1.13: + resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + + tsup@8.5.1: + resolution: {integrity: sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==} + engines: {node: '>=18'} + hasBin: true + 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 + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-detect@4.1.0: + resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} + engines: {node: '>=4'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite-node@1.6.1: + resolution: {integrity: sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + 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 + + vitest@1.6.1: + resolution: {integrity: sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 1.6.1 + '@vitest/ui': 1.6.1 + 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 + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + yocto-queue@1.2.2: + resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} + engines: {node: '>=12.20'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.28.5': + dependencies: + '@babel/types': 7.28.5 + + '@babel/types@7.28.5': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + + '@esbuild/aix-ppc64@0.21.5': + optional: true + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.21.5': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.21.5': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.21.5': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.21.5': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.21.5': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.21.5': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.21.5': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.21.5': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.21.5': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.21.5': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.21.5': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.21.5': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.21.5': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.21.5': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.21.5': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.21.5': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.21.5': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.21.5': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.21.5': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.21.5': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.21.5': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.21.5': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2)': + dependencies: + eslint: 9.39.2 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.1': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.3': + dependencies: + ajv: 6.12.6 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.2': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.7': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.4.3 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@istanbuljs/schema@0.1.3': {} + + '@jest/schemas@29.6.3': + dependencies: + '@sinclair/typebox': 0.27.8 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@rollup/rollup-android-arm-eabi@4.54.0': + optional: true + + '@rollup/rollup-android-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.54.0': + optional: true + + '@rollup/rollup-darwin-x64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.54.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.54.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.54.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.54.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.54.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.54.0': + optional: true + + '@sinclair/typebox@0.27.8': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/node@20.19.27': + dependencies: + undici-types: 6.21.0 + + '@typescript-eslint/eslint-plugin@8.50.1(@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3))(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/type-utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + eslint: 9.39.2 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + debug: 4.4.3 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + + '@typescript-eslint/tsconfig-utils@8.50.1(typescript@5.9.3)': + dependencies: + typescript: 5.9.3 + + '@typescript-eslint/type-utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + '@typescript-eslint/utils': 8.50.1(eslint@9.39.2)(typescript@5.9.3) + debug: 4.4.3 + eslint: 9.39.2 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.50.1': {} + + '@typescript-eslint/typescript-estree@8.50.1(typescript@5.9.3)': + dependencies: + '@typescript-eslint/project-service': 8.50.1(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.50.1(typescript@5.9.3) + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/visitor-keys': 8.50.1 + debug: 4.4.3 + minimatch: 9.0.5 + semver: 7.7.3 + tinyglobby: 0.2.15 + ts-api-utils: 2.1.0(typescript@5.9.3) + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.50.1(eslint@9.39.2)(typescript@5.9.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@typescript-eslint/scope-manager': 8.50.1 + '@typescript-eslint/types': 8.50.1 + '@typescript-eslint/typescript-estree': 8.50.1(typescript@5.9.3) + eslint: 9.39.2 + typescript: 5.9.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.50.1': + dependencies: + '@typescript-eslint/types': 8.50.1 + eslint-visitor-keys: 4.2.1 + + '@vitest/coverage-v8@1.6.1(vitest@1.6.1(@types/node@20.19.27))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + test-exclude: 6.0.0 + vitest: 1.6.1(@types/node@20.19.27) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@1.6.1': + dependencies: + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + chai: 4.5.0 + + '@vitest/runner@1.6.1': + dependencies: + '@vitest/utils': 1.6.1 + p-limit: 5.0.0 + pathe: 1.1.2 + + '@vitest/snapshot@1.6.1': + dependencies: + magic-string: 0.30.21 + pathe: 1.1.2 + pretty-format: 29.7.0 + + '@vitest/spy@1.6.1': + dependencies: + tinyspy: 2.2.1 + + '@vitest/utils@1.6.1': + dependencies: + diff-sequences: 29.6.3 + estree-walker: 3.0.3 + loupe: 2.3.7 + pretty-format: 29.7.0 + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn-walk@8.3.4: + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + any-promise@1.3.0: {} + + argparse@2.0.1: {} + + assertion-error@1.1.0: {} + + asynckit@0.4.0: {} + + axios@1.13.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.5 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + balanced-match@1.0.2: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + bundle-require@5.1.0(esbuild@0.27.2): + dependencies: + esbuild: 0.27.2 + load-tsconfig: 0.2.5 + + cac@6.7.14: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + callsites@3.1.0: {} + + chai@4.5.0: + dependencies: + assertion-error: 1.1.0 + check-error: 1.0.3 + deep-eql: 4.1.4 + get-func-name: 2.0.2 + loupe: 2.3.7 + pathval: 1.1.1 + type-detect: 4.1.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + check-error@1.0.3: + dependencies: + get-func-name: 2.0.2 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@4.1.1: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + consola@3.4.2: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@4.1.4: + dependencies: + type-detect: 4.1.0 + + deep-is@0.1.4: {} + + delayed-stream@1.0.0: {} + + diff-sequences@29.6.3: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esbuild@0.21.5: + 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 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escape-string-regexp@4.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.39.2: + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.1 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.3 + '@eslint/js': 9.39.2 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.7 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + execa@8.0.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 8.0.1 + human-signals: 5.0.0 + is-stream: 3.0.0 + merge-stream: 2.0.0 + npm-run-path: 5.3.0 + onetime: 6.0.0 + signal-exit: 4.1.0 + strip-final-newline: 3.0.0 + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + fix-dts-default-cjs-exports@1.0.1: + dependencies: + magic-string: 0.30.21 + mlly: 1.8.0 + rollup: 4.54.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + follow-redirects@1.15.11: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs.realpath@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-func-name@2.0.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@8.0.1: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + html-escaper@2.0.2: {} + + human-signals@5.0.0: {} + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-stream@3.0.0: {} + + isexe@2.0.0: {} + + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + joycon@3.1.1: {} + + js-tokens@9.0.1: {} + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lilconfig@3.1.3: {} + + lines-and-columns@1.2.4: {} + + load-tsconfig@0.2.5: {} + + local-pkg@0.5.1: + dependencies: + mlly: 1.8.0 + pkg-types: 1.3.1 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loupe@2.3.7: + dependencies: + get-func-name: 2.0.2 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.28.5 + '@babel/types': 7.28.5 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + + math-intrinsics@1.1.0: {} + + merge-stream@2.0.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-fn@4.0.0: {} + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + ms@2.1.3: {} + + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + npm-run-path@5.3.0: + dependencies: + path-key: 4.0.0 + + object-assign@4.1.1: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + onetime@6.0.0: + dependencies: + mimic-fn: 4.0.0 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-limit@5.0.0: + dependencies: + yocto-queue: 1.2.2 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + path-exists@4.0.0: {} + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@1.1.2: {} + + pathe@2.0.3: {} + + pathval@1.1.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pirates@4.0.7: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + postcss-load-config@6.0.1(postcss@8.5.6): + dependencies: + lilconfig: 3.1.3 + optionalDependencies: + postcss: 8.5.6 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + pretty-format@29.7.0: + dependencies: + '@jest/schemas': 29.6.3 + ansi-styles: 5.2.0 + react-is: 18.3.1 + + proxy-from-env@1.1.0: {} + + punycode@2.3.1: {} + + react-is@18.3.1: {} + + readdirp@4.1.2: {} + + resolve-from@4.0.0: {} + + resolve-from@5.0.0: {} + + rollup@4.54.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.54.0 + '@rollup/rollup-android-arm64': 4.54.0 + '@rollup/rollup-darwin-arm64': 4.54.0 + '@rollup/rollup-darwin-x64': 4.54.0 + '@rollup/rollup-freebsd-arm64': 4.54.0 + '@rollup/rollup-freebsd-x64': 4.54.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.54.0 + '@rollup/rollup-linux-arm-musleabihf': 4.54.0 + '@rollup/rollup-linux-arm64-gnu': 4.54.0 + '@rollup/rollup-linux-arm64-musl': 4.54.0 + '@rollup/rollup-linux-loong64-gnu': 4.54.0 + '@rollup/rollup-linux-ppc64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-gnu': 4.54.0 + '@rollup/rollup-linux-riscv64-musl': 4.54.0 + '@rollup/rollup-linux-s390x-gnu': 4.54.0 + '@rollup/rollup-linux-x64-gnu': 4.54.0 + '@rollup/rollup-linux-x64-musl': 4.54.0 + '@rollup/rollup-openharmony-arm64': 4.54.0 + '@rollup/rollup-win32-arm64-msvc': 4.54.0 + '@rollup/rollup-win32-ia32-msvc': 4.54.0 + '@rollup/rollup-win32-x64-gnu': 4.54.0 + '@rollup/rollup-win32-x64-msvc': 4.54.0 + fsevents: 2.3.3 + + semver@7.7.3: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + signal-exit@4.1.0: {} + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-final-newline@3.0.0: {} + + strip-json-comments@3.1.1: {} + + strip-literal@2.1.1: + dependencies: + js-tokens: 9.0.1 + + sucrase@3.35.1: + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + commander: 4.1.1 + lines-and-columns: 1.2.4 + mz: 2.7.0 + pirates: 4.0.7 + tinyglobby: 0.2.15 + ts-interface-checker: 0.1.13 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + test-exclude@6.0.0: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 7.2.3 + minimatch: 3.1.2 + + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@0.8.4: {} + + tinyspy@2.2.1: {} + + tree-kill@1.2.2: {} + + ts-api-utils@2.1.0(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + ts-interface-checker@0.1.13: {} + + tsup@8.5.1(postcss@8.5.6)(typescript@5.9.3): + dependencies: + bundle-require: 5.1.0(esbuild@0.27.2) + cac: 6.7.14 + chokidar: 4.0.3 + consola: 3.4.2 + debug: 4.4.3 + esbuild: 0.27.2 + fix-dts-default-cjs-exports: 1.0.1 + joycon: 3.1.1 + picocolors: 1.1.1 + postcss-load-config: 6.0.1(postcss@8.5.6) + resolve-from: 5.0.0 + rollup: 4.54.0 + source-map: 0.7.6 + sucrase: 3.35.1 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tree-kill: 1.2.2 + optionalDependencies: + postcss: 8.5.6 + typescript: 5.9.3 + transitivePeerDependencies: + - jiti + - supports-color + - tsx + - yaml + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-detect@4.1.0: {} + + typescript@5.9.3: {} + + ufo@1.6.1: {} + + undici-types@6.21.0: {} + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite-node@1.6.1(@types/node@20.19.27): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + pathe: 1.1.2 + picocolors: 1.1.1 + vite: 5.4.21(@types/node@20.19.27) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + vite@5.4.21(@types/node@20.19.27): + dependencies: + esbuild: 0.21.5 + postcss: 8.5.6 + rollup: 4.54.0 + optionalDependencies: + '@types/node': 20.19.27 + fsevents: 2.3.3 + + vitest@1.6.1(@types/node@20.19.27): + dependencies: + '@vitest/expect': 1.6.1 + '@vitest/runner': 1.6.1 + '@vitest/snapshot': 1.6.1 + '@vitest/spy': 1.6.1 + '@vitest/utils': 1.6.1 + acorn-walk: 8.3.4 + chai: 4.5.0 + debug: 4.4.3 + execa: 8.0.1 + local-pkg: 0.5.1 + magic-string: 0.30.21 + pathe: 1.1.2 + picocolors: 1.1.1 + std-env: 3.10.0 + strip-literal: 2.1.1 + tinybench: 2.9.0 + tinypool: 0.8.4 + vite: 5.4.21(@types/node@20.19.27) + vite-node: 1.6.1(@types/node@20.19.27) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 20.19.27 + transitivePeerDependencies: + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + wrappy@1.0.2: {} + + yocto-queue@0.1.0: {} + + yocto-queue@1.2.2: {} diff --git a/sdks/nodejs-client/scripts/publish.sh b/sdks/nodejs-client/scripts/publish.sh new file mode 100755 index 0000000000..043cac046d --- /dev/null +++ b/sdks/nodejs-client/scripts/publish.sh @@ -0,0 +1,261 @@ +#!/usr/bin/env bash +# +# Dify Node.js SDK Publish Script +# ================================ +# A beautiful and reliable script to publish the SDK to npm +# +# Usage: +# ./scripts/publish.sh # Normal publish +# ./scripts/publish.sh --dry-run # Test without publishing +# ./scripts/publish.sh --skip-tests # Skip tests (not recommended) +# + +set -euo pipefail + +# ============================================================================ +# Colors and Formatting +# ============================================================================ +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +MAGENTA='\033[0;35m' +CYAN='\033[0;36m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' # No Color + +# ============================================================================ +# Helper Functions +# ============================================================================ +print_banner() { + echo -e "${CYAN}" + echo "╔═══════════════════════════════════════════════════════════════╗" + echo "║ ║" + echo "║ 🚀 Dify Node.js SDK Publish Script 🚀 ║" + echo "║ ║" + echo "╚═══════════════════════════════════════════════════════════════╝" + echo -e "${NC}" +} + +info() { + echo -e "${BLUE}ℹ ${NC}$1" +} + +success() { + echo -e "${GREEN}✔ ${NC}$1" +} + +warning() { + echo -e "${YELLOW}⚠ ${NC}$1" +} + +error() { + echo -e "${RED}✖ ${NC}$1" +} + +step() { + echo -e "\n${MAGENTA}▶ ${BOLD}$1${NC}" +} + +divider() { + echo -e "${DIM}─────────────────────────────────────────────────────────────────${NC}" +} + +# ============================================================================ +# Configuration +# ============================================================================ +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +DRY_RUN=false +SKIP_TESTS=false + +# Parse arguments +for arg in "$@"; do + case $arg in + --dry-run) + DRY_RUN=true + ;; + --skip-tests) + SKIP_TESTS=true + ;; + --help|-h) + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --dry-run Run without actually publishing" + echo " --skip-tests Skip running tests (not recommended)" + echo " --help, -h Show this help message" + exit 0 + ;; + esac +done + +# ============================================================================ +# Main Script +# ============================================================================ +main() { + print_banner + cd "$PROJECT_DIR" + + # Show mode + if [[ "$DRY_RUN" == true ]]; then + warning "Running in DRY-RUN mode - no actual publish will occur" + divider + fi + + # ======================================================================== + # Step 1: Environment Check + # ======================================================================== + step "Step 1/6: Checking environment..." + + # Check Node.js + if ! command -v node &> /dev/null; then + error "Node.js is not installed" + exit 1 + fi + NODE_VERSION=$(node -v) + success "Node.js: $NODE_VERSION" + + # Check npm + if ! command -v npm &> /dev/null; then + error "npm is not installed" + exit 1 + fi + NPM_VERSION=$(npm -v) + success "npm: v$NPM_VERSION" + + # Check pnpm (optional, for local dev) + if command -v pnpm &> /dev/null; then + PNPM_VERSION=$(pnpm -v) + success "pnpm: v$PNPM_VERSION" + else + info "pnpm not found (optional)" + fi + + # Check npm login status + if ! npm whoami &> /dev/null; then + error "Not logged in to npm. Run 'npm login' first." + exit 1 + fi + NPM_USER=$(npm whoami) + success "Logged in as: ${BOLD}$NPM_USER${NC}" + + # ======================================================================== + # Step 2: Read Package Info + # ======================================================================== + step "Step 2/6: Reading package info..." + + PACKAGE_NAME=$(node -p "require('./package.json').name") + PACKAGE_VERSION=$(node -p "require('./package.json').version") + + success "Package: ${BOLD}$PACKAGE_NAME${NC}" + success "Version: ${BOLD}$PACKAGE_VERSION${NC}" + + # Check if version already exists on npm + if npm view "$PACKAGE_NAME@$PACKAGE_VERSION" version &> /dev/null; then + error "Version $PACKAGE_VERSION already exists on npm!" + echo "" + info "Current published versions:" + npm view "$PACKAGE_NAME" versions --json 2>/dev/null | tail -5 + echo "" + warning "Please update the version in package.json before publishing." + exit 1 + fi + success "Version $PACKAGE_VERSION is available" + + # ======================================================================== + # Step 3: Install Dependencies + # ======================================================================== + step "Step 3/6: Installing dependencies..." + + if command -v pnpm &> /dev/null; then + pnpm install --frozen-lockfile 2>/dev/null || pnpm install + else + npm ci 2>/dev/null || npm install + fi + success "Dependencies installed" + + # ======================================================================== + # Step 4: Run Tests + # ======================================================================== + step "Step 4/6: Running tests..." + + if [[ "$SKIP_TESTS" == true ]]; then + warning "Skipping tests (--skip-tests flag)" + else + if command -v pnpm &> /dev/null; then + pnpm test + else + npm test + fi + success "All tests passed" + fi + + # ======================================================================== + # Step 5: Build + # ======================================================================== + step "Step 5/6: Building package..." + + # Clean previous build + rm -rf dist + + if command -v pnpm &> /dev/null; then + pnpm run build + else + npm run build + fi + success "Build completed" + + # Verify build output + if [[ ! -f "dist/index.js" ]]; then + error "Build failed - dist/index.js not found" + exit 1 + fi + if [[ ! -f "dist/index.d.ts" ]]; then + error "Build failed - dist/index.d.ts not found" + exit 1 + fi + success "Build output verified" + + # ======================================================================== + # Step 6: Publish + # ======================================================================== + step "Step 6/6: Publishing to npm..." + + divider + echo -e "${CYAN}Package contents:${NC}" + npm pack --dry-run 2>&1 | head -30 + divider + + if [[ "$DRY_RUN" == true ]]; then + warning "DRY-RUN: Skipping actual publish" + echo "" + info "To publish for real, run without --dry-run flag" + else + echo "" + echo -e "${YELLOW}About to publish ${BOLD}$PACKAGE_NAME@$PACKAGE_VERSION${NC}${YELLOW} to npm${NC}" + echo -e "${DIM}Press Enter to continue, or Ctrl+C to cancel...${NC}" + read -r + + npm publish --access public + + echo "" + success "🎉 Successfully published ${BOLD}$PACKAGE_NAME@$PACKAGE_VERSION${NC} to npm!" + echo "" + echo -e "${GREEN}Install with:${NC}" + echo -e " ${CYAN}npm install $PACKAGE_NAME${NC}" + echo -e " ${CYAN}pnpm add $PACKAGE_NAME${NC}" + echo -e " ${CYAN}yarn add $PACKAGE_NAME${NC}" + echo "" + echo -e "${GREEN}View on npm:${NC}" + echo -e " ${CYAN}https://www.npmjs.com/package/$PACKAGE_NAME${NC}" + fi + + divider + echo -e "${GREEN}${BOLD}✨ All done!${NC}" +} + +# Run main function +main "$@" diff --git a/sdks/nodejs-client/src/client/base.test.js b/sdks/nodejs-client/src/client/base.test.js new file mode 100644 index 0000000000..5e1b21d0f1 --- /dev/null +++ b/sdks/nodejs-client/src/client/base.test.js @@ -0,0 +1,175 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { DifyClient } from "./base"; +import { ValidationError } from "../errors/dify-error"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("DifyClient base", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("getRoot calls root endpoint", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.getRoot(); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/", + }); + }); + + it("getApplicationParameters includes optional user", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.getApplicationParameters(); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/parameters", + query: undefined, + }); + + await dify.getApplicationParameters("user-1"); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/parameters", + query: { user: "user-1" }, + }); + }); + + it("getMeta includes optional user", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.getMeta("user-1"); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/meta", + query: { user: "user-1" }, + }); + }); + + it("getInfo and getSite support optional user", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.getInfo(); + await dify.getSite("user"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/info", + query: undefined, + }); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/site", + query: { user: "user" }, + }); + }); + + it("messageFeedback builds payload from request object", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.messageFeedback({ + messageId: "msg", + user: "user", + rating: "like", + content: "good", + }); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/messages/msg/feedbacks", + data: { user: "user", rating: "like", content: "good" }, + }); + }); + + it("fileUpload appends user to form data", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + const form = { append: vi.fn(), getHeaders: () => ({}) }; + + await dify.fileUpload(form, "user"); + + expect(form.append).toHaveBeenCalledWith("user", "user"); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/files/upload", + data: form, + }); + }); + + it("filePreview uses arraybuffer response", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.filePreview("file", "user", true); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/files/file/preview", + query: { user: "user", as_attachment: "true" }, + responseType: "arraybuffer", + }); + }); + + it("audioToText appends user and sends form", async () => { + const { client, request } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + const form = { append: vi.fn(), getHeaders: () => ({}) }; + + await dify.audioToText(form, "user"); + + expect(form.append).toHaveBeenCalledWith("user", "user"); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/audio-to-text", + data: form, + }); + }); + + it("textToAudio supports streaming and message id", async () => { + const { client, request, requestBinaryStream } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + await dify.textToAudio({ + user: "user", + message_id: "msg", + streaming: true, + }); + + expect(requestBinaryStream).toHaveBeenCalledWith({ + method: "POST", + path: "/text-to-audio", + data: { + user: "user", + message_id: "msg", + streaming: true, + }, + }); + + await dify.textToAudio("hello", "user", false, "voice"); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/text-to-audio", + data: { + text: "hello", + user: "user", + streaming: false, + voice: "voice", + }, + responseType: "arraybuffer", + }); + }); + + it("textToAudio requires text or message id", async () => { + const { client } = createHttpClientWithSpies(); + const dify = new DifyClient(client); + + expect(() => dify.textToAudio({ user: "user" })).toThrow(ValidationError); + }); +}); diff --git a/sdks/nodejs-client/src/client/base.ts b/sdks/nodejs-client/src/client/base.ts new file mode 100644 index 0000000000..0fa535a488 --- /dev/null +++ b/sdks/nodejs-client/src/client/base.ts @@ -0,0 +1,284 @@ +import type { + BinaryStream, + DifyClientConfig, + DifyResponse, + MessageFeedbackRequest, + QueryParams, + RequestMethod, + TextToAudioRequest, +} from "../types/common"; +import { HttpClient } from "../http/client"; +import { ensureNonEmptyString, ensureRating } from "./validation"; +import { FileUploadError, ValidationError } from "../errors/dify-error"; +import { isFormData } from "../http/form-data"; + +const toConfig = ( + init: string | DifyClientConfig, + baseUrl?: string +): DifyClientConfig => { + if (typeof init === "string") { + return { + apiKey: init, + baseUrl, + }; + } + return init; +}; + +const appendUserToFormData = (form: unknown, user: string): void => { + if (!isFormData(form)) { + throw new FileUploadError("FormData is required for file uploads"); + } + if (typeof form.append === "function") { + form.append("user", user); + } +}; + +export class DifyClient { + protected http: HttpClient; + + constructor(config: string | DifyClientConfig | HttpClient, baseUrl?: string) { + if (config instanceof HttpClient) { + this.http = config; + } else { + this.http = new HttpClient(toConfig(config, baseUrl)); + } + } + + updateApiKey(apiKey: string): void { + ensureNonEmptyString(apiKey, "apiKey"); + this.http.updateApiKey(apiKey); + } + + getHttpClient(): HttpClient { + return this.http; + } + + sendRequest( + method: RequestMethod, + endpoint: string, + data: unknown = null, + params: QueryParams | null = null, + stream = false, + headerParams: Record = {} + ): ReturnType { + return this.http.requestRaw({ + method, + path: endpoint, + data, + query: params ?? undefined, + headers: headerParams, + responseType: stream ? "stream" : "json", + }); + } + + getRoot(): Promise> { + return this.http.request({ + method: "GET", + path: "/", + }); + } + + getApplicationParameters(user?: string): Promise> { + if (user) { + ensureNonEmptyString(user, "user"); + } + return this.http.request({ + method: "GET", + path: "/parameters", + query: user ? { user } : undefined, + }); + } + + async getParameters(user?: string): Promise> { + return this.getApplicationParameters(user); + } + + getMeta(user?: string): Promise> { + if (user) { + ensureNonEmptyString(user, "user"); + } + return this.http.request({ + method: "GET", + path: "/meta", + query: user ? { user } : undefined, + }); + } + + messageFeedback( + request: MessageFeedbackRequest + ): Promise>>; + messageFeedback( + messageId: string, + rating: "like" | "dislike" | null, + user: string, + content?: string + ): Promise>>; + messageFeedback( + messageIdOrRequest: string | MessageFeedbackRequest, + rating?: "like" | "dislike" | null, + user?: string, + content?: string + ): Promise>> { + let messageId: string; + const payload: Record = {}; + + if (typeof messageIdOrRequest === "string") { + messageId = messageIdOrRequest; + ensureNonEmptyString(messageId, "messageId"); + ensureNonEmptyString(user, "user"); + payload.user = user; + if (rating !== undefined && rating !== null) { + ensureRating(rating); + payload.rating = rating; + } + if (content !== undefined) { + payload.content = content; + } + } else { + const request = messageIdOrRequest; + messageId = request.messageId; + ensureNonEmptyString(messageId, "messageId"); + ensureNonEmptyString(request.user, "user"); + payload.user = request.user; + if (request.rating !== undefined && request.rating !== null) { + ensureRating(request.rating); + payload.rating = request.rating; + } + if (request.content !== undefined) { + payload.content = request.content; + } + } + + return this.http.request({ + method: "POST", + path: `/messages/${messageId}/feedbacks`, + data: payload, + }); + } + + getInfo(user?: string): Promise> { + if (user) { + ensureNonEmptyString(user, "user"); + } + return this.http.request({ + method: "GET", + path: "/info", + query: user ? { user } : undefined, + }); + } + + getSite(user?: string): Promise> { + if (user) { + ensureNonEmptyString(user, "user"); + } + return this.http.request({ + method: "GET", + path: "/site", + query: user ? { user } : undefined, + }); + } + + fileUpload(form: unknown, user: string): Promise> { + if (!isFormData(form)) { + throw new FileUploadError("FormData is required for file uploads"); + } + ensureNonEmptyString(user, "user"); + appendUserToFormData(form, user); + return this.http.request({ + method: "POST", + path: "/files/upload", + data: form, + }); + } + + filePreview( + fileId: string, + user: string, + asAttachment?: boolean + ): Promise> { + ensureNonEmptyString(fileId, "fileId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "GET", + path: `/files/${fileId}/preview`, + query: { + user, + as_attachment: asAttachment ? "true" : undefined, + }, + responseType: "arraybuffer", + }); + } + + audioToText(form: unknown, user: string): Promise> { + if (!isFormData(form)) { + throw new FileUploadError("FormData is required for audio uploads"); + } + ensureNonEmptyString(user, "user"); + appendUserToFormData(form, user); + return this.http.request({ + method: "POST", + path: "/audio-to-text", + data: form, + }); + } + + textToAudio( + request: TextToAudioRequest + ): Promise | BinaryStream>; + textToAudio( + text: string, + user: string, + streaming?: boolean, + voice?: string + ): Promise | BinaryStream>; + textToAudio( + textOrRequest: string | TextToAudioRequest, + user?: string, + streaming = false, + voice?: string + ): Promise | BinaryStream> { + let payload: TextToAudioRequest; + + if (typeof textOrRequest === "string") { + ensureNonEmptyString(textOrRequest, "text"); + ensureNonEmptyString(user, "user"); + payload = { + text: textOrRequest, + user, + streaming, + }; + if (voice) { + payload.voice = voice; + } + } else { + payload = { ...textOrRequest }; + ensureNonEmptyString(payload.user, "user"); + if (payload.text !== undefined && payload.text !== null) { + ensureNonEmptyString(payload.text, "text"); + } + if (payload.message_id !== undefined && payload.message_id !== null) { + ensureNonEmptyString(payload.message_id, "messageId"); + } + if (!payload.text && !payload.message_id) { + throw new ValidationError("text or message_id is required"); + } + payload.streaming = payload.streaming ?? false; + } + + if (payload.streaming) { + return this.http.requestBinaryStream({ + method: "POST", + path: "/text-to-audio", + data: payload, + }); + } + + return this.http.request({ + method: "POST", + path: "/text-to-audio", + data: payload, + responseType: "arraybuffer", + }); + } +} diff --git a/sdks/nodejs-client/src/client/chat.test.js b/sdks/nodejs-client/src/client/chat.test.js new file mode 100644 index 0000000000..a97c9d4a5c --- /dev/null +++ b/sdks/nodejs-client/src/client/chat.test.js @@ -0,0 +1,239 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ChatClient } from "./chat"; +import { ValidationError } from "../errors/dify-error"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("ChatClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("creates chat messages in blocking mode", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.createChatMessage({ input: "x" }, "hello", "user", false, null); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/chat-messages", + data: { + inputs: { input: "x" }, + query: "hello", + user: "user", + response_mode: "blocking", + files: undefined, + }, + }); + }); + + it("creates chat messages in streaming mode", async () => { + const { client, requestStream } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.createChatMessage({ + inputs: { input: "x" }, + query: "hello", + user: "user", + response_mode: "streaming", + }); + + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/chat-messages", + data: { + inputs: { input: "x" }, + query: "hello", + user: "user", + response_mode: "streaming", + }, + }); + }); + + it("stops chat messages", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.stopChatMessage("task", "user"); + await chat.stopMessage("task", "user"); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/chat-messages/task/stop", + data: { user: "user" }, + }); + }); + + it("gets suggested questions", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.getSuggested("msg", "user"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/messages/msg/suggested", + query: { user: "user" }, + }); + }); + + it("submits message feedback", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.messageFeedback("msg", "like", "user", "good"); + await chat.messageFeedback({ + messageId: "msg", + user: "user", + rating: "dislike", + }); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/messages/msg/feedbacks", + data: { user: "user", rating: "like", content: "good" }, + }); + }); + + it("lists app feedbacks", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.getAppFeedbacks(2, 5); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/app/feedbacks", + query: { page: 2, limit: 5 }, + }); + }); + + it("lists conversations and messages", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.getConversations("user", "last", 10, "-updated_at"); + await chat.getConversationMessages("user", "conv", "first", 5); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/conversations", + query: { + user: "user", + last_id: "last", + limit: 10, + sort_by: "-updated_at", + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/messages", + query: { + user: "user", + conversation_id: "conv", + first_id: "first", + limit: 5, + }, + }); + }); + + it("renames conversations with optional auto-generate", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.renameConversation("conv", "name", "user", false); + await chat.renameConversation("conv", "user", { autoGenerate: true }); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/conversations/conv/name", + data: { user: "user", auto_generate: false, name: "name" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/conversations/conv/name", + data: { user: "user", auto_generate: true }, + }); + }); + + it("requires name when autoGenerate is false", async () => { + const { client } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + expect(() => + chat.renameConversation("conv", "", "user", false) + ).toThrow(ValidationError); + }); + + it("deletes conversations", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.deleteConversation("conv", "user"); + + expect(request).toHaveBeenCalledWith({ + method: "DELETE", + path: "/conversations/conv", + data: { user: "user" }, + }); + }); + + it("manages conversation variables", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.getConversationVariables("conv", "user", "last", 10, "name"); + await chat.updateConversationVariable("conv", "var", "user", "value"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/conversations/conv/variables", + query: { + user: "user", + last_id: "last", + limit: 10, + variable_name: "name", + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "PUT", + path: "/conversations/conv/variables/var", + data: { user: "user", value: "value" }, + }); + }); + + it("handles annotation APIs", async () => { + const { client, request } = createHttpClientWithSpies(); + const chat = new ChatClient(client); + + await chat.annotationReplyAction("enable", { + score_threshold: 0.5, + embedding_provider_name: "prov", + embedding_model_name: "model", + }); + await chat.getAnnotationReplyStatus("enable", "job"); + await chat.listAnnotations({ page: 1, limit: 10, keyword: "k" }); + await chat.createAnnotation({ question: "q", answer: "a" }); + await chat.updateAnnotation("id", { question: "q", answer: "a" }); + await chat.deleteAnnotation("id"); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/apps/annotation-reply/enable", + data: { + score_threshold: 0.5, + embedding_provider_name: "prov", + embedding_model_name: "model", + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/apps/annotation-reply/enable/status/job", + }); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/apps/annotations", + query: { page: 1, limit: 10, keyword: "k" }, + }); + }); +}); diff --git a/sdks/nodejs-client/src/client/chat.ts b/sdks/nodejs-client/src/client/chat.ts new file mode 100644 index 0000000000..745c999552 --- /dev/null +++ b/sdks/nodejs-client/src/client/chat.ts @@ -0,0 +1,377 @@ +import { DifyClient } from "./base"; +import type { ChatMessageRequest, ChatMessageResponse } from "../types/chat"; +import type { + AnnotationCreateRequest, + AnnotationListOptions, + AnnotationReplyActionRequest, + AnnotationResponse, +} from "../types/annotation"; +import type { + DifyResponse, + DifyStream, + QueryParams, +} from "../types/common"; +import { + ensureNonEmptyString, + ensureOptionalInt, + ensureOptionalString, +} from "./validation"; + +export class ChatClient extends DifyClient { + createChatMessage( + request: ChatMessageRequest + ): Promise | DifyStream>; + createChatMessage( + inputs: Record, + query: string, + user: string, + stream?: boolean, + conversationId?: string | null, + files?: Array> | null + ): Promise | DifyStream>; + createChatMessage( + inputOrRequest: ChatMessageRequest | Record, + query?: string, + user?: string, + stream = false, + conversationId?: string | null, + files?: Array> | null + ): Promise | DifyStream> { + let payload: ChatMessageRequest; + let shouldStream = stream; + + if (query === undefined && "user" in (inputOrRequest as ChatMessageRequest)) { + payload = inputOrRequest as ChatMessageRequest; + shouldStream = payload.response_mode === "streaming"; + } else { + ensureNonEmptyString(query, "query"); + ensureNonEmptyString(user, "user"); + payload = { + inputs: inputOrRequest as Record, + query, + user, + response_mode: stream ? "streaming" : "blocking", + files, + }; + if (conversationId) { + payload.conversation_id = conversationId; + } + } + + ensureNonEmptyString(payload.user, "user"); + ensureNonEmptyString(payload.query, "query"); + + if (shouldStream) { + return this.http.requestStream({ + method: "POST", + path: "/chat-messages", + data: payload, + }); + } + + return this.http.request({ + method: "POST", + path: "/chat-messages", + data: payload, + }); + } + + stopChatMessage( + taskId: string, + user: string + ): Promise> { + ensureNonEmptyString(taskId, "taskId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "POST", + path: `/chat-messages/${taskId}/stop`, + data: { user }, + }); + } + + stopMessage( + taskId: string, + user: string + ): Promise> { + return this.stopChatMessage(taskId, user); + } + + getSuggested( + messageId: string, + user: string + ): Promise> { + ensureNonEmptyString(messageId, "messageId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "GET", + path: `/messages/${messageId}/suggested`, + query: { user }, + }); + } + + // Note: messageFeedback is inherited from DifyClient + + getAppFeedbacks( + page?: number, + limit?: number + ): Promise>> { + ensureOptionalInt(page, "page"); + ensureOptionalInt(limit, "limit"); + return this.http.request({ + method: "GET", + path: "/app/feedbacks", + query: { + page, + limit, + }, + }); + } + + getConversations( + user: string, + lastId?: string | null, + limit?: number | null, + sortByOrPinned?: string | boolean | null + ): Promise>> { + ensureNonEmptyString(user, "user"); + ensureOptionalString(lastId, "lastId"); + ensureOptionalInt(limit, "limit"); + + const params: QueryParams = { user }; + if (lastId) { + params.last_id = lastId; + } + if (limit) { + params.limit = limit; + } + if (typeof sortByOrPinned === "string") { + params.sort_by = sortByOrPinned; + } else if (typeof sortByOrPinned === "boolean") { + params.pinned = sortByOrPinned; + } + + return this.http.request({ + method: "GET", + path: "/conversations", + query: params, + }); + } + + getConversationMessages( + user: string, + conversationId: string, + firstId?: string | null, + limit?: number | null + ): Promise>> { + ensureNonEmptyString(user, "user"); + ensureNonEmptyString(conversationId, "conversationId"); + ensureOptionalString(firstId, "firstId"); + ensureOptionalInt(limit, "limit"); + + const params: QueryParams = { user }; + params.conversation_id = conversationId; + if (firstId) { + params.first_id = firstId; + } + if (limit) { + params.limit = limit; + } + + return this.http.request({ + method: "GET", + path: "/messages", + query: params, + }); + } + + renameConversation( + conversationId: string, + name: string, + user: string, + autoGenerate?: boolean + ): Promise>>; + renameConversation( + conversationId: string, + user: string, + options?: { name?: string | null; autoGenerate?: boolean } + ): Promise>>; + renameConversation( + conversationId: string, + nameOrUser: string, + userOrOptions?: string | { name?: string | null; autoGenerate?: boolean }, + autoGenerate?: boolean + ): Promise>> { + ensureNonEmptyString(conversationId, "conversationId"); + + let name: string | null | undefined; + let user: string; + let resolvedAutoGenerate: boolean; + + if (typeof userOrOptions === "string" || userOrOptions === undefined) { + name = nameOrUser; + user = userOrOptions ?? ""; + resolvedAutoGenerate = autoGenerate ?? false; + } else { + user = nameOrUser; + name = userOrOptions.name; + resolvedAutoGenerate = userOrOptions.autoGenerate ?? false; + } + + ensureNonEmptyString(user, "user"); + if (!resolvedAutoGenerate) { + ensureNonEmptyString(name, "name"); + } + + const payload: Record = { + user, + auto_generate: resolvedAutoGenerate, + }; + if (typeof name === "string" && name.trim().length > 0) { + payload.name = name; + } + + return this.http.request({ + method: "POST", + path: `/conversations/${conversationId}/name`, + data: payload, + }); + } + + deleteConversation( + conversationId: string, + user: string + ): Promise>> { + ensureNonEmptyString(conversationId, "conversationId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "DELETE", + path: `/conversations/${conversationId}`, + data: { user }, + }); + } + + getConversationVariables( + conversationId: string, + user: string, + lastId?: string | null, + limit?: number | null, + variableName?: string | null + ): Promise>> { + ensureNonEmptyString(conversationId, "conversationId"); + ensureNonEmptyString(user, "user"); + ensureOptionalString(lastId, "lastId"); + ensureOptionalInt(limit, "limit"); + ensureOptionalString(variableName, "variableName"); + + return this.http.request({ + method: "GET", + path: `/conversations/${conversationId}/variables`, + query: { + user, + last_id: lastId ?? undefined, + limit: limit ?? undefined, + variable_name: variableName ?? undefined, + }, + }); + } + + updateConversationVariable( + conversationId: string, + variableId: string, + user: string, + value: unknown + ): Promise>> { + ensureNonEmptyString(conversationId, "conversationId"); + ensureNonEmptyString(variableId, "variableId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "PUT", + path: `/conversations/${conversationId}/variables/${variableId}`, + data: { + user, + value, + }, + }); + } + + annotationReplyAction( + action: "enable" | "disable", + request: AnnotationReplyActionRequest + ): Promise> { + ensureNonEmptyString(action, "action"); + ensureNonEmptyString(request.embedding_provider_name, "embedding_provider_name"); + ensureNonEmptyString(request.embedding_model_name, "embedding_model_name"); + return this.http.request({ + method: "POST", + path: `/apps/annotation-reply/${action}`, + data: request, + }); + } + + getAnnotationReplyStatus( + action: "enable" | "disable", + jobId: string + ): Promise> { + ensureNonEmptyString(action, "action"); + ensureNonEmptyString(jobId, "jobId"); + return this.http.request({ + method: "GET", + path: `/apps/annotation-reply/${action}/status/${jobId}`, + }); + } + + listAnnotations( + options?: AnnotationListOptions + ): Promise> { + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + ensureOptionalString(options?.keyword, "keyword"); + return this.http.request({ + method: "GET", + path: "/apps/annotations", + query: { + page: options?.page, + limit: options?.limit, + keyword: options?.keyword ?? undefined, + }, + }); + } + + createAnnotation( + request: AnnotationCreateRequest + ): Promise> { + ensureNonEmptyString(request.question, "question"); + ensureNonEmptyString(request.answer, "answer"); + return this.http.request({ + method: "POST", + path: "/apps/annotations", + data: request, + }); + } + + updateAnnotation( + annotationId: string, + request: AnnotationCreateRequest + ): Promise> { + ensureNonEmptyString(annotationId, "annotationId"); + ensureNonEmptyString(request.question, "question"); + ensureNonEmptyString(request.answer, "answer"); + return this.http.request({ + method: "PUT", + path: `/apps/annotations/${annotationId}`, + data: request, + }); + } + + deleteAnnotation( + annotationId: string + ): Promise> { + ensureNonEmptyString(annotationId, "annotationId"); + return this.http.request({ + method: "DELETE", + path: `/apps/annotations/${annotationId}`, + }); + } + + // Note: audioToText is inherited from DifyClient +} diff --git a/sdks/nodejs-client/src/client/completion.test.js b/sdks/nodejs-client/src/client/completion.test.js new file mode 100644 index 0000000000..b79cf3fb8f --- /dev/null +++ b/sdks/nodejs-client/src/client/completion.test.js @@ -0,0 +1,83 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { CompletionClient } from "./completion"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("CompletionClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("creates completion messages in blocking mode", async () => { + const { client, request } = createHttpClientWithSpies(); + const completion = new CompletionClient(client); + + await completion.createCompletionMessage({ input: "x" }, "user", false); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/completion-messages", + data: { + inputs: { input: "x" }, + user: "user", + files: undefined, + response_mode: "blocking", + }, + }); + }); + + it("creates completion messages in streaming mode", async () => { + const { client, requestStream } = createHttpClientWithSpies(); + const completion = new CompletionClient(client); + + await completion.createCompletionMessage({ + inputs: { input: "x" }, + user: "user", + response_mode: "streaming", + }); + + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/completion-messages", + data: { + inputs: { input: "x" }, + user: "user", + response_mode: "streaming", + }, + }); + }); + + it("stops completion messages", async () => { + const { client, request } = createHttpClientWithSpies(); + const completion = new CompletionClient(client); + + await completion.stopCompletionMessage("task", "user"); + await completion.stop("task", "user"); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/completion-messages/task/stop", + data: { user: "user" }, + }); + }); + + it("supports deprecated runWorkflow", async () => { + const { client, request, requestStream } = createHttpClientWithSpies(); + const completion = new CompletionClient(client); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await completion.runWorkflow({ input: "x" }, "user", false); + await completion.runWorkflow({ input: "x" }, "user", true); + + expect(warn).toHaveBeenCalled(); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/run", + data: { inputs: { input: "x" }, user: "user", response_mode: "blocking" }, + }); + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/run", + data: { inputs: { input: "x" }, user: "user", response_mode: "streaming" }, + }); + }); +}); diff --git a/sdks/nodejs-client/src/client/completion.ts b/sdks/nodejs-client/src/client/completion.ts new file mode 100644 index 0000000000..9e39898e8b --- /dev/null +++ b/sdks/nodejs-client/src/client/completion.ts @@ -0,0 +1,111 @@ +import { DifyClient } from "./base"; +import type { CompletionRequest, CompletionResponse } from "../types/completion"; +import type { DifyResponse, DifyStream } from "../types/common"; +import { ensureNonEmptyString } from "./validation"; + +const warned = new Set(); +const warnOnce = (message: string): void => { + if (warned.has(message)) { + return; + } + warned.add(message); + console.warn(message); +}; + +export class CompletionClient extends DifyClient { + createCompletionMessage( + request: CompletionRequest + ): Promise | DifyStream>; + createCompletionMessage( + inputs: Record, + user: string, + stream?: boolean, + files?: Array> | null + ): Promise | DifyStream>; + createCompletionMessage( + inputOrRequest: CompletionRequest | Record, + user?: string, + stream = false, + files?: Array> | null + ): Promise | DifyStream> { + let payload: CompletionRequest; + let shouldStream = stream; + + if (user === undefined && "user" in (inputOrRequest as CompletionRequest)) { + payload = inputOrRequest as CompletionRequest; + shouldStream = payload.response_mode === "streaming"; + } else { + ensureNonEmptyString(user, "user"); + payload = { + inputs: inputOrRequest as Record, + user, + files, + response_mode: stream ? "streaming" : "blocking", + }; + } + + ensureNonEmptyString(payload.user, "user"); + + if (shouldStream) { + return this.http.requestStream({ + method: "POST", + path: "/completion-messages", + data: payload, + }); + } + + return this.http.request({ + method: "POST", + path: "/completion-messages", + data: payload, + }); + } + + stopCompletionMessage( + taskId: string, + user: string + ): Promise> { + ensureNonEmptyString(taskId, "taskId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "POST", + path: `/completion-messages/${taskId}/stop`, + data: { user }, + }); + } + + stop( + taskId: string, + user: string + ): Promise> { + return this.stopCompletionMessage(taskId, user); + } + + runWorkflow( + inputs: Record, + user: string, + stream = false + ): Promise> | DifyStream>> { + warnOnce( + "CompletionClient.runWorkflow is deprecated. Use WorkflowClient.run instead." + ); + ensureNonEmptyString(user, "user"); + const payload = { + inputs, + user, + response_mode: stream ? "streaming" : "blocking", + }; + if (stream) { + return this.http.requestStream>({ + method: "POST", + path: "/workflows/run", + data: payload, + }); + } + return this.http.request>({ + method: "POST", + path: "/workflows/run", + data: payload, + }); + } +} diff --git a/sdks/nodejs-client/src/client/knowledge-base.test.js b/sdks/nodejs-client/src/client/knowledge-base.test.js new file mode 100644 index 0000000000..4381b39e56 --- /dev/null +++ b/sdks/nodejs-client/src/client/knowledge-base.test.js @@ -0,0 +1,249 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { KnowledgeBaseClient } from "./knowledge-base"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("KnowledgeBaseClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("handles dataset and tag operations", async () => { + const { client, request } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + + await kb.listDatasets({ + page: 1, + limit: 2, + keyword: "k", + includeAll: true, + tagIds: ["t1"], + }); + await kb.createDataset({ name: "dataset" }); + await kb.getDataset("ds"); + await kb.updateDataset("ds", { name: "new" }); + await kb.deleteDataset("ds"); + await kb.updateDocumentStatus("ds", "enable", ["doc1"]); + + await kb.listTags(); + await kb.createTag({ name: "tag" }); + await kb.updateTag({ tag_id: "tag", name: "name" }); + await kb.deleteTag({ tag_id: "tag" }); + await kb.bindTags({ tag_ids: ["tag"], target_id: "doc" }); + await kb.unbindTags({ tag_id: "tag", target_id: "doc" }); + await kb.getDatasetTags("ds"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/datasets", + query: { + page: 1, + limit: 2, + keyword: "k", + include_all: true, + tag_ids: ["t1"], + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets", + data: { name: "dataset" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "PATCH", + path: "/datasets/ds", + data: { name: "new" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "PATCH", + path: "/datasets/ds/documents/status/enable", + data: { document_ids: ["doc1"] }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/tags/binding", + data: { tag_ids: ["tag"], target_id: "doc" }, + }); + }); + + it("handles document operations", async () => { + const { client, request } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + const form = { append: vi.fn(), getHeaders: () => ({}) }; + + await kb.createDocumentByText("ds", { name: "doc", text: "text" }); + await kb.updateDocumentByText("ds", "doc", { name: "doc2" }); + await kb.createDocumentByFile("ds", form); + await kb.updateDocumentByFile("ds", "doc", form); + await kb.listDocuments("ds", { page: 1, limit: 20, keyword: "k" }); + await kb.getDocument("ds", "doc", { metadata: "all" }); + await kb.deleteDocument("ds", "doc"); + await kb.getDocumentIndexingStatus("ds", "batch"); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/document/create_by_text", + data: { name: "doc", text: "text" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/documents/doc/update_by_text", + data: { name: "doc2" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/document/create_by_file", + data: form, + }); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/datasets/ds/documents", + query: { page: 1, limit: 20, keyword: "k", status: undefined }, + }); + }); + + it("handles segments and child chunks", async () => { + const { client, request } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + + await kb.createSegments("ds", "doc", { segments: [{ content: "x" }] }); + await kb.listSegments("ds", "doc", { page: 1, limit: 10, keyword: "k" }); + await kb.getSegment("ds", "doc", "seg"); + await kb.updateSegment("ds", "doc", "seg", { + segment: { content: "y" }, + }); + await kb.deleteSegment("ds", "doc", "seg"); + + await kb.createChildChunk("ds", "doc", "seg", { content: "c" }); + await kb.listChildChunks("ds", "doc", "seg", { page: 1, limit: 10 }); + await kb.updateChildChunk("ds", "doc", "seg", "child", { + content: "c2", + }); + await kb.deleteChildChunk("ds", "doc", "seg", "child"); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/documents/doc/segments", + data: { segments: [{ content: "x" }] }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/documents/doc/segments/seg", + data: { segment: { content: "y" } }, + }); + expect(request).toHaveBeenCalledWith({ + method: "PATCH", + path: "/datasets/ds/documents/doc/segments/seg/child_chunks/child", + data: { content: "c2" }, + }); + }); + + it("handles metadata and retrieval", async () => { + const { client, request } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + + await kb.listMetadata("ds"); + await kb.createMetadata("ds", { name: "m", type: "string" }); + await kb.updateMetadata("ds", "mid", { name: "m2" }); + await kb.deleteMetadata("ds", "mid"); + await kb.listBuiltInMetadata("ds"); + await kb.updateBuiltInMetadata("ds", "enable"); + await kb.updateDocumentsMetadata("ds", { + operation_data: [ + { document_id: "doc", metadata_list: [{ id: "m", name: "n" }] }, + ], + }); + await kb.hitTesting("ds", { query: "q" }); + await kb.retrieve("ds", { query: "q" }); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/datasets/ds/metadata", + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/metadata", + data: { name: "m", type: "string" }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/hit-testing", + data: { query: "q" }, + }); + }); + + it("handles pipeline operations", async () => { + const { client, request, requestStream } = createHttpClientWithSpies(); + const kb = new KnowledgeBaseClient(client); + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + const form = { append: vi.fn(), getHeaders: () => ({}) }; + + await kb.listDatasourcePlugins("ds", { isPublished: true }); + await kb.runDatasourceNode("ds", "node", { + inputs: { input: "x" }, + datasource_type: "custom", + is_published: true, + }); + await kb.runPipeline("ds", { + inputs: { input: "x" }, + datasource_type: "custom", + datasource_info_list: [], + start_node_id: "start", + is_published: true, + response_mode: "streaming", + }); + await kb.runPipeline("ds", { + inputs: { input: "x" }, + datasource_type: "custom", + datasource_info_list: [], + start_node_id: "start", + is_published: true, + response_mode: "blocking", + }); + await kb.uploadPipelineFile(form); + + expect(warn).toHaveBeenCalled(); + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/datasets/ds/pipeline/datasource-plugins", + query: { is_published: true }, + }); + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/pipeline/datasource/nodes/node/run", + data: { + inputs: { input: "x" }, + datasource_type: "custom", + is_published: true, + }, + }); + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/pipeline/run", + data: { + inputs: { input: "x" }, + datasource_type: "custom", + datasource_info_list: [], + start_node_id: "start", + is_published: true, + response_mode: "streaming", + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/ds/pipeline/run", + data: { + inputs: { input: "x" }, + datasource_type: "custom", + datasource_info_list: [], + start_node_id: "start", + is_published: true, + response_mode: "blocking", + }, + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/datasets/pipeline/file-upload", + data: form, + }); + }); +}); diff --git a/sdks/nodejs-client/src/client/knowledge-base.ts b/sdks/nodejs-client/src/client/knowledge-base.ts new file mode 100644 index 0000000000..7a0e39898b --- /dev/null +++ b/sdks/nodejs-client/src/client/knowledge-base.ts @@ -0,0 +1,706 @@ +import { DifyClient } from "./base"; +import type { + DatasetCreateRequest, + DatasetListOptions, + DatasetTagBindingRequest, + DatasetTagCreateRequest, + DatasetTagDeleteRequest, + DatasetTagUnbindingRequest, + DatasetTagUpdateRequest, + DatasetUpdateRequest, + DocumentGetOptions, + DocumentListOptions, + DocumentStatusAction, + DocumentTextCreateRequest, + DocumentTextUpdateRequest, + SegmentCreateRequest, + SegmentListOptions, + SegmentUpdateRequest, + ChildChunkCreateRequest, + ChildChunkListOptions, + ChildChunkUpdateRequest, + MetadataCreateRequest, + MetadataOperationRequest, + MetadataUpdateRequest, + HitTestingRequest, + DatasourcePluginListOptions, + DatasourceNodeRunRequest, + PipelineRunRequest, + KnowledgeBaseResponse, + PipelineStreamEvent, +} from "../types/knowledge-base"; +import type { DifyResponse, DifyStream, QueryParams } from "../types/common"; +import { + ensureNonEmptyString, + ensureOptionalBoolean, + ensureOptionalInt, + ensureOptionalString, + ensureStringArray, +} from "./validation"; +import { FileUploadError, ValidationError } from "../errors/dify-error"; +import { isFormData } from "../http/form-data"; + +const warned = new Set(); +const warnOnce = (message: string): void => { + if (warned.has(message)) { + return; + } + warned.add(message); + console.warn(message); +}; + +const ensureFormData = (form: unknown, context: string): void => { + if (!isFormData(form)) { + throw new FileUploadError(`${context} requires FormData`); + } +}; + +const ensureNonEmptyArray = (value: unknown, name: string): void => { + if (!Array.isArray(value) || value.length === 0) { + throw new ValidationError(`${name} must be a non-empty array`); + } +}; + +const warnPipelineRoutes = (): void => { + warnOnce( + "RAG pipeline endpoints may be unavailable unless the service API registers dataset/rag_pipeline routes." + ); +}; + +export class KnowledgeBaseClient extends DifyClient { + async listDatasets( + options?: DatasetListOptions + ): Promise> { + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + ensureOptionalString(options?.keyword, "keyword"); + ensureOptionalBoolean(options?.includeAll, "includeAll"); + + const query: QueryParams = { + page: options?.page, + limit: options?.limit, + keyword: options?.keyword ?? undefined, + include_all: options?.includeAll ?? undefined, + }; + + if (options?.tagIds && options.tagIds.length > 0) { + ensureStringArray(options.tagIds, "tagIds"); + query.tag_ids = options.tagIds; + } + + return this.http.request({ + method: "GET", + path: "/datasets", + query, + }); + } + + async createDataset( + request: DatasetCreateRequest + ): Promise> { + ensureNonEmptyString(request.name, "name"); + return this.http.request({ + method: "POST", + path: "/datasets", + data: request, + }); + } + + async getDataset(datasetId: string): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}`, + }); + } + + async updateDataset( + datasetId: string, + request: DatasetUpdateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + if (request.name !== undefined && request.name !== null) { + ensureNonEmptyString(request.name, "name"); + } + return this.http.request({ + method: "PATCH", + path: `/datasets/${datasetId}`, + data: request, + }); + } + + async deleteDataset(datasetId: string): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "DELETE", + path: `/datasets/${datasetId}`, + }); + } + + async updateDocumentStatus( + datasetId: string, + action: DocumentStatusAction, + documentIds: string[] + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(action, "action"); + ensureStringArray(documentIds, "documentIds"); + return this.http.request({ + method: "PATCH", + path: `/datasets/${datasetId}/documents/status/${action}`, + data: { + document_ids: documentIds, + }, + }); + } + + async listTags(): Promise> { + return this.http.request({ + method: "GET", + path: "/datasets/tags", + }); + } + + async createTag( + request: DatasetTagCreateRequest + ): Promise> { + ensureNonEmptyString(request.name, "name"); + return this.http.request({ + method: "POST", + path: "/datasets/tags", + data: request, + }); + } + + async updateTag( + request: DatasetTagUpdateRequest + ): Promise> { + ensureNonEmptyString(request.tag_id, "tag_id"); + ensureNonEmptyString(request.name, "name"); + return this.http.request({ + method: "PATCH", + path: "/datasets/tags", + data: request, + }); + } + + async deleteTag( + request: DatasetTagDeleteRequest + ): Promise> { + ensureNonEmptyString(request.tag_id, "tag_id"); + return this.http.request({ + method: "DELETE", + path: "/datasets/tags", + data: request, + }); + } + + async bindTags( + request: DatasetTagBindingRequest + ): Promise> { + ensureStringArray(request.tag_ids, "tag_ids"); + ensureNonEmptyString(request.target_id, "target_id"); + return this.http.request({ + method: "POST", + path: "/datasets/tags/binding", + data: request, + }); + } + + async unbindTags( + request: DatasetTagUnbindingRequest + ): Promise> { + ensureNonEmptyString(request.tag_id, "tag_id"); + ensureNonEmptyString(request.target_id, "target_id"); + return this.http.request({ + method: "POST", + path: "/datasets/tags/unbinding", + data: request, + }); + } + + async getDatasetTags( + datasetId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/tags`, + }); + } + + async createDocumentByText( + datasetId: string, + request: DocumentTextCreateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(request.name, "name"); + ensureNonEmptyString(request.text, "text"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/document/create_by_text`, + data: request, + }); + } + + async updateDocumentByText( + datasetId: string, + documentId: string, + request: DocumentTextUpdateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + if (request.name !== undefined && request.name !== null) { + ensureNonEmptyString(request.name, "name"); + } + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/${documentId}/update_by_text`, + data: request, + }); + } + + async createDocumentByFile( + datasetId: string, + form: unknown + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureFormData(form, "createDocumentByFile"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/document/create_by_file`, + data: form, + }); + } + + async updateDocumentByFile( + datasetId: string, + documentId: string, + form: unknown + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureFormData(form, "updateDocumentByFile"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/${documentId}/update_by_file`, + data: form, + }); + } + + async listDocuments( + datasetId: string, + options?: DocumentListOptions + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + ensureOptionalString(options?.keyword, "keyword"); + ensureOptionalString(options?.status, "status"); + + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents`, + query: { + page: options?.page, + limit: options?.limit, + keyword: options?.keyword ?? undefined, + status: options?.status ?? undefined, + }, + }); + } + + async getDocument( + datasetId: string, + documentId: string, + options?: DocumentGetOptions + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + if (options?.metadata) { + const allowed = new Set(["all", "only", "without"]); + if (!allowed.has(options.metadata)) { + throw new ValidationError("metadata must be one of all, only, without"); + } + } + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents/${documentId}`, + query: { + metadata: options?.metadata ?? undefined, + }, + }); + } + + async deleteDocument( + datasetId: string, + documentId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + return this.http.request({ + method: "DELETE", + path: `/datasets/${datasetId}/documents/${documentId}`, + }); + } + + async getDocumentIndexingStatus( + datasetId: string, + batch: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(batch, "batch"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents/${batch}/indexing-status`, + }); + } + + async createSegments( + datasetId: string, + documentId: string, + request: SegmentCreateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyArray(request.segments, "segments"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/${documentId}/segments`, + data: request, + }); + } + + async listSegments( + datasetId: string, + documentId: string, + options?: SegmentListOptions + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + ensureOptionalString(options?.keyword, "keyword"); + if (options?.status && options.status.length > 0) { + ensureStringArray(options.status, "status"); + } + + const query: QueryParams = { + page: options?.page, + limit: options?.limit, + keyword: options?.keyword ?? undefined, + }; + if (options?.status && options.status.length > 0) { + query.status = options.status; + } + + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents/${documentId}/segments`, + query, + }); + } + + async getSegment( + datasetId: string, + documentId: string, + segmentId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + }); + } + + async updateSegment( + datasetId: string, + documentId: string, + segmentId: string, + request: SegmentUpdateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + data: request, + }); + } + + async deleteSegment( + datasetId: string, + documentId: string, + segmentId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + return this.http.request({ + method: "DELETE", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}`, + }); + } + + async createChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + request: ChildChunkCreateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + ensureNonEmptyString(request.content, "content"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, + data: request, + }); + } + + async listChildChunks( + datasetId: string, + documentId: string, + segmentId: string, + options?: ChildChunkListOptions + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + ensureOptionalString(options?.keyword, "keyword"); + + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks`, + query: { + page: options?.page, + limit: options?.limit, + keyword: options?.keyword ?? undefined, + }, + }); + } + + async updateChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + childChunkId: string, + request: ChildChunkUpdateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + ensureNonEmptyString(childChunkId, "childChunkId"); + ensureNonEmptyString(request.content, "content"); + return this.http.request({ + method: "PATCH", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, + data: request, + }); + } + + async deleteChildChunk( + datasetId: string, + documentId: string, + segmentId: string, + childChunkId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(documentId, "documentId"); + ensureNonEmptyString(segmentId, "segmentId"); + ensureNonEmptyString(childChunkId, "childChunkId"); + return this.http.request({ + method: "DELETE", + path: `/datasets/${datasetId}/documents/${documentId}/segments/${segmentId}/child_chunks/${childChunkId}`, + }); + } + + async listMetadata( + datasetId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/metadata`, + }); + } + + async createMetadata( + datasetId: string, + request: MetadataCreateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(request.name, "name"); + ensureNonEmptyString(request.type, "type"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/metadata`, + data: request, + }); + } + + async updateMetadata( + datasetId: string, + metadataId: string, + request: MetadataUpdateRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(metadataId, "metadataId"); + ensureNonEmptyString(request.name, "name"); + return this.http.request({ + method: "PATCH", + path: `/datasets/${datasetId}/metadata/${metadataId}`, + data: request, + }); + } + + async deleteMetadata( + datasetId: string, + metadataId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(metadataId, "metadataId"); + return this.http.request({ + method: "DELETE", + path: `/datasets/${datasetId}/metadata/${metadataId}`, + }); + } + + async listBuiltInMetadata( + datasetId: string + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/metadata/built-in`, + }); + } + + async updateBuiltInMetadata( + datasetId: string, + action: "enable" | "disable" + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(action, "action"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/metadata/built-in/${action}`, + }); + } + + async updateDocumentsMetadata( + datasetId: string, + request: MetadataOperationRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyArray(request.operation_data, "operation_data"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/documents/metadata`, + data: request, + }); + } + + async hitTesting( + datasetId: string, + request: HitTestingRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + if (request.query !== undefined && request.query !== null) { + ensureOptionalString(request.query, "query"); + } + if (request.attachment_ids && request.attachment_ids.length > 0) { + ensureStringArray(request.attachment_ids, "attachment_ids"); + } + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/hit-testing`, + data: request, + }); + } + + async retrieve( + datasetId: string, + request: HitTestingRequest + ): Promise> { + ensureNonEmptyString(datasetId, "datasetId"); + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/retrieve`, + data: request, + }); + } + + async listDatasourcePlugins( + datasetId: string, + options?: DatasourcePluginListOptions + ): Promise> { + warnPipelineRoutes(); + ensureNonEmptyString(datasetId, "datasetId"); + ensureOptionalBoolean(options?.isPublished, "isPublished"); + return this.http.request({ + method: "GET", + path: `/datasets/${datasetId}/pipeline/datasource-plugins`, + query: { + is_published: options?.isPublished ?? undefined, + }, + }); + } + + async runDatasourceNode( + datasetId: string, + nodeId: string, + request: DatasourceNodeRunRequest + ): Promise> { + warnPipelineRoutes(); + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(nodeId, "nodeId"); + ensureNonEmptyString(request.datasource_type, "datasource_type"); + return this.http.requestStream({ + method: "POST", + path: `/datasets/${datasetId}/pipeline/datasource/nodes/${nodeId}/run`, + data: request, + }); + } + + async runPipeline( + datasetId: string, + request: PipelineRunRequest + ): Promise | DifyStream> { + warnPipelineRoutes(); + ensureNonEmptyString(datasetId, "datasetId"); + ensureNonEmptyString(request.datasource_type, "datasource_type"); + ensureNonEmptyString(request.start_node_id, "start_node_id"); + const shouldStream = request.response_mode === "streaming"; + if (shouldStream) { + return this.http.requestStream({ + method: "POST", + path: `/datasets/${datasetId}/pipeline/run`, + data: request, + }); + } + return this.http.request({ + method: "POST", + path: `/datasets/${datasetId}/pipeline/run`, + data: request, + }); + } + + async uploadPipelineFile( + form: unknown + ): Promise> { + warnPipelineRoutes(); + ensureFormData(form, "uploadPipelineFile"); + return this.http.request({ + method: "POST", + path: "/datasets/pipeline/file-upload", + data: form, + }); + } +} diff --git a/sdks/nodejs-client/src/client/validation.test.js b/sdks/nodejs-client/src/client/validation.test.js new file mode 100644 index 0000000000..65bfa471a6 --- /dev/null +++ b/sdks/nodejs-client/src/client/validation.test.js @@ -0,0 +1,91 @@ +import { describe, expect, it } from "vitest"; +import { + ensureNonEmptyString, + ensureOptionalBoolean, + ensureOptionalInt, + ensureOptionalString, + ensureOptionalStringArray, + ensureRating, + ensureStringArray, + validateParams, +} from "./validation"; + +const makeLongString = (length) => "a".repeat(length); + +describe("validation utilities", () => { + it("ensureNonEmptyString throws on empty or whitespace", () => { + expect(() => ensureNonEmptyString("", "name")).toThrow(); + expect(() => ensureNonEmptyString(" ", "name")).toThrow(); + }); + + it("ensureNonEmptyString throws on overly long strings", () => { + expect(() => + ensureNonEmptyString(makeLongString(10001), "name") + ).toThrow(); + }); + + it("ensureOptionalString ignores undefined and validates when set", () => { + expect(() => ensureOptionalString(undefined, "opt")).not.toThrow(); + expect(() => ensureOptionalString("", "opt")).toThrow(); + }); + + it("ensureOptionalString throws on overly long strings", () => { + expect(() => ensureOptionalString(makeLongString(10001), "opt")).toThrow(); + }); + + it("ensureOptionalInt validates integer", () => { + expect(() => ensureOptionalInt(undefined, "limit")).not.toThrow(); + expect(() => ensureOptionalInt(1.2, "limit")).toThrow(); + }); + + it("ensureOptionalBoolean validates boolean", () => { + expect(() => ensureOptionalBoolean(undefined, "flag")).not.toThrow(); + expect(() => ensureOptionalBoolean("yes", "flag")).toThrow(); + }); + + it("ensureStringArray enforces size and content", () => { + expect(() => ensureStringArray([], "items")).toThrow(); + expect(() => ensureStringArray([""], "items")).toThrow(); + expect(() => + ensureStringArray(Array.from({ length: 1001 }, () => "a"), "items") + ).toThrow(); + expect(() => ensureStringArray(["ok"], "items")).not.toThrow(); + }); + + it("ensureOptionalStringArray ignores undefined", () => { + expect(() => ensureOptionalStringArray(undefined, "tags")).not.toThrow(); + }); + + it("ensureOptionalStringArray validates when set", () => { + expect(() => ensureOptionalStringArray(["valid"], "tags")).not.toThrow(); + expect(() => ensureOptionalStringArray([], "tags")).toThrow(); + expect(() => ensureOptionalStringArray([""], "tags")).toThrow(); + }); + + it("ensureRating validates allowed values", () => { + expect(() => ensureRating(undefined)).not.toThrow(); + expect(() => ensureRating("like")).not.toThrow(); + expect(() => ensureRating("bad")).toThrow(); + }); + + it("validateParams enforces generic rules", () => { + expect(() => validateParams({ user: 123 })).toThrow(); + expect(() => validateParams({ rating: "bad" })).toThrow(); + expect(() => validateParams({ page: 1.1 })).toThrow(); + expect(() => validateParams({ files: "bad" })).toThrow(); + // Empty strings are allowed for optional params (e.g., keyword: "" means no filter) + expect(() => validateParams({ keyword: "" })).not.toThrow(); + expect(() => validateParams({ name: makeLongString(10001) })).toThrow(); + expect(() => + validateParams({ items: Array.from({ length: 1001 }, () => "a") }) + ).toThrow(); + expect(() => + validateParams({ + data: Object.fromEntries( + Array.from({ length: 101 }, (_, i) => [String(i), i]) + ), + }) + ).toThrow(); + expect(() => validateParams({ user: "u", page: 1 })).not.toThrow(); + }); +}); diff --git a/sdks/nodejs-client/src/client/validation.ts b/sdks/nodejs-client/src/client/validation.ts new file mode 100644 index 0000000000..6aeec36bdc --- /dev/null +++ b/sdks/nodejs-client/src/client/validation.ts @@ -0,0 +1,136 @@ +import { ValidationError } from "../errors/dify-error"; + +const MAX_STRING_LENGTH = 10000; +const MAX_LIST_LENGTH = 1000; +const MAX_DICT_LENGTH = 100; + +export function ensureNonEmptyString( + value: unknown, + name: string +): asserts value is string { + if (typeof value !== "string" || value.trim().length === 0) { + throw new ValidationError(`${name} must be a non-empty string`); + } + if (value.length > MAX_STRING_LENGTH) { + throw new ValidationError( + `${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters` + ); + } +} + +/** + * Validates optional string fields that must be non-empty when provided. + * Use this for fields like `name` that are optional but should not be empty strings. + * + * For filter parameters that accept empty strings (e.g., `keyword: ""`), + * use `validateParams` which allows empty strings for optional params. + */ +export function ensureOptionalString(value: unknown, name: string): void { + if (value === undefined || value === null) { + return; + } + if (typeof value !== "string" || value.trim().length === 0) { + throw new ValidationError(`${name} must be a non-empty string when set`); + } + if (value.length > MAX_STRING_LENGTH) { + throw new ValidationError( + `${name} exceeds maximum length of ${MAX_STRING_LENGTH} characters` + ); + } +} + +export function ensureOptionalInt(value: unknown, name: string): void { + if (value === undefined || value === null) { + return; + } + if (!Number.isInteger(value)) { + throw new ValidationError(`${name} must be an integer when set`); + } +} + +export function ensureOptionalBoolean(value: unknown, name: string): void { + if (value === undefined || value === null) { + return; + } + if (typeof value !== "boolean") { + throw new ValidationError(`${name} must be a boolean when set`); + } +} + +export function ensureStringArray(value: unknown, name: string): void { + if (!Array.isArray(value) || value.length === 0) { + throw new ValidationError(`${name} must be a non-empty string array`); + } + if (value.length > MAX_LIST_LENGTH) { + throw new ValidationError( + `${name} exceeds maximum size of ${MAX_LIST_LENGTH} items` + ); + } + value.forEach((item) => { + if (typeof item !== "string" || item.trim().length === 0) { + throw new ValidationError(`${name} must contain non-empty strings`); + } + }); +} + +export function ensureOptionalStringArray(value: unknown, name: string): void { + if (value === undefined || value === null) { + return; + } + ensureStringArray(value, name); +} + +export function ensureRating(value: unknown): void { + if (value === undefined || value === null) { + return; + } + if (value !== "like" && value !== "dislike") { + throw new ValidationError("rating must be either 'like' or 'dislike'"); + } +} + +export function validateParams(params: Record): void { + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + + // Only check max length for strings; empty strings are allowed for optional params + // Required fields are validated at method level via ensureNonEmptyString + if (typeof value === "string") { + if (value.length > MAX_STRING_LENGTH) { + throw new ValidationError( + `Parameter '${key}' exceeds maximum length of ${MAX_STRING_LENGTH} characters` + ); + } + } else if (Array.isArray(value)) { + if (value.length > MAX_LIST_LENGTH) { + throw new ValidationError( + `Parameter '${key}' exceeds maximum size of ${MAX_LIST_LENGTH} items` + ); + } + } else if (typeof value === "object") { + if (Object.keys(value as Record).length > MAX_DICT_LENGTH) { + throw new ValidationError( + `Parameter '${key}' exceeds maximum size of ${MAX_DICT_LENGTH} items` + ); + } + } + + if (key === "user" && typeof value !== "string") { + throw new ValidationError(`Parameter '${key}' must be a string`); + } + if ( + (key === "page" || key === "limit" || key === "page_size") && + !Number.isInteger(value) + ) { + throw new ValidationError(`Parameter '${key}' must be an integer`); + } + if (key === "files" && !Array.isArray(value) && typeof value !== "object") { + throw new ValidationError(`Parameter '${key}' must be a list or dict`); + } + if (key === "rating" && value !== "like" && value !== "dislike") { + throw new ValidationError(`Parameter '${key}' must be 'like' or 'dislike'`); + } + }); +} diff --git a/sdks/nodejs-client/src/client/workflow.test.js b/sdks/nodejs-client/src/client/workflow.test.js new file mode 100644 index 0000000000..79c419b55a --- /dev/null +++ b/sdks/nodejs-client/src/client/workflow.test.js @@ -0,0 +1,119 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { WorkflowClient } from "./workflow"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("WorkflowClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("runs workflows with blocking and streaming modes", async () => { + const { client, request, requestStream } = createHttpClientWithSpies(); + const workflow = new WorkflowClient(client); + + await workflow.run({ inputs: { input: "x" }, user: "user" }); + await workflow.run({ input: "x" }, "user", true); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/run", + data: { + inputs: { input: "x" }, + user: "user", + }, + }); + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/run", + data: { + inputs: { input: "x" }, + user: "user", + response_mode: "streaming", + }, + }); + }); + + it("runs workflow by id", async () => { + const { client, request, requestStream } = createHttpClientWithSpies(); + const workflow = new WorkflowClient(client); + + await workflow.runById("wf", { + inputs: { input: "x" }, + user: "user", + response_mode: "blocking", + }); + await workflow.runById("wf", { + inputs: { input: "x" }, + user: "user", + response_mode: "streaming", + }); + + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/wf/run", + data: { + inputs: { input: "x" }, + user: "user", + response_mode: "blocking", + }, + }); + expect(requestStream).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/wf/run", + data: { + inputs: { input: "x" }, + user: "user", + response_mode: "streaming", + }, + }); + }); + + it("gets run details and stops workflow", async () => { + const { client, request } = createHttpClientWithSpies(); + const workflow = new WorkflowClient(client); + + await workflow.getRun("run"); + await workflow.stop("task", "user"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/workflows/run/run", + }); + expect(request).toHaveBeenCalledWith({ + method: "POST", + path: "/workflows/tasks/task/stop", + data: { user: "user" }, + }); + }); + + it("fetches workflow logs", async () => { + const { client, request } = createHttpClientWithSpies(); + const workflow = new WorkflowClient(client); + + // Use createdByEndUserSessionId to filter by user session (backend API parameter) + await workflow.getLogs({ + keyword: "k", + status: "succeeded", + startTime: "2024-01-01", + endTime: "2024-01-02", + createdByEndUserSessionId: "session-123", + page: 1, + limit: 20, + }); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/workflows/logs", + query: { + keyword: "k", + status: "succeeded", + created_at__before: "2024-01-02", + created_at__after: "2024-01-01", + created_by_end_user_session_id: "session-123", + created_by_account: undefined, + page: 1, + limit: 20, + }, + }); + }); +}); diff --git a/sdks/nodejs-client/src/client/workflow.ts b/sdks/nodejs-client/src/client/workflow.ts new file mode 100644 index 0000000000..ae4d5861fa --- /dev/null +++ b/sdks/nodejs-client/src/client/workflow.ts @@ -0,0 +1,165 @@ +import { DifyClient } from "./base"; +import type { WorkflowRunRequest, WorkflowRunResponse } from "../types/workflow"; +import type { DifyResponse, DifyStream, QueryParams } from "../types/common"; +import { + ensureNonEmptyString, + ensureOptionalInt, + ensureOptionalString, +} from "./validation"; + +export class WorkflowClient extends DifyClient { + run( + request: WorkflowRunRequest + ): Promise | DifyStream>; + run( + inputs: Record, + user: string, + stream?: boolean + ): Promise | DifyStream>; + run( + inputOrRequest: WorkflowRunRequest | Record, + user?: string, + stream = false + ): Promise | DifyStream> { + let payload: WorkflowRunRequest; + let shouldStream = stream; + + if (user === undefined && "user" in (inputOrRequest as WorkflowRunRequest)) { + payload = inputOrRequest as WorkflowRunRequest; + shouldStream = payload.response_mode === "streaming"; + } else { + ensureNonEmptyString(user, "user"); + payload = { + inputs: inputOrRequest as Record, + user, + response_mode: stream ? "streaming" : "blocking", + }; + } + + ensureNonEmptyString(payload.user, "user"); + + if (shouldStream) { + return this.http.requestStream({ + method: "POST", + path: "/workflows/run", + data: payload, + }); + } + + return this.http.request({ + method: "POST", + path: "/workflows/run", + data: payload, + }); + } + + runById( + workflowId: string, + request: WorkflowRunRequest + ): Promise | DifyStream> { + ensureNonEmptyString(workflowId, "workflowId"); + ensureNonEmptyString(request.user, "user"); + if (request.response_mode === "streaming") { + return this.http.requestStream({ + method: "POST", + path: `/workflows/${workflowId}/run`, + data: request, + }); + } + return this.http.request({ + method: "POST", + path: `/workflows/${workflowId}/run`, + data: request, + }); + } + + getRun(workflowRunId: string): Promise> { + ensureNonEmptyString(workflowRunId, "workflowRunId"); + return this.http.request({ + method: "GET", + path: `/workflows/run/${workflowRunId}`, + }); + } + + stop( + taskId: string, + user: string + ): Promise> { + ensureNonEmptyString(taskId, "taskId"); + ensureNonEmptyString(user, "user"); + return this.http.request({ + method: "POST", + path: `/workflows/tasks/${taskId}/stop`, + data: { user }, + }); + } + + /** + * Get workflow execution logs with filtering options. + * + * Note: The backend API filters by `createdByEndUserSessionId` (end user session ID) + * or `createdByAccount` (account ID), not by a generic `user` parameter. + */ + getLogs(options?: { + keyword?: string; + status?: string; + createdAtBefore?: string; + createdAtAfter?: string; + createdByEndUserSessionId?: string; + createdByAccount?: string; + page?: number; + limit?: number; + startTime?: string; + endTime?: string; + }): Promise>> { + if (options?.keyword) { + ensureOptionalString(options.keyword, "keyword"); + } + if (options?.status) { + ensureOptionalString(options.status, "status"); + } + if (options?.createdAtBefore) { + ensureOptionalString(options.createdAtBefore, "createdAtBefore"); + } + if (options?.createdAtAfter) { + ensureOptionalString(options.createdAtAfter, "createdAtAfter"); + } + if (options?.createdByEndUserSessionId) { + ensureOptionalString( + options.createdByEndUserSessionId, + "createdByEndUserSessionId" + ); + } + if (options?.createdByAccount) { + ensureOptionalString(options.createdByAccount, "createdByAccount"); + } + if (options?.startTime) { + ensureOptionalString(options.startTime, "startTime"); + } + if (options?.endTime) { + ensureOptionalString(options.endTime, "endTime"); + } + ensureOptionalInt(options?.page, "page"); + ensureOptionalInt(options?.limit, "limit"); + + const createdAtAfter = options?.createdAtAfter ?? options?.startTime; + const createdAtBefore = options?.createdAtBefore ?? options?.endTime; + + const query: QueryParams = { + keyword: options?.keyword, + status: options?.status, + created_at__before: createdAtBefore, + created_at__after: createdAtAfter, + created_by_end_user_session_id: options?.createdByEndUserSessionId, + created_by_account: options?.createdByAccount, + page: options?.page, + limit: options?.limit, + }; + + return this.http.request({ + method: "GET", + path: "/workflows/logs", + query, + }); + } +} diff --git a/sdks/nodejs-client/src/client/workspace.test.js b/sdks/nodejs-client/src/client/workspace.test.js new file mode 100644 index 0000000000..f8fb6e375a --- /dev/null +++ b/sdks/nodejs-client/src/client/workspace.test.js @@ -0,0 +1,21 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { WorkspaceClient } from "./workspace"; +import { createHttpClientWithSpies } from "../../tests/test-utils"; + +describe("WorkspaceClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("gets models by type", async () => { + const { client, request } = createHttpClientWithSpies(); + const workspace = new WorkspaceClient(client); + + await workspace.getModelsByType("llm"); + + expect(request).toHaveBeenCalledWith({ + method: "GET", + path: "/workspaces/current/models/model-types/llm", + }); + }); +}); diff --git a/sdks/nodejs-client/src/client/workspace.ts b/sdks/nodejs-client/src/client/workspace.ts new file mode 100644 index 0000000000..daee540c99 --- /dev/null +++ b/sdks/nodejs-client/src/client/workspace.ts @@ -0,0 +1,16 @@ +import { DifyClient } from "./base"; +import type { WorkspaceModelType, WorkspaceModelsResponse } from "../types/workspace"; +import type { DifyResponse } from "../types/common"; +import { ensureNonEmptyString } from "./validation"; + +export class WorkspaceClient extends DifyClient { + async getModelsByType( + modelType: WorkspaceModelType + ): Promise> { + ensureNonEmptyString(modelType, "modelType"); + return this.http.request({ + method: "GET", + path: `/workspaces/current/models/model-types/${modelType}`, + }); + } +} diff --git a/sdks/nodejs-client/src/errors/dify-error.test.js b/sdks/nodejs-client/src/errors/dify-error.test.js new file mode 100644 index 0000000000..d151eca391 --- /dev/null +++ b/sdks/nodejs-client/src/errors/dify-error.test.js @@ -0,0 +1,37 @@ +import { describe, expect, it } from "vitest"; +import { + APIError, + AuthenticationError, + DifyError, + FileUploadError, + NetworkError, + RateLimitError, + TimeoutError, + ValidationError, +} from "./dify-error"; + +describe("Dify errors", () => { + it("sets base error fields", () => { + const err = new DifyError("base", { + statusCode: 400, + responseBody: { message: "bad" }, + requestId: "req", + retryAfter: 1, + }); + expect(err.name).toBe("DifyError"); + expect(err.statusCode).toBe(400); + expect(err.responseBody).toEqual({ message: "bad" }); + expect(err.requestId).toBe("req"); + expect(err.retryAfter).toBe(1); + }); + + it("creates specific error types", () => { + expect(new APIError("api").name).toBe("APIError"); + expect(new AuthenticationError("auth").name).toBe("AuthenticationError"); + expect(new RateLimitError("rate").name).toBe("RateLimitError"); + expect(new ValidationError("val").name).toBe("ValidationError"); + expect(new NetworkError("net").name).toBe("NetworkError"); + expect(new TimeoutError("timeout").name).toBe("TimeoutError"); + expect(new FileUploadError("upload").name).toBe("FileUploadError"); + }); +}); diff --git a/sdks/nodejs-client/src/errors/dify-error.ts b/sdks/nodejs-client/src/errors/dify-error.ts new file mode 100644 index 0000000000..e393e1b132 --- /dev/null +++ b/sdks/nodejs-client/src/errors/dify-error.ts @@ -0,0 +1,75 @@ +export type DifyErrorOptions = { + statusCode?: number; + responseBody?: unknown; + requestId?: string; + retryAfter?: number; + cause?: unknown; +}; + +export class DifyError extends Error { + statusCode?: number; + responseBody?: unknown; + requestId?: string; + retryAfter?: number; + + constructor(message: string, options: DifyErrorOptions = {}) { + super(message); + this.name = "DifyError"; + this.statusCode = options.statusCode; + this.responseBody = options.responseBody; + this.requestId = options.requestId; + this.retryAfter = options.retryAfter; + if (options.cause) { + (this as { cause?: unknown }).cause = options.cause; + } + } +} + +export class APIError extends DifyError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "APIError"; + } +} + +export class AuthenticationError extends APIError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "AuthenticationError"; + } +} + +export class RateLimitError extends APIError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "RateLimitError"; + } +} + +export class ValidationError extends APIError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "ValidationError"; + } +} + +export class NetworkError extends DifyError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "NetworkError"; + } +} + +export class TimeoutError extends DifyError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "TimeoutError"; + } +} + +export class FileUploadError extends DifyError { + constructor(message: string, options: DifyErrorOptions = {}) { + super(message, options); + this.name = "FileUploadError"; + } +} diff --git a/sdks/nodejs-client/src/http/client.test.js b/sdks/nodejs-client/src/http/client.test.js new file mode 100644 index 0000000000..05892547ed --- /dev/null +++ b/sdks/nodejs-client/src/http/client.test.js @@ -0,0 +1,304 @@ +import axios from "axios"; +import { Readable } from "node:stream"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { + APIError, + AuthenticationError, + FileUploadError, + NetworkError, + RateLimitError, + TimeoutError, + ValidationError, +} from "../errors/dify-error"; +import { HttpClient } from "./client"; + +describe("HttpClient", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + it("builds requests with auth headers and JSON content type", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: { ok: true }, + headers: { "x-request-id": "req" }, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + + const client = new HttpClient({ apiKey: "test" }); + const response = await client.request({ + method: "POST", + path: "/chat-messages", + data: { user: "u" }, + }); + + expect(response.requestId).toBe("req"); + const config = mockRequest.mock.calls[0][0]; + expect(config.headers.Authorization).toBe("Bearer test"); + expect(config.headers["Content-Type"]).toBe("application/json"); + expect(config.responseType).toBe("json"); + }); + + it("serializes array query params", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: "ok", + headers: {}, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + + const client = new HttpClient({ apiKey: "test" }); + await client.requestRaw({ + method: "GET", + path: "/datasets", + query: { tag_ids: ["a", "b"], limit: 2 }, + }); + + const config = mockRequest.mock.calls[0][0]; + const queryString = config.paramsSerializer.serialize({ + tag_ids: ["a", "b"], + limit: 2, + }); + expect(queryString).toBe("tag_ids=a&tag_ids=b&limit=2"); + }); + + it("returns SSE stream helpers", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: Readable.from(["data: {\"text\":\"hi\"}\n\n"]), + headers: { "x-request-id": "req" }, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + + const client = new HttpClient({ apiKey: "test" }); + const stream = await client.requestStream({ + method: "POST", + path: "/chat-messages", + data: { user: "u" }, + }); + + expect(stream.status).toBe(200); + expect(stream.requestId).toBe("req"); + await expect(stream.toText()).resolves.toBe("hi"); + }); + + it("returns binary stream helpers", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: Readable.from(["chunk"]), + headers: { "x-request-id": "req" }, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + + const client = new HttpClient({ apiKey: "test" }); + const stream = await client.requestBinaryStream({ + method: "POST", + path: "/text-to-audio", + data: { user: "u", text: "hi" }, + }); + + expect(stream.status).toBe(200); + expect(stream.requestId).toBe("req"); + }); + + it("respects form-data headers", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: "ok", + headers: {}, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + + const client = new HttpClient({ apiKey: "test" }); + const form = { + append: () => {}, + getHeaders: () => ({ "content-type": "multipart/form-data; boundary=abc" }), + }; + + await client.requestRaw({ + method: "POST", + path: "/files/upload", + data: form, + }); + + const config = mockRequest.mock.calls[0][0]; + expect(config.headers["content-type"]).toBe( + "multipart/form-data; boundary=abc" + ); + expect(config.headers["Content-Type"]).toBeUndefined(); + }); + + it("maps 401 and 429 errors", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 401, + data: { message: "unauthorized" }, + headers: {}, + }, + }); + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(AuthenticationError); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 429, + data: { message: "rate" }, + headers: { "retry-after": "2" }, + }, + }); + const error = await client + .requestRaw({ method: "GET", path: "/meta" }) + .catch((err) => err); + expect(error).toBeInstanceOf(RateLimitError); + expect(error.retryAfter).toBe(2); + }); + + it("maps validation and upload errors", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + response: { + status: 422, + data: { message: "invalid" }, + headers: {}, + }, + }); + await expect( + client.requestRaw({ method: "POST", path: "/chat-messages", data: { user: "u" } }) + ).rejects.toBeInstanceOf(ValidationError); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + config: { url: "/files/upload" }, + response: { + status: 400, + data: { message: "bad upload" }, + headers: {}, + }, + }); + await expect( + client.requestRaw({ method: "POST", path: "/files/upload", data: { user: "u" } }) + ).rejects.toBeInstanceOf(FileUploadError); + }); + + it("maps timeout and network errors", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + code: "ECONNABORTED", + message: "timeout", + }); + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(TimeoutError); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + message: "network", + }); + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(NetworkError); + }); + + it("retries on timeout errors", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", maxRetries: 1, retryDelay: 0 }); + + mockRequest + .mockRejectedValueOnce({ + isAxiosError: true, + code: "ECONNABORTED", + message: "timeout", + }) + .mockResolvedValueOnce({ status: 200, data: "ok", headers: {} }); + + await client.requestRaw({ method: "GET", path: "/meta" }); + expect(mockRequest).toHaveBeenCalledTimes(2); + }); + + it("validates query parameters before request", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test" }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta", query: { user: 1 } }) + ).rejects.toBeInstanceOf(ValidationError); + expect(mockRequest).not.toHaveBeenCalled(); + }); + + it("returns APIError for other http failures", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", maxRetries: 0 }); + + mockRequest.mockRejectedValueOnce({ + isAxiosError: true, + response: { status: 500, data: { message: "server" }, headers: {} }, + }); + + await expect( + client.requestRaw({ method: "GET", path: "/meta" }) + ).rejects.toBeInstanceOf(APIError); + }); + + it("logs requests and responses when enableLogging is true", async () => { + const mockRequest = vi.fn().mockResolvedValue({ + status: 200, + data: { ok: true }, + headers: {}, + }); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + + const client = new HttpClient({ apiKey: "test", enableLogging: true }); + await client.requestRaw({ method: "GET", path: "/meta" }); + + expect(consoleInfo).toHaveBeenCalledWith( + expect.stringContaining("dify-client-node response 200 GET") + ); + consoleInfo.mockRestore(); + }); + + it("logs retry attempts when enableLogging is true", async () => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const consoleInfo = vi.spyOn(console, "info").mockImplementation(() => {}); + + const client = new HttpClient({ + apiKey: "test", + maxRetries: 1, + retryDelay: 0, + enableLogging: true, + }); + + mockRequest + .mockRejectedValueOnce({ + isAxiosError: true, + code: "ECONNABORTED", + message: "timeout", + }) + .mockResolvedValueOnce({ status: 200, data: "ok", headers: {} }); + + await client.requestRaw({ method: "GET", path: "/meta" }); + + expect(consoleInfo).toHaveBeenCalledWith( + expect.stringContaining("dify-client-node retry") + ); + consoleInfo.mockRestore(); + }); +}); diff --git a/sdks/nodejs-client/src/http/client.ts b/sdks/nodejs-client/src/http/client.ts new file mode 100644 index 0000000000..44b63c9903 --- /dev/null +++ b/sdks/nodejs-client/src/http/client.ts @@ -0,0 +1,368 @@ +import axios from "axios"; +import type { + AxiosError, + AxiosInstance, + AxiosRequestConfig, + AxiosResponse, +} from "axios"; +import type { Readable } from "node:stream"; +import { + DEFAULT_BASE_URL, + DEFAULT_MAX_RETRIES, + DEFAULT_RETRY_DELAY_SECONDS, + DEFAULT_TIMEOUT_SECONDS, +} from "../types/common"; +import type { + DifyClientConfig, + DifyResponse, + Headers, + QueryParams, + RequestMethod, +} from "../types/common"; +import type { DifyError } from "../errors/dify-error"; +import { + APIError, + AuthenticationError, + FileUploadError, + NetworkError, + RateLimitError, + TimeoutError, + ValidationError, +} from "../errors/dify-error"; +import { getFormDataHeaders, isFormData } from "./form-data"; +import { createBinaryStream, createSseStream } from "./sse"; +import { getRetryDelayMs, shouldRetry, sleep } from "./retry"; +import { validateParams } from "../client/validation"; + +const DEFAULT_USER_AGENT = "dify-client-node"; + +export type RequestOptions = { + method: RequestMethod; + path: string; + query?: QueryParams; + data?: unknown; + headers?: Headers; + responseType?: AxiosRequestConfig["responseType"]; +}; + +export type HttpClientSettings = Required< + Omit +> & { + apiKey: string; +}; + +const normalizeSettings = (config: DifyClientConfig): HttpClientSettings => ({ + apiKey: config.apiKey, + baseUrl: config.baseUrl ?? DEFAULT_BASE_URL, + timeout: config.timeout ?? DEFAULT_TIMEOUT_SECONDS, + maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES, + retryDelay: config.retryDelay ?? DEFAULT_RETRY_DELAY_SECONDS, + enableLogging: config.enableLogging ?? false, +}); + +const normalizeHeaders = (headers: AxiosResponse["headers"]): Headers => { + const result: Headers = {}; + if (!headers) { + return result; + } + Object.entries(headers).forEach(([key, value]) => { + if (Array.isArray(value)) { + result[key.toLowerCase()] = value.join(", "); + } else if (typeof value === "string") { + result[key.toLowerCase()] = value; + } else if (typeof value === "number") { + result[key.toLowerCase()] = value.toString(); + } + }); + return result; +}; + +const resolveRequestId = (headers: Headers): string | undefined => + headers["x-request-id"] ?? headers["x-requestid"]; + +const buildRequestUrl = (baseUrl: string, path: string): string => { + const trimmed = baseUrl.replace(/\/+$/, ""); + return `${trimmed}${path}`; +}; + +const buildQueryString = (params?: QueryParams): string => { + if (!params) { + return ""; + } + const searchParams = new URLSearchParams(); + Object.entries(params).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((item) => { + searchParams.append(key, String(item)); + }); + return; + } + searchParams.append(key, String(value)); + }); + return searchParams.toString(); +}; + +const parseRetryAfterSeconds = (headerValue?: string): number | undefined => { + if (!headerValue) { + return undefined; + } + const asNumber = Number.parseInt(headerValue, 10); + if (!Number.isNaN(asNumber)) { + return asNumber; + } + const asDate = Date.parse(headerValue); + if (!Number.isNaN(asDate)) { + const diff = asDate - Date.now(); + return diff > 0 ? Math.ceil(diff / 1000) : 0; + } + return undefined; +}; + +const isReadableStream = (value: unknown): value is Readable => { + if (!value || typeof value !== "object") { + return false; + } + return typeof (value as { pipe?: unknown }).pipe === "function"; +}; + +const isUploadLikeRequest = (config?: AxiosRequestConfig): boolean => { + const url = (config?.url ?? "").toLowerCase(); + if (!url) { + return false; + } + return ( + url.includes("upload") || + url.includes("/files/") || + url.includes("audio-to-text") || + url.includes("create_by_file") || + url.includes("update_by_file") + ); +}; + +const resolveErrorMessage = (status: number, responseBody: unknown): string => { + if (typeof responseBody === "string" && responseBody.trim().length > 0) { + return responseBody; + } + if ( + responseBody && + typeof responseBody === "object" && + "message" in responseBody + ) { + const message = (responseBody as Record).message; + if (typeof message === "string" && message.trim().length > 0) { + return message; + } + } + return `Request failed with status code ${status}`; +}; + +const mapAxiosError = (error: unknown): DifyError => { + if (axios.isAxiosError(error)) { + const axiosError = error as AxiosError; + if (axiosError.response) { + const status = axiosError.response.status; + const headers = normalizeHeaders(axiosError.response.headers); + const requestId = resolveRequestId(headers); + const responseBody = axiosError.response.data; + const message = resolveErrorMessage(status, responseBody); + + if (status === 401) { + return new AuthenticationError(message, { + statusCode: status, + responseBody, + requestId, + }); + } + if (status === 429) { + const retryAfter = parseRetryAfterSeconds(headers["retry-after"]); + return new RateLimitError(message, { + statusCode: status, + responseBody, + requestId, + retryAfter, + }); + } + if (status === 422) { + return new ValidationError(message, { + statusCode: status, + responseBody, + requestId, + }); + } + if (status === 400) { + if (isUploadLikeRequest(axiosError.config)) { + return new FileUploadError(message, { + statusCode: status, + responseBody, + requestId, + }); + } + } + return new APIError(message, { + statusCode: status, + responseBody, + requestId, + }); + } + if (axiosError.code === "ECONNABORTED") { + return new TimeoutError("Request timed out", { cause: axiosError }); + } + return new NetworkError(axiosError.message, { cause: axiosError }); + } + if (error instanceof Error) { + return new NetworkError(error.message, { cause: error }); + } + return new NetworkError("Unexpected network error", { cause: error }); +}; + +export class HttpClient { + private axios: AxiosInstance; + private settings: HttpClientSettings; + + constructor(config: DifyClientConfig) { + this.settings = normalizeSettings(config); + this.axios = axios.create({ + baseURL: this.settings.baseUrl, + timeout: this.settings.timeout * 1000, + }); + } + + updateApiKey(apiKey: string): void { + this.settings.apiKey = apiKey; + } + + getSettings(): HttpClientSettings { + return { ...this.settings }; + } + + async request(options: RequestOptions): Promise> { + const response = await this.requestRaw(options); + const headers = normalizeHeaders(response.headers); + return { + data: response.data as T, + status: response.status, + headers, + requestId: resolveRequestId(headers), + }; + } + + async requestStream(options: RequestOptions) { + const response = await this.requestRaw({ + ...options, + responseType: "stream", + }); + const headers = normalizeHeaders(response.headers); + return createSseStream(response.data as Readable, { + status: response.status, + headers, + requestId: resolveRequestId(headers), + }); + } + + async requestBinaryStream(options: RequestOptions) { + const response = await this.requestRaw({ + ...options, + responseType: "stream", + }); + const headers = normalizeHeaders(response.headers); + return createBinaryStream(response.data as Readable, { + status: response.status, + headers, + requestId: resolveRequestId(headers), + }); + } + + async requestRaw(options: RequestOptions): Promise { + const { method, path, query, data, headers, responseType } = options; + const { apiKey, enableLogging, maxRetries, retryDelay, timeout } = + this.settings; + + if (query) { + validateParams(query as Record); + } + if ( + data && + typeof data === "object" && + !Array.isArray(data) && + !isFormData(data) && + !isReadableStream(data) + ) { + validateParams(data as Record); + } + + const requestHeaders: Headers = { + Authorization: `Bearer ${apiKey}`, + ...headers, + }; + if ( + typeof process !== "undefined" && + !!process.versions?.node && + !requestHeaders["User-Agent"] && + !requestHeaders["user-agent"] + ) { + requestHeaders["User-Agent"] = DEFAULT_USER_AGENT; + } + + if (isFormData(data)) { + Object.assign(requestHeaders, getFormDataHeaders(data)); + } else if (data && method !== "GET") { + requestHeaders["Content-Type"] = "application/json"; + } + + const url = buildRequestUrl(this.settings.baseUrl, path); + + if (enableLogging) { + console.info(`dify-client-node request ${method} ${url}`); + } + + const axiosConfig: AxiosRequestConfig = { + method, + url: path, + params: query, + paramsSerializer: { + serialize: (params) => buildQueryString(params as QueryParams), + }, + headers: requestHeaders, + responseType: responseType ?? "json", + timeout: timeout * 1000, + }; + + if (method !== "GET" && data !== undefined) { + axiosConfig.data = data; + } + + let attempt = 0; + // `attempt` is a zero-based retry counter + // Total attempts = 1 (initial) + maxRetries + // e.g., maxRetries=3 means: attempt 0 (initial), then retries at 1, 2, 3 + while (true) { + try { + const response = await this.axios.request(axiosConfig); + if (enableLogging) { + console.info( + `dify-client-node response ${response.status} ${method} ${url}` + ); + } + return response; + } catch (error) { + const mapped = mapAxiosError(error); + if (!shouldRetry(mapped, attempt, maxRetries)) { + throw mapped; + } + const retryAfterSeconds = + mapped instanceof RateLimitError ? mapped.retryAfter : undefined; + const delay = getRetryDelayMs(attempt + 1, retryDelay, retryAfterSeconds); + if (enableLogging) { + console.info( + `dify-client-node retry ${attempt + 1} in ${delay}ms for ${method} ${url}` + ); + } + attempt += 1; + await sleep(delay); + } + } + } +} diff --git a/sdks/nodejs-client/src/http/form-data.test.js b/sdks/nodejs-client/src/http/form-data.test.js new file mode 100644 index 0000000000..2938e41435 --- /dev/null +++ b/sdks/nodejs-client/src/http/form-data.test.js @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { getFormDataHeaders, isFormData } from "./form-data"; + +describe("form-data helpers", () => { + it("detects form-data like objects", () => { + const formLike = { + append: () => {}, + getHeaders: () => ({ "content-type": "multipart/form-data" }), + }; + expect(isFormData(formLike)).toBe(true); + expect(isFormData({})).toBe(false); + }); + + it("returns headers from form-data", () => { + const formLike = { + append: () => {}, + getHeaders: () => ({ "content-type": "multipart/form-data" }), + }; + expect(getFormDataHeaders(formLike)).toEqual({ + "content-type": "multipart/form-data", + }); + }); +}); diff --git a/sdks/nodejs-client/src/http/form-data.ts b/sdks/nodejs-client/src/http/form-data.ts new file mode 100644 index 0000000000..2efa23e54e --- /dev/null +++ b/sdks/nodejs-client/src/http/form-data.ts @@ -0,0 +1,31 @@ +import type { Headers } from "../types/common"; + +export type FormDataLike = { + append: (...args: unknown[]) => void; + getHeaders?: () => Headers; + constructor?: { name?: string }; +}; + +export const isFormData = (value: unknown): value is FormDataLike => { + if (!value || typeof value !== "object") { + return false; + } + if (typeof FormData !== "undefined" && value instanceof FormData) { + return true; + } + const candidate = value as FormDataLike; + if (typeof candidate.append !== "function") { + return false; + } + if (typeof candidate.getHeaders === "function") { + return true; + } + return candidate.constructor?.name === "FormData"; +}; + +export const getFormDataHeaders = (form: FormDataLike): Headers => { + if (typeof form.getHeaders === "function") { + return form.getHeaders(); + } + return {}; +}; diff --git a/sdks/nodejs-client/src/http/retry.test.js b/sdks/nodejs-client/src/http/retry.test.js new file mode 100644 index 0000000000..fc017f631b --- /dev/null +++ b/sdks/nodejs-client/src/http/retry.test.js @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { getRetryDelayMs, shouldRetry } from "./retry"; +import { NetworkError, RateLimitError, TimeoutError } from "../errors/dify-error"; + +const withMockedRandom = (value, fn) => { + const original = Math.random; + Math.random = () => value; + try { + fn(); + } finally { + Math.random = original; + } +}; + +describe("retry helpers", () => { + it("getRetryDelayMs honors retry-after header", () => { + expect(getRetryDelayMs(1, 1, 3)).toBe(3000); + }); + + it("getRetryDelayMs uses exponential backoff with jitter", () => { + withMockedRandom(0, () => { + expect(getRetryDelayMs(1, 1)).toBe(1000); + expect(getRetryDelayMs(2, 1)).toBe(2000); + expect(getRetryDelayMs(3, 1)).toBe(4000); + }); + }); + + it("shouldRetry respects max retries", () => { + expect(shouldRetry(new TimeoutError("timeout"), 3, 3)).toBe(false); + }); + + it("shouldRetry retries on network, timeout, and rate limit", () => { + expect(shouldRetry(new TimeoutError("timeout"), 0, 3)).toBe(true); + expect(shouldRetry(new NetworkError("network"), 0, 3)).toBe(true); + expect(shouldRetry(new RateLimitError("limit"), 0, 3)).toBe(true); + expect(shouldRetry(new Error("other"), 0, 3)).toBe(false); + }); +}); diff --git a/sdks/nodejs-client/src/http/retry.ts b/sdks/nodejs-client/src/http/retry.ts new file mode 100644 index 0000000000..3776b78d5f --- /dev/null +++ b/sdks/nodejs-client/src/http/retry.ts @@ -0,0 +1,40 @@ +import { RateLimitError, NetworkError, TimeoutError } from "../errors/dify-error"; + +export const sleep = (ms: number): Promise => + new Promise((resolve) => { + setTimeout(resolve, ms); + }); + +export const getRetryDelayMs = ( + attempt: number, + retryDelaySeconds: number, + retryAfterSeconds?: number +): number => { + if (retryAfterSeconds && retryAfterSeconds > 0) { + return retryAfterSeconds * 1000; + } + const base = retryDelaySeconds * 1000; + const exponential = base * Math.pow(2, Math.max(0, attempt - 1)); + const jitter = Math.random() * base; + return exponential + jitter; +}; + +export const shouldRetry = ( + error: unknown, + attempt: number, + maxRetries: number +): boolean => { + if (attempt >= maxRetries) { + return false; + } + if (error instanceof TimeoutError) { + return true; + } + if (error instanceof NetworkError) { + return true; + } + if (error instanceof RateLimitError) { + return true; + } + return false; +}; diff --git a/sdks/nodejs-client/src/http/sse.test.js b/sdks/nodejs-client/src/http/sse.test.js new file mode 100644 index 0000000000..fff85fd29b --- /dev/null +++ b/sdks/nodejs-client/src/http/sse.test.js @@ -0,0 +1,76 @@ +import { Readable } from "node:stream"; +import { describe, expect, it } from "vitest"; +import { createBinaryStream, createSseStream, parseSseStream } from "./sse"; + +describe("sse parsing", () => { + it("parses event and data lines", async () => { + const stream = Readable.from([ + "event: message\n", + "data: {\"answer\":\"hi\"}\n", + "\n", + ]); + const events = []; + for await (const event of parseSseStream(stream)) { + events.push(event); + } + expect(events).toHaveLength(1); + expect(events[0].event).toBe("message"); + expect(events[0].data).toEqual({ answer: "hi" }); + }); + + it("handles multi-line data payloads", async () => { + const stream = Readable.from(["data: line1\n", "data: line2\n", "\n"]); + const events = []; + for await (const event of parseSseStream(stream)) { + events.push(event); + } + expect(events[0].raw).toBe("line1\nline2"); + expect(events[0].data).toBe("line1\nline2"); + }); + + it("createSseStream exposes toText", async () => { + const stream = Readable.from([ + "data: {\"answer\":\"hello\"}\n\n", + "data: {\"delta\":\" world\"}\n\n", + ]); + const sseStream = createSseStream(stream, { + status: 200, + headers: {}, + requestId: "req", + }); + const text = await sseStream.toText(); + expect(text).toBe("hello world"); + }); + + it("toText extracts text from string data", async () => { + const stream = Readable.from(["data: plain text\n\n"]); + const sseStream = createSseStream(stream, { status: 200, headers: {} }); + const text = await sseStream.toText(); + expect(text).toBe("plain text"); + }); + + it("toText extracts text field from object", async () => { + const stream = Readable.from(['data: {"text":"hello"}\n\n']); + const sseStream = createSseStream(stream, { status: 200, headers: {} }); + const text = await sseStream.toText(); + expect(text).toBe("hello"); + }); + + it("toText returns empty for invalid data", async () => { + const stream = Readable.from(["data: null\n\n", "data: 123\n\n"]); + const sseStream = createSseStream(stream, { status: 200, headers: {} }); + const text = await sseStream.toText(); + expect(text).toBe(""); + }); + + it("createBinaryStream exposes metadata", () => { + const stream = Readable.from(["chunk"]); + const binary = createBinaryStream(stream, { + status: 200, + headers: { "content-type": "audio/mpeg" }, + requestId: "req", + }); + expect(binary.status).toBe(200); + expect(binary.headers["content-type"]).toBe("audio/mpeg"); + }); +}); diff --git a/sdks/nodejs-client/src/http/sse.ts b/sdks/nodejs-client/src/http/sse.ts new file mode 100644 index 0000000000..ed5a17fe39 --- /dev/null +++ b/sdks/nodejs-client/src/http/sse.ts @@ -0,0 +1,133 @@ +import type { Readable } from "node:stream"; +import { StringDecoder } from "node:string_decoder"; +import type { BinaryStream, DifyStream, Headers, StreamEvent } from "../types/common"; + +const readLines = async function* (stream: Readable): AsyncIterable { + const decoder = new StringDecoder("utf8"); + let buffered = ""; + for await (const chunk of stream) { + buffered += decoder.write(chunk as Buffer); + let index = buffered.indexOf("\n"); + while (index >= 0) { + let line = buffered.slice(0, index); + buffered = buffered.slice(index + 1); + if (line.endsWith("\r")) { + line = line.slice(0, -1); + } + yield line; + index = buffered.indexOf("\n"); + } + } + buffered += decoder.end(); + if (buffered) { + yield buffered; + } +}; + +const parseMaybeJson = (value: string): unknown => { + if (!value) { + return null; + } + try { + return JSON.parse(value); + } catch { + return value; + } +}; + +export const parseSseStream = async function* ( + stream: Readable +): AsyncIterable> { + let eventName: string | undefined; + const dataLines: string[] = []; + + const emitEvent = function* (): Iterable> { + if (!eventName && dataLines.length === 0) { + return; + } + const raw = dataLines.join("\n"); + const parsed = parseMaybeJson(raw) as T | string | null; + yield { + event: eventName, + data: parsed, + raw, + }; + eventName = undefined; + dataLines.length = 0; + }; + + for await (const line of readLines(stream)) { + if (!line) { + yield* emitEvent(); + continue; + } + if (line.startsWith(":")) { + continue; + } + if (line.startsWith("event:")) { + eventName = line.slice("event:".length).trim(); + continue; + } + if (line.startsWith("data:")) { + dataLines.push(line.slice("data:".length).trimStart()); + continue; + } + } + + yield* emitEvent(); +}; + +const extractTextFromEvent = (data: unknown): string => { + if (typeof data === "string") { + return data; + } + if (!data || typeof data !== "object") { + return ""; + } + const record = data as Record; + if (typeof record.answer === "string") { + return record.answer; + } + if (typeof record.text === "string") { + return record.text; + } + if (typeof record.delta === "string") { + return record.delta; + } + return ""; +}; + +export const createSseStream = ( + stream: Readable, + meta: { status: number; headers: Headers; requestId?: string } +): DifyStream => { + const iterator = parseSseStream(stream)[Symbol.asyncIterator](); + const iterable = { + [Symbol.asyncIterator]: () => iterator, + data: stream, + status: meta.status, + headers: meta.headers, + requestId: meta.requestId, + toReadable: () => stream, + toText: async () => { + let text = ""; + for await (const event of iterable) { + text += extractTextFromEvent(event.data); + } + return text; + }, + } satisfies DifyStream; + + return iterable; +}; + +export const createBinaryStream = ( + stream: Readable, + meta: { status: number; headers: Headers; requestId?: string } +): BinaryStream => ({ + data: stream, + status: meta.status, + headers: meta.headers, + requestId: meta.requestId, + toReadable: () => stream, +}); diff --git a/sdks/nodejs-client/src/index.test.js b/sdks/nodejs-client/src/index.test.js new file mode 100644 index 0000000000..289f4d9b1b --- /dev/null +++ b/sdks/nodejs-client/src/index.test.js @@ -0,0 +1,227 @@ +import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { ChatClient, DifyClient, WorkflowClient, BASE_URL, routes } from "./index"; +import axios from "axios"; + +const mockRequest = vi.fn(); + +const setupAxiosMock = () => { + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); +}; + +beforeEach(() => { + vi.restoreAllMocks(); + mockRequest.mockReset(); + setupAxiosMock(); +}); + +describe("Client", () => { + it("should create a client", () => { + new DifyClient("test"); + + expect(axios.create).toHaveBeenCalledWith({ + baseURL: BASE_URL, + timeout: 60000, + }); + }); + + it("should update the api key", () => { + const difyClient = new DifyClient("test"); + difyClient.updateApiKey("test2"); + + expect(difyClient.getHttpClient().getSettings().apiKey).toBe("test2"); + }); +}); + +describe("Send Requests", () => { + it("should make a successful request to the application parameter", async () => { + const difyClient = new DifyClient("test"); + const method = "GET"; + const endpoint = routes.application.url(); + mockRequest.mockResolvedValue({ + status: 200, + data: "response", + headers: {}, + }); + + await difyClient.sendRequest(method, endpoint); + + const requestConfig = mockRequest.mock.calls[0][0]; + expect(requestConfig).toMatchObject({ + method, + url: endpoint, + params: undefined, + responseType: "json", + timeout: 60000, + }); + expect(requestConfig.headers.Authorization).toBe("Bearer test"); + }); + + it("uses the getMeta route configuration", async () => { + const difyClient = new DifyClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await difyClient.getMeta("end-user"); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: routes.getMeta.method, + url: routes.getMeta.url(), + params: { user: "end-user" }, + headers: expect.objectContaining({ + Authorization: "Bearer test", + }), + responseType: "json", + timeout: 60000, + })); + }); +}); + +describe("File uploads", () => { + const OriginalFormData = globalThis.FormData; + + beforeAll(() => { + globalThis.FormData = class FormDataMock { + append() {} + + getHeaders() { + return { + "content-type": "multipart/form-data; boundary=test", + }; + } + }; + }); + + afterAll(() => { + globalThis.FormData = OriginalFormData; + }); + + it("does not override multipart boundary headers for FormData", async () => { + const difyClient = new DifyClient("test"); + const form = new globalThis.FormData(); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await difyClient.fileUpload(form, "end-user"); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: routes.fileUpload.method, + url: routes.fileUpload.url(), + params: undefined, + headers: expect.objectContaining({ + Authorization: "Bearer test", + "content-type": "multipart/form-data; boundary=test", + }), + responseType: "json", + timeout: 60000, + data: form, + })); + }); +}); + +describe("Workflow client", () => { + it("uses tasks stop path for workflow stop", async () => { + const workflowClient = new WorkflowClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "stopped", headers: {} }); + + await workflowClient.stop("task-1", "end-user"); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: routes.stopWorkflow.method, + url: routes.stopWorkflow.url("task-1"), + params: undefined, + headers: expect.objectContaining({ + Authorization: "Bearer test", + "Content-Type": "application/json", + }), + responseType: "json", + timeout: 60000, + data: { user: "end-user" }, + })); + }); + + it("maps workflow log filters to service api params", async () => { + const workflowClient = new WorkflowClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await workflowClient.getLogs({ + createdAtAfter: "2024-01-01T00:00:00Z", + createdAtBefore: "2024-01-02T00:00:00Z", + createdByEndUserSessionId: "sess-1", + createdByAccount: "acc-1", + page: 2, + limit: 10, + }); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: "GET", + url: "/workflows/logs", + params: { + created_at__after: "2024-01-01T00:00:00Z", + created_at__before: "2024-01-02T00:00:00Z", + created_by_end_user_session_id: "sess-1", + created_by_account: "acc-1", + page: 2, + limit: 10, + }, + headers: expect.objectContaining({ + Authorization: "Bearer test", + }), + responseType: "json", + timeout: 60000, + })); + }); +}); + +describe("Chat client", () => { + it("places user in query for suggested messages", async () => { + const chatClient = new ChatClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await chatClient.getSuggested("msg-1", "end-user"); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: routes.getSuggested.method, + url: routes.getSuggested.url("msg-1"), + params: { user: "end-user" }, + headers: expect.objectContaining({ + Authorization: "Bearer test", + }), + responseType: "json", + timeout: 60000, + })); + }); + + it("uses last_id when listing conversations", async () => { + const chatClient = new ChatClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await chatClient.getConversations("end-user", "last-1", 10); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: routes.getConversations.method, + url: routes.getConversations.url(), + params: { user: "end-user", last_id: "last-1", limit: 10 }, + headers: expect.objectContaining({ + Authorization: "Bearer test", + }), + responseType: "json", + timeout: 60000, + })); + }); + + it("lists app feedbacks without user params", async () => { + const chatClient = new ChatClient("test"); + mockRequest.mockResolvedValue({ status: 200, data: "ok", headers: {} }); + + await chatClient.getAppFeedbacks(1, 20); + + expect(mockRequest).toHaveBeenCalledWith(expect.objectContaining({ + method: "GET", + url: "/app/feedbacks", + params: { page: 1, limit: 20 }, + headers: expect.objectContaining({ + Authorization: "Bearer test", + }), + responseType: "json", + timeout: 60000, + })); + }); +}); diff --git a/sdks/nodejs-client/src/index.ts b/sdks/nodejs-client/src/index.ts new file mode 100644 index 0000000000..3ba3757baa --- /dev/null +++ b/sdks/nodejs-client/src/index.ts @@ -0,0 +1,103 @@ +import { DEFAULT_BASE_URL } from "./types/common"; + +export const BASE_URL = DEFAULT_BASE_URL; + +export const routes = { + feedback: { + method: "POST", + url: (messageId: string) => `/messages/${messageId}/feedbacks`, + }, + application: { + method: "GET", + url: () => "/parameters", + }, + fileUpload: { + method: "POST", + url: () => "/files/upload", + }, + filePreview: { + method: "GET", + url: (fileId: string) => `/files/${fileId}/preview`, + }, + textToAudio: { + method: "POST", + url: () => "/text-to-audio", + }, + audioToText: { + method: "POST", + url: () => "/audio-to-text", + }, + getMeta: { + method: "GET", + url: () => "/meta", + }, + getInfo: { + method: "GET", + url: () => "/info", + }, + getSite: { + method: "GET", + url: () => "/site", + }, + createCompletionMessage: { + method: "POST", + url: () => "/completion-messages", + }, + stopCompletionMessage: { + method: "POST", + url: (taskId: string) => `/completion-messages/${taskId}/stop`, + }, + createChatMessage: { + method: "POST", + url: () => "/chat-messages", + }, + getSuggested: { + method: "GET", + url: (messageId: string) => `/messages/${messageId}/suggested`, + }, + stopChatMessage: { + method: "POST", + url: (taskId: string) => `/chat-messages/${taskId}/stop`, + }, + getConversations: { + method: "GET", + url: () => "/conversations", + }, + getConversationMessages: { + method: "GET", + url: () => "/messages", + }, + renameConversation: { + method: "POST", + url: (conversationId: string) => `/conversations/${conversationId}/name`, + }, + deleteConversation: { + method: "DELETE", + url: (conversationId: string) => `/conversations/${conversationId}`, + }, + runWorkflow: { + method: "POST", + url: () => "/workflows/run", + }, + stopWorkflow: { + method: "POST", + url: (taskId: string) => `/workflows/tasks/${taskId}/stop`, + }, +}; + +export { DifyClient } from "./client/base"; +export { ChatClient } from "./client/chat"; +export { CompletionClient } from "./client/completion"; +export { WorkflowClient } from "./client/workflow"; +export { KnowledgeBaseClient } from "./client/knowledge-base"; +export { WorkspaceClient } from "./client/workspace"; + +export * from "./errors/dify-error"; +export * from "./types/common"; +export * from "./types/annotation"; +export * from "./types/chat"; +export * from "./types/completion"; +export * from "./types/knowledge-base"; +export * from "./types/workflow"; +export * from "./types/workspace"; +export { HttpClient } from "./http/client"; diff --git a/sdks/nodejs-client/src/types/annotation.ts b/sdks/nodejs-client/src/types/annotation.ts new file mode 100644 index 0000000000..dcbd644dab --- /dev/null +++ b/sdks/nodejs-client/src/types/annotation.ts @@ -0,0 +1,18 @@ +export type AnnotationCreateRequest = { + question: string; + answer: string; +}; + +export type AnnotationReplyActionRequest = { + score_threshold: number; + embedding_provider_name: string; + embedding_model_name: string; +}; + +export type AnnotationListOptions = { + page?: number; + limit?: number; + keyword?: string; +}; + +export type AnnotationResponse = Record; diff --git a/sdks/nodejs-client/src/types/chat.ts b/sdks/nodejs-client/src/types/chat.ts new file mode 100644 index 0000000000..5b627f6cf6 --- /dev/null +++ b/sdks/nodejs-client/src/types/chat.ts @@ -0,0 +1,17 @@ +import type { StreamEvent } from "./common"; + +export type ChatMessageRequest = { + inputs?: Record; + query: string; + user: string; + response_mode?: "blocking" | "streaming"; + files?: Array> | null; + conversation_id?: string; + auto_generate_name?: boolean; + workflow_id?: string; + retriever_from?: "app" | "dataset"; +}; + +export type ChatMessageResponse = Record; + +export type ChatStreamEvent = StreamEvent>; diff --git a/sdks/nodejs-client/src/types/common.ts b/sdks/nodejs-client/src/types/common.ts new file mode 100644 index 0000000000..00b0fcc756 --- /dev/null +++ b/sdks/nodejs-client/src/types/common.ts @@ -0,0 +1,71 @@ +export const DEFAULT_BASE_URL = "https://api.dify.ai/v1"; +export const DEFAULT_TIMEOUT_SECONDS = 60; +export const DEFAULT_MAX_RETRIES = 3; +export const DEFAULT_RETRY_DELAY_SECONDS = 1; + +export type RequestMethod = "GET" | "POST" | "PATCH" | "PUT" | "DELETE"; + +export type QueryParamValue = + | string + | number + | boolean + | Array + | undefined; + +export type QueryParams = Record; + +export type Headers = Record; + +export type DifyClientConfig = { + apiKey: string; + baseUrl?: string; + timeout?: number; + maxRetries?: number; + retryDelay?: number; + enableLogging?: boolean; +}; + +export type DifyResponse = { + data: T; + status: number; + headers: Headers; + requestId?: string; +}; + +export type MessageFeedbackRequest = { + messageId: string; + user: string; + rating?: "like" | "dislike" | null; + content?: string | null; +}; + +export type TextToAudioRequest = { + user: string; + text?: string; + message_id?: string; + streaming?: boolean; + voice?: string; +}; + +export type StreamEvent = { + event?: string; + data: T | string | null; + raw: string; +}; + +export type DifyStream = AsyncIterable> & { + data: NodeJS.ReadableStream; + status: number; + headers: Headers; + requestId?: string; + toText(): Promise; + toReadable(): NodeJS.ReadableStream; +}; + +export type BinaryStream = { + data: NodeJS.ReadableStream; + status: number; + headers: Headers; + requestId?: string; + toReadable(): NodeJS.ReadableStream; +}; diff --git a/sdks/nodejs-client/src/types/completion.ts b/sdks/nodejs-client/src/types/completion.ts new file mode 100644 index 0000000000..4074137c5d --- /dev/null +++ b/sdks/nodejs-client/src/types/completion.ts @@ -0,0 +1,13 @@ +import type { StreamEvent } from "./common"; + +export type CompletionRequest = { + inputs?: Record; + response_mode?: "blocking" | "streaming"; + user: string; + files?: Array> | null; + retriever_from?: "app" | "dataset"; +}; + +export type CompletionResponse = Record; + +export type CompletionStreamEvent = StreamEvent>; diff --git a/sdks/nodejs-client/src/types/knowledge-base.ts b/sdks/nodejs-client/src/types/knowledge-base.ts new file mode 100644 index 0000000000..a4ddef50ea --- /dev/null +++ b/sdks/nodejs-client/src/types/knowledge-base.ts @@ -0,0 +1,184 @@ +export type DatasetListOptions = { + page?: number; + limit?: number; + keyword?: string | null; + tagIds?: string[]; + includeAll?: boolean; +}; + +export type DatasetCreateRequest = { + name: string; + description?: string; + indexing_technique?: "high_quality" | "economy"; + permission?: string | null; + external_knowledge_api_id?: string | null; + provider?: string; + external_knowledge_id?: string | null; + retrieval_model?: Record | null; + embedding_model?: string | null; + embedding_model_provider?: string | null; +}; + +export type DatasetUpdateRequest = { + name?: string; + description?: string | null; + indexing_technique?: "high_quality" | "economy" | null; + permission?: string | null; + embedding_model?: string | null; + embedding_model_provider?: string | null; + retrieval_model?: Record | null; + partial_member_list?: Array> | null; + external_retrieval_model?: Record | null; + external_knowledge_id?: string | null; + external_knowledge_api_id?: string | null; +}; + +export type DocumentStatusAction = "enable" | "disable" | "archive" | "un_archive"; + +export type DatasetTagCreateRequest = { + name: string; +}; + +export type DatasetTagUpdateRequest = { + tag_id: string; + name: string; +}; + +export type DatasetTagDeleteRequest = { + tag_id: string; +}; + +export type DatasetTagBindingRequest = { + tag_ids: string[]; + target_id: string; +}; + +export type DatasetTagUnbindingRequest = { + tag_id: string; + target_id: string; +}; + +export type DocumentTextCreateRequest = { + name: string; + text: string; + process_rule?: Record | null; + original_document_id?: string | null; + doc_form?: string; + doc_language?: string; + indexing_technique?: string | null; + retrieval_model?: Record | null; + embedding_model?: string | null; + embedding_model_provider?: string | null; +}; + +export type DocumentTextUpdateRequest = { + name?: string | null; + text?: string | null; + process_rule?: Record | null; + doc_form?: string; + doc_language?: string; + retrieval_model?: Record | null; +}; + +export type DocumentListOptions = { + page?: number; + limit?: number; + keyword?: string | null; + status?: string | null; +}; + +export type DocumentGetOptions = { + metadata?: "all" | "only" | "without"; +}; + +export type SegmentCreateRequest = { + segments: Array>; +}; + +export type SegmentUpdateRequest = { + segment: { + content?: string | null; + answer?: string | null; + keywords?: string[] | null; + regenerate_child_chunks?: boolean; + enabled?: boolean | null; + attachment_ids?: string[] | null; + }; +}; + +export type SegmentListOptions = { + page?: number; + limit?: number; + status?: string[]; + keyword?: string | null; +}; + +export type ChildChunkCreateRequest = { + content: string; +}; + +export type ChildChunkUpdateRequest = { + content: string; +}; + +export type ChildChunkListOptions = { + page?: number; + limit?: number; + keyword?: string | null; +}; + +export type MetadataCreateRequest = { + type: "string" | "number" | "time"; + name: string; +}; + +export type MetadataUpdateRequest = { + name: string; + value?: string | number | null; +}; + +export type DocumentMetadataDetail = { + id: string; + name: string; + value?: string | number | null; +}; + +export type DocumentMetadataOperation = { + document_id: string; + metadata_list: DocumentMetadataDetail[]; + partial_update?: boolean; +}; + +export type MetadataOperationRequest = { + operation_data: DocumentMetadataOperation[]; +}; + +export type HitTestingRequest = { + query?: string | null; + retrieval_model?: Record | null; + external_retrieval_model?: Record | null; + attachment_ids?: string[] | null; +}; + +export type DatasourcePluginListOptions = { + isPublished?: boolean; +}; + +export type DatasourceNodeRunRequest = { + inputs: Record; + datasource_type: string; + credential_id?: string | null; + is_published: boolean; +}; + +export type PipelineRunRequest = { + inputs: Record; + datasource_type: string; + datasource_info_list: Array>; + start_node_id: string; + is_published: boolean; + response_mode: "streaming" | "blocking"; +}; + +export type KnowledgeBaseResponse = Record; +export type PipelineStreamEvent = Record; diff --git a/sdks/nodejs-client/src/types/workflow.ts b/sdks/nodejs-client/src/types/workflow.ts new file mode 100644 index 0000000000..2b507c7352 --- /dev/null +++ b/sdks/nodejs-client/src/types/workflow.ts @@ -0,0 +1,12 @@ +import type { StreamEvent } from "./common"; + +export type WorkflowRunRequest = { + inputs?: Record; + user: string; + response_mode?: "blocking" | "streaming"; + files?: Array> | null; +}; + +export type WorkflowRunResponse = Record; + +export type WorkflowStreamEvent = StreamEvent>; diff --git a/sdks/nodejs-client/src/types/workspace.ts b/sdks/nodejs-client/src/types/workspace.ts new file mode 100644 index 0000000000..0ab6743063 --- /dev/null +++ b/sdks/nodejs-client/src/types/workspace.ts @@ -0,0 +1,2 @@ +export type WorkspaceModelType = string; +export type WorkspaceModelsResponse = Record; diff --git a/sdks/nodejs-client/tests/test-utils.js b/sdks/nodejs-client/tests/test-utils.js new file mode 100644 index 0000000000..0d42514e9a --- /dev/null +++ b/sdks/nodejs-client/tests/test-utils.js @@ -0,0 +1,30 @@ +import axios from "axios"; +import { vi } from "vitest"; +import { HttpClient } from "../src/http/client"; + +export const createHttpClient = (configOverrides = {}) => { + const mockRequest = vi.fn(); + vi.spyOn(axios, "create").mockReturnValue({ request: mockRequest }); + const client = new HttpClient({ apiKey: "test", ...configOverrides }); + return { client, mockRequest }; +}; + +export const createHttpClientWithSpies = (configOverrides = {}) => { + const { client, mockRequest } = createHttpClient(configOverrides); + const request = vi + .spyOn(client, "request") + .mockResolvedValue({ data: "ok", status: 200, headers: {} }); + const requestStream = vi + .spyOn(client, "requestStream") + .mockResolvedValue({ data: null }); + const requestBinaryStream = vi + .spyOn(client, "requestBinaryStream") + .mockResolvedValue({ data: null }); + return { + client, + mockRequest, + request, + requestStream, + requestBinaryStream, + }; +}; diff --git a/sdks/nodejs-client/tsconfig.json b/sdks/nodejs-client/tsconfig.json new file mode 100644 index 0000000000..d2da9a2a59 --- /dev/null +++ b/sdks/nodejs-client/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "Bundler", + "rootDir": "src", + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src/**/*.ts"] +} diff --git a/sdks/nodejs-client/tsup.config.ts b/sdks/nodejs-client/tsup.config.ts new file mode 100644 index 0000000000..522382c2a5 --- /dev/null +++ b/sdks/nodejs-client/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["esm"], + dts: true, + clean: true, + sourcemap: true, + splitting: false, + treeshake: true, + outDir: "dist", +}); diff --git a/sdks/nodejs-client/vitest.config.ts b/sdks/nodejs-client/vitest.config.ts new file mode 100644 index 0000000000..5a0a8637a2 --- /dev/null +++ b/sdks/nodejs-client/vitest.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["**/*.test.js"], + coverage: { + provider: "v8", + reporter: ["text", "text-summary"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.test.*", "src/**/*.spec.*"], + }, + }, +});