diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ed3215ab..85513415 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,5 +1,5 @@ { - ".": "0.94.0", + ".": "0.95.0", "packages/vertex-sdk": "0.16.0", "packages/bedrock-sdk": "0.29.1", "packages/foundry-sdk": "0.2.3", diff --git a/.stats.yml b/.stats.yml index 7027e4e5..5a84c2e7 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 91 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-ad9228826393d94e86ecf4c22853ae51b1d4094960c836238b3ab79a1044be32.yml -openapi_spec_hash: dc43ed54947d427a084a891b7c4a783a -config_hash: bbf09e23cb2e12b5bb8cbcee3044ceec +configured_endpoints: 97 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/anthropic/anthropic-0df2c793ea4c3ad955e8e488be39d7041a0a95e2fe144dd69ae4d9fb72835190.yml +openapi_spec_hash: b169b786bdf1f07d7f77f18f7b94abfa +config_hash: ed43b84afda7441f472a59dda6badb05 diff --git a/CHANGELOG.md b/CHANGELOG.md index c885766a..51a2a955 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## 0.95.0 (2026-05-06) + +Full Changelog: [sdk-v0.94.0...sdk-v0.95.0](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.94.0...sdk-v0.95.0) + +### Features + +* **api:** add support for Managed Agents multiagents and outcomes, webhooks, vault validation ([e0c0e9b](https://github.com/anthropics/anthropic-sdk-typescript/commit/e0c0e9bef90b1919a5b806eb252e028981841e68)) + + +### Bug Fixes + +* **api:** Adjust webhook configuration ([deed3f6](https://github.com/anthropics/anthropic-sdk-typescript/commit/deed3f6290c0728dfc688e19117e1c01efb81a52)) + ## 0.94.0 (2026-05-05) Full Changelog: [sdk-v0.93.0...sdk-v0.94.0](https://github.com/anthropics/anthropic-sdk-typescript/compare/sdk-v0.93.0...sdk-v0.94.0) diff --git a/MIGRATION.md b/MIGRATION.md index 21a0ed42..1aa57cdd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -57,10 +57,15 @@ This affects the following methods: - `client.beta.sessions.resources.retrieve()` - `client.beta.sessions.resources.update()` - `client.beta.sessions.resources.delete()` +- `client.beta.sessions.threads.retrieve()` +- `client.beta.sessions.threads.archive()` +- `client.beta.sessions.threads.events.list()` +- `client.beta.sessions.threads.events.stream()` - `client.beta.vaults.credentials.retrieve()` - `client.beta.vaults.credentials.update()` - `client.beta.vaults.credentials.delete()` - `client.beta.vaults.credentials.archive()` +- `client.beta.vaults.credentials.mcpOAuthValidate()` - `client.beta.memoryStores.memories.retrieve()` - `client.beta.memoryStores.memories.update()` - `client.beta.memoryStores.memories.delete()` @@ -125,6 +130,7 @@ client.example.list(undefined, { headers: { ... } }); - `client.beta.sessions.events.list()` - `client.beta.sessions.events.stream()` - `client.beta.sessions.resources.list()` +- `client.beta.sessions.threads.list()` - `client.beta.vaults.retrieve()` - `client.beta.vaults.list()` - `client.beta.vaults.delete()` diff --git a/api.md b/api.md index 8dce22e4..0d2b0728 100644 --- a/api.md +++ b/api.md @@ -518,6 +518,7 @@ Methods: Types: - BetaManagedAgentsAgent +- BetaManagedAgentsAgentReference - BetaManagedAgentsAgentToolConfig - BetaManagedAgentsAgentToolConfigParams - BetaManagedAgentsAgentToolsetDefaultConfig @@ -543,6 +544,9 @@ Types: - BetaManagedAgentsModel - BetaManagedAgentsModelConfig - BetaManagedAgentsModelConfigParams +- BetaManagedAgentsMultiagentCoordinator +- BetaManagedAgentsMultiagentCoordinatorParams +- BetaManagedAgentsMultiagentSelfParams - BetaManagedAgentsSkillParams - BetaManagedAgentsURLMCPServerParams @@ -595,8 +599,13 @@ Types: - BetaManagedAgentsFileResourceParams - BetaManagedAgentsGitHubRepositoryResourceParams - BetaManagedAgentsMemoryStoreResourceParam +- BetaManagedAgentsMultiagent +- BetaManagedAgentsMultiagentParams +- BetaManagedAgentsMultiagentRosterEntryParams +- BetaManagedAgentsOutcomeEvaluationResource - BetaManagedAgentsSession - BetaManagedAgentsSessionAgent +- BetaManagedAgentsSessionMultiagentCoordinator - BetaManagedAgentsSessionStats - BetaManagedAgentsSessionUsage @@ -619,6 +628,8 @@ Types: - BetaManagedAgentsAgentMessageEvent - BetaManagedAgentsAgentThinkingEvent - BetaManagedAgentsAgentThreadContextCompactedEvent +- BetaManagedAgentsAgentThreadMessageReceivedEvent +- BetaManagedAgentsAgentThreadMessageSentEvent - BetaManagedAgentsAgentToolResultEvent - BetaManagedAgentsAgentToolUseEvent - BetaManagedAgentsBase64DocumentSource @@ -628,6 +639,8 @@ Types: - BetaManagedAgentsEventParams - BetaManagedAgentsFileDocumentSource - BetaManagedAgentsFileImageSource +- BetaManagedAgentsFileRubric +- BetaManagedAgentsFileRubricParams - BetaManagedAgentsImageBlock - BetaManagedAgentsMCPAuthenticationFailedError - BetaManagedAgentsMCPConnectionFailedError @@ -649,16 +662,28 @@ Types: - BetaManagedAgentsSessionStatusRescheduledEvent - BetaManagedAgentsSessionStatusRunningEvent - BetaManagedAgentsSessionStatusTerminatedEvent +- BetaManagedAgentsSessionThreadCreatedEvent +- BetaManagedAgentsSessionThreadStatusIdleEvent +- BetaManagedAgentsSessionThreadStatusRescheduledEvent +- BetaManagedAgentsSessionThreadStatusRunningEvent +- BetaManagedAgentsSessionThreadStatusTerminatedEvent - BetaManagedAgentsSpanModelRequestEndEvent - BetaManagedAgentsSpanModelRequestStartEvent - BetaManagedAgentsSpanModelUsage +- BetaManagedAgentsSpanOutcomeEvaluationEndEvent +- BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent +- BetaManagedAgentsSpanOutcomeEvaluationStartEvent - BetaManagedAgentsStreamSessionEvents - BetaManagedAgentsTextBlock +- BetaManagedAgentsTextRubric +- BetaManagedAgentsTextRubricParams - BetaManagedAgentsUnknownError - BetaManagedAgentsURLDocumentSource - BetaManagedAgentsURLImageSource - BetaManagedAgentsUserCustomToolResultEvent - BetaManagedAgentsUserCustomToolResultEventParams +- BetaManagedAgentsUserDefineOutcomeEvent +- BetaManagedAgentsUserDefineOutcomeEventParams - BetaManagedAgentsUserInterruptEvent - BetaManagedAgentsUserInterruptEventParams - BetaManagedAgentsUserMessageEvent @@ -692,6 +717,30 @@ Methods: - client.beta.sessions.resources.delete(resourceID, { ...params }) -> BetaManagedAgentsDeleteSessionResource - client.beta.sessions.resources.add(sessionID, { ...params }) -> BetaManagedAgentsFileResource +### Threads + +Types: + +- BetaManagedAgentsSessionThread +- BetaManagedAgentsSessionThreadAgent +- BetaManagedAgentsSessionThreadStats +- BetaManagedAgentsSessionThreadStatus +- BetaManagedAgentsSessionThreadUsage +- BetaManagedAgentsStreamSessionThreadEvents + +Methods: + +- client.beta.sessions.threads.retrieve(threadID, { ...params }) -> BetaManagedAgentsSessionThread +- client.beta.sessions.threads.list(sessionID, { ...params }) -> BetaManagedAgentsSessionThreadsPageCursor +- client.beta.sessions.threads.archive(threadID, { ...params }) -> BetaManagedAgentsSessionThread + +#### Events + +Methods: + +- client.beta.sessions.threads.events.list(threadID, { ...params }) -> BetaManagedAgentsSessionEventsPageCursor +- client.beta.sessions.threads.events.stream(threadID, { ...params }) -> BetaManagedAgentsStreamSessionThreadEvents + ## Vaults Types: @@ -713,6 +762,8 @@ Methods: Types: - BetaManagedAgentsCredential +- BetaManagedAgentsCredentialValidation +- BetaManagedAgentsCredentialValidationStatus - BetaManagedAgentsDeletedCredential - BetaManagedAgentsMCPOAuthAuthResponse - BetaManagedAgentsMCPOAuthCreateParams @@ -720,6 +771,9 @@ Types: - BetaManagedAgentsMCPOAuthRefreshResponse - BetaManagedAgentsMCPOAuthRefreshUpdateParams - BetaManagedAgentsMCPOAuthUpdateParams +- BetaManagedAgentsMCPProbe +- BetaManagedAgentsRefreshHTTPResponse +- BetaManagedAgentsRefreshObject - BetaManagedAgentsStaticBearerAuthResponse - BetaManagedAgentsStaticBearerCreateParams - BetaManagedAgentsStaticBearerUpdateParams @@ -740,6 +794,7 @@ Methods: - client.beta.vaults.credentials.list(vaultID, { ...params }) -> BetaManagedAgentsCredentialsPageCursor - client.beta.vaults.credentials.delete(credentialID, { ...params }) -> BetaManagedAgentsDeletedCredential - client.beta.vaults.credentials.archive(credentialID, { ...params }) -> BetaManagedAgentsCredential +- client.beta.vaults.credentials.mcpOAuthValidate(credentialID, { ...params }) -> BetaManagedAgentsCredentialValidation ## MemoryStores @@ -846,6 +901,40 @@ Methods: - client.beta.skills.versions.list(skillID, { ...params }) -> VersionListResponsesPageCursor - client.beta.skills.versions.delete(version, { ...params }) -> VersionDeleteResponse +## Webhooks + +Types: + +- BetaWebhookEvent +- BetaWebhookEventData +- BetaWebhookSessionArchivedEventData +- BetaWebhookSessionCreatedEventData +- BetaWebhookSessionDeletedEventData +- BetaWebhookSessionIdledEventData +- BetaWebhookSessionOutcomeEvaluationEndedEventData +- BetaWebhookSessionPendingEventData +- BetaWebhookSessionRequiresActionEventData +- BetaWebhookSessionRunningEventData +- BetaWebhookSessionStatusIdledEventData +- BetaWebhookSessionStatusRescheduledEventData +- BetaWebhookSessionStatusRunStartedEventData +- BetaWebhookSessionStatusTerminatedEventData +- BetaWebhookSessionThreadCreatedEventData +- BetaWebhookSessionThreadIdledEventData +- BetaWebhookSessionThreadTerminatedEventData +- BetaWebhookVaultArchivedEventData +- BetaWebhookVaultCreatedEventData +- BetaWebhookVaultCredentialArchivedEventData +- BetaWebhookVaultCredentialCreatedEventData +- BetaWebhookVaultCredentialDeletedEventData +- BetaWebhookVaultCredentialRefreshFailedEventData +- BetaWebhookVaultDeletedEventData +- UnwrapWebhookEvent + +Methods: + +- client.beta.webhooks.unwrap(body) -> void + ## UserProfiles Types: diff --git a/bin/migration-config.json b/bin/migration-config.json index f577891b..9d443b46 100644 --- a/bin/migration-config.json +++ b/bin/migration-config.json @@ -115,6 +115,154 @@ } ] }, + { + "base": "beta.sessions.threads", + "name": "retrieve", + "params": [ + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "session_id", + "location": "path" + }, + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": true + }, + { + "type": "options" + } + ] + }, + { + "base": "beta.sessions.threads", + "name": "archive", + "params": [ + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "session_id", + "location": "path" + }, + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": true + }, + { + "type": "options" + } + ] + }, + { + "base": "beta.sessions.threads.events", + "name": "list", + "params": [ + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "session_id", + "location": "path" + }, + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": true + }, + { + "type": "options" + } + ] + }, + { + "base": "beta.sessions.threads.events", + "name": "stream", + "params": [ + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "session_id", + "location": "path" + }, + { + "type": "param", + "key": "thread_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": true + }, + { + "type": "options" + } + ] + }, { "base": "beta.vaults.credentials", "name": "retrieve", @@ -263,6 +411,43 @@ } ] }, + { + "base": "beta.vaults.credentials", + "name": "mcpOAuthValidate", + "params": [ + { + "type": "param", + "key": "credential_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": false + }, + { + "type": "options" + } + ], + "oldParams": [ + { + "type": "param", + "key": "vault_id", + "location": "path" + }, + { + "type": "param", + "key": "credential_id", + "location": "path" + }, + { + "type": "params", + "maybeOverload": true + }, + { + "type": "options" + } + ] + }, { "base": "beta.memoryStores.memories", "name": "retrieve", diff --git a/package.json b/package.json index 38414771..ee520965 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@anthropic-ai/sdk", - "version": "0.94.0", + "version": "0.95.0", "description": "The official TypeScript library for the Anthropic API", "author": "Anthropic ", "types": "dist/index.d.ts", @@ -27,6 +27,7 @@ "fix": "./scripts/format" }, "dependencies": { + "standardwebhooks": "^1.0.0", "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { diff --git a/packages/vertex-sdk/yarn.lock b/packages/vertex-sdk/yarn.lock index 9e054594..29b19730 100644 --- a/packages/vertex-sdk/yarn.lock +++ b/packages/vertex-sdk/yarn.lock @@ -17,7 +17,7 @@ "@anthropic-ai/sdk@file:../../dist": # x-release-please-start-version - version "0.94.0" + version "0.95.0" # x-release-please-end-version dependencies: json-schema-to-ts "^3.1.1" diff --git a/scripts/detect-breaking-changes b/scripts/detect-breaking-changes index f2c8c80c..c13daedd 100755 --- a/scripts/detect-breaking-changes +++ b/scripts/detect-breaking-changes @@ -21,6 +21,8 @@ TEST_PATHS=( tests/api-resources/beta/sessions/sessions.test.ts tests/api-resources/beta/sessions/events.test.ts tests/api-resources/beta/sessions/resources.test.ts + tests/api-resources/beta/sessions/threads/threads.test.ts + tests/api-resources/beta/sessions/threads/events.test.ts tests/api-resources/beta/vaults/vaults.test.ts tests/api-resources/beta/vaults/credentials.test.ts tests/api-resources/beta/memory-stores/memory-stores.test.ts @@ -29,6 +31,7 @@ TEST_PATHS=( tests/api-resources/beta/files.test.ts tests/api-resources/beta/skills/skills.test.ts tests/api-resources/beta/skills/versions.test.ts + tests/api-resources/beta/webhooks.test.ts tests/api-resources/beta/user-profiles.test.ts tests/index.test.ts ) diff --git a/scripts/mock b/scripts/mock index 2147c2f8..04d29019 100755 --- a/scripts/mock +++ b/scripts/mock @@ -24,7 +24,7 @@ if [ "$1" == "--daemon" ]; then # Pre-install the package so the download doesn't eat into the startup timeout npm exec --package=@stdy/cli@0.22.1 -- steady --version - npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" &> .stdy.log & # Wait for server to come online via health endpoint (max 30s) echo -n "Waiting for server" @@ -48,5 +48,5 @@ if [ "$1" == "--daemon" ]; then echo else - npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" + npm exec --package=@stdy/cli@0.22.1 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets "$URL" fi diff --git a/scripts/test b/scripts/test index ff118ade..7766a231 100755 --- a/scripts/test +++ b/scripts/test @@ -43,7 +43,7 @@ elif ! steady_is_running ; then echo -e "To run the server, pass in the path or url of your OpenAPI" echo -e "spec to the steady command:" echo - echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=comma --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" + echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.22.1 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=brackets --validator-form-array-format=brackets --validator-query-object-format=brackets --validator-form-object-format=brackets${NC}" echo exit 1 diff --git a/src/client.ts b/src/client.ts index 15895c5b..af73be6d 100644 --- a/src/client.ts +++ b/src/client.ts @@ -341,6 +341,11 @@ export interface ClientOptions { */ profile?: string | null | undefined; + /** + * Defaults to process.env['ANTHROPIC_WEBHOOK_SIGNING_KEY']. + */ + webhookKey?: string | null | undefined; + /** * Override the default base URL for the API, e.g., "https://api.example.com/v2/" * @@ -425,6 +430,7 @@ export const AI_PROMPT = '\\n\\nAssistant:'; export class BaseAnthropic { apiKey: string | null; authToken: string | null; + webhookKey: string | null; /** * The active credential provider. Default credential resolution runs once @@ -460,6 +466,7 @@ export class BaseAnthropic { * * @param {string | null | undefined} [opts.apiKey=process.env['ANTHROPIC_API_KEY'] ?? null] * @param {string | null | undefined} [opts.authToken=process.env['ANTHROPIC_AUTH_TOKEN'] ?? null] + * @param {string | null | undefined} [opts.webhookKey=process.env['ANTHROPIC_WEBHOOK_SIGNING_KEY'] ?? null] * @param {string} [opts.baseURL=process.env['ANTHROPIC_BASE_URL'] ?? https://api.anthropic.com] - Override the default base URL for the API. * @param {number} [opts.timeout=10 minutes] - The maximum amount of time (in milliseconds) the client will wait for a response before timing out. * @param {MergedRequestInit} [opts.fetchOptions] - Additional `RequestInit` options to be passed to `fetch` calls. @@ -469,7 +476,13 @@ export class BaseAnthropic { * @param {Record} opts.defaultQuery - Default query parameters to include with every request to the API. * @param {boolean} [opts.dangerouslyAllowBrowser=false] - By default, client-side use of this library is not allowed, as it risks exposing your secret API credentials to attackers. */ - constructor({ baseURL = readEnv('ANTHROPIC_BASE_URL'), apiKey, authToken, ...opts }: ClientOptions = {}) { + constructor({ + baseURL = readEnv('ANTHROPIC_BASE_URL'), + apiKey, + authToken, + webhookKey = readEnv('ANTHROPIC_WEBHOOK_SIGNING_KEY') ?? null, + ...opts + }: ClientOptions = {}) { // An explicit `profile` is a constructor-level credential choice; when set, // do not let env ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN shadow it. if (apiKey === undefined) { @@ -484,6 +497,7 @@ export class BaseAnthropic { const options: ClientOptions = { apiKey, authToken, + webhookKey, ...opts, baseURL: baseURL || `https://api.anthropic.com`, }; @@ -538,6 +552,7 @@ export class BaseAnthropic { this.apiKey = typeof apiKey === 'string' ? apiKey : null; this.authToken = authToken; + this.webhookKey = webhookKey; if (inherited) { this._authState = inherited; @@ -644,6 +659,7 @@ export class BaseAnthropic { fetchOptions: this.fetchOptions, apiKey: this.apiKey, authToken: this.authToken, + webhookKey: this.webhookKey, // credentials: this.credentials is a no-op when __auth is shared (the // ctor takes the inherited path and ignores options.credentials); when // overridesAuth is true via apiKey/authToken only, it lets the clone @@ -780,9 +796,6 @@ export class BaseAnthropic { return buildHeaders([{ Authorization: `Bearer ${this.authToken}` }]); } - /** - * Basic re-implementation of `qs.stringify` for primitive types. - */ protected stringifyQuery(query: object | Record): string { return stringifyQuery(query); } diff --git a/src/core/streaming.ts b/src/core/streaming.ts index 5d6252a5..e8821db3 100644 --- a/src/core/streaming.ts +++ b/src/core/streaming.ts @@ -86,7 +86,21 @@ export class Stream implements AsyncIterable { sse.event === 'session.error' || sse.event === 'session.deleted' || sse.event === 'span.model_request_start' || - sse.event === 'span.model_request_end' + sse.event === 'span.model_request_end' || + sse.event === 'span.outcome_evaluation_start' || + sse.event === 'span.outcome_evaluation_ongoing' || + sse.event === 'span.outcome_evaluation_end' || + sse.event === 'user.define_outcome' || + sse.event === 'agent.thread_message_received' || + sse.event === 'agent.thread_message_sent' || + sse.event === 'agent.session_thread_message_received' || + sse.event === 'agent.session_thread_message_sent' || + sse.event === 'session.thread_created' || + sse.event === 'session.thread_status_created' || + sse.event === 'session.thread_status_running' || + sse.event === 'session.thread_status_idle' || + sse.event === 'session.thread_status_rescheduled' || + sse.event === 'session.thread_status_terminated' ) { try { yield JSON.parse(sse.data) as Item; diff --git a/src/internal/qs/LICENSE.md b/src/internal/qs/LICENSE.md new file mode 100644 index 00000000..3fda1573 --- /dev/null +++ b/src/internal/qs/LICENSE.md @@ -0,0 +1,13 @@ +BSD 3-Clause License + +Copyright (c) 2014, Nathan LaFreniere and other [contributors](https://github.com/puruvj/neoqs/graphs/contributors) All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/internal/qs/README.md b/src/internal/qs/README.md new file mode 100644 index 00000000..67ae04ec --- /dev/null +++ b/src/internal/qs/README.md @@ -0,0 +1,3 @@ +# qs + +This is a vendored version of [neoqs](https://github.com/PuruVJ/neoqs) which is a TypeScript rewrite of [qs](https://github.com/ljharb/qs), a query string library. diff --git a/src/internal/qs/formats.ts b/src/internal/qs/formats.ts new file mode 100644 index 00000000..e76a742f --- /dev/null +++ b/src/internal/qs/formats.ts @@ -0,0 +1,10 @@ +import type { Format } from './types'; + +export const default_format: Format = 'RFC3986'; +export const default_formatter = (v: PropertyKey) => String(v); +export const formatters: Record string> = { + RFC1738: (v: PropertyKey) => String(v).replace(/%20/g, '+'), + RFC3986: default_formatter, +}; +export const RFC1738 = 'RFC1738'; +export const RFC3986 = 'RFC3986'; diff --git a/src/internal/qs/index.ts b/src/internal/qs/index.ts new file mode 100644 index 00000000..c3a3620d --- /dev/null +++ b/src/internal/qs/index.ts @@ -0,0 +1,13 @@ +import { default_format, formatters, RFC1738, RFC3986 } from './formats'; + +const formats = { + formatters, + RFC1738, + RFC3986, + default: default_format, +}; + +export { stringify } from './stringify'; +export { formats }; + +export type { DefaultDecoder, DefaultEncoder, Format, ParseOptions, StringifyOptions } from './types'; diff --git a/src/internal/qs/stringify.ts b/src/internal/qs/stringify.ts new file mode 100644 index 00000000..7e71387f --- /dev/null +++ b/src/internal/qs/stringify.ts @@ -0,0 +1,385 @@ +import { encode, is_buffer, maybe_map, has } from './utils'; +import { default_format, default_formatter, formatters } from './formats'; +import type { NonNullableProperties, StringifyOptions } from './types'; +import { isArray } from '../utils/values'; + +const array_prefix_generators = { + brackets(prefix: PropertyKey) { + return String(prefix) + '[]'; + }, + comma: 'comma', + indices(prefix: PropertyKey, key: string) { + return String(prefix) + '[' + key + ']'; + }, + repeat(prefix: PropertyKey) { + return String(prefix); + }, +}; + +const push_to_array = function (arr: any[], value_or_array: any) { + Array.prototype.push.apply(arr, isArray(value_or_array) ? value_or_array : [value_or_array]); +}; + +let toISOString; + +const defaults = { + addQueryPrefix: false, + allowDots: false, + allowEmptyArrays: false, + arrayFormat: 'indices', + charset: 'utf-8', + charsetSentinel: false, + delimiter: '&', + encode: true, + encodeDotInKeys: false, + encoder: encode, + encodeValuesOnly: false, + format: default_format, + formatter: default_formatter, + /** @deprecated */ + indices: false, + serializeDate(date) { + return (toISOString ??= Function.prototype.call.bind(Date.prototype.toISOString))(date); + }, + skipNulls: false, + strictNullHandling: false, +} as NonNullableProperties; + +function is_non_nullish_primitive(v: unknown): v is string | number | boolean | symbol | bigint { + return ( + typeof v === 'string' || + typeof v === 'number' || + typeof v === 'boolean' || + typeof v === 'symbol' || + typeof v === 'bigint' + ); +} + +const sentinel = {}; + +function inner_stringify( + object: any, + prefix: PropertyKey, + generateArrayPrefix: StringifyOptions['arrayFormat'] | ((prefix: string, key: string) => string), + commaRoundTrip: boolean, + allowEmptyArrays: boolean, + strictNullHandling: boolean, + skipNulls: boolean, + encodeDotInKeys: boolean, + encoder: StringifyOptions['encoder'], + filter: StringifyOptions['filter'], + sort: StringifyOptions['sort'], + allowDots: StringifyOptions['allowDots'], + serializeDate: StringifyOptions['serializeDate'], + format: StringifyOptions['format'], + formatter: StringifyOptions['formatter'], + encodeValuesOnly: boolean, + charset: StringifyOptions['charset'], + sideChannel: WeakMap, +) { + let obj = object; + + let tmp_sc = sideChannel; + let step = 0; + let find_flag = false; + while ((tmp_sc = tmp_sc.get(sentinel)) !== void undefined && !find_flag) { + // Where object last appeared in the ref tree + const pos = tmp_sc.get(object); + step += 1; + if (typeof pos !== 'undefined') { + if (pos === step) { + throw new RangeError('Cyclic object value'); + } else { + find_flag = true; // Break while + } + } + if (typeof tmp_sc.get(sentinel) === 'undefined') { + step = 0; + } + } + + if (typeof filter === 'function') { + obj = filter(prefix, obj); + } else if (obj instanceof Date) { + obj = serializeDate?.(obj); + } else if (generateArrayPrefix === 'comma' && isArray(obj)) { + obj = maybe_map(obj, function (value) { + if (value instanceof Date) { + return serializeDate?.(value); + } + return value; + }); + } + + if (obj === null) { + if (strictNullHandling) { + return encoder && !encodeValuesOnly ? + // @ts-expect-error + encoder(prefix, defaults.encoder, charset, 'key', format) + : prefix; + } + + obj = ''; + } + + if (is_non_nullish_primitive(obj) || is_buffer(obj)) { + if (encoder) { + const key_value = + encodeValuesOnly ? prefix + // @ts-expect-error + : encoder(prefix, defaults.encoder, charset, 'key', format); + return [ + formatter?.(key_value) + + '=' + + // @ts-expect-error + formatter?.(encoder(obj, defaults.encoder, charset, 'value', format)), + ]; + } + return [formatter?.(prefix) + '=' + formatter?.(String(obj))]; + } + + const values: string[] = []; + + if (typeof obj === 'undefined') { + return values; + } + + let obj_keys; + if (generateArrayPrefix === 'comma' && isArray(obj)) { + // we need to join elements in + if (encodeValuesOnly && encoder) { + // @ts-expect-error values only + obj = maybe_map(obj, encoder); + } + obj_keys = [{ value: obj.length > 0 ? obj.join(',') || null : void undefined }]; + } else if (isArray(filter)) { + obj_keys = filter; + } else { + const keys = Object.keys(obj); + obj_keys = sort ? keys.sort(sort) : keys; + } + + const encoded_prefix = encodeDotInKeys ? String(prefix).replace(/\./g, '%2E') : String(prefix); + + const adjusted_prefix = + commaRoundTrip && isArray(obj) && obj.length === 1 ? encoded_prefix + '[]' : encoded_prefix; + + if (allowEmptyArrays && isArray(obj) && obj.length === 0) { + return adjusted_prefix + '[]'; + } + + for (let j = 0; j < obj_keys.length; ++j) { + const key = obj_keys[j]; + const value = + // @ts-ignore + typeof key === 'object' && typeof key.value !== 'undefined' ? key.value : obj[key as any]; + + if (skipNulls && value === null) { + continue; + } + + // @ts-ignore + const encoded_key = allowDots && encodeDotInKeys ? (key as any).replace(/\./g, '%2E') : key; + const key_prefix = + isArray(obj) ? + typeof generateArrayPrefix === 'function' ? + generateArrayPrefix(adjusted_prefix, encoded_key) + : adjusted_prefix + : adjusted_prefix + (allowDots ? '.' + encoded_key : '[' + encoded_key + ']'); + + sideChannel.set(object, step); + const valueSideChannel = new WeakMap(); + valueSideChannel.set(sentinel, sideChannel); + push_to_array( + values, + inner_stringify( + value, + key_prefix, + generateArrayPrefix, + commaRoundTrip, + allowEmptyArrays, + strictNullHandling, + skipNulls, + encodeDotInKeys, + // @ts-ignore + generateArrayPrefix === 'comma' && encodeValuesOnly && isArray(obj) ? null : encoder, + filter, + sort, + allowDots, + serializeDate, + format, + formatter, + encodeValuesOnly, + charset, + valueSideChannel, + ), + ); + } + + return values; +} + +function normalize_stringify_options( + opts: StringifyOptions = defaults, +): NonNullableProperties> & { indices?: boolean } { + if (typeof opts.allowEmptyArrays !== 'undefined' && typeof opts.allowEmptyArrays !== 'boolean') { + throw new TypeError('`allowEmptyArrays` option can only be `true` or `false`, when provided'); + } + + if (typeof opts.encodeDotInKeys !== 'undefined' && typeof opts.encodeDotInKeys !== 'boolean') { + throw new TypeError('`encodeDotInKeys` option can only be `true` or `false`, when provided'); + } + + if (opts.encoder !== null && typeof opts.encoder !== 'undefined' && typeof opts.encoder !== 'function') { + throw new TypeError('Encoder has to be a function.'); + } + + const charset = opts.charset || defaults.charset; + if (typeof opts.charset !== 'undefined' && opts.charset !== 'utf-8' && opts.charset !== 'iso-8859-1') { + throw new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined'); + } + + let format = default_format; + if (typeof opts.format !== 'undefined') { + if (!has(formatters, opts.format)) { + throw new TypeError('Unknown format option provided.'); + } + format = opts.format; + } + const formatter = formatters[format]; + + let filter = defaults.filter; + if (typeof opts.filter === 'function' || isArray(opts.filter)) { + filter = opts.filter; + } + + let arrayFormat: StringifyOptions['arrayFormat']; + if (opts.arrayFormat && opts.arrayFormat in array_prefix_generators) { + arrayFormat = opts.arrayFormat; + } else if ('indices' in opts) { + arrayFormat = opts.indices ? 'indices' : 'repeat'; + } else { + arrayFormat = defaults.arrayFormat; + } + + if ('commaRoundTrip' in opts && typeof opts.commaRoundTrip !== 'boolean') { + throw new TypeError('`commaRoundTrip` must be a boolean, or absent'); + } + + const allowDots = + typeof opts.allowDots === 'undefined' ? + !!opts.encodeDotInKeys === true ? + true + : defaults.allowDots + : !!opts.allowDots; + + return { + addQueryPrefix: typeof opts.addQueryPrefix === 'boolean' ? opts.addQueryPrefix : defaults.addQueryPrefix, + // @ts-ignore + allowDots: allowDots, + allowEmptyArrays: + typeof opts.allowEmptyArrays === 'boolean' ? !!opts.allowEmptyArrays : defaults.allowEmptyArrays, + arrayFormat: arrayFormat, + charset: charset, + charsetSentinel: + typeof opts.charsetSentinel === 'boolean' ? opts.charsetSentinel : defaults.charsetSentinel, + commaRoundTrip: !!opts.commaRoundTrip, + delimiter: typeof opts.delimiter === 'undefined' ? defaults.delimiter : opts.delimiter, + encode: typeof opts.encode === 'boolean' ? opts.encode : defaults.encode, + encodeDotInKeys: + typeof opts.encodeDotInKeys === 'boolean' ? opts.encodeDotInKeys : defaults.encodeDotInKeys, + encoder: typeof opts.encoder === 'function' ? opts.encoder : defaults.encoder, + encodeValuesOnly: + typeof opts.encodeValuesOnly === 'boolean' ? opts.encodeValuesOnly : defaults.encodeValuesOnly, + filter: filter, + format: format, + formatter: formatter, + serializeDate: typeof opts.serializeDate === 'function' ? opts.serializeDate : defaults.serializeDate, + skipNulls: typeof opts.skipNulls === 'boolean' ? opts.skipNulls : defaults.skipNulls, + // @ts-ignore + sort: typeof opts.sort === 'function' ? opts.sort : null, + strictNullHandling: + typeof opts.strictNullHandling === 'boolean' ? opts.strictNullHandling : defaults.strictNullHandling, + }; +} + +export function stringify(object: any, opts: StringifyOptions = {}) { + let obj = object; + const options = normalize_stringify_options(opts); + + let obj_keys: PropertyKey[] | undefined; + let filter; + + if (typeof options.filter === 'function') { + filter = options.filter; + obj = filter('', obj); + } else if (isArray(options.filter)) { + filter = options.filter; + obj_keys = filter; + } + + const keys: string[] = []; + + if (typeof obj !== 'object' || obj === null) { + return ''; + } + + const generateArrayPrefix = array_prefix_generators[options.arrayFormat]; + const commaRoundTrip = generateArrayPrefix === 'comma' && options.commaRoundTrip; + + if (!obj_keys) { + obj_keys = Object.keys(obj); + } + + if (options.sort) { + obj_keys.sort(options.sort); + } + + const sideChannel = new WeakMap(); + for (let i = 0; i < obj_keys.length; ++i) { + const key = obj_keys[i]!; + + if (options.skipNulls && obj[key] === null) { + continue; + } + push_to_array( + keys, + inner_stringify( + obj[key], + key, + // @ts-expect-error + generateArrayPrefix, + commaRoundTrip, + options.allowEmptyArrays, + options.strictNullHandling, + options.skipNulls, + options.encodeDotInKeys, + options.encode ? options.encoder : null, + options.filter, + options.sort, + options.allowDots, + options.serializeDate, + options.format, + options.formatter, + options.encodeValuesOnly, + options.charset, + sideChannel, + ), + ); + } + + const joined = keys.join(options.delimiter); + let prefix = options.addQueryPrefix === true ? '?' : ''; + + if (options.charsetSentinel) { + if (options.charset === 'iso-8859-1') { + // encodeURIComponent('✓'), the "numeric entity" representation of a checkmark + prefix += 'utf8=%26%2310003%3B&'; + } else { + // encodeURIComponent('✓') + prefix += 'utf8=%E2%9C%93&'; + } + } + + return joined.length > 0 ? prefix + joined : ''; +} diff --git a/src/internal/qs/types.ts b/src/internal/qs/types.ts new file mode 100644 index 00000000..7c28dbb4 --- /dev/null +++ b/src/internal/qs/types.ts @@ -0,0 +1,71 @@ +export type Format = 'RFC1738' | 'RFC3986'; + +export type DefaultEncoder = (str: any, defaultEncoder?: any, charset?: string) => string; +export type DefaultDecoder = (str: string, decoder?: any, charset?: string) => string; + +export type BooleanOptional = boolean | undefined; + +export type StringifyBaseOptions = { + delimiter?: string; + allowDots?: boolean; + encodeDotInKeys?: boolean; + strictNullHandling?: boolean; + skipNulls?: boolean; + encode?: boolean; + encoder?: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format?: Format, + ) => string; + filter?: Array | ((prefix: PropertyKey, value: any) => any); + arrayFormat?: 'indices' | 'brackets' | 'repeat' | 'comma'; + indices?: boolean; + sort?: ((a: PropertyKey, b: PropertyKey) => number) | null; + serializeDate?: (d: Date) => string; + format?: 'RFC1738' | 'RFC3986'; + formatter?: (str: PropertyKey) => string; + encodeValuesOnly?: boolean; + addQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + allowEmptyArrays?: boolean; + commaRoundTrip?: boolean; +}; + +export type StringifyOptions = StringifyBaseOptions; + +export type ParseBaseOptions = { + comma?: boolean; + delimiter?: string | RegExp; + depth?: number | false; + decoder?: (str: string, defaultDecoder: DefaultDecoder, charset: string, type: 'key' | 'value') => any; + arrayLimit?: number; + parseArrays?: boolean; + plainObjects?: boolean; + allowPrototypes?: boolean; + allowSparse?: boolean; + parameterLimit?: number; + strictDepth?: boolean; + strictNullHandling?: boolean; + ignoreQueryPrefix?: boolean; + charset?: 'utf-8' | 'iso-8859-1'; + charsetSentinel?: boolean; + interpretNumericEntities?: boolean; + allowEmptyArrays?: boolean; + duplicates?: 'combine' | 'first' | 'last'; + allowDots?: boolean; + decodeDotInKeys?: boolean; +}; + +export type ParseOptions = ParseBaseOptions; + +export type ParsedQs = { + [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[]; +}; + +// Type to remove null or undefined union from each property +export type NonNullableProperties = { + [K in keyof T]-?: Exclude; +}; diff --git a/src/internal/qs/utils.ts b/src/internal/qs/utils.ts new file mode 100644 index 00000000..4cd56579 --- /dev/null +++ b/src/internal/qs/utils.ts @@ -0,0 +1,265 @@ +import { RFC1738 } from './formats'; +import type { DefaultEncoder, Format } from './types'; +import { isArray } from '../utils/values'; + +export let has = (obj: object, key: PropertyKey): boolean => ( + (has = (Object as any).hasOwn ?? Function.prototype.call.bind(Object.prototype.hasOwnProperty)), + has(obj, key) +); + +const hex_table = /* @__PURE__ */ (() => { + const array = []; + for (let i = 0; i < 256; ++i) { + array.push('%' + ((i < 16 ? '0' : '') + i.toString(16)).toUpperCase()); + } + + return array; +})(); + +function compact_queue>(queue: Array<{ obj: T; prop: string }>) { + while (queue.length > 1) { + const item = queue.pop(); + if (!item) continue; + + const obj = item.obj[item.prop]; + + if (isArray(obj)) { + const compacted: unknown[] = []; + + for (let j = 0; j < obj.length; ++j) { + if (typeof obj[j] !== 'undefined') { + compacted.push(obj[j]); + } + } + + // @ts-ignore + item.obj[item.prop] = compacted; + } + } +} + +function array_to_object(source: any[], options: { plainObjects: boolean }) { + const obj = options && options.plainObjects ? Object.create(null) : {}; + for (let i = 0; i < source.length; ++i) { + if (typeof source[i] !== 'undefined') { + obj[i] = source[i]; + } + } + + return obj; +} + +export function merge( + target: any, + source: any, + options: { plainObjects?: boolean; allowPrototypes?: boolean } = {}, +) { + if (!source) { + return target; + } + + if (typeof source !== 'object') { + if (isArray(target)) { + target.push(source); + } else if (target && typeof target === 'object') { + if ((options && (options.plainObjects || options.allowPrototypes)) || !has(Object.prototype, source)) { + target[source] = true; + } + } else { + return [target, source]; + } + + return target; + } + + if (!target || typeof target !== 'object') { + return [target].concat(source); + } + + let mergeTarget = target; + if (isArray(target) && !isArray(source)) { + // @ts-ignore + mergeTarget = array_to_object(target, options); + } + + if (isArray(target) && isArray(source)) { + source.forEach(function (item, i) { + if (has(target, i)) { + const targetItem = target[i]; + if (targetItem && typeof targetItem === 'object' && item && typeof item === 'object') { + target[i] = merge(targetItem, item, options); + } else { + target.push(item); + } + } else { + target[i] = item; + } + }); + return target; + } + + return Object.keys(source).reduce(function (acc, key) { + const value = source[key]; + + if (has(acc, key)) { + acc[key] = merge(acc[key], value, options); + } else { + acc[key] = value; + } + return acc; + }, mergeTarget); +} + +export function assign_single_source(target: any, source: any) { + return Object.keys(source).reduce(function (acc, key) { + acc[key] = source[key]; + return acc; + }, target); +} + +export function decode(str: string, _: any, charset: string) { + const strWithoutPlus = str.replace(/\+/g, ' '); + if (charset === 'iso-8859-1') { + // unescape never throws, no try...catch needed: + return strWithoutPlus.replace(/%[0-9a-f]{2}/gi, unescape); + } + // utf-8 + try { + return decodeURIComponent(strWithoutPlus); + } catch (e) { + return strWithoutPlus; + } +} + +const limit = 1024; + +export const encode: ( + str: any, + defaultEncoder: DefaultEncoder, + charset: string, + type: 'key' | 'value', + format: Format, +) => string = (str, _defaultEncoder, charset, _kind, format: Format) => { + // This code was originally written by Brian White for the io.js core querystring library. + // It has been adapted here for stricter adherence to RFC 3986 + if (str.length === 0) { + return str; + } + + let string = str; + if (typeof str === 'symbol') { + string = Symbol.prototype.toString.call(str); + } else if (typeof str !== 'string') { + string = String(str); + } + + if (charset === 'iso-8859-1') { + return escape(string).replace(/%u[0-9a-f]{4}/gi, function ($0) { + return '%26%23' + parseInt($0.slice(2), 16) + '%3B'; + }); + } + + let out = ''; + for (let j = 0; j < string.length; j += limit) { + const segment = string.length >= limit ? string.slice(j, j + limit) : string; + const arr = []; + + for (let i = 0; i < segment.length; ++i) { + let c = segment.charCodeAt(i); + if ( + c === 0x2d || // - + c === 0x2e || // . + c === 0x5f || // _ + c === 0x7e || // ~ + (c >= 0x30 && c <= 0x39) || // 0-9 + (c >= 0x41 && c <= 0x5a) || // a-z + (c >= 0x61 && c <= 0x7a) || // A-Z + (format === RFC1738 && (c === 0x28 || c === 0x29)) // ( ) + ) { + arr[arr.length] = segment.charAt(i); + continue; + } + + if (c < 0x80) { + arr[arr.length] = hex_table[c]; + continue; + } + + if (c < 0x800) { + arr[arr.length] = hex_table[0xc0 | (c >> 6)]! + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + if (c < 0xd800 || c >= 0xe000) { + arr[arr.length] = + hex_table[0xe0 | (c >> 12)]! + hex_table[0x80 | ((c >> 6) & 0x3f)] + hex_table[0x80 | (c & 0x3f)]; + continue; + } + + i += 1; + c = 0x10000 + (((c & 0x3ff) << 10) | (segment.charCodeAt(i) & 0x3ff)); + + arr[arr.length] = + hex_table[0xf0 | (c >> 18)]! + + hex_table[0x80 | ((c >> 12) & 0x3f)] + + hex_table[0x80 | ((c >> 6) & 0x3f)] + + hex_table[0x80 | (c & 0x3f)]; + } + + out += arr.join(''); + } + + return out; +}; + +export function compact(value: any) { + const queue = [{ obj: { o: value }, prop: 'o' }]; + const refs = []; + + for (let i = 0; i < queue.length; ++i) { + const item = queue[i]; + // @ts-ignore + const obj = item.obj[item.prop]; + + const keys = Object.keys(obj); + for (let j = 0; j < keys.length; ++j) { + const key = keys[j]!; + const val = obj[key]; + if (typeof val === 'object' && val !== null && refs.indexOf(val) === -1) { + queue.push({ obj: obj, prop: key }); + refs.push(val); + } + } + } + + compact_queue(queue); + + return value; +} + +export function is_regexp(obj: any) { + return Object.prototype.toString.call(obj) === '[object RegExp]'; +} + +export function is_buffer(obj: any) { + if (!obj || typeof obj !== 'object') { + return false; + } + + return !!(obj.constructor && obj.constructor.isBuffer && obj.constructor.isBuffer(obj)); +} + +export function combine(a: any, b: any) { + return [].concat(a, b); +} + +export function maybe_map(val: T[], fn: (v: T) => T) { + if (isArray(val)) { + const mapped = []; + for (let i = 0; i < val.length; i += 1) { + mapped.push(fn(val[i]!)); + } + return mapped; + } + return fn(val); +} diff --git a/src/internal/utils/query.ts b/src/internal/utils/query.ts index 86403053..f7d1b4b2 100644 --- a/src/internal/utils/query.ts +++ b/src/internal/utils/query.ts @@ -1,23 +1,7 @@ // File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -import { AnthropicError } from '../../core/error'; +import * as qs from '../qs/stringify'; -/** - * Basic re-implementation of `qs.stringify` for primitive types. - */ export function stringifyQuery(query: object | Record) { - return Object.entries(query) - .filter(([_, value]) => typeof value !== 'undefined') - .map(([key, value]) => { - if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') { - return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`; - } - if (value === null) { - return `${encodeURIComponent(key)}=`; - } - throw new AnthropicError( - `Cannot stringify type ${typeof value}; Expected string, number, boolean, or null. If you need to pass nested query parameters, you can manually encode them, e.g. { query: { 'foo[key1]': value1, 'foo[key2]': value2 } }, and please open a GitHub issue requesting better support for your use case.`, - ); - }) - .join('&'); + return qs.stringify(query, { arrayFormat: 'brackets' }); } diff --git a/src/resources/beta/agents/agents.ts b/src/resources/beta/agents/agents.ts index bb765ef2..3bb35a06 100644 --- a/src/resources/beta/agents/agents.ts +++ b/src/resources/beta/agents/agents.ts @@ -4,6 +4,7 @@ import { APIResource } from '../../../core/resource'; import * as BetaAPI from '../beta'; import * as VersionsAPI from './versions'; import { VersionListParams, Versions } from './versions'; +import * as SessionsAPI from '../sessions/sessions'; import { APIPromise } from '../../../core/api-promise'; import { PageCursor, type PageCursorParams, PagePromise } from '../../../core/pagination'; import { buildHeaders } from '../../../internal/headers'; @@ -174,6 +175,11 @@ export interface BetaManagedAgentsAgent { */ model: BetaManagedAgentsModelConfig; + /** + * Resolved coordinator topology with a concrete agent roster. + */ + multiagent: SessionsAPI.BetaManagedAgentsMultiagent | null; + name: string; skills: Array; @@ -198,6 +204,17 @@ export interface BetaManagedAgentsAgent { version: number; } +/** + * A resolved agent reference with a concrete version. + */ +export interface BetaManagedAgentsAgentReference { + id: string; + + type: 'agent'; + + version: number; +} + /** * Configuration for a specific agent tool. */ @@ -589,6 +606,45 @@ export interface BetaManagedAgentsModelConfigParams { speed?: 'standard' | 'fast' | null; } +/** + * Resolved coordinator topology with a concrete agent roster. + */ +export interface BetaManagedAgentsMultiagentCoordinator { + /** + * Agents the coordinator may spawn as session threads, each resolved to a specific + * version. + */ + agents: Array; + + type: 'coordinator'; +} + +/** + * A coordinator topology: the session's primary thread orchestrates work by + * spawning session threads, each running an agent drawn from the `agents` roster. + */ +export interface BetaManagedAgentsMultiagentCoordinatorParams { + /** + * Agents the coordinator may spawn as session threads. 1–20 entries. Each entry is + * an agent ID string, a versioned `{"type":"agent","id","version"}` reference, or + * `{"type":"self"}` to allow recursive self-invocation. Entries must reference + * distinct agents (after resolving `self` and string forms); at most one `self`. + * Referenced agents must exist, must not be archived, and must not themselves have + * `multiagent` set (depth limit 1). + */ + agents: Array; + + type: 'coordinator'; +} + +/** + * Sentinel roster entry meaning "the agent that owns this configuration". Resolved + * server-side to a concrete agent reference. + */ +export interface BetaManagedAgentsMultiagentSelfParams { + type: 'self'; +} + /** * Skill to load in the session container. */ @@ -645,6 +701,13 @@ export interface AgentCreateParams { */ metadata?: { [key: string]: string }; + /** + * Body param: A coordinator topology: the session's primary thread orchestrates + * work by spawning session threads, each running an agent drawn from the `agents` + * roster. + */ + multiagent?: SessionsAPI.BetaManagedAgentsMultiagentParams | null; + /** * Body param: Skills available to the agent. Maximum 20. */ @@ -719,6 +782,13 @@ export interface AgentUpdateParams { */ model?: BetaManagedAgentsModel | BetaManagedAgentsModelConfigParams; + /** + * Body param: A coordinator topology: the session's primary thread orchestrates + * work by spawning session threads, each running an agent drawn from the `agents` + * roster. + */ + multiagent?: SessionsAPI.BetaManagedAgentsMultiagentParams | null; + /** * Body param: Human-readable name. 1-256 characters. Omit to preserve. Cannot be * cleared. @@ -788,6 +858,7 @@ Agents.Versions = Versions; export declare namespace Agents { export { type BetaManagedAgentsAgent as BetaManagedAgentsAgent, + type BetaManagedAgentsAgentReference as BetaManagedAgentsAgentReference, type BetaManagedAgentsAgentToolConfig as BetaManagedAgentsAgentToolConfig, type BetaManagedAgentsAgentToolConfigParams as BetaManagedAgentsAgentToolConfigParams, type BetaManagedAgentsAgentToolsetDefaultConfig as BetaManagedAgentsAgentToolsetDefaultConfig, @@ -813,6 +884,9 @@ export declare namespace Agents { type BetaManagedAgentsModel as BetaManagedAgentsModel, type BetaManagedAgentsModelConfig as BetaManagedAgentsModelConfig, type BetaManagedAgentsModelConfigParams as BetaManagedAgentsModelConfigParams, + type BetaManagedAgentsMultiagentCoordinator as BetaManagedAgentsMultiagentCoordinator, + type BetaManagedAgentsMultiagentCoordinatorParams as BetaManagedAgentsMultiagentCoordinatorParams, + type BetaManagedAgentsMultiagentSelfParams as BetaManagedAgentsMultiagentSelfParams, type BetaManagedAgentsSkillParams as BetaManagedAgentsSkillParams, type BetaManagedAgentsURLMCPServerParams as BetaManagedAgentsURLMCPServerParams, type BetaManagedAgentsAgentsPageCursor as BetaManagedAgentsAgentsPageCursor, diff --git a/src/resources/beta/agents/index.ts b/src/resources/beta/agents/index.ts index 5a7ca634..64f2d8b4 100644 --- a/src/resources/beta/agents/index.ts +++ b/src/resources/beta/agents/index.ts @@ -3,6 +3,7 @@ export { Agents, type BetaManagedAgentsAgent, + type BetaManagedAgentsAgentReference, type BetaManagedAgentsAgentToolConfig, type BetaManagedAgentsAgentToolConfigParams, type BetaManagedAgentsAgentToolsetDefaultConfig, @@ -28,6 +29,9 @@ export { type BetaManagedAgentsModel, type BetaManagedAgentsModelConfig, type BetaManagedAgentsModelConfigParams, + type BetaManagedAgentsMultiagentCoordinator, + type BetaManagedAgentsMultiagentCoordinatorParams, + type BetaManagedAgentsMultiagentSelfParams, type BetaManagedAgentsSkillParams, type BetaManagedAgentsURLMCPServerParams, type AgentCreateParams, diff --git a/src/resources/beta/beta.ts b/src/resources/beta/beta.ts index bef111d2..52492d79 100644 --- a/src/resources/beta/beta.ts +++ b/src/resources/beta/beta.ts @@ -61,6 +61,35 @@ import { UserProfileUpdateParams, UserProfiles, } from './user-profiles'; +import * as WebhooksAPI from './webhooks'; +import { + BetaWebhookEvent, + BetaWebhookEventData, + BetaWebhookSessionArchivedEventData, + BetaWebhookSessionCreatedEventData, + BetaWebhookSessionDeletedEventData, + BetaWebhookSessionIdledEventData, + BetaWebhookSessionOutcomeEvaluationEndedEventData, + BetaWebhookSessionPendingEventData, + BetaWebhookSessionRequiresActionEventData, + BetaWebhookSessionRunningEventData, + BetaWebhookSessionStatusIdledEventData, + BetaWebhookSessionStatusRescheduledEventData, + BetaWebhookSessionStatusRunStartedEventData, + BetaWebhookSessionStatusTerminatedEventData, + BetaWebhookSessionThreadCreatedEventData, + BetaWebhookSessionThreadIdledEventData, + BetaWebhookSessionThreadTerminatedEventData, + BetaWebhookVaultArchivedEventData, + BetaWebhookVaultCreatedEventData, + BetaWebhookVaultCredentialArchivedEventData, + BetaWebhookVaultCredentialCreatedEventData, + BetaWebhookVaultCredentialDeletedEventData, + BetaWebhookVaultCredentialRefreshFailedEventData, + BetaWebhookVaultDeletedEventData, + UnwrapWebhookEvent, + Webhooks, +} from './webhooks'; import * as AgentsAPI from './agents/agents'; import { AgentArchiveParams, @@ -70,6 +99,7 @@ import { AgentUpdateParams, Agents, BetaManagedAgentsAgent, + BetaManagedAgentsAgentReference, BetaManagedAgentsAgentToolConfig, BetaManagedAgentsAgentToolConfigParams, BetaManagedAgentsAgentToolset20260401, @@ -96,6 +126,9 @@ import { BetaManagedAgentsModel, BetaManagedAgentsModelConfig, BetaManagedAgentsModelConfigParams, + BetaManagedAgentsMultiagentCoordinator, + BetaManagedAgentsMultiagentCoordinatorParams, + BetaManagedAgentsMultiagentSelfParams, BetaManagedAgentsSkillParams, BetaManagedAgentsURLMCPServerParams, } from './agents/agents'; @@ -331,8 +364,13 @@ import { BetaManagedAgentsFileResourceParams, BetaManagedAgentsGitHubRepositoryResourceParams, BetaManagedAgentsMemoryStoreResourceParam, + BetaManagedAgentsMultiagent, + BetaManagedAgentsMultiagentParams, + BetaManagedAgentsMultiagentRosterEntryParams, + BetaManagedAgentsOutcomeEvaluationResource, BetaManagedAgentsSession, BetaManagedAgentsSessionAgent, + BetaManagedAgentsSessionMultiagentCoordinator, BetaManagedAgentsSessionStats, BetaManagedAgentsSessionUsage, BetaManagedAgentsSessionsPageCursor, @@ -381,6 +419,7 @@ export class Beta extends APIResource { memoryStores: MemoryStoresAPI.MemoryStores = new MemoryStoresAPI.MemoryStores(this._client); files: FilesAPI.Files = new FilesAPI.Files(this._client); skills: SkillsAPI.Skills = new SkillsAPI.Skills(this._client); + webhooks: WebhooksAPI.Webhooks = new WebhooksAPI.Webhooks(this._client); userProfiles: UserProfilesAPI.UserProfiles = new UserProfilesAPI.UserProfiles(this._client); } @@ -408,7 +447,8 @@ export type AnthropicBeta = | 'fast-mode-2026-02-01' | 'output-300k-2026-03-24' | 'user-profiles-2026-03-24' - | 'advisor-tool-2026-03-01'; + | 'advisor-tool-2026-03-01' + | 'managed-agents-2026-04-01'; export interface BetaAPIError { message: string; @@ -492,6 +532,7 @@ Beta.Vaults = Vaults; Beta.MemoryStores = MemoryStores; Beta.Files = Files; Beta.Skills = Skills; +Beta.Webhooks = Webhooks; Beta.UserProfiles = UserProfiles; export declare namespace Beta { @@ -736,6 +777,7 @@ export declare namespace Beta { export { Agents as Agents, type BetaManagedAgentsAgent as BetaManagedAgentsAgent, + type BetaManagedAgentsAgentReference as BetaManagedAgentsAgentReference, type BetaManagedAgentsAgentToolConfig as BetaManagedAgentsAgentToolConfig, type BetaManagedAgentsAgentToolConfigParams as BetaManagedAgentsAgentToolConfigParams, type BetaManagedAgentsAgentToolsetDefaultConfig as BetaManagedAgentsAgentToolsetDefaultConfig, @@ -761,6 +803,9 @@ export declare namespace Beta { type BetaManagedAgentsModel as BetaManagedAgentsModel, type BetaManagedAgentsModelConfig as BetaManagedAgentsModelConfig, type BetaManagedAgentsModelConfigParams as BetaManagedAgentsModelConfigParams, + type BetaManagedAgentsMultiagentCoordinator as BetaManagedAgentsMultiagentCoordinator, + type BetaManagedAgentsMultiagentCoordinatorParams as BetaManagedAgentsMultiagentCoordinatorParams, + type BetaManagedAgentsMultiagentSelfParams as BetaManagedAgentsMultiagentSelfParams, type BetaManagedAgentsSkillParams as BetaManagedAgentsSkillParams, type BetaManagedAgentsURLMCPServerParams as BetaManagedAgentsURLMCPServerParams, type BetaManagedAgentsAgentsPageCursor as BetaManagedAgentsAgentsPageCursor, @@ -801,8 +846,13 @@ export declare namespace Beta { type BetaManagedAgentsFileResourceParams as BetaManagedAgentsFileResourceParams, type BetaManagedAgentsGitHubRepositoryResourceParams as BetaManagedAgentsGitHubRepositoryResourceParams, type BetaManagedAgentsMemoryStoreResourceParam as BetaManagedAgentsMemoryStoreResourceParam, + type BetaManagedAgentsMultiagent as BetaManagedAgentsMultiagent, + type BetaManagedAgentsMultiagentParams as BetaManagedAgentsMultiagentParams, + type BetaManagedAgentsMultiagentRosterEntryParams as BetaManagedAgentsMultiagentRosterEntryParams, + type BetaManagedAgentsOutcomeEvaluationResource as BetaManagedAgentsOutcomeEvaluationResource, type BetaManagedAgentsSession as BetaManagedAgentsSession, type BetaManagedAgentsSessionAgent as BetaManagedAgentsSessionAgent, + type BetaManagedAgentsSessionMultiagentCoordinator as BetaManagedAgentsSessionMultiagentCoordinator, type BetaManagedAgentsSessionStats as BetaManagedAgentsSessionStats, type BetaManagedAgentsSessionUsage as BetaManagedAgentsSessionUsage, type BetaManagedAgentsSessionsPageCursor as BetaManagedAgentsSessionsPageCursor, @@ -866,6 +916,35 @@ export declare namespace Beta { type SkillDeleteParams as SkillDeleteParams, }; + export { + Webhooks as Webhooks, + type BetaWebhookEvent as BetaWebhookEvent, + type BetaWebhookEventData as BetaWebhookEventData, + type BetaWebhookSessionArchivedEventData as BetaWebhookSessionArchivedEventData, + type BetaWebhookSessionCreatedEventData as BetaWebhookSessionCreatedEventData, + type BetaWebhookSessionDeletedEventData as BetaWebhookSessionDeletedEventData, + type BetaWebhookSessionIdledEventData as BetaWebhookSessionIdledEventData, + type BetaWebhookSessionOutcomeEvaluationEndedEventData as BetaWebhookSessionOutcomeEvaluationEndedEventData, + type BetaWebhookSessionPendingEventData as BetaWebhookSessionPendingEventData, + type BetaWebhookSessionRequiresActionEventData as BetaWebhookSessionRequiresActionEventData, + type BetaWebhookSessionRunningEventData as BetaWebhookSessionRunningEventData, + type BetaWebhookSessionStatusIdledEventData as BetaWebhookSessionStatusIdledEventData, + type BetaWebhookSessionStatusRescheduledEventData as BetaWebhookSessionStatusRescheduledEventData, + type BetaWebhookSessionStatusRunStartedEventData as BetaWebhookSessionStatusRunStartedEventData, + type BetaWebhookSessionStatusTerminatedEventData as BetaWebhookSessionStatusTerminatedEventData, + type BetaWebhookSessionThreadCreatedEventData as BetaWebhookSessionThreadCreatedEventData, + type BetaWebhookSessionThreadIdledEventData as BetaWebhookSessionThreadIdledEventData, + type BetaWebhookSessionThreadTerminatedEventData as BetaWebhookSessionThreadTerminatedEventData, + type BetaWebhookVaultArchivedEventData as BetaWebhookVaultArchivedEventData, + type BetaWebhookVaultCreatedEventData as BetaWebhookVaultCreatedEventData, + type BetaWebhookVaultCredentialArchivedEventData as BetaWebhookVaultCredentialArchivedEventData, + type BetaWebhookVaultCredentialCreatedEventData as BetaWebhookVaultCredentialCreatedEventData, + type BetaWebhookVaultCredentialDeletedEventData as BetaWebhookVaultCredentialDeletedEventData, + type BetaWebhookVaultCredentialRefreshFailedEventData as BetaWebhookVaultCredentialRefreshFailedEventData, + type BetaWebhookVaultDeletedEventData as BetaWebhookVaultDeletedEventData, + type UnwrapWebhookEvent as UnwrapWebhookEvent, + }; + export { UserProfiles as UserProfiles, type BetaUserProfile as BetaUserProfile, diff --git a/src/resources/beta/index.ts b/src/resources/beta/index.ts index 21478dac..54dcf969 100644 --- a/src/resources/beta/index.ts +++ b/src/resources/beta/index.ts @@ -3,6 +3,7 @@ export { Agents, type BetaManagedAgentsAgent, + type BetaManagedAgentsAgentReference, type BetaManagedAgentsAgentToolConfig, type BetaManagedAgentsAgentToolConfigParams, type BetaManagedAgentsAgentToolsetDefaultConfig, @@ -28,6 +29,9 @@ export { type BetaManagedAgentsModel, type BetaManagedAgentsModelConfig, type BetaManagedAgentsModelConfigParams, + type BetaManagedAgentsMultiagentCoordinator, + type BetaManagedAgentsMultiagentCoordinatorParams, + type BetaManagedAgentsMultiagentSelfParams, type BetaManagedAgentsSkillParams, type BetaManagedAgentsURLMCPServerParams, type AgentCreateParams, @@ -336,8 +340,13 @@ export { type BetaManagedAgentsFileResourceParams, type BetaManagedAgentsGitHubRepositoryResourceParams, type BetaManagedAgentsMemoryStoreResourceParam, + type BetaManagedAgentsMultiagent, + type BetaManagedAgentsMultiagentParams, + type BetaManagedAgentsMultiagentRosterEntryParams, + type BetaManagedAgentsOutcomeEvaluationResource, type BetaManagedAgentsSession, type BetaManagedAgentsSessionAgent, + type BetaManagedAgentsSessionMultiagentCoordinator, type BetaManagedAgentsSessionStats, type BetaManagedAgentsSessionUsage, type SessionCreateParams, @@ -384,3 +393,31 @@ export { type VaultArchiveParams, type BetaManagedAgentsVaultsPageCursor, } from './vaults/index'; +export { + Webhooks, + type BetaWebhookEvent, + type BetaWebhookEventData, + type BetaWebhookSessionArchivedEventData, + type BetaWebhookSessionCreatedEventData, + type BetaWebhookSessionDeletedEventData, + type BetaWebhookSessionIdledEventData, + type BetaWebhookSessionOutcomeEvaluationEndedEventData, + type BetaWebhookSessionPendingEventData, + type BetaWebhookSessionRequiresActionEventData, + type BetaWebhookSessionRunningEventData, + type BetaWebhookSessionStatusIdledEventData, + type BetaWebhookSessionStatusRescheduledEventData, + type BetaWebhookSessionStatusRunStartedEventData, + type BetaWebhookSessionStatusTerminatedEventData, + type BetaWebhookSessionThreadCreatedEventData, + type BetaWebhookSessionThreadIdledEventData, + type BetaWebhookSessionThreadTerminatedEventData, + type BetaWebhookVaultArchivedEventData, + type BetaWebhookVaultCreatedEventData, + type BetaWebhookVaultCredentialArchivedEventData, + type BetaWebhookVaultCredentialCreatedEventData, + type BetaWebhookVaultCredentialDeletedEventData, + type BetaWebhookVaultCredentialRefreshFailedEventData, + type BetaWebhookVaultDeletedEventData, + type UnwrapWebhookEvent, +} from './webhooks'; diff --git a/src/resources/beta/messages/messages.ts b/src/resources/beta/messages/messages.ts index a69c88a9..b1375dda 100644 --- a/src/resources/beta/messages/messages.ts +++ b/src/resources/beta/messages/messages.ts @@ -597,30 +597,66 @@ export interface BetaCitationConfig { } export interface BetaCitationContentBlockLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; file_id: string | null; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; } export interface BetaCitationContentBlockLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; @@ -657,14 +693,40 @@ export interface BetaCitationPageLocationParam { } export interface BetaCitationSearchResultLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; @@ -673,14 +735,40 @@ export interface BetaCitationSearchResultLocation { } export interface BetaCitationSearchResultLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; diff --git a/src/resources/beta/sessions/events.ts b/src/resources/beta/sessions/events.ts index 6dafc6e3..0fa9e35b 100644 --- a/src/resources/beta/sessions/events.ts +++ b/src/resources/beta/sessions/events.ts @@ -139,6 +139,13 @@ export interface BetaManagedAgentsAgentCustomToolUseEvent { processed_at: string; type: 'agent.custom_tool_use'; + + /** + * When set, this event was cross-posted from a subagent's thread to surface its + * custom tool use on the primary thread's stream. Empty on the thread's own + * events. Echo this on a `user.custom_tool_result` event to route the result back. + */ + session_thread_id?: string | null; } /** @@ -208,6 +215,14 @@ export interface BetaManagedAgentsAgentMCPToolUseEvent { * AgentEvaluatedPermission enum */ evaluated_permission?: 'allow' | 'ask' | 'deny'; + + /** + * When set, this event was cross-posted from a subagent's thread to surface its + * permission request on the primary thread's stream. Empty on the thread's own + * events. Echo this on a `user.tool_confirmation` event to route the approval + * back. + */ + session_thread_id?: string | null; } /** @@ -267,6 +282,74 @@ export interface BetaManagedAgentsAgentThreadContextCompactedEvent { type: 'agent.thread_context_compacted'; } +/** + * Delivery event written to the target thread's input stream when an + * agent-to-agent message arrives. + */ +export interface BetaManagedAgentsAgentThreadMessageReceivedEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Message content blocks. + */ + content: Array; + + /** + * Public `sthr_` ID of the thread that sent the message. + */ + from_session_thread_id: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + type: 'agent.thread_message_received'; + + /** + * Name of the callable agent this message came from. Absent when received from the + * primary agent. + */ + from_agent_name?: string | null; +} + +/** + * Observability event emitted to the sender's output stream when an agent-to-agent + * message is sent. + */ +export interface BetaManagedAgentsAgentThreadMessageSentEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Message content blocks. + */ + content: Array; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public `sthr_` ID of the thread the message was sent to. + */ + to_session_thread_id: string; + + type: 'agent.thread_message_sent'; + + /** + * Name of the callable agent this message was sent to. Absent when sent to the + * primary agent. + */ + to_agent_name?: string | null; +} + /** * Event representing the result of an agent tool execution. */ @@ -329,6 +412,14 @@ export interface BetaManagedAgentsAgentToolUseEvent { * AgentEvaluatedPermission enum */ evaluated_permission?: 'allow' | 'ask' | 'deny'; + + /** + * When set, this event was cross-posted from a subagent's thread to surface its + * permission request on the primary thread's stream. Empty on the thread's own + * events. Echo this on a `user.tool_confirmation` event to route the approval + * back. + */ + session_thread_id?: string | null; } /** @@ -422,7 +513,8 @@ export type BetaManagedAgentsEventParams = | BetaManagedAgentsUserMessageEventParams | BetaManagedAgentsUserInterruptEventParams | BetaManagedAgentsUserToolConfirmationEventParams - | BetaManagedAgentsUserCustomToolResultEventParams; + | BetaManagedAgentsUserCustomToolResultEventParams + | BetaManagedAgentsUserDefineOutcomeEventParams; /** * Document referenced by file ID. @@ -448,6 +540,30 @@ export interface BetaManagedAgentsFileImageSource { type: 'file'; } +/** + * Rubric referenced by a file uploaded via the Files API. + */ +export interface BetaManagedAgentsFileRubric { + /** + * ID of the rubric file. + */ + file_id: string; + + type: 'file'; +} + +/** + * Rubric referenced by a file uploaded via the Files API. + */ +export interface BetaManagedAgentsFileRubricParams { + /** + * ID of the rubric file. + */ + file_id: string; + + type: 'file'; +} + /** * Image content specified directly as base64 data or as a reference via a URL. */ @@ -628,6 +744,7 @@ export interface BetaManagedAgentsSendSessionEvents { | BetaManagedAgentsUserInterruptEvent | BetaManagedAgentsUserToolConfirmationEvent | BetaManagedAgentsUserCustomToolResultEvent + | BetaManagedAgentsUserDefineOutcomeEvent >; } @@ -702,15 +819,26 @@ export type BetaManagedAgentsSessionEvent = | BetaManagedAgentsAgentMCPToolResultEvent | BetaManagedAgentsAgentToolUseEvent | BetaManagedAgentsAgentToolResultEvent + | BetaManagedAgentsAgentThreadMessageReceivedEvent + | BetaManagedAgentsAgentThreadMessageSentEvent | BetaManagedAgentsAgentThreadContextCompactedEvent | BetaManagedAgentsSessionErrorEvent | BetaManagedAgentsSessionStatusRescheduledEvent | BetaManagedAgentsSessionStatusRunningEvent | BetaManagedAgentsSessionStatusIdleEvent | BetaManagedAgentsSessionStatusTerminatedEvent + | BetaManagedAgentsSessionThreadCreatedEvent + | BetaManagedAgentsSpanOutcomeEvaluationStartEvent + | BetaManagedAgentsSpanOutcomeEvaluationEndEvent | BetaManagedAgentsSpanModelRequestStartEvent | BetaManagedAgentsSpanModelRequestEndEvent - | BetaManagedAgentsSessionDeletedEvent; + | BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent + | BetaManagedAgentsUserDefineOutcomeEvent + | BetaManagedAgentsSessionDeletedEvent + | BetaManagedAgentsSessionThreadStatusRunningEvent + | BetaManagedAgentsSessionThreadStatusIdleEvent + | BetaManagedAgentsSessionThreadStatusTerminatedEvent + | BetaManagedAgentsSessionThreadStatusRescheduledEvent; /** * The agent is idle waiting on one or more blocking user-input events (tool @@ -812,6 +940,155 @@ export interface BetaManagedAgentsSessionStatusTerminatedEvent { type: 'session.status_terminated'; } +/** + * Emitted when a subagent is spawned as a new thread. Written to the parent + * thread's output stream so clients observing the session see child creation. + */ +export interface BetaManagedAgentsSessionThreadCreatedEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Name of the callable agent the thread runs. + */ + agent_name: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public `sthr_` ID of the newly created thread. + */ + session_thread_id: string; + + type: 'session.thread_created'; +} + +/** + * A session thread has yielded and is awaiting input. Emitted on the thread's own + * stream and cross-posted to the primary stream for child threads. + */ +export interface BetaManagedAgentsSessionThreadStatusIdleEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Name of the agent the thread runs. + */ + agent_name: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public sthr\_ ID of the thread that went idle. + */ + session_thread_id: string; + + /** + * The agent completed its turn naturally and is ready for the next user message. + */ + stop_reason: + | BetaManagedAgentsSessionEndTurn + | BetaManagedAgentsSessionRequiresAction + | BetaManagedAgentsSessionRetriesExhausted; + + type: 'session.thread_status_idle'; +} + +/** + * A session thread hit a transient error and is retrying automatically. Emitted on + * the thread's own stream and cross-posted to the primary stream for child + * threads. + */ +export interface BetaManagedAgentsSessionThreadStatusRescheduledEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Name of the agent the thread runs. + */ + agent_name: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public sthr\_ ID of the thread that is retrying. + */ + session_thread_id: string; + + type: 'session.thread_status_rescheduled'; +} + +/** + * A session thread has begun executing. Emitted on the thread's own stream and + * cross-posted to the primary stream for child threads. + */ +export interface BetaManagedAgentsSessionThreadStatusRunningEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Name of the agent the thread runs. + */ + agent_name: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public sthr\_ ID of the thread that started running. + */ + session_thread_id: string; + + type: 'session.thread_status_running'; +} + +/** + * A session thread has terminated and will accept no further input. Emitted on the + * thread's own stream and cross-posted to the primary stream for child threads. + */ +export interface BetaManagedAgentsSessionThreadStatusTerminatedEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Name of the agent the thread runs. + */ + agent_name: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Public sthr\_ ID of the thread that terminated. + */ + session_thread_id: string; + + type: 'session.thread_status_terminated'; +} + /** * Emitted when a model request completes. */ @@ -893,6 +1170,123 @@ export interface BetaManagedAgentsSpanModelUsage { speed?: 'standard' | 'fast' | null; } +/** + * Emitted when an outcome evaluation cycle completes. Carries the verdict and + * aggregate token usage. A verdict of `needs_revision` means another evaluation + * cycle follows; `satisfied`, `max_iterations_reached`, `failed`, or `interrupted` + * are terminal — no further evaluation cycles follow. + */ +export interface BetaManagedAgentsSpanOutcomeEvaluationEndEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * Human-readable explanation of the verdict. For `needs_revision`, describes which + * criteria failed and why. + */ + explanation: string; + + /** + * 0-indexed revision cycle, matching the corresponding + * `span.outcome_evaluation_start`. + */ + iteration: number; + + /** + * The id of the corresponding `span.outcome_evaluation_start` event. + */ + outcome_evaluation_start_id: string; + + /** + * The `outc_` ID of the outcome being evaluated. + */ + outcome_id: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Evaluation verdict. 'satisfied': criteria met, session goes idle. + * 'needs_revision': criteria not met, another revision cycle follows. + * 'max_iterations_reached': evaluation budget exhausted with criteria still unmet + * — one final acknowledgment turn follows before the session goes idle, but no + * further evaluation runs. 'failed': grader determined the rubric does not apply + * to the deliverables. 'interrupted': user sent an interrupt while evaluation was + * in progress. + */ + result: string; + + type: 'span.outcome_evaluation_end'; + + /** + * Token usage for a single model request. + */ + usage: BetaManagedAgentsSpanModelUsage; +} + +/** + * Periodic heartbeat emitted while an outcome evaluation cycle is in progress. + * Distinguishes 'evaluation is actively running' from 'evaluation is stuck' + * between the corresponding `span.outcome_evaluation_start` and + * `span.outcome_evaluation_end` events. + */ +export interface BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * 0-indexed revision cycle, matching the corresponding + * `span.outcome_evaluation_start`. + */ + iteration: number; + + /** + * The `outc_` ID of the outcome being evaluated. + */ + outcome_id: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + type: 'span.outcome_evaluation_ongoing'; +} + +/** + * Emitted when an outcome evaluation cycle begins. + */ +export interface BetaManagedAgentsSpanOutcomeEvaluationStartEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * 0-indexed revision cycle. 0 is the first evaluation; 1 is the re-evaluation + * after the first revision; etc. + */ + iteration: number; + + /** + * The `outc_` ID of the outcome being evaluated. + */ + outcome_id: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + type: 'span.outcome_evaluation_start'; +} + /** * Server-sent event in the session stream. */ @@ -908,15 +1302,26 @@ export type BetaManagedAgentsStreamSessionEvents = | BetaManagedAgentsAgentMCPToolResultEvent | BetaManagedAgentsAgentToolUseEvent | BetaManagedAgentsAgentToolResultEvent + | BetaManagedAgentsAgentThreadMessageReceivedEvent + | BetaManagedAgentsAgentThreadMessageSentEvent | BetaManagedAgentsAgentThreadContextCompactedEvent | BetaManagedAgentsSessionErrorEvent | BetaManagedAgentsSessionStatusRescheduledEvent | BetaManagedAgentsSessionStatusRunningEvent | BetaManagedAgentsSessionStatusIdleEvent | BetaManagedAgentsSessionStatusTerminatedEvent + | BetaManagedAgentsSessionThreadCreatedEvent + | BetaManagedAgentsSpanOutcomeEvaluationStartEvent + | BetaManagedAgentsSpanOutcomeEvaluationEndEvent | BetaManagedAgentsSpanModelRequestStartEvent | BetaManagedAgentsSpanModelRequestEndEvent - | BetaManagedAgentsSessionDeletedEvent; + | BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent + | BetaManagedAgentsUserDefineOutcomeEvent + | BetaManagedAgentsSessionDeletedEvent + | BetaManagedAgentsSessionThreadStatusRunningEvent + | BetaManagedAgentsSessionThreadStatusIdleEvent + | BetaManagedAgentsSessionThreadStatusTerminatedEvent + | BetaManagedAgentsSessionThreadStatusRescheduledEvent; /** * Regular text content. @@ -930,6 +1335,31 @@ export interface BetaManagedAgentsTextBlock { type: 'text'; } +/** + * Rubric content provided inline as text. + */ +export interface BetaManagedAgentsTextRubric { + /** + * Rubric content. Plain text or markdown — the grader treats it as freeform text. + */ + content: string; + + type: 'text'; +} + +/** + * Rubric content provided inline as text. + */ +export interface BetaManagedAgentsTextRubricParams { + /** + * Rubric content. Plain text or markdown — the grader treats it as freeform text. + * Maximum 262144 characters. + */ + content: string; + + type: 'text'; +} + /** * An unknown or unexpected error occurred during session execution. A fallback * variant; clients that don't recognize a new error code can match on @@ -1009,6 +1439,12 @@ export interface BetaManagedAgentsUserCustomToolResultEvent { * A timestamp in RFC 3339 format */ processed_at?: string | null; + + /** + * Routes this result to a subagent thread. Copy from the `agent.custom_tool_use` + * event's `session_thread_id`. + */ + session_thread_id?: string | null; } /** @@ -1036,6 +1472,68 @@ export interface BetaManagedAgentsUserCustomToolResultEventParams { is_error?: boolean | null; } +/** + * Echo of a `user.define_outcome` input event. Carries the server-generated + * `outcome_id` that subsequent `span.outcome_evaluation_*` events reference. + */ +export interface BetaManagedAgentsUserDefineOutcomeEvent { + /** + * Unique identifier for this event. + */ + id: string; + + /** + * What the agent should produce. Copied from the input event. + */ + description: string; + + /** + * Evaluate-then-revise cycles before giving up. Default 3, max 20. + */ + max_iterations: number | null; + + /** + * Server-generated `outc_` ID for this outcome. Referenced by + * `span.outcome_evaluation_*` events and the session's `outcome_evaluations` list. + */ + outcome_id: string; + + /** + * A timestamp in RFC 3339 format + */ + processed_at: string; + + /** + * Rubric for grading the quality of an outcome. + */ + rubric: BetaManagedAgentsFileRubric | BetaManagedAgentsTextRubric; + + type: 'user.define_outcome'; +} + +/** + * Parameters for defining an outcome the agent should work toward. The agent + * begins work on receipt. + */ +export interface BetaManagedAgentsUserDefineOutcomeEventParams { + /** + * What the agent should produce. This is the task specification. + */ + description: string; + + /** + * Rubric for grading the quality of an outcome. + */ + rubric: BetaManagedAgentsFileRubricParams | BetaManagedAgentsTextRubricParams; + + type: 'user.define_outcome'; + + /** + * Eval→revision cycles before giving up. Default 3, max 20. + */ + max_iterations?: number | null; +} + /** * An interrupt event that pauses agent execution and returns control to the user. */ @@ -1051,6 +1549,13 @@ export interface BetaManagedAgentsUserInterruptEvent { * A timestamp in RFC 3339 format */ processed_at?: string | null; + + /** + * If absent, interrupts every non-archived thread in a multiagent session (or the + * primary alone in a single-agent session). If present, interrupts only the named + * thread. + */ + session_thread_id?: string | null; } /** @@ -1058,6 +1563,13 @@ export interface BetaManagedAgentsUserInterruptEvent { */ export interface BetaManagedAgentsUserInterruptEventParams { type: 'user.interrupt'; + + /** + * If absent, interrupts every non-archived thread in a multiagent session (or the + * primary alone in a single-agent session). If present, interrupts only the named + * thread. + */ + session_thread_id?: string | null; } /** @@ -1128,6 +1640,13 @@ export interface BetaManagedAgentsUserToolConfirmationEvent { * A timestamp in RFC 3339 format */ processed_at?: string | null; + + /** + * When set, the confirmation routes to this subagent's thread rather than the + * primary. Echo this from the `session_thread_id` on the `agent.tool_use` or + * `agent.mcp_tool_use` event that prompted the approval. + */ + session_thread_id?: string | null; } /** @@ -1157,12 +1676,39 @@ export interface BetaManagedAgentsUserToolConfirmationEventParams { } export interface EventListParams extends PageCursorParams { + /** + * Query param: Return events created after this time (exclusive). + */ + 'created_at[gt]'?: string; + + /** + * Query param: Return events created at or after this time (inclusive). + */ + 'created_at[gte]'?: string; + + /** + * Query param: Return events created before this time (exclusive). + */ + 'created_at[lt]'?: string; + + /** + * Query param: Return events created at or before this time (inclusive). + */ + 'created_at[lte]'?: string; + /** * Query param: Sort direction for results, ordered by created_at. Defaults to asc * (chronological). */ order?: 'asc' | 'desc'; + /** + * Query param: Filter by event type. Values match the `type` field on returned + * events (for example, `user.message` or `agent.tool_use`). Omit to return all + * event types. + */ + types?: Array; + /** * Header param: Optional header to specify the beta version(s) you want to use. */ @@ -1196,6 +1742,8 @@ export declare namespace Events { type BetaManagedAgentsAgentMessageEvent as BetaManagedAgentsAgentMessageEvent, type BetaManagedAgentsAgentThinkingEvent as BetaManagedAgentsAgentThinkingEvent, type BetaManagedAgentsAgentThreadContextCompactedEvent as BetaManagedAgentsAgentThreadContextCompactedEvent, + type BetaManagedAgentsAgentThreadMessageReceivedEvent as BetaManagedAgentsAgentThreadMessageReceivedEvent, + type BetaManagedAgentsAgentThreadMessageSentEvent as BetaManagedAgentsAgentThreadMessageSentEvent, type BetaManagedAgentsAgentToolResultEvent as BetaManagedAgentsAgentToolResultEvent, type BetaManagedAgentsAgentToolUseEvent as BetaManagedAgentsAgentToolUseEvent, type BetaManagedAgentsBase64DocumentSource as BetaManagedAgentsBase64DocumentSource, @@ -1205,6 +1753,8 @@ export declare namespace Events { type BetaManagedAgentsEventParams as BetaManagedAgentsEventParams, type BetaManagedAgentsFileDocumentSource as BetaManagedAgentsFileDocumentSource, type BetaManagedAgentsFileImageSource as BetaManagedAgentsFileImageSource, + type BetaManagedAgentsFileRubric as BetaManagedAgentsFileRubric, + type BetaManagedAgentsFileRubricParams as BetaManagedAgentsFileRubricParams, type BetaManagedAgentsImageBlock as BetaManagedAgentsImageBlock, type BetaManagedAgentsMCPAuthenticationFailedError as BetaManagedAgentsMCPAuthenticationFailedError, type BetaManagedAgentsMCPConnectionFailedError as BetaManagedAgentsMCPConnectionFailedError, @@ -1226,16 +1776,28 @@ export declare namespace Events { type BetaManagedAgentsSessionStatusRescheduledEvent as BetaManagedAgentsSessionStatusRescheduledEvent, type BetaManagedAgentsSessionStatusRunningEvent as BetaManagedAgentsSessionStatusRunningEvent, type BetaManagedAgentsSessionStatusTerminatedEvent as BetaManagedAgentsSessionStatusTerminatedEvent, + type BetaManagedAgentsSessionThreadCreatedEvent as BetaManagedAgentsSessionThreadCreatedEvent, + type BetaManagedAgentsSessionThreadStatusIdleEvent as BetaManagedAgentsSessionThreadStatusIdleEvent, + type BetaManagedAgentsSessionThreadStatusRescheduledEvent as BetaManagedAgentsSessionThreadStatusRescheduledEvent, + type BetaManagedAgentsSessionThreadStatusRunningEvent as BetaManagedAgentsSessionThreadStatusRunningEvent, + type BetaManagedAgentsSessionThreadStatusTerminatedEvent as BetaManagedAgentsSessionThreadStatusTerminatedEvent, type BetaManagedAgentsSpanModelRequestEndEvent as BetaManagedAgentsSpanModelRequestEndEvent, type BetaManagedAgentsSpanModelRequestStartEvent as BetaManagedAgentsSpanModelRequestStartEvent, type BetaManagedAgentsSpanModelUsage as BetaManagedAgentsSpanModelUsage, + type BetaManagedAgentsSpanOutcomeEvaluationEndEvent as BetaManagedAgentsSpanOutcomeEvaluationEndEvent, + type BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent as BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent, + type BetaManagedAgentsSpanOutcomeEvaluationStartEvent as BetaManagedAgentsSpanOutcomeEvaluationStartEvent, type BetaManagedAgentsStreamSessionEvents as BetaManagedAgentsStreamSessionEvents, type BetaManagedAgentsTextBlock as BetaManagedAgentsTextBlock, + type BetaManagedAgentsTextRubric as BetaManagedAgentsTextRubric, + type BetaManagedAgentsTextRubricParams as BetaManagedAgentsTextRubricParams, type BetaManagedAgentsUnknownError as BetaManagedAgentsUnknownError, type BetaManagedAgentsURLDocumentSource as BetaManagedAgentsURLDocumentSource, type BetaManagedAgentsURLImageSource as BetaManagedAgentsURLImageSource, type BetaManagedAgentsUserCustomToolResultEvent as BetaManagedAgentsUserCustomToolResultEvent, type BetaManagedAgentsUserCustomToolResultEventParams as BetaManagedAgentsUserCustomToolResultEventParams, + type BetaManagedAgentsUserDefineOutcomeEvent as BetaManagedAgentsUserDefineOutcomeEvent, + type BetaManagedAgentsUserDefineOutcomeEventParams as BetaManagedAgentsUserDefineOutcomeEventParams, type BetaManagedAgentsUserInterruptEvent as BetaManagedAgentsUserInterruptEvent, type BetaManagedAgentsUserInterruptEventParams as BetaManagedAgentsUserInterruptEventParams, type BetaManagedAgentsUserMessageEvent as BetaManagedAgentsUserMessageEvent, diff --git a/src/resources/beta/sessions/index.ts b/src/resources/beta/sessions/index.ts index 0a93785a..4dbf9c74 100644 --- a/src/resources/beta/sessions/index.ts +++ b/src/resources/beta/sessions/index.ts @@ -8,6 +8,8 @@ export { type BetaManagedAgentsAgentMessageEvent, type BetaManagedAgentsAgentThinkingEvent, type BetaManagedAgentsAgentThreadContextCompactedEvent, + type BetaManagedAgentsAgentThreadMessageReceivedEvent, + type BetaManagedAgentsAgentThreadMessageSentEvent, type BetaManagedAgentsAgentToolResultEvent, type BetaManagedAgentsAgentToolUseEvent, type BetaManagedAgentsBase64DocumentSource, @@ -17,6 +19,8 @@ export { type BetaManagedAgentsEventParams, type BetaManagedAgentsFileDocumentSource, type BetaManagedAgentsFileImageSource, + type BetaManagedAgentsFileRubric, + type BetaManagedAgentsFileRubricParams, type BetaManagedAgentsImageBlock, type BetaManagedAgentsMCPAuthenticationFailedError, type BetaManagedAgentsMCPConnectionFailedError, @@ -38,16 +42,28 @@ export { type BetaManagedAgentsSessionStatusRescheduledEvent, type BetaManagedAgentsSessionStatusRunningEvent, type BetaManagedAgentsSessionStatusTerminatedEvent, + type BetaManagedAgentsSessionThreadCreatedEvent, + type BetaManagedAgentsSessionThreadStatusIdleEvent, + type BetaManagedAgentsSessionThreadStatusRescheduledEvent, + type BetaManagedAgentsSessionThreadStatusRunningEvent, + type BetaManagedAgentsSessionThreadStatusTerminatedEvent, type BetaManagedAgentsSpanModelRequestEndEvent, type BetaManagedAgentsSpanModelRequestStartEvent, type BetaManagedAgentsSpanModelUsage, + type BetaManagedAgentsSpanOutcomeEvaluationEndEvent, + type BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent, + type BetaManagedAgentsSpanOutcomeEvaluationStartEvent, type BetaManagedAgentsStreamSessionEvents, type BetaManagedAgentsTextBlock, + type BetaManagedAgentsTextRubric, + type BetaManagedAgentsTextRubricParams, type BetaManagedAgentsUnknownError, type BetaManagedAgentsURLDocumentSource, type BetaManagedAgentsURLImageSource, type BetaManagedAgentsUserCustomToolResultEvent, type BetaManagedAgentsUserCustomToolResultEventParams, + type BetaManagedAgentsUserDefineOutcomeEvent, + type BetaManagedAgentsUserDefineOutcomeEventParams, type BetaManagedAgentsUserInterruptEvent, type BetaManagedAgentsUserInterruptEventParams, type BetaManagedAgentsUserMessageEvent, @@ -85,8 +101,13 @@ export { type BetaManagedAgentsFileResourceParams, type BetaManagedAgentsGitHubRepositoryResourceParams, type BetaManagedAgentsMemoryStoreResourceParam, + type BetaManagedAgentsMultiagent, + type BetaManagedAgentsMultiagentParams, + type BetaManagedAgentsMultiagentRosterEntryParams, + type BetaManagedAgentsOutcomeEvaluationResource, type BetaManagedAgentsSession, type BetaManagedAgentsSessionAgent, + type BetaManagedAgentsSessionMultiagentCoordinator, type BetaManagedAgentsSessionStats, type BetaManagedAgentsSessionUsage, type SessionCreateParams, @@ -97,3 +118,16 @@ export { type SessionArchiveParams, type BetaManagedAgentsSessionsPageCursor, } from './sessions'; +export { + Threads, + type BetaManagedAgentsSessionThread, + type BetaManagedAgentsSessionThreadAgent, + type BetaManagedAgentsSessionThreadStats, + type BetaManagedAgentsSessionThreadStatus, + type BetaManagedAgentsSessionThreadUsage, + type BetaManagedAgentsStreamSessionThreadEvents, + type ThreadRetrieveParams, + type ThreadListParams, + type ThreadArchiveParams, + type BetaManagedAgentsSessionThreadsPageCursor, +} from './threads/index'; diff --git a/src/resources/beta/sessions/sessions.ts b/src/resources/beta/sessions/sessions.ts index 6641684f..8fc4f76d 100644 --- a/src/resources/beta/sessions/sessions.ts +++ b/src/resources/beta/sessions/sessions.ts @@ -11,6 +11,8 @@ import { BetaManagedAgentsAgentMessageEvent, BetaManagedAgentsAgentThinkingEvent, BetaManagedAgentsAgentThreadContextCompactedEvent, + BetaManagedAgentsAgentThreadMessageReceivedEvent, + BetaManagedAgentsAgentThreadMessageSentEvent, BetaManagedAgentsAgentToolResultEvent, BetaManagedAgentsAgentToolUseEvent, BetaManagedAgentsBase64DocumentSource, @@ -20,6 +22,8 @@ import { BetaManagedAgentsEventParams, BetaManagedAgentsFileDocumentSource, BetaManagedAgentsFileImageSource, + BetaManagedAgentsFileRubric, + BetaManagedAgentsFileRubricParams, BetaManagedAgentsImageBlock, BetaManagedAgentsMCPAuthenticationFailedError, BetaManagedAgentsMCPConnectionFailedError, @@ -42,16 +46,28 @@ import { BetaManagedAgentsSessionStatusRescheduledEvent, BetaManagedAgentsSessionStatusRunningEvent, BetaManagedAgentsSessionStatusTerminatedEvent, + BetaManagedAgentsSessionThreadCreatedEvent, + BetaManagedAgentsSessionThreadStatusIdleEvent, + BetaManagedAgentsSessionThreadStatusRescheduledEvent, + BetaManagedAgentsSessionThreadStatusRunningEvent, + BetaManagedAgentsSessionThreadStatusTerminatedEvent, BetaManagedAgentsSpanModelRequestEndEvent, BetaManagedAgentsSpanModelRequestStartEvent, BetaManagedAgentsSpanModelUsage, + BetaManagedAgentsSpanOutcomeEvaluationEndEvent, + BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent, + BetaManagedAgentsSpanOutcomeEvaluationStartEvent, BetaManagedAgentsStreamSessionEvents, BetaManagedAgentsTextBlock, + BetaManagedAgentsTextRubric, + BetaManagedAgentsTextRubricParams, BetaManagedAgentsURLDocumentSource, BetaManagedAgentsURLImageSource, BetaManagedAgentsUnknownError, BetaManagedAgentsUserCustomToolResultEvent, BetaManagedAgentsUserCustomToolResultEventParams, + BetaManagedAgentsUserDefineOutcomeEvent, + BetaManagedAgentsUserDefineOutcomeEventParams, BetaManagedAgentsUserInterruptEvent, BetaManagedAgentsUserInterruptEventParams, BetaManagedAgentsUserMessageEvent, @@ -80,6 +96,20 @@ import { ResourceUpdateResponse, Resources, } from './resources'; +import * as ThreadsAPI from './threads/threads'; +import { + BetaManagedAgentsSessionThread, + BetaManagedAgentsSessionThreadAgent, + BetaManagedAgentsSessionThreadStats, + BetaManagedAgentsSessionThreadStatus, + BetaManagedAgentsSessionThreadUsage, + BetaManagedAgentsSessionThreadsPageCursor, + BetaManagedAgentsStreamSessionThreadEvents, + ThreadArchiveParams, + ThreadListParams, + ThreadRetrieveParams, + Threads, +} from './threads/threads'; import { APIPromise } from '../../../core/api-promise'; import { PageCursor, type PageCursorParams, PagePromise } from '../../../core/pagination'; import { buildHeaders } from '../../../internal/headers'; @@ -89,6 +119,7 @@ import { path } from '../../../internal/utils/path'; export class Sessions extends APIResource { events: EventsAPI.Events = new EventsAPI.Events(this._client); resources: ResourcesAPI.Resources = new ResourcesAPI.Resources(this._client); + threads: ThreadsAPI.Threads = new ThreadsAPI.Threads(this._client); /** * Create Session @@ -377,6 +408,87 @@ export interface BetaManagedAgentsMemoryStoreResourceParam { instructions?: string | null; } +/** + * Resolved coordinator topology with a concrete agent roster. + */ +export interface BetaManagedAgentsMultiagent { + /** + * Agents the coordinator may spawn as session threads, each resolved to a specific + * version. + */ + agents: Array; + + type: 'coordinator'; +} + +/** + * A coordinator topology: the session's primary thread orchestrates work by + * spawning session threads, each running an agent drawn from the `agents` roster. + */ +export interface BetaManagedAgentsMultiagentParams { + /** + * Agents the coordinator may spawn as session threads. 1–20 entries. Each entry is + * an agent ID string, a versioned `{"type":"agent","id","version"}` reference, or + * `{"type":"self"}` to allow recursive self-invocation. Entries must reference + * distinct agents (after resolving `self` and string forms); at most one `self`. + * Referenced agents must exist, must not be archived, and must not themselves have + * `multiagent` set (depth limit 1). + */ + agents: Array; + + type: 'coordinator'; +} + +/** + * An entry in a multiagent roster: an agent ID string, a versioned agent + * reference, or `self`. + */ +export type BetaManagedAgentsMultiagentRosterEntryParams = + | string + | BetaManagedAgentsAgentParams + | AgentsAPI.BetaManagedAgentsMultiagentSelfParams; + +/** + * Evaluation state for a single outcome defined via a define_outcome event. + */ +export interface BetaManagedAgentsOutcomeEvaluationResource { + /** + * A timestamp in RFC 3339 format + */ + completed_at: string | null; + + /** + * What the agent should produce. + */ + description: string; + + /** + * Grader's verdict text from the most recent evaluation. For satisfied, explains + * why criteria are met; for needs_revision (intermediate), what's missing; for + * failed, why unrecoverable. + */ + explanation: string | null; + + /** + * 0-indexed revision cycle the outcome is currently on. + */ + iteration: number; + + /** + * Server-generated outc\_ ID for this outcome. + */ + outcome_id: string; + + /** + * Current evaluation state. 'pending' before the agent begins work; 'running' + * while producing or revising; 'evaluating' while the grader scores; + * 'satisfied'/'max_iterations_reached'/'failed'/'interrupted' are terminal. + */ + result: string; + + type: 'outcome_evaluation'; +} + /** * A Managed Agents `session`. */ @@ -403,6 +515,12 @@ export interface BetaManagedAgentsSession { metadata: { [key: string]: string }; + /** + * Per-outcome evaluation state. One entry per define_outcome event sent to the + * session. + */ + outcome_evaluations: Array; + resources: Array; /** @@ -452,6 +570,12 @@ export interface BetaManagedAgentsSessionAgent { */ model: AgentsAPI.BetaManagedAgentsModelConfig; + /** + * Resolved coordinator topology with full agent definitions for each roster + * member. + */ + multiagent: BetaManagedAgentsSessionMultiagentCoordinator | null; + name: string; skills: Array; @@ -469,6 +593,19 @@ export interface BetaManagedAgentsSessionAgent { version: number; } +/** + * Resolved coordinator topology with full agent definitions for each roster + * member. + */ +export interface BetaManagedAgentsSessionMultiagentCoordinator { + /** + * Full `agent` definitions the coordinator may spawn as session threads. + */ + agents: Array; + + type: 'coordinator'; +} + /** * Timing statistics for a session. */ @@ -638,6 +775,12 @@ export interface SessionListParams extends PageCursorParams { */ order?: 'asc' | 'desc'; + /** + * Query param: Filter by session status. Repeat the parameter to match any of + * multiple statuses. + */ + statuses?: Array<'rescheduling' | 'running' | 'idle' | 'terminated'>; + /** * Header param: Optional header to specify the beta version(s) you want to use. */ @@ -660,6 +803,7 @@ export interface SessionArchiveParams { Sessions.Events = Events; Sessions.Resources = Resources; +Sessions.Threads = Threads; export declare namespace Sessions { export { @@ -671,8 +815,13 @@ export declare namespace Sessions { type BetaManagedAgentsFileResourceParams as BetaManagedAgentsFileResourceParams, type BetaManagedAgentsGitHubRepositoryResourceParams as BetaManagedAgentsGitHubRepositoryResourceParams, type BetaManagedAgentsMemoryStoreResourceParam as BetaManagedAgentsMemoryStoreResourceParam, + type BetaManagedAgentsMultiagent as BetaManagedAgentsMultiagent, + type BetaManagedAgentsMultiagentParams as BetaManagedAgentsMultiagentParams, + type BetaManagedAgentsMultiagentRosterEntryParams as BetaManagedAgentsMultiagentRosterEntryParams, + type BetaManagedAgentsOutcomeEvaluationResource as BetaManagedAgentsOutcomeEvaluationResource, type BetaManagedAgentsSession as BetaManagedAgentsSession, type BetaManagedAgentsSessionAgent as BetaManagedAgentsSessionAgent, + type BetaManagedAgentsSessionMultiagentCoordinator as BetaManagedAgentsSessionMultiagentCoordinator, type BetaManagedAgentsSessionStats as BetaManagedAgentsSessionStats, type BetaManagedAgentsSessionUsage as BetaManagedAgentsSessionUsage, type BetaManagedAgentsSessionsPageCursor as BetaManagedAgentsSessionsPageCursor, @@ -692,6 +841,8 @@ export declare namespace Sessions { type BetaManagedAgentsAgentMessageEvent as BetaManagedAgentsAgentMessageEvent, type BetaManagedAgentsAgentThinkingEvent as BetaManagedAgentsAgentThinkingEvent, type BetaManagedAgentsAgentThreadContextCompactedEvent as BetaManagedAgentsAgentThreadContextCompactedEvent, + type BetaManagedAgentsAgentThreadMessageReceivedEvent as BetaManagedAgentsAgentThreadMessageReceivedEvent, + type BetaManagedAgentsAgentThreadMessageSentEvent as BetaManagedAgentsAgentThreadMessageSentEvent, type BetaManagedAgentsAgentToolResultEvent as BetaManagedAgentsAgentToolResultEvent, type BetaManagedAgentsAgentToolUseEvent as BetaManagedAgentsAgentToolUseEvent, type BetaManagedAgentsBase64DocumentSource as BetaManagedAgentsBase64DocumentSource, @@ -701,6 +852,8 @@ export declare namespace Sessions { type BetaManagedAgentsEventParams as BetaManagedAgentsEventParams, type BetaManagedAgentsFileDocumentSource as BetaManagedAgentsFileDocumentSource, type BetaManagedAgentsFileImageSource as BetaManagedAgentsFileImageSource, + type BetaManagedAgentsFileRubric as BetaManagedAgentsFileRubric, + type BetaManagedAgentsFileRubricParams as BetaManagedAgentsFileRubricParams, type BetaManagedAgentsImageBlock as BetaManagedAgentsImageBlock, type BetaManagedAgentsMCPAuthenticationFailedError as BetaManagedAgentsMCPAuthenticationFailedError, type BetaManagedAgentsMCPConnectionFailedError as BetaManagedAgentsMCPConnectionFailedError, @@ -722,16 +875,28 @@ export declare namespace Sessions { type BetaManagedAgentsSessionStatusRescheduledEvent as BetaManagedAgentsSessionStatusRescheduledEvent, type BetaManagedAgentsSessionStatusRunningEvent as BetaManagedAgentsSessionStatusRunningEvent, type BetaManagedAgentsSessionStatusTerminatedEvent as BetaManagedAgentsSessionStatusTerminatedEvent, + type BetaManagedAgentsSessionThreadCreatedEvent as BetaManagedAgentsSessionThreadCreatedEvent, + type BetaManagedAgentsSessionThreadStatusIdleEvent as BetaManagedAgentsSessionThreadStatusIdleEvent, + type BetaManagedAgentsSessionThreadStatusRescheduledEvent as BetaManagedAgentsSessionThreadStatusRescheduledEvent, + type BetaManagedAgentsSessionThreadStatusRunningEvent as BetaManagedAgentsSessionThreadStatusRunningEvent, + type BetaManagedAgentsSessionThreadStatusTerminatedEvent as BetaManagedAgentsSessionThreadStatusTerminatedEvent, type BetaManagedAgentsSpanModelRequestEndEvent as BetaManagedAgentsSpanModelRequestEndEvent, type BetaManagedAgentsSpanModelRequestStartEvent as BetaManagedAgentsSpanModelRequestStartEvent, type BetaManagedAgentsSpanModelUsage as BetaManagedAgentsSpanModelUsage, + type BetaManagedAgentsSpanOutcomeEvaluationEndEvent as BetaManagedAgentsSpanOutcomeEvaluationEndEvent, + type BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent as BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent, + type BetaManagedAgentsSpanOutcomeEvaluationStartEvent as BetaManagedAgentsSpanOutcomeEvaluationStartEvent, type BetaManagedAgentsStreamSessionEvents as BetaManagedAgentsStreamSessionEvents, type BetaManagedAgentsTextBlock as BetaManagedAgentsTextBlock, + type BetaManagedAgentsTextRubric as BetaManagedAgentsTextRubric, + type BetaManagedAgentsTextRubricParams as BetaManagedAgentsTextRubricParams, type BetaManagedAgentsUnknownError as BetaManagedAgentsUnknownError, type BetaManagedAgentsURLDocumentSource as BetaManagedAgentsURLDocumentSource, type BetaManagedAgentsURLImageSource as BetaManagedAgentsURLImageSource, type BetaManagedAgentsUserCustomToolResultEvent as BetaManagedAgentsUserCustomToolResultEvent, type BetaManagedAgentsUserCustomToolResultEventParams as BetaManagedAgentsUserCustomToolResultEventParams, + type BetaManagedAgentsUserDefineOutcomeEvent as BetaManagedAgentsUserDefineOutcomeEvent, + type BetaManagedAgentsUserDefineOutcomeEventParams as BetaManagedAgentsUserDefineOutcomeEventParams, type BetaManagedAgentsUserInterruptEvent as BetaManagedAgentsUserInterruptEvent, type BetaManagedAgentsUserInterruptEventParams as BetaManagedAgentsUserInterruptEventParams, type BetaManagedAgentsUserMessageEvent as BetaManagedAgentsUserMessageEvent, @@ -760,4 +925,18 @@ export declare namespace Sessions { type ResourceDeleteParams as ResourceDeleteParams, type ResourceAddParams as ResourceAddParams, }; + + export { + Threads as Threads, + type BetaManagedAgentsSessionThread as BetaManagedAgentsSessionThread, + type BetaManagedAgentsSessionThreadAgent as BetaManagedAgentsSessionThreadAgent, + type BetaManagedAgentsSessionThreadStats as BetaManagedAgentsSessionThreadStats, + type BetaManagedAgentsSessionThreadStatus as BetaManagedAgentsSessionThreadStatus, + type BetaManagedAgentsSessionThreadUsage as BetaManagedAgentsSessionThreadUsage, + type BetaManagedAgentsStreamSessionThreadEvents as BetaManagedAgentsStreamSessionThreadEvents, + type BetaManagedAgentsSessionThreadsPageCursor as BetaManagedAgentsSessionThreadsPageCursor, + type ThreadRetrieveParams as ThreadRetrieveParams, + type ThreadListParams as ThreadListParams, + type ThreadArchiveParams as ThreadArchiveParams, + }; } diff --git a/src/resources/beta/sessions/threads.ts b/src/resources/beta/sessions/threads.ts new file mode 100644 index 00000000..705f6701 --- /dev/null +++ b/src/resources/beta/sessions/threads.ts @@ -0,0 +1,3 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export * from './threads/index'; diff --git a/src/resources/beta/sessions/threads/events.ts b/src/resources/beta/sessions/threads/events.ts new file mode 100644 index 00000000..94334e4d --- /dev/null +++ b/src/resources/beta/sessions/threads/events.ts @@ -0,0 +1,107 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../../core/resource'; +import * as BetaAPI from '../../beta'; +import * as EventsAPI from '../events'; +import { BetaManagedAgentsSessionEventsPageCursor } from '../events'; +import * as ThreadsAPI from './threads'; +import { APIPromise } from '../../../../core/api-promise'; +import { PageCursor, type PageCursorParams, PagePromise } from '../../../../core/pagination'; +import { Stream } from '../../../../core/streaming'; +import { buildHeaders } from '../../../../internal/headers'; +import { RequestOptions } from '../../../../internal/request-options'; +import { path } from '../../../../internal/utils/path'; + +export class Events extends APIResource { + /** + * List Session Thread Events + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const betaManagedAgentsSessionEvent of client.beta.sessions.threads.events.list( + * 'sthr_011CZkZVWa6oIjw0rgXZpnBt', + * { session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7' }, + * )) { + * // ... + * } + * ``` + */ + list( + threadID: string, + params: EventListParams, + options?: RequestOptions, + ): PagePromise { + const { session_id, betas, ...query } = params; + return this._client.getAPIList( + path`/v1/sessions/${session_id}/threads/${threadID}/events?beta=true`, + PageCursor, + { + query, + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + }, + ); + } + + /** + * Stream Session Thread Events + * + * @example + * ```ts + * const betaManagedAgentsStreamSessionThreadEvents = + * await client.beta.sessions.threads.events.stream( + * 'sthr_011CZkZVWa6oIjw0rgXZpnBt', + * { session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7' }, + * ); + * ``` + */ + stream( + threadID: string, + params: EventStreamParams, + options?: RequestOptions, + ): APIPromise> { + const { session_id, betas } = params; + return this._client.get(path`/v1/sessions/${session_id}/threads/${threadID}/stream?beta=true`, { + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + stream: true, + }) as APIPromise>; + } +} + +export interface EventListParams extends PageCursorParams { + /** + * Path param: Path parameter session_id + */ + session_id: string; + + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + +export interface EventStreamParams { + /** + * Path param: Path parameter session_id + */ + session_id: string; + + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + +export declare namespace Events { + export { type EventListParams as EventListParams, type EventStreamParams as EventStreamParams }; +} + +export { type BetaManagedAgentsSessionEventsPageCursor }; diff --git a/src/resources/beta/sessions/threads/index.ts b/src/resources/beta/sessions/threads/index.ts new file mode 100644 index 00000000..ba1ec109 --- /dev/null +++ b/src/resources/beta/sessions/threads/index.ts @@ -0,0 +1,16 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +export { Events, type EventListParams, type EventStreamParams } from './events'; +export { + Threads, + type BetaManagedAgentsSessionThread, + type BetaManagedAgentsSessionThreadAgent, + type BetaManagedAgentsSessionThreadStats, + type BetaManagedAgentsSessionThreadStatus, + type BetaManagedAgentsSessionThreadUsage, + type BetaManagedAgentsStreamSessionThreadEvents, + type ThreadRetrieveParams, + type ThreadListParams, + type ThreadArchiveParams, + type BetaManagedAgentsSessionThreadsPageCursor, +} from './threads'; diff --git a/src/resources/beta/sessions/threads/threads.ts b/src/resources/beta/sessions/threads/threads.ts new file mode 100644 index 00000000..5a9f8ad7 --- /dev/null +++ b/src/resources/beta/sessions/threads/threads.ts @@ -0,0 +1,344 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../../../core/resource'; +import * as BetaAPI from '../../beta'; +import * as AgentsAPI from '../../agents/agents'; +import * as EventsAPI from '../events'; +import * as SessionsAPI from '../sessions'; +import * as ThreadsEventsAPI from './events'; +import { EventListParams, EventStreamParams, Events } from './events'; +import { APIPromise } from '../../../../core/api-promise'; +import { PageCursor, type PageCursorParams, PagePromise } from '../../../../core/pagination'; +import { buildHeaders } from '../../../../internal/headers'; +import { RequestOptions } from '../../../../internal/request-options'; +import { path } from '../../../../internal/utils/path'; + +export class Threads extends APIResource { + events: ThreadsEventsAPI.Events = new ThreadsEventsAPI.Events(this._client); + + /** + * Get Session Thread + * + * @example + * ```ts + * const betaManagedAgentsSessionThread = + * await client.beta.sessions.threads.retrieve( + * 'sthr_011CZkZVWa6oIjw0rgXZpnBt', + * { session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7' }, + * ); + * ``` + */ + retrieve( + threadID: string, + params: ThreadRetrieveParams, + options?: RequestOptions, + ): APIPromise { + const { session_id, betas } = params; + return this._client.get(path`/v1/sessions/${session_id}/threads/${threadID}?beta=true`, { + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + }); + } + + /** + * List Session Threads + * + * @example + * ```ts + * // Automatically fetches more pages as needed. + * for await (const betaManagedAgentsSessionThread of client.beta.sessions.threads.list( + * 'sesn_011CZkZAtmR3yMPDzynEDxu7', + * )) { + * // ... + * } + * ``` + */ + list( + sessionID: string, + params: ThreadListParams | null | undefined = {}, + options?: RequestOptions, + ): PagePromise { + const { betas, ...query } = params ?? {}; + return this._client.getAPIList( + path`/v1/sessions/${sessionID}/threads?beta=true`, + PageCursor, + { + query, + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + }, + ); + } + + /** + * Archive Session Thread + * + * @example + * ```ts + * const betaManagedAgentsSessionThread = + * await client.beta.sessions.threads.archive( + * 'sthr_011CZkZVWa6oIjw0rgXZpnBt', + * { session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7' }, + * ); + * ``` + */ + archive( + threadID: string, + params: ThreadArchiveParams, + options?: RequestOptions, + ): APIPromise { + const { session_id, betas } = params; + return this._client.post(path`/v1/sessions/${session_id}/threads/${threadID}/archive?beta=true`, { + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + }); + } +} + +export type BetaManagedAgentsSessionThreadsPageCursor = PageCursor; + +/** + * An execution thread within a `session`. Each session has one primary thread plus + * zero or more child threads spawned by the coordinator. + */ +export interface BetaManagedAgentsSessionThread { + /** + * Unique identifier for this thread. + */ + id: string; + + /** + * Resolved `agent` definition for a single `session_thread`. Snapshot of the agent + * at thread creation time. The multiagent roster is not repeated here; read it + * from `Session.agent`. + */ + agent: BetaManagedAgentsSessionThreadAgent; + + /** + * A timestamp in RFC 3339 format + */ + archived_at: string | null; + + /** + * A timestamp in RFC 3339 format + */ + created_at: string; + + /** + * Parent thread that spawned this thread. Null for the primary thread. + */ + parent_thread_id: string | null; + + /** + * The session this thread belongs to. + */ + session_id: string; + + /** + * Timing statistics for a session thread. + */ + stats: BetaManagedAgentsSessionThreadStats | null; + + /** + * SessionThreadStatus enum + */ + status: BetaManagedAgentsSessionThreadStatus; + + type: 'session_thread'; + + /** + * A timestamp in RFC 3339 format + */ + updated_at: string; + + /** + * Cumulative token usage for a session thread across all turns. + */ + usage: BetaManagedAgentsSessionThreadUsage | null; +} + +/** + * Resolved `agent` definition for a single `session_thread`. Snapshot of the agent + * at thread creation time. The multiagent roster is not repeated here; read it + * from `Session.agent`. + */ +export interface BetaManagedAgentsSessionThreadAgent { + id: string; + + description: string | null; + + mcp_servers: Array; + + /** + * Model identifier and configuration. + */ + model: AgentsAPI.BetaManagedAgentsModelConfig; + + name: string; + + skills: Array; + + system: string | null; + + tools: Array< + | AgentsAPI.BetaManagedAgentsAgentToolset20260401 + | AgentsAPI.BetaManagedAgentsMCPToolset + | AgentsAPI.BetaManagedAgentsCustomTool + >; + + type: 'agent'; + + version: number; +} + +/** + * Timing statistics for a session thread. + */ +export interface BetaManagedAgentsSessionThreadStats { + /** + * Cumulative time in seconds the thread spent actively running. Excludes idle + * time. + */ + active_seconds?: number; + + /** + * Elapsed time since thread creation in seconds. For archived threads, frozen at + * the final update. + */ + duration_seconds?: number; + + /** + * Time in seconds for the thread to begin running. Zero for child threads, which + * start immediately. + */ + startup_seconds?: number; +} + +/** + * SessionThreadStatus enum + */ +export type BetaManagedAgentsSessionThreadStatus = 'running' | 'idle' | 'rescheduling' | 'terminated'; + +/** + * Cumulative token usage for a session thread across all turns. + */ +export interface BetaManagedAgentsSessionThreadUsage { + /** + * Prompt-cache creation token usage broken down by cache lifetime. + */ + cache_creation?: SessionsAPI.BetaManagedAgentsCacheCreationUsage; + + /** + * Total tokens read from prompt cache. + */ + cache_read_input_tokens?: number; + + /** + * Total input tokens consumed across all turns. + */ + input_tokens?: number; + + /** + * Total output tokens generated across all turns. + */ + output_tokens?: number; +} + +/** + * Server-sent event in a single thread's stream. + */ +export type BetaManagedAgentsStreamSessionThreadEvents = + | EventsAPI.BetaManagedAgentsUserMessageEvent + | EventsAPI.BetaManagedAgentsUserInterruptEvent + | EventsAPI.BetaManagedAgentsUserToolConfirmationEvent + | EventsAPI.BetaManagedAgentsUserCustomToolResultEvent + | EventsAPI.BetaManagedAgentsAgentCustomToolUseEvent + | EventsAPI.BetaManagedAgentsAgentMessageEvent + | EventsAPI.BetaManagedAgentsAgentThinkingEvent + | EventsAPI.BetaManagedAgentsAgentMCPToolUseEvent + | EventsAPI.BetaManagedAgentsAgentMCPToolResultEvent + | EventsAPI.BetaManagedAgentsAgentToolUseEvent + | EventsAPI.BetaManagedAgentsAgentToolResultEvent + | EventsAPI.BetaManagedAgentsAgentThreadMessageReceivedEvent + | EventsAPI.BetaManagedAgentsAgentThreadMessageSentEvent + | EventsAPI.BetaManagedAgentsAgentThreadContextCompactedEvent + | EventsAPI.BetaManagedAgentsSessionErrorEvent + | EventsAPI.BetaManagedAgentsSessionStatusRescheduledEvent + | EventsAPI.BetaManagedAgentsSessionStatusRunningEvent + | EventsAPI.BetaManagedAgentsSessionStatusIdleEvent + | EventsAPI.BetaManagedAgentsSessionStatusTerminatedEvent + | EventsAPI.BetaManagedAgentsSessionThreadCreatedEvent + | EventsAPI.BetaManagedAgentsSpanOutcomeEvaluationStartEvent + | EventsAPI.BetaManagedAgentsSpanOutcomeEvaluationEndEvent + | EventsAPI.BetaManagedAgentsSpanModelRequestStartEvent + | EventsAPI.BetaManagedAgentsSpanModelRequestEndEvent + | EventsAPI.BetaManagedAgentsSpanOutcomeEvaluationOngoingEvent + | EventsAPI.BetaManagedAgentsUserDefineOutcomeEvent + | EventsAPI.BetaManagedAgentsSessionDeletedEvent + | EventsAPI.BetaManagedAgentsSessionThreadStatusRunningEvent + | EventsAPI.BetaManagedAgentsSessionThreadStatusIdleEvent + | EventsAPI.BetaManagedAgentsSessionThreadStatusTerminatedEvent + | EventsAPI.BetaManagedAgentsSessionThreadStatusRescheduledEvent; + +export interface ThreadRetrieveParams { + /** + * Path param: Path parameter session_id + */ + session_id: string; + + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + +export interface ThreadListParams extends PageCursorParams { + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + +export interface ThreadArchiveParams { + /** + * Path param: Path parameter session_id + */ + session_id: string; + + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + +Threads.Events = Events; + +export declare namespace Threads { + export { + type BetaManagedAgentsSessionThread as BetaManagedAgentsSessionThread, + type BetaManagedAgentsSessionThreadAgent as BetaManagedAgentsSessionThreadAgent, + type BetaManagedAgentsSessionThreadStats as BetaManagedAgentsSessionThreadStats, + type BetaManagedAgentsSessionThreadStatus as BetaManagedAgentsSessionThreadStatus, + type BetaManagedAgentsSessionThreadUsage as BetaManagedAgentsSessionThreadUsage, + type BetaManagedAgentsStreamSessionThreadEvents as BetaManagedAgentsStreamSessionThreadEvents, + type BetaManagedAgentsSessionThreadsPageCursor as BetaManagedAgentsSessionThreadsPageCursor, + type ThreadRetrieveParams as ThreadRetrieveParams, + type ThreadListParams as ThreadListParams, + type ThreadArchiveParams as ThreadArchiveParams, + }; + + export { + Events as Events, + type EventListParams as EventListParams, + type EventStreamParams as EventStreamParams, + }; +} diff --git a/src/resources/beta/user-profiles.ts b/src/resources/beta/user-profiles.ts index 7a6bdffb..991a4338 100644 --- a/src/resources/beta/user-profiles.ts +++ b/src/resources/beta/user-profiles.ts @@ -155,6 +155,13 @@ export interface BetaUserProfile { */ metadata: { [key: string]: string }; + /** + * How the entity behind a user profile relates to the platform that owns the API + * key. `external`: an individual end-user of the platform. `resold`: a company the + * platform resells Claude access to. `internal`: the platform's own usage. + */ + relationship: 'external' | 'resold' | 'internal'; + /** * Trust grants for this profile, keyed by grant name. Key omitted when no grant is * active or in flight. @@ -175,6 +182,12 @@ export interface BetaUserProfile { * Platform's own identifier for this user. Not enforced unique. */ external_id?: string | null; + + /** + * Display name of the entity this profile represents. For `resold` this is the + * resold-to company's name. + */ + name?: string | null; } export interface BetaUserProfileEnrollmentURL { @@ -215,6 +228,21 @@ export interface UserProfileCreateParams { */ metadata?: { [key: string]: string }; + /** + * Body param: Display name of the entity this profile represents. Required when + * relationship is `resold` (the resold-to company's name); optional otherwise. + * Maximum 255 characters. + */ + name?: string | null; + + /** + * Body param: How the entity behind a user profile relates to the platform that + * owns the API key. `external`: an individual end-user of the platform. `resold`: + * a company the platform resells Claude access to. `internal`: the platform's own + * usage. + */ + relationship?: 'external' | 'resold' | 'internal'; + /** * Header param: Optional header to specify the beta version(s) you want to use. */ @@ -243,6 +271,20 @@ export interface UserProfileUpdateParams { */ metadata?: { [key: string]: string }; + /** + * Body param: If present, replaces the stored name. Omit to leave unchanged. + * Maximum 255 characters. + */ + name?: string | null; + + /** + * Body param: How the entity behind a user profile relates to the platform that + * owns the API key. `external`: an individual end-user of the platform. `resold`: + * a company the platform resells Claude access to. `internal`: the platform's own + * usage. + */ + relationship?: 'external' | 'resold' | 'internal' | null; + /** * Header param: Optional header to specify the beta version(s) you want to use. */ diff --git a/src/resources/beta/vaults/credentials.ts b/src/resources/beta/vaults/credentials.ts index bf2c04ea..290b37b8 100644 --- a/src/resources/beta/vaults/credentials.ts +++ b/src/resources/beta/vaults/credentials.ts @@ -185,6 +185,36 @@ export class Credentials extends APIResource { ]), }); } + + /** + * Validate Credential + * + * @example + * ```ts + * const betaManagedAgentsCredentialValidation = + * await client.beta.vaults.credentials.mcpOAuthValidate( + * 'vcrd_011CZkZEMt8gZan2iYOQfSkw', + * { vault_id: 'vlt_011CZkZDLs7fYzm1hXNPeRjv' }, + * ); + * ``` + */ + mcpOAuthValidate( + credentialID: string, + params: CredentialMCPOAuthValidateParams, + options?: RequestOptions, + ): APIPromise { + const { vault_id, betas } = params; + return this._client.post( + path`/v1/vaults/${vault_id}/credentials/${credentialID}/mcp_oauth_validate?beta=true`, + { + ...options, + headers: buildHeaders([ + { 'anthropic-beta': [...(betas ?? []), 'managed-agents-2026-04-01'].toString() }, + options?.headers, + ]), + }, + ); + } } export type BetaManagedAgentsCredentialsPageCursor = PageCursor; @@ -237,6 +267,53 @@ export interface BetaManagedAgentsCredential { display_name?: string | null; } +/** + * Result of live-probing a credential against its configured MCP server. + */ +export interface BetaManagedAgentsCredentialValidation { + /** + * Unique identifier of the credential that was validated. + */ + credential_id: string; + + /** + * Whether the credential has a refresh token configured. + */ + has_refresh_token: boolean; + + /** + * The failing step of an MCP validation probe. + */ + mcp_probe: BetaManagedAgentsMCPProbe | null; + + /** + * Outcome of a refresh-token exchange attempted during credential validation. + */ + refresh: BetaManagedAgentsRefreshObject | null; + + /** + * Overall verdict of a credential validation probe. + */ + status: BetaManagedAgentsCredentialValidationStatus; + + type: 'vault_credential_validation'; + + /** + * A timestamp in RFC 3339 format + */ + validated_at: string; + + /** + * Identifier of the vault containing the credential. + */ + vault_id: string; +} + +/** + * Overall verdict of a credential validation probe. + */ +export type BetaManagedAgentsCredentialValidationStatus = 'valid' | 'invalid' | 'unknown'; + /** * Confirmation of a deleted credential. */ @@ -414,6 +491,61 @@ export interface BetaManagedAgentsMCPOAuthUpdateParams { refresh?: BetaManagedAgentsMCPOAuthRefreshUpdateParams | null; } +/** + * The failing step of an MCP validation probe. + */ +export interface BetaManagedAgentsMCPProbe { + /** + * An HTTP response captured during a credential validation probe. + */ + http_response: BetaManagedAgentsRefreshHTTPResponse | null; + + /** + * The MCP method that failed (for example `initialize` or `tools/list`). + */ + method: string; +} + +/** + * An HTTP response captured during a credential validation probe. + */ +export interface BetaManagedAgentsRefreshHTTPResponse { + /** + * Response body. May be truncated and has sensitive values scrubbed. + */ + body: string; + + /** + * Whether `body` was truncated. + */ + body_truncated: boolean; + + /** + * Value of the `Content-Type` response header. + */ + content_type: string; + + /** + * HTTP status code. + */ + status_code: number; +} + +/** + * Outcome of a refresh-token exchange attempted during credential validation. + */ +export interface BetaManagedAgentsRefreshObject { + /** + * An HTTP response captured during a credential validation probe. + */ + http_response: BetaManagedAgentsRefreshHTTPResponse | null; + + /** + * Outcome of a refresh-token exchange attempted during credential validation. + */ + status: 'succeeded' | 'failed' | 'connect_error' | 'no_refresh_token'; +} + /** * Static bearer token credential details for an MCP server. */ @@ -631,9 +763,23 @@ export interface CredentialArchiveParams { betas?: Array; } +export interface CredentialMCPOAuthValidateParams { + /** + * Path param: Path parameter vault_id + */ + vault_id: string; + + /** + * Header param: Optional header to specify the beta version(s) you want to use. + */ + betas?: Array; +} + export declare namespace Credentials { export { type BetaManagedAgentsCredential as BetaManagedAgentsCredential, + type BetaManagedAgentsCredentialValidation as BetaManagedAgentsCredentialValidation, + type BetaManagedAgentsCredentialValidationStatus as BetaManagedAgentsCredentialValidationStatus, type BetaManagedAgentsDeletedCredential as BetaManagedAgentsDeletedCredential, type BetaManagedAgentsMCPOAuthAuthResponse as BetaManagedAgentsMCPOAuthAuthResponse, type BetaManagedAgentsMCPOAuthCreateParams as BetaManagedAgentsMCPOAuthCreateParams, @@ -641,6 +787,9 @@ export declare namespace Credentials { type BetaManagedAgentsMCPOAuthRefreshResponse as BetaManagedAgentsMCPOAuthRefreshResponse, type BetaManagedAgentsMCPOAuthRefreshUpdateParams as BetaManagedAgentsMCPOAuthRefreshUpdateParams, type BetaManagedAgentsMCPOAuthUpdateParams as BetaManagedAgentsMCPOAuthUpdateParams, + type BetaManagedAgentsMCPProbe as BetaManagedAgentsMCPProbe, + type BetaManagedAgentsRefreshHTTPResponse as BetaManagedAgentsRefreshHTTPResponse, + type BetaManagedAgentsRefreshObject as BetaManagedAgentsRefreshObject, type BetaManagedAgentsStaticBearerAuthResponse as BetaManagedAgentsStaticBearerAuthResponse, type BetaManagedAgentsStaticBearerCreateParams as BetaManagedAgentsStaticBearerCreateParams, type BetaManagedAgentsStaticBearerUpdateParams as BetaManagedAgentsStaticBearerUpdateParams, @@ -659,5 +808,6 @@ export declare namespace Credentials { type CredentialListParams as CredentialListParams, type CredentialDeleteParams as CredentialDeleteParams, type CredentialArchiveParams as CredentialArchiveParams, + type CredentialMCPOAuthValidateParams as CredentialMCPOAuthValidateParams, }; } diff --git a/src/resources/beta/vaults/index.ts b/src/resources/beta/vaults/index.ts index d294ba3a..665871a9 100644 --- a/src/resources/beta/vaults/index.ts +++ b/src/resources/beta/vaults/index.ts @@ -3,6 +3,8 @@ export { Credentials, type BetaManagedAgentsCredential, + type BetaManagedAgentsCredentialValidation, + type BetaManagedAgentsCredentialValidationStatus, type BetaManagedAgentsDeletedCredential, type BetaManagedAgentsMCPOAuthAuthResponse, type BetaManagedAgentsMCPOAuthCreateParams, @@ -10,6 +12,9 @@ export { type BetaManagedAgentsMCPOAuthRefreshResponse, type BetaManagedAgentsMCPOAuthRefreshUpdateParams, type BetaManagedAgentsMCPOAuthUpdateParams, + type BetaManagedAgentsMCPProbe, + type BetaManagedAgentsRefreshHTTPResponse, + type BetaManagedAgentsRefreshObject, type BetaManagedAgentsStaticBearerAuthResponse, type BetaManagedAgentsStaticBearerCreateParams, type BetaManagedAgentsStaticBearerUpdateParams, @@ -27,6 +32,7 @@ export { type CredentialListParams, type CredentialDeleteParams, type CredentialArchiveParams, + type CredentialMCPOAuthValidateParams, type BetaManagedAgentsCredentialsPageCursor, } from './credentials'; export { diff --git a/src/resources/beta/vaults/vaults.ts b/src/resources/beta/vaults/vaults.ts index 8884e421..c512c988 100644 --- a/src/resources/beta/vaults/vaults.ts +++ b/src/resources/beta/vaults/vaults.ts @@ -5,6 +5,8 @@ import * as BetaAPI from '../beta'; import * as CredentialsAPI from './credentials'; import { BetaManagedAgentsCredential, + BetaManagedAgentsCredentialValidation, + BetaManagedAgentsCredentialValidationStatus, BetaManagedAgentsCredentialsPageCursor, BetaManagedAgentsDeletedCredential, BetaManagedAgentsMCPOAuthAuthResponse, @@ -13,6 +15,9 @@ import { BetaManagedAgentsMCPOAuthRefreshResponse, BetaManagedAgentsMCPOAuthRefreshUpdateParams, BetaManagedAgentsMCPOAuthUpdateParams, + BetaManagedAgentsMCPProbe, + BetaManagedAgentsRefreshHTTPResponse, + BetaManagedAgentsRefreshObject, BetaManagedAgentsStaticBearerAuthResponse, BetaManagedAgentsStaticBearerCreateParams, BetaManagedAgentsStaticBearerUpdateParams, @@ -28,6 +33,7 @@ import { CredentialCreateParams, CredentialDeleteParams, CredentialListParams, + CredentialMCPOAuthValidateParams, CredentialRetrieveParams, CredentialUpdateParams, Credentials, @@ -334,6 +340,8 @@ export declare namespace Vaults { export { Credentials as Credentials, type BetaManagedAgentsCredential as BetaManagedAgentsCredential, + type BetaManagedAgentsCredentialValidation as BetaManagedAgentsCredentialValidation, + type BetaManagedAgentsCredentialValidationStatus as BetaManagedAgentsCredentialValidationStatus, type BetaManagedAgentsDeletedCredential as BetaManagedAgentsDeletedCredential, type BetaManagedAgentsMCPOAuthAuthResponse as BetaManagedAgentsMCPOAuthAuthResponse, type BetaManagedAgentsMCPOAuthCreateParams as BetaManagedAgentsMCPOAuthCreateParams, @@ -341,6 +349,9 @@ export declare namespace Vaults { type BetaManagedAgentsMCPOAuthRefreshResponse as BetaManagedAgentsMCPOAuthRefreshResponse, type BetaManagedAgentsMCPOAuthRefreshUpdateParams as BetaManagedAgentsMCPOAuthRefreshUpdateParams, type BetaManagedAgentsMCPOAuthUpdateParams as BetaManagedAgentsMCPOAuthUpdateParams, + type BetaManagedAgentsMCPProbe as BetaManagedAgentsMCPProbe, + type BetaManagedAgentsRefreshHTTPResponse as BetaManagedAgentsRefreshHTTPResponse, + type BetaManagedAgentsRefreshObject as BetaManagedAgentsRefreshObject, type BetaManagedAgentsStaticBearerAuthResponse as BetaManagedAgentsStaticBearerAuthResponse, type BetaManagedAgentsStaticBearerCreateParams as BetaManagedAgentsStaticBearerCreateParams, type BetaManagedAgentsStaticBearerUpdateParams as BetaManagedAgentsStaticBearerUpdateParams, @@ -359,5 +370,6 @@ export declare namespace Vaults { type CredentialListParams as CredentialListParams, type CredentialDeleteParams as CredentialDeleteParams, type CredentialArchiveParams as CredentialArchiveParams, + type CredentialMCPOAuthValidateParams as CredentialMCPOAuthValidateParams, }; } diff --git a/src/resources/beta/webhooks.ts b/src/resources/beta/webhooks.ts new file mode 100644 index 00000000..29839534 --- /dev/null +++ b/src/resources/beta/webhooks.ts @@ -0,0 +1,417 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { APIResource } from '../../core/resource'; +import { Webhook } from 'standardwebhooks'; + +export class Webhooks extends APIResource { + unwrap( + body: string, + { headers, key }: { headers: Record; key?: string }, + ): UnwrapWebhookEvent { + if (headers !== undefined) { + const keyStr: string | null = key === undefined ? this._client.webhookKey : key; + if (keyStr === null) throw new Error('Webhook key must not be null in order to unwrap'); + const wh = new Webhook(keyStr); + wh.verify(body, headers); + } + return JSON.parse(body) as UnwrapWebhookEvent; + } +} + +export interface BetaWebhookEvent { + /** + * Unique event identifier for idempotency. + */ + id: string; + + /** + * RFC 3339 timestamp when the event occurred. + */ + created_at: string; + + data: BetaWebhookEventData; + + /** + * Object type. Always `event` for webhook payloads. + */ + type: 'event'; +} + +export type BetaWebhookEventData = + | BetaWebhookSessionCreatedEventData + | BetaWebhookSessionPendingEventData + | BetaWebhookSessionRunningEventData + | BetaWebhookSessionIdledEventData + | BetaWebhookSessionRequiresActionEventData + | BetaWebhookSessionArchivedEventData + | BetaWebhookSessionDeletedEventData + | BetaWebhookSessionStatusRescheduledEventData + | BetaWebhookSessionStatusRunStartedEventData + | BetaWebhookSessionStatusIdledEventData + | BetaWebhookSessionStatusTerminatedEventData + | BetaWebhookSessionThreadCreatedEventData + | BetaWebhookSessionThreadIdledEventData + | BetaWebhookSessionThreadTerminatedEventData + | BetaWebhookSessionOutcomeEvaluationEndedEventData + | BetaWebhookVaultCreatedEventData + | BetaWebhookVaultArchivedEventData + | BetaWebhookVaultDeletedEventData + | BetaWebhookVaultCredentialCreatedEventData + | BetaWebhookVaultCredentialArchivedEventData + | BetaWebhookVaultCredentialDeletedEventData + | BetaWebhookVaultCredentialRefreshFailedEventData; + +export interface BetaWebhookSessionArchivedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.archived'; + + workspace_id: string; +} + +export interface BetaWebhookSessionCreatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.created'; + + workspace_id: string; +} + +export interface BetaWebhookSessionDeletedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.deleted'; + + workspace_id: string; +} + +export interface BetaWebhookSessionIdledEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.idled'; + + workspace_id: string; +} + +export interface BetaWebhookSessionOutcomeEvaluationEndedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.outcome_evaluation_ended'; + + workspace_id: string; +} + +export interface BetaWebhookSessionPendingEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.pending'; + + workspace_id: string; +} + +export interface BetaWebhookSessionRequiresActionEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.requires_action'; + + workspace_id: string; +} + +export interface BetaWebhookSessionRunningEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.running'; + + workspace_id: string; +} + +export interface BetaWebhookSessionStatusIdledEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.status_idled'; + + workspace_id: string; +} + +export interface BetaWebhookSessionStatusRescheduledEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.status_rescheduled'; + + workspace_id: string; +} + +export interface BetaWebhookSessionStatusRunStartedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.status_run_started'; + + workspace_id: string; +} + +export interface BetaWebhookSessionStatusTerminatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.status_terminated'; + + workspace_id: string; +} + +export interface BetaWebhookSessionThreadCreatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.thread_created'; + + workspace_id: string; +} + +export interface BetaWebhookSessionThreadIdledEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.thread_idled'; + + workspace_id: string; +} + +export interface BetaWebhookSessionThreadTerminatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'session.thread_terminated'; + + workspace_id: string; +} + +export interface BetaWebhookVaultArchivedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault.archived'; + + workspace_id: string; +} + +export interface BetaWebhookVaultCreatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault.created'; + + workspace_id: string; +} + +export interface BetaWebhookVaultCredentialArchivedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault_credential.archived'; + + /** + * ID of the vault that owns this credential. + */ + vault_id: string; + + workspace_id: string; +} + +export interface BetaWebhookVaultCredentialCreatedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault_credential.created'; + + /** + * ID of the vault that owns this credential. + */ + vault_id: string; + + workspace_id: string; +} + +export interface BetaWebhookVaultCredentialDeletedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault_credential.deleted'; + + /** + * ID of the vault that owns this credential. + */ + vault_id: string; + + workspace_id: string; +} + +export interface BetaWebhookVaultCredentialRefreshFailedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault_credential.refresh_failed'; + + /** + * ID of the vault that owns this credential. + */ + vault_id: string; + + workspace_id: string; +} + +export interface BetaWebhookVaultDeletedEventData { + /** + * ID of the resource that triggered the event. + */ + id: string; + + organization_id: string; + + type: 'vault.deleted'; + + workspace_id: string; +} + +export interface UnwrapWebhookEvent { + /** + * Unique event identifier for idempotency. + */ + id: string; + + /** + * RFC 3339 timestamp when the event occurred. + */ + created_at: string; + + data: BetaWebhookEventData; + + /** + * Object type. Always `event` for webhook payloads. + */ + type: 'event'; +} + +export declare namespace Webhooks { + export { + type BetaWebhookEvent as BetaWebhookEvent, + type BetaWebhookEventData as BetaWebhookEventData, + type BetaWebhookSessionArchivedEventData as BetaWebhookSessionArchivedEventData, + type BetaWebhookSessionCreatedEventData as BetaWebhookSessionCreatedEventData, + type BetaWebhookSessionDeletedEventData as BetaWebhookSessionDeletedEventData, + type BetaWebhookSessionIdledEventData as BetaWebhookSessionIdledEventData, + type BetaWebhookSessionOutcomeEvaluationEndedEventData as BetaWebhookSessionOutcomeEvaluationEndedEventData, + type BetaWebhookSessionPendingEventData as BetaWebhookSessionPendingEventData, + type BetaWebhookSessionRequiresActionEventData as BetaWebhookSessionRequiresActionEventData, + type BetaWebhookSessionRunningEventData as BetaWebhookSessionRunningEventData, + type BetaWebhookSessionStatusIdledEventData as BetaWebhookSessionStatusIdledEventData, + type BetaWebhookSessionStatusRescheduledEventData as BetaWebhookSessionStatusRescheduledEventData, + type BetaWebhookSessionStatusRunStartedEventData as BetaWebhookSessionStatusRunStartedEventData, + type BetaWebhookSessionStatusTerminatedEventData as BetaWebhookSessionStatusTerminatedEventData, + type BetaWebhookSessionThreadCreatedEventData as BetaWebhookSessionThreadCreatedEventData, + type BetaWebhookSessionThreadIdledEventData as BetaWebhookSessionThreadIdledEventData, + type BetaWebhookSessionThreadTerminatedEventData as BetaWebhookSessionThreadTerminatedEventData, + type BetaWebhookVaultArchivedEventData as BetaWebhookVaultArchivedEventData, + type BetaWebhookVaultCreatedEventData as BetaWebhookVaultCreatedEventData, + type BetaWebhookVaultCredentialArchivedEventData as BetaWebhookVaultCredentialArchivedEventData, + type BetaWebhookVaultCredentialCreatedEventData as BetaWebhookVaultCredentialCreatedEventData, + type BetaWebhookVaultCredentialDeletedEventData as BetaWebhookVaultCredentialDeletedEventData, + type BetaWebhookVaultCredentialRefreshFailedEventData as BetaWebhookVaultCredentialRefreshFailedEventData, + type BetaWebhookVaultDeletedEventData as BetaWebhookVaultDeletedEventData, + type UnwrapWebhookEvent as UnwrapWebhookEvent, + }; +} diff --git a/src/resources/messages/messages.ts b/src/resources/messages/messages.ts index 3d732535..d75aafac 100644 --- a/src/resources/messages/messages.ts +++ b/src/resources/messages/messages.ts @@ -337,30 +337,66 @@ export interface CitationCharLocationParam { } export interface CitationContentBlockLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; file_id: string | null; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; } export interface CitationContentBlockLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; document_index: number; document_title: string | null; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; type: 'content_block_location'; @@ -397,14 +433,40 @@ export interface CitationPageLocationParam { } export interface CitationSearchResultLocationParam { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; @@ -444,14 +506,40 @@ export interface CitationsDelta { } export interface CitationsSearchResultLocation { + /** + * The full text of the cited block range, concatenated. + * + * Always equals the contents of `content[start_block_index:end_block_index]` + * joined together. The text block is the minimal citable unit; this field is never + * a substring of a single block. Not counted toward output tokens, and not counted + * toward input tokens when sent back in subsequent turns. + */ cited_text: string; + /** + * Exclusive 0-based end index of the cited block range in the source's `content` + * array. + * + * Always greater than `start_block_index`; a single-block citation has + * `end_block_index = start_block_index + 1`. + */ end_block_index: number; + /** + * 0-based index of the cited search result among all `search_result` content + * blocks in the request, in the order they appear across messages and tool + * results. + * + * Counted separately from `document_index`; server-side web search results are not + * included in this count. + */ search_result_index: number; source: string; + /** + * 0-based index of the first cited block in the source's `content` array. + */ start_block_index: number; title: string | null; diff --git a/src/version.ts b/src/version.ts index a914c676..d1d95222 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const VERSION = '0.94.0'; // x-release-please-version +export const VERSION = '0.95.0'; // x-release-please-version diff --git a/tests/api-resources/beta/agents/agents.test.ts b/tests/api-resources/beta/agents/agents.test.ts index e385d41f..06aa50da 100644 --- a/tests/api-resources/beta/agents/agents.test.ts +++ b/tests/api-resources/beta/agents/agents.test.ts @@ -32,6 +32,7 @@ describe('resource agents', () => { }, ], metadata: { foo: 'bar' }, + multiagent: { agents: ['agent_011CZkYqphY8vELVzwCUpqiQ', { type: 'self' }], type: 'coordinator' }, skills: [ { skill_id: 'xlsx', @@ -109,6 +110,7 @@ describe('resource agents', () => { ], metadata: { foo: 'string' }, model: { id: 'claude-opus-4-6', speed: 'standard' }, + multiagent: { agents: ['agent_011CZkYqphY8vELVzwCUpqiQ', { type: 'self' }], type: 'coordinator' }, name: 'name', skills: [ { diff --git a/tests/api-resources/beta/sessions/events.test.ts b/tests/api-resources/beta/sessions/events.test.ts index 7b360419..d56aea41 100644 --- a/tests/api-resources/beta/sessions/events.test.ts +++ b/tests/api-resources/beta/sessions/events.test.ts @@ -27,9 +27,14 @@ describe('resource events', () => { client.beta.sessions.events.list( 'sesn_011CZkZAtmR3yMPDzynEDxu7', { + 'created_at[gt]': '2019-12-27T18:11:19.117Z', + 'created_at[gte]': '2019-12-27T18:11:19.117Z', + 'created_at[lt]': '2019-12-27T18:11:19.117Z', + 'created_at[lte]': '2019-12-27T18:11:19.117Z', limit: 0, order: 'asc', page: 'page', + types: ['string'], betas: ['message-batches-2024-09-24'], }, { path: '/_stainless_unknown_path' }, diff --git a/tests/api-resources/beta/sessions/sessions.test.ts b/tests/api-resources/beta/sessions/sessions.test.ts index 4b77eb3e..ea937a18 100644 --- a/tests/api-resources/beta/sessions/sessions.test.ts +++ b/tests/api-resources/beta/sessions/sessions.test.ts @@ -102,6 +102,7 @@ describe('resource sessions', () => { memory_store_id: 'memory_store_id', order: 'asc', page: 'page', + statuses: ['rescheduling'], betas: ['message-batches-2024-09-24'], }, { path: '/_stainless_unknown_path' }, diff --git a/tests/api-resources/beta/sessions/threads/events.test.ts b/tests/api-resources/beta/sessions/threads/events.test.ts new file mode 100644 index 00000000..257013c0 --- /dev/null +++ b/tests/api-resources/beta/sessions/threads/events.test.ts @@ -0,0 +1,54 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ + apiKey: 'my-anthropic-api-key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource events', () => { + // buildURL drops path-level query params (SDK-4349) + test.skip('list: only required params', async () => { + const responsePromise = client.beta.sessions.threads.events.list('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // buildURL drops path-level query params (SDK-4349) + test.skip('list: required and optional params', async () => { + const response = await client.beta.sessions.threads.events.list('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + limit: 0, + page: 'page', + betas: ['message-batches-2024-09-24'], + }); + }); + + test('stream: only required params', async () => { + const responsePromise = client.beta.sessions.threads.events.stream('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('stream: required and optional params', async () => { + const response = await client.beta.sessions.threads.events.stream('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + betas: ['message-batches-2024-09-24'], + }); + }); +}); diff --git a/tests/api-resources/beta/sessions/threads/threads.test.ts b/tests/api-resources/beta/sessions/threads/threads.test.ts new file mode 100644 index 00000000..76d0375f --- /dev/null +++ b/tests/api-resources/beta/sessions/threads/threads.test.ts @@ -0,0 +1,78 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ + apiKey: 'my-anthropic-api-key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource threads', () => { + test('retrieve: only required params', async () => { + const responsePromise = client.beta.sessions.threads.retrieve('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('retrieve: required and optional params', async () => { + const response = await client.beta.sessions.threads.retrieve('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + betas: ['message-batches-2024-09-24'], + }); + }); + + // buildURL drops path-level query params (SDK-4349) + test.skip('list', async () => { + const responsePromise = client.beta.sessions.threads.list('sesn_011CZkZAtmR3yMPDzynEDxu7'); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // buildURL drops path-level query params (SDK-4349) + test.skip('list: request options and params are passed correctly', async () => { + // ensure the request options are being passed correctly by passing an invalid HTTP method in order to cause an error + await expect( + client.beta.sessions.threads.list( + 'sesn_011CZkZAtmR3yMPDzynEDxu7', + { + limit: 0, + page: 'page', + betas: ['message-batches-2024-09-24'], + }, + { path: '/_stainless_unknown_path' }, + ), + ).rejects.toThrow(Anthropic.NotFoundError); + }); + + test('archive: only required params', async () => { + const responsePromise = client.beta.sessions.threads.archive('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + test('archive: required and optional params', async () => { + const response = await client.beta.sessions.threads.archive('sthr_011CZkZVWa6oIjw0rgXZpnBt', { + session_id: 'sesn_011CZkZAtmR3yMPDzynEDxu7', + betas: ['message-batches-2024-09-24'], + }); + }); +}); diff --git a/tests/api-resources/beta/vaults/credentials.test.ts b/tests/api-resources/beta/vaults/credentials.test.ts index 39c5f1dc..4f46da22 100644 --- a/tests/api-resources/beta/vaults/credentials.test.ts +++ b/tests/api-resources/beta/vaults/credentials.test.ts @@ -158,4 +158,26 @@ describe('resource credentials', () => { betas: ['message-batches-2024-09-24'], }); }); + + // prism can't find endpoint with beta only tag + test.skip('mcpOAuthValidate: only required params', async () => { + const responsePromise = client.beta.vaults.credentials.mcpOAuthValidate('vcrd_011CZkZEMt8gZan2iYOQfSkw', { + vault_id: 'vlt_011CZkZDLs7fYzm1hXNPeRjv', + }); + const rawResponse = await responsePromise.asResponse(); + expect(rawResponse).toBeInstanceOf(Response); + const response = await responsePromise; + expect(response).not.toBeInstanceOf(Response); + const dataAndResponse = await responsePromise.withResponse(); + expect(dataAndResponse.data).toBe(response); + expect(dataAndResponse.response).toBe(rawResponse); + }); + + // prism can't find endpoint with beta only tag + test.skip('mcpOAuthValidate: required and optional params', async () => { + const response = await client.beta.vaults.credentials.mcpOAuthValidate('vcrd_011CZkZEMt8gZan2iYOQfSkw', { + vault_id: 'vlt_011CZkZDLs7fYzm1hXNPeRjv', + betas: ['message-batches-2024-09-24'], + }); + }); }); diff --git a/tests/api-resources/beta/webhooks.test.ts b/tests/api-resources/beta/webhooks.test.ts new file mode 100644 index 00000000..21dccd08 --- /dev/null +++ b/tests/api-resources/beta/webhooks.test.ts @@ -0,0 +1,64 @@ +// File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +import { Webhook } from 'standardwebhooks'; + +import Anthropic from '@anthropic-ai/sdk'; + +const client = new Anthropic({ + apiKey: 'my-anthropic-api-key', + baseURL: process.env['TEST_API_BASE_URL'] ?? 'http://127.0.0.1:4010', +}); + +describe('resource webhooks', () => { + test('unwrap', () => { + const key = 'whsec_c2VjcmV0Cg=='; + const payload = + '{"id":"wevt_011CZkZYZd9rLmz3ujAcsqEw","created_at":"2026-03-15T10:00:00Z","data":{"id":"sesn_011CZkZAtmR3yMPDzynEDxu7","organization_id":"org_011CZkZZAe0sMna4vkBdtrfx","type":"session.status_idled","workspace_id":"wrkspc_011CZkZaBF1tNoB5wlCeusgy"},"type":"event"}'; + const msgID = '1'; + const timestamp = new Date(); + const wh = new Webhook('whsec_c2VjcmV0Cg=='); + const signature = wh.sign(msgID, timestamp, payload); + const headers: Record = { + 'webhook-signature': signature, + 'webhook-id': msgID, + 'webhook-timestamp': String(Math.floor(timestamp.getTime() / 1000)), + }; + client.beta.webhooks.unwrap(payload, { headers, key }); + client.withOptions({ webhookKey: key }).beta.webhooks.unwrap(payload, { headers }); + client.withOptions({ webhookKey: 'whsec_aaaaaaaaaa==' }).beta.webhooks.unwrap(payload, { headers, key }); + expect(() => { + const wrongKey = 'whsec_aaaaaaaaaa=='; + client.beta.webhooks.unwrap(payload, { headers, key: wrongKey }); + }).toThrow('No matching signature found'); + expect(() => { + const wrongKey = 'whsec_aaaaaaaaaa=='; + client.withOptions({ webhookKey: wrongKey }).beta.webhooks.unwrap(payload, { headers }); + }).toThrow('No matching signature found'); + expect(() => { + const badSig = wh.sign(msgID, timestamp, 'some other payload'); + client.beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-signature': badSig }, key }); + }).toThrow('No matching signature found'); + expect(() => { + const badSig = wh.sign(msgID, timestamp, 'some other payload'); + client + .withOptions({ webhookKey: key }) + .beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-signature': badSig } }); + }).toThrow('No matching signature found'); + expect(() => { + client.beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-timestamp': '5' }, key }); + }).toThrow('Message timestamp too old'); + expect(() => { + client + .withOptions({ webhookKey: key }) + .beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-timestamp': '5' } }); + }).toThrow('Message timestamp too old'); + expect(() => { + client.beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-id': 'wrong' }, key }); + }).toThrow('No matching signature found'); + expect(() => { + client + .withOptions({ webhookKey: key }) + .beta.webhooks.unwrap(payload, { headers: { ...headers, 'webhook-id': 'wrong' } }); + }).toThrow('No matching signature found'); + }); +}); diff --git a/tests/qs/empty-keys-cases.ts b/tests/qs/empty-keys-cases.ts new file mode 100644 index 00000000..ea2c1b0a --- /dev/null +++ b/tests/qs/empty-keys-cases.ts @@ -0,0 +1,271 @@ +export const empty_test_cases = [ + { + input: '&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&&', + with_empty_keys: {}, + stringify_output: { + brackets: '', + indices: '', + repeat: '', + }, + no_empty_keys: {}, + }, + { + input: '&=', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '&=&=&', + with_empty_keys: { '': ['', ''] }, + stringify_output: { + brackets: '[]=&[]=', + indices: '[0]=&[1]=', + repeat: '=&=', + }, + no_empty_keys: {}, + }, + { + input: '=', + with_empty_keys: { '': '' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + }, + { + input: '=&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&&&', + with_empty_keys: { '': '' }, + stringify_output: { + brackets: '=', + indices: '=', + repeat: '=', + }, + no_empty_keys: {}, + }, + { + input: '=&=&=&', + with_empty_keys: { '': ['', '', ''] }, + stringify_output: { + brackets: '[]=&[]=&[]=', + indices: '[0]=&[1]=&[2]=', + repeat: '=&=&=', + }, + no_empty_keys: {}, + }, + { + input: '=&a[]=b&a[1]=c', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '=a', + with_empty_keys: { '': 'a' }, + no_empty_keys: {}, + stringify_output: { + brackets: '=a', + indices: '=a', + repeat: '=a', + }, + }, + { + input: 'a==a', + with_empty_keys: { a: '=a' }, + no_empty_keys: { a: '=a' }, + stringify_output: { + brackets: 'a==a', + indices: 'a==a', + repeat: 'a==a', + }, + }, + { + input: '=&a[]=b', + with_empty_keys: { '': '', a: ['b'] }, + stringify_output: { + brackets: '=&a[]=b', + indices: '=&a[0]=b', + repeat: '=&a=b', + }, + no_empty_keys: { a: ['b'] }, + }, + { + input: '=&a[]=b&a[]=c&a[2]=d', + with_empty_keys: { '': '', a: ['b', 'c', 'd'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c&a[]=d', + indices: '=&a[0]=b&a[1]=c&a[2]=d', + repeat: '=&a=b&a=c&a=d', + }, + no_empty_keys: { a: ['b', 'c', 'd'] }, + }, + { + input: '=a&=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: {}, + }, + { + input: '=a&foo=b', + with_empty_keys: { '': 'a', foo: 'b' }, + no_empty_keys: { foo: 'b' }, + stringify_output: { + brackets: '=a&foo=b', + indices: '=a&foo=b', + repeat: '=a&foo=b', + }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a[0]=b&a=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: 'a=b&a[0]=c&=', + with_empty_keys: { '': '', a: ['b', 'c'] }, + stringify_output: { + brackets: '=&a[]=b&a[]=c', + indices: '=&a[0]=b&a[1]=c', + repeat: '=&a=b&a=c', + }, + no_empty_keys: { a: ['b', 'c'] }, + }, + { + input: '[]=a&[]=b& []=1', + with_empty_keys: { '': ['a', 'b'], ' ': ['1'] }, + stringify_output: { + brackets: '[]=a&[]=b& []=1', + indices: '[0]=a&[1]=b& [0]=1', + repeat: '=a&=b& =1', + }, + no_empty_keys: { 0: 'a', 1: 'b', ' ': ['1'] }, + }, + { + input: '[0]=a&[1]=b&a[0]=1&a[1]=2', + with_empty_keys: { '': ['a', 'b'], a: ['1', '2'] }, + no_empty_keys: { 0: 'a', 1: 'b', a: ['1', '2'] }, + stringify_output: { + brackets: '[]=a&[]=b&a[]=1&a[]=2', + indices: '[0]=a&[1]=b&a[0]=1&a[1]=2', + repeat: '=a&=b&a=1&a=2', + }, + }, + { + input: '[deep]=a&[deep]=2', + with_empty_keys: { '': { deep: ['a', '2'] } }, + stringify_output: { + brackets: '[deep][]=a&[deep][]=2', + indices: '[deep][0]=a&[deep][1]=2', + repeat: '[deep]=a&[deep]=2', + }, + no_empty_keys: { deep: ['a', '2'] }, + }, + { + input: '%5B0%5D=a&%5B1%5D=b', + with_empty_keys: { '': ['a', 'b'] }, + stringify_output: { + brackets: '[]=a&[]=b', + indices: '[0]=a&[1]=b', + repeat: '=a&=b', + }, + no_empty_keys: { 0: 'a', 1: 'b' }, + }, +] satisfies { + input: string; + with_empty_keys: Record; + stringify_output: { + brackets: string; + indices: string; + repeat: string; + }; + no_empty_keys: Record; +}[]; diff --git a/tests/qs/stringify.test.ts b/tests/qs/stringify.test.ts new file mode 100644 index 00000000..f01db8d1 --- /dev/null +++ b/tests/qs/stringify.test.ts @@ -0,0 +1,2232 @@ +import iconv from 'iconv-lite'; +import { stringify } from '@anthropic-ai/sdk/internal/qs'; +import { encode } from '@anthropic-ai/sdk/internal/qs/utils'; +import { StringifyOptions } from '@anthropic-ai/sdk/internal/qs/types'; +import { empty_test_cases } from './empty-keys-cases'; +import assert from 'assert'; + +describe('stringify()', function () { + test('stringifies a querystring object', function () { + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: 1 })).toBe('a=1'); + expect(stringify({ a: 1, b: 2 })).toBe('a=1&b=2'); + expect(stringify({ a: 'A_Z' })).toBe('a=A_Z'); + expect(stringify({ a: '€' })).toBe('a=%E2%82%AC'); + expect(stringify({ a: '' })).toBe('a=%EE%80%80'); + expect(stringify({ a: 'א' })).toBe('a=%D7%90'); + expect(stringify({ a: '𐐷' })).toBe('a=%F0%90%90%B7'); + }); + + test('stringifies falsy values', function () { + expect(stringify(undefined)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify(null, { strictNullHandling: true })).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(0)).toBe(''); + }); + + test('stringifies symbols', function () { + expect(stringify(Symbol.iterator)).toBe(''); + expect(stringify([Symbol.iterator])).toBe('0=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: Symbol.iterator })).toBe('a=Symbol%28Symbol.iterator%29'); + expect(stringify({ a: [Symbol.iterator] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=Symbol%28Symbol.iterator%29', + ); + }); + + test('stringifies bigints', function () { + var three = BigInt(3); + // @ts-expect-error + var encodeWithN = function (value, defaultEncoder, charset) { + var result = defaultEncoder(value, defaultEncoder, charset); + return typeof value === 'bigint' ? result + 'n' : result; + }; + + expect(stringify(three)).toBe(''); + expect(stringify([three])).toBe('0=3'); + expect(stringify([three], { encoder: encodeWithN })).toBe('0=3n'); + expect(stringify({ a: three })).toBe('a=3'); + expect(stringify({ a: three }, { encoder: encodeWithN })).toBe('a=3n'); + expect(stringify({ a: [three] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=3'); + expect( + stringify({ a: [three] }, { encodeValuesOnly: true, encoder: encodeWithN, arrayFormat: 'brackets' }), + ).toBe('a[]=3n'); + }); + + test('encodes dot in key of object when encodeDotInKeys and allowDots is provided', function () { + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: false }), + ).toBe('name.obj%5Bfirst%5D=John&name.obj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: false }), + ).toBe('name.obj.first=John&name.obj.last=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: false, encodeDotInKeys: true }), + ).toBe('name%252Eobj%5Bfirst%5D=John&name%252Eobj%5Blast%5D=Doe'); + expect( + stringify({ 'name.obj': { first: 'John', last: 'Doe' } }, { allowDots: true, encodeDotInKeys: true }), + ).toBe('name%252Eobj.first=John&name%252Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: false }, + // ), + // 'name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe', + // 'with allowDots false and encodeDotInKeys false', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: false, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe', + // 'with allowDots false and encodeDotInKeys true', + // ); + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots true and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject%5Bfirst.godly.name%5D=John&name.obj.subobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: false }, + ), + ).toBe('name.obj.subobject.first.godly.name=John&name.obj.subobject.last=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: false, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject%5Bfirst.godly.name%5D=John&name%252Eobj%252Esubobject%5Blast%5D=Doe'); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object, and automatically set allowDots to `true` when encodeDotInKeys is true and allowDots in undefined', function () { + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { encodeDotInKeys: true }, + // ), + // 'name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe', + // 'with allowDots undefined and encodeDotInKeys true', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { encodeDotInKeys: true }, + ), + ).toBe('name%252Eobj%252Esubobject.first%252Egodly%252Ename=John&name%252Eobj%252Esubobject.last=Doe'); + }); + + test('should encode dot in key of object when encodeDotInKeys and allowDots is provided, and nothing else when encodeValuesOnly is provided', function () { + // st.equal( + // stringify( + // { 'name.obj': { first: 'John', last: 'Doe' } }, + // { + // encodeDotInKeys: true, + // allowDots: true, + // encodeValuesOnly: true, + // }, + // ), + // 'name%2Eobj.first=John&name%2Eobj.last=Doe', + // ); + expect( + stringify( + { 'name.obj': { first: 'John', last: 'Doe' } }, + { + encodeDotInKeys: true, + allowDots: true, + encodeValuesOnly: true, + }, + ), + ).toBe('name%2Eobj.first=John&name%2Eobj.last=Doe'); + + // st.equal( + // stringify( + // { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + // { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + // ), + // 'name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe', + // ); + expect( + stringify( + { 'name.obj.subobject': { 'first.godly.name': 'John', last: 'Doe' } }, + { allowDots: true, encodeDotInKeys: true, encodeValuesOnly: true }, + ), + ).toBe('name%2Eobj%2Esubobject.first%2Egodly%2Ename=John&name%2Eobj%2Esubobject.last=Doe'); + }); + + test('throws when `commaRoundTrip` is not a boolean', function () { + // st['throws']( + // function () { + // stringify({}, { commaRoundTrip: 'not a boolean' }); + // }, + // TypeError, + // 'throws when `commaRoundTrip` is not a boolean', + // ); + expect(() => { + // @ts-expect-error + stringify({}, { commaRoundTrip: 'not a boolean' }); + }).toThrow(TypeError); + }); + + test('throws when `encodeDotInKeys` is not a boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { encodeDotInKeys: null }); + }).toThrow(TypeError); + }); + + test('adds query prefix', function () { + // st.equal(stringify({ a: 'b' }, { addQueryPrefix: true }), '?a=b'); + expect(stringify({ a: 'b' }, { addQueryPrefix: true })).toBe('?a=b'); + }); + + test('with query prefix, outputs blank string given an empty object', function () { + // st.equal(stringify({}, { addQueryPrefix: true }), ''); + expect(stringify({}, { addQueryPrefix: true })).toBe(''); + }); + + test('stringifies nested falsy values', function () { + // st.equal(stringify({ a: { b: { c: null } } }), 'a%5Bb%5D%5Bc%5D='); + // st.equal( + // stringify({ a: { b: { c: null } } }, { strictNullHandling: true }), + // 'a%5Bb%5D%5Bc%5D', + // ); + // st.equal(stringify({ a: { b: { c: false } } }), 'a%5Bb%5D%5Bc%5D=false'); + expect(stringify({ a: { b: { c: null } } })).toBe('a%5Bb%5D%5Bc%5D='); + expect(stringify({ a: { b: { c: null } } }, { strictNullHandling: true })).toBe('a%5Bb%5D%5Bc%5D'); + expect(stringify({ a: { b: { c: false } } })).toBe('a%5Bb%5D%5Bc%5D=false'); + }); + + test('stringifies a nested object', function () { + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }), 'a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } })).toBe('a%5Bb%5D%5Bc%5D%5Bd%5D=e'); + }); + + test('`allowDots` option: stringifies a nested object with dots notation', function () { + // st.equal(stringify({ a: { b: 'c' } }, { allowDots: true }), 'a.b=c'); + // st.equal(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true }), 'a.b.c.d=e'); + expect(stringify({ a: { b: 'c' } }, { allowDots: true })).toBe('a.b=c'); + expect(stringify({ a: { b: { c: { d: 'e' } } } }, { allowDots: true })).toBe('a.b.c.d=e'); + }); + + test('stringifies an array value', function () { + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' }), + // 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' }), + // 'a=b%2Cc%2Cd', + // 'comma => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=b%2Cc%2Cd', + // 'comma round trip => comma', + // ); + // st.equal( + // stringify({ a: ['b', 'c', 'd'] }), + // 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + // 'default => indices', + // ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'indices' })).toBe( + 'a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'brackets' })).toBe( + 'a%5B%5D=b&a%5B%5D=c&a%5B%5D=d', + ); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma' })).toBe('a=b%2Cc%2Cd'); + expect(stringify({ a: ['b', 'c', 'd'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=b%2Cc%2Cd', + ); + expect(stringify({ a: ['b', 'c', 'd'] })).toBe('a%5B0%5D=b&a%5B1%5D=c&a%5B2%5D=d'); + }); + + test('`skipNulls` option', function () { + // st.equal( + // stringify({ a: 'b', c: null }, { skipNulls: true }), + // 'a=b', + // 'omits nulls when asked', + // ); + expect(stringify({ a: 'b', c: null }, { skipNulls: true })).toBe('a=b'); + + // st.equal( + // stringify({ a: { b: 'c', d: null } }, { skipNulls: true }), + // 'a%5Bb%5D=c', + // 'omits nested nulls when asked', + // ); + expect(stringify({ a: { b: 'c', d: null } }, { skipNulls: true })).toBe('a%5Bb%5D=c'); + }); + + test('omits array indices when asked', function () { + // st.equal(stringify({ a: ['b', 'c', 'd'] }, { indices: false }), 'a=b&a=c&a=d'); + expect(stringify({ a: ['b', 'c', 'd'] }, { indices: false })).toBe('a=b&a=c&a=d'); + }); + + test('omits object key/value pair when value is empty array', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + }); + + test('should not omit object key/value pair when value is empty array and when asked', function () { + // st.equal(stringify({ a: [], b: 'zz' }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false }), 'b=zz'); + // st.equal(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true }), 'a[]&b=zz'); + expect(stringify({ a: [], b: 'zz' })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: false })).toBe('b=zz'); + expect(stringify({ a: [], b: 'zz' }, { allowEmptyArrays: true })).toBe('a[]&b=zz'); + }); + + test('should throw when allowEmptyArrays is not of type boolean', function () { + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 'foobar' }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: 0 }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: NaN }); + }).toThrow(TypeError); + + // st['throws'](function () { + // stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + // }, TypeError); + expect(() => { + // @ts-expect-error + stringify({ a: [], b: 'zz' }, { allowEmptyArrays: null }); + }).toThrow(TypeError); + }); + + test('allowEmptyArrays + strictNullHandling', function () { + // st.equal( + // stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true }), + // 'testEmptyArray[]', + // ); + expect(stringify({ testEmptyArray: [] }, { strictNullHandling: true, allowEmptyArrays: true })).toBe( + 'testEmptyArray[]', + ); + }); + + describe('stringifies an array value with one item vs multiple items', function () { + test('non-array item', function () { + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a=c', + // ); + // s2t.equal( + // stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a=c', + // ); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' }), 'a=c'); + // s2t.equal(stringify({ a: 'c' }, { encodeValuesOnly: true }), 'a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect(stringify({ a: 'c' }, { encodeValuesOnly: true })).toBe('a=c'); + }); + + test('array with a single item', function () { + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c', + // ); + // s2t.equal( + // stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c', + // ); + // s2t.equal( + // stringify( + // { a: ['c'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a[]=c', + // ); // so it parses back as an array + // s2t.equal(stringify({ a: ['c'] }, { encodeValuesOnly: true }), 'a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe('a[0]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c'); + expect( + stringify({ a: ['c'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a[]=c'); + expect(stringify({ a: ['c'] }, { encodeValuesOnly: true })).toBe('a[0]=c'); + }); + + test('array with multiple items', function () { + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0]=c&a[1]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=c&a[]=d', + // ); + // s2t.equal( + // stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c,d', + // ); + // s2t.equal( + // stringify( + // { a: ['c', 'd'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c,d', + // ); + // s2t.equal(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true }), 'a[0]=c&a[1]=d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0]=c&a[1]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=c&a[]=d', + ); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe('a=c,d'); + expect( + stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('a=c,d'); + expect(stringify({ a: ['c', 'd'] }, { encodeValuesOnly: true })).toBe('a[0]=c&a[1]=d'); + }); + + test('array with multiple items with a comma inside', function () { + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // 'a=c%2Cd,e', + // ); + // s2t.equal(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' }), 'a=c%2Cd%2Ce'); + expect(stringify({ a: ['c,d', 'e'] }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a=c%2Cd,e', + ); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma' })).toBe('a=c%2Cd%2Ce'); + + // s2t.equal( + // stringify( + // { a: ['c,d', 'e'] }, + // { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'a=c%2Cd,e', + // ); + // s2t.equal( + // stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true }), + // 'a=c%2Cd%2Ce', + // ); + expect( + stringify( + { a: ['c,d', 'e'] }, + { encodeValuesOnly: true, arrayFormat: 'comma', commaRoundTrip: true }, + ), + ).toBe('a=c%2Cd,e'); + expect(stringify({ a: ['c,d', 'e'] }, { arrayFormat: 'comma', commaRoundTrip: true })).toBe( + 'a=c%2Cd%2Ce', + ); + }); + }); + + test('stringifies a nested array value', function () { + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[b][0]=c&a[b][1]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[b][]=c&a[b][]=d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true, arrayFormat: 'comma' })).toBe( + 'a[b]=c,d', + ); + expect(stringify({ a: { b: ['c', 'd'] } }, { encodeValuesOnly: true })).toBe('a[b][0]=c&a[b][1]=d'); + }); + + test('stringifies comma and empty array values', function () { + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' }), + // 'a[0]=,&a[1]=&a[2]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' }), + // 'a[]=,&a[]=&a[]=c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' }), + // 'a=,,,c,d%', + // ); + // st.equal( + // stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&a=&a=c,d%', + // ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a[0]=,&a[1]=&a[2]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a[]=,&a[]=&a[]=c,d%', + ); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'comma' })).toBe('a=,,,c,d%'); + expect(stringify({ a: [',', '', 'c,d%'] }, { encode: false, arrayFormat: 'repeat' })).toBe( + 'a=,&a=&a=c,d%', + ); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[0]=%2C&a[1]=&a[2]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[]=%2C&a[]=&a[]=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C,,c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a%5B%5D=%2C&a%5B%5D=&a%5B%5D=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C%2C%2Cc%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: [',', '', 'c,d%'] }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&a=&a=c%2Cd%25', + // ); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }), + ).toBe('a%5B0%5D=%2C&a%5B1%5D=&a%5B2%5D=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[]=%2C&a[]=&a[]=c%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }), + ).toBe('a=%2C%2C%2Cc%2Cd%25'); + expect( + stringify({ a: [',', '', 'c,d%'] }, { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }), + ).toBe('a=%2C&a=&a=c%2Cd%25'); + }); + + test('stringifies comma and empty non-array values', function () { + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'comma' }), + // 'a=,&b=&c=c,d%', + // ); + // st.equal( + // stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'repeat' }), + // 'a=,&b=&c=c,d%', + // ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'a=,&b=&c=c,d%', + ); + expect(stringify({ a: ',', b: '', c: 'c,d%' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'a=,&b=&c=c,d%', + ); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify({ a: ',', b: '', c: 'c,d%' }, { encode: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + // st.equal( + // stringify( + // { a: ',', b: '', c: 'c,d%' }, + // { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + // ), + // 'a=%2C&b=&c=c%2Cd%25', + // ); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'indices' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'brackets' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'comma' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + expect( + stringify( + { a: ',', b: '', c: 'c,d%' }, + { encode: true, encodeValuesOnly: false, arrayFormat: 'repeat' }, + ), + ).toBe('a=%2C&b=&c=c%2Cd%25'); + }); + + test('stringifies a nested array value with dots notation', function () { + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a.b[0]=c&a.b[1]=d', + // 'indices: stringifies with dots + indices', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a.b[]=c&a.b[]=d', + // 'brackets: stringifies with dots + brackets', + // ); + // st.equal( + // stringify( + // { a: { b: ['c', 'd'] } }, + // { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }, + // ), + // 'a.b=c,d', + // 'comma: stringifies with dots + comma', + // ); + // st.equal( + // stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true }), + // 'a.b[0]=c&a.b[1]=d', + // 'default: stringifies with dots + indices', + // ); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a.b[0]=c&a.b[1]=d'); + expect( + stringify( + { a: { b: ['c', 'd'] } }, + { allowDots: true, encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a.b[]=c&a.b[]=d'); + expect( + stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true, arrayFormat: 'comma' }), + ).toBe('a.b=c,d'); + expect(stringify({ a: { b: ['c', 'd'] } }, { allowDots: true, encodeValuesOnly: true })).toBe( + 'a.b[0]=c&a.b[1]=d', + ); + }); + + test('stringifies an object inside an array', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b]=c', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b]=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true }), + // 'a[0][b]=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe('a[b]=c'); + expect(stringify({ a: [{ b: 'c' }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b]=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { encodeValuesOnly: true })).toBe('a[0][b]=c'); + + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true }), + // 'a[b][c]=1', + // 'repeat => repeat', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true }), + // 'a[][b][c][]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true }), + // 'a[0][b][c][0]=1', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'indices', encodeValuesOnly: true })).toBe( + 'a[0][b][c][0]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'repeat', encodeValuesOnly: true })).toBe( + 'a[b][c]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { arrayFormat: 'brackets', encodeValuesOnly: true })).toBe( + 'a[][b][c][]=1', + ); + expect(stringify({ a: [{ b: { c: [1] } }] }, { encodeValuesOnly: true })).toBe('a[0][b][c][0]=1'); + }); + + test('stringifies an array with mixed objects and primitives', function () { + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'indices => indices', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[][b]=1&a[]=2&a[]=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // '???', + // 'brackets => brackets', + // { skip: 'TODO: figure out what this should do' }, + // ); + // st.equal( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true }), + // 'a[0][b]=1&a[1]=2&a[2]=3', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[0][b]=1&a[1]=2&a[2]=3', + ); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[][b]=1&a[]=2&a[]=3', + ); + // !Skipped: Figure out what this should do + // expect( + // stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true, arrayFormat: 'comma' }), + // ).toBe('???'); + expect(stringify({ a: [{ b: 1 }, 2, 3] }, { encodeValuesOnly: true })).toBe('a[0][b]=1&a[1]=2&a[2]=3'); + }); + + test('stringifies an object inside an array with dots notation', function () { + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + // 'a[0].b=c', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: 'c' }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b=c', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false }), + // 'a[0].b=c', + // 'default => indices', + // ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'indices' })).toBe( + 'a[0].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' })).toBe( + 'a[].b=c', + ); + expect(stringify({ a: [{ b: 'c' }] }, { allowDots: true, encode: false })).toBe('a[0].b=c'); + + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'indices' }, + // ), + // 'a[0].b.c[0]=1', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: [{ b: { c: [1] } }] }, + // { allowDots: true, encode: false, arrayFormat: 'brackets' }, + // ), + // 'a[].b.c[]=1', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false }), + // 'a[0].b.c[0]=1', + // 'default => indices', + // ); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'indices' }), + ).toBe('a[0].b.c[0]=1'); + expect( + stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false, arrayFormat: 'brackets' }), + ).toBe('a[].b.c[]=1'); + expect(stringify({ a: [{ b: { c: [1] } }] }, { allowDots: true, encode: false })).toBe('a[0].b.c[0]=1'); + }); + + test('does not omit object keys when indices = false', function () { + // st.equal(stringify({ a: [{ b: 'c' }] }, { indices: false }), 'a%5Bb%5D=c'); + expect(stringify({ a: [{ b: 'c' }] }, { indices: false })).toBe('a%5Bb%5D=c'); + }); + + test('uses indices notation for arrays when indices=true', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { indices: true }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { indices: true })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when no arrayFormat is specified', function () { + // st.equal(stringify({ a: ['b', 'c'] }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses indices notation for arrays when arrayFormat=indices', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' }), 'a%5B0%5D=b&a%5B1%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' })).toBe('a%5B0%5D=b&a%5B1%5D=c'); + }); + + test('uses repeat notation for arrays when arrayFormat=repeat', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' }), 'a=b&a=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' })).toBe('a=b&a=c'); + }); + + test('uses brackets notation for arrays when arrayFormat=brackets', function () { + // st.equal(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' }), 'a%5B%5D=b&a%5B%5D=c'); + expect(stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' })).toBe('a%5B%5D=b&a%5B%5D=c'); + }); + + test('stringifies a complicated object', function () { + // st.equal(stringify({ a: { b: 'c', d: 'e' } }), 'a%5Bb%5D=c&a%5Bd%5D=e'); + expect(stringify({ a: { b: 'c', d: 'e' } })).toBe('a%5Bb%5D=c&a%5Bd%5D=e'); + }); + + test('stringifies an empty value', function () { + // st.equal(stringify({ a: '' }), 'a='); + // st.equal(stringify({ a: null }, { strictNullHandling: true }), 'a'); + expect(stringify({ a: '' })).toBe('a='); + expect(stringify({ a: null }, { strictNullHandling: true })).toBe('a'); + + // st.equal(stringify({ a: '', b: '' }), 'a=&b='); + // st.equal(stringify({ a: null, b: '' }, { strictNullHandling: true }), 'a&b='); + expect(stringify({ a: '', b: '' })).toBe('a=&b='); + expect(stringify({ a: null, b: '' }, { strictNullHandling: true })).toBe('a&b='); + + // st.equal(stringify({ a: { b: '' } }), 'a%5Bb%5D='); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: true }), 'a%5Bb%5D'); + // st.equal(stringify({ a: { b: null } }, { strictNullHandling: false }), 'a%5Bb%5D='); + expect(stringify({ a: { b: '' } })).toBe('a%5Bb%5D='); + expect(stringify({ a: { b: null } }, { strictNullHandling: true })).toBe('a%5Bb%5D'); + expect(stringify({ a: { b: null } }, { strictNullHandling: false })).toBe('a%5Bb%5D='); + }); + + test('stringifies an empty array in different arrayFormat', function () { + // st.equal(stringify({ a: [], b: [null], c: 'c' }, { encode: false }), 'b[0]=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false })).toBe('b[0]=&c=c'); + // arrayFormat default + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' }), + // 'b[0]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' }), + // 'b[]=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' }), + // 'b=&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', commaRoundTrip: true }, + // ), + // 'b[]=&c=c', + // ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices' })).toBe( + 'b[0]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets' })).toBe( + 'b[]=&c=c', + ); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat' })).toBe('b=&c=c'); + expect(stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma' })).toBe('b=&c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', commaRoundTrip: true }), + ).toBe('b[]=&c=c'); + + // with strictNullHandling + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + // ), + // 'b[0]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + // ), + // 'b[]&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + // ), + // 'b&c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + // ), + // 'b[]&c=c', + // ); + + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'indices', strictNullHandling: true }, + ), + ).toBe('b[0]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'brackets', strictNullHandling: true }, + ), + ).toBe('b[]&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'repeat', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true }, + ), + ).toBe('b&c=c'); + expect( + stringify( + { a: [], b: [null], c: 'c' }, + { encode: false, arrayFormat: 'comma', strictNullHandling: true, commaRoundTrip: true }, + ), + ).toBe('b[]&c=c'); + + // with skipNulls + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'indices', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'brackets', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'repeat', skipNulls: true }, + // ), + // 'c=c', + // ); + // st.equal( + // stringify( + // { a: [], b: [null], c: 'c' }, + // { encode: false, arrayFormat: 'comma', skipNulls: true }, + // ), + // 'c=c', + // ); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'indices', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'brackets', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'repeat', skipNulls: true }), + ).toBe('c=c'); + expect( + stringify({ a: [], b: [null], c: 'c' }, { encode: false, arrayFormat: 'comma', skipNulls: true }), + ).toBe('c=c'); + }); + + test('stringifies a null object', function () { + var obj = Object.create(null); + obj.a = 'b'; + // st.equal(stringify(obj), 'a=b'); + expect(stringify(obj)).toBe('a=b'); + }); + + test('returns an empty string for invalid input', function () { + // st.equal(stringify(undefined), ''); + // st.equal(stringify(false), ''); + // st.equal(stringify(null), ''); + // st.equal(stringify(''), ''); + expect(stringify(undefined)).toBe(''); + expect(stringify(false)).toBe(''); + expect(stringify(null)).toBe(''); + expect(stringify('')).toBe(''); + }); + + test('stringifies an object with a null object as a child', function () { + var obj = { a: Object.create(null) }; + + obj.a.b = 'c'; + // st.equal(stringify(obj), 'a%5Bb%5D=c'); + expect(stringify(obj)).toBe('a%5Bb%5D=c'); + }); + + test('drops keys with a value of undefined', function () { + // st.equal(stringify({ a: undefined }), ''); + expect(stringify({ a: undefined })).toBe(''); + + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true }), + // 'a%5Bc%5D', + // ); + // st.equal( + // stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false }), + // 'a%5Bc%5D=', + // ); + // st.equal(stringify({ a: { b: undefined, c: '' } }), 'a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: true })).toBe('a%5Bc%5D'); + expect(stringify({ a: { b: undefined, c: null } }, { strictNullHandling: false })).toBe('a%5Bc%5D='); + expect(stringify({ a: { b: undefined, c: '' } })).toBe('a%5Bc%5D='); + }); + + test('url encodes values', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + }); + + test('stringifies a date', function () { + var now = new Date(); + var str = 'a=' + encodeURIComponent(now.toISOString()); + // st.equal(stringify({ a: now }), str); + expect(stringify({ a: now })).toBe(str); + }); + + test('stringifies the weird object from qs', function () { + // st.equal( + // stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' }), + // 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + // ); + expect(stringify({ 'my weird field': '~q1!2"\'w$5&7/z8)?' })).toBe( + 'my%20weird%20field=~q1%212%22%27w%245%267%2Fz8%29%3F', + ); + }); + + // TODO: Investigate how to to intercept in vitest + // TODO(rob) + test('skips properties that are part of the object prototype', function () { + // st.intercept(Object.prototype, 'crash', { value: 'test' }); + // @ts-expect-error + Object.prototype.crash = 'test'; + + // st.equal(stringify({ a: 'b' }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }), 'a%5Bb%5D=c'); + expect(stringify({ a: 'b' })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } })).toBe('a%5Bb%5D=c'); + }); + + test('stringifies boolean values', function () { + // st.equal(stringify({ a: true }), 'a=true'); + // st.equal(stringify({ a: { b: true } }), 'a%5Bb%5D=true'); + // st.equal(stringify({ b: false }), 'b=false'); + // st.equal(stringify({ b: { c: false } }), 'b%5Bc%5D=false'); + expect(stringify({ a: true })).toBe('a=true'); + expect(stringify({ a: { b: true } })).toBe('a%5Bb%5D=true'); + expect(stringify({ b: false })).toBe('b=false'); + expect(stringify({ b: { c: false } })).toBe('b%5Bc%5D=false'); + }); + + test('stringifies buffer values', function () { + // st.equal(stringify({ a: Buffer.from('test') }), 'a=test'); + // st.equal(stringify({ a: { b: Buffer.from('test') } }), 'a%5Bb%5D=test'); + }); + + test('stringifies an object using an alternative delimiter', function () { + // st.equal(stringify({ a: 'b', c: 'd' }, { delimiter: ';' }), 'a=b;c=d'); + expect(stringify({ a: 'b', c: 'd' }, { delimiter: ';' })).toBe('a=b;c=d'); + }); + + // We dont target environments which dont even have Buffer + // test('does not blow up when Buffer global is missing', function () { + // var restore = mockProperty(global, 'Buffer', { delete: true }); + + // var result = stringify({ a: 'b', c: 'd' }); + + // restore(); + + // st.equal(result, 'a=b&c=d'); + // st.end(); + // }); + + test('does not crash when parsing circular references', function () { + var a: any = {}; + a.b = a; + + // st['throws']( + // function () { + // stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify({ 'foo[bar]': 'baz', 'foo[baz]': a }); + }).toThrow('Cyclic object value'); + + var circular: any = { + a: 'value', + }; + circular.a = circular; + // st['throws']( + // function () { + // stringify(circular); + // }, + // /RangeError: Cyclic object value/, + // 'cyclic values throw', + // ); + expect(() => { + stringify(circular); + }).toThrow('Cyclic object value'); + + var arr = ['a']; + // st.doesNotThrow(function () { + // stringify({ x: arr, y: arr }); + // }, 'non-cyclic values do not throw'); + expect(() => { + stringify({ x: arr, y: arr }); + }).not.toThrow(); + }); + + test('non-circular duplicated references can still work', function () { + var hourOfDay = { + function: 'hour_of_day', + }; + + var p1 = { + function: 'gte', + arguments: [hourOfDay, 0], + }; + var p2 = { + function: 'lte', + arguments: [hourOfDay, 23], + }; + + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + // ); + // st.equal( + // stringify( + // { filters: { $and: [p1, p2] } }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + // ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe( + 'filters[$and][0][function]=gte&filters[$and][0][arguments][0][function]=hour_of_day&filters[$and][0][arguments][1]=0&filters[$and][1][function]=lte&filters[$and][1][arguments][0][function]=hour_of_day&filters[$and][1][arguments][1]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe( + 'filters[$and][][function]=gte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=0&filters[$and][][function]=lte&filters[$and][][arguments][][function]=hour_of_day&filters[$and][][arguments][]=23', + ); + expect( + stringify({ filters: { $and: [p1, p2] } }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe( + 'filters[$and][function]=gte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=0&filters[$and][function]=lte&filters[$and][arguments][function]=hour_of_day&filters[$and][arguments]=23', + ); + }); + + test('selects properties when filter=array', function () { + // st.equal(stringify({ a: 'b' }, { filter: ['a'] }), 'a=b'); + // st.equal(stringify({ a: 1 }, { filter: [] }), ''); + expect(stringify({ a: 'b' }, { filter: ['a'] })).toBe('a=b'); + expect(stringify({ a: 1 }, { filter: [] })).toBe(''); + + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + // ), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'indices => indices', + // ); + // st.equal( + // stringify( + // { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + // { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + // ), + // 'a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3', + // 'brackets => brackets', + // ); + // st.equal( + // stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] }), + // 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + // 'default => indices', + // ); + expect(stringify({ a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, { filter: ['a', 'b', 0, 2] })).toBe( + 'a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3', + ); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'indices' }, + ), + ).toBe('a%5Bb%5D%5B0%5D=1&a%5Bb%5D%5B2%5D=3'); + expect( + stringify( + { a: { b: [1, 2, 3, 4], c: 'd' }, c: 'f' }, + { filter: ['a', 'b', 0, 2], arrayFormat: 'brackets' }, + ), + ).toBe('a%5Bb%5D%5B%5D=1&a%5Bb%5D%5B%5D=3'); + }); + + test('supports custom representations when filter=function', function () { + var calls = 0; + var obj = { a: 'b', c: 'd', e: { f: new Date(1257894000000) } }; + var filterFunc: StringifyOptions['filter'] = function (prefix, value) { + calls += 1; + if (calls === 1) { + // st.equal(prefix, '', 'prefix is empty'); + // st.equal(value, obj); + expect(prefix).toBe(''); + expect(value).toBe(obj); + } else if (prefix === 'c') { + return void 0; + } else if (value instanceof Date) { + // st.equal(prefix, 'e[f]'); + expect(prefix).toBe('e[f]'); + return value.getTime(); + } + return value; + }; + + // st.equal(stringify(obj, { filter: filterFunc }), 'a=b&e%5Bf%5D=1257894000000'); + // st.equal(calls, 5); + expect(stringify(obj, { filter: filterFunc })).toBe('a=b&e%5Bf%5D=1257894000000'); + expect(calls).toBe(5); + }); + + test('can disable uri encoding', function () { + // st.equal(stringify({ a: 'b' }, { encode: false }), 'a=b'); + // st.equal(stringify({ a: { b: 'c' } }, { encode: false }), 'a[b]=c'); + // st.equal( + // stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false }), + // 'a=b&c', + // ); + expect(stringify({ a: 'b' }, { encode: false })).toBe('a=b'); + expect(stringify({ a: { b: 'c' } }, { encode: false })).toBe('a[b]=c'); + expect(stringify({ a: 'b', c: null }, { strictNullHandling: true, encode: false })).toBe('a=b&c'); + }); + + test('can sort the keys', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort }), 'a=c&b=f&z=y'); + // st.equal( + // stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort }), + // 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + // ); + expect(stringify({ a: 'c', z: 'y', b: 'f' }, { sort: sort })).toBe('a=c&b=f&z=y'); + expect(stringify({ a: 'c', z: { j: 'a', i: 'b' }, b: 'f' }, { sort: sort })).toBe( + 'a=c&b=f&z%5Bi%5D=b&z%5Bj%5D=a', + ); + }); + + test('can sort the keys at depth 3 or more too', function () { + // @ts-expect-error + var sort: NonNullable = function (a: string, b: string) { + return a.localeCompare(b); + }; + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: sort, encode: false }, + // ), + // 'a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb', + // ); + // st.equal( + // stringify( + // { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + // { sort: null, encode: false }, + // ), + // 'a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b', + // ); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: sort, encode: false }, + ), + ).toBe('a=a&b=b&z[zi][zia]=zia&z[zi][zib]=zib&z[zj][zja]=zja&z[zj][zjb]=zjb'); + expect( + stringify( + { a: 'a', z: { zj: { zjb: 'zjb', zja: 'zja' }, zi: { zib: 'zib', zia: 'zia' } }, b: 'b' }, + { sort: null, encode: false }, + ), + ).toBe('a=a&z[zj][zjb]=zjb&z[zj][zja]=zja&z[zi][zib]=zib&z[zi][zia]=zia&b=b'); + }); + + test('can stringify with custom encoding', function () { + // st.equal( + // stringify( + // { 県: '大阪府', '': '' }, + // { + // encoder: function (str) { + // if (str.length === 0) { + // return ''; + // } + // var buf = iconv.encode(str, 'shiftjis'); + // var result = []; + // for (var i = 0; i < buf.length; ++i) { + // result.push(buf.readUInt8(i).toString(16)); + // } + // return '%' + result.join('%'); + // }, + // }, + // ), + // '%8c%a7=%91%e5%8d%e3%95%7b&=', + // ); + expect( + stringify( + { 県: '大阪府', '': '' }, + { + encoder: function (str) { + if (str.length === 0) { + return ''; + } + var buf = iconv.encode(str, 'shiftjis'); + var result = []; + for (var i = 0; i < buf.length; ++i) { + result.push(buf.readUInt8(i).toString(16)); + } + return '%' + result.join('%'); + }, + }, + ), + ).toBe('%8c%a7=%91%e5%8d%e3%95%7b&='); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1, b: new Date(), c: true, d: [1] }, + // { + // encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + // return ''; + // }, + // }, + // ); + + stringify( + { a: 1, b: new Date(), c: true, d: [1] }, + { + encoder: function (str) { + // st.match(typeof str, /^(?:string|number|boolean)$/); + assert.match(typeof str, /^(?:string|number|boolean)$/); + return ''; + }, + }, + ); + }); + + test('receives the default encoder as a second argument', function () { + // stringify( + // { a: 1 }, + // { + // encoder: function (str, defaultEncoder) { + // st.equal(defaultEncoder, utils.encode); + // }, + // }, + // ); + + stringify( + { a: 1 }, + { + // @ts-ignore + encoder: function (_str, defaultEncoder) { + expect(defaultEncoder).toBe(encode); + }, + }, + ); + }); + + test('throws error with wrong encoder', function () { + // st['throws'](function () { + // stringify({}, { encoder: 'string' }); + // }, new TypeError('Encoder has to be a function.')); + // st.end(); + expect(() => { + // @ts-expect-error + stringify({}, { encoder: 'string' }); + }).toThrow(TypeError); + }); + + (typeof Buffer === 'undefined' ? test.skip : test)( + 'can use custom encoder for a buffer object', + function () { + // st.equal( + // stringify( + // { a: Buffer.from([1]) }, + // { + // encoder: function (buffer) { + // if (typeof buffer === 'string') { + // return buffer; + // } + // return String.fromCharCode(buffer.readUInt8(0) + 97); + // }, + // }, + // ), + // 'a=b', + // ); + expect( + stringify( + { a: Buffer.from([1]) }, + { + encoder: function (buffer) { + if (typeof buffer === 'string') { + return buffer; + } + return String.fromCharCode(buffer.readUInt8(0) + 97); + }, + }, + ), + ).toBe('a=b'); + + // st.equal( + // stringify( + // { a: Buffer.from('a b') }, + // { + // encoder: function (buffer) { + // return buffer; + // }, + // }, + // ), + // 'a=a b', + // ); + expect( + stringify( + { a: Buffer.from('a b') }, + { + encoder: function (buffer) { + return buffer; + }, + }, + ), + ).toBe('a=a b'); + }, + ); + + test('serializeDate option', function () { + var date = new Date(); + // st.equal( + // stringify({ a: date }), + // 'a=' + date.toISOString().replace(/:/g, '%3A'), + // 'default is toISOString', + // ); + expect(stringify({ a: date })).toBe('a=' + date.toISOString().replace(/:/g, '%3A')); + + var mutatedDate = new Date(); + mutatedDate.toISOString = function () { + throw new SyntaxError(); + }; + // st['throws'](function () { + // mutatedDate.toISOString(); + // }, SyntaxError); + expect(() => { + mutatedDate.toISOString(); + }).toThrow(SyntaxError); + // st.equal( + // stringify({ a: mutatedDate }), + // 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + // 'toISOString works even when method is not locally present', + // ); + expect(stringify({ a: mutatedDate })).toBe( + 'a=' + Date.prototype.toISOString.call(mutatedDate).replace(/:/g, '%3A'), + ); + + var specificDate = new Date(6); + // st.equal( + // stringify( + // { a: specificDate }, + // { + // serializeDate: function (d) { + // return d.getTime() * 7; + // }, + // }, + // ), + // 'a=42', + // 'custom serializeDate function called', + // ); + expect( + stringify( + { a: specificDate }, + { + // @ts-ignore + serializeDate: function (d) { + return d.getTime() * 7; + }, + }, + ), + ).toBe('a=42'); + + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // }, + // ), + // 'a=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + // st.equal( + // stringify( + // { a: [date] }, + // { + // serializeDate: function (d) { + // return d.getTime(); + // }, + // arrayFormat: 'comma', + // commaRoundTrip: true, + // }, + // ), + // 'a%5B%5D=' + date.getTime(), + // 'works with arrayFormat comma', + // ); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + }, + ), + ).toBe('a=' + date.getTime()); + expect( + stringify( + { a: [date] }, + { + // @ts-expect-error + serializeDate: function (d) { + return d.getTime(); + }, + arrayFormat: 'comma', + commaRoundTrip: true, + }, + ), + ).toBe('a%5B%5D=' + date.getTime()); + }); + + test('RFC 1738 serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC1738 }), 'a=b+c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC1738 }), 'a+b=c+d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC1738 }), + // 'a+b=a+b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC1738' })).toBe('a=b+c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC1738' })).toBe('a+b=c+d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC1738' })).toBe('a+b=a+b'); + + // st.equal(stringify({ 'foo(ref)': 'bar' }, { format: formats.RFC1738 }), 'foo(ref)=bar'); + expect(stringify({ 'foo(ref)': 'bar' }, { format: 'RFC1738' })).toBe('foo(ref)=bar'); + }); + + test('RFC 3986 spaces serialization', function () { + // st.equal(stringify({ a: 'b c' }, { format: formats.RFC3986 }), 'a=b%20c'); + // st.equal(stringify({ 'a b': 'c d' }, { format: formats.RFC3986 }), 'a%20b=c%20d'); + // st.equal( + // stringify({ 'a b': Buffer.from('a b') }, { format: formats.RFC3986 }), + // 'a%20b=a%20b', + // ); + expect(stringify({ a: 'b c' }, { format: 'RFC3986' })).toBe('a=b%20c'); + expect(stringify({ 'a b': 'c d' }, { format: 'RFC3986' })).toBe('a%20b=c%20d'); + expect(stringify({ 'a b': Buffer.from('a b') }, { format: 'RFC3986' })).toBe('a%20b=a%20b'); + }); + + test('Backward compatibility to RFC 3986', function () { + // st.equal(stringify({ a: 'b c' }), 'a=b%20c'); + // st.equal(stringify({ 'a b': Buffer.from('a b') }), 'a%20b=a%20b'); + expect(stringify({ a: 'b c' })).toBe('a=b%20c'); + expect(stringify({ 'a b': Buffer.from('a b') })).toBe('a%20b=a%20b'); + }); + + test('Edge cases and unknown formats', function () { + ['UFO1234', false, 1234, null, {}, []].forEach(function (format) { + // st['throws'](function () { + // stringify({ a: 'b c' }, { format: format }); + // }, new TypeError('Unknown format option provided.')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b c' }, { format: format }); + }).toThrow(TypeError); + }); + }); + + test('encodeValuesOnly', function () { + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h', + // 'encodeValuesOnly + indices', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h', + // 'encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify( + // { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a=b&c=d&c=e%3Df&f=g&f=h', + // 'encodeValuesOnly + repeat', + // ); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'indices' }, + ), + ).toBe('a=b&c[0]=d&c[1]=e%3Df&f[0][0]=g&f[1][0]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'brackets' }, + ), + ).toBe('a=b&c[]=d&c[]=e%3Df&f[][]=g&f[][]=h'); + expect( + stringify( + { a: 'b', c: ['d', 'e=f'], f: [['g'], ['h']] }, + { encodeValuesOnly: true, arrayFormat: 'repeat' }, + ), + ).toBe('a=b&c=d&c=e%3Df&f=g&f=h'); + + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' }), + // 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + // 'no encodeValuesOnly + indices', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' }), + // 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + // 'no encodeValuesOnly + brackets', + // ); + // st.equal( + // stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' }), + // 'a=b&c=d&c=e&f=g&f=h', + // 'no encodeValuesOnly + repeat', + // ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'indices' })).toBe( + 'a=b&c%5B0%5D=d&c%5B1%5D=e&f%5B0%5D%5B0%5D=g&f%5B1%5D%5B0%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'brackets' })).toBe( + 'a=b&c%5B%5D=d&c%5B%5D=e&f%5B%5D%5B%5D=g&f%5B%5D%5B%5D=h', + ); + expect(stringify({ a: 'b', c: ['d', 'e'], f: [['g'], ['h']] }, { arrayFormat: 'repeat' })).toBe( + 'a=b&c=d&c=e&f=g&f=h', + ); + }); + + test('encodeValuesOnly - strictNullHandling', function () { + // st.equal( + // stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true }), + // 'a[b]', + // ); + expect(stringify({ a: { b: null } }, { encodeValuesOnly: true, strictNullHandling: true })).toBe('a[b]'); + }); + + test('throws if an invalid charset is specified', function () { + // st['throws'](function () { + // stringify({ a: 'b' }, { charset: 'foobar' }); + // }, new TypeError('The charset option must be either utf-8, iso-8859-1, or undefined')); + expect(() => { + // @ts-expect-error + stringify({ a: 'b' }, { charset: 'foobar' }); + }).toThrow(TypeError); + }); + + test('respects a charset of iso-8859-1', function () { + // st.equal(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' }), '%E6=%E6'); + expect(stringify({ æ: 'æ' }, { charset: 'iso-8859-1' })).toBe('%E6=%E6'); + }); + + test('encodes unrepresentable chars as numeric entities in iso-8859-1 mode', function () { + // st.equal(stringify({ a: '☺' }, { charset: 'iso-8859-1' }), 'a=%26%239786%3B'); + expect(stringify({ a: '☺' }, { charset: 'iso-8859-1' })).toBe('a=%26%239786%3B'); + }); + + test('respects an explicit charset of utf-8 (the default)', function () { + // st.equal(stringify({ a: 'æ' }, { charset: 'utf-8' }), 'a=%C3%A6'); + expect(stringify({ a: 'æ' }, { charset: 'utf-8' })).toBe('a=%C3%A6'); + }); + + test('`charsetSentinel` option', function () { + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' }), + // 'utf8=%E2%9C%93&a=%C3%A6', + // 'adds the right sentinel when instructed to and the charset is utf-8', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'utf-8' })).toBe( + 'utf8=%E2%9C%93&a=%C3%A6', + ); + + // st.equal( + // stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' }), + // 'utf8=%26%2310003%3B&a=%E6', + // 'adds the right sentinel when instructed to and the charset is iso-8859-1', + // ); + expect(stringify({ a: 'æ' }, { charsetSentinel: true, charset: 'iso-8859-1' })).toBe( + 'utf8=%26%2310003%3B&a=%E6', + ); + }); + + test('does not mutate the options argument', function () { + var options = {}; + stringify({}, options); + // st.deepEqual(options, {}); + expect(options).toEqual({}); + }); + + test('strictNullHandling works with custom filter', function () { + // @ts-expect-error + var filter = function (_prefix, value) { + return value; + }; + + var options = { strictNullHandling: true, filter: filter }; + // st.equal(stringify({ key: null }, options), 'key'); + expect(stringify({ key: null }, options)).toBe('key'); + }); + + test('strictNullHandling works with null serializeDate', function () { + var serializeDate = function () { + return null; + }; + var options = { strictNullHandling: true, serializeDate: serializeDate }; + var date = new Date(); + // st.equal(stringify({ key: date }, options), 'key'); + // @ts-expect-error + expect(stringify({ key: date }, options)).toBe('key'); + }); + + test('allows for encoding keys and values differently', function () { + // @ts-expect-error + var encoder = function (str, defaultEncoder, charset, type) { + if (type === 'key') { + return defaultEncoder(str, defaultEncoder, charset, type).toLowerCase(); + } + if (type === 'value') { + return defaultEncoder(str, defaultEncoder, charset, type).toUpperCase(); + } + throw 'this should never happen! type: ' + type; + }; + + // st.deepEqual(stringify({ KeY: 'vAlUe' }, { encoder: encoder }), 'key=VALUE'); + expect(stringify({ KeY: 'vAlUe' }, { encoder: encoder })).toBe('key=VALUE'); + }); + + test('objects inside arrays', function () { + var obj = { a: { b: { c: 'd', e: 'f' } } }; + var withArray = { a: { b: [{ c: 'd', e: 'f' }] } }; + + // st.equal( + // stringify(obj, { encode: false }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, no arrayFormat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, bracket', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'indices' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, indices', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, repeat', + // ); + // st.equal( + // stringify(obj, { encode: false, arrayFormat: 'comma' }), + // 'a[b][c]=d&a[b][e]=f', + // 'no array, comma', + // ); + expect(stringify(obj, { encode: false })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'indices' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + expect(stringify(obj, { encode: false, arrayFormat: 'comma' })).toBe('a[b][c]=d&a[b][e]=f'); + + // st.equal( + // stringify(withArray, { encode: false }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, no arrayFormat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'brackets' }), + // 'a[b][][c]=d&a[b][][e]=f', + // 'array, bracket', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'indices' }), + // 'a[b][0][c]=d&a[b][0][e]=f', + // 'array, indices', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'repeat' }), + // 'a[b][c]=d&a[b][e]=f', + // 'array, repeat', + // ); + // st.equal( + // stringify(withArray, { encode: false, arrayFormat: 'comma' }), + // '???', + // 'array, comma', + // { skip: 'TODO: figure out what this should do' }, + // ); + expect(stringify(withArray, { encode: false })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'brackets' })).toBe('a[b][][c]=d&a[b][][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'indices' })).toBe('a[b][0][c]=d&a[b][0][e]=f'); + expect(stringify(withArray, { encode: false, arrayFormat: 'repeat' })).toBe('a[b][c]=d&a[b][e]=f'); + // !TODo: Figure out what this should do + // expect(stringify(withArray, { encode: false, arrayFormat: 'comma' })).toBe( + // 'a[b][c]=d&a[b][e]=f', + // ); + }); + + test('stringifies sparse arrays', function () { + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + // 'a[1]=2&a[4]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + // 'a[]=2&a[]=1', + // ); + // st.equal( + // stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + // 'a=2&a=1', + // ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'indices' })).toBe( + 'a[1]=2&a[4]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'brackets' })).toBe( + 'a[]=2&a[]=1', + ); + expect(stringify({ a: [, '2', , , '1'] }, { encodeValuesOnly: true, arrayFormat: 'repeat' })).toBe( + 'a=2&a=1', + ); + + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][b][2][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][b][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, { b: [, , { c: '1' }] }] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[b][c]=1', + // ); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][b][2][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][b][][c]=1'); + expect( + stringify({ a: [, { b: [, , { c: '1' }] }] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[b][c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: '1' }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c]=1'); + expect( + stringify({ a: [, [, , [, , , { c: '1' }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'indices' }, + // ), + // 'a[1][2][3][c][1]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'brackets' }, + // ), + // 'a[][][][c][]=1', + // ); + // st.equal( + // stringify( + // { a: [, [, , [, , , { c: [, '1'] }]]] }, + // { encodeValuesOnly: true, arrayFormat: 'repeat' }, + // ), + // 'a[c]=1', + // ); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'indices' }), + ).toBe('a[1][2][3][c][1]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'brackets' }), + ).toBe('a[][][][c][]=1'); + expect( + stringify({ a: [, [, , [, , , { c: [, '1'] }]]] }, { encodeValuesOnly: true, arrayFormat: 'repeat' }), + ).toBe('a[c]=1'); + }); + + test('encodes a very long string', function () { + var chars = []; + var expected = []; + for (var i = 0; i < 5e3; i++) { + chars.push(' ' + i); + + expected.push('%20' + i); + } + + var obj = { + foo: chars.join(''), + }; + + // st.equal( + // stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' }), + // 'foo=' + expected.join(''), + // ); + // @ts-expect-error + expect(stringify(obj, { arrayFormat: 'bracket', charset: 'utf-8' })).toBe('foo=' + expected.join('')); + }); +}); + +describe('stringifies empty keys', function () { + empty_test_cases.forEach(function (testCase) { + test('stringifies an object with empty string key with ' + testCase.input, function () { + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'indices' }), + // testCase.stringifyOutput.indices, + // 'test case: ' + testCase.input + ', indices', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'brackets' }), + // testCase.stringifyOutput.brackets, + // 'test case: ' + testCase.input + ', brackets', + // ); + // st.deepEqual( + // stringify(testCase.withEmptyKeys, { encode: false, arrayFormat: 'repeat' }), + // testCase.stringifyOutput.repeat, + // 'test case: ' + testCase.input + ', repeat', + // ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'indices' })).toBe( + testCase.stringify_output.indices, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'brackets' })).toBe( + testCase.stringify_output.brackets, + ); + expect(stringify(testCase.with_empty_keys, { encode: false, arrayFormat: 'repeat' })).toBe( + testCase.stringify_output.repeat, + ); + }); + }); + + test('edge case with object/arrays', function () { + // st.deepEqual(stringify({ '': { '': [2, 3] } }, { encode: false }), '[][0]=2&[][1]=3'); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3', + // ); + // st.deepEqual( + // stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' }), + // '[][0]=2&[][1]=3&[a]=2', + // ); + expect(stringify({ '': { '': [2, 3] } }, { encode: false })).toBe('[][0]=2&[][1]=3'); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false })).toBe('[][0]=2&[][1]=3&[a]=2'); + expect(stringify({ '': { '': [2, 3] } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3', + ); + expect(stringify({ '': { '': [2, 3], a: 2 } }, { encode: false, arrayFormat: 'indices' })).toBe( + '[][0]=2&[][1]=3&[a]=2', + ); + }); +}); diff --git a/tests/qs/utils.test.ts b/tests/qs/utils.test.ts new file mode 100644 index 00000000..ce9680dd --- /dev/null +++ b/tests/qs/utils.test.ts @@ -0,0 +1,169 @@ +import { combine, merge, is_buffer, assign_single_source } from '@anthropic-ai/sdk/internal/qs/utils'; + +describe('merge()', function () { + // t.deepEqual(merge(null, true), [null, true], 'merges true into null'); + expect(merge(null, true)).toEqual([null, true]); + + // t.deepEqual(merge(null, [42]), [null, 42], 'merges null into an array'); + expect(merge(null, [42])).toEqual([null, 42]); + + // t.deepEqual( + // merge({ a: 'b' }, { a: 'c' }), + // { a: ['b', 'c'] }, + // 'merges two objects with the same key', + // ); + expect(merge({ a: 'b' }, { a: 'c' })).toEqual({ a: ['b', 'c'] }); + + var oneMerged = merge({ foo: 'bar' }, { foo: { first: '123' } }); + // t.deepEqual( + // oneMerged, + // { foo: ['bar', { first: '123' }] }, + // 'merges a standalone and an object into an array', + // ); + expect(oneMerged).toEqual({ foo: ['bar', { first: '123' }] }); + + var twoMerged = merge({ foo: ['bar', { first: '123' }] }, { foo: { second: '456' } }); + // t.deepEqual( + // twoMerged, + // { foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }, + // 'merges a standalone and two objects into an array', + // ); + expect(twoMerged).toEqual({ foo: { 0: 'bar', 1: { first: '123' }, second: '456' } }); + + var sandwiched = merge({ foo: ['bar', { first: '123', second: '456' }] }, { foo: 'baz' }); + // t.deepEqual( + // sandwiched, + // { foo: ['bar', { first: '123', second: '456' }, 'baz'] }, + // 'merges an object sandwiched by two standalones into an array', + // ); + expect(sandwiched).toEqual({ foo: ['bar', { first: '123', second: '456' }, 'baz'] }); + + var nestedArrays = merge({ foo: ['baz'] }, { foo: ['bar', 'xyzzy'] }); + // t.deepEqual(nestedArrays, { foo: ['baz', 'bar', 'xyzzy'] }); + expect(nestedArrays).toEqual({ foo: ['baz', 'bar', 'xyzzy'] }); + + var noOptionsNonObjectSource = merge({ foo: 'baz' }, 'bar'); + // t.deepEqual(noOptionsNonObjectSource, { foo: 'baz', bar: true }); + expect(noOptionsNonObjectSource).toEqual({ foo: 'baz', bar: true }); + + (typeof Object.defineProperty !== 'function' ? test.skip : test)( + 'avoids invoking array setters unnecessarily', + function () { + var setCount = 0; + var getCount = 0; + var observed: any[] = []; + Object.defineProperty(observed, 0, { + get: function () { + getCount += 1; + return { bar: 'baz' }; + }, + set: function () { + setCount += 1; + }, + }); + merge(observed, [null]); + // st.equal(setCount, 0); + // st.equal(getCount, 1); + expect(setCount).toEqual(0); + expect(getCount).toEqual(1); + observed[0] = observed[0]; + // st.equal(setCount, 1); + // st.equal(getCount, 2); + expect(setCount).toEqual(1); + expect(getCount).toEqual(2); + }, + ); +}); + +test('assign()', function () { + var target = { a: 1, b: 2 }; + var source = { b: 3, c: 4 }; + var result = assign_single_source(target, source); + + expect(result).toEqual(target); + expect(target).toEqual({ a: 1, b: 3, c: 4 }); + expect(source).toEqual({ b: 3, c: 4 }); +}); + +describe('combine()', function () { + test('both arrays', function () { + var a = [1]; + var b = [2]; + var combined = combine(a, b); + + // st.deepEqual(a, [1], 'a is not mutated'); + // st.deepEqual(b, [2], 'b is not mutated'); + // st.notEqual(a, combined, 'a !== combined'); + // st.notEqual(b, combined, 'b !== combined'); + // st.deepEqual(combined, [1, 2], 'combined is a + b'); + expect(a).toEqual([1]); + expect(b).toEqual([2]); + expect(combined).toEqual([1, 2]); + expect(a).not.toEqual(combined); + expect(b).not.toEqual(combined); + }); + + test('one array, one non-array', function () { + var aN = 1; + var a = [aN]; + var bN = 2; + var b = [bN]; + + var combinedAnB = combine(aN, b); + // st.deepEqual(b, [bN], 'b is not mutated'); + // st.notEqual(aN, combinedAnB, 'aN + b !== aN'); + // st.notEqual(a, combinedAnB, 'aN + b !== a'); + // st.notEqual(bN, combinedAnB, 'aN + b !== bN'); + // st.notEqual(b, combinedAnB, 'aN + b !== b'); + // st.deepEqual([1, 2], combinedAnB, 'first argument is array-wrapped when not an array'); + expect(b).toEqual([bN]); + expect(combinedAnB).not.toEqual(aN); + expect(combinedAnB).not.toEqual(a); + expect(combinedAnB).not.toEqual(bN); + expect(combinedAnB).not.toEqual(b); + expect(combinedAnB).toEqual([1, 2]); + + var combinedABn = combine(a, bN); + // st.deepEqual(a, [aN], 'a is not mutated'); + // st.notEqual(aN, combinedABn, 'a + bN !== aN'); + // st.notEqual(a, combinedABn, 'a + bN !== a'); + // st.notEqual(bN, combinedABn, 'a + bN !== bN'); + // st.notEqual(b, combinedABn, 'a + bN !== b'); + // st.deepEqual([1, 2], combinedABn, 'second argument is array-wrapped when not an array'); + expect(a).toEqual([aN]); + expect(combinedABn).not.toEqual(aN); + expect(combinedABn).not.toEqual(a); + expect(combinedABn).not.toEqual(bN); + expect(combinedABn).not.toEqual(b); + expect(combinedABn).toEqual([1, 2]); + }); + + test('neither is an array', function () { + var combined = combine(1, 2); + // st.notEqual(1, combined, '1 + 2 !== 1'); + // st.notEqual(2, combined, '1 + 2 !== 2'); + // st.deepEqual([1, 2], combined, 'both arguments are array-wrapped when not an array'); + expect(combined).not.toEqual(1); + expect(combined).not.toEqual(2); + expect(combined).toEqual([1, 2]); + }); +}); + +test('is_buffer()', function () { + for (const x of [null, undefined, true, false, '', 'abc', 42, 0, NaN, {}, [], function () {}, /a/g]) { + // t.equal(is_buffer(x), false, inspect(x) + ' is not a buffer'); + expect(is_buffer(x)).toEqual(false); + } + + var fakeBuffer = { constructor: Buffer }; + // t.equal(is_buffer(fakeBuffer), false, 'fake buffer is not a buffer'); + expect(is_buffer(fakeBuffer)).toEqual(false); + + var saferBuffer = Buffer.from('abc'); + // t.equal(is_buffer(saferBuffer), true, 'SaferBuffer instance is a buffer'); + expect(is_buffer(saferBuffer)).toEqual(true); + + var buffer = Buffer.from('abc'); + // t.equal(is_buffer(buffer), true, 'real Buffer instance is a buffer'); + expect(is_buffer(buffer)).toEqual(true); +}); diff --git a/tests/stringifyQuery.test.ts b/tests/stringifyQuery.test.ts index 2690d773..b4b1942d 100644 --- a/tests/stringifyQuery.test.ts +++ b/tests/stringifyQuery.test.ts @@ -18,10 +18,4 @@ describe(stringifyQuery, () => { expect(stringifyQuery(input)).toEqual(expected); }); } - - for (const value of [[], {}, new Date()]) { - it(`${JSON.stringify(value)} -> `, () => { - expect(() => stringifyQuery({ value })).toThrow(`Cannot stringify type ${typeof value}`); - }); - } }); diff --git a/yarn.lock b/yarn.lock index 37a9bfb9..b8158ccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -800,6 +800,11 @@ dependencies: "@sinonjs/commons" "^3.0.0" +"@stablelib/base64@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@stablelib/base64/-/base64-1.0.1.tgz#bdfc1c6d3a62d7a3b7bbc65b6cce1bb4561641be" + integrity sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ== + "@swc/core-darwin-arm64@1.4.16": version "1.4.16" resolved "https://registry.yarnpkg.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.4.16.tgz#2cd45d709ce76d448d96bf8d0006849541436611" @@ -1981,10 +1986,6 @@ fast-levenshtein@^2.0.6: resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== -fast-uri@^3.0.1: - version "3.1.0" - resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa" - integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA== fastq@^1.6.0: version "1.17.1" @@ -3671,15 +3672,6 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" -statuses@^2.0.1, statuses@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382" - integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw== - -strict-event-emitter@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz#1602ece81c51574ca39c6815e09f1a3e8550bd93" - integrity sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ== string-length@^4.0.1: version "4.0.2"