diff --git a/.fern/metadata.json b/.fern/metadata.json new file mode 100644 index 0000000..f28f365 --- /dev/null +++ b/.fern/metadata.json @@ -0,0 +1,8 @@ +{ + "cliVersion": "0.0.0", + "generatorName": "fernapi/fern-typescript-sdk", + "generatorVersion": "99.99.99", + "generatorConfig": { + "namespaceExport": "Lattice" + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21fe64b..2db6106 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,7 @@ jobs: uses: actions/checkout@v4 - name: Set up node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -30,7 +30,7 @@ jobs: uses: actions/checkout@v4 - name: Set up node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -50,7 +50,7 @@ jobs: uses: actions/checkout@v4 - name: Set up node - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -64,12 +64,15 @@ jobs: - name: Publish to npm run: | npm config set //registry.npmjs.org/:_authToken ${NPM_TOKEN} + publish() { # use latest npm to ensure OIDC support + npx -y npm@latest publish "$@" + } if [[ ${GITHUB_REF} == *alpha* ]]; then - npm publish --access public --tag alpha + publish --access public --tag alpha elif [[ ${GITHUB_REF} == *beta* ]]; then - npm publish --access public --tag beta + publish --access public --tag beta else - npm publish --access public + publish --access public fi env: NPM_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file diff --git a/.npmignore b/.npmignore index b7e5ad3..c0c40ac 100644 --- a/.npmignore +++ b/.npmignore @@ -4,6 +4,7 @@ tests .gitignore .github .fernignore +.prettierrc.yml biome.json tsconfig.json yarn.lock diff --git a/LICENSE b/LICENSE index 9623e9f..87b1a69 100644 --- a/LICENSE +++ b/LICENSE @@ -186,4 +186,4 @@ of any court action, you agree to submit to the exclusive jurisdiction of the co Notwithstanding this, you agree that Anduril shall still be allowed to apply for injunctive remedies (or an equivalent type of urgent legal relief) in any jurisdiction. -**April 14, 2025** \ No newline at end of file +**April 14, 2025** diff --git a/README.md b/README.md deleted file mode 100644 index 073c6dc..0000000 --- a/README.md +++ /dev/null @@ -1,649 +0,0 @@ -# Lattice SDK TypeScript Library - -![](https://www.anduril.com/lattice-sdk/) - -[![npm shield](https://img.shields.io/npm/v/@anduril-industries/lattice-sdk)](https://www.npmjs.com/package/@anduril-industries/lattice-sdk) - -The Lattice SDK TypeScript library provides convenient access to the Lattice SDK APIs from TypeScript. - -## Documentation - -API reference documentation is available [here](https://developer.anduril.com/). - -## Requirements - -To use the SDK please ensure you have the following installed: - -- [NodeJS](https://nodejs.org/en/download/package-manager) - -## Installation - -```sh -npm i -s @anduril-industries/lattice-sdk -``` - -## Support - -For support with this library, please reach out to your Anduril representative. - -## Reference - -A full reference for this library is available [here](https://github.com/anduril/lattice-sdk-javascript/blob/HEAD/./reference.md). - -## Usage - -Instantiate and use the client with the following: - -```typescript -import { LatticeClient } from "@anduril-industries/lattice-sdk"; - -const client = new LatticeClient({ token: "YOUR_TOKEN" }); -await client.entities.longPollEntityEvents({ - sessionToken: "sessionToken" -}); -``` - -## Request And Response Types - -The SDK exports all request and response types as TypeScript interfaces. Simply import them with the -following namespace: - -```typescript -import { Lattice } from "@anduril-industries/lattice-sdk"; - -const request: Lattice.EntityOverride = { - ... -}; -``` - -## Exception Handling - -When the API returns a non-success status code (4xx or 5xx response), a subclass of the following error -will be thrown. - -```typescript -import { LatticeError } from "@anduril-industries/lattice-sdk"; - -try { - await client.entities.longPollEntityEvents(...); -} catch (err) { - if (err instanceof LatticeError) { - console.log(err.statusCode); - console.log(err.message); - console.log(err.body); - console.log(err.rawResponse); - } -} -``` - -## File Uploads - -You can upload files using the client: - -```typescript -import { createReadStream } from "fs"; - -await client.objects.uploadObject(createReadStream("path/to/file"), ...); -await client.objects.uploadObject(new ReadableStream(), ...); -await client.objects.uploadObject(Buffer.from('binary data'), ...); -await client.objects.uploadObject(new Blob(['binary data'], { type: 'audio/mpeg' }), ...); -await client.objects.uploadObject(new File(['binary data'], 'file.mp3'), ...); -await client.objects.uploadObject(new ArrayBuffer(8), ...); -await client.objects.uploadObject(new Uint8Array([0, 1, 2]), ...); -``` -The client accepts a variety of types for file upload parameters: -* Stream types: `fs.ReadStream`, `stream.Readable`, and `ReadableStream` -* Buffered types: `Buffer`, `Blob`, `File`, `ArrayBuffer`, `ArrayBufferView`, and `Uint8Array` - -### Metadata - -You can configure metadata when uploading a file: -```typescript -const file: Uploadable.WithMetadata = { - data: createReadStream("path/to/file"), - filename: "my-file", // optional - contentType: "audio/mpeg", // optional - contentLength: 1949, // optional -}; -``` - -Alternatively, you can upload a file directly from a file path: -```typescript -const file : Uploadable.FromPath = { - path: "path/to/file", - filename: "my-file", // optional - contentType: "audio/mpeg", // optional - contentLength: 1949, // optional -}; -``` - -The metadata is used to set the `Content-Length`, `Content-Type`, and `Content-Disposition` headers. If not provided, the client will attempt to determine them automatically. -For example, `fs.ReadStream` has a `path` property which the SDK uses to retrieve the file size from the filesystem without loading it into memory. - - -## Binary Response - -You can consume binary data from endpoints using the `BinaryResponse` type which lets you choose how to consume the data: - -```typescript -const response = await client.objects.getObject(...); -const stream: ReadableStream = response.stream(); -// const arrayBuffer: ArrayBuffer = await response.arrayBuffer(); -// const blob: Blob = response.blob(); -// const bytes: Uint8Array = response.bytes(); -// You can only use the response body once, so you must choose one of the above methods. -// If you want to check if the response body has been used, you can use the following property. -const bodyUsed = response.bodyUsed; -``` -
-Save binary response to a file - -
-
-Node.js - -
-
-ReadableStream (most-efficient) - -```ts -import { createWriteStream } from 'fs'; -import { Readable } from 'stream'; -import { pipeline } from 'stream/promises'; - -const response = await client.objects.getObject(...); - -const stream = response.stream(); -const nodeStream = Readable.fromWeb(stream); -const writeStream = createWriteStream('path/to/file'); - -await pipeline(nodeStream, writeStream); -``` - -
-
- -
-
-ArrayBuffer - -```ts -import { writeFile } from 'fs/promises'; - -const response = await client.objects.getObject(...); - -const arrayBuffer = await response.arrayBuffer(); -await writeFile('path/to/file', Buffer.from(arrayBuffer)); -``` - -
-
- -
-
-Blob - -```ts -import { writeFile } from 'fs/promises'; - -const response = await client.objects.getObject(...); - -const blob = await response.blob(); -const arrayBuffer = await blob.arrayBuffer(); -await writeFile('output.bin', Buffer.from(arrayBuffer)); -``` - -
-
- -
-
-Bytes (UIntArray8) - -```ts -import { writeFile } from 'fs/promises'; - -const response = await client.objects.getObject(...); - -const bytes = await response.bytes(); -await writeFile('path/to/file', bytes); -``` - -
-
- -
-
- -
-
-Bun - -
-
-ReadableStream (most-efficient) - -```ts -const response = await client.objects.getObject(...); - -const stream = response.stream(); -await Bun.write('path/to/file', stream); -``` - -
-
- -
-
-ArrayBuffer - -```ts -const response = await client.objects.getObject(...); - -const arrayBuffer = await response.arrayBuffer(); -await Bun.write('path/to/file', arrayBuffer); -``` - -
-
- -
-
-Blob - -```ts -const response = await client.objects.getObject(...); - -const blob = await response.blob(); -await Bun.write('path/to/file', blob); -``` - -
-
- -
-
-Bytes (UIntArray8) - -```ts -const response = await client.objects.getObject(...); - -const bytes = await response.bytes(); -await Bun.write('path/to/file', bytes); -``` - -
-
- -
-
- -
-
-Deno - -
-
-ReadableStream (most-efficient) - -```ts -const response = await client.objects.getObject(...); - -const stream = response.stream(); -const file = await Deno.open('path/to/file', { write: true, create: true }); -await stream.pipeTo(file.writable); -``` - -
-
- -
-
-ArrayBuffer - -```ts -const response = await client.objects.getObject(...); - -const arrayBuffer = await response.arrayBuffer(); -await Deno.writeFile('path/to/file', new Uint8Array(arrayBuffer)); -``` - -
-
- -
-
-Blob - -```ts -const response = await client.objects.getObject(...); - -const blob = await response.blob(); -const arrayBuffer = await blob.arrayBuffer(); -await Deno.writeFile('path/to/file', new Uint8Array(arrayBuffer)); -``` - -
-
- -
-
-Bytes (UIntArray8) - -```ts -const response = await client.objects.getObject(...); - -const bytes = await response.bytes(); -await Deno.writeFile('path/to/file', bytes); -``` - -
-
- -
-
- -
-
-Browser - -
-
-Blob (most-efficient) - -```ts -const response = await client.objects.getObject(...); - -const blob = await response.blob(); -const url = URL.createObjectURL(blob); - -// trigger download -const a = document.createElement('a'); -a.href = url; -a.download = 'filename'; -a.click(); -URL.revokeObjectURL(url); -``` - -
-
- -
-
-ReadableStream - -```ts -const response = await client.objects.getObject(...); - -const stream = response.stream(); -const reader = stream.getReader(); -const chunks = []; - -while (true) { - const { done, value } = await reader.read(); - if (done) break; - chunks.push(value); -} - -const blob = new Blob(chunks); -const url = URL.createObjectURL(blob); - -// trigger download -const a = document.createElement('a'); -a.href = url; -a.download = 'filename'; -a.click(); -URL.revokeObjectURL(url); -``` - -
-
- -
-
-ArrayBuffer - -```ts -const response = await client.objects.getObject(...); - -const arrayBuffer = await response.arrayBuffer(); -const blob = new Blob([arrayBuffer]); -const url = URL.createObjectURL(blob); - -// trigger download -const a = document.createElement('a'); -a.href = url; -a.download = 'filename'; -a.click(); -URL.revokeObjectURL(url); -``` - -
-
- -
-
-Bytes (UIntArray8) - -```ts -const response = await client.objects.getObject(...); - -const bytes = await response.bytes(); -const blob = new Blob([bytes]); -const url = URL.createObjectURL(blob); - -// trigger download -const a = document.createElement('a'); -a.href = url; -a.download = 'filename'; -a.click(); -URL.revokeObjectURL(url); -``` - -
-
- -
-
- -
- - -
-Convert binary response to text - -
-
-ReadableStream - -```ts -const response = await client.objects.getObject(...); - -const stream = response.stream(); -const text = await new Response(stream).text(); -``` - -
-
- -
-
-ArrayBuffer - -```ts -const response = await client.objects.getObject(...); - -const arrayBuffer = await response.arrayBuffer(); -const text = new TextDecoder().decode(arrayBuffer); -``` - -
-
- -
-
-Blob - -```ts -const response = await client.objects.getObject(...); - -const blob = await response.blob(); -const text = await blob.text(); -``` - -
-
- -
-
-Bytes (UIntArray8) - -```ts -const response = await client.objects.getObject(...); - -const bytes = await response.bytes(); -const text = new TextDecoder().decode(bytes); -``` - -
-
- -
- -## Pagination - -List endpoints are paginated. The SDK provides an iterator so that you can simply loop over the items: - -```typescript -import { LatticeClient } from "@anduril-industries/lattice-sdk"; - -const client = new LatticeClient({ token: "YOUR_TOKEN" }); -const response = await client.objects.listObjects({ - prefix: "prefix", - sinceTimestamp: "2024-01-15T09:30:00Z", - pageToken: "pageToken", - allObjectsInMesh: true -}); -for await (const item of response) { - console.log(item); -} - -// Or you can manually iterate page-by-page -let page = await client.objects.listObjects({ - prefix: "prefix", - sinceTimestamp: "2024-01-15T09:30:00Z", - pageToken: "pageToken", - allObjectsInMesh: true -}); -while (page.hasNextPage()) { - page = page.getNextPage(); -} -``` - -## Advanced - -### Additional Headers - -If you would like to send additional headers as part of the request, use the `headers` request option. - -```typescript -const response = await client.entities.longPollEntityEvents(..., { - headers: { - 'X-Custom-Header': 'custom value' - } -}); -``` - -### Additional Query String Parameters - -If you would like to send additional query string parameters as part of the request, use the `queryParams` request option. - -```typescript -const response = await client.entities.longPollEntityEvents(..., { - queryParams: { - 'customQueryParamKey': 'custom query param value' - } -}); -``` - -### Retries - -The SDK is instrumented with automatic retries with exponential backoff. A request will be retried as long -as the request is deemed retryable and the number of retry attempts has not grown larger than the configured -retry limit (default: 2). - -A request is deemed retryable when any of the following HTTP status codes is returned: - -- [408](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/408) (Timeout) -- [429](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429) (Too Many Requests) -- [5XX](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500) (Internal Server Errors) - -Use the `maxRetries` request option to configure this behavior. - -```typescript -const response = await client.entities.longPollEntityEvents(..., { - maxRetries: 0 // override maxRetries at the request level -}); -``` - -### Timeouts - -The SDK defaults to a 60 second timeout. Use the `timeoutInSeconds` option to configure this behavior. - -```typescript -const response = await client.entities.longPollEntityEvents(..., { - timeoutInSeconds: 30 // override timeout to 30s -}); -``` - -### Aborting Requests - -The SDK allows users to abort requests at any point by passing in an abort signal. - -```typescript -const controller = new AbortController(); -const response = await client.entities.longPollEntityEvents(..., { - abortSignal: controller.signal -}); -controller.abort(); // aborts the request -``` - -### Access Raw Response Data - -The SDK provides access to raw response data, including headers, through the `.withRawResponse()` method. -The `.withRawResponse()` method returns a promise that results to an object with a `data` and a `rawResponse` property. - -```typescript -const { data, rawResponse } = await client.entities.longPollEntityEvents(...).withRawResponse(); - -console.log(data); -console.log(rawResponse.headers['X-My-Header']); -``` - -### Runtime Compatibility - - -The SDK works in the following runtimes: - - - -- Node.js 18+ -- Vercel -- Cloudflare Workers -- Deno v1.25+ -- Bun 1.0+ -- React Native - -### Customizing Fetch Client - -The SDK provides a way for you to customize the underlying HTTP client / Fetch function. If you're running in an -unsupported environment, this provides a way for you to break glass and ensure the SDK works. - -```typescript -import { LatticeClient } from "@anduril-industries/lattice-sdk"; - -const client = new LatticeClient({ - ... - fetcher: // provide your implementation here -}); -``` diff --git a/biome.json b/biome.json index b6890df..a777468 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.1/schema.json", "root": true, "vcs": { "enabled": false @@ -7,16 +7,21 @@ "files": { "ignoreUnknown": true, "includes": [ - "./**", - "!dist", - "!lib", - "!*.tsbuildinfo", - "!_tmp_*", - "!*.tmp", - "!.tmp/", - "!*.log", - "!.DS_Store", - "!Thumbs.db" + "**", + "!!dist", + "!!**/dist", + "!!lib", + "!!**/lib", + "!!_tmp_*", + "!!**/_tmp_*", + "!!*.tmp", + "!!**/*.tmp", + "!!.tmp/", + "!!**/.tmp/", + "!!*.log", + "!!**/*.log", + "!!**/.DS_Store", + "!!**/Thumbs.db" ] }, "formatter": { diff --git a/package.json b/package.json index 8584c1c..d89686c 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,8 @@ { "name": "@anduril-industries/lattice-sdk", - "version": "3.0.0", + "version": "3.0.1", "private": false, - "repository": "github:anduril/lattice-sdk-javascript", - "license": "See LICENSE", + "repository": "github:fern-api/lattice-sdk-javascript", "type": "commonjs", "main": "./dist/cjs/index.js", "module": "./dist/esm/index.mjs", @@ -31,6 +30,9 @@ ], "scripts": { "format": "biome format --write --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "format:check": "biome format --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint": "biome lint --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", + "lint:fix": "biome lint --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", "check": "biome check --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", "check:fix": "biome check --fix --unsafe --skip-parse-errors --no-errors-on-unmatched --max-diagnostics=none", "build": "pnpm build:cjs && pnpm build:esm", @@ -40,14 +42,15 @@ "test:unit": "vitest --project unit", "test:wire": "vitest --project wire" }, + "dependencies": {}, "devDependencies": { "webpack": "^5.97.1", "ts-loader": "^9.5.1", "vitest": "^3.2.4", "msw": "2.11.2", "@types/node": "^18.19.70", - "@biomejs/biome": "2.2.5", - "typescript": "~5.7.2" + "typescript": "~5.7.2", + "@biomejs/biome": "2.3.1" }, "browser": { "fs": false, @@ -55,7 +58,7 @@ "path": false, "stream": false }, - "packageManager": "pnpm@10.14.0", + "packageManager": "pnpm@10.20.0", "engines": { "node": ">=18.0.0" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5b89b8e..589fe51 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: devDependencies: '@biomejs/biome': - specifier: 2.2.5 - version: 2.2.5 + specifier: 2.3.1 + version: 2.3.1 '@types/node': specifier: ^18.19.70 version: 18.19.130 @@ -32,55 +32,55 @@ importers: packages: - '@biomejs/biome@2.2.5': - resolution: {integrity: sha512-zcIi+163Rc3HtyHbEO7CjeHq8DjQRs40HsGbW6vx2WI0tg8mYQOPouhvHSyEnCBAorfYNnKdR64/IxO7xQ5faw==} + '@biomejs/biome@2.3.1': + resolution: {integrity: sha512-A29evf1R72V5bo4o2EPxYMm5mtyGvzp2g+biZvRFx29nWebGyyeOSsDWGx3tuNNMFRepGwxmA9ZQ15mzfabK2w==} engines: {node: '>=14.21.3'} hasBin: true - '@biomejs/cli-darwin-arm64@2.2.5': - resolution: {integrity: sha512-MYT+nZ38wEIWVcL5xLyOhYQQ7nlWD0b/4mgATW2c8dvq7R4OQjt/XGXFkXrmtWmQofaIM14L7V8qIz/M+bx5QQ==} + '@biomejs/cli-darwin-arm64@2.3.1': + resolution: {integrity: sha512-ombSf3MnTUueiYGN1SeI9tBCsDUhpWzOwS63Dove42osNh0PfE1cUtHFx6eZ1+MYCCLwXzlFlYFdrJ+U7h6LcA==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [darwin] - '@biomejs/cli-darwin-x64@2.2.5': - resolution: {integrity: sha512-FLIEl73fv0R7dI10EnEiZLw+IMz3mWLnF95ASDI0kbx6DDLJjWxE5JxxBfmG+udz1hIDd3fr5wsuP7nwuTRdAg==} + '@biomejs/cli-darwin-x64@2.3.1': + resolution: {integrity: sha512-pcOfwyoQkrkbGvXxRvZNe5qgD797IowpJPovPX5biPk2FwMEV+INZqfCaz4G5bVq9hYnjwhRMamg11U4QsRXrQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [darwin] - '@biomejs/cli-linux-arm64-musl@2.2.5': - resolution: {integrity: sha512-5Ov2wgAFwqDvQiESnu7b9ufD1faRa+40uwrohgBopeY84El2TnBDoMNXx6iuQdreoFGjwW8vH6k68G21EpNERw==} + '@biomejs/cli-linux-arm64-musl@2.3.1': + resolution: {integrity: sha512-+DZYv8l7FlUtTrWs1Tdt1KcNCAmRO87PyOnxKGunbWm5HKg1oZBSbIIPkjrCtDZaeqSG1DiGx7qF+CPsquQRcg==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-arm64@2.2.5': - resolution: {integrity: sha512-5DjiiDfHqGgR2MS9D+AZ8kOfrzTGqLKywn8hoXpXXlJXIECGQ32t+gt/uiS2XyGBM2XQhR6ztUvbjZWeccFMoQ==} + '@biomejs/cli-linux-arm64@2.3.1': + resolution: {integrity: sha512-td5O8pFIgLs8H1sAZsD6v+5quODihyEw4nv2R8z7swUfIK1FKk+15e4eiYVLcAE4jUqngvh4j3JCNgg0Y4o4IQ==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [linux] - '@biomejs/cli-linux-x64-musl@2.2.5': - resolution: {integrity: sha512-AVqLCDb/6K7aPNIcxHaTQj01sl1m989CJIQFQEaiQkGr2EQwyOpaATJ473h+nXDUuAcREhccfRpe/tu+0wu0eQ==} + '@biomejs/cli-linux-x64-musl@2.3.1': + resolution: {integrity: sha512-Y3Ob4nqgv38Mh+6EGHltuN+Cq8aj/gyMTJYzkFZV2AEj+9XzoXB9VNljz9pjfFNHUxvLEV4b55VWyxozQTBaUQ==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-linux-x64@2.2.5': - resolution: {integrity: sha512-fq9meKm1AEXeAWan3uCg6XSP5ObA6F/Ovm89TwaMiy1DNIwdgxPkNwxlXJX8iM6oRbFysYeGnT0OG8diCWb9ew==} + '@biomejs/cli-linux-x64@2.3.1': + resolution: {integrity: sha512-PYWgEO7up7XYwSAArOpzsVCiqxBCXy53gsReAb1kKYIyXaoAlhBaBMvxR/k2Rm9aTuZ662locXUmPk/Aj+Xu+Q==} engines: {node: '>=14.21.3'} cpu: [x64] os: [linux] - '@biomejs/cli-win32-arm64@2.2.5': - resolution: {integrity: sha512-xaOIad4wBambwJa6mdp1FigYSIF9i7PCqRbvBqtIi9y29QtPVQ13sDGtUnsRoe6SjL10auMzQ6YAe+B3RpZXVg==} + '@biomejs/cli-win32-arm64@2.3.1': + resolution: {integrity: sha512-RHIG/zgo+69idUqVvV3n8+j58dKYABRpMyDmfWu2TITC+jwGPiEaT0Q3RKD+kQHiS80mpBrST0iUGeEXT0bU9A==} engines: {node: '>=14.21.3'} cpu: [arm64] os: [win32] - '@biomejs/cli-win32-x64@2.2.5': - resolution: {integrity: sha512-F/jhuXCssPFAuciMhHKk00xnCAxJRS/pUzVfXYmOMUp//XW7mO6QeCjsjvnm8L4AO/dG2VOB0O+fJPiJ2uXtIw==} + '@biomejs/cli-win32-x64@2.3.1': + resolution: {integrity: sha512-izl30JJ5Dp10mi90Eko47zhxE6pYyWPcnX1NQxKpL/yMhXxf95oLTzfpu4q+MDBh/gemNqyJEwjBpe0MT5iWPA==} engines: {node: '>=14.21.3'} cpu: [x64] os: [win32] @@ -91,158 +91,158 @@ packages: '@bundled-es-modules/statuses@1.0.1': resolution: {integrity: sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==} - '@esbuild/aix-ppc64@0.25.11': - resolution: {integrity: sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==} + '@esbuild/aix-ppc64@0.25.12': + resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] - '@esbuild/android-arm64@0.25.11': - resolution: {integrity: sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==} + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} engines: {node: '>=18'} cpu: [arm64] os: [android] - '@esbuild/android-arm@0.25.11': - resolution: {integrity: sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==} + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} engines: {node: '>=18'} cpu: [arm] os: [android] - '@esbuild/android-x64@0.25.11': - resolution: {integrity: sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==} + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} engines: {node: '>=18'} cpu: [x64] os: [android] - '@esbuild/darwin-arm64@0.25.11': - resolution: {integrity: sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==} + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] - '@esbuild/darwin-x64@0.25.11': - resolution: {integrity: sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==} + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} engines: {node: '>=18'} cpu: [x64] os: [darwin] - '@esbuild/freebsd-arm64@0.25.11': - resolution: {integrity: sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==} + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-x64@0.25.11': - resolution: {integrity: sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==} + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] - '@esbuild/linux-arm64@0.25.11': - resolution: {integrity: sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==} + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} engines: {node: '>=18'} cpu: [arm64] os: [linux] - '@esbuild/linux-arm@0.25.11': - resolution: {integrity: sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==} + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} engines: {node: '>=18'} cpu: [arm] os: [linux] - '@esbuild/linux-ia32@0.25.11': - resolution: {integrity: sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==} + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} engines: {node: '>=18'} cpu: [ia32] os: [linux] - '@esbuild/linux-loong64@0.25.11': - resolution: {integrity: sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==} + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} engines: {node: '>=18'} cpu: [loong64] os: [linux] - '@esbuild/linux-mips64el@0.25.11': - resolution: {integrity: sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==} + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] - '@esbuild/linux-ppc64@0.25.11': - resolution: {integrity: sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==} + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] - '@esbuild/linux-riscv64@0.25.11': - resolution: {integrity: sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==} + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] - '@esbuild/linux-s390x@0.25.11': - resolution: {integrity: sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==} + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} engines: {node: '>=18'} cpu: [s390x] os: [linux] - '@esbuild/linux-x64@0.25.11': - resolution: {integrity: sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==} + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} engines: {node: '>=18'} cpu: [x64] os: [linux] - '@esbuild/netbsd-arm64@0.25.11': - resolution: {integrity: sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==} + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-x64@0.25.11': - resolution: {integrity: sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==} + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] - '@esbuild/openbsd-arm64@0.25.11': - resolution: {integrity: sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==} + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-x64@0.25.11': - resolution: {integrity: sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==} + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] - '@esbuild/openharmony-arm64@0.25.11': - resolution: {integrity: sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==} + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] - '@esbuild/sunos-x64@0.25.11': - resolution: {integrity: sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==} + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} engines: {node: '>=18'} cpu: [x64] os: [sunos] - '@esbuild/win32-arm64@0.25.11': - resolution: {integrity: sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==} + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} engines: {node: '>=18'} cpu: [arm64] os: [win32] - '@esbuild/win32-ia32@0.25.11': - resolution: {integrity: sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==} + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} engines: {node: '>=18'} cpu: [ia32] os: [win32] - '@esbuild/win32-x64@0.25.11': - resolution: {integrity: sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==} + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -567,16 +567,16 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - baseline-browser-mapping@2.8.19: - resolution: {integrity: sha512-zoKGUdu6vb2jd3YOq0nnhEDQVbPcHhco3UImJrv5dSkvxTc2pl2WjOPsjZXDwPDSl5eghIMuY3R6J9NDKF3KcQ==} + baseline-browser-mapping@2.8.23: + resolution: {integrity: sha512-616V5YX4bepJFzNyOfce5Fa8fDJMfoxzOIzDCZwaGL8MKVpFrXqfNUoIpRn9YMI5pXf/VKgzjB4htFMsFKKdiQ==} hasBin: true braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.26.3: - resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + browserslist@4.27.0: + resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -587,8 +587,8 @@ packages: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} - caniuse-lite@1.0.30001751: - resolution: {integrity: sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==} + caniuse-lite@1.0.30001753: + resolution: {integrity: sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==} chai@5.3.3: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} @@ -641,8 +641,8 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - electron-to-chromium@1.5.237: - resolution: {integrity: sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==} + electron-to-chromium@1.5.244: + resolution: {integrity: sha512-OszpBN7xZX4vWMPJwB9illkN/znA8M36GQqQxi6MNy9axWxhOfJyZZJtSLQCpEFLHP2xK33BiWx9aIuIEXVCcw==} emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} @@ -654,8 +654,8 @@ packages: es-module-lexer@1.7.0: resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - esbuild@0.25.11: - resolution: {integrity: sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==} + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} engines: {node: '>=18'} hasBin: true @@ -724,8 +724,8 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphql@16.11.0: - resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} + graphql@16.12.0: + resolution: {integrity: sha512-DKKrynuQRne0PNpEbzuEdHlYOMksHSUI8Zc9Unei5gTsMNA2/vMpoMz/yKba50pejK56qj98qM0SjYxAKi13gQ==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} has-flag@4.0.0: @@ -766,8 +766,8 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - magic-string@0.30.19: - resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} @@ -809,8 +809,8 @@ packages: neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - node-releases@2.0.26: - resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==} + node-releases@2.0.27: + resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==} outvariant@1.4.3: resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==} @@ -1009,8 +1009,8 @@ packages: undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - update-browserslist-db@1.1.3: - resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + update-browserslist-db@1.1.4: + resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1020,8 +1020,8 @@ packages: engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true - vite@7.1.11: - resolution: {integrity: sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==} + vite@7.1.12: + resolution: {integrity: sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -1137,39 +1137,39 @@ packages: snapshots: - '@biomejs/biome@2.2.5': + '@biomejs/biome@2.3.1': optionalDependencies: - '@biomejs/cli-darwin-arm64': 2.2.5 - '@biomejs/cli-darwin-x64': 2.2.5 - '@biomejs/cli-linux-arm64': 2.2.5 - '@biomejs/cli-linux-arm64-musl': 2.2.5 - '@biomejs/cli-linux-x64': 2.2.5 - '@biomejs/cli-linux-x64-musl': 2.2.5 - '@biomejs/cli-win32-arm64': 2.2.5 - '@biomejs/cli-win32-x64': 2.2.5 - - '@biomejs/cli-darwin-arm64@2.2.5': + '@biomejs/cli-darwin-arm64': 2.3.1 + '@biomejs/cli-darwin-x64': 2.3.1 + '@biomejs/cli-linux-arm64': 2.3.1 + '@biomejs/cli-linux-arm64-musl': 2.3.1 + '@biomejs/cli-linux-x64': 2.3.1 + '@biomejs/cli-linux-x64-musl': 2.3.1 + '@biomejs/cli-win32-arm64': 2.3.1 + '@biomejs/cli-win32-x64': 2.3.1 + + '@biomejs/cli-darwin-arm64@2.3.1': optional: true - '@biomejs/cli-darwin-x64@2.2.5': + '@biomejs/cli-darwin-x64@2.3.1': optional: true - '@biomejs/cli-linux-arm64-musl@2.2.5': + '@biomejs/cli-linux-arm64-musl@2.3.1': optional: true - '@biomejs/cli-linux-arm64@2.2.5': + '@biomejs/cli-linux-arm64@2.3.1': optional: true - '@biomejs/cli-linux-x64-musl@2.2.5': + '@biomejs/cli-linux-x64-musl@2.3.1': optional: true - '@biomejs/cli-linux-x64@2.2.5': + '@biomejs/cli-linux-x64@2.3.1': optional: true - '@biomejs/cli-win32-arm64@2.2.5': + '@biomejs/cli-win32-arm64@2.3.1': optional: true - '@biomejs/cli-win32-x64@2.2.5': + '@biomejs/cli-win32-x64@2.3.1': optional: true '@bundled-es-modules/cookie@2.0.1': @@ -1180,82 +1180,82 @@ snapshots: dependencies: statuses: 2.0.2 - '@esbuild/aix-ppc64@0.25.11': + '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/android-arm64@0.25.11': + '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm@0.25.11': + '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-x64@0.25.11': + '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.25.11': + '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-x64@0.25.11': + '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.25.11': + '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.25.11': + '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/linux-arm64@0.25.11': + '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm@0.25.11': + '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-ia32@0.25.11': + '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-loong64@0.25.11': + '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-mips64el@0.25.11': + '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-ppc64@0.25.11': + '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.25.11': + '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-s390x@0.25.11': + '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-x64@0.25.11': + '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.25.11': + '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.25.11': + '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.25.11': + '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.25.11': + '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.25.11': + '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/sunos-x64@0.25.11': + '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/win32-arm64@0.25.11': + '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-ia32@0.25.11': + '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-x64@0.25.11': + '@esbuild/win32-x64@0.25.12': optional: true '@inquirer/ansi@1.0.1': {} @@ -1426,14 +1426,14 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.11(@types/node@18.19.130)(terser@5.44.0))': + '@vitest/mocker@3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.12(@types/node@18.19.130)(terser@5.44.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 - magic-string: 0.30.19 + magic-string: 0.30.21 optionalDependencies: msw: 2.11.2(@types/node@18.19.130)(typescript@5.7.3) - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) + vite: 7.1.12(@types/node@18.19.130)(terser@5.44.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -1448,7 +1448,7 @@ snapshots: '@vitest/snapshot@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 '@vitest/spy@3.2.4': @@ -1571,25 +1571,25 @@ snapshots: assertion-error@2.0.1: {} - baseline-browser-mapping@2.8.19: {} + baseline-browser-mapping@2.8.23: {} braces@3.0.3: dependencies: fill-range: 7.1.1 - browserslist@4.26.3: + browserslist@4.27.0: dependencies: - baseline-browser-mapping: 2.8.19 - caniuse-lite: 1.0.30001751 - electron-to-chromium: 1.5.237 - node-releases: 2.0.26 - update-browserslist-db: 1.1.3(browserslist@4.26.3) + baseline-browser-mapping: 2.8.23 + caniuse-lite: 1.0.30001753 + electron-to-chromium: 1.5.244 + node-releases: 2.0.27 + update-browserslist-db: 1.1.4(browserslist@4.27.0) buffer-from@1.1.2: {} cac@6.7.14: {} - caniuse-lite@1.0.30001751: {} + caniuse-lite@1.0.30001753: {} chai@5.3.3: dependencies: @@ -1632,7 +1632,7 @@ snapshots: deep-eql@5.0.2: {} - electron-to-chromium@1.5.237: {} + electron-to-chromium@1.5.244: {} emoji-regex@8.0.0: {} @@ -1643,34 +1643,34 @@ snapshots: es-module-lexer@1.7.0: {} - esbuild@0.25.11: + esbuild@0.25.12: optionalDependencies: - '@esbuild/aix-ppc64': 0.25.11 - '@esbuild/android-arm': 0.25.11 - '@esbuild/android-arm64': 0.25.11 - '@esbuild/android-x64': 0.25.11 - '@esbuild/darwin-arm64': 0.25.11 - '@esbuild/darwin-x64': 0.25.11 - '@esbuild/freebsd-arm64': 0.25.11 - '@esbuild/freebsd-x64': 0.25.11 - '@esbuild/linux-arm': 0.25.11 - '@esbuild/linux-arm64': 0.25.11 - '@esbuild/linux-ia32': 0.25.11 - '@esbuild/linux-loong64': 0.25.11 - '@esbuild/linux-mips64el': 0.25.11 - '@esbuild/linux-ppc64': 0.25.11 - '@esbuild/linux-riscv64': 0.25.11 - '@esbuild/linux-s390x': 0.25.11 - '@esbuild/linux-x64': 0.25.11 - '@esbuild/netbsd-arm64': 0.25.11 - '@esbuild/netbsd-x64': 0.25.11 - '@esbuild/openbsd-arm64': 0.25.11 - '@esbuild/openbsd-x64': 0.25.11 - '@esbuild/openharmony-arm64': 0.25.11 - '@esbuild/sunos-x64': 0.25.11 - '@esbuild/win32-arm64': 0.25.11 - '@esbuild/win32-ia32': 0.25.11 - '@esbuild/win32-x64': 0.25.11 + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 escalade@3.2.0: {} @@ -1716,7 +1716,7 @@ snapshots: graceful-fs@4.2.11: {} - graphql@16.11.0: {} + graphql@16.12.0: {} has-flag@4.0.0: {} @@ -1744,7 +1744,7 @@ snapshots: loupe@3.2.1: {} - magic-string@0.30.19: + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -1773,7 +1773,7 @@ snapshots: '@open-draft/until': 2.1.0 '@types/cookie': 0.6.0 '@types/statuses': 2.0.6 - graphql: 16.11.0 + graphql: 16.12.0 headers-polyfill: 4.0.3 is-node-process: 1.2.0 outvariant: 1.4.3 @@ -1795,7 +1795,7 @@ snapshots: neo-async@2.6.2: {} - node-releases@2.0.26: {} + node-releases@2.0.27: {} outvariant@1.4.3: {} @@ -1978,9 +1978,9 @@ snapshots: undici-types@5.26.5: {} - update-browserslist-db@1.1.3(browserslist@4.26.3): + update-browserslist-db@1.1.4(browserslist@4.27.0): dependencies: - browserslist: 4.26.3 + browserslist: 4.27.0 escalade: 3.2.0 picocolors: 1.1.1 @@ -1990,7 +1990,7 @@ snapshots: debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) + vite: 7.1.12(@types/node@18.19.130)(terser@5.44.0) transitivePeerDependencies: - '@types/node' - jiti @@ -2005,9 +2005,9 @@ snapshots: - tsx - yaml - vite@7.1.11(@types/node@18.19.130)(terser@5.44.0): + vite@7.1.12(@types/node@18.19.130)(terser@5.44.0): dependencies: - esbuild: 0.25.11 + esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 @@ -2022,7 +2022,7 @@ snapshots: dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.11(@types/node@18.19.130)(terser@5.44.0)) + '@vitest/mocker': 3.2.4(msw@2.11.2(@types/node@18.19.130)(typescript@5.7.3))(vite@7.1.12(@types/node@18.19.130)(terser@5.44.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -2031,7 +2031,7 @@ snapshots: chai: 5.3.3 debug: 4.4.3 expect-type: 1.2.2 - magic-string: 0.30.19 + magic-string: 0.30.21 pathe: 2.0.3 picomatch: 4.0.3 std-env: 3.10.0 @@ -2040,7 +2040,7 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.11(@types/node@18.19.130)(terser@5.44.0) + vite: 7.1.12(@types/node@18.19.130)(terser@5.44.0) vite-node: 3.2.4(@types/node@18.19.130)(terser@5.44.0) why-is-node-running: 2.3.0 optionalDependencies: @@ -2076,7 +2076,7 @@ snapshots: '@webassemblyjs/wasm-parser': 1.14.1 acorn: 8.15.0 acorn-import-phases: 1.0.4(acorn@8.15.0) - browserslist: 4.26.3 + browserslist: 4.27.0 chrome-trace-event: 1.0.4 enhanced-resolve: 5.18.3 es-module-lexer: 1.7.0 diff --git a/reference.md b/reference.md deleted file mode 100644 index f0bb9f0..0000000 --- a/reference.md +++ /dev/null @@ -1,1007 +0,0 @@ -# Reference -## Entities -
client.entities.publishEntity({ ...params }) -> Lattice.Entity -
-
- -#### πŸ“ Description - -
-
- -
-
- -Publish an entity for ingest into the Entities API. Entities created with this method are "owned" by the originator: other sources, -such as the UI, may not edit or delete these entities. The server validates entities at API call time and -returns an error if the entity is invalid. - -An entity ID must be provided when calling this endpoint. If the entity referenced by the entity ID does not exist -then it will be created. Otherwise the entity will be updated. An entity will only be updated if its -provenance.sourceUpdateTime is greater than the provenance.sourceUpdateTime of the existing entity. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.entities.publishEntity({}); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.Entity` - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.entities.getEntity(entityId) -> Lattice.Entity -
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.entities.getEntity("entityId"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**entityId:** `string` β€” ID of the entity to return - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.entities.overrideEntity(entityId, fieldPath, { ...params }) -> Lattice.Entity -
-
- -#### πŸ“ Description - -
-
- -
-
- -Only fields marked with overridable can be overridden. Please refer to our documentation to see the comprehensive -list of fields that can be overridden. The entity in the request body should only have a value set on the field -specified in the field path parameter. Field paths are rooted in the base entity object and must be represented -using lower_snake_case. Do not include "entity" in the field path. - -Note that overrides are applied in an eventually consistent manner. If multiple overrides are created -concurrently for the same field path, the last writer wins. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.entities.overrideEntity("entityId", "mil_view.disposition"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**entityId:** `string` β€” The unique ID of the entity to override - -
-
- -
-
- -**fieldPath:** `string` β€” fieldPath to override - -
-
- -
-
- -**request:** `Lattice.EntityOverride` - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.entities.removeEntityOverride(entityId, fieldPath) -> Lattice.Entity -
-
- -#### πŸ“ Description - -
-
- -
-
- -This operation clears the override value from the specified field path on the entity. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.entities.removeEntityOverride("entityId", "mil_view.disposition"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**entityId:** `string` β€” The unique ID of the entity to undo an override from. - -
-
- -
-
- -**fieldPath:** `string` β€” The fieldPath to clear overrides from. - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.entities.longPollEntityEvents({ ...params }) -> Lattice.EntityEventResponse -
-
- -#### πŸ“ Description - -
-
- -
-
- -This is a long polling API that will first return all pre-existing data and then return all new data as -it becomes available. If you want to start a new polling session then open a request with an empty -'sessionToken' in the request body. The server will return a new session token in the response. -If you want to retrieve the next batch of results from an existing polling session then send the session -token you received from the server in the request body. If no new data is available then the server will -hold the connection open for up to 5 minutes. After the 5 minute timeout period, the server will close the -connection with no results and you may resume polling with the same session token. If your session falls behind -more than 3x the total number of entities in the environment, the server will terminate your session. -In this case you must start a new session by sending a request with an empty session token. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.entities.longPollEntityEvents({ - sessionToken: "sessionToken" -}); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.EntityEventRequest` - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.entities.streamEntities({ ...params }) -> core.Stream -
-
- -#### πŸ“ Description - -
-
- -
-
- -Establishes a persistent connection to stream entity events as they occur. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -const response = await client.entities.streamEntities(); -for await (const item of response) { - console.log(item); -} - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.EntityStreamRequest` - -
-
- -
-
- -**requestOptions:** `Entities.RequestOptions` - -
-
-
-
- - -
-
-
- -## Tasks -
client.tasks.createTask({ ...params }) -> Lattice.Task -
-
- -#### πŸ“ Description - -
-
- -
-
- -Submit a request to create a task and schedule it for delivery. Tasks, once delivered, will -be asynchronously updated by their destined agent. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.tasks.createTask(); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.TaskCreation` - -
-
- -
-
- -**requestOptions:** `Tasks.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.tasks.getTask(taskId) -> Lattice.Task -
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.tasks.getTask("taskId"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**taskId:** `string` β€” ID of task to return - -
-
- -
-
- -**requestOptions:** `Tasks.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.tasks.updateTaskStatus(taskId, { ...params }) -> Lattice.Task -
-
- -#### πŸ“ Description - -
-
- -
-
- -Update the status of a task. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.tasks.updateTaskStatus("taskId"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**taskId:** `string` β€” ID of task to update status of - -
-
- -
-
- -**request:** `Lattice.TaskStatusUpdate` - -
-
- -
-
- -**requestOptions:** `Tasks.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.tasks.queryTasks({ ...params }) -> Lattice.TaskQueryResults -
-
- -#### πŸ“ Description - -
-
- -
-
- -Query for tasks by a specified search criteria. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.tasks.queryTasks(); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.TaskQuery` - -
-
- -
-
- -**requestOptions:** `Tasks.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.tasks.listenAsAgent({ ...params }) -> Lattice.AgentRequest -
-
- -#### πŸ“ Description - -
-
- -
-
- -This is a long polling API that will block until a new task is ready for delivery. If no new task is -available then the server will hold on to your request for up to 5 minutes, after that 5 minute timeout -period you will be expected to reinitiate a new request. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.tasks.listenAsAgent(); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.AgentListener` - -
-
- -
-
- -**requestOptions:** `Tasks.RequestOptions` - -
-
-
-
- - -
-
-
- -## Objects -
client.objects.listObjects({ ...params }) -> core.Page -
-
- -#### πŸ“ Description - -
-
- -
-
- -Lists objects in your environment. You can define a prefix to list a subset of your objects. If you do not set a prefix, Lattice returns all available objects. By default this endpoint will list local objects only. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -const response = await client.objects.listObjects({ - prefix: "prefix", - sinceTimestamp: "2024-01-15T09:30:00Z", - pageToken: "pageToken", - allObjectsInMesh: true -}); -for await (const item of response) { - console.log(item); -} - -// Or you can manually iterate page-by-page -let page = await client.objects.listObjects({ - prefix: "prefix", - sinceTimestamp: "2024-01-15T09:30:00Z", - pageToken: "pageToken", - allObjectsInMesh: true -}); -while (page.hasNextPage()) { - page = page.getNextPage(); -} - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**request:** `Lattice.ListObjectsRequest` - -
-
- -
-
- -**requestOptions:** `Objects.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.objects.getObject(objectPath, { ...params }) -> core.BinaryResponse -
-
- -#### πŸ“ Description - -
-
- -
-
- -Fetches an object from your environment using the objectPath path parameter. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.objects.getObject("objectPath"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**objectPath:** `string` β€” The path of the object to fetch. - -
-
- -
-
- -**request:** `Lattice.GetObjectRequest` - -
-
- -
-
- -**requestOptions:** `Objects.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.objects.deleteObject(objectPath) -> void -
-
- -#### πŸ“ Description - -
-
- -
-
- -Deletes an object from your environment given the objectPath path parameter. -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.objects.deleteObject("objectPath"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**objectPath:** `string` β€” The path of the object to delete. - -
-
- -
-
- -**requestOptions:** `Objects.RequestOptions` - -
-
-
-
- - -
-
-
- -
client.objects.getObjectMetadata(objectPath) -> Headers -
-
- -#### πŸ“ Description - -
-
- -
-
- -Returns metadata for a specified object path. Use this to fetch metadata such as object size (size_bytes), its expiry time (expiry_time), or its latest update timestamp (last_updated_at). -
-
-
-
- -#### πŸ”Œ Usage - -
-
- -
-
- -```typescript -await client.objects.getObjectMetadata("objectPath"); - -``` -
-
-
-
- -#### βš™οΈ Parameters - -
-
- -
-
- -**objectPath:** `string` β€” The path of the object to query. - -
-
- -
-
- -**requestOptions:** `Objects.RequestOptions` - -
-
-
-
- - -
-
-
diff --git a/src/BaseClient.ts b/src/BaseClient.ts index 2ef235a..954db15 100644 --- a/src/BaseClient.ts +++ b/src/BaseClient.ts @@ -14,6 +14,10 @@ export interface BaseClientOptions { timeoutInSeconds?: number; /** The default number of times to retry the request. Defaults to 2. */ maxRetries?: number; + /** Provide a custom fetch implementation. Useful for platforms that don't have a built-in fetch or need a custom implementation. */ + fetch?: typeof fetch; + /** Configure logging for the client. */ + logging?: core.logging.LogConfig | core.logging.Logger; } export interface BaseRequestOptions { diff --git a/src/Client.ts b/src/Client.ts index 483efe7..267a38e 100644 --- a/src/Client.ts +++ b/src/Client.ts @@ -22,12 +22,13 @@ export class LatticeClient { constructor(_options: LatticeClient.Options = {}) { this._options = { ..._options, + logging: core.logging.createLogger(_options?.logging), headers: mergeHeaders( { "X-Fern-Language": "JavaScript", "X-Fern-SDK-Name": "@anduril-industries/lattice-sdk", - "X-Fern-SDK-Version": "3.0.0", - "User-Agent": "@anduril-industries/lattice-sdk/3.0.0", + "X-Fern-SDK-Version": "3.0.1", + "User-Agent": "@anduril-industries/lattice-sdk/3.0.1", "X-Fern-Runtime": core.RUNTIME.type, "X-Fern-Runtime-Version": core.RUNTIME.version, }, diff --git a/src/api/resources/entities/client/Client.ts b/src/api/resources/entities/client/Client.ts index 28973e5..fec9ffa 100644 --- a/src/api/resources/entities/client/Client.ts +++ b/src/api/resources/entities/client/Client.ts @@ -73,6 +73,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Entity, rawResponse: _response.rawResponse }; @@ -150,6 +152,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Entity, rawResponse: _response.rawResponse }; @@ -248,6 +252,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Entity, rawResponse: _response.rawResponse }; @@ -334,6 +340,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Entity, rawResponse: _response.rawResponse }; @@ -432,6 +440,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.EntityEventResponse, rawResponse: _response.rawResponse }; @@ -511,6 +521,8 @@ export class Entities { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { diff --git a/src/api/resources/objects/client/Client.ts b/src/api/resources/objects/client/Client.ts index b548535..fe76530 100644 --- a/src/api/resources/objects/client/Client.ts +++ b/src/api/resources/objects/client/Client.ts @@ -44,7 +44,7 @@ export class Objects { public async listObjects( request: Lattice.ListObjectsRequest = {}, requestOptions?: Objects.RequestOptions, - ): Promise> { + ): Promise> { const list = core.HttpResponsePromise.interceptFunction( async (request: Lattice.ListObjectsRequest): Promise> => { const { prefix, sinceTimestamp, pageToken, allObjectsInMesh } = request; @@ -79,6 +79,8 @@ export class Objects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.ListResponse, rawResponse: _response.rawResponse }; @@ -120,7 +122,7 @@ export class Objects { }, ); const dataWithRawResponse = await list(request).withRawResponse(); - return new core.Pageable({ + return new core.Page({ response: dataWithRawResponse.data, rawResponse: dataWithRawResponse.rawResponse, hasNextPage: (response) => @@ -177,6 +179,8 @@ export class Objects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body, rawResponse: _response.rawResponse }; @@ -268,6 +272,8 @@ export class Objects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.PathMetadata, rawResponse: _response.rawResponse }; @@ -353,6 +359,8 @@ export class Objects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: undefined, rawResponse: _response.rawResponse }; @@ -438,6 +446,8 @@ export class Objects { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.rawResponse.headers, rawResponse: _response.rawResponse }; diff --git a/src/api/resources/tasks/client/Client.ts b/src/api/resources/tasks/client/Client.ts index 07ea354..ff05a1c 100644 --- a/src/api/resources/tasks/client/Client.ts +++ b/src/api/resources/tasks/client/Client.ts @@ -68,6 +68,8 @@ export class Tasks { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Task, rawResponse: _response.rawResponse }; @@ -142,6 +144,8 @@ export class Tasks { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Task, rawResponse: _response.rawResponse }; @@ -229,6 +233,8 @@ export class Tasks { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.Task, rawResponse: _response.rawResponse }; @@ -315,6 +321,8 @@ export class Tasks { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.TaskQueryResults, rawResponse: _response.rawResponse }; @@ -400,6 +408,8 @@ export class Tasks { timeoutMs: (requestOptions?.timeoutInSeconds ?? this._options?.timeoutInSeconds ?? 60) * 1000, maxRetries: requestOptions?.maxRetries ?? this._options?.maxRetries, abortSignal: requestOptions?.abortSignal, + fetchFn: this._options?.fetch, + logging: this._options.logging, }); if (_response.ok) { return { data: _response.body as Lattice.AgentRequest, rawResponse: _response.rawResponse }; diff --git a/src/core/exports.ts b/src/core/exports.ts index e415a8f..c21f056 100644 --- a/src/core/exports.ts +++ b/src/core/exports.ts @@ -1 +1,3 @@ export * from "./file/exports.js"; +export * from "./logging/exports.js"; +export * from "./pagination/exports.js"; diff --git a/src/core/fetcher/Fetcher.ts b/src/core/fetcher/Fetcher.ts index 202e134..ef020d4 100644 --- a/src/core/fetcher/Fetcher.ts +++ b/src/core/fetcher/Fetcher.ts @@ -1,4 +1,5 @@ import { toJson } from "../json.js"; +import { createLogger, type LogConfig, type Logger } from "../logging/logger.js"; import type { APIResponse } from "./APIResponse.js"; import { createRequestUrl } from "./createRequestUrl.js"; import type { EndpointMetadata } from "./EndpointMetadata.js"; @@ -25,10 +26,12 @@ export declare namespace Fetcher { maxRetries?: number; withCredentials?: boolean; abortSignal?: AbortSignal; - requestType?: "json" | "file" | "bytes"; + requestType?: "json" | "file" | "bytes" | "form" | "other"; responseType?: "json" | "blob" | "sse" | "streaming" | "text" | "arrayBuffer" | "binary-response"; duplex?: "half"; endpointMetadata?: EndpointMetadata; + fetchFn?: typeof fetch; + logging?: LogConfig | Logger; } export type Error = FailedStatusCodeError | NonJsonError | TimeoutError | UnknownError; @@ -55,6 +58,141 @@ export declare namespace Fetcher { } } +const SENSITIVE_HEADERS = new Set([ + "authorization", + "x-api-key", + "api-key", + "x-auth-token", + "cookie", + "set-cookie", + "proxy-authorization", + "x-csrf-token", + "x-xsrf-token", +]); + +function redactHeaders(headers: Record): Record { + const filtered: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (SENSITIVE_HEADERS.has(key.toLowerCase())) { + filtered[key] = "[REDACTED]"; + } else { + filtered[key] = value; + } + } + return filtered; +} + +const SENSITIVE_QUERY_PARAMS = new Set([ + "api_key", + "api-key", + "apikey", + "token", + "access_token", + "access-token", + "auth_token", + "auth-token", + "password", + "passwd", + "secret", + "api_secret", + "api-secret", + "apisecret", + "key", + "session", + "session_id", + "session-id", +]); + +function redactQueryParameters(queryParameters?: Record): Record | undefined { + if (queryParameters == null) { + return queryParameters; + } + const redacted: Record = {}; + for (const [key, value] of Object.entries(queryParameters)) { + if (SENSITIVE_QUERY_PARAMS.has(key.toLowerCase())) { + redacted[key] = "[REDACTED]"; + } else { + redacted[key] = value; + } + } + return redacted; +} + +function redactUrl(url: string): string { + const protocolIndex = url.indexOf("://"); + if (protocolIndex === -1) return url; + + const afterProtocol = protocolIndex + 3; + const atIndex = url.indexOf("@", afterProtocol); + + if (atIndex !== -1) { + const pathStart = url.indexOf("/", afterProtocol); + const queryStart = url.indexOf("?", afterProtocol); + const fragmentStart = url.indexOf("#", afterProtocol); + + const firstDelimiter = Math.min( + pathStart === -1 ? url.length : pathStart, + queryStart === -1 ? url.length : queryStart, + fragmentStart === -1 ? url.length : fragmentStart, + ); + + if (atIndex < firstDelimiter) { + url = `${url.slice(0, afterProtocol)}[REDACTED]@${url.slice(atIndex + 1)}`; + } + } + + const queryStart = url.indexOf("?"); + if (queryStart === -1) return url; + + const fragmentStart = url.indexOf("#", queryStart); + const queryEnd = fragmentStart !== -1 ? fragmentStart : url.length; + const queryString = url.slice(queryStart + 1, queryEnd); + + if (queryString.length === 0) return url; + + // FAST PATH: Quick check if any sensitive keywords present + // Using indexOf is faster than regex for simple substring matching + const lower = queryString.toLowerCase(); + const hasSensitive = + lower.includes("token") || // catches token, access_token, auth_token, etc. + lower.includes("key") || // catches key, api_key, apikey, api-key, etc. + lower.includes("password") || // catches password + lower.includes("passwd") || // catches passwd + lower.includes("secret") || // catches secret, api_secret, etc. + lower.includes("session") || // catches session, session_id, session-id + lower.includes("auth"); // catches auth_token, auth-token, etc. + + if (!hasSensitive) { + return url; // Early exit - no sensitive params + } + + // SLOW PATH: Parse and redact + const redactedParams: string[] = []; + const params = queryString.split("&"); + + for (const param of params) { + const equalIndex = param.indexOf("="); + if (equalIndex === -1) { + redactedParams.push(param); + continue; + } + + const key = param.slice(0, equalIndex); + let shouldRedact = SENSITIVE_QUERY_PARAMS.has(key.toLowerCase()); + + if (!shouldRedact && key.includes("%")) { + try { + const decodedKey = decodeURIComponent(key); + shouldRedact = SENSITIVE_QUERY_PARAMS.has(decodedKey.toLowerCase()); + } catch {} + } + + redactedParams.push(shouldRedact ? `${key}=[REDACTED]` : param); + } + + return url.slice(0, queryStart + 1) + redactedParams.join("&") + url.slice(queryEnd); +} + async function getHeaders(args: Fetcher.Args): Promise> { const newHeaders: Record = {}; if (args.body !== undefined && args.contentType != null) { @@ -83,9 +221,22 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise= 200 && response.status < 400) { + if (logger.isDebug()) { + const metadata = { + method: args.method, + url: redactUrl(url), + statusCode: response.status, + }; + logger.debug("HTTP request succeeded", metadata); + } return { ok: true, body: (await getResponseBody(response, args.responseType)) as R, @@ -112,6 +271,14 @@ export async function fetcherImpl(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise(args: Fetcher.Args): Promise { + if (type === "form") { + return toQueryString(body, { arrayFormat: "repeat", encode: true }); + } if (type.includes("json")) { return toJson(body); } else { diff --git a/src/core/headers.ts b/src/core/headers.ts index a723d22..78ed8b5 100644 --- a/src/core/headers.ts +++ b/src/core/headers.ts @@ -6,10 +6,11 @@ export function mergeHeaders( for (const [key, value] of headersArray .filter((headers) => headers != null) .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); if (value != null) { - result[key] = value; - } else if (key in result) { - delete result[key]; + result[insensitiveKey] = value; + } else if (insensitiveKey in result) { + delete result[insensitiveKey]; } } @@ -24,8 +25,9 @@ export function mergeOnlyDefinedHeaders( for (const [key, value] of headersArray .filter((headers) => headers != null) .flatMap((headers) => Object.entries(headers))) { + const insensitiveKey = key.toLowerCase(); if (value != null) { - result[key] = value; + result[insensitiveKey] = value; } } diff --git a/src/core/index.ts b/src/core/index.ts index e838642..e000452 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -2,6 +2,7 @@ export * from "./auth/index.js"; export * from "./base64.js"; export * from "./fetcher/index.js"; export * as file from "./file/index.js"; +export * as logging from "./logging/index.js"; export * from "./pagination/index.js"; export * from "./runtime/index.js"; export * from "./stream/index.js"; diff --git a/src/core/logging/exports.ts b/src/core/logging/exports.ts new file mode 100644 index 0000000..88f6c00 --- /dev/null +++ b/src/core/logging/exports.ts @@ -0,0 +1,19 @@ +import * as logger from "./logger.js"; + +export namespace logging { + /** + * Configuration for logger instances. + */ + export type LogConfig = logger.LogConfig; + export type LogLevel = logger.LogLevel; + export const LogLevel: typeof logger.LogLevel = logger.LogLevel; + export type ILogger = logger.ILogger; + /** + * Console logger implementation that outputs to the console. + */ + export type ConsoleLogger = logger.ConsoleLogger; + /** + * Console logger implementation that outputs to the console. + */ + export const ConsoleLogger: typeof logger.ConsoleLogger = logger.ConsoleLogger; +} diff --git a/src/core/logging/index.ts b/src/core/logging/index.ts new file mode 100644 index 0000000..d81cc32 --- /dev/null +++ b/src/core/logging/index.ts @@ -0,0 +1 @@ +export * from "./logger.js"; diff --git a/src/core/logging/logger.ts b/src/core/logging/logger.ts new file mode 100644 index 0000000..a2bdef4 --- /dev/null +++ b/src/core/logging/logger.ts @@ -0,0 +1,203 @@ +export const LogLevel = { + Debug: "debug", + Info: "info", + Warn: "warn", + Error: "error", +} as const; +export type LogLevel = (typeof LogLevel)[keyof typeof LogLevel]; +const logLevelMap: Record = { + [LogLevel.Debug]: 1, + [LogLevel.Info]: 2, + [LogLevel.Warn]: 3, + [LogLevel.Error]: 4, +}; + +export interface ILogger { + /** + * Logs a debug message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + debug(message: string, ...args: unknown[]): void; + /** + * Logs an info message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + info(message: string, ...args: unknown[]): void; + /** + * Logs a warning message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + warn(message: string, ...args: unknown[]): void; + /** + * Logs an error message. + * @param message - The message to log + * @param args - Additional arguments to log + */ + error(message: string, ...args: unknown[]): void; +} + +/** + * Configuration for logger initialization. + */ +export interface LogConfig { + /** + * Minimum log level to output. + * @default LogLevel.Info + */ + level?: LogLevel; + /** + * Logger implementation to use. + * @default new ConsoleLogger() + */ + logger?: ILogger; + /** + * Whether logging should be silenced. + * @default true + */ + silent?: boolean; +} + +/** + * Default console-based logger implementation. + */ +export class ConsoleLogger implements ILogger { + debug(message: string, ...args: unknown[]): void { + console.debug(message, ...args); + } + info(message: string, ...args: unknown[]): void { + console.info(message, ...args); + } + warn(message: string, ...args: unknown[]): void { + console.warn(message, ...args); + } + error(message: string, ...args: unknown[]): void { + console.error(message, ...args); + } +} + +/** + * Logger class that provides level-based logging functionality. + */ +export class Logger { + private readonly level: number; + private readonly logger: ILogger; + private readonly silent: boolean; + + /** + * Creates a new logger instance. + * @param config - Logger configuration + */ + constructor(config: Required) { + this.level = logLevelMap[config.level]; + this.logger = config.logger; + this.silent = config.silent; + } + + /** + * Checks if a log level should be output based on configuration. + * @param level - The log level to check + * @returns True if the level should be logged + */ + public shouldLog(level: LogLevel): boolean { + return !this.silent && this.level >= logLevelMap[level]; + } + + /** + * Checks if debug logging is enabled. + * @returns True if debug logs should be output + */ + public isDebug(): boolean { + return this.shouldLog(LogLevel.Debug); + } + + /** + * Logs a debug message if debug logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public debug(message: string, ...args: unknown[]): void { + if (this.isDebug()) { + this.logger.debug(message, ...args); + } + } + + /** + * Checks if info logging is enabled. + * @returns True if info logs should be output + */ + public isInfo(): boolean { + return this.shouldLog(LogLevel.Info); + } + + /** + * Logs an info message if info logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public info(message: string, ...args: unknown[]): void { + if (this.isInfo()) { + this.logger.info(message, ...args); + } + } + + /** + * Checks if warning logging is enabled. + * @returns True if warning logs should be output + */ + public isWarn(): boolean { + return this.shouldLog(LogLevel.Warn); + } + + /** + * Logs a warning message if warning logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public warn(message: string, ...args: unknown[]): void { + if (this.isWarn()) { + this.logger.warn(message, ...args); + } + } + + /** + * Checks if error logging is enabled. + * @returns True if error logs should be output + */ + public isError(): boolean { + return this.shouldLog(LogLevel.Error); + } + + /** + * Logs an error message if error logging is enabled. + * @param message - The message to log + * @param args - Additional arguments to log + */ + public error(message: string, ...args: unknown[]): void { + if (this.isError()) { + this.logger.error(message, ...args); + } + } +} + +export function createLogger(config?: LogConfig | Logger): Logger { + if (config == null) { + return defaultLogger; + } + if (config instanceof Logger) { + return config; + } + config = config ?? {}; + config.level ??= LogLevel.Info; + config.logger ??= new ConsoleLogger(); + config.silent ??= true; + return new Logger(config as Required); +} + +const defaultLogger: Logger = new Logger({ + level: LogLevel.Info, + logger: new ConsoleLogger(), + silent: true, +}); diff --git a/src/core/pagination/Page.ts b/src/core/pagination/Page.ts index 1aa08e5..6621a6f 100644 --- a/src/core/pagination/Page.ts +++ b/src/core/pagination/Page.ts @@ -4,15 +4,16 @@ import type { HttpResponsePromise, RawResponse } from "../fetcher/index.js"; * A page of results from a paginated API. * * @template T The type of the items in the page. + * @template R The type of the API response. */ -export class Page implements AsyncIterable { +export class Page implements AsyncIterable { public data: T[]; public rawResponse: RawResponse; + public response: R; - private response: unknown; - private _hasNextPage: (response: unknown) => boolean; - private getItems: (response: unknown) => T[]; - private loadNextPage: (response: unknown) => HttpResponsePromise; + private _hasNextPage: (response: R) => boolean; + private getItems: (response: R) => T[]; + private loadNextPage: (response: R) => HttpResponsePromise; constructor({ response, @@ -21,11 +22,11 @@ export class Page implements AsyncIterable { getItems, loadPage, }: { - response: unknown; + response: R; rawResponse: RawResponse; - hasNextPage: (response: unknown) => boolean; - getItems: (response: unknown) => T[]; - loadPage: (response: unknown) => HttpResponsePromise; + hasNextPage: (response: R) => boolean; + getItems: (response: R) => T[]; + loadPage: (response: R) => HttpResponsePromise; }) { this.response = response; this.rawResponse = rawResponse; diff --git a/src/core/pagination/Pageable.ts b/src/core/pagination/Pageable.ts deleted file mode 100644 index 5689e1e..0000000 --- a/src/core/pagination/Pageable.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { RawResponse } from "../fetcher/index.js"; -import { Page } from "./Page.js"; - -export declare namespace Pageable { - interface Args { - response: Response; - rawResponse: RawResponse; - hasNextPage: (response: Response) => boolean; - getItems: (response: Response) => Item[]; - loadPage: (response: Response) => Promise; - } -} - -export class Pageable extends Page { - constructor(args: Pageable.Args) { - super(args as any); - } -} diff --git a/src/core/pagination/exports.ts b/src/core/pagination/exports.ts new file mode 100644 index 0000000..d3acc60 --- /dev/null +++ b/src/core/pagination/exports.ts @@ -0,0 +1 @@ +export type { Page } from "./Page.js"; diff --git a/src/core/pagination/index.ts b/src/core/pagination/index.ts index b0cd68f..7781cbd 100644 --- a/src/core/pagination/index.ts +++ b/src/core/pagination/index.ts @@ -1,2 +1 @@ export { Page } from "./Page.js"; -export { Pageable } from "./Pageable.js"; diff --git a/src/core/stream/Stream.ts b/src/core/stream/Stream.ts index e41f05a..4d4b97f 100644 --- a/src/core/stream/Stream.ts +++ b/src/core/stream/Stream.ts @@ -43,6 +43,7 @@ export class Stream implements AsyncIterable { private messageTerminator: string; private streamTerminator: string | undefined; private controller: AbortController = new AbortController(); + private decoder: TextDecoder | undefined; constructor({ stream, parse, eventShape, signal }: Stream.Args & { parse: (val: unknown) => Promise }) { this.stream = stream; @@ -55,6 +56,11 @@ export class Stream implements AsyncIterable { this.messageTerminator = eventShape.messageTerminator; } signal?.addEventListener("abort", () => this.controller.abort()); + + // Initialize shared TextDecoder + if (typeof TextDecoder !== "undefined") { + this.decoder = new TextDecoder("utf-8"); + } } private async *iterMessages(): AsyncGenerator { @@ -67,7 +73,7 @@ export class Stream implements AsyncIterable { let terminatorIndex: number; while ((terminatorIndex = buf.indexOf(this.messageTerminator)) >= 0) { - let line = buf.slice(0, terminatorIndex + 1); + let line = buf.slice(0, terminatorIndex); buf = buf.slice(terminatorIndex + this.messageTerminator.length); if (!line.trim()) { @@ -101,10 +107,9 @@ export class Stream implements AsyncIterable { private decodeChunk(chunk: any): string { let decoded = ""; - // If TextDecoder is present, use it - if (typeof TextDecoder !== "undefined") { - const decoder = new TextDecoder("utf8"); - decoded += decoder.decode(chunk); + // If TextDecoder is available, use the streaming decoder instance + if (this.decoder != null) { + decoded += this.decoder.decode(chunk, { stream: true }); } // Buffer is present in Node.js environment else if (RUNTIME.type === "node" && typeof chunk !== "undefined") { diff --git a/src/version.ts b/src/version.ts index b54c0db..68e5be6 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1 +1 @@ -export const SDK_VERSION = "2.5.0"; +export const SDK_VERSION = "3.0.1"; diff --git a/tests/mock-server/mockEndpointBuilder.ts b/tests/mock-server/mockEndpointBuilder.ts index 18557ec..1b0e510 100644 --- a/tests/mock-server/mockEndpointBuilder.ts +++ b/tests/mock-server/mockEndpointBuilder.ts @@ -2,6 +2,7 @@ import { type DefaultBodyType, type HttpHandler, HttpResponse, type HttpResponse import { url } from "../../src/core"; import { toJson } from "../../src/core/json"; +import { withFormUrlEncoded } from "./withFormUrlEncoded"; import { withHeaders } from "./withHeaders"; import { withJson } from "./withJson"; @@ -26,6 +27,7 @@ interface RequestHeadersStage extends RequestBodyStage, ResponseStage { interface RequestBodyStage extends ResponseStage { jsonBody(body: unknown): ResponseStage; + formUrlEncodedBody(body: unknown): ResponseStage; } interface ResponseStage { @@ -135,6 +137,16 @@ class RequestBuilder implements MethodStage, RequestHeadersStage, RequestBodySta return this; } + formUrlEncodedBody(body: unknown): ResponseStage { + if (body === undefined) { + throw new Error( + "Undefined is not valid for form-urlencoded. Do not call formUrlEncodedBody if you want an empty body.", + ); + } + this.predicates.push((resolver) => withFormUrlEncoded(body, resolver)); + return this; + } + respondWith(): ResponseStatusStage { return new ResponseBuilder(this.method, this.buildUrl(), this.predicates, this.handlerOptions); } diff --git a/tests/mock-server/withFormUrlEncoded.ts b/tests/mock-server/withFormUrlEncoded.ts new file mode 100644 index 0000000..e9e6ff2 --- /dev/null +++ b/tests/mock-server/withFormUrlEncoded.ts @@ -0,0 +1,80 @@ +import { type HttpResponseResolver, passthrough } from "msw"; + +import { toJson } from "../../src/core/json"; + +/** + * Creates a request matcher that validates if the request form-urlencoded body exactly matches the expected object + * @param expectedBody - The exact body object to match against + * @param resolver - Response resolver to execute if body matches + */ +export function withFormUrlEncoded(expectedBody: unknown, resolver: HttpResponseResolver): HttpResponseResolver { + return async (args) => { + const { request } = args; + + let clonedRequest: Request; + let bodyText: string | undefined; + let actualBody: Record; + try { + clonedRequest = request.clone(); + bodyText = await clonedRequest.text(); + if (bodyText === "") { + console.error("Request body is empty, expected a form-urlencoded body."); + return passthrough(); + } + const params = new URLSearchParams(bodyText); + actualBody = {}; + for (const [key, value] of params.entries()) { + actualBody[key] = value; + } + } catch (error) { + console.error(`Error processing form-urlencoded request body:\n\tError: ${error}\n\tBody: ${bodyText}`); + return passthrough(); + } + + const mismatches = findMismatches(actualBody, expectedBody); + if (Object.keys(mismatches).length > 0) { + console.error("Form-urlencoded body mismatch:", toJson(mismatches, undefined, 2)); + return passthrough(); + } + + return resolver(args); + }; +} + +function findMismatches(actual: any, expected: any): Record { + const mismatches: Record = {}; + + if (typeof actual !== typeof expected) { + return { value: { actual, expected } }; + } + + if (typeof actual !== "object" || actual === null || expected === null) { + if (actual !== expected) { + return { value: { actual, expected } }; + } + return {}; + } + + const actualKeys = Object.keys(actual); + const expectedKeys = Object.keys(expected); + + const allKeys = new Set([...actualKeys, ...expectedKeys]); + + for (const key of allKeys) { + if (!expectedKeys.includes(key)) { + if (actual[key] === undefined) { + continue; + } + mismatches[key] = { actual: actual[key], expected: undefined }; + } else if (!actualKeys.includes(key)) { + if (expected[key] === undefined) { + continue; + } + mismatches[key] = { actual: undefined, expected: expected[key] }; + } else if (actual[key] !== expected[key]) { + mismatches[key] = { actual: actual[key], expected: expected[key] }; + } + } + + return mismatches; +} diff --git a/tests/unit/fetcher/getRequestBody.test.ts b/tests/unit/fetcher/getRequestBody.test.ts index e864c8b..e3da10c 100644 --- a/tests/unit/fetcher/getRequestBody.test.ts +++ b/tests/unit/fetcher/getRequestBody.test.ts @@ -45,7 +45,65 @@ describe("Test getRequestBody", () => { expect(result).toBe(input); }); - it("should return the input for content-type 'application/x-www-form-urlencoded'", async () => { + it("should serialize objects for form-urlencoded content type", async () => { + const input = { username: "johndoe", email: "john@example.com" }; + const result = await getRequestBody({ + body: input, + type: "form", + }); + expect(result).toBe("username=johndoe&email=john%40example.com"); + }); + + it("should serialize complex nested objects and arrays for form-urlencoded content type", async () => { + const input = { + user: { + profile: { + name: "John Doe", + settings: { + theme: "dark", + notifications: true, + }, + }, + tags: ["admin", "user"], + contacts: [ + { type: "email", value: "john@example.com" }, + { type: "phone", value: "+1234567890" }, + ], + }, + filters: { + status: ["active", "pending"], + metadata: { + created: "2024-01-01", + categories: ["electronics", "books"], + }, + }, + preferences: ["notifications", "updates"], + }; + const result = await getRequestBody({ + body: input, + type: "form", + }); + expect(result).toBe( + "user%5Bprofile%5D%5Bname%5D=John%20Doe&" + + "user%5Bprofile%5D%5Bsettings%5D%5Btheme%5D=dark&" + + "user%5Bprofile%5D%5Bsettings%5D%5Bnotifications%5D=true&" + + "user%5Btags%5D=admin&" + + "user%5Btags%5D=user&" + + "user%5Bcontacts%5D%5Btype%5D=email&" + + "user%5Bcontacts%5D%5Bvalue%5D=john%40example.com&" + + "user%5Bcontacts%5D%5Btype%5D=phone&" + + "user%5Bcontacts%5D%5Bvalue%5D=%2B1234567890&" + + "filters%5Bstatus%5D=active&" + + "filters%5Bstatus%5D=pending&" + + "filters%5Bmetadata%5D%5Bcreated%5D=2024-01-01&" + + "filters%5Bmetadata%5D%5Bcategories%5D=electronics&" + + "filters%5Bmetadata%5D%5Bcategories%5D=books&" + + "preferences=notifications&" + + "preferences=updates", + ); + }); + + it("should return the input for pre-serialized form-urlencoded strings", async () => { const input = "key=value&another=param"; const result = await getRequestBody({ body: input, diff --git a/tests/unit/stream/Stream.test.ts b/tests/unit/stream/Stream.test.ts new file mode 100644 index 0000000..f82b90a --- /dev/null +++ b/tests/unit/stream/Stream.test.ts @@ -0,0 +1,348 @@ +import { Stream } from "../../../src/core/stream/Stream"; + +describe("Stream", () => { + describe("JSON streaming", () => { + it("should parse single JSON message", async () => { + const mockStream = createReadableStream(['{"value": 1}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }]); + }); + + it("should parse multiple JSON messages", async () => { + const mockStream = createReadableStream(['{"value": 1}\n{"value": 2}\n{"value": 3}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }]); + }); + + it("should handle messages split across chunks", async () => { + const mockStream = createReadableStream(['{"val', 'ue": 1}\n{"value":', " 2}\n"]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }]); + }); + + it("should skip empty lines", async () => { + const mockStream = createReadableStream(['{"value": 1}\n\n\n{"value": 2}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }]); + }); + + it("should handle custom message terminator", async () => { + const mockStream = createReadableStream(['{"value": 1}|||{"value": 2}|||']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "|||" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }]); + }); + }); + + describe("SSE streaming", () => { + it("should parse SSE data with prefix", async () => { + const mockStream = createReadableStream(['data: {"value": 1}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "sse" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }]); + }); + + it("should parse multiple SSE events", async () => { + const mockStream = createReadableStream(['data: {"value": 1}\ndata: {"value": 2}\ndata: {"value": 3}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "sse" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }, { value: 3 }]); + }); + + it("should stop at stream terminator", async () => { + const mockStream = createReadableStream(['data: {"value": 1}\ndata: [DONE]\ndata: {"value": 2}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "sse", streamTerminator: "[DONE]" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }]); + }); + + it("should skip lines without data prefix", async () => { + const mockStream = createReadableStream([ + 'event: message\ndata: {"value": 1}\nid: 123\ndata: {"value": 2}\n', + ]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "sse" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }, { value: 2 }]); + }); + }); + + describe("encoding and decoding", () => { + it("should decode UTF-8 text using TextDecoder", async () => { + const encoder = new TextEncoder(); + const mockStream = createReadableStream([encoder.encode('{"text": "cafΓ©"}\n')]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { text: string }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ text: "cafΓ©" }]); + }); + + it("should decode emoji correctly", async () => { + const encoder = new TextEncoder(); + const mockStream = createReadableStream([encoder.encode('{"emoji": "πŸŽ‰"}\n')]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { emoji: string }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ emoji: "πŸŽ‰" }]); + }); + + it("should handle binary data chunks", async () => { + const encoder = new TextEncoder(); + const mockStream = createReadableStream([encoder.encode('{"val'), encoder.encode('ue": 1}\n')]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }]); + }); + + it("should handle multi-byte UTF-8 characters split across chunk boundaries", async () => { + // Test string with Japanese (3 bytes), Russian (2 bytes), German (2 bytes), and Chinese (3 bytes) + const testString = '{"text": "こんにけは ΠŸΡ€ΠΈΠ²Π΅Ρ‚ Grâße δ½ ε₯½"}\n'; + const fullBytes = new TextEncoder().encode(testString); + + // Split the bytes in the middle of multi-byte characters + // Japanese "こ" starts at byte 11, is 3 bytes (E3 81 93) + // Split after first byte of "こ" to test mid-character splitting + const splitPoint = 12; // This splits "こ" in the middle + const chunk1 = fullBytes.slice(0, splitPoint); + const chunk2 = fullBytes.slice(splitPoint); + + const mockStream = createReadableStream([chunk1, chunk2]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { text: string }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ text: "こんにけは ΠŸΡ€ΠΈΠ²Π΅Ρ‚ Grâße δ½ ε₯½" }]); + }); + }); + + describe("abort signal", () => { + it("should handle abort signal", async () => { + const controller = new AbortController(); + const mockStream = createReadableStream(['{"value": 1}\n{"value": 2}\n{"value": 3}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + signal: controller.signal, + }); + + const messages: unknown[] = []; + let count = 0; + for await (const message of stream) { + messages.push(message); + count++; + if (count === 2) { + controller.abort(); + break; + } + } + + expect(messages.length).toBe(2); + }); + }); + + describe("async iteration", () => { + it("should support async iterator protocol", async () => { + const mockStream = createReadableStream(['{"value": 1}\n{"value": 2}\n']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const iterator = stream[Symbol.asyncIterator](); + const first = await iterator.next(); + expect(first.done).toBe(false); + expect(first.value).toEqual({ value: 1 }); + + const second = await iterator.next(); + expect(second.done).toBe(false); + expect(second.value).toEqual({ value: 2 }); + + const third = await iterator.next(); + expect(third.done).toBe(true); + }); + }); + + describe("edge cases", () => { + it("should handle empty stream", async () => { + const mockStream = createReadableStream([]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([]); + }); + + it("should handle stream with only whitespace", async () => { + const mockStream = createReadableStream([" \n\n\t\n "]); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([]); + }); + + it("should handle incomplete message at end of stream", async () => { + const mockStream = createReadableStream(['{"value": 1}\n{"incomplete']); + const stream = new Stream({ + stream: mockStream, + parse: async (val: unknown) => val as { value: number }, + eventShape: { type: "json", messageTerminator: "\n" }, + }); + + const messages: unknown[] = []; + for await (const message of stream) { + messages.push(message); + } + + expect(messages).toEqual([{ value: 1 }]); + }); + }); +}); + +// Helper function to create a ReadableStream from string chunks +function createReadableStream(chunks: (string | Uint8Array)[]): ReadableStream { + // For standard type, return ReadableStream + let index = 0; + return new ReadableStream({ + pull(controller) { + if (index < chunks.length) { + const chunk = chunks[index++]; + controller.enqueue(typeof chunk === "string" ? new TextEncoder().encode(chunk) : chunk); + } else { + controller.close(); + } + }, + }); +}