diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 0000000..bf2cad0 --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,80 @@ +# Unique name for this workflow +name: CI on PR + +# Definition when the workflow should run +on: + workflow_dispatch: + pull_request: + types: [opened, edited, synchronize, reopened] + +# Jobs to be executed +jobs: + format-lint-test: + runs-on: ubuntu-latest + env: + SALESFORCE_LOGIN_URL: ${{ secrets.SALESFORCE_LOGIN_URL }} + SALESFORCE_USERNAME: ${{ secrets.SALESFORCE_USERNAME }} + SALESFORCE_PASSWORD: ${{ secrets.SALESFORCE_PASSWORD }} + SALESFORCE_TOKEN: ${{ secrets.SALESFORCE_TOKEN }} + SALESFORCE_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }} + SALESFORCE_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }} + SALESFORCE_JWT_LOGIN_URL: ${{ secrets.SALESFORCE_JWT_LOGIN_URL }} + SALESFORCE_JWT_CLIENT_ID: ${{ secrets.SALESFORCE_JWT_CLIENT_ID }} + SALESFORCE_PRIVATE_KEY: ${{ secrets.SALESFORCE_PRIVATE_KEY }} + + steps: + # Checkout the source code + - name: 'Checkout source code' + uses: actions/checkout@v4 + + # Install Volta to enforce proper node and package manager versions + - name: 'Install Volta' + uses: volta-cli/action@v4 + + # Cache node_modules to speed up the process + - name: 'Restore node_modules cache' + id: cache-npm + uses: actions/cache@v4 + with: + path: node_modules + key: npm-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + npm-${{ env.cache-name }}- + npm- + + # Install npm dependencies + - name: 'Install npm dependencies' + if: steps.cache-npm.outputs.cache-hit != 'true' + run: HUSKY=0 npm ci + + # Format + - name: 'Format' + run: npm run format:verify + + # Lint + - name: 'Lint' + run: npm run lint + + # Configure test env + - name: 'Configure test environment' + run: | + touch .env + echo SALESFORCE_LOGIN_URL=$SALESFORCE_LOGIN_URL >> .env + echo SALESFORCE_USERNAME=$SALESFORCE_USERNAME >> .env + echo SALESFORCE_PASSWORD=$SALESFORCE_PASSWORD >> .env + echo SALESFORCE_TOKEN=$SALESFORCE_TOKEN >> .env + echo SALESFORCE_CLIENT_ID=$SALESFORCE_CLIENT_ID >> .env + echo SALESFORCE_CLIENT_SECRET=$SALESFORCE_CLIENT_SECRET >> .env + echo SALESFORCE_PRIVATE_KEY_PATH=server.key >> .env + echo SALESFORCE_JWT_LOGIN_URL=$SALESFORCE_JWT_LOGIN_URL >> .env + echo SALESFORCE_JWT_CLIENT_ID=$SALESFORCE_JWT_CLIENT_ID >> .env + echo "$SALESFORCE_PRIVATE_KEY" > server.key + + # Integration tests + - name: 'Integration tests' + run: npm run test + + # Housekeeping + - name: 'Delete test environment configuration' + if: always() + run: rm -f .env server.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..2a5f00e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,81 @@ +# Unique name for this workflow +name: CI + +# Definition when the workflow should run +on: + workflow_dispatch: + push: + branches: + - main + +# Jobs to be executed +jobs: + format-lint-test: + runs-on: ubuntu-latest + env: + SALESFORCE_LOGIN_URL: ${{ secrets.SALESFORCE_LOGIN_URL }} + SALESFORCE_USERNAME: ${{ secrets.SALESFORCE_USERNAME }} + SALESFORCE_PASSWORD: ${{ secrets.SALESFORCE_PASSWORD }} + SALESFORCE_TOKEN: ${{ secrets.SALESFORCE_TOKEN }} + SALESFORCE_CLIENT_ID: ${{ secrets.SALESFORCE_CLIENT_ID }} + SALESFORCE_CLIENT_SECRET: ${{ secrets.SALESFORCE_CLIENT_SECRET }} + SALESFORCE_JWT_LOGIN_URL: ${{ secrets.SALESFORCE_JWT_LOGIN_URL }} + SALESFORCE_JWT_CLIENT_ID: ${{ secrets.SALESFORCE_JWT_CLIENT_ID }} + SALESFORCE_PRIVATE_KEY: ${{ secrets.SALESFORCE_PRIVATE_KEY }} + + steps: + # Checkout the source code + - name: 'Checkout source code' + uses: actions/checkout@v4 + + # Install Volta to enforce proper node and package manager versions + - name: 'Install Volta' + uses: volta-cli/action@v4 + + # Cache node_modules to speed up the process + - name: 'Restore node_modules cache' + id: cache-npm + uses: actions/cache@v4 + with: + path: node_modules + key: npm-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + npm-${{ env.cache-name }}- + npm- + + # Install npm dependencies + - name: 'Install npm dependencies' + if: steps.cache-npm.outputs.cache-hit != 'true' + run: HUSKY=0 npm ci + + # Format + - name: 'Format' + run: npm run format:verify + + # Lint + - name: 'Lint' + run: npm run lint + + # Configure test env + - name: 'Configure test environment' + run: | + touch .env + echo SALESFORCE_LOGIN_URL=$SALESFORCE_LOGIN_URL >> .env + echo SALESFORCE_USERNAME=$SALESFORCE_USERNAME >> .env + echo SALESFORCE_PASSWORD=$SALESFORCE_PASSWORD >> .env + echo SALESFORCE_TOKEN=$SALESFORCE_TOKEN >> .env + echo SALESFORCE_CLIENT_ID=$SALESFORCE_CLIENT_ID >> .env + echo SALESFORCE_CLIENT_SECRET=$SALESFORCE_CLIENT_SECRET >> .env + echo SALESFORCE_PRIVATE_KEY_PATH=server.key >> .env + echo SALESFORCE_JWT_LOGIN_URL=$SALESFORCE_JWT_LOGIN_URL >> .env + echo SALESFORCE_JWT_CLIENT_ID=$SALESFORCE_JWT_CLIENT_ID >> .env + echo "$SALESFORCE_PRIVATE_KEY" > server.key + + # Integration tests + - name: 'Integration tests' + run: npm run test + + # Housekeeping + - name: 'Delete test environment configuration' + if: always() + run: rm -f .env server.key diff --git a/.gitignore b/.gitignore index 8384bbe..2858896 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ node_modules jsconfig.json .vscode .idea -package-lock.json # MacOS system files .DS_Store @@ -25,4 +24,4 @@ ehthumbs.db $RECYCLE.BIN/ # Sample code -sample.js +sample* \ No newline at end of file diff --git a/README.md b/README.md index 22924a2..442c17f 100644 --- a/README.md +++ b/README.md @@ -4,140 +4,240 @@ See the [official Pub/Sub API repo](https://github.com/developerforce/pub-sub-api) and the [documentation](https://developer.salesforce.com/docs/platform/pub-sub-api/guide/intro.html) for more information on the Salesforce gRPC-based Pub/Sub API. +- [v4 to v5 Migration](#v4-to-v5-migration) +- [v4 Documentation](v4-documentation.md) - [Installation and Configuration](#installation-and-configuration) - - [User supplied authentication](#user-supplied-authentication) - - [Username/password flow](#usernamepassword-flow) - - [OAuth 2.0 client credentials flow (client_credentials)](#oauth-20-client-credentials-flow-client_credentials) - - [OAuth 2.0 JWT bearer flow](#oauth-20-jwt-bearer-flow) -- [Basic Example](#basic-example) + - [Authentication](#authentication) + - [User supplied authentication](#user-supplied-authentication) + - [Username/password flow](#usernamepassword-flow) + - [OAuth 2.0 client credentials flow (client_credentials)](#oauth-20-client-credentials-flow-client_credentials) + - [OAuth 2.0 JWT bearer flow](#oauth-20-jwt-bearer-flow) + - [Logging](#logging) +- [Quick Start Example](#quick-start-example) - [Other Examples](#other-examples) - [Publish a platform event](#publish-a-platform-event) - [Subscribe with a replay ID](#subscribe-with-a-replay-id) - [Subscribe to past events in retention window](#subscribe-to-past-events-in-retention-window) - [Work with flow control for high volumes of events](#work-with-flow-control-for-high-volumes-of-events) - [Handle gRPC stream lifecycle events](#handle-grpc-stream-lifecycle-events) - - [Use a custom logger](#use-a-custom-logger) - [Common Issues](#common-issues) - [Reference](#reference) - [PubSubApiClient](#pubsubapiclient) - - [PubSubEventEmitter](#pubsubeventemitter) + - [SubscribeCallback](#subscribecallback) + - [SubscriptionInfo](#subscriptioninfo) - [EventParseError](#eventparseerror) + - [Configuration](#configuration) -## Installation and Configuration +## v4 to v5 Migration -Install the client library with `npm install salesforce-pubsub-api-client`. +> [!WARNING] +> Version 5 of the Pub/Sub API client introduces a couple of breaking changes which require a small migration effort. Read this section for an overview of the changes. + +### Configuration and Connection + +In v4 and earlier versions of this client: -Create a `.env` file at the root of the project for configuration. +- you specify the configuration in a `.env` file with specific property names. +- you connect with either the `connect()` or `connectWithAuth()` method depending on the authentication flow. -Pick one of these authentication flows and fill the relevant configuration: +In v5: -- User supplied authentication -- Username/password authentication (recommended for tests) -- OAuth 2.0 client credentials -- OAuth 2.0 JWT Bearer (recommended for production) +- you pass your configuration with an object in the client constructor. The `.env` file is no longer a requirement, you are free to store your configuration where you want. +- you connect with a unique `connect()` method. -> [!TIP] -> The default client logger is fine for a test environment but you'll want to switch to a [custom logger](#use-a-custom-logger) with asynchronous logging for increased performance. +### Event handling -### User supplied authentication +In v4 and earlier versions of this client you use an asynchronous `EventEmitter` to receive updates such as incoming messages or lifecycle events: + +```js +// Subscribe to account change events +const eventEmitter = await client.subscribe( + '/data/AccountChangeEvent' +); + +// Handle incoming events +eventEmitter.on('data', (event) => { + // Event handling logic goes here +}): +``` -If you already have a Salesforce client in your app, you can reuse its authentication information. You only need this minimal configuration: +In v5 you use a synchronous callback function to receive the same information. This helps to ensure that events are received in the right order. -```properties -SALESFORCE_AUTH_TYPE=user-supplied +```js +const subscribeCallback = (subscription, callbackType, data) => { + // Event handling logic goes here +}; -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +// Subscribe to account change events +await client.subscribe('/data/AccountChangeEvent', subscribeCallback); ``` -When connecting to the Pub/Sub API, use the following method instead of the standard `connect()` method to specify authentication information: +## Installation and Configuration + +Install the client library with `npm install salesforce-pubsub-api-client`. + +### Authentication + +Pick one of these authentication flows and pass the relevant configuration to the `PubSubApiClient` constructor: + +- [User supplied authentication](#user-supplied-authentication) +- [Username/password flow](#usernamepassword-flow) (recommended for tests) +- [OAuth 2.0 client flow](#oauth-20-client-credentials-flow-client_credentials) +- [OAuth 2.0 JWT Bearer flow](#oauth-20-jwt-bearer-flow) (recommended for production) + +#### User supplied authentication + +If you already have a Salesforce client in your app, you can reuse its authentication information. +In the example below, we assume that `sfConnection` is a connection obtained with [jsforce](https://jsforce.github.io/) ```js -await client.connectWithAuth(accessToken, instanceUrl, organizationId); +const client = new PubSubApiClient({ + authType: 'user-supplied', + accessToken: sfConnection.accessToken, + instanceUrl: sfConnection.instanceUrl, + organizationId: sfConnection.userInfo.organizationId +}); ``` -### Username/password flow +#### Username/password flow > [!WARNING] > Relying on a username/password authentication flow for production is not recommended. Consider switching to JWT auth for extra security. -```properties -SALESFORCE_AUTH_TYPE=username-password -SALESFORCE_LOGIN_URL=https://login.salesforce.com -SALESFORCE_USERNAME=YOUR_SALESFORCE_USERNAME -SALESFORCE_PASSWORD=YOUR_SALESFORCE_PASSWORD -SALESFORCE_TOKEN=YOUR_SALESFORCE_USER_SECURITY_TOKEN +```js +const client = new PubSubApiClient({ + authType: 'username-password', + loginUrl: process.env.SALESFORCE_LOGIN_URL, + username: process.env.SALESFORCE_USERNAME, + password: process.env.SALESFORCE_PASSWORD, + userToken: process.env.SALESFORCE_TOKEN +}); +``` + +#### OAuth 2.0 client credentials flow (client_credentials) -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +```js +const client = new PubSubApiClient({ + authType: 'oauth-client-credentials', + loginUrl: process.env.SALESFORCE_LOGIN_URL, + clientId: process.env.SALESFORCE_CLIENT_ID, + clientSecret: process.env.SALESFORCE_CLIENT_SECRET +}); ``` -### OAuth 2.0 client credentials flow (client_credentials) +#### OAuth 2.0 JWT bearer flow -```properties -SALESFORCE_AUTH_TYPE=oauth-client-credentials -SALESFORCE_LOGIN_URL=YOUR_DOMAIN_URL -SALESFORCE_CLIENT_ID=YOUR_CONNECTED_APP_CLIENT_ID -SALESFORCE_CLIENT_SECRET=YOUR_CONNECTED_APP_CLIENT_SECRET +This is the most secure authentication option. Recommended for production use. -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +```js +// Read private key file +const privateKey = fs.readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE); + +// Build PubSub client +const client = new PubSubApiClient({ + authType: 'oauth-jwt-bearer', + loginUrl: process.env.SALESFORCE_JWT_LOGIN_URL, + clientId: process.env.SALESFORCE_JWT_CLIENT_ID, + username: process.env.SALESFORCE_USERNAME, + privateKey +}); ``` -### OAuth 2.0 JWT bearer flow +### Logging -This is the most secure authentication option. Recommended for production use. +The client uses debug level messages so you can lower the default logging level if you need more information. + +The documentation examples use the default client logger (the console). The console is fine for a test environment but you'll want to switch to a custom logger with asynchronous logging for increased performance. + +You can pass a logger like pino in the client constructor: -```properties -SALESFORCE_AUTH_TYPE=oauth-jwt-bearer -SALESFORCE_LOGIN_URL=https://login.salesforce.com -SALESFORCE_CLIENT_ID=YOUR_CONNECTED_APP_CLIENT_ID -SALESFORCE_USERNAME=YOUR_SALESFORCE_USERNAME -SALESFORCE_PRIVATE_KEY_FILE=PATH_TO_YOUR_KEY_FILE +```js +import pino from 'pino'; -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +const config = { + /* your config goes here */ +}; +const logger = pino(); +const client = new PubSubApiClient(config, logger); ``` -## Basic Example +## Quick Start Example -Here's an example that will get you started quickly. It listens to a single account change event. +Here's an example that will get you started quickly. It listens to up to 3 account change events. Once the third event is reached, the client closes gracefully. 1. Activate Account change events in **Salesforce Setup > Change Data Capture**. -1. Create a `sample.js` file with this content: +1. Install the client and `dotenv` in your project: + + ```sh + npm install salesforce-pubsub-api-client dotenv + ``` + +1. Create a `.env` file at the root of the project and replace the values: + + ```properties + SALESFORCE_LOGIN_URL=... + SALESFORCE_USERNAME=... + SALESFORCE_PASSWORD=... + SALESFORCE_TOKEN=... + ``` + +1. Create a `sample.js` file with the following content: ```js + import * as dotenv from 'dotenv'; import PubSubApiClient from 'salesforce-pubsub-api-client'; async function run() { try { - const client = new PubSubApiClient(); + // Load config from .env file + dotenv.config(); + + // Build and connect Pub/Sub API client + const client = new PubSubApiClient({ + authType: 'username-password', + loginUrl: process.env.SALESFORCE_LOGIN_URL, + username: process.env.SALESFORCE_USERNAME, + password: process.env.SALESFORCE_PASSWORD, + userToken: process.env.SALESFORCE_TOKEN + }); await client.connect(); - // Subscribe to account change events - const eventEmitter = await client.subscribe( - '/data/AccountChangeEvent' - ); - - // Handle incoming events - eventEmitter.on('data', (event) => { - console.log( - `Handling ${event.payload.ChangeEventHeader.entityName} change event ` + - `with ID ${event.replayId} ` + - `on channel ${eventEmitter.getTopicName()} ` + - `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + - `events received so far)` - ); - // Safely log event as a JSON string - console.log( - JSON.stringify( - event, - (key, value) => - /* Convert BigInt values into strings and keep other types unchanged */ - typeof value === 'bigint' - ? value.toString() - : value, - 2 - ) - ); - }); + // Prepare event callback + const subscribeCallback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + // Event received + console.log( + `${subscription.topicName} - ``Handling ${event.payload.ChangeEventHeader.entityName} change event ` + + `with ID ${event.replayId} ` + + `(${subscription.receivedEventCount}/${subscription.requestedEventCount} ` + + `events received so far)` + ); + // Safely log event payload as a JSON string + console.log( + JSON.stringify( + event, + (key, value) => + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === 'bigint' + ? value.toString() + : value, + 2 + ) + ); + } else if (callbackType === 'lastEvent') { + // Last event received + console.log( + `${subscription.topicName} - Reached last of ${subscription.requestedEventCount} requested event on channel. Closing connection.` + ); + } else if (callbackType === 'end') { + // Client closed the connection + console.log('Client shut down gracefully.'); + } + }; + + // Subscribe to 3 account change event + client.subscribe('/data/AccountChangeEvent', subscribeCallback, 3); } catch (error) { console.error(error); } @@ -151,21 +251,20 @@ Here's an example that will get you started quickly. It listens to a single acco If everything goes well, you'll see output like this: ``` - Connected to Salesforce org https://pozil-dev-ed.my.salesforce.com as grpc@pozil.com + Connected to Salesforce org https://pozil-dev-ed.my.salesforce.com (00D58000000arpqEAA) as grpc@pozil.com Connected to Pub/Sub API endpoint api.pubsub.salesforce.com:7443 - Topic schema loaded: /data/AccountChangeEvent - Subscribe request sent for 100 events from /data/AccountChangeEvent... + /data/AccountChangeEvent - Subscribe request sent for 3 events ``` - At this point the script will be on hold and will wait for events. + At this point, the script is on hold and waits for events. 1. Modify an account record in Salesforce. This fires an account change event. - Once the client receives an event, it will display it like this: + Once the client receives an event, it displays it like this: ``` - Received 1 events, latest replay ID: 18098167 - Handling Account change event with ID 18098167 on channel /data/AccountChangeEvent (1/100 events received so far) + /data/AccountChangeEvent - Received 1 events, latest replay ID: 18098167 + /data/AccountChangeEvent - Handling Account change event with ID 18098167 (1/3 events received so far) { "replayId": 18098167, "payload": { @@ -264,8 +363,9 @@ console.log('Published event: ', JSON.stringify(publishResult)); Subscribe to 5 account change events starting from a replay ID: ```js -const eventEmitter = await client.subscribeFromReplayId( +await client.subscribeFromReplayId( '/data/AccountChangeEvent', + subscribeCallback, 5, 17092989 ); @@ -273,11 +373,12 @@ const eventEmitter = await client.subscribeFromReplayId( ### Subscribe to past events in retention window -Subscribe to the 3 earliest past account change events in retention window: +Subscribe to the 3 earliest past account change events in the retention window: ```js -const eventEmitter = await client.subscribeFromEarliestEvent( +await client.subscribeFromEarliestEvent( '/data/AccountChangeEvent', + subscribeCallback, 3 ); ``` @@ -289,7 +390,7 @@ When working with high volumes of events you can control the incoming flow of ev This is the overall process: 1. Pass a number of requested events in your subscribe call. -1. Handle the `lastevent` event from `PubSubEventEmitter` to detect the end of the event batch. +1. Handle the `lastevent` callback type from subscribe callback to detect the end of the event batch. 1. Subscribe to an additional batch of events with `client.requestAdditionalEvents(...)`. If you don't request additional events at this point, the gRPC subscription will close automatically (default Pub/Sub API behavior). The code below illustrate how you can achieve event flow control: @@ -297,27 +398,30 @@ The code below illustrate how you can achieve event flow control: ```js try { // Connect with the Pub/Sub API - const client = new PubSubApiClient(); + const client = new PubSubApiClient(/* config goes here */); await client.connect(); + // Prepare event callback + const subscribeCallback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + // Logic for handling a single event. + // Unless you request additional events later, this should get called up to 10 times + // given the initial subscription boundary. + } else if (callbackType === 'lastEvent') { + // Last event received + console.log( + `${eventEmitter.getTopicName()} - Reached last requested event on channel.` + ); + // Request 10 additional events + client.requestAdditionalEvents(eventEmitter, 10); + } else if (callbackType === 'end') { + // Client closed the connection + console.log('Client shut down gracefully.'); + } + }; + // Subscribe to a batch of 10 account change event - const eventEmitter = await client.subscribe('/data/AccountChangeEvent', 10); - - // Handle incoming events - eventEmitter.on('data', (event) => { - // Logic for handling a single event. - // Unless you request additional events later, this should get called up to 10 times - // given the initial subscription boundary. - }); - - // Handle last requested event - eventEmitter.on('lastevent', () => { - console.log( - `Reached last requested event on channel ${eventEmitter.getTopicName()}.` - ); - // Request 10 additional events - client.requestAdditionalEvents(eventEmitter, 10); - }); + await client.subscribe('/data/AccountChangeEvent', subscribeCallback 10); } catch (error) { console.error(error); } @@ -325,38 +429,21 @@ try { ### Handle gRPC stream lifecycle events -Use the `EventEmmitter` returned by subscribe methods to handle gRPC stream lifecycle events: +Use callback types from subscribe callback to handle gRPC stream lifecycle events: ```js -// Stream end -eventEmitter.on('end', () => { - console.log('gRPC stream ended'); -}); - -// Stream error -eventEmitter.on('error', (error) => { - console.error('gRPC stream error: ', JSON.stringify(error)); -}); - -// Stream status update -eventEmitter.on('status', (status) => { - console.log('gRPC stream status: ', status); -}); -``` - -### Use a custom logger - -The client logs output to the console by default but you can provide your favorite logger in the client constructor. - -When in production, asynchronous logging is preferable for performance reasons. - -For example: - -```js -import pino from 'pino'; - -const logger = pino(); -const client = new PubSubApiClient(logger); +const subscribeCallback = (subscription, callbackType, data) => { + if (callbackType === 'grpcStatus') { + // Stream status update + console.log('gRPC stream status: ', status); + } else if (callbackType === 'error') { + // Stream error + console.error('gRPC stream error: ', JSON.stringify(error)); + } else if (callbackType === 'end') { + // Stream end + console.log('gRPC stream ended'); + } +}; ``` ## Common Issues @@ -390,13 +477,14 @@ console.log( Client for the Salesforce Pub/Sub API -#### `PubSubApiClient([logger])` +#### `PubSubApiClient(configuration, [logger])` Builds a new Pub/Sub API client. -| Name | Type | Description | -| -------- | ------ | ------------------------------------------------------------------------------- | -| `logger` | Logger | an optional custom logger. The client uses the console if no value is supplied. | +| Name | Type | Description | +| --------------- | ------------------------------- | ------------------------------------------------------------------------------- | +| `configuration` | [Configuration](#configuration) | The client configuration (authentication...). | +| `logger` | Logger | An optional custom logger. The client uses the console if no value is supplied. | #### `close()` @@ -404,29 +492,17 @@ Closes the gRPC connection. The client will no longer receive events for any top #### `async connect() → {Promise.}` -Authenticates with Salesforce then, connects to the Pub/Sub API. +Authenticates with Salesforce then connects to the Pub/Sub API. Returns: Promise that resolves once the connection is established. -#### `async connectWithAuth(accessToken, instanceUrl, organizationIdopt) → {Promise.}` - -Connects to the Pub/Sub API with user-supplied authentication. - -Returns: Promise that resolves once the connection is established. - -| Name | Type | Description | -| ---------------- | ------ | --------------------------------------------------------------------------------------------------- | -| `accessToken` | string | Salesforce access token | -| `instanceUrl` | string | Salesforce instance URL | -| `organizationId` | string | optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. | - #### `async getConnectivityState() → Promise}` Get connectivity state from current channel. -Returns: Promise that holds channel's [connectivity state](https://grpc.github.io/grpc/node/grpc.html#.connectivityState). +Returns: Promise that holds the channel's [connectivity state](https://grpc.github.io/grpc/node/grpc.html#.connectivityState). -#### `async publish(topicName, payload, correlationKeyopt) → {Promise.}` +#### `async publish(topicName, payload, [correlationKey]) → {Promise.}` Publishes a payload to a topic using the gRPC client. @@ -438,72 +514,79 @@ Returns: Promise holding a `PublishResult` object with `replayId` and `correlati | `payload` | Object | | | `correlationKey` | string | optional correlation key. If you don't provide one, we'll generate a random UUID for you. | -#### `async subscribe(topicName, [numRequested]) → {Promise.}` +#### `async subscribe(topicName, subscribeCallback, [numRequested])` Subscribes to a topic. -Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. - -| Name | Type | Description | -| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| `topicName` | string | name of the topic that we're subscribing to | -| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | +| Name | Type | Description | +| ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `subscribeCallback` | [SubscribeCallback](#subscribecallback) | subscribe callback function | +| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | -#### `async subscribeFromEarliestEvent(topicName, [numRequested]) → {Promise.}` +#### `async subscribeFromEarliestEvent(topicName, subscribeCallback, [numRequested])` Subscribes to a topic and retrieves all past events in retention window. -Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. +| Name | Type | Description | +| ------------------- | --------------------------------------- | -------------------------------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `subscribeCallback` | [SubscribeCallback](#subscribecallback) | subscribe callback function | +| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | -| Name | Type | Description | -| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | -| `topicName` | string | name of the topic that we're subscribing to | -| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | - -#### `async subscribeFromReplayId(topicName, numRequested, replayId) → {Promise.}` +#### `async subscribeFromReplayId(topicName, subscribeCallback, numRequested, replayId)` Subscribes to a topic and retrieves past events starting from a replay ID. -Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. - -| Name | Type | Description | -| -------------- | ------ | --------------------------------------------------------------------------------------- | -| `topicName` | string | name of the topic that we're subscribing to | -| `numRequested` | number | number of events requested. If `null`, the client keeps the subscription alive forever. | -| `replayId` | number | replay ID | +| Name | Type | Description | +| ------------------- | --------------------------------------- | --------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `subscribeCallback` | [SubscribeCallback](#subscribecallback) | subscribe callback function | +| `numRequested` | number | number of events requested. If `null`, the client keeps the subscription alive forever. | +| `replayId` | number | replay ID | -#### `requestAdditionalEvents(eventEmitter, numRequested)` +#### `requestAdditionalEvents(topicName, numRequested)` Request additional events on an existing subscription. -| Name | Type | Description | -| -------------- | ------------------ | ----------------------------------------------------------- | -| `eventEmitter` | PubSubEventEmitter | event emitter that was obtained in the first subscribe call | -| `numRequested` | number | number of events requested. | +| Name | Type | Description | +| -------------- | ------ | --------------------------- | +| `topicName` | string | name of the topic. | +| `numRequested` | number | number of events requested. | -### PubSubEventEmitter +### SubscribeCallback -EventEmitter wrapper for processing incoming Pub/Sub API events while keeping track of the topic name and the volume of events requested/received. +Callback function that lets you process incoming Pub/Sub API events while keeping track of the topic name and the volume of events requested/received. -The emitter sends the following events: +The function takes three parameters: -| Event Name | Event Data | Description | -| ----------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| `data` | Object | Client received a new event. The attached data is the parsed event data. | -| `error` | `EventParseError \| Object` | Signals an event parsing error or a gRPC stream error. | -| `lastevent` | void | Signals that we received the last event that the client requested. The stream will end shortly. | -| `keepalive` | `{ latestReplayId: number, pendingNumRequested: number }` | Server publishes this keep alive message every 270 seconds (or less) if there are no events. | -| `end` | void | Signals the end of the gRPC stream. | -| `status` | Object | Misc gRPC stream status information. | +| Name | Type | Description | +| -------------- | ------------------------------------- | --------------------------------------------------------------------- | +| `subscription` | [SubscriptionInfo](#subscriptioninfo) | subscription information | +| `callbackType` | string | name of the callback type (see table below). | +| `data` | [Object] | data that is passed with the callback (depends on the callback type). | -The emitter also exposes these methods: +Callback types: -| Method | Description | -| -------------------------- | ------------------------------------------------------------------------------------------ | -| `getRequestedEventCount()` | Returns the number of events that were requested when subscribing. | -| `getReceivedEventCount()` | Returns the number of events that were received since subscribing. | -| `getTopicName()` | Returns the topic name for this subscription. | -| `getLatestReplayId()` | Returns the replay ID of the last processed event or `null` if no event was processed yet. | +| Name | Callback Data | Description | +| --------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| `data` | Object | Client received a new event. The attached data is the parsed event data. | +| `error` | [EventParseError](#eventparseerror) or Object | Signals an event parsing error or a gRPC stream error. | +| `lastevent` | void | Signals that we received the last event that the client requested. The stream will end shortly. | +| `end` | void | Signals the end of the gRPC stream. | +| `grpcKeepalive` | `{ latestReplayId: number, pendingNumRequested: number }` | Server publishes this gRPC keep alive message every 270 seconds (or less) if there are no events. | +| `grpcStatus` | Object | Misc gRPC stream status information. | + +### SubscriptionInfo + +Holds the information related to a subscription. + +| Name | Type | Description | +| --------------------- | ------ | ------------------------------------------------------------------------------ | +| `topicName` | string | topic name for this subscription. | +| `requestedEventCount` | number | number of events that were requested when subscribing. | +| `receivedEventCount` | number | the number of events that were received since subscribing. | +| `lastReplayId` | number | replay ID of the last processed event or `null` if no event was processed yet. | ### EventParseError @@ -516,3 +599,22 @@ Holds the information related to an event parsing error. This class attempts to | `replayId` | number | The replay ID of the event at the origin of the error. Could be undefined if we're not able to extract it from the event data. | | `event` | Object | The un-parsed event data at the origin of the error. | | `latestReplayId` | number | The latest replay ID that was received before the error. | + +### Configuration + +Check out the [authentication](#authentication) section for more information on how to provide the right values. + +| Name | Type | Description | +| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------- | +| `authType` | string | Authentication type. One of `user-supplied`, `username-password`, `oauth-client-credentials` or `oauth-jwt-bearer`. | +| `pubSubEndpoint` | string | A custom Pub/Sub API endpoint. The default endpoint `api.pubsub.salesforce.com:7443` is used if none is supplied. | +| `accessToken` | string | Salesforce access token. | +| `instanceUrl` | string | Salesforce instance URL. | +| `organizationId` | string | Optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. | +| `loginUrl` | string | Salesforce login host. One of `https://login.salesforce.com`, `https://test.salesforce.com` or your domain specific host. | +| `clientId` | string | Connected app client ID. | +| `clientSecret` | string | Connected app client secret. | +| `privateKey` | string | Private key content. | +| `username` | string | Salesforce username. | +| `password` | string | Salesforce user password. | +| `userToken` | string | Salesforce user security token. | diff --git a/dist/client.cjs b/dist/client.cjs index f35cfbd..646a3da 100644 --- a/dist/client.cjs +++ b/dist/client.cjs @@ -33,7 +33,7 @@ __export(client_exports, { }); module.exports = __toCommonJS(client_exports); var import_crypto2 = __toESM(require("crypto"), 1); -var import_fs2 = __toESM(require("fs"), 1); +var import_fs = __toESM(require("fs"), 1); var import_url = require("url"); var import_avro_js3 = __toESM(require("avro-js"), 1); var import_certifi = __toESM(require("certifi"), 1); @@ -115,73 +115,6 @@ var EventParseError = class extends Error { } }; -// src/utils/pubSubEventEmitter.js -var import_events = require("events"); -var PubSubEventEmitter = class extends import_events.EventEmitter { - #topicName; - #requestedEventCount; - #receivedEventCount; - #latestReplayId; - /** - * Create a new EventEmitter for Pub/Sub API events - * @param {string} topicName - * @param {number} requestedEventCount - * @protected - */ - constructor(topicName, requestedEventCount) { - super(); - this.#topicName = topicName; - this.#requestedEventCount = requestedEventCount; - this.#receivedEventCount = 0; - this.#latestReplayId = null; - } - emit(eventName, args) { - if (eventName === "data") { - this.#receivedEventCount++; - this.#latestReplayId = args.replayId; - } - return super.emit(eventName, args); - } - /** - * Returns the number of events that were requested when subscribing. - * @returns {number} the number of events that were requested - */ - getRequestedEventCount() { - return this.#requestedEventCount; - } - /** - * Returns the number of events that were received since subscribing. - * @returns {number} the number of events that were received - */ - getReceivedEventCount() { - return this.#receivedEventCount; - } - /** - * Returns the topic name for this subscription. - * @returns {string} the topic name - */ - getTopicName() { - return this.#topicName; - } - /** - * Returns the replay ID of the last processed event or null if no event was processed yet. - * @return {number} replay ID - */ - getLatestReplayId() { - return this.#latestReplayId; - } - /** - * @protected - * Resets the requested/received event counts. - * This method should only be be used internally by the client when it resubscribes. - * @param {number} newRequestedEventCount - */ - _resetEventCount(newRequestedEventCount) { - this.#requestedEventCount = newRequestedEventCount; - this.#receivedEventCount = 0; - } -}; - // src/utils/avroHelper.js var import_avro_js = __toESM(require("avro-js"), 1); var CustomLongAvroType = import_avro_js.default.types.LongType.using({ @@ -213,95 +146,94 @@ var CustomLongAvroType = import_avro_js.default.types.LongType.using({ }); // src/utils/configuration.js -var dotenv = __toESM(require("dotenv"), 1); -var import_fs = __toESM(require("fs"), 1); -var AUTH_USER_SUPPLIED = "user-supplied"; -var AUTH_USERNAME_PASSWORD = "username-password"; -var AUTH_OAUTH_CLIENT_CREDENTIALS = "oauth-client-credentials"; -var AUTH_OAUTH_JWT_BEARER = "oauth-jwt-bearer"; +var DEFAULT_PUB_SUB_ENDPOINT = "api.pubsub.salesforce.com:7443"; +var AuthType = { + USER_SUPPLIED: "user-supplied", + USERNAME_PASSWORD: "username-password", + OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", + OAUTH_JWT_BEARER: "oauth-jwt-bearer" +}; var Configuration = class _Configuration { - static load() { - dotenv.config(); - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_AUTH_TYPE", - "PUB_SUB_ENDPOINT" + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} the sanitized client configuration + */ + static load(config) { + config.pubSubEndpoint = config.pubSubEndpoint ?? DEFAULT_PUB_SUB_ENDPOINT; + _Configuration.#checkMandatoryVariables(config, ["authType"]); + switch (config.authType) { + case AuthType.USER_SUPPLIED: + config = _Configuration.#loadUserSuppliedAuth(config); + break; + case AuthType.USERNAME_PASSWORD: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "username", + "password" + ]); + config.userToken = config.userToken ?? ""; + break; + case AuthType.OAUTH_CLIENT_CREDENTIALS: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "clientId", + "clientSecret" + ]); + break; + case AuthType.OAUTH_JWT_BEARER: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "clientId", + "username", + "privateKey" + ]); + break; + default: + throw new Error( + `Unsupported authType value: ${config.authType}` + ); + } + return config; + } + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} sanitized configuration + */ + static #loadUserSuppliedAuth(config) { + _Configuration.#checkMandatoryVariables(config, [ + "accessToken", + "instanceUrl" ]); - if (_Configuration.isUsernamePasswordAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_USERNAME", - "SALESFORCE_PASSWORD" - ]); - } else if (_Configuration.isOAuthClientCredentialsAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_CLIENT_ID", - "SALESFORCE_CLIENT_SECRET" - ]); - } else if (_Configuration.isOAuthJwtBearerAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_CLIENT_ID", - "SALESFORCE_USERNAME", - "SALESFORCE_PRIVATE_KEY_FILE" - ]); - _Configuration.getSfPrivateKey(); - } else if (!_Configuration.isUserSuppliedAuth()) { + if (!config.instanceUrl.startsWith("https://")) { throw new Error( - `Invalid value for SALESFORCE_AUTH_TYPE environment variable: ${_Configuration.getAuthType()}` + `Invalid Salesforce Instance URL format supplied: ${config.instanceUrl}` ); } - } - static getAuthType() { - return process.env.SALESFORCE_AUTH_TYPE; - } - static getSfLoginUrl() { - return process.env.SALESFORCE_LOGIN_URL; - } - static getSfUsername() { - return process.env.SALESFORCE_USERNAME; - } - static getSfSecuredPassword() { - if (process.env.SALESFORCE_TOKEN) { - return process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_TOKEN; + if (!config.organizationId) { + try { + config.organizationId = config.accessToken.split("!").at(0); + } catch (error) { + throw new Error( + "Unable to parse organizationId from access token", + { + cause: error + } + ); + } } - return process.env.SALESFORCE_PASSWORD; - } - static getSfClientId() { - return process.env.SALESFORCE_CLIENT_ID; - } - static getSfClientSecret() { - return process.env.SALESFORCE_CLIENT_SECRET; - } - static getSfPrivateKey() { - try { - const keyPath = process.env.SALESFORCE_PRIVATE_KEY_FILE; - return import_fs.default.readFileSync(keyPath, "utf8"); - } catch (error) { - throw new Error("Failed to load private key file", { - cause: error - }); + if (config.organizationId.length !== 15 && config.organizationId.length !== 18) { + throw new Error( + `Invalid Salesforce Org ID format supplied: ${config.organizationId}` + ); } + return config; } - static getPubSubEndpoint() { - return process.env.PUB_SUB_ENDPOINT; - } - static isUserSuppliedAuth() { - return _Configuration.getAuthType() === AUTH_USER_SUPPLIED; - } - static isUsernamePasswordAuth() { - return _Configuration.getAuthType() === AUTH_USERNAME_PASSWORD; - } - static isOAuthClientCredentialsAuth() { - return _Configuration.getAuthType() === AUTH_OAUTH_CLIENT_CREDENTIALS; - } - static isOAuthJwtBearerAuth() { - return _Configuration.getAuthType() === AUTH_OAUTH_JWT_BEARER; - } - static #checkMandatoryVariables(varNames) { + static #checkMandatoryVariables(config, varNames) { varNames.forEach((varName) => { - if (!process.env[varName]) { - throw new Error(`Missing ${varName} environment variable`); + if (!config[varName]) { + throw new Error( + `Missing value for ${varName} mandatory configuration key` + ); } }); } @@ -416,6 +348,15 @@ function encodeReplayId(replayId) { buf.writeBigUInt64BE(BigInt(replayId), 0); return buf; } +function toJsonString(event) { + return JSON.stringify( + event, + (key, value) => ( + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === "bigint" ? value.toString() : value + ) + ); +} function hexToBin(hex) { let bin = hex.substring(2); bin = bin.replaceAll("0", "0000"); @@ -441,88 +382,113 @@ function hexToBin(hex) { var import_crypto = __toESM(require("crypto"), 1); var import_jsforce = __toESM(require("jsforce"), 1); var import_undici = require("undici"); -var SalesforceAuth = class _SalesforceAuth { +var SalesforceAuth = class { + /** + * Client configuration + * @type {Configuration} + */ + #config; + /** + * Logger + * @type {Logger} + */ + #logger; + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + * @param {Logger} logger a logger + */ + constructor(config, logger) { + this.#config = config; + this.#logger = logger; + } /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ - static async authenticate() { - if (Configuration.isUsernamePasswordAuth()) { - return _SalesforceAuth.#authWithUsernamePassword(); - } else if (Configuration.isOAuthClientCredentialsAuth()) { - return _SalesforceAuth.#authWithOAuthClientCredentials(); - } else if (Configuration.isOAuthJwtBearerAuth()) { - return _SalesforceAuth.#authWithJwtBearer(); - } else { - throw new Error("Unsupported authentication mode."); + async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + throw new Error( + "Authenticate method should not be called in user-supplied mode." + ); + case AuthType.USERNAME_PASSWORD: + return this.#authWithUsernamePassword(); + case AuthType.OAUTH_CLIENT_CREDENTIALS: + return this.#authWithOAuthClientCredentials(); + case AuthType.OAUTH_JWT_BEARER: + return this.#authWithJwtBearer(); + default: + throw new Error( + `Unsupported authType value: ${this.#config.authType}` + ); } } /** * Authenticates with the username/password flow * @returns {ConnectionMetadata} */ - static async #authWithUsernamePassword() { + async #authWithUsernamePassword() { + const { loginUrl, username, password, userToken } = this.#config; const sfConnection = new import_jsforce.default.Connection({ - loginUrl: Configuration.getSfLoginUrl() + loginUrl }); - await sfConnection.login( - Configuration.getSfUsername(), - Configuration.getSfSecuredPassword() - ); + await sfConnection.login(username, `${password}${userToken}`); return { accessToken: sfConnection.accessToken, instanceUrl: sfConnection.instanceUrl, organizationId: sfConnection.userInfo.organizationId, - username: Configuration.getSfUsername() + username }; } /** * Authenticates with the OAuth 2.0 client credentials flow * @returns {ConnectionMetadata} */ - static async #authWithOAuthClientCredentials() { + async #authWithOAuthClientCredentials() { + const { clientId, clientSecret } = this.#config; const params = new URLSearchParams(); params.append("grant_type", "client_credentials"); - params.append("client_id", Configuration.getSfClientId()); - params.append("client_secret", Configuration.getSfClientSecret()); - return _SalesforceAuth.#authWithOAuth(params.toString()); + params.append("client_id", clientId); + params.append("client_secret", clientSecret); + return this.#authWithOAuth(params.toString()); } /** * Authenticates with the OAuth 2.0 JWT bearer flow * @returns {ConnectionMetadata} */ - static async #authWithJwtBearer() { + async #authWithJwtBearer() { + const { clientId, username, loginUrl, privateKey } = this.#config; const header = JSON.stringify({ alg: "RS256" }); const claims = JSON.stringify({ - iss: Configuration.getSfClientId(), - sub: Configuration.getSfUsername(), - aud: Configuration.getSfLoginUrl(), + iss: clientId, + sub: username, + aud: loginUrl, exp: Math.floor(Date.now() / 1e3) + 60 * 5 }); let token = `${base64url(header)}.${base64url(claims)}`; const sign = import_crypto.default.createSign("RSA-SHA256"); sign.update(token); sign.end(); - token += `.${base64url(sign.sign(Configuration.getSfPrivateKey()))}`; + token += `.${base64url(sign.sign(privateKey))}`; const body = `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${token}`; - return _SalesforceAuth.#authWithOAuth(body); + return this.#authWithOAuth(body); } /** * Generic OAuth 2.0 connect method * @param {string} body URL encoded body * @returns {ConnectionMetadata} connection metadata */ - static async #authWithOAuth(body) { - const loginResponse = await (0, import_undici.fetch)( - `${Configuration.getSfLoginUrl()}/services/oauth2/token`, - { - method: "post", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body - } - ); + async #authWithOAuth(body) { + const { loginUrl } = this.#config; + const loginResponse = await (0, import_undici.fetch)(`${loginUrl}/services/oauth2/token`, { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body + }); if (loginResponse.status !== 200) { throw new Error( `Authentication error: HTTP ${loginResponse.status} - ${await loginResponse.text()}` @@ -530,7 +496,7 @@ var SalesforceAuth = class _SalesforceAuth { } const { access_token, instance_url } = await loginResponse.json(); const userInfoResponse = await (0, import_undici.fetch)( - `${Configuration.getSfLoginUrl()}/services/oauth2/userinfo`, + `${loginUrl}/services/oauth2/userinfo`, { headers: { authorization: `Bearer ${access_token}` } } @@ -555,8 +521,21 @@ function base64url(input) { } // src/client.js +var SubscribeCallbackType = { + EVENT: "event", + LAST_EVENT: "lastEvent", + ERROR: "error", + END: "end", + GRPC_STATUS: "grpcStatus", + GRPC_KEEP_ALIVE: "grpcKeepAlive" +}; var MAX_EVENT_BATCH_SIZE = 100; var PubSubApiClient = class { + /** + * Client configuration + * @type {Configuration} + */ + #config; /** * gRPC client * @type {Object} @@ -568,21 +547,26 @@ var PubSubApiClient = class { */ #schemaChache; /** - * Map of subscribitions indexed by topic name - * @type {Map} + * Map of subscriptions indexed by topic name + * @type {Map} */ #subscriptions; + /** + * Logger + * @type {Logger} + */ #logger; /** * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration * @param {Logger} [logger] an optional custom logger. The client uses the console if no value is supplied. */ - constructor(logger = console) { + constructor(config, logger = console) { this.#logger = logger; this.#schemaChache = new SchemaCache(); this.#subscriptions = /* @__PURE__ */ new Map(); try { - Configuration.load(); + this.#config = Configuration.load(config); } catch (error) { this.#logger.error(error); throw new Error("Failed to initialize Pub/Sub API client", { @@ -591,86 +575,43 @@ var PubSubApiClient = class { } } /** - * Authenticates with Salesforce then, connects to the Pub/Sub API. + * Authenticates with Salesforce (if not using user-supplied authentication mode) then, + * connects to the Pub/Sub API. * @returns {Promise} Promise that resolves once the connection is established * @memberof PubSubApiClient.prototype */ async connect() { - if (Configuration.isUserSuppliedAuth()) { - throw new Error( - 'You selected user-supplied authentication mode so you cannot use the "connect()" method. Use "connectWithAuth(...)" instead.' - ); - } - let conMetadata; - try { - conMetadata = await SalesforceAuth.authenticate(); - this.#logger.info( - `Connected to Salesforce org ${conMetadata.instanceUrl} as ${conMetadata.username}` - ); - } catch (error) { - throw new Error("Failed to authenticate with Salesforce", { - cause: error - }); - } - return this.#connectToPubSubApi(conMetadata); - } - /** - * Connects to the Pub/Sub API with user-supplied authentication. - * @param {string} accessToken Salesforce access token - * @param {string} instanceUrl Salesforce instance URL - * @param {string} [organizationId] optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. - * @returns {Promise} Promise that resolves once the connection is established - * @memberof PubSubApiClient.prototype - */ - async connectWithAuth(accessToken, instanceUrl, organizationId) { - if (!instanceUrl || !instanceUrl.startsWith("https://")) { - throw new Error( - `Invalid Salesforce Instance URL format supplied: ${instanceUrl}` - ); - } - let validOrganizationId = organizationId; - if (!organizationId) { + if (this.#config.authType !== AuthType.USER_SUPPLIED) { try { - validOrganizationId = accessToken.split("!").at(0); - } catch (error) { - throw new Error( - "Unable to parse organizationId from given access token", - { - cause: error - } + const auth = new SalesforceAuth(this.#config, this.#logger); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#config.organizationId = conMetadata.organizationId; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); + } catch (error) { + throw new Error("Failed to authenticate with Salesforce", { + cause: error + }); } } - if (validOrganizationId.length !== 15 && validOrganizationId.length !== 18) { - throw new Error( - `Invalid Salesforce Org ID format supplied: ${validOrganizationId}` - ); - } - return this.#connectToPubSubApi({ - accessToken, - instanceUrl, - organizationId: validOrganizationId - }); - } - /** - * Connects to the Pub/Sub API. - * @param {import('./auth.js').ConnectionMetadata} conMetadata - * @returns {Promise} Promise that resolves once the connection is established - */ - async #connectToPubSubApi(conMetadata) { try { - const rootCert = import_fs2.default.readFileSync(import_certifi.default); + this.#logger.debug(`Connecting to Pub/Sub API`); + const rootCert = import_fs.default.readFileSync(import_certifi.default); const protoFilePath = (0, import_url.fileURLToPath)( - new URL("./pubsub_api-be352429.proto?hash=be352429", "file://" + __filename) + new URL("./pubsub_api-07e1f84a.proto?hash=07e1f84a", "file://" + __filename) ); const packageDef = import_proto_loader.default.loadSync(protoFilePath, {}); const grpcObj = import_grpc_js.default.loadPackageDefinition(packageDef); const sfdcPackage = grpcObj.eventbus.v1; const metaCallback = (_params, callback) => { const meta = new import_grpc_js.default.Metadata(); - meta.add("accesstoken", conMetadata.accessToken); - meta.add("instanceurl", conMetadata.instanceUrl); - meta.add("tenantid", conMetadata.organizationId); + meta.add("accesstoken", this.#config.accessToken); + meta.add("instanceurl", this.#config.instanceUrl); + meta.add("tenantid", this.#config.organizationId); callback(null, meta); }; const callCreds = import_grpc_js.default.credentials.createFromMetadataGenerator(metaCallback); @@ -679,11 +620,11 @@ var PubSubApiClient = class { callCreds ); this.#client = new sfdcPackage.PubSub( - Configuration.getPubSubEndpoint(), + this.#config.pubSubEndpoint, combCreds ); this.#logger.info( - `Connected to Pub/Sub API endpoint ${Configuration.getPubSubEndpoint()}` + `Connected to Pub/Sub API endpoint ${this.#config.pubSubEndpoint}` ); } catch (error) { throw new Error("Failed to connect to Pub/Sub API", { @@ -702,52 +643,64 @@ var PubSubApiClient = class { /** * Subscribes to a topic and retrieves all past events in retention window. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromEarliestEvent(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 1 - }); + subscribeFromEarliestEvent(topicName, subscribeCallback, numRequested = null) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 1 + }, + subscribeCallback + ); } /** * Subscribes to a topic and retrieves past events starting from a replay ID. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} numRequested number of events requested. If null, the client keeps the subscription alive forever. * @param {number} replayId replay ID - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromReplayId(topicName, numRequested, replayId) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 2, - replayId: encodeReplayId(replayId) - }); + subscribeFromReplayId(topicName, subscribeCallback, numRequested, replayId) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 2, + replayId: encodeReplayId(replayId) + }, + subscribeCallback + ); } /** * Subscribes to a topic. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribe(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested - }); + subscribe(topicName, subscribeCallback, numRequested = null) { + this.#subscribe( + { + topicName, + numRequested + }, + subscribeCallback + ); } /** * Subscribes to a topic using the gRPC client and an event schema - * @param {object} subscribeRequest subscription request - * @return {PubSubEventEmitter} emitter that allows you to listen to received events and stream lifecycle events + * @param {SubscribeRequest} subscribeRequest subscription request + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ - async #subscribe(subscribeRequest) { + #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -769,38 +722,72 @@ var PubSubApiClient = class { this.#logger.warn( `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` ); + subscribeRequest.numRequested = MAX_EVENT_BATCH_SIZE; } } if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); } let subscription = this.#subscriptions.get(topicName); - if (!subscription) { - subscription = this.#client.Subscribe(); + let grpcSubscription; + if (subscription) { + this.#logger.debug( + `${topicName} - Reusing cached gRPC subscription` + ); + grpcSubscription = subscription.grpcSubscription; + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = subscribeRequest.numRequested; + } else { + this.#logger.debug( + `${topicName} - Establishing new gRPC subscription` + ); + grpcSubscription = this.#client.Subscribe(); + subscription = { + info: { + topicName, + requestedEventCount: subscribeRequest.numRequested, + receivedEventCount: 0, + lastReplayId: null + }, + grpcSubscription, + subscribeCallback + }; this.#subscriptions.set(topicName, subscription); } - subscription.write(subscribeRequest); - this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` - ); - const eventEmitter = new PubSubEventEmitter( - topicName, - numRequested - ); - subscription.on("data", async (data) => { + grpcSubscription.on("data", async (data) => { const latestReplayId = decodeReplayId(data.latestReplayId); + subscription.info.lastReplayId = latestReplayId; if (data.events) { this.#logger.info( - `Received ${data.events.length} events, latest replay ID: ${latestReplayId}` + `${topicName} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` ); for (const event of data.events) { try { + this.#logger.debug( + `${topicName} - Raw event: ${toJsonString(event)}` + ); + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${event.event.schemaId}` + ); const schema = await this.#getEventSchemaFromId( event.event.schemaId ); + const subscription2 = this.#subscriptions.get(topicName); + if (!subscription2) { + throw new Error( + `Failed to retrieve subscription for topic ${topicName}.` + ); + } + subscription2.info.receivedEventCount++; const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); - eventEmitter.emit("data", parsedEvent); + this.#logger.debug( + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` + ); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { let replayId; try { @@ -815,46 +802,67 @@ var PubSubApiClient = class { event, latestReplayId ); - eventEmitter.emit("error", parseError); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + parseError + ); this.#logger.error(parseError); } - if (eventEmitter.getReceivedEventCount() === eventEmitter.getRequestedEventCount()) { + if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { if (isInfiniteEventRequest) { this.requestAdditionalEvents( - eventEmitter, - MAX_EVENT_BATCH_SIZE + subscription.info.topicName, + subscription.info.requestedEventCount ); } else { - eventEmitter.emit("lastevent"); + subscribeCallback( + subscription.info, + SubscribeCallbackType.LAST_EVENT + ); } } } } else { this.#logger.debug( - `Received keepalive message. Latest replay ID: ${latestReplayId}` + `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` ); data.latestReplayId = latestReplayId; - eventEmitter.emit("keepalive", data); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_KEEP_ALIVE + ); } }); - subscription.on("end", () => { + grpcSubscription.on("end", () => { this.#subscriptions.delete(topicName); - this.#logger.info("gRPC stream ended"); - eventEmitter.emit("end"); + this.#logger.info(`${topicName} - gRPC stream ended`); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on("error", (error) => { + grpcSubscription.on("error", (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error ); - eventEmitter.emit("error", error); }); - subscription.on("status", (status) => { + grpcSubscription.on("status", (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status ); - eventEmitter.emit("status", status); }); - return eventEmitter; + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `${topicName} - Subscribe request sent for ${numRequested} events` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -864,24 +872,24 @@ var PubSubApiClient = class { } /** * Request additional events on an existing subscription. - * @param {PubSubEventEmitter} eventEmitter event emitter that was obtained in the first subscribe call + * @param {string} topicName topic name * @param {number} numRequested number of events requested. */ - async requestAdditionalEvents(eventEmitter, numRequested) { - const topicName = eventEmitter.getTopicName(); + requestAdditionalEvents(topicName, numRequested) { const subscription = this.#subscriptions.get(topicName); if (!subscription) { throw new Error( `Failed to request additional events for topic ${topicName}, no active subscription found.` ); } - eventEmitter._resetEventCount(numRequested); + subscription.receivedEventCount = 0; + subscription.requestedEventCount = numRequested; subscription.write({ topicName, numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } /** @@ -894,6 +902,9 @@ var PubSubApiClient = class { */ async publish(topicName, payload, correlationKey) { try { + this.#logger.debug( + `${topicName} - Preparing to publish event: ${toJsonString(payload)}` + ); if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); } @@ -936,9 +947,6 @@ var PubSubApiClient = class { */ close() { this.#logger.info("Clear subscriptions"); - this.#subscriptions.forEach((subscription) => { - subscription.removeAllListeners(); - }); this.#subscriptions.clear(); this.#logger.info("Closing gRPC stream"); this.#client.close(); @@ -977,13 +985,15 @@ var PubSubApiClient = class { reject(topicError); } else { const { schemaId } = response; + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${schemaId}` + ); let schema = this.#schemaChache.getFromId(schemaId); if (!schema) { schema = await this.#fetchEventSchemaFromIdWithClient( schemaId ); } - this.#logger.info(`Topic schema loaded: ${topicName}`); this.#schemaChache.set(schema); resolve(schema); } diff --git a/dist/client.d.ts b/dist/client.d.ts index 76f367f..839e9ee 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -6,24 +6,17 @@ export default class PubSubApiClient { /** * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration * @param {Logger} [logger] an optional custom logger. The client uses the console if no value is supplied. */ - constructor(logger?: Logger); + constructor(config: Configuration, logger?: Logger); /** - * Authenticates with Salesforce then, connects to the Pub/Sub API. + * Authenticates with Salesforce (if not using user-supplied authentication mode) then, + * connects to the Pub/Sub API. * @returns {Promise} Promise that resolves once the connection is established * @memberof PubSubApiClient.prototype */ connect(): Promise; - /** - * Connects to the Pub/Sub API with user-supplied authentication. - * @param {string} accessToken Salesforce access token - * @param {string} instanceUrl Salesforce instance URL - * @param {string} [organizationId] optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. - * @returns {Promise} Promise that resolves once the connection is established - * @memberof PubSubApiClient.prototype - */ - connectWithAuth(accessToken: string, instanceUrl: string, organizationId?: string): Promise; /** * Get connectivity state from current channel. * @returns {Promise} Promise that holds channel's connectivity information {@link connectivityState} @@ -33,34 +26,34 @@ export default class PubSubApiClient { /** * Subscribes to a topic and retrieves all past events in retention window. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - subscribeFromEarliestEvent(topicName: string, numRequested?: number | null): Promise; + subscribeFromEarliestEvent(topicName: string, subscribeCallback: SubscribeCallback, numRequested?: number | null): void; /** * Subscribes to a topic and retrieves past events starting from a replay ID. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} numRequested number of events requested. If null, the client keeps the subscription alive forever. * @param {number} replayId replay ID - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - subscribeFromReplayId(topicName: string, numRequested: number | null, replayId: number): Promise; + subscribeFromReplayId(topicName: string, subscribeCallback: SubscribeCallback, numRequested: number | null, replayId: number): void; /** * Subscribes to a topic. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - subscribe(topicName: string, numRequested?: number | null): Promise; + subscribe(topicName: string, subscribeCallback: SubscribeCallback, numRequested?: number | null): void; /** * Request additional events on an existing subscription. - * @param {PubSubEventEmitter} eventEmitter event emitter that was obtained in the first subscribe call + * @param {string} topicName topic name * @param {number} numRequested number of events requested. */ - requestAdditionalEvents(eventEmitter: PubSubEventEmitter, numRequested: number): Promise; + requestAdditionalEvents(topicName: string, numRequested: number): void; /** * Publishes a payload to a topic using the gRPC client. * @param {string} topicName name of the topic that we're subscribing to @@ -81,12 +74,58 @@ export type PublishResult = { replayId: number; correlationKey: string; }; +export type SubscribeCallback = (subscription: SubscriptionInfo, callbackType: SubscribeCallbackType, data?: any) => any; +export type Subscription = { + info: SubscriptionInfo; + grpcSubscription: any; + subscribeCallback: SubscribeCallback; +}; +export type SubscriptionInfo = { + topicName: string; + requestedEventCount: number; + receivedEventCount: number; + lastReplayId: number; +}; +export type Configuration = { + authType: AuthType; + pubSubEndpoint: string; + loginUrl: string; + username: string; + password: string; + userToken: string; + clientId: string; + clientSecret: string; + privateKey: string; + accessToken: string; + instanceUrl: string; + organizationId: string; +}; export type Logger = { debug: Function; info: Function; error: Function; warn: Function; }; +export type SubscribeRequest = { + topicName: string; + numRequested: number; + replayPreset?: number; + replayId?: number; +}; import { connectivityState } from '@grpc/grpc-js'; -import PubSubEventEmitter from './utils/pubSubEventEmitter.js'; +import { Configuration } from './utils/configuration.js'; +/** + * Enum for subscripe callback type values + */ +type SubscribeCallbackType = string; +declare namespace SubscribeCallbackType { + let EVENT: string; + let LAST_EVENT: string; + let ERROR: string; + let END: string; + let GRPC_STATUS: string; + let GRPC_KEEP_ALIVE: string; +} +import { AuthType } from './utils/configuration.js'; +export {}; //# sourceMappingURL=client.d.ts.map \ No newline at end of file diff --git a/dist/client.d.ts.map b/dist/client.d.ts.map index 8e01edc..8cd54d6 100644 --- a/dist/client.d.ts.map +++ b/dist/client.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.js"],"names":[],"mappings":"AA+CA;;;;GAIG;AACH;IAqBI;;;OAGG;IACH,qBAFW,MAAM,EAehB;IAED;;;;OAIG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAuBzB;IAED;;;;;;;OAOG;IACH,6BANW,MAAM,eACN,MAAM,mBACN,MAAM,GACJ,OAAO,CAAC,IAAI,CAAC,CAmCzB;IAmDD;;;;OAIG;IACH,wBAHa,OAAO,CAAC,iBAAiB,CAAC,CAKtC;IAED;;;;;;OAMG;IACH,sCALW,MAAM,iBACN,MAAM,GAAG,IAAI,GACX,OAAO,CAAC,kBAAkB,CAAC,CASvC;IAED;;;;;;;OAOG;IACH,iCANW,MAAM,gBACN,MAAM,GAAG,IAAI,YACb,MAAM,GACJ,OAAO,CAAC,kBAAkB,CAAC,CAUvC;IAED;;;;;;OAMG;IACH,qBALW,MAAM,iBACN,MAAM,GAAG,IAAI,GACX,OAAO,CAAC,kBAAkB,CAAC,CAQvC;IAmJD;;;;OAIG;IACH,sCAHW,kBAAkB,gBAClB,MAAM,iBAsBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CAyClC;IAED;;;OAGG;IACH,cASC;;CA+EJ;;cAtiBa,MAAM;oBACN,MAAM;;;;;;;;kCAjBc,eAAe;+BAIlB,+BAA+B"} \ No newline at end of file +{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.js"],"names":[],"mappings":"AA6GA;;;;GAIG;AACH;IA+BI;;;;OAIG;IACH,oBAHW,aAAa,WACb,MAAM,EAehB;IAED;;;;;OAKG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAkEzB;IAED;;;;OAIG;IACH,wBAHa,OAAO,CAAC,iBAAiB,CAAC,CAKtC;IAED;;;;;;OAMG;IACH,sCALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAgBvB;IAED;;;;;;;OAOG;IACH,iCANW,MAAM,qBACN,iBAAiB,gBACjB,MAAM,GAAG,IAAI,YACb,MAAM,QAkBhB;IAED;;;;;;OAMG;IACH,qBALW,MAAM,qBACN,iBAAiB,iBACjB,MAAM,GAAG,IAAI,QAWvB;IAgND;;;;OAIG;IACH,mCAHW,MAAM,gBACN,MAAM,QAqBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CA4ClC;IAED;;;OAGG;IACH,cAMC;;CAkFJ;;cAtoBa,MAAM;oBACN,MAAM;;+CAMT,gBAAgB,gBAChB,qBAAqB;;UAOlB,gBAAgB;;uBAEhB,iBAAiB;;;eAMjB,MAAM;yBACN,MAAM;wBACN,MAAM;kBACN,MAAM;;;cAMN,QAAQ;oBACR,MAAM;cACN,MAAM;cACN,MAAM;cACN,MAAM;eACN,MAAM;cACN,MAAM;kBACN,MAAM;gBACN,MAAM;iBACN,MAAM;iBACN,MAAM;oBACN,MAAM;;;;;;;;;eAeN,MAAM;kBACN,MAAM;mBACN,MAAM;eACN,MAAM;;kCA1Fc,eAAe;8BAKT,0BAA0B;;;;6BAWxD,MAAM;;;;;;;;;yBAXwB,0BAA0B"} diff --git a/dist/client.js b/dist/client.js index 6ef7da7..61fdf0a 100644 --- a/dist/client.js +++ b/dist/client.js @@ -1,6 +1,6 @@ // src/client.js import crypto2 from "crypto"; -import fs2 from "fs"; +import fs from "fs"; import { fileURLToPath } from "url"; import avro3 from "avro-js"; import certifi from "certifi"; @@ -82,73 +82,6 @@ var EventParseError = class extends Error { } }; -// src/utils/pubSubEventEmitter.js -import { EventEmitter } from "events"; -var PubSubEventEmitter = class extends EventEmitter { - #topicName; - #requestedEventCount; - #receivedEventCount; - #latestReplayId; - /** - * Create a new EventEmitter for Pub/Sub API events - * @param {string} topicName - * @param {number} requestedEventCount - * @protected - */ - constructor(topicName, requestedEventCount) { - super(); - this.#topicName = topicName; - this.#requestedEventCount = requestedEventCount; - this.#receivedEventCount = 0; - this.#latestReplayId = null; - } - emit(eventName, args) { - if (eventName === "data") { - this.#receivedEventCount++; - this.#latestReplayId = args.replayId; - } - return super.emit(eventName, args); - } - /** - * Returns the number of events that were requested when subscribing. - * @returns {number} the number of events that were requested - */ - getRequestedEventCount() { - return this.#requestedEventCount; - } - /** - * Returns the number of events that were received since subscribing. - * @returns {number} the number of events that were received - */ - getReceivedEventCount() { - return this.#receivedEventCount; - } - /** - * Returns the topic name for this subscription. - * @returns {string} the topic name - */ - getTopicName() { - return this.#topicName; - } - /** - * Returns the replay ID of the last processed event or null if no event was processed yet. - * @return {number} replay ID - */ - getLatestReplayId() { - return this.#latestReplayId; - } - /** - * @protected - * Resets the requested/received event counts. - * This method should only be be used internally by the client when it resubscribes. - * @param {number} newRequestedEventCount - */ - _resetEventCount(newRequestedEventCount) { - this.#requestedEventCount = newRequestedEventCount; - this.#receivedEventCount = 0; - } -}; - // src/utils/avroHelper.js import avro from "avro-js"; var CustomLongAvroType = avro.types.LongType.using({ @@ -180,95 +113,94 @@ var CustomLongAvroType = avro.types.LongType.using({ }); // src/utils/configuration.js -import * as dotenv from "dotenv"; -import fs from "fs"; -var AUTH_USER_SUPPLIED = "user-supplied"; -var AUTH_USERNAME_PASSWORD = "username-password"; -var AUTH_OAUTH_CLIENT_CREDENTIALS = "oauth-client-credentials"; -var AUTH_OAUTH_JWT_BEARER = "oauth-jwt-bearer"; +var DEFAULT_PUB_SUB_ENDPOINT = "api.pubsub.salesforce.com:7443"; +var AuthType = { + USER_SUPPLIED: "user-supplied", + USERNAME_PASSWORD: "username-password", + OAUTH_CLIENT_CREDENTIALS: "oauth-client-credentials", + OAUTH_JWT_BEARER: "oauth-jwt-bearer" +}; var Configuration = class _Configuration { - static load() { - dotenv.config(); - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_AUTH_TYPE", - "PUB_SUB_ENDPOINT" + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} the sanitized client configuration + */ + static load(config) { + config.pubSubEndpoint = config.pubSubEndpoint ?? DEFAULT_PUB_SUB_ENDPOINT; + _Configuration.#checkMandatoryVariables(config, ["authType"]); + switch (config.authType) { + case AuthType.USER_SUPPLIED: + config = _Configuration.#loadUserSuppliedAuth(config); + break; + case AuthType.USERNAME_PASSWORD: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "username", + "password" + ]); + config.userToken = config.userToken ?? ""; + break; + case AuthType.OAUTH_CLIENT_CREDENTIALS: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "clientId", + "clientSecret" + ]); + break; + case AuthType.OAUTH_JWT_BEARER: + _Configuration.#checkMandatoryVariables(config, [ + "loginUrl", + "clientId", + "username", + "privateKey" + ]); + break; + default: + throw new Error( + `Unsupported authType value: ${config.authType}` + ); + } + return config; + } + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} sanitized configuration + */ + static #loadUserSuppliedAuth(config) { + _Configuration.#checkMandatoryVariables(config, [ + "accessToken", + "instanceUrl" ]); - if (_Configuration.isUsernamePasswordAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_USERNAME", - "SALESFORCE_PASSWORD" - ]); - } else if (_Configuration.isOAuthClientCredentialsAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_CLIENT_ID", - "SALESFORCE_CLIENT_SECRET" - ]); - } else if (_Configuration.isOAuthJwtBearerAuth()) { - _Configuration.#checkMandatoryVariables([ - "SALESFORCE_LOGIN_URL", - "SALESFORCE_CLIENT_ID", - "SALESFORCE_USERNAME", - "SALESFORCE_PRIVATE_KEY_FILE" - ]); - _Configuration.getSfPrivateKey(); - } else if (!_Configuration.isUserSuppliedAuth()) { + if (!config.instanceUrl.startsWith("https://")) { throw new Error( - `Invalid value for SALESFORCE_AUTH_TYPE environment variable: ${_Configuration.getAuthType()}` + `Invalid Salesforce Instance URL format supplied: ${config.instanceUrl}` ); } - } - static getAuthType() { - return process.env.SALESFORCE_AUTH_TYPE; - } - static getSfLoginUrl() { - return process.env.SALESFORCE_LOGIN_URL; - } - static getSfUsername() { - return process.env.SALESFORCE_USERNAME; - } - static getSfSecuredPassword() { - if (process.env.SALESFORCE_TOKEN) { - return process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_TOKEN; + if (!config.organizationId) { + try { + config.organizationId = config.accessToken.split("!").at(0); + } catch (error) { + throw new Error( + "Unable to parse organizationId from access token", + { + cause: error + } + ); + } } - return process.env.SALESFORCE_PASSWORD; - } - static getSfClientId() { - return process.env.SALESFORCE_CLIENT_ID; - } - static getSfClientSecret() { - return process.env.SALESFORCE_CLIENT_SECRET; - } - static getSfPrivateKey() { - try { - const keyPath = process.env.SALESFORCE_PRIVATE_KEY_FILE; - return fs.readFileSync(keyPath, "utf8"); - } catch (error) { - throw new Error("Failed to load private key file", { - cause: error - }); + if (config.organizationId.length !== 15 && config.organizationId.length !== 18) { + throw new Error( + `Invalid Salesforce Org ID format supplied: ${config.organizationId}` + ); } + return config; } - static getPubSubEndpoint() { - return process.env.PUB_SUB_ENDPOINT; - } - static isUserSuppliedAuth() { - return _Configuration.getAuthType() === AUTH_USER_SUPPLIED; - } - static isUsernamePasswordAuth() { - return _Configuration.getAuthType() === AUTH_USERNAME_PASSWORD; - } - static isOAuthClientCredentialsAuth() { - return _Configuration.getAuthType() === AUTH_OAUTH_CLIENT_CREDENTIALS; - } - static isOAuthJwtBearerAuth() { - return _Configuration.getAuthType() === AUTH_OAUTH_JWT_BEARER; - } - static #checkMandatoryVariables(varNames) { + static #checkMandatoryVariables(config, varNames) { varNames.forEach((varName) => { - if (!process.env[varName]) { - throw new Error(`Missing ${varName} environment variable`); + if (!config[varName]) { + throw new Error( + `Missing value for ${varName} mandatory configuration key` + ); } }); } @@ -383,6 +315,15 @@ function encodeReplayId(replayId) { buf.writeBigUInt64BE(BigInt(replayId), 0); return buf; } +function toJsonString(event) { + return JSON.stringify( + event, + (key, value) => ( + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === "bigint" ? value.toString() : value + ) + ); +} function hexToBin(hex) { let bin = hex.substring(2); bin = bin.replaceAll("0", "0000"); @@ -408,88 +349,113 @@ function hexToBin(hex) { import crypto from "crypto"; import jsforce from "jsforce"; import { fetch } from "undici"; -var SalesforceAuth = class _SalesforceAuth { +var SalesforceAuth = class { + /** + * Client configuration + * @type {Configuration} + */ + #config; + /** + * Logger + * @type {Logger} + */ + #logger; + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + * @param {Logger} logger a logger + */ + constructor(config, logger) { + this.#config = config; + this.#logger = logger; + } /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ - static async authenticate() { - if (Configuration.isUsernamePasswordAuth()) { - return _SalesforceAuth.#authWithUsernamePassword(); - } else if (Configuration.isOAuthClientCredentialsAuth()) { - return _SalesforceAuth.#authWithOAuthClientCredentials(); - } else if (Configuration.isOAuthJwtBearerAuth()) { - return _SalesforceAuth.#authWithJwtBearer(); - } else { - throw new Error("Unsupported authentication mode."); + async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + throw new Error( + "Authenticate method should not be called in user-supplied mode." + ); + case AuthType.USERNAME_PASSWORD: + return this.#authWithUsernamePassword(); + case AuthType.OAUTH_CLIENT_CREDENTIALS: + return this.#authWithOAuthClientCredentials(); + case AuthType.OAUTH_JWT_BEARER: + return this.#authWithJwtBearer(); + default: + throw new Error( + `Unsupported authType value: ${this.#config.authType}` + ); } } /** * Authenticates with the username/password flow * @returns {ConnectionMetadata} */ - static async #authWithUsernamePassword() { + async #authWithUsernamePassword() { + const { loginUrl, username, password, userToken } = this.#config; const sfConnection = new jsforce.Connection({ - loginUrl: Configuration.getSfLoginUrl() + loginUrl }); - await sfConnection.login( - Configuration.getSfUsername(), - Configuration.getSfSecuredPassword() - ); + await sfConnection.login(username, `${password}${userToken}`); return { accessToken: sfConnection.accessToken, instanceUrl: sfConnection.instanceUrl, organizationId: sfConnection.userInfo.organizationId, - username: Configuration.getSfUsername() + username }; } /** * Authenticates with the OAuth 2.0 client credentials flow * @returns {ConnectionMetadata} */ - static async #authWithOAuthClientCredentials() { + async #authWithOAuthClientCredentials() { + const { clientId, clientSecret } = this.#config; const params = new URLSearchParams(); params.append("grant_type", "client_credentials"); - params.append("client_id", Configuration.getSfClientId()); - params.append("client_secret", Configuration.getSfClientSecret()); - return _SalesforceAuth.#authWithOAuth(params.toString()); + params.append("client_id", clientId); + params.append("client_secret", clientSecret); + return this.#authWithOAuth(params.toString()); } /** * Authenticates with the OAuth 2.0 JWT bearer flow * @returns {ConnectionMetadata} */ - static async #authWithJwtBearer() { + async #authWithJwtBearer() { + const { clientId, username, loginUrl, privateKey } = this.#config; const header = JSON.stringify({ alg: "RS256" }); const claims = JSON.stringify({ - iss: Configuration.getSfClientId(), - sub: Configuration.getSfUsername(), - aud: Configuration.getSfLoginUrl(), + iss: clientId, + sub: username, + aud: loginUrl, exp: Math.floor(Date.now() / 1e3) + 60 * 5 }); let token = `${base64url(header)}.${base64url(claims)}`; const sign = crypto.createSign("RSA-SHA256"); sign.update(token); sign.end(); - token += `.${base64url(sign.sign(Configuration.getSfPrivateKey()))}`; + token += `.${base64url(sign.sign(privateKey))}`; const body = `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${token}`; - return _SalesforceAuth.#authWithOAuth(body); + return this.#authWithOAuth(body); } /** * Generic OAuth 2.0 connect method * @param {string} body URL encoded body * @returns {ConnectionMetadata} connection metadata */ - static async #authWithOAuth(body) { - const loginResponse = await fetch( - `${Configuration.getSfLoginUrl()}/services/oauth2/token`, - { - method: "post", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - body - } - ); + async #authWithOAuth(body) { + const { loginUrl } = this.#config; + const loginResponse = await fetch(`${loginUrl}/services/oauth2/token`, { + method: "post", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body + }); if (loginResponse.status !== 200) { throw new Error( `Authentication error: HTTP ${loginResponse.status} - ${await loginResponse.text()}` @@ -497,7 +463,7 @@ var SalesforceAuth = class _SalesforceAuth { } const { access_token, instance_url } = await loginResponse.json(); const userInfoResponse = await fetch( - `${Configuration.getSfLoginUrl()}/services/oauth2/userinfo`, + `${loginUrl}/services/oauth2/userinfo`, { headers: { authorization: `Bearer ${access_token}` } } @@ -522,8 +488,21 @@ function base64url(input) { } // src/client.js +var SubscribeCallbackType = { + EVENT: "event", + LAST_EVENT: "lastEvent", + ERROR: "error", + END: "end", + GRPC_STATUS: "grpcStatus", + GRPC_KEEP_ALIVE: "grpcKeepAlive" +}; var MAX_EVENT_BATCH_SIZE = 100; var PubSubApiClient = class { + /** + * Client configuration + * @type {Configuration} + */ + #config; /** * gRPC client * @type {Object} @@ -535,21 +514,26 @@ var PubSubApiClient = class { */ #schemaChache; /** - * Map of subscribitions indexed by topic name - * @type {Map} + * Map of subscriptions indexed by topic name + * @type {Map} */ #subscriptions; + /** + * Logger + * @type {Logger} + */ #logger; /** * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration * @param {Logger} [logger] an optional custom logger. The client uses the console if no value is supplied. */ - constructor(logger = console) { + constructor(config, logger = console) { this.#logger = logger; this.#schemaChache = new SchemaCache(); this.#subscriptions = /* @__PURE__ */ new Map(); try { - Configuration.load(); + this.#config = Configuration.load(config); } catch (error) { this.#logger.error(error); throw new Error("Failed to initialize Pub/Sub API client", { @@ -558,86 +542,43 @@ var PubSubApiClient = class { } } /** - * Authenticates with Salesforce then, connects to the Pub/Sub API. + * Authenticates with Salesforce (if not using user-supplied authentication mode) then, + * connects to the Pub/Sub API. * @returns {Promise} Promise that resolves once the connection is established * @memberof PubSubApiClient.prototype */ async connect() { - if (Configuration.isUserSuppliedAuth()) { - throw new Error( - 'You selected user-supplied authentication mode so you cannot use the "connect()" method. Use "connectWithAuth(...)" instead.' - ); - } - let conMetadata; - try { - conMetadata = await SalesforceAuth.authenticate(); - this.#logger.info( - `Connected to Salesforce org ${conMetadata.instanceUrl} as ${conMetadata.username}` - ); - } catch (error) { - throw new Error("Failed to authenticate with Salesforce", { - cause: error - }); - } - return this.#connectToPubSubApi(conMetadata); - } - /** - * Connects to the Pub/Sub API with user-supplied authentication. - * @param {string} accessToken Salesforce access token - * @param {string} instanceUrl Salesforce instance URL - * @param {string} [organizationId] optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. - * @returns {Promise} Promise that resolves once the connection is established - * @memberof PubSubApiClient.prototype - */ - async connectWithAuth(accessToken, instanceUrl, organizationId) { - if (!instanceUrl || !instanceUrl.startsWith("https://")) { - throw new Error( - `Invalid Salesforce Instance URL format supplied: ${instanceUrl}` - ); - } - let validOrganizationId = organizationId; - if (!organizationId) { + if (this.#config.authType !== AuthType.USER_SUPPLIED) { try { - validOrganizationId = accessToken.split("!").at(0); - } catch (error) { - throw new Error( - "Unable to parse organizationId from given access token", - { - cause: error - } + const auth = new SalesforceAuth(this.#config, this.#logger); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#config.organizationId = conMetadata.organizationId; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); + } catch (error) { + throw new Error("Failed to authenticate with Salesforce", { + cause: error + }); } } - if (validOrganizationId.length !== 15 && validOrganizationId.length !== 18) { - throw new Error( - `Invalid Salesforce Org ID format supplied: ${validOrganizationId}` - ); - } - return this.#connectToPubSubApi({ - accessToken, - instanceUrl, - organizationId: validOrganizationId - }); - } - /** - * Connects to the Pub/Sub API. - * @param {import('./auth.js').ConnectionMetadata} conMetadata - * @returns {Promise} Promise that resolves once the connection is established - */ - async #connectToPubSubApi(conMetadata) { try { - const rootCert = fs2.readFileSync(certifi); + this.#logger.debug(`Connecting to Pub/Sub API`); + const rootCert = fs.readFileSync(certifi); const protoFilePath = fileURLToPath( - new URL("./pubsub_api-be352429.proto?hash=be352429", import.meta.url) + new URL("./pubsub_api-07e1f84a.proto?hash=07e1f84a", import.meta.url) ); const packageDef = protoLoader.loadSync(protoFilePath, {}); const grpcObj = grpc.loadPackageDefinition(packageDef); const sfdcPackage = grpcObj.eventbus.v1; const metaCallback = (_params, callback) => { const meta = new grpc.Metadata(); - meta.add("accesstoken", conMetadata.accessToken); - meta.add("instanceurl", conMetadata.instanceUrl); - meta.add("tenantid", conMetadata.organizationId); + meta.add("accesstoken", this.#config.accessToken); + meta.add("instanceurl", this.#config.instanceUrl); + meta.add("tenantid", this.#config.organizationId); callback(null, meta); }; const callCreds = grpc.credentials.createFromMetadataGenerator(metaCallback); @@ -646,11 +587,11 @@ var PubSubApiClient = class { callCreds ); this.#client = new sfdcPackage.PubSub( - Configuration.getPubSubEndpoint(), + this.#config.pubSubEndpoint, combCreds ); this.#logger.info( - `Connected to Pub/Sub API endpoint ${Configuration.getPubSubEndpoint()}` + `Connected to Pub/Sub API endpoint ${this.#config.pubSubEndpoint}` ); } catch (error) { throw new Error("Failed to connect to Pub/Sub API", { @@ -669,52 +610,64 @@ var PubSubApiClient = class { /** * Subscribes to a topic and retrieves all past events in retention window. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromEarliestEvent(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 1 - }); + subscribeFromEarliestEvent(topicName, subscribeCallback, numRequested = null) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 1 + }, + subscribeCallback + ); } /** * Subscribes to a topic and retrieves past events starting from a replay ID. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} numRequested number of events requested. If null, the client keeps the subscription alive forever. * @param {number} replayId replay ID - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromReplayId(topicName, numRequested, replayId) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 2, - replayId: encodeReplayId(replayId) - }); + subscribeFromReplayId(topicName, subscribeCallback, numRequested, replayId) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 2, + replayId: encodeReplayId(replayId) + }, + subscribeCallback + ); } /** * Subscribes to a topic. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribe(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested - }); + subscribe(topicName, subscribeCallback, numRequested = null) { + this.#subscribe( + { + topicName, + numRequested + }, + subscribeCallback + ); } /** * Subscribes to a topic using the gRPC client and an event schema - * @param {object} subscribeRequest subscription request - * @return {PubSubEventEmitter} emitter that allows you to listen to received events and stream lifecycle events + * @param {SubscribeRequest} subscribeRequest subscription request + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ - async #subscribe(subscribeRequest) { + #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -736,38 +689,72 @@ var PubSubApiClient = class { this.#logger.warn( `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` ); + subscribeRequest.numRequested = MAX_EVENT_BATCH_SIZE; } } if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); } let subscription = this.#subscriptions.get(topicName); - if (!subscription) { - subscription = this.#client.Subscribe(); + let grpcSubscription; + if (subscription) { + this.#logger.debug( + `${topicName} - Reusing cached gRPC subscription` + ); + grpcSubscription = subscription.grpcSubscription; + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = subscribeRequest.numRequested; + } else { + this.#logger.debug( + `${topicName} - Establishing new gRPC subscription` + ); + grpcSubscription = this.#client.Subscribe(); + subscription = { + info: { + topicName, + requestedEventCount: subscribeRequest.numRequested, + receivedEventCount: 0, + lastReplayId: null + }, + grpcSubscription, + subscribeCallback + }; this.#subscriptions.set(topicName, subscription); } - subscription.write(subscribeRequest); - this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` - ); - const eventEmitter = new PubSubEventEmitter( - topicName, - numRequested - ); - subscription.on("data", async (data) => { + grpcSubscription.on("data", async (data) => { const latestReplayId = decodeReplayId(data.latestReplayId); + subscription.info.lastReplayId = latestReplayId; if (data.events) { this.#logger.info( - `Received ${data.events.length} events, latest replay ID: ${latestReplayId}` + `${topicName} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` ); for (const event of data.events) { try { + this.#logger.debug( + `${topicName} - Raw event: ${toJsonString(event)}` + ); + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${event.event.schemaId}` + ); const schema = await this.#getEventSchemaFromId( event.event.schemaId ); + const subscription2 = this.#subscriptions.get(topicName); + if (!subscription2) { + throw new Error( + `Failed to retrieve subscription for topic ${topicName}.` + ); + } + subscription2.info.receivedEventCount++; const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); - eventEmitter.emit("data", parsedEvent); + this.#logger.debug( + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` + ); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { let replayId; try { @@ -782,46 +769,67 @@ var PubSubApiClient = class { event, latestReplayId ); - eventEmitter.emit("error", parseError); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + parseError + ); this.#logger.error(parseError); } - if (eventEmitter.getReceivedEventCount() === eventEmitter.getRequestedEventCount()) { + if (subscription.info.receivedEventCount === subscription.info.requestedEventCount) { if (isInfiniteEventRequest) { this.requestAdditionalEvents( - eventEmitter, - MAX_EVENT_BATCH_SIZE + subscription.info.topicName, + subscription.info.requestedEventCount ); } else { - eventEmitter.emit("lastevent"); + subscribeCallback( + subscription.info, + SubscribeCallbackType.LAST_EVENT + ); } } } } else { this.#logger.debug( - `Received keepalive message. Latest replay ID: ${latestReplayId}` + `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` ); data.latestReplayId = latestReplayId; - eventEmitter.emit("keepalive", data); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_KEEP_ALIVE + ); } }); - subscription.on("end", () => { + grpcSubscription.on("end", () => { this.#subscriptions.delete(topicName); - this.#logger.info("gRPC stream ended"); - eventEmitter.emit("end"); + this.#logger.info(`${topicName} - gRPC stream ended`); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on("error", (error) => { + grpcSubscription.on("error", (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error ); - eventEmitter.emit("error", error); }); - subscription.on("status", (status) => { + grpcSubscription.on("status", (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status ); - eventEmitter.emit("status", status); }); - return eventEmitter; + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `${topicName} - Subscribe request sent for ${numRequested} events` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -831,24 +839,24 @@ var PubSubApiClient = class { } /** * Request additional events on an existing subscription. - * @param {PubSubEventEmitter} eventEmitter event emitter that was obtained in the first subscribe call + * @param {string} topicName topic name * @param {number} numRequested number of events requested. */ - async requestAdditionalEvents(eventEmitter, numRequested) { - const topicName = eventEmitter.getTopicName(); + requestAdditionalEvents(topicName, numRequested) { const subscription = this.#subscriptions.get(topicName); if (!subscription) { throw new Error( `Failed to request additional events for topic ${topicName}, no active subscription found.` ); } - eventEmitter._resetEventCount(numRequested); + subscription.receivedEventCount = 0; + subscription.requestedEventCount = numRequested; subscription.write({ topicName, numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } /** @@ -861,6 +869,9 @@ var PubSubApiClient = class { */ async publish(topicName, payload, correlationKey) { try { + this.#logger.debug( + `${topicName} - Preparing to publish event: ${toJsonString(payload)}` + ); if (!this.#client) { throw new Error("Pub/Sub API client is not connected."); } @@ -903,9 +914,6 @@ var PubSubApiClient = class { */ close() { this.#logger.info("Clear subscriptions"); - this.#subscriptions.forEach((subscription) => { - subscription.removeAllListeners(); - }); this.#subscriptions.clear(); this.#logger.info("Closing gRPC stream"); this.#client.close(); @@ -944,13 +952,15 @@ var PubSubApiClient = class { reject(topicError); } else { const { schemaId } = response; + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${schemaId}` + ); let schema = this.#schemaChache.getFromId(schemaId); if (!schema) { schema = await this.#fetchEventSchemaFromIdWithClient( schemaId ); } - this.#logger.info(`Topic schema loaded: ${topicName}`); this.#schemaChache.set(schema); resolve(schema); } diff --git a/dist/pubsub_api-be352429.proto b/dist/pubsub_api-07e1f84a.proto similarity index 87% rename from dist/pubsub_api-be352429.proto rename to dist/pubsub_api-07e1f84a.proto index 0c9ec82..0152e77 100644 --- a/dist/pubsub_api-be352429.proto +++ b/dist/pubsub_api-07e1f84a.proto @@ -223,10 +223,16 @@ string rpc_id = 3; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Request for the ManagedSubscribe streaming RPC method. This request is used to: + * 1. Establish the initial managed subscribe stream. + * 2. Request more events from the subscription stream. + * 3. Commit a Replay ID using CommitReplayRequest. + */ message ManagedFetchRequest { /* * Managed subscription ID or developer name. This value corresponds to the @@ -251,10 +257,14 @@ } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Response for the ManagedSubscribe streaming RPC method. This can return + * ConsumerEvent(s) or CommitReplayResponse along with other metadata. + */ message ManagedFetchResponse { // Received events for subscription for client consumption repeated ConsumerEvent events = 1; @@ -268,10 +278,14 @@ CommitReplayResponse commit_response = 5; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Request to commit a Replay ID for the last processed event or for the latest + * replay ID received in an empty batch of events. + */ message CommitReplayRequest { // commit_request_id to identify commit responses string commit_request_id = 1; @@ -279,17 +293,25 @@ bytes replay_id = 2; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * There is no guaranteed 1:1 CommitReplayRequest to CommitReplayResponse. + * N CommitReplayRequest(s) can get compressed in a batch resulting in a single + * CommitReplayResponse which reflects the latest values of last + * CommitReplayRequest in that batch. + */ message CommitReplayResponse { - // commit_request_id to identify commit responses + // commit_request_id to identify commit responses. string commit_request_id = 1; // replayId that may have been committed bytes replay_id = 2; // for failed commits Error error = 3; + // time when server received request in epoch ms + int64 process_time = 4; } /* @@ -378,10 +400,12 @@ rpc PublishStream (stream PublishRequest) returns (stream PublishResponse); /* - * Same as Subscribe, but for Managed Subscribtions clients. - * EXPERIMENTAL: This feature is part of a closed alpha release. This - * API might be changed in backward-incompatible ways and is not recommended - * for production use. It is not subject to any SLA or deprecation policy. + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Same as Subscribe, but for Managed Subscription clients. + * This feature is part of an open beta release. */ rpc ManagedSubscribe (stream ManagedFetchRequest) returns (stream ManagedFetchResponse); } diff --git a/dist/utils/auth.d.ts b/dist/utils/auth.d.ts index ea75ebb..66f3ed0 100644 --- a/dist/utils/auth.d.ts +++ b/dist/utils/auth.d.ts @@ -7,31 +7,17 @@ */ export default class SalesforceAuth { /** - * Authenticates with the auth mode specified in configuration - * @returns {ConnectionMetadata} - */ - static authenticate(): ConnectionMetadata; - /** - * Authenticates with the username/password flow - * @returns {ConnectionMetadata} + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + * @param {Logger} logger a logger */ - static "__#4@#authWithUsernamePassword"(): ConnectionMetadata; + constructor(config: Configuration, logger: Logger); /** - * Authenticates with the OAuth 2.0 client credentials flow - * @returns {ConnectionMetadata} - */ - static "__#4@#authWithOAuthClientCredentials"(): ConnectionMetadata; - /** - * Authenticates with the OAuth 2.0 JWT bearer flow + * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ - static "__#4@#authWithJwtBearer"(): ConnectionMetadata; - /** - * Generic OAuth 2.0 connect method - * @param {string} body URL encoded body - * @returns {ConnectionMetadata} connection metadata - */ - static "__#4@#authWithOAuth"(body: string): ConnectionMetadata; + authenticate(): ConnectionMetadata; + #private; } export type ConnectionMetadata = { accessToken: string; diff --git a/dist/utils/auth.d.ts.map b/dist/utils/auth.d.ts.map index 1e0873f..c659315 100644 --- a/dist/utils/auth.d.ts.map +++ b/dist/utils/auth.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IACI;;;OAGG;IACH,uBAFa,kBAAkB,CAY9B;IAED;;;OAGG;IACH,2CAFa,kBAAkB,CAgB9B;IAED;;;OAGG;IACH,iDAFa,kBAAkB,CAQ9B;IAED;;;OAGG;IACH,oCAFa,kBAAkB,CAoB9B;IAED;;;;OAIG;IACH,mCAHW,MAAM,GACJ,kBAAkB,CA4C9B;CACJ;;iBA/Ha,MAAM;iBACN,MAAM;;;;qBACN,MAAM;;;;eACN,MAAM"} \ No newline at end of file +{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/utils/auth.js"],"names":[],"mappings":"AAKA;;;;;;GAMG;AAEH;IAaI;;;;OAIG;IACH,oBAHW,aAAa,UACb,MAAM,EAKhB;IAED;;;OAGG;IACH,gBAFa,kBAAkB,CAoB9B;;CAyGJ;;iBA5Ja,MAAM;iBACN,MAAM;;;;qBACN,MAAM;;;;eACN,MAAM"} \ No newline at end of file diff --git a/dist/utils/configuration.d.ts b/dist/utils/configuration.d.ts index e7193c0..73cc818 100644 --- a/dist/utils/configuration.d.ts +++ b/dist/utils/configuration.d.ts @@ -1,17 +1,24 @@ -export default class Configuration { - static load(): void; - static getAuthType(): string; - static getSfLoginUrl(): string; - static getSfUsername(): string; - static getSfSecuredPassword(): string; - static getSfClientId(): string; - static getSfClientSecret(): string; - static getSfPrivateKey(): any; - static getPubSubEndpoint(): string; - static isUserSuppliedAuth(): boolean; - static isUsernamePasswordAuth(): boolean; - static isOAuthClientCredentialsAuth(): boolean; - static isOAuthJwtBearerAuth(): boolean; - static "__#3@#checkMandatoryVariables"(varNames: any): void; +/** + * Enum for auth type values + */ +export type AuthType = string; +export namespace AuthType { + let USER_SUPPLIED: string; + let USERNAME_PASSWORD: string; + let OAUTH_CLIENT_CREDENTIALS: string; + let OAUTH_JWT_BEARER: string; +} +export class Configuration { + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} the sanitized client configuration + */ + static load(config: Configuration): Configuration; + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} sanitized configuration + */ + static "__#2@#loadUserSuppliedAuth"(config: Configuration): Configuration; + static "__#2@#checkMandatoryVariables"(config: any, varNames: any): void; } //# sourceMappingURL=configuration.d.ts.map \ No newline at end of file diff --git a/dist/utils/configuration.d.ts.map b/dist/utils/configuration.d.ts.map index 5b2d61f..593c0cc 100644 --- a/dist/utils/configuration.d.ts.map +++ b/dist/utils/configuration.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../src/utils/configuration.js"],"names":[],"mappings":"AAQA;IACI,oBAkCC;IAED,6BAEC;IAED,+BAEC;IAED,+BAEC;IAED,sCAOC;IAED,+BAEC;IAED,mCAEC;IAED,8BASC;IAED,mCAEC;IAED,qCAEC;IAED,yCAEC;IAED,+CAEC;IAED,uCAEC;IAED,4DAMC;CACJ"} \ No newline at end of file +{"version":3,"file":"configuration.d.ts","sourceRoot":"","sources":["../../src/utils/configuration.js"],"names":[],"mappings":";;;uBAIU,MAAM;;;;;;;AAShB;IACI;;;OAGG;IACH,oBAHW,aAAa,GACX,aAAa,CAyCzB;IAED;;;OAGG;IACH,4CAHW,aAAa,GACX,aAAa,CAoCzB;IAED,yEAQC;CACJ"} \ No newline at end of file diff --git a/dist/utils/eventParser.d.ts b/dist/utils/eventParser.d.ts index d759ccf..4d8dbf8 100644 --- a/dist/utils/eventParser.d.ts +++ b/dist/utils/eventParser.d.ts @@ -20,4 +20,11 @@ export function decodeReplayId(encodedReplayId: Buffer): number; * @protected */ export function encodeReplayId(replayId: number): Buffer; +/** + * Safely serializes an event into a JSON string + * @param {any} event the event object + * @returns {string} a string holding the JSON respresentation of the event + * @protected + */ +export function toJsonString(event: any): string; //# sourceMappingURL=eventParser.d.ts.map \ No newline at end of file diff --git a/dist/utils/eventParser.d.ts.map b/dist/utils/eventParser.d.ts.map index 6c348b2..73739dd 100644 --- a/dist/utils/eventParser.d.ts.map +++ b/dist/utils/eventParser.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"eventParser.d.ts","sourceRoot":"","sources":["../../src/utils/eventParser.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCALW,GAAC,SACD,GAAC,GACC,GAAC,CAyCb;AA0GD;;;;;GAKG;AACH,gDAJW,MAAM,GACJ,MAAM,CAKlB;AAED;;;;;GAKG;AACH,yCAJW,MAAM,GACJ,MAAM,CAOlB"} \ No newline at end of file +{"version":3,"file":"eventParser.d.ts","sourceRoot":"","sources":["../../src/utils/eventParser.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,mCALW,GAAC,SACD,GAAC,GACC,GAAC,CAyCb;AA0GD;;;;;GAKG;AACH,gDAJW,MAAM,GACJ,MAAM,CAKlB;AAED;;;;;GAKG;AACH,yCAJW,MAAM,GACJ,MAAM,CAOlB;AAED;;;;;GAKG;AACH,oCAJW,GAAG,GACD,MAAM,CAQlB"} \ No newline at end of file diff --git a/dist/utils/pubSubEventEmitter.d.ts b/dist/utils/pubSubEventEmitter.d.ts deleted file mode 100644 index 859fb33..0000000 --- a/dist/utils/pubSubEventEmitter.d.ts +++ /dev/null @@ -1,46 +0,0 @@ -/** - * EventEmitter wrapper for processing incoming Pub/Sub API events - * while keeping track of the topic name and the volume of events requested/received. - * @alias PubSubEventEmitter - * @global - */ -export default class PubSubEventEmitter extends EventEmitter<[never]> { - /** - * Create a new EventEmitter for Pub/Sub API events - * @param {string} topicName - * @param {number} requestedEventCount - * @protected - */ - protected constructor(); - emit(eventName: any, args: any): boolean; - /** - * Returns the number of events that were requested when subscribing. - * @returns {number} the number of events that were requested - */ - getRequestedEventCount(): number; - /** - * Returns the number of events that were received since subscribing. - * @returns {number} the number of events that were received - */ - getReceivedEventCount(): number; - /** - * Returns the topic name for this subscription. - * @returns {string} the topic name - */ - getTopicName(): string; - /** - * Returns the replay ID of the last processed event or null if no event was processed yet. - * @return {number} replay ID - */ - getLatestReplayId(): number; - /** - * @protected - * Resets the requested/received event counts. - * This method should only be be used internally by the client when it resubscribes. - * @param {number} newRequestedEventCount - */ - protected _resetEventCount(newRequestedEventCount: number): void; - #private; -} -import { EventEmitter } from 'events'; -//# sourceMappingURL=pubSubEventEmitter.d.ts.map \ No newline at end of file diff --git a/dist/utils/pubSubEventEmitter.d.ts.map b/dist/utils/pubSubEventEmitter.d.ts.map deleted file mode 100644 index 322cd2c..0000000 --- a/dist/utils/pubSubEventEmitter.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"pubSubEventEmitter.d.ts","sourceRoot":"","sources":["../../src/utils/pubSubEventEmitter.js"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH;IAMI;;;;;OAKG;IACH,wBAMC;IAED,yCAOC;IAED;;;OAGG;IACH,0BAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,yBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,gBAFa,MAAM,CAIlB;IAED;;;OAGG;IACH,qBAFY,MAAM,CAIjB;IAED;;;;;OAKG;IACH,mDAFW,MAAM,QAKhB;;CACJ;6BA/E4B,QAAQ"} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index a7edcf7..775d7c6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,14 +1,23 @@ import js from '@eslint/js'; +import jasmine from 'eslint-plugin-jasmine'; import globals from 'globals'; export default [ js.configs.recommended, + jasmine.configs.recommended, { languageOptions: { ecmaVersion: 13, globals: { - ...globals.node + ...globals.node, + ...globals.jasmine } + }, + plugins: { + jasmine + }, + rules: { + 'jasmine/new-line-before-expect': 'off' } } ]; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8c64719 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4187 @@ +{ + "name": "salesforce-pubsub-api-client", + "version": "5.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "salesforce-pubsub-api-client", + "version": "5.0.0", + "license": "CC0-1.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.2", + "@grpc/proto-loader": "^0.7.13", + "avro-js": "^1.12.0", + "certifi": "^14.5.15", + "jsforce": "^3.5.2", + "undici": "^6.20.0" + }, + "devDependencies": { + "@chialab/esbuild-plugin-meta-url": "^0.18.2", + "dotenv": "^16.4.5", + "eslint": "^9.12.0", + "eslint-plugin-jasmine": "^4.2.2", + "husky": "^9.1.6", + "jasmine": "^5.4.0", + "lint-staged": "^15.2.10", + "prettier": "^3.3.3", + "tsup": "^8.3.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.25.0.tgz", + "integrity": "sha512-BOehWE7MgQ8W8Qn0CQnMtg2tHPHPulcS/5AVpFvs2KCK1ET+0WqZqPvnpRpFN81gYoFopdIEJX9Sgjw3ZBccPg==", + "dependencies": { + "core-js-pure": "^3.30.2", + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chialab/esbuild-plugin-meta-url": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-meta-url/-/esbuild-plugin-meta-url-0.18.2.tgz", + "integrity": "sha512-uIRIdLvYnw5mLrTRXY0BTgeZx6ANL2/OHkWFl8FaiTYNb7cyXmwEDRE1mh6kBXPRPtGuqv6XSpNX+koEkElu4g==", + "dev": true, + "dependencies": { + "@chialab/esbuild-rna": "^0.18.1", + "@chialab/estransform": "^0.18.1", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-rna": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", + "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", + "dev": true, + "dependencies": { + "@chialab/estransform": "^0.18.0", + "@chialab/node-resolve": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/estransform": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", + "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", + "dev": true, + "dependencies": { + "@parcel/source-map": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/node-resolve": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", + "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.0.tgz", + "integrity": "sha512-YLntie/IdS31H54Ogdn+v50NuoWF5BDkEUFpiOChVa9UnKpftgwzZRrI4J132ETIi+D8n6xh9IviFV3eXdxfow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.2.tgz", + "integrity": "sha512-bgxdZmgTrJZX50OjyVwz3+mNEnCTNkh3cIqGPWVNeW9jX6bn1ZkU80uPd+67/ZpIJIjRQ9qaHCjhavyoWYxumg==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.5.tgz", + "integrity": "sha512-250ZGg4ipTL0TGvLlfACkIxS9+KLtIbn7BCZjsZj88zSg2Lvu3Xdw6dhAhfe/FjjXPVNCtcSp+WZjVsD3a/Zlw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.12.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz", + "integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", + "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/avro-js": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/avro-js/-/avro-js-1.12.0.tgz", + "integrity": "sha512-mBhOjtHHua2MHrrgQ71YKKTGfZpS1sPvgL+QcCQ5SkUyp6qLkeTsCnQXUmATfpiOvoXB6CczzFEqn5UKbPUn3Q==", + "dependencies": { + "underscore": "^1.13.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64url": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", + "integrity": "sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bundle-require": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.0.0.tgz", + "integrity": "sha512-GuziW3fSSmopcx4KRymQEJVbZUfqlCqcq7dvs6TYwKRZiegK/2buMxQTPs6MGlNv50wms1699qYO54R8XfRX4w==", + "dev": true, + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/certifi": { + "version": "14.5.15", + "resolved": "https://registry.npmjs.org/certifi/-/certifi-14.5.15.tgz", + "integrity": "sha512-NeLXuKCqSzwQNjpJ+WaSp5m8ntdTKJ8HnBu+eA7DxHfgzU7F1sjwrJFang+4U38+vmWbiFUpPZMV3uwwnHAisQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/core-js": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz", + "integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-pure": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.37.1.tgz", + "integrity": "sha512-J/r5JTHSmzTxbiYYrzXg9w1VpqrYt+gexenBE9pugeyhwPZTAEJddyiReJWsLO6uNQ8xJZFbod6XC7KKwatCiA==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csprng": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/csprng/-/csprng-0.1.2.tgz", + "integrity": "sha512-D3WAbvvgUVIqSxUfdvLeGjuotsB32bvfVPd+AaaTWMtyUeC9zgCnw5xs94no89yFLVsafvY9dMZEhTwsY/ZecA==", + "dependencies": { + "sequin": "*" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/csv-parse": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.5.6.tgz", + "integrity": "sha512-uNpm30m/AGSkLxxy7d9yRXpJQFrZzVWLFBkS+6ngPcZkw/5k3L/jjFuj7tVnEpRn+QgmiXr21nDlhCiUK4ij2A==" + }, + "node_modules/csv-stringify": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.1.tgz", + "integrity": "sha512-+9lpZfwpLntpTIEpFbwQyWuW/hmI/eHuJZD1XzeZpfZTqkf1fyvBbBLXTJJMsBuuS11uTShMqPwzx4A6ffXgRQ==" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esbuild": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.0.tgz", + "integrity": "sha512-1lvV17H2bMYda/WaFb2jLPeHU3zml2k4/yagNMG8Q/YtfMjCwEUZa2eXXMgZTVSL5q1n4H7sQ0X6CdJDqqeCFA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.0", + "@esbuild/android-arm": "0.23.0", + "@esbuild/android-arm64": "0.23.0", + "@esbuild/android-x64": "0.23.0", + "@esbuild/darwin-arm64": "0.23.0", + "@esbuild/darwin-x64": "0.23.0", + "@esbuild/freebsd-arm64": "0.23.0", + "@esbuild/freebsd-x64": "0.23.0", + "@esbuild/linux-arm": "0.23.0", + "@esbuild/linux-arm64": "0.23.0", + "@esbuild/linux-ia32": "0.23.0", + "@esbuild/linux-loong64": "0.23.0", + "@esbuild/linux-mips64el": "0.23.0", + "@esbuild/linux-ppc64": "0.23.0", + "@esbuild/linux-riscv64": "0.23.0", + "@esbuild/linux-s390x": "0.23.0", + "@esbuild/linux-x64": "0.23.0", + "@esbuild/netbsd-x64": "0.23.0", + "@esbuild/openbsd-arm64": "0.23.0", + "@esbuild/openbsd-x64": "0.23.0", + "@esbuild/sunos-x64": "0.23.0", + "@esbuild/win32-arm64": "0.23.0", + "@esbuild/win32-ia32": "0.23.0", + "@esbuild/win32-x64": "0.23.0" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jasmine": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jasmine/-/eslint-plugin-jasmine-4.2.2.tgz", + "integrity": "sha512-nALbewRk63uz28UGNhUTJyd6GofXxVNFpWFNAwr9ySc6kpSRIoO4suwZqIYz3cfJmCacilmjp7+1Ocjr7zRagA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8", + "npm": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/faye": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/faye/-/faye-1.4.0.tgz", + "integrity": "sha512-kRrIg4be8VNYhycS2PY//hpBJSzZPr/DBbcy9VWelhZMW3KhyLkQR0HL0k0MNpmVoNFF4EdfMFkNAWjTP65g6w==", + "dependencies": { + "asap": "*", + "csprng": "*", + "faye-websocket": ">=0.9.1", + "safe-buffer": "*", + "tough-cookie": "*", + "tunnel-agent": "*" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/figures/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz", + "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/husky": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.6.tgz", + "integrity": "sha512-sqbjZKK7kf44hfdE94EoX8MZNk0n7HeW37O4YrVGCF4wzgQjp+akPAkfUK5LZ6KuR/6sqeAVuXHji+RzQgOn5A==", + "dev": true, + "license": "MIT", + "bin": { + "husky": "bin.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/inquirer": { + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^6.0.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/inquirer/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/inquirer/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inquirer/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/inquirer/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jasmine/-/jasmine-5.4.0.tgz", + "integrity": "sha512-E2u4ylX5tgGYvbynImU6EUBKKrSVB1L72FEPjGh4M55ov1VsxR26RA2JU91L9YSPFgcjo4mCLyKn/QXvEYGBkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.2.2", + "jasmine-core": "~5.4.0" + }, + "bin": { + "jasmine": "bin/jasmine.js" + } + }, + "node_modules/jasmine-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", + "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsforce": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/jsforce/-/jsforce-3.5.2.tgz", + "integrity": "sha512-yjERrUSCX/T2Tc/geYhh1uTr1LvafVNmtzphlk2C4AY9X+iZUdsRxvKRKUipWQNdQ1raAfD1zAtdjGgMsnSUSw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.23.1", + "@babel/runtime-corejs3": "^7.23.1", + "@sindresorhus/is": "^4", + "base64url": "^3.0.1", + "commander": "^4.0.1", + "core-js": "^3.33.0", + "csv-parse": "^5.5.2", + "csv-stringify": "^6.4.4", + "faye": "^1.4.0", + "form-data": "^4.0.0", + "https-proxy-agent": "^5.0.0", + "inquirer": "^8.2.6", + "multistream": "^3.1.0", + "node-fetch": "^2.6.1", + "open": "^7.0.0", + "strip-ansi": "^6.0.0", + "xml2js": "^0.6.2" + }, + "bin": { + "jsforce": "bin/jsforce", + "jsforce-gen-schema": "bin/jsforce-gen-schema" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lint-staged": { + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", + "dev": true, + "dependencies": { + "chalk": "~5.3.0", + "commander": "~12.1.0", + "debug": "~4.3.6", + "execa": "~8.0.1", + "lilconfig": "~3.1.2", + "listr2": "~8.2.4", + "micromatch": "~4.0.8", + "pidtree": "~0.6.0", + "string-argv": "~0.3.2", + "yaml": "~2.5.0" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + } + }, + "node_modules/lint-staged/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/lint-staged/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/listr2": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.4.tgz", + "integrity": "sha512-opevsywziHd3zHCVQGAj8zu+Z3yHNkkoYhWIGnq54RrCVwLz0MozotJEDnKsIBLvkfLGN6BLOyAeRrYI0pKA4g==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lodash.sortby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", + "integrity": "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/multistream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/multistream/-/multistream-3.1.0.tgz", + "integrity": "sha512-zBgD3kn8izQAN/TaL1PCMv15vYpf+Vcrsfub06njuYVYlzUldzpopTlrEZ53pZVEbfn3Shtv7vRFoOv6LOV87Q==", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^3.4.0" + } + }, + "node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.1.0.tgz", + "integrity": "sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q==", + "dev": true, + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pidtree": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/pidtree/-/pidtree-0.6.0.tgz", + "integrity": "sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==", + "dev": true, + "bin": { + "pidtree": "bin/pidtree.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.0.tgz", + "integrity": "sha512-YWD03n3shzV9ImZRX3ccbjqLxj7NokGN0V/ESiBV5xWqrommYHYiihuIyavq03pWSGqlyvYUFmfoMKd+1rPA/g==", + "hasInstallScript": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rollup": { + "version": "4.22.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.5.tgz", + "integrity": "sha512-WoinX7GeQOFMGznEcWA1WrTQCd/tpEbMkc3nuMs9BT0CPjMdSjPMTVClwWd4pgSQwJdP65SK9mTCNvItlr5o7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.5", + "@rollup/rollup-android-arm64": "4.22.5", + "@rollup/rollup-darwin-arm64": "4.22.5", + "@rollup/rollup-darwin-x64": "4.22.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.5", + "@rollup/rollup-linux-arm-musleabihf": "4.22.5", + "@rollup/rollup-linux-arm64-gnu": "4.22.5", + "@rollup/rollup-linux-arm64-musl": "4.22.5", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.5", + "@rollup/rollup-linux-riscv64-gnu": "4.22.5", + "@rollup/rollup-linux-s390x-gnu": "4.22.5", + "@rollup/rollup-linux-x64-gnu": "4.22.5", + "@rollup/rollup-linux-x64-musl": "4.22.5", + "@rollup/rollup-win32-arm64-msvc": "4.22.5", + "@rollup/rollup-win32-ia32-msvc": "4.22.5", + "@rollup/rollup-win32-x64-msvc": "4.22.5", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/sequin": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/sequin/-/sequin-0.1.1.tgz", + "integrity": "sha512-hJWMZRwP75ocoBM+1/YaCsvS0j5MTPeBHJkS2/wruehl9xwtX30HlDF1Gt6UZ8HHHY8SJa2/IL+jo+JJCd59rA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/source-map": { + "version": "0.8.0-beta.0", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", + "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "dev": true, + "dependencies": { + "whatwg-url": "^7.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==" + }, + "node_modules/tinyglobby": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.6.tgz", + "integrity": "sha512-NbBoFBpqfcgd1tCiO8Lkfdk+xrA7mlLR9zgvZcZWQQwU63XAfUePyd6wZBaU93Hqw347lHnwFzttAkemHzzz4g==", + "dev": true, + "license": "ISC", + "dependencies": { + "fdir": "^6.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.3.0.tgz", + "integrity": "sha512-QOnuT+BOtivR77wYvCWHfGt9s4Pz1VIMbD463vegT5MLqNXy8rYFT/lPVEqf/bhYeT6qmqrNHhsX+rWwe3rOCQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-1.0.1.tgz", + "integrity": "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" + }, + "node_modules/tsup": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.3.0.tgz", + "integrity": "sha512-ALscEeyS03IomcuNdFdc0YWGVIkwH1Ws7nfTbAPuoILvEV2hpGQAY72LIOjglGo4ShWpZfpBqP/jpQVCzqYQag==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.0.0", + "cac": "^6.7.14", + "chokidar": "^3.6.0", + "consola": "^3.2.3", + "debug": "^4.3.5", + "esbuild": "^0.23.0", + "execa": "^5.1.1", + "joycon": "^3.1.1", + "picocolors": "^1.0.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.19.0", + "source-map": "0.8.0-beta.0", + "sucrase": "^3.35.0", + "tinyglobby": "^0.2.1", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/tsup/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/tsup/node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/tsup/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsup/node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tsup/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsup/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tsup/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tsup/node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/underscore": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.6.tgz", + "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==" + }, + "node_modules/undici": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.20.0.tgz", + "integrity": "sha512-AITZfPuxubm31Sx0vr8bteSalEbs9wQb/BOBi9FPlD9Qpd6HxZ4Q0+hI742jBhkPb4RT2v5MQzaW5VhRVyj+9A==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-4.0.2.tgz", + "integrity": "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==", + "dev": true + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/whatwg-url": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-7.1.0.tgz", + "integrity": "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==", + "dev": true, + "dependencies": { + "lodash.sortby": "^4.7.0", + "tr46": "^1.0.1", + "webidl-conversions": "^4.0.2" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json index a7c9382..f369ef8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salesforce-pubsub-api-client", - "version": "4.2.0", + "version": "5.0.0", "type": "module", "description": "A node client for the Salesforce Pub/Sub API", "author": "pozil", @@ -15,35 +15,39 @@ }, "scripts": { "build": "tsup && tsc", - "prettier": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", - "lint": "eslint src", + "test": "jasmine", + "format": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", + "format:verify": "prettier --check '**/*.{css,html,js,json,md,yaml,yml}'", + "lint": "eslint \"src/**\" \"spec/**\"", "prepare": "husky || true", "precommit": "lint-staged", "prepublishOnly": "npm run build" }, "dependencies": { - "@grpc/grpc-js": "^1.11.3", + "@grpc/grpc-js": "^1.12.2", "@grpc/proto-loader": "^0.7.13", "avro-js": "^1.12.0", "certifi": "^14.5.15", - "dotenv": "^16.4.5", - "jsforce": "^3.4.2", - "undici": "^6.19.8" + "jsforce": "^3.5.2", + "undici": "^6.20.0" }, "devDependencies": { "@chialab/esbuild-plugin-meta-url": "^0.18.2", - "eslint": "^9.11.1", + "dotenv": "^16.4.5", + "eslint": "^9.12.0", + "eslint-plugin-jasmine": "^4.2.2", "husky": "^9.1.6", + "jasmine": "^5.4.0", "lint-staged": "^15.2.10", "prettier": "^3.3.3", "tsup": "^8.3.0", - "typescript": "^5.6.2" + "typescript": "^5.6.3" }, "lint-staged": { - "**/src/*.{css,html,js,json,md,yaml,yml}": [ + "**/*.{css,html,js,json,md,yaml,yml}": [ "prettier --write" ], - "**/src/**/*.js": [ + "**/{src,spec}/**/*.js": [ "eslint" ] }, @@ -62,6 +66,6 @@ "pubsub_api.proto" ], "volta": { - "node": "20.14.0" + "node": "20.17.0" } } diff --git a/pubsub_api.proto b/pubsub_api.proto index 0c9ec82..0152e77 100644 --- a/pubsub_api.proto +++ b/pubsub_api.proto @@ -223,10 +223,16 @@ string rpc_id = 3; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Request for the ManagedSubscribe streaming RPC method. This request is used to: + * 1. Establish the initial managed subscribe stream. + * 2. Request more events from the subscription stream. + * 3. Commit a Replay ID using CommitReplayRequest. + */ message ManagedFetchRequest { /* * Managed subscription ID or developer name. This value corresponds to the @@ -251,10 +257,14 @@ } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Response for the ManagedSubscribe streaming RPC method. This can return + * ConsumerEvent(s) or CommitReplayResponse along with other metadata. + */ message ManagedFetchResponse { // Received events for subscription for client consumption repeated ConsumerEvent events = 1; @@ -268,10 +278,14 @@ CommitReplayResponse commit_response = 5; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Request to commit a Replay ID for the last processed event or for the latest + * replay ID received in an empty batch of events. + */ message CommitReplayRequest { // commit_request_id to identify commit responses string commit_request_id = 1; @@ -279,17 +293,25 @@ bytes replay_id = 2; } - /* EXPERIMENTAL: This API might be changed in backward-incompatible - * ways and is not recommended for production use. It is not subject to any - * SLA or deprecation policy. - */ + /* + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * There is no guaranteed 1:1 CommitReplayRequest to CommitReplayResponse. + * N CommitReplayRequest(s) can get compressed in a batch resulting in a single + * CommitReplayResponse which reflects the latest values of last + * CommitReplayRequest in that batch. + */ message CommitReplayResponse { - // commit_request_id to identify commit responses + // commit_request_id to identify commit responses. string commit_request_id = 1; // replayId that may have been committed bytes replay_id = 2; // for failed commits Error error = 3; + // time when server received request in epoch ms + int64 process_time = 4; } /* @@ -378,10 +400,12 @@ rpc PublishStream (stream PublishRequest) returns (stream PublishResponse); /* - * Same as Subscribe, but for Managed Subscribtions clients. - * EXPERIMENTAL: This feature is part of a closed alpha release. This - * API might be changed in backward-incompatible ways and is not recommended - * for production use. It is not subject to any SLA or deprecation policy. + * This feature is part of an open beta release and is subject to the applicable + * Beta Services Terms provided at Agreements and Terms + * (https://www.salesforce.com/company/legal/agreements/). + * + * Same as Subscribe, but for Managed Subscription clients. + * This feature is part of an open beta release. */ rpc ManagedSubscribe (stream ManagedFetchRequest) returns (stream ManagedFetchResponse); } diff --git a/spec/helper/asyncUtilities.js b/spec/helper/asyncUtilities.js new file mode 100644 index 0000000..7d007a8 --- /dev/null +++ b/spec/helper/asyncUtilities.js @@ -0,0 +1,20 @@ +export async function sleep(duration) { + return new Promise((resolve) => setTimeout(() => resolve(), duration)); +} + +export async function waitFor(timeoutDuration, checkFunction) { + return new Promise((resolve, reject) => { + let checkInterval; + const waitTimeout = setTimeout(() => { + clearInterval(checkInterval); + reject(`waitFor timed out after ${timeoutDuration} ms`); + }, timeoutDuration); + checkInterval = setInterval(() => { + if (checkFunction()) { + clearTimeout(waitTimeout); + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); +} diff --git a/spec/helper/reporter.js b/spec/helper/reporter.js new file mode 100644 index 0000000..5d1d660 --- /dev/null +++ b/spec/helper/reporter.js @@ -0,0 +1,25 @@ +let isReporterInjected = false; + +export default function injectJasmineReporter(logger) { + // Only inject report once + if (isReporterInjected) { + return; + } + isReporterInjected = true; + + // Build and inject reporter + const customReporter = { + specStarted: (result) => { + logger.info('----------'); + logger.info(`START TEST: ${result.description}`); + logger.info('----------'); + }, + specDone: (result) => { + logger.info('--------'); + logger.info(`END TEST: [${result.status}] ${result.description}`); + logger.info('--------'); + } + }; + const env = jasmine.getEnv(); + env.addReporter(customReporter); +} diff --git a/spec/helper/sfUtility.js b/spec/helper/sfUtility.js new file mode 100644 index 0000000..2bd59a4 --- /dev/null +++ b/spec/helper/sfUtility.js @@ -0,0 +1,42 @@ +import jsforce from 'jsforce'; + +let sfConnection; + +export async function getSalesforceConnection() { + if (!sfConnection) { + sfConnection = new jsforce.Connection({ + loginUrl: process.env.SALESFORCE_LOGIN_URL + }); + await sfConnection.login( + process.env.SALESFORCE_USERNAME, + process.env.SALESFORCE_TOKEN + ? process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_TOKEN + : process.env.SALESFORCE_PASSWORD + ); + } + return sfConnection; +} + +export async function getSampleAccount() { + const res = await sfConnection.query( + `SELECT Id, Name, BillingCity FROM Account WHERE Name='Sample Account'` + ); + let sampleAccount; + if (res.totalSize === 0) { + sampleAccount = { Name: 'Sample Account', BillingCity: 'SFO' }; + const ret = await sfConnection.sobject('Account').create(sampleAccount); + sampleAccount.Id = ret.id; + } else { + sampleAccount = res.records[0]; + } + return sampleAccount; +} + +export async function updateSampleAccount(updatedAccount) { + sfConnection.sobject('Account').update(updatedAccount, (err, ret) => { + if (err || !ret.success) { + throw new Error('Failed to update sample account'); + } + console.log('Record updated'); + }); +} diff --git a/spec/helper/simpleFileLogger.js b/spec/helper/simpleFileLogger.js new file mode 100644 index 0000000..c6cb601 --- /dev/null +++ b/spec/helper/simpleFileLogger.js @@ -0,0 +1,43 @@ +import fs from 'fs'; + +const LOG_LEVELS = ['debug', 'info', 'warn', 'error']; + +export default class SimpleFileLogger { + #filePath; + #level; + + constructor(filePath, levelString = 'info') { + this.#filePath = filePath; + const lcLevelString = levelString.toLowerCase(); + const level = LOG_LEVELS.indexOf(lcLevelString); + this.#level = level === -1 ? 1 : level; + } + + clear() { + fs.rmSync(this.#filePath, { force: true }); + } + + debug(...data) { + if (this.#level <= 0) this.log('DEBUG', data); + } + + info(...data) { + if (this.#level <= 1) this.log('INFO', data); + } + + warn(...data) { + if (this.#level <= 2) this.log('WARN', data); + } + + error(...data) { + if (this.#level <= 3) this.log('ERROR', data); + } + + log(level, data) { + const ts = new Date().toISOString(); + fs.appendFileSync( + this.#filePath, + `${ts}\t${level}\t${data.join('')}\n` + ); + } +} diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js new file mode 100644 index 0000000..5a531e2 --- /dev/null +++ b/spec/integration/client.spec.js @@ -0,0 +1,329 @@ +import fs from 'fs'; +import * as dotenv from 'dotenv'; +import PubSubApiClient from '../../src/client.js'; +import { AuthType } from '../../src/utils/configuration.js'; +import { + getSalesforceConnection, + getSampleAccount, + updateSampleAccount +} from '../helper/sfUtility.js'; +import SimpleFileLogger from '../helper/simpleFileLogger.js'; +import injectJasmineReporter from '../helper/reporter.js'; +import { sleep, waitFor } from '../helper/asyncUtilities.js'; + +// Load config from .env file +dotenv.config(); + +// Prepare logger +let logger; +if (process.env.TEST_LOGGER === 'simpleFileLogger') { + logger = new SimpleFileLogger('test.log', 'debug'); + logger.clear(); + injectJasmineReporter(logger); +} else { + logger = console; +} + +const EXTENDED_JASMINE_TIMEOUT = 10000; +const PLATFORM_EVENT_TOPIC = '/event/Sample__e'; +const CHANGE_EVENT_TOPIC = '/data/AccountChangeEvent'; + +describe('Client', function () { + var client; + + afterEach(async () => { + if (client) { + client.close(); + await sleep(500); + } + }); + + it( + 'supports user supplied auth with platform event', + async function () { + let receivedEvent, receivedSub; + + // Establish connection with jsforce + const sfConnection = await getSalesforceConnection(); + + // Build PubSub client with existing connection + client = new PubSubApiClient( + { + authType: AuthType.USER_SUPPLIED, + accessToken: sfConnection.accessToken, + instanceUrl: sfConnection.instanceUrl, + organizationId: sfConnection.userInfo.organizationId + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + receivedEvent = data; + receivedSub = subscription; + } + }; + client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective + await sleep(1000); + + // Publish platform event + const payload = { + CreatedDate: new Date().getTime(), // Non-null value required but there's no validity check performed on this field + CreatedById: '00558000000yFyDAAU', // Valid user ID + Message__c: { string: 'Hello world' } // Field is nullable so we need to specify the 'string' type + }; + const publishResult = await client.publish( + PLATFORM_EVENT_TOPIC, + payload + ); + expect(publishResult.replayId).toBeDefined(); + const publishedReplayId = publishResult.replayId; + + // Wait for event to be received + await waitFor(5000, () => receivedEvent !== undefined); + + // Check received event and subcription info + expect(receivedEvent?.replayId).toBe(publishedReplayId); + expect(receivedSub?.topicName).toBe(PLATFORM_EVENT_TOPIC); + expect(receivedSub?.receivedEventCount).toBe(1); + expect(receivedSub?.requestedEventCount).toBe(1); + }, + EXTENDED_JASMINE_TIMEOUT + ); + + it( + 'supports user supplied auth with change event', + async function () { + let receivedEvent, receivedSub; + + // Establish connection with jsforce + const sfConnection = await getSalesforceConnection(); + + // Build PubSub client with existing connection + client = new PubSubApiClient( + { + authType: AuthType.USER_SUPPLIED, + accessToken: sfConnection.accessToken, + instanceUrl: sfConnection.instanceUrl, + organizationId: sfConnection.userInfo.organizationId + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + receivedEvent = data; + receivedSub = subscription; + } + }; + client.subscribe(CHANGE_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective + await sleep(1000); + + // Update sample record + const account = await getSampleAccount(); + account.BillingCity = 'SFO' + Math.random(); + await updateSampleAccount(account); + + // Wait for event to be received + await waitFor(5000, () => receivedEvent !== undefined); + + // Check received event and subcription info + expect(receivedEvent?.replayId).toBeDefined(); + expect(receivedSub?.topicName).toBe(CHANGE_EVENT_TOPIC); + expect(receivedSub?.receivedEventCount).toBe(1); + expect(receivedSub?.requestedEventCount).toBe(1); + expect(receivedEvent.payload.ChangeEventHeader.entityName).toBe( + 'Account' + ); + expect(receivedEvent.payload.ChangeEventHeader.recordIds[0]).toBe( + account.Id + ); + expect( + receivedEvent.payload.ChangeEventHeader.changedFields.includes( + 'BillingAddress.City' + ) + ).toBeTrue(); + expect(receivedEvent.payload.BillingAddress.City).toBe( + account.BillingCity + ); + }, + EXTENDED_JASMINE_TIMEOUT + ); + + it( + 'supports usermame/password auth with platform event', + async function () { + let receivedEvent, receivedSub; + + // Build PubSub client + client = new PubSubApiClient( + { + authType: AuthType.USERNAME_PASSWORD, + loginUrl: process.env.SALESFORCE_LOGIN_URL, + username: process.env.SALESFORCE_USERNAME, + password: process.env.SALESFORCE_PASSWORD, + userToken: process.env.SALESFORCE_TOKEN + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + receivedEvent = data; + receivedSub = subscription; + } + }; + client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective + await sleep(1000); + + // Publish platform event + const payload = { + CreatedDate: new Date().getTime(), // Non-null value required but there's no validity check performed on this field + CreatedById: '00558000000yFyDAAU', // Valid user ID + Message__c: { string: 'Hello world' } // Field is nullable so we need to specify the 'string' type + }; + const publishResult = await client.publish( + PLATFORM_EVENT_TOPIC, + payload + ); + expect(publishResult.replayId).toBeDefined(); + const publishedReplayId = publishResult.replayId; + + // Wait for event to be received + await waitFor(5000, () => receivedEvent !== undefined); + + // Check received event and subcription info + expect(receivedEvent?.replayId).toBe(publishedReplayId); + expect(receivedSub?.topicName).toBe(PLATFORM_EVENT_TOPIC); + expect(receivedSub?.receivedEventCount).toBe(1); + expect(receivedSub?.requestedEventCount).toBe(1); + }, + EXTENDED_JASMINE_TIMEOUT + ); + + it( + 'supports client credentials OAuth flow with platform event', + async function () { + let receivedEvent, receivedSub; + + // Build PubSub client + client = new PubSubApiClient( + { + authType: AuthType.OAUTH_CLIENT_CREDENTIALS, + loginUrl: process.env.SALESFORCE_LOGIN_URL, + clientId: process.env.SALESFORCE_CLIENT_ID, + clientSecret: process.env.SALESFORCE_CLIENT_SECRET + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + receivedEvent = data; + receivedSub = subscription; + } + }; + client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective + await sleep(1000); + + // Publish platform event + const payload = { + CreatedDate: new Date().getTime(), // Non-null value required but there's no validity check performed on this field + CreatedById: '00558000000yFyDAAU', // Valid user ID + Message__c: { string: 'Hello world' } // Field is nullable so we need to specify the 'string' type + }; + const publishResult = await client.publish( + PLATFORM_EVENT_TOPIC, + payload + ); + expect(publishResult.replayId).toBeDefined(); + const publishedReplayId = publishResult.replayId; + + // Wait for event to be received + await waitFor(5000, () => receivedEvent !== undefined); + + // Check received event and subcription info + expect(receivedEvent?.replayId).toBe(publishedReplayId); + expect(receivedSub?.topicName).toBe(PLATFORM_EVENT_TOPIC); + expect(receivedSub?.receivedEventCount).toBe(1); + expect(receivedSub?.requestedEventCount).toBe(1); + }, + EXTENDED_JASMINE_TIMEOUT + ); + + it( + 'supports JWT OAuth flow with platform event', + async function () { + let receivedEvent, receivedSub; + + // Read private key and remove potential invalid characters from key + const privateKey = fs.readFileSync( + process.env.SALESFORCE_PRIVATE_KEY_PATH + ); + + // Build PubSub client + client = new PubSubApiClient( + { + authType: AuthType.OAUTH_JWT_BEARER, + loginUrl: process.env.SALESFORCE_JWT_LOGIN_URL, + clientId: process.env.SALESFORCE_JWT_CLIENT_ID, + username: process.env.SALESFORCE_USERNAME, + privateKey + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'event') { + receivedEvent = data; + receivedSub = subscription; + } + }; + client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective + await sleep(1000); + + // Publish platform event + const payload = { + CreatedDate: new Date().getTime(), // Non-null value required but there's no validity check performed on this field + CreatedById: '00558000000yFyDAAU', // Valid user ID + Message__c: { string: 'Hello world' } // Field is nullable so we need to specify the 'string' type + }; + const publishResult = await client.publish( + PLATFORM_EVENT_TOPIC, + payload + ); + expect(publishResult.replayId).toBeDefined(); + const publishedReplayId = publishResult.replayId; + + // Wait for event to be received + await waitFor(5000, () => receivedEvent !== undefined); + + // Check received event and subcription info + expect(receivedEvent?.replayId).toBe(publishedReplayId); + expect(receivedSub?.topicName).toBe(PLATFORM_EVENT_TOPIC); + expect(receivedSub?.receivedEventCount).toBe(1); + expect(receivedSub?.requestedEventCount).toBe(1); + }, + EXTENDED_JASMINE_TIMEOUT + ); +}); diff --git a/spec/integration/clientFailures.spec.js b/spec/integration/clientFailures.spec.js new file mode 100644 index 0000000..1c8ecd9 --- /dev/null +++ b/spec/integration/clientFailures.spec.js @@ -0,0 +1,107 @@ +import * as dotenv from 'dotenv'; +import PubSubApiClient from '../../src/client.js'; +import { AuthType } from '../../src/utils/configuration.js'; +import SimpleFileLogger from '../helper/simpleFileLogger.js'; +import injectJasmineReporter from '../helper/reporter.js'; +import { sleep, waitFor } from '../helper/asyncUtilities.js'; + +// Load config from .env file +dotenv.config(); + +// Prepare logger +let logger; +if (process.env.TEST_LOGGER === 'simpleFileLogger') { + logger = new SimpleFileLogger('test.log', 'debug'); + logger.clear(); + injectJasmineReporter(logger); +} else { + logger = console; +} + +const PLATFORM_EVENT_TOPIC = '/event/Sample__e'; + +describe('Client failures', function () { + var client; + + afterEach(async () => { + if (client) { + client.close(); + await sleep(500); + } + }); + + it('fails to connect with invalid user supplied auth', async function () { + let grpcStatusCode, errorCode; + let isConnectionClosed = false; + + // Build PubSub client with invalid credentials + client = new PubSubApiClient( + { + authType: AuthType.USER_SUPPLIED, + accessToken: 'invalidToken', + instanceUrl: 'https://pozil-dev-ed.my.salesforce.com', + organizationId: '00D58000000arpq' + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'error') { + errorCode = data.code; + } else if (callbackType === 'grpcStatus') { + grpcStatusCode = data.code; + } else if (callbackType === 'end') { + isConnectionClosed = true; + } + }; + client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); + + // Wait for subscribe to be effective and error to surface + await waitFor(5000, () => errorCode !== undefined); + + // Check for gRPC auth error and closed connection + expect(errorCode).toBe(16); + expect(grpcStatusCode).toBe(16); + expect(isConnectionClosed).toBeTrue(); + }); + + it('fails to subscribe to an invalid event', async function () { + let grpcStatusCode, errorCode; + let isConnectionClosed = false; + + // Build PubSub client + client = new PubSubApiClient( + { + authType: AuthType.USERNAME_PASSWORD, + loginUrl: process.env.SALESFORCE_LOGIN_URL, + username: process.env.SALESFORCE_USERNAME, + password: process.env.SALESFORCE_PASSWORD, + userToken: process.env.SALESFORCE_TOKEN + }, + logger + ); + await client.connect(); + + // Prepare callback & send subscribe request + const callback = (subscription, callbackType, data) => { + if (callbackType === 'error') { + errorCode = data.code; + } else if (callbackType === 'grpcStatus') { + grpcStatusCode = data.code; + } else if (callbackType === 'end') { + isConnectionClosed = true; + } + }; + client.subscribe('/event/INVALID', callback, 1); + + // Wait for subscribe to be effective and error to surface + await waitFor(5000, () => errorCode !== undefined); + + // Check for gRPC auth error and closed connection + expect(errorCode).toBe(7); + expect(grpcStatusCode).toBe(7); + expect(isConnectionClosed).toBeTrue(); + }); +}); diff --git a/spec/support/jasmine.json b/spec/support/jasmine.json new file mode 100644 index 0000000..327753a --- /dev/null +++ b/spec/support/jasmine.json @@ -0,0 +1,8 @@ +{ + "spec_dir": "spec", + "spec_files": ["**/*.spec.js"], + "env": { + "stopSpecOnExpectationFailure": true, + "random": true + } +} diff --git a/src/client.js b/src/client.js index ccedba3..9c96a51 100644 --- a/src/client.js +++ b/src/client.js @@ -7,22 +7,33 @@ import certifi from 'certifi'; import grpc from '@grpc/grpc-js'; import protoLoader from '@grpc/proto-loader'; // eslint-disable-next-line no-unused-vars -import { EventEmitter } from 'events'; -// eslint-disable-next-line no-unused-vars import { connectivityState } from '@grpc/grpc-js'; import SchemaCache from './utils/schemaCache.js'; import EventParseError from './utils/eventParseError.js'; -import PubSubEventEmitter from './utils/pubSubEventEmitter.js'; import { CustomLongAvroType } from './utils/avroHelper.js'; -import Configuration from './utils/configuration.js'; +import { AuthType, Configuration } from './utils/configuration.js'; import { parseEvent, encodeReplayId, - decodeReplayId + decodeReplayId, + toJsonString } from './utils/eventParser.js'; import SalesforceAuth from './utils/auth.js'; +/** + * Enum for subscripe callback type values + * @enum {string} + */ +const SubscribeCallbackType = { + EVENT: 'event', + LAST_EVENT: 'lastEvent', + ERROR: 'error', + END: 'end', + GRPC_STATUS: 'grpcStatus', + GRPC_KEEP_ALIVE: 'grpcKeepAlive' +}; + /** * @typedef {Object} PublishResult * @property {number} replayId @@ -30,6 +41,48 @@ import SalesforceAuth from './utils/auth.js'; * @global */ +/** + * @callback SubscribeCallback + * @param {SubscriptionInfo} subscription + * @param {SubscribeCallbackType} callbackType + * @param {Object} [data] + * @global + */ + +/** + * @typedef {Object} Subscription + * @property {SubscriptionInfo} info + * @property {Object} grpcSubscription + * @property {SubscribeCallback} subscribeCallback + * @protected + */ + +/** + * @typedef {Object} SubscriptionInfo + * @property {string} topicName + * @property {number} requestedEventCount + * @property {number} receivedEventCount + * @property {number} lastReplayId + * @protected + */ + +/** + * @typedef {Object} Configuration + * @property {AuthType} authType + * @property {string} pubSubEndpoint + * @property {string} loginUrl + * @property {string} username + * @property {string} password + * @property {string} userToken + * @property {string} clientId + * @property {string} clientSecret + * @property {string} privateKey + * @property {string} accessToken + * @property {string} instanceUrl + * @property {string} organizationId + * @protected + */ + /** * @typedef {Object} Logger * @property {Function} debug @@ -39,6 +92,15 @@ import SalesforceAuth from './utils/auth.js'; * @protected */ +/** + * @typedef {Object} SubscribeRequest + * @property {string} topicName + * @property {number} numRequested + * @property {number} [replayPreset] + * @property {number} [replayId] + * @protected + */ + /** * Maximum event batch size suppported by the Pub/Sub API as documented here: * https://developer.salesforce.com/docs/platform/pub-sub-api/guide/flow-control.html @@ -51,6 +113,12 @@ const MAX_EVENT_BATCH_SIZE = 100; * @global */ export default class PubSubApiClient { + /** + * Client configuration + * @type {Configuration} + */ + #config; + /** * gRPC client * @type {Object} @@ -64,24 +132,29 @@ export default class PubSubApiClient { #schemaChache; /** - * Map of subscribitions indexed by topic name - * @type {Map} + * Map of subscriptions indexed by topic name + * @type {Map} */ #subscriptions; + /** + * Logger + * @type {Logger} + */ #logger; /** * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration * @param {Logger} [logger] an optional custom logger. The client uses the console if no value is supplied. */ - constructor(logger = console) { + constructor(config, logger = console) { this.#logger = logger; this.#schemaChache = new SchemaCache(); this.#subscriptions = new Map(); // Check and load config try { - Configuration.load(); + this.#config = Configuration.load(config); } catch (error) { this.#logger.error(error); throw new Error('Failed to initialize Pub/Sub API client', { @@ -91,82 +164,35 @@ export default class PubSubApiClient { } /** - * Authenticates with Salesforce then, connects to the Pub/Sub API. + * Authenticates with Salesforce (if not using user-supplied authentication mode) then, + * connects to the Pub/Sub API. * @returns {Promise} Promise that resolves once the connection is established * @memberof PubSubApiClient.prototype */ async connect() { - if (Configuration.isUserSuppliedAuth()) { - throw new Error( - 'You selected user-supplied authentication mode so you cannot use the "connect()" method. Use "connectWithAuth(...)" instead.' - ); - } - - // Connect to Salesforce to obtain an access token - let conMetadata; - try { - conMetadata = await SalesforceAuth.authenticate(); - this.#logger.info( - `Connected to Salesforce org ${conMetadata.instanceUrl} as ${conMetadata.username}` - ); - } catch (error) { - throw new Error('Failed to authenticate with Salesforce', { - cause: error - }); - } - return this.#connectToPubSubApi(conMetadata); - } - - /** - * Connects to the Pub/Sub API with user-supplied authentication. - * @param {string} accessToken Salesforce access token - * @param {string} instanceUrl Salesforce instance URL - * @param {string} [organizationId] optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. - * @returns {Promise} Promise that resolves once the connection is established - * @memberof PubSubApiClient.prototype - */ - async connectWithAuth(accessToken, instanceUrl, organizationId) { - if (!instanceUrl || !instanceUrl.startsWith('https://')) { - throw new Error( - `Invalid Salesforce Instance URL format supplied: ${instanceUrl}` - ); - } - let validOrganizationId = organizationId; - if (!organizationId) { + // Retrieve access token if not using user-supplied auth + if (this.#config.authType !== AuthType.USER_SUPPLIED) { + // Connect to Salesforce to obtain an access token try { - validOrganizationId = accessToken.split('!').at(0); - } catch (error) { - throw new Error( - 'Unable to parse organizationId from given access token', - { - cause: error - } + const auth = new SalesforceAuth(this.#config, this.#logger); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#config.organizationId = conMetadata.organizationId; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); + } catch (error) { + throw new Error('Failed to authenticate with Salesforce', { + cause: error + }); } } - if ( - validOrganizationId.length !== 15 && - validOrganizationId.length !== 18 - ) { - throw new Error( - `Invalid Salesforce Org ID format supplied: ${validOrganizationId}` - ); - } - return this.#connectToPubSubApi({ - accessToken, - instanceUrl, - organizationId: validOrganizationId - }); - } - /** - * Connects to the Pub/Sub API. - * @param {import('./auth.js').ConnectionMetadata} conMetadata - * @returns {Promise} Promise that resolves once the connection is established - */ - async #connectToPubSubApi(conMetadata) { // Connect to Pub/Sub API try { + this.#logger.debug(`Connecting to Pub/Sub API`); // Read certificates const rootCert = fs.readFileSync(certifi); @@ -181,9 +207,9 @@ export default class PubSubApiClient { // Prepare gRPC connection const metaCallback = (_params, callback) => { const meta = new grpc.Metadata(); - meta.add('accesstoken', conMetadata.accessToken); - meta.add('instanceurl', conMetadata.instanceUrl); - meta.add('tenantid', conMetadata.organizationId); + meta.add('accesstoken', this.#config.accessToken); + meta.add('instanceurl', this.#config.instanceUrl); + meta.add('tenantid', this.#config.organizationId); callback(null, meta); }; const callCreds = @@ -195,11 +221,11 @@ export default class PubSubApiClient { // Return pub/sub gRPC client this.#client = new sfdcPackage.PubSub( - Configuration.getPubSubEndpoint(), + this.#config.pubSubEndpoint, combCreds ); this.#logger.info( - `Connected to Pub/Sub API endpoint ${Configuration.getPubSubEndpoint()}` + `Connected to Pub/Sub API endpoint ${this.#config.pubSubEndpoint}` ); } catch (error) { throw new Error('Failed to connect to Pub/Sub API', { @@ -220,55 +246,76 @@ export default class PubSubApiClient { /** * Subscribes to a topic and retrieves all past events in retention window. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromEarliestEvent(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 1 - }); + subscribeFromEarliestEvent( + topicName, + subscribeCallback, + numRequested = null + ) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 1 + }, + subscribeCallback + ); } /** * Subscribes to a topic and retrieves past events starting from a replay ID. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} numRequested number of events requested. If null, the client keeps the subscription alive forever. * @param {number} replayId replay ID - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribeFromReplayId(topicName, numRequested, replayId) { - return this.#subscribe({ - topicName, - numRequested, - replayPreset: 2, - replayId: encodeReplayId(replayId) - }); + subscribeFromReplayId( + topicName, + subscribeCallback, + numRequested, + replayId + ) { + this.#subscribe( + { + topicName, + numRequested, + replayPreset: 2, + replayId: encodeReplayId(replayId) + }, + subscribeCallback + ); } /** * Subscribes to a topic. * @param {string} topicName name of the topic that we're subscribing to + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events * @param {number | null} [numRequested] optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. - * @returns {Promise} Promise that holds an emitter that allows you to listen to received events and stream lifecycle events * @memberof PubSubApiClient.prototype */ - async subscribe(topicName, numRequested = null) { - return this.#subscribe({ - topicName, - numRequested - }); + subscribe(topicName, subscribeCallback, numRequested = null) { + this.#subscribe( + { + topicName, + numRequested + }, + subscribeCallback + ); } /** * Subscribes to a topic using the gRPC client and an event schema - * @param {object} subscribeRequest subscription request - * @return {PubSubEventEmitter} emitter that allows you to listen to received events and stream lifecycle events + * @param {SubscribeRequest} subscribeRequest subscription request + * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ - async #subscribe(subscribeRequest) { + #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { // Check number of requested events @@ -292,6 +339,7 @@ export default class PubSubApiClient { this.#logger.warn( `The number of requested events for ${topicName} exceeds max event batch size (${MAX_EVENT_BATCH_SIZE}).` ); + subscribeRequest.numRequested = MAX_EVENT_BATCH_SIZE; } } // Check client connection @@ -301,39 +349,74 @@ export default class PubSubApiClient { // Check for an existing subscription let subscription = this.#subscriptions.get(topicName); - - // Send subscription request - if (!subscription) { - subscription = this.#client.Subscribe(); + let grpcSubscription; + if (subscription) { + // Reuse existing gRPC connection and reset event counters + this.#logger.debug( + `${topicName} - Reusing cached gRPC subscription` + ); + grpcSubscription = subscription.grpcSubscription; + subscription.info.receivedEventCount = 0; + subscription.info.requestedEventCount = + subscribeRequest.numRequested; + } else { + // Establish new gRPC subscription + this.#logger.debug( + `${topicName} - Establishing new gRPC subscription` + ); + grpcSubscription = this.#client.Subscribe(); + subscription = { + info: { + topicName, + requestedEventCount: subscribeRequest.numRequested, + receivedEventCount: 0, + lastReplayId: null + }, + grpcSubscription, + subscribeCallback + }; this.#subscriptions.set(topicName, subscription); } - subscription.write(subscribeRequest); - this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` - ); - // Listen to new events - const eventEmitter = new PubSubEventEmitter( - topicName, - numRequested - ); - subscription.on('data', async (data) => { + grpcSubscription.on('data', async (data) => { const latestReplayId = decodeReplayId(data.latestReplayId); + subscription.info.lastReplayId = latestReplayId; if (data.events) { this.#logger.info( - `Received ${data.events.length} events, latest replay ID: ${latestReplayId}` + `${topicName} - Received ${data.events.length} events, latest replay ID: ${latestReplayId}` ); for (const event of data.events) { try { + this.#logger.debug( + `${topicName} - Raw event: ${toJsonString(event)}` + ); // Load event schema from cache or from the gRPC client + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${event.event.schemaId}` + ); const schema = await this.#getEventSchemaFromId( event.event.schemaId ); + // Retrieve subscription + const subscription = + this.#subscriptions.get(topicName); + if (!subscription) { + throw new Error( + `Failed to retrieve subscription for topic ${topicName}.` + ); + } + subscription.info.receivedEventCount++; // Parse event thanks to schema const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); - eventEmitter.emit('data', parsedEvent); + this.#logger.debug( + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { // Report event parsing error with replay ID if possible let replayId; @@ -351,24 +434,31 @@ export default class PubSubApiClient { event, latestReplayId ); - eventEmitter.emit('error', parseError); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + parseError + ); this.#logger.error(parseError); } // Handle last requested event if ( - eventEmitter.getReceivedEventCount() === - eventEmitter.getRequestedEventCount() + subscription.info.receivedEventCount === + subscription.info.requestedEventCount ) { if (isInfiniteEventRequest) { // Request additional events this.requestAdditionalEvents( - eventEmitter, - MAX_EVENT_BATCH_SIZE + subscription.info.topicName, + subscription.info.requestedEventCount ); } else { // Emit a 'lastevent' event when reaching the last requested event count - eventEmitter.emit('lastevent'); + subscribeCallback( + subscription.info, + SubscribeCallbackType.LAST_EVENT + ); } } } @@ -376,30 +466,45 @@ export default class PubSubApiClient { // If there are no events then, every 270 seconds (or less) the server publishes a keepalive message with // the latestReplayId and pendingNumRequested (the number of events that the client is still waiting for) this.#logger.debug( - `Received keepalive message. Latest replay ID: ${latestReplayId}` + `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` ); data.latestReplayId = latestReplayId; // Replace original value with decoded value - eventEmitter.emit('keepalive', data); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_KEEP_ALIVE + ); } }); - subscription.on('end', () => { + grpcSubscription.on('end', () => { this.#subscriptions.delete(topicName); - this.#logger.info('gRPC stream ended'); - eventEmitter.emit('end'); + this.#logger.info(`${topicName} - gRPC stream ended`); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on('error', (error) => { + grpcSubscription.on('error', (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error ); - eventEmitter.emit('error', error); }); - subscription.on('status', (status) => { + grpcSubscription.on('status', (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` + ); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status ); - eventEmitter.emit('status', status); }); - return eventEmitter; + + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `${topicName} - Subscribe request sent for ${numRequested} events` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -410,12 +515,10 @@ export default class PubSubApiClient { /** * Request additional events on an existing subscription. - * @param {PubSubEventEmitter} eventEmitter event emitter that was obtained in the first subscribe call + * @param {string} topicName topic name * @param {number} numRequested number of events requested. */ - async requestAdditionalEvents(eventEmitter, numRequested) { - const topicName = eventEmitter.getTopicName(); - + requestAdditionalEvents(topicName, numRequested) { // Retrieve existing subscription const subscription = this.#subscriptions.get(topicName); if (!subscription) { @@ -425,13 +528,14 @@ export default class PubSubApiClient { } // Request additional events - eventEmitter._resetEventCount(numRequested); + subscription.receivedEventCount = 0; + subscription.requestedEventCount = numRequested; subscription.write({ topicName, numRequested: numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } @@ -445,6 +549,9 @@ export default class PubSubApiClient { */ async publish(topicName, payload, correlationKey) { try { + this.#logger.debug( + `${topicName} - Preparing to publish event: ${toJsonString(payload)}` + ); if (!this.#client) { throw new Error('Pub/Sub API client is not connected.'); } @@ -489,13 +596,10 @@ export default class PubSubApiClient { */ close() { this.#logger.info('Clear subscriptions'); - this.#subscriptions.forEach((subscription) => { - subscription.removeAllListeners(); - }); this.#subscriptions.clear(); this.#logger.info('Closing gRPC stream'); - this.#client.close(); + this.#client?.close(); } /** @@ -533,7 +637,11 @@ export default class PubSubApiClient { if (topicError) { reject(topicError); } else { + // Get the schema information const { schemaId } = response; + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${schemaId}` + ); // Check cache for schema thanks to ID let schema = this.#schemaChache.getFromId(schemaId); if (!schema) { @@ -543,7 +651,6 @@ export default class PubSubApiClient { schemaId ); } - this.#logger.info(`Topic schema loaded: ${topicName}`); // Add schema to cache this.#schemaChache.set(schema); resolve(schema); diff --git a/src/utils/auth.js b/src/utils/auth.js index 5dfd6f6..528c696 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -1,7 +1,7 @@ import crypto from 'crypto'; import jsforce from 'jsforce'; import { fetch } from 'undici'; -import Configuration from './configuration.js'; +import { AuthType } from './configuration.js'; /** * @typedef {Object} ConnectionMetadata @@ -12,19 +12,49 @@ import Configuration from './configuration.js'; */ export default class SalesforceAuth { + /** + * Client configuration + * @type {Configuration} + */ + #config; + + /** + * Logger + * @type {Logger} + */ + #logger; + + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + * @param {Logger} logger a logger + */ + constructor(config, logger) { + this.#config = config; + this.#logger = logger; + } + /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ - static async authenticate() { - if (Configuration.isUsernamePasswordAuth()) { - return SalesforceAuth.#authWithUsernamePassword(); - } else if (Configuration.isOAuthClientCredentialsAuth()) { - return SalesforceAuth.#authWithOAuthClientCredentials(); - } else if (Configuration.isOAuthJwtBearerAuth()) { - return SalesforceAuth.#authWithJwtBearer(); - } else { - throw new Error('Unsupported authentication mode.'); + async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + throw new Error( + 'Authenticate method should not be called in user-supplied mode.' + ); + case AuthType.USERNAME_PASSWORD: + return this.#authWithUsernamePassword(); + case AuthType.OAUTH_CLIENT_CREDENTIALS: + return this.#authWithOAuthClientCredentials(); + case AuthType.OAUTH_JWT_BEARER: + return this.#authWithJwtBearer(); + default: + throw new Error( + `Unsupported authType value: ${this.#config.authType}` + ); } } @@ -32,19 +62,18 @@ export default class SalesforceAuth { * Authenticates with the username/password flow * @returns {ConnectionMetadata} */ - static async #authWithUsernamePassword() { + async #authWithUsernamePassword() { + const { loginUrl, username, password, userToken } = this.#config; + const sfConnection = new jsforce.Connection({ - loginUrl: Configuration.getSfLoginUrl() + loginUrl }); - await sfConnection.login( - Configuration.getSfUsername(), - Configuration.getSfSecuredPassword() - ); + await sfConnection.login(username, `${password}${userToken}`); return { accessToken: sfConnection.accessToken, instanceUrl: sfConnection.instanceUrl, organizationId: sfConnection.userInfo.organizationId, - username: Configuration.getSfUsername() + username }; } @@ -52,25 +81,37 @@ export default class SalesforceAuth { * Authenticates with the OAuth 2.0 client credentials flow * @returns {ConnectionMetadata} */ - static async #authWithOAuthClientCredentials() { + async #authWithOAuthClientCredentials() { + const { clientId, clientSecret } = this.#config; const params = new URLSearchParams(); params.append('grant_type', 'client_credentials'); - params.append('client_id', Configuration.getSfClientId()); - params.append('client_secret', Configuration.getSfClientSecret()); - return SalesforceAuth.#authWithOAuth(params.toString()); + params.append('client_id', clientId); + params.append('client_secret', clientSecret); + return this.#authWithOAuth(params.toString()); } /** * Authenticates with the OAuth 2.0 JWT bearer flow * @returns {ConnectionMetadata} */ - static async #authWithJwtBearer() { + async #authWithJwtBearer() { + const { clientId, username, loginUrl, privateKey } = this.#config; + if ( + !privateKey + .toString() + .trim() + .startsWith('-----BEGIN RSA PRIVATE KEY-----') + ) { + throw new Error( + `Private key is missing -----BEGIN RSA PRIVATE KEY----- header` + ); + } // Prepare token const header = JSON.stringify({ alg: 'RS256' }); const claims = JSON.stringify({ - iss: Configuration.getSfClientId(), - sub: Configuration.getSfUsername(), - aud: Configuration.getSfLoginUrl(), + iss: clientId, + sub: username, + aud: loginUrl, exp: Math.floor(Date.now() / 1000) + 60 * 5 }); let token = `${base64url(header)}.${base64url(claims)}`; @@ -78,10 +119,10 @@ export default class SalesforceAuth { const sign = crypto.createSign('RSA-SHA256'); sign.update(token); sign.end(); - token += `.${base64url(sign.sign(Configuration.getSfPrivateKey()))}`; + token += `.${base64url(sign.sign(privateKey))}`; // Log in const body = `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${token}`; - return SalesforceAuth.#authWithOAuth(body); + return this.#authWithOAuth(body); } /** @@ -89,18 +130,16 @@ export default class SalesforceAuth { * @param {string} body URL encoded body * @returns {ConnectionMetadata} connection metadata */ - static async #authWithOAuth(body) { + async #authWithOAuth(body) { + const { loginUrl } = this.#config; // Log in - const loginResponse = await fetch( - `${Configuration.getSfLoginUrl()}/services/oauth2/token`, - { - method: 'post', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - body - } - ); + const loginResponse = await fetch(`${loginUrl}/services/oauth2/token`, { + method: 'post', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded' + }, + body + }); if (loginResponse.status !== 200) { throw new Error( `Authentication error: HTTP ${ @@ -111,7 +150,7 @@ export default class SalesforceAuth { const { access_token, instance_url } = await loginResponse.json(); // Get org and user info const userInfoResponse = await fetch( - `${Configuration.getSfLoginUrl()}/services/oauth2/userinfo`, + `${loginUrl}/services/oauth2/userinfo`, { headers: { authorization: `Bearer ${access_token}` } } diff --git a/src/utils/configuration.js b/src/utils/configuration.js index 6708c0c..247101e 100644 --- a/src/utils/configuration.js +++ b/src/utils/configuration.js @@ -1,112 +1,108 @@ -import * as dotenv from 'dotenv'; -import fs from 'fs'; +const DEFAULT_PUB_SUB_ENDPOINT = 'api.pubsub.salesforce.com:7443'; -const AUTH_USER_SUPPLIED = 'user-supplied', - AUTH_USERNAME_PASSWORD = 'username-password', - AUTH_OAUTH_CLIENT_CREDENTIALS = 'oauth-client-credentials', - AUTH_OAUTH_JWT_BEARER = 'oauth-jwt-bearer'; +/** + * Enum for auth type values + * @enum {string} + */ +export const AuthType = { + USER_SUPPLIED: 'user-supplied', + USERNAME_PASSWORD: 'username-password', + OAUTH_CLIENT_CREDENTIALS: 'oauth-client-credentials', + OAUTH_JWT_BEARER: 'oauth-jwt-bearer' +}; -export default class Configuration { - static load() { - // Load config from .env file - dotenv.config(); - // Check mandatory variables - Configuration.#checkMandatoryVariables([ - 'SALESFORCE_AUTH_TYPE', - 'PUB_SUB_ENDPOINT' - ]); - // Check variable for specific auth types - if (Configuration.isUsernamePasswordAuth()) { - Configuration.#checkMandatoryVariables([ - 'SALESFORCE_LOGIN_URL', - 'SALESFORCE_USERNAME', - 'SALESFORCE_PASSWORD' - ]); - } else if (Configuration.isOAuthClientCredentialsAuth()) { - Configuration.#checkMandatoryVariables([ - 'SALESFORCE_LOGIN_URL', - 'SALESFORCE_CLIENT_ID', - 'SALESFORCE_CLIENT_SECRET' - ]); - } else if (Configuration.isOAuthJwtBearerAuth()) { - Configuration.#checkMandatoryVariables([ - 'SALESFORCE_LOGIN_URL', - 'SALESFORCE_CLIENT_ID', - 'SALESFORCE_USERNAME', - 'SALESFORCE_PRIVATE_KEY_FILE' - ]); - Configuration.getSfPrivateKey(); - } else if (!Configuration.isUserSuppliedAuth()) { - throw new Error( - `Invalid value for SALESFORCE_AUTH_TYPE environment variable: ${Configuration.getAuthType()}` - ); +export class Configuration { + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} the sanitized client configuration + */ + static load(config) { + // Set default pub sub endpoint if not specified + config.pubSubEndpoint = + config.pubSubEndpoint ?? DEFAULT_PUB_SUB_ENDPOINT; + // Check config for specific auth types + Configuration.#checkMandatoryVariables(config, ['authType']); + switch (config.authType) { + case AuthType.USER_SUPPLIED: + config = Configuration.#loadUserSuppliedAuth(config); + break; + case AuthType.USERNAME_PASSWORD: + Configuration.#checkMandatoryVariables(config, [ + 'loginUrl', + 'username', + 'password' + ]); + config.userToken = config.userToken ?? ''; + break; + case AuthType.OAUTH_CLIENT_CREDENTIALS: + Configuration.#checkMandatoryVariables(config, [ + 'loginUrl', + 'clientId', + 'clientSecret' + ]); + break; + case AuthType.OAUTH_JWT_BEARER: + Configuration.#checkMandatoryVariables(config, [ + 'loginUrl', + 'clientId', + 'username', + 'privateKey' + ]); + break; + default: + throw new Error( + `Unsupported authType value: ${config.authType}` + ); } + return config; } - static getAuthType() { - return process.env.SALESFORCE_AUTH_TYPE; - } - - static getSfLoginUrl() { - return process.env.SALESFORCE_LOGIN_URL; - } - - static getSfUsername() { - return process.env.SALESFORCE_USERNAME; - } - - static getSfSecuredPassword() { - if (process.env.SALESFORCE_TOKEN) { - return ( - process.env.SALESFORCE_PASSWORD + process.env.SALESFORCE_TOKEN + /** + * @param {Configuration} config the client configuration + * @returns {Configuration} sanitized configuration + */ + static #loadUserSuppliedAuth(config) { + Configuration.#checkMandatoryVariables(config, [ + 'accessToken', + 'instanceUrl' + ]); + // Check instance URL format + if (!config.instanceUrl.startsWith('https://')) { + throw new Error( + `Invalid Salesforce Instance URL format supplied: ${config.instanceUrl}` ); } - return process.env.SALESFORCE_PASSWORD; - } - - static getSfClientId() { - return process.env.SALESFORCE_CLIENT_ID; - } - - static getSfClientSecret() { - return process.env.SALESFORCE_CLIENT_SECRET; - } - - static getSfPrivateKey() { - try { - const keyPath = process.env.SALESFORCE_PRIVATE_KEY_FILE; - return fs.readFileSync(keyPath, 'utf8'); - } catch (error) { - throw new Error('Failed to load private key file', { - cause: error - }); + // Extract org ID from access token + if (!config.organizationId) { + try { + config.organizationId = config.accessToken.split('!').at(0); + } catch (error) { + throw new Error( + 'Unable to parse organizationId from access token', + { + cause: error + } + ); + } } + // Check org ID length + if ( + config.organizationId.length !== 15 && + config.organizationId.length !== 18 + ) { + throw new Error( + `Invalid Salesforce Org ID format supplied: ${config.organizationId}` + ); + } + return config; } - static getPubSubEndpoint() { - return process.env.PUB_SUB_ENDPOINT; - } - - static isUserSuppliedAuth() { - return Configuration.getAuthType() === AUTH_USER_SUPPLIED; - } - - static isUsernamePasswordAuth() { - return Configuration.getAuthType() === AUTH_USERNAME_PASSWORD; - } - - static isOAuthClientCredentialsAuth() { - return Configuration.getAuthType() === AUTH_OAUTH_CLIENT_CREDENTIALS; - } - - static isOAuthJwtBearerAuth() { - return Configuration.getAuthType() === AUTH_OAUTH_JWT_BEARER; - } - - static #checkMandatoryVariables(varNames) { + static #checkMandatoryVariables(config, varNames) { varNames.forEach((varName) => { - if (!process.env[varName]) { - throw new Error(`Missing ${varName} environment variable`); + if (!config[varName]) { + throw new Error( + `Missing value for ${varName} mandatory configuration key` + ); } }); } diff --git a/src/utils/eventParser.js b/src/utils/eventParser.js index 6164eca..4d6f0f1 100644 --- a/src/utils/eventParser.js +++ b/src/utils/eventParser.js @@ -173,6 +173,19 @@ export function encodeReplayId(replayId) { return buf; } +/** + * Safely serializes an event into a JSON string + * @param {any} event the event object + * @returns {string} a string holding the JSON respresentation of the event + * @protected + */ +export function toJsonString(event) { + return JSON.stringify(event, (key, value) => + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === 'bigint' ? value.toString() : value + ); +} + /** * Converts a hexadecimal string into a string binary representation * @param {string} hex diff --git a/src/utils/pubSubEventEmitter.js b/src/utils/pubSubEventEmitter.js deleted file mode 100644 index d8489a3..0000000 --- a/src/utils/pubSubEventEmitter.js +++ /dev/null @@ -1,80 +0,0 @@ -import { EventEmitter } from 'events'; - -/** - * EventEmitter wrapper for processing incoming Pub/Sub API events - * while keeping track of the topic name and the volume of events requested/received. - * @alias PubSubEventEmitter - * @global - */ -export default class PubSubEventEmitter extends EventEmitter { - #topicName; - #requestedEventCount; - #receivedEventCount; - #latestReplayId; - - /** - * Create a new EventEmitter for Pub/Sub API events - * @param {string} topicName - * @param {number} requestedEventCount - * @protected - */ - constructor(topicName, requestedEventCount) { - super(); - this.#topicName = topicName; - this.#requestedEventCount = requestedEventCount; - this.#receivedEventCount = 0; - this.#latestReplayId = null; - } - - emit(eventName, args) { - // Track Pub/Sub API events - if (eventName === 'data') { - this.#receivedEventCount++; - this.#latestReplayId = args.replayId; - } - return super.emit(eventName, args); - } - - /** - * Returns the number of events that were requested when subscribing. - * @returns {number} the number of events that were requested - */ - getRequestedEventCount() { - return this.#requestedEventCount; - } - - /** - * Returns the number of events that were received since subscribing. - * @returns {number} the number of events that were received - */ - getReceivedEventCount() { - return this.#receivedEventCount; - } - - /** - * Returns the topic name for this subscription. - * @returns {string} the topic name - */ - getTopicName() { - return this.#topicName; - } - - /** - * Returns the replay ID of the last processed event or null if no event was processed yet. - * @return {number} replay ID - */ - getLatestReplayId() { - return this.#latestReplayId; - } - - /** - * @protected - * Resets the requested/received event counts. - * This method should only be be used internally by the client when it resubscribes. - * @param {number} newRequestedEventCount - */ - _resetEventCount(newRequestedEventCount) { - this.#requestedEventCount = newRequestedEventCount; - this.#receivedEventCount = 0; - } -} diff --git a/v4-documentation.md b/v4-documentation.md new file mode 100644 index 0000000..1f6f266 --- /dev/null +++ b/v4-documentation.md @@ -0,0 +1,517 @@ +# Pub/Sub API Node Client - v4 Documentation + +> [!INFO] +> This documentation is kept to support a legacy version. Please consider upgrading to the latest version. + +- [Installation and Configuration](#installation-and-configuration) + - [User supplied authentication](#user-supplied-authentication) + - [Username/password flow](#usernamepassword-flow) + - [OAuth 2.0 client credentials flow (client_credentials)](#oauth-20-client-credentials-flow-client_credentials) + - [OAuth 2.0 JWT bearer flow](#oauth-20-jwt-bearer-flow) +- [Basic Example](#basic-example) +- [Other Examples](#other-examples) + - [Publish a platform event](#publish-a-platform-event) + - [Subscribe with a replay ID](#subscribe-with-a-replay-id) + - [Subscribe to past events in retention window](#subscribe-to-past-events-in-retention-window) + - [Work with flow control for high volumes of events](#work-with-flow-control-for-high-volumes-of-events) + - [Handle gRPC stream lifecycle events](#handle-grpc-stream-lifecycle-events) + - [Use a custom logger](#use-a-custom-logger) +- [Common Issues](#common-issues) +- [Reference](#reference) + - [PubSubApiClient](#pubsubapiclient) + - [PubSubEventEmitter](#pubsubeventemitter) + - [EventParseError](#eventparseerror) + +## Installation and Configuration + +Install the client library with `npm install salesforce-pubsub-api-client`. + +Create a `.env` file at the root of the project for configuration. + +Pick one of these authentication flows and fill the relevant configuration: + +- User supplied authentication +- Username/password authentication (recommended for tests) +- OAuth 2.0 client credentials +- OAuth 2.0 JWT Bearer (recommended for production) + +> [!TIP] +> The default client logger is fine for a test environment but you'll want to switch to a [custom logger](#use-a-custom-logger) with asynchronous logging for increased performance. + +### User supplied authentication + +If you already have a Salesforce client in your app, you can reuse its authentication information. You only need this minimal configuration: + +```properties +SALESFORCE_AUTH_TYPE=user-supplied + +PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +``` + +When connecting to the Pub/Sub API, use the following method instead of the standard `connect()` method to specify authentication information: + +```js +await client.connectWithAuth(accessToken, instanceUrl, organizationId); +``` + +### Username/password flow + +> [!WARNING] +> Relying on a username/password authentication flow for production is not recommended. Consider switching to JWT auth for extra security. + +```properties +SALESFORCE_AUTH_TYPE=username-password +SALESFORCE_LOGIN_URL=https://login.salesforce.com +SALESFORCE_USERNAME=YOUR_SALESFORCE_USERNAME +SALESFORCE_PASSWORD=YOUR_SALESFORCE_PASSWORD +SALESFORCE_TOKEN=YOUR_SALESFORCE_USER_SECURITY_TOKEN + +PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +``` + +### OAuth 2.0 client credentials flow (client_credentials) + +```properties +SALESFORCE_AUTH_TYPE=oauth-client-credentials +SALESFORCE_LOGIN_URL=YOUR_DOMAIN_URL +SALESFORCE_CLIENT_ID=YOUR_CONNECTED_APP_CLIENT_ID +SALESFORCE_CLIENT_SECRET=YOUR_CONNECTED_APP_CLIENT_SECRET + +PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +``` + +### OAuth 2.0 JWT bearer flow + +This is the most secure authentication option. Recommended for production use. + +```properties +SALESFORCE_AUTH_TYPE=oauth-jwt-bearer +SALESFORCE_LOGIN_URL=https://login.salesforce.com +SALESFORCE_CLIENT_ID=YOUR_CONNECTED_APP_CLIENT_ID +SALESFORCE_USERNAME=YOUR_SALESFORCE_USERNAME +SALESFORCE_PRIVATE_KEY_FILE=PATH_TO_YOUR_KEY_FILE + +PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +``` + +## Basic Example + +Here's an example that will get you started quickly. It listens to a single account change event. + +1. Activate Account change events in **Salesforce Setup > Change Data Capture**. + +1. Create a `sample.js` file with this content: + + ```js + import PubSubApiClient from 'salesforce-pubsub-api-client'; + + async function run() { + try { + const client = new PubSubApiClient(); + await client.connect(); + + // Subscribe to account change events + const eventEmitter = await client.subscribe( + '/data/AccountChangeEvent' + ); + + // Handle incoming events + eventEmitter.on('data', (event) => { + console.log( + `Handling ${event.payload.ChangeEventHeader.entityName} change event ` + + `with ID ${event.replayId} ` + + `on channel ${eventEmitter.getTopicName()} ` + + `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + + `events received so far)` + ); + // Safely log event as a JSON string + console.log( + JSON.stringify( + event, + (key, value) => + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === 'bigint' + ? value.toString() + : value, + 2 + ) + ); + }); + } catch (error) { + console.error(error); + } + } + + run(); + ``` + +1. Run the project with `node sample.js` + + If everything goes well, you'll see output like this: + + ``` + Connected to Salesforce org https://pozil-dev-ed.my.salesforce.com as grpc@pozil.com + Connected to Pub/Sub API endpoint api.pubsub.salesforce.com:7443 + Topic schema loaded: /data/AccountChangeEvent + Subscribe request sent for 100 events from /data/AccountChangeEvent... + ``` + + At this point the script will be on hold and will wait for events. + +1. Modify an account record in Salesforce. This fires an account change event. + + Once the client receives an event, it will display it like this: + + ``` + Received 1 events, latest replay ID: 18098167 + Handling Account change event with ID 18098167 on channel /data/AccountChangeEvent (1/100 events received so far) + { + "replayId": 18098167, + "payload": { + "ChangeEventHeader": { + "entityName": "Account", + "recordIds": [ + "0014H00002LbR7QQAV" + ], + "changeType": "UPDATE", + "changeOrigin": "com/salesforce/api/soap/58.0;client=SfdcInternalAPI/", + "transactionKey": "000046c7-a642-11e2-c29b-229c6786473e", + "sequenceNumber": 1, + "commitTimestamp": 1696444513000, + "commitNumber": 11657372702432, + "commitUser": "00558000000yFyDAAU", + "nulledFields": [], + "diffFields": [], + "changedFields": [ + "LastModifiedDate", + "BillingAddress.City", + "BillingAddress.State" + ] + }, + "Name": null, + "Type": null, + "ParentId": null, + "BillingAddress": { + "Street": null, + "City": "San Francisco", + "State": "CA", + "PostalCode": null, + "Country": null, + "StateCode": null, + "CountryCode": null, + "Latitude": null, + "Longitude": null, + "Xyz": null, + "GeocodeAccuracy": null + }, + "ShippingAddress": null, + "Phone": null, + "Fax": null, + "AccountNumber": null, + "Website": null, + "Sic": null, + "Industry": null, + "AnnualRevenue": null, + "NumberOfEmployees": null, + "Ownership": null, + "TickerSymbol": null, + "Description": null, + "Rating": null, + "Site": null, + "OwnerId": null, + "CreatedDate": null, + "CreatedById": null, + "LastModifiedDate": 1696444513000, + "LastModifiedById": null, + "Jigsaw": null, + "JigsawCompanyId": null, + "CleanStatus": null, + "AccountSource": null, + "DunsNumber": null, + "Tradestyle": null, + "NaicsCode": null, + "NaicsDesc": null, + "YearStarted": null, + "SicDesc": null, + "DandbCompanyId": null + } + } + ``` + + Note that the change event payloads include all object fields but fields that haven't changed are null. In the above example, the only changes are the Billing State, Billing City and Last Modified Date. + + Use the values from `ChangeEventHeader.nulledFields`, `ChangeEventHeader.diffFields` and `ChangeEventHeader.changedFields` to identify actual value changes. + +## Other Examples + +### Publish a platform event + +Publish a `Sample__e` Platform Event with a `Message__c` field: + +```js +const payload = { + CreatedDate: new Date().getTime(), // Non-null value required but there's no validity check performed on this field + CreatedById: '005_________', // Valid user ID + Message__c: { string: 'Hello world' } // Field is nullable so we need to specify the 'string' type +}; +const publishResult = await client.publish('/event/Sample__e', payload); +console.log('Published event: ', JSON.stringify(publishResult)); +``` + +### Subscribe with a replay ID + +Subscribe to 5 account change events starting from a replay ID: + +```js +const eventEmitter = await client.subscribeFromReplayId( + '/data/AccountChangeEvent', + 5, + 17092989 +); +``` + +### Subscribe to past events in retention window + +Subscribe to the 3 earliest past account change events in retention window: + +```js +const eventEmitter = await client.subscribeFromEarliestEvent( + '/data/AccountChangeEvent', + 3 +); +``` + +### Work with flow control for high volumes of events + +When working with high volumes of events you can control the incoming flow of events by requesting a limited batch of events. This event flow control ensures that the client doesn’t get overwhelmed by accepting more events that it can handle if there is a spike in event publishing. + +This is the overall process: + +1. Pass a number of requested events in your subscribe call. +1. Handle the `lastevent` event from `PubSubEventEmitter` to detect the end of the event batch. +1. Subscribe to an additional batch of events with `client.requestAdditionalEvents(...)`. If you don't request additional events at this point, the gRPC subscription will close automatically (default Pub/Sub API behavior). + +The code below illustrate how you can achieve event flow control: + +```js +try { + // Connect with the Pub/Sub API + const client = new PubSubApiClient(); + await client.connect(); + + // Subscribe to a batch of 10 account change event + const eventEmitter = await client.subscribe('/data/AccountChangeEvent', 10); + + // Handle incoming events + eventEmitter.on('data', (event) => { + // Logic for handling a single event. + // Unless you request additional events later, this should get called up to 10 times + // given the initial subscription boundary. + }); + + // Handle last requested event + eventEmitter.on('lastevent', () => { + console.log( + `Reached last requested event on channel ${eventEmitter.getTopicName()}.` + ); + // Request 10 additional events + client.requestAdditionalEvents(eventEmitter, 10); + }); +} catch (error) { + console.error(error); +} +``` + +### Handle gRPC stream lifecycle events + +Use the `EventEmmitter` returned by subscribe methods to handle gRPC stream lifecycle events: + +```js +// Stream end +eventEmitter.on('end', () => { + console.log('gRPC stream ended'); +}); + +// Stream error +eventEmitter.on('error', (error) => { + console.error('gRPC stream error: ', JSON.stringify(error)); +}); + +// Stream status update +eventEmitter.on('status', (status) => { + console.log('gRPC stream status: ', status); +}); +``` + +### Use a custom logger + +The client logs output to the console by default but you can provide your favorite logger in the client constructor. + +When in production, asynchronous logging is preferable for performance reasons. + +For example: + +```js +import pino from 'pino'; + +const logger = pino(); +const client = new PubSubApiClient(logger); +``` + +## Common Issues + +### TypeError: Do not know how to serialize a BigInt + +If you attempt to call `JSON.stringify` on an event you will likely see the following error: + +> TypeError: Do not know how to serialize a BigInt + +This happens when an integer value stored in an event field exceeds the range of the `Number` JS type (this typically happens with `commitNumber` values). In this case, we use a `BigInt` type to safely store the integer value. However, the `BigInt` type is not yet supported in standard JSON representation (see step 10 in the [BigInt TC39 spec](https://tc39.es/proposal-bigint/#sec-serializejsonproperty)) so this triggers a `TypeError`. + +To avoid this error, use a replacer function to safely escape BigInt values so that they can be serialized as a string (or any other format of your choice) in JSON: + +```js +// Safely log event as a JSON string +console.log( + JSON.stringify( + event, + (key, value) => + /* Convert BigInt values into strings and keep other types unchanged */ + typeof value === 'bigint' ? value.toString() : value, + 2 + ) +); +``` + +## Reference + +### PubSubApiClient + +Client for the Salesforce Pub/Sub API + +#### `PubSubApiClient([logger])` + +Builds a new Pub/Sub API client. + +| Name | Type | Description | +| -------- | ------ | ------------------------------------------------------------------------------- | +| `logger` | Logger | an optional custom logger. The client uses the console if no value is supplied. | + +#### `close()` + +Closes the gRPC connection. The client will no longer receive events for any topic. + +#### `async connect() → {Promise.}` + +Authenticates with Salesforce then, connects to the Pub/Sub API. + +Returns: Promise that resolves once the connection is established. + +#### `async connectWithAuth(accessToken, instanceUrl, organizationIdopt) → {Promise.}` + +Connects to the Pub/Sub API with user-supplied authentication. + +Returns: Promise that resolves once the connection is established. + +| Name | Type | Description | +| ---------------- | ------ | --------------------------------------------------------------------------------------------------- | +| `accessToken` | string | Salesforce access token | +| `instanceUrl` | string | Salesforce instance URL | +| `organizationId` | string | optional organization ID. If you don't provide one, we'll attempt to parse it from the accessToken. | + +#### `async getConnectivityState() → Promise}` + +Get connectivity state from current channel. + +Returns: Promise that holds channel's [connectivity state](https://grpc.github.io/grpc/node/grpc.html#.connectivityState). + +#### `async publish(topicName, payload, correlationKeyopt) → {Promise.}` + +Publishes a payload to a topic using the gRPC client. + +Returns: Promise holding a `PublishResult` object with `replayId` and `correlationKey`. + +| Name | Type | Description | +| ---------------- | ------ | ----------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `payload` | Object | | +| `correlationKey` | string | optional correlation key. If you don't provide one, we'll generate a random UUID for you. | + +#### `async subscribe(topicName, [numRequested]) → {Promise.}` + +Subscribes to a topic. + +Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | + +#### `async subscribeFromEarliestEvent(topicName, [numRequested]) → {Promise.}` + +Subscribes to a topic and retrieves all past events in retention window. + +Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. + +| Name | Type | Description | +| -------------- | ------ | -------------------------------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `numRequested` | number | optional number of events requested. If not supplied or null, the client keeps the subscription alive forever. | + +#### `async subscribeFromReplayId(topicName, numRequested, replayId) → {Promise.}` + +Subscribes to a topic and retrieves past events starting from a replay ID. + +Returns: Promise that holds an `PubSubEventEmitter` that allows you to listen to received events and stream lifecycle events. + +| Name | Type | Description | +| -------------- | ------ | --------------------------------------------------------------------------------------- | +| `topicName` | string | name of the topic that we're subscribing to | +| `numRequested` | number | number of events requested. If `null`, the client keeps the subscription alive forever. | +| `replayId` | number | replay ID | + +#### `requestAdditionalEvents(eventEmitter, numRequested)` + +Request additional events on an existing subscription. + +| Name | Type | Description | +| -------------- | ------------------ | ----------------------------------------------------------- | +| `eventEmitter` | PubSubEventEmitter | event emitter that was obtained in the first subscribe call | +| `numRequested` | number | number of events requested. | + +### PubSubEventEmitter + +EventEmitter wrapper for processing incoming Pub/Sub API events while keeping track of the topic name and the volume of events requested/received. + +The emitter sends the following events: + +| Event Name | Event Data | Description | +| ----------- | --------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| `data` | Object | Client received a new event. The attached data is the parsed event data. | +| `error` | `EventParseError \| Object` | Signals an event parsing error or a gRPC stream error. | +| `lastevent` | void | Signals that we received the last event that the client requested. The stream will end shortly. | +| `keepalive` | `{ latestReplayId: number, pendingNumRequested: number }` | Server publishes this keep alive message every 270 seconds (or less) if there are no events. | +| `end` | void | Signals the end of the gRPC stream. | +| `status` | Object | Misc gRPC stream status information. | + +The emitter also exposes these methods: + +| Method | Description | +| -------------------------- | ------------------------------------------------------------------------------------------ | +| `getRequestedEventCount()` | Returns the number of events that were requested when subscribing. | +| `getReceivedEventCount()` | Returns the number of events that were received since subscribing. | +| `getTopicName()` | Returns the topic name for this subscription. | +| `getLatestReplayId()` | Returns the replay ID of the last processed event or `null` if no event was processed yet. | + +### EventParseError + +Holds the information related to an event parsing error. This class attempts to extract the event replay ID from the event that caused the error. + +| Name | Type | Description | +| ---------------- | ------ | ------------------------------------------------------------------------------------------------------------------------------ | +| `message` | string | The error message. | +| `cause` | Error | The cause of the error. | +| `replayId` | number | The replay ID of the event at the origin of the error. Could be undefined if we're not able to extract it from the event data. | +| `event` | Object | The un-parsed event data at the origin of the error. | +| `latestReplayId` | number | The latest replay ID that was received before the error. |