From 483e70c3673862940d65eec1e26278bd562f081f Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sat, 28 Sep 2024 16:23:01 +0200 Subject: [PATCH 01/33] feat: replaced event emitter by callback --- dist/client.cjs | 583 ++++++++++++------------- dist/client.d.ts | 76 +++- dist/client.d.ts.map | 2 +- dist/client.js | 583 ++++++++++++------------- dist/utils/auth.d.ts | 27 +- dist/utils/auth.d.ts.map | 2 +- dist/utils/configuration.d.ts | 37 +- dist/utils/configuration.d.ts.map | 2 +- dist/utils/pubSubEventEmitter.d.ts | 46 -- dist/utils/pubSubEventEmitter.d.ts.map | 1 - package.json | 12 +- src/client.js | 345 +++++++++------ src/utils/auth.js | 96 ++-- src/utils/configuration.js | 192 ++++---- src/utils/pubSubEventEmitter.js | 80 ---- 15 files changed, 1001 insertions(+), 1083 deletions(-) delete mode 100644 dist/utils/pubSubEventEmitter.d.ts delete mode 100644 dist/utils/pubSubEventEmitter.d.ts.map delete mode 100644 src/utils/pubSubEventEmitter.js diff --git a/dist/client.cjs b/dist/client.cjs index 5b61111..22039b0 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); @@ -153,73 +153,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({ @@ -251,95 +184,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` + ); } }); } @@ -479,88 +411,103 @@ 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; + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + */ + constructor(config) { + this.#config = config; + } /** * 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() { + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + return null; + 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()}` @@ -568,7 +515,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}` } } @@ -593,8 +540,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} @@ -606,21 +566,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", { @@ -634,70 +599,24 @@ var PubSubApiClient = class { * @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); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} 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); + 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) ); @@ -706,9 +625,9 @@ var PubSubApiClient = class { 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); @@ -717,11 +636,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", { @@ -740,52 +659,61 @@ 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) { let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -807,30 +735,36 @@ 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); + let grpcSubscription = subscription?.grpcSubscription; if (!subscription) { - subscription = this.#client.Subscribe(); + 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}` ); + this.#logger.info(JSON.stringify(data.events)); for (const event of data.events) { try { let schema; @@ -854,9 +788,20 @@ var PubSubApiClient = class { ); } } + 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); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { let replayId; try { @@ -871,17 +816,24 @@ 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 + ); } } } @@ -890,27 +842,41 @@ var PubSubApiClient = class { `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"); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on("error", (error) => { + grpcSubscription.on("error", (error) => { this.#logger.error( `gRPC stream error: ${JSON.stringify(error)}` ); - eventEmitter.emit("error", error); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error + ); }); - subscription.on("status", (status) => { + grpcSubscription.on("status", (status) => { this.#logger.info( `gRPC stream status: ${JSON.stringify(status)}` ); - eventEmitter.emit("status", status); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status + ); }); - return eventEmitter; + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `Subscribe request sent for ${numRequested} events from ${topicName}...` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -920,18 +886,18 @@ 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 @@ -992,9 +958,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(); diff --git a/dist/client.d.ts b/dist/client.d.ts index 76f367f..0b8901d 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -6,24 +6,16 @@ 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. * @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 +25,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 +73,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 530322d..3aff746 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;IA2KD;;;;OAIG;IACH,sCAHW,kBAAkB,gBAClB,MAAM,iBAsBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CAwClC;IAED;;;OAGG;IACH,cASC;;CAiGJ;;cA/kBa,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":"AA4GA;;;;GAIG;AACH;IA+BI;;;;OAIG;IACH,oBAHW,aAAa,WACb,MAAM,EAehB;IAED;;;;OAIG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAgEzB;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;IAqND;;;;OAIG;IACH,mCAHW,MAAM,gBACN,MAAM,QAqBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CAwClC;IAED;;;OAGG;IACH,cAMC;;CAiGJ;;cAnpBa,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;;kCAzFc,eAAe;8BAKT,0BAA0B;;;;6BAUxD,MAAM;;;;;;;;;yBAVwB,0BAA0B"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js index 24eaa25..52161d9 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"; @@ -120,73 +120,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({ @@ -218,95 +151,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` + ); } }); } @@ -446,88 +378,103 @@ 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; + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + */ + constructor(config) { + this.#config = config; + } /** * 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() { + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + return null; + 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()}` @@ -535,7 +482,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}` } } @@ -560,8 +507,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} @@ -573,21 +533,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", { @@ -601,70 +566,24 @@ var PubSubApiClient = class { * @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); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} 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); + const rootCert = fs.readFileSync(certifi); const protoFilePath = fileURLToPath( new URL("./pubsub_api-be352429.proto?hash=be352429", import.meta.url) ); @@ -673,9 +592,9 @@ var PubSubApiClient = class { 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); @@ -684,11 +603,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", { @@ -707,52 +626,61 @@ 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) { let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -774,30 +702,36 @@ 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); + let grpcSubscription = subscription?.grpcSubscription; if (!subscription) { - subscription = this.#client.Subscribe(); + 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}` ); + this.#logger.info(JSON.stringify(data.events)); for (const event of data.events) { try { let schema; @@ -821,9 +755,20 @@ var PubSubApiClient = class { ); } } + 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); + subscribeCallback( + subscription2.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { let replayId; try { @@ -838,17 +783,24 @@ 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 + ); } } } @@ -857,27 +809,41 @@ var PubSubApiClient = class { `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"); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on("error", (error) => { + grpcSubscription.on("error", (error) => { this.#logger.error( `gRPC stream error: ${JSON.stringify(error)}` ); - eventEmitter.emit("error", error); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error + ); }); - subscription.on("status", (status) => { + grpcSubscription.on("status", (status) => { this.#logger.info( `gRPC stream status: ${JSON.stringify(status)}` ); - eventEmitter.emit("status", status); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status + ); }); - return eventEmitter; + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `Subscribe request sent for ${numRequested} events from ${topicName}...` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -887,18 +853,18 @@ 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 @@ -959,9 +925,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(); diff --git a/dist/utils/auth.d.ts b/dist/utils/auth.d.ts index ea75ebb..ddda340 100644 --- a/dist/utils/auth.d.ts +++ b/dist/utils/auth.d.ts @@ -7,31 +7,16 @@ */ 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 */ - static "__#4@#authWithUsernamePassword"(): ConnectionMetadata; + constructor(config: Configuration); /** - * 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..c1b6dfc 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;IAOI;;;OAGG;IACH,oBAFW,aAAa,EAIvB;IAED;;;OAGG;IACH,gBAFa,kBAAkB,CAiB9B;;CAyGJ;;iBAjJa,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/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/package.json b/package.json index 19acd13..e4086c6 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "prepublishOnly": "npm run build" }, "dependencies": { - "@grpc/grpc-js": "^1.11.2", + "@grpc/grpc-js": "^1.11.3", "@grpc/proto-loader": "^0.7.13", "avro-js": "^1.12.0", "certifi": "^14.5.15", @@ -32,12 +32,12 @@ }, "devDependencies": { "@chialab/esbuild-plugin-meta-url": "^0.18.2", - "eslint": "^9.10.0", - "husky": "^9.1.5", + "eslint": "^9.11.1", + "husky": "^9.1.6", "lint-staged": "^15.2.10", "prettier": "^3.3.3", - "tsup": "^8.2.4", - "typescript": "^5.5.4" + "tsup": "^8.3.0", + "typescript": "^5.6.2" }, "lint-staged": { "**/src/*.{css,html,js,json,md,yaml,yml}": [ @@ -62,6 +62,6 @@ "pubsub_api.proto" ], "volta": { - "node": "20.14.0" + "node": "20.17.0" } } diff --git a/src/client.js b/src/client.js index 41a96b4..b07a072 100644 --- a/src/client.js +++ b/src/client.js @@ -7,15 +7,12 @@ 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, @@ -23,6 +20,19 @@ import { } 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 +40,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 +91,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 +112,12 @@ const MAX_EVENT_BATCH_SIZE = 100; * @global */ export default class PubSubApiClient { + /** + * Client configuration + * @type {Configuration} + */ + #config; + /** * gRPC client * @type {Object} @@ -64,24 +131,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', { @@ -96,75 +168,25 @@ export default class PubSubApiClient { * @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); + const conMetadata = await auth.authenticate(); + this.#config.accessToken = conMetadata.accessToken; + this.#config.username = conMetadata.username; + this.#config.instanceUrl = conMetadata.instanceUrl; + this.#logger.info( + `Connected to Salesforce org ${conMetadata.instanceUrl} 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 { // Read certificates @@ -181,9 +203,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 +217,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 +242,73 @@ 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) { let { topicName, numRequested } = subscribeRequest; try { // Check number of requested events @@ -292,6 +332,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,25 +342,28 @@ export default class PubSubApiClient { // Check for an existing subscription let subscription = this.#subscriptions.get(topicName); + let grpcSubscription = subscription?.grpcSubscription; // Send subscription request if (!subscription) { - subscription = this.#client.Subscribe(); + 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}` @@ -354,10 +398,23 @@ export default class PubSubApiClient { ); } } + // 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); + subscribeCallback( + subscription.info, + SubscribeCallbackType.EVENT, + parsedEvent + ); } catch (error) { // Report event parsing error with replay ID if possible let replayId; @@ -375,24 +432,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 + ); } } } @@ -403,27 +467,42 @@ export default class PubSubApiClient { `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'); + subscribeCallback(subscription.info, SubscribeCallbackType.END); }); - subscription.on('error', (error) => { + grpcSubscription.on('error', (error) => { this.#logger.error( `gRPC stream error: ${JSON.stringify(error)}` ); - eventEmitter.emit('error', error); + subscribeCallback( + subscription.info, + SubscribeCallbackType.ERROR, + error + ); }); - subscription.on('status', (status) => { + grpcSubscription.on('status', (status) => { this.#logger.info( `gRPC stream status: ${JSON.stringify(status)}` ); - eventEmitter.emit('status', status); + subscribeCallback( + subscription.info, + SubscribeCallbackType.GRPC_STATUS, + status + ); }); - return eventEmitter; + + grpcSubscription.write(subscribeRequest); + this.#logger.info( + `Subscribe request sent for ${numRequested} events from ${topicName}...` + ); } catch (error) { throw new Error( `Failed to subscribe to events for topic ${topicName}`, @@ -434,12 +513,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) { @@ -449,7 +526,8 @@ export default class PubSubApiClient { } // Request additional events - eventEmitter._resetEventCount(numRequested); + subscription.receivedEventCount = 0; + subscription.requestedEventCount = numRequested; subscription.write({ topicName, numRequested: numRequested @@ -512,9 +590,6 @@ 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'); diff --git a/src/utils/auth.js b/src/utils/auth.js index 5dfd6f6..0c22860 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,38 @@ import Configuration from './configuration.js'; */ export default class SalesforceAuth { + /** + * Client configuration + * @type {Configuration} + */ + #config; + + /** + * Builds a new Pub/Sub API client + * @param {Configuration} config the client configuration + */ + constructor(config) { + this.#config = config; + } + /** * 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() { + switch (this.#config.authType) { + case AuthType.USER_SUPPLIED: + return null; // No op + 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 +51,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 +70,27 @@ 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; // 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 +98,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 +109,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 +129,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/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; - } -} From 63375854d284bc3c9fc95bcfb4d9b170e91a0d22 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sat, 28 Sep 2024 23:36:59 +0200 Subject: [PATCH 02/33] feat: port switch to ID only schema cache --- src/client.js | 78 ++++++++++------------------------------ src/utils/schemaCache.js | 42 ---------------------- 2 files changed, 19 insertions(+), 101 deletions(-) diff --git a/src/client.js b/src/client.js index b07a072..a127183 100644 --- a/src/client.js +++ b/src/client.js @@ -370,34 +370,10 @@ export default class PubSubApiClient { ); for (const event of data.events) { try { - let schema; - // Are we subscribing to a custom channel? - if (topicName.endsWith('__chn')) { - // Use schema ID instead of topic name to retrieve schema - schema = await this.#getEventSchemaFromId( - event.event.schemaId - ); - } else { - // Load event schema from cache or from the client - schema = - await this.#getEventSchemaFromTopicName( - topicName - ); - // Make sure that schema ID matches. If not, event fields may have changed - // and client needs to reload schema - if (schema.id !== event.event.schemaId) { - this.#logger.info( - `Event schema changed (${schema.id} != ${event.event.schemaId}), reloading: ${topicName}` - ); - this.#schemaChache.deleteWithTopicName( - topicName - ); - schema = - await this.#getEventSchemaFromTopicName( - topicName - ); - } - } + // Load event schema from cache or from the gRPC client + const schema = await this.#getEventSchemaFromId( + event.event.schemaId + ); // Retrieve subscription const subscription = this.#subscriptions.get(topicName); @@ -550,7 +526,8 @@ export default class PubSubApiClient { if (!this.#client) { throw new Error('Pub/Sub API client is not connected.'); } - const schema = await this.#getEventSchemaFromTopicName(topicName); + const schema = + await this.#fetchEventSchemaFromTopicNameWithClient(topicName); const id = correlationKey ? correlationKey : crypto.randomUUID(); const response = await new Promise((resolve, reject) => { @@ -596,31 +573,6 @@ export default class PubSubApiClient { this.#client.close(); } - /** - * Retrieves an event schema from the cache based on a topic name. - * If it's not cached, fetches the shema with the gRPC client. - * @param {string} topicName name of the topic that we're fetching - * @returns {Promise} Promise holding parsed event schema - */ - async #getEventSchemaFromTopicName(topicName) { - let schema = this.#schemaChache.getFromTopicName(topicName); - if (!schema) { - try { - schema = - await this.#fetchEventSchemaFromTopicNameWithClient( - topicName - ); - this.#schemaChache.setWithTopicName(topicName, schema); - } catch (error) { - throw new Error( - `Failed to load schema for topic ${topicName}`, - { cause: error } - ); - } - } - return schema; - } - /** * Retrieves an event schema from the cache based on its ID. * If it's not cached, fetches the shema with the gRPC client. @@ -649,6 +601,7 @@ export default class PubSubApiClient { */ async #fetchEventSchemaFromTopicNameWithClient(topicName) { return new Promise((resolve, reject) => { + // Query topic to obtain schema ID this.#client.GetTopic( { topicName }, async (topicError, response) => { @@ -657,12 +610,19 @@ export default class PubSubApiClient { } else { // Get the schema information const { schemaId } = response; - const schemaInfo = - await this.#fetchEventSchemaFromIdWithClient( - schemaId - ); + // Check cache for schema thanks to ID + let schema = this.#schemaChache.getFromId(schemaId); + if (!schema) { + // Fetch schema with gRPC client + schema = + await this.#fetchEventSchemaFromIdWithClient( + schemaId + ); + } this.#logger.info(`Topic schema loaded: ${topicName}`); - resolve(schemaInfo); + // Add schema to cache + this.#schemaChache.set(schema); + resolve(schema); } } ); diff --git a/src/utils/schemaCache.js b/src/utils/schemaCache.js index 515fb2a..4c4a5fa 100644 --- a/src/utils/schemaCache.js +++ b/src/utils/schemaCache.js @@ -12,15 +12,8 @@ export default class SchemaCache { */ #schemaChache; - /** - * Map of schemas IDs indexed by topic name - * @type {Map} - */ - #topicNameCache; - constructor() { this.#schemaChache = new Map(); - this.#topicNameCache = new Map(); } /** @@ -32,19 +25,6 @@ export default class SchemaCache { return this.#schemaChache.get(schemaId); } - /** - * Retrieves a schema based on a topic name - * @param {string} topicName - * @returns {Schema} schema or undefined if not found - */ - getFromTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - return this.getFromId(schemaId); - } - return undefined; - } - /** * Caches a schema * @param {Schema} schema @@ -52,26 +32,4 @@ export default class SchemaCache { set(schema) { this.#schemaChache.set(schema.id, schema); } - - /** - * Caches a schema with a topic name - * @param {string} topicName - * @param {Schema} schema - */ - setWithTopicName(topicName, schema) { - this.#topicNameCache.set(topicName, schema.id); - this.set(schema); - } - - /** - * Delete a schema based on the topic name - * @param {string} topicName - */ - deleteWithTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - this.#schemaChache.delete(schemaId); - } - this.#topicNameCache.delete(topicName); - } } From b487a8a2900abc4855c59b3a8bfd0f3469217f34 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Tue, 1 Oct 2024 14:45:54 +0200 Subject: [PATCH 03/33] feat: improved event debug log --- src/client.js | 13 ++++++++++--- src/utils/eventParser.js | 13 +++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/client.js b/src/client.js index a127183..6d4f80b 100644 --- a/src/client.js +++ b/src/client.js @@ -16,7 +16,8 @@ import { AuthType, Configuration } from './utils/configuration.js'; import { parseEvent, encodeReplayId, - decodeReplayId + decodeReplayId, + toJsonString } from './utils/eventParser.js'; import SalesforceAuth from './utils/auth.js'; @@ -163,7 +164,8 @@ 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 */ @@ -370,6 +372,9 @@ export default class PubSubApiClient { ); for (const event of data.events) { try { + this.#logger.debug( + `Raw event: ${toJsonString(event)}` + ); // Load event schema from cache or from the gRPC client const schema = await this.#getEventSchemaFromId( event.event.schemaId @@ -385,7 +390,9 @@ export default class PubSubApiClient { subscription.info.receivedEventCount++; // Parse event thanks to schema const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); + this.#logger.debug( + `Parsed event: ${toJsonString(parseEvent)}` + ); subscribeCallback( subscription.info, SubscribeCallbackType.EVENT, 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 From 48b2448228d6a56622f2ce3660b638fff1c4f560 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 11:48:30 +0200 Subject: [PATCH 04/33] feat: release v5.0.0 --- README.md | 485 +++++++++------- dist/client.cjs | 180 +++--- dist/client.d.ts | 3 +- dist/client.d.ts.map | 2 +- dist/client.js | 180 +++--- ...352429.proto => pubsub_api-07e1f84a.proto} | 66 ++- dist/utils/auth.d.ts | 3 +- dist/utils/auth.d.ts.map | 2 +- dist/utils/eventParser.d.ts | 7 + dist/utils/eventParser.d.ts.map | 2 +- dist/utils/schemaCache.d.ts | 17 - dist/utils/schemaCache.d.ts.map | 2 +- eslint.config.js | 5 + package.json | 17 +- pubsub_api.proto | 66 ++- spec/helper/reporter.js | 25 + spec/helper/sfUtility.js | 42 ++ spec/helper/simpleFileLogger.js | 43 ++ spec/integration/client.spec.js | 328 +++++++++++ spec/integration/clientFailures.spec.js | 111 ++++ spec/support/jasmine.json | 8 + src/client.js | 56 +- src/utils/auth.js | 15 +- v4-documentation.md | 517 ++++++++++++++++++ 24 files changed, 1681 insertions(+), 501 deletions(-) rename dist/{pubsub_api-be352429.proto => pubsub_api-07e1f84a.proto} (87%) create mode 100644 spec/helper/reporter.js create mode 100644 spec/helper/sfUtility.js create mode 100644 spec/helper/simpleFileLogger.js create mode 100644 spec/integration/client.spec.js create mode 100644 spec/integration/clientFailures.spec.js create mode 100644 spec/support/jasmine.json create mode 100644 v4-documentation.md diff --git a/README.md b/README.md index 22924a2..c0583d9 100644 --- a/README.md +++ b/README.md @@ -4,140 +4,217 @@ 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. -Create a `.env` file at the root of the project for configuration. +### Configuration and Connection -Pick one of these authentication flows and fill the relevant configuration: +In v4 and earlier versions of this client: +- you had to specify configuration in a `.env` file with specific property names. +- you connect with either the `connect()` or `connectWithAuth()` method depending on the authentication flow. -- User supplied authentication -- Username/password authentication (recommended for tests) -- OAuth 2.0 client credentials -- OAuth 2.0 JWT Bearer (recommended for production) +In v5: +- 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 used an asynchronous `EventEmitter` to receive updates such as incoming messages or lifecycle events. -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 +## Installation and Configuration -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 -``` +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) -When connecting to the Pub/Sub API, use the following method instead of the standard `connect()` method to specify authentication information: +#### 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 +}); +``` -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +#### OAuth 2.0 client credentials flow (client_credentials) + +```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. -```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 +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. -PUB_SUB_ENDPOINT=api.pubsub.salesforce.com:7443 +You can pass a logger like pino in the client constructor: + +```js +import pino from 'pino'; + +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 +228,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 +340,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 +350,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 +367,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 +375,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 +406,21 @@ try { ### 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: +Use callback types from subscribe callback to handle gRPC stream lifecycle events: ```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 +454,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 +469,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. - -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. +Authenticates with Salesforce then connects to the Pub/Sub API. 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 +491,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 +576,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 22039b0..646a3da 100644 --- a/dist/client.cjs +++ b/dist/client.cjs @@ -48,14 +48,8 @@ var SchemaCache = class { * @type {Map} */ #schemaChache; - /** - * Map of schemas IDs indexed by topic name - * @type {Map} - */ - #topicNameCache; constructor() { this.#schemaChache = /* @__PURE__ */ new Map(); - this.#topicNameCache = /* @__PURE__ */ new Map(); } /** * Retrieves a schema based on its ID @@ -65,18 +59,6 @@ var SchemaCache = class { getFromId(schemaId) { return this.#schemaChache.get(schemaId); } - /** - * Retrieves a schema based on a topic name - * @param {string} topicName - * @returns {Schema} schema or undefined if not found - */ - getFromTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - return this.getFromId(schemaId); - } - return void 0; - } /** * Caches a schema * @param {Schema} schema @@ -84,26 +66,6 @@ var SchemaCache = class { set(schema) { this.#schemaChache.set(schema.id, schema); } - /** - * Caches a schema with a topic name - * @param {string} topicName - * @param {Schema} schema - */ - setWithTopicName(topicName, schema) { - this.#topicNameCache.set(topicName, schema.id); - this.set(schema); - } - /** - * Delete a schema based on the topic name - * @param {string} topicName - */ - deleteWithTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - this.#schemaChache.delete(schemaId); - } - this.#topicNameCache.delete(topicName); - } }; // src/utils/eventParseError.js @@ -386,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"); @@ -417,21 +388,31 @@ var SalesforceAuth = class { * @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) { + constructor(config, logger) { this.#config = config; + this.#logger = logger; } /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); switch (this.#config.authType) { case AuthType.USER_SUPPLIED: - return null; + 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: @@ -594,20 +575,22 @@ 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 (this.#config.authType !== AuthType.USER_SUPPLIED) { try { - const auth = new SalesforceAuth(this.#config); + 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} as ${conMetadata.username}` + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); } catch (error) { throw new Error("Failed to authenticate with Salesforce", { @@ -616,9 +599,10 @@ var PubSubApiClient = class { } } try { + 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); @@ -714,6 +698,9 @@ var PubSubApiClient = class { * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -742,8 +729,18 @@ var PubSubApiClient = class { throw new Error("Pub/Sub API client is not connected."); } let subscription = this.#subscriptions.get(topicName); - let grpcSubscription = subscription?.grpcSubscription; - if (!subscription) { + 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: { @@ -762,32 +759,19 @@ var PubSubApiClient = class { 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}` ); - this.#logger.info(JSON.stringify(data.events)); for (const event of data.events) { try { - let schema; - if (topicName.endsWith("__chn")) { - schema = await this.#getEventSchemaFromId( - event.event.schemaId - ); - } else { - schema = await this.#getEventSchemaFromTopicName( - topicName - ); - if (schema.id !== event.event.schemaId) { - this.#logger.info( - `Event schema changed (${schema.id} != ${event.event.schemaId}), reloading: ${topicName}` - ); - this.#schemaChache.deleteWithTopicName( - topicName - ); - schema = await this.#getEventSchemaFromTopicName( - topicName - ); - } - } + 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( @@ -796,7 +780,9 @@ var PubSubApiClient = class { } subscription2.info.receivedEventCount++; const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); + this.#logger.debug( + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` + ); subscribeCallback( subscription2.info, SubscribeCallbackType.EVENT, @@ -839,7 +825,7 @@ var PubSubApiClient = class { } } else { this.#logger.debug( - `Received keepalive message. Latest replay ID: ${latestReplayId}` + `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` ); data.latestReplayId = latestReplayId; subscribeCallback( @@ -850,12 +836,12 @@ var PubSubApiClient = class { }); grpcSubscription.on("end", () => { this.#subscriptions.delete(topicName); - this.#logger.info("gRPC stream ended"); + this.#logger.info(`${topicName} - gRPC stream ended`); subscribeCallback(subscription.info, SubscribeCallbackType.END); }); grpcSubscription.on("error", (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` ); subscribeCallback( subscription.info, @@ -865,7 +851,7 @@ var PubSubApiClient = class { }); grpcSubscription.on("status", (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` ); subscribeCallback( subscription.info, @@ -875,7 +861,7 @@ var PubSubApiClient = class { }); grpcSubscription.write(subscribeRequest); this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` + `${topicName} - Subscribe request sent for ${numRequested} events` ); } catch (error) { throw new Error( @@ -903,7 +889,7 @@ var PubSubApiClient = class { numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } /** @@ -916,10 +902,13 @@ 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."); } - const schema = await this.#getEventSchemaFromTopicName(topicName); + const schema = await this.#fetchEventSchemaFromTopicNameWithClient(topicName); const id = correlationKey ? correlationKey : import_crypto2.default.randomUUID(); const response = await new Promise((resolve, reject) => { this.#client.Publish( @@ -962,29 +951,6 @@ var PubSubApiClient = class { this.#logger.info("Closing gRPC stream"); this.#client.close(); } - /** - * Retrieves an event schema from the cache based on a topic name. - * If it's not cached, fetches the shema with the gRPC client. - * @param {string} topicName name of the topic that we're fetching - * @returns {Promise} Promise holding parsed event schema - */ - async #getEventSchemaFromTopicName(topicName) { - let schema = this.#schemaChache.getFromTopicName(topicName); - if (!schema) { - try { - schema = await this.#fetchEventSchemaFromTopicNameWithClient( - topicName - ); - this.#schemaChache.setWithTopicName(topicName, schema); - } catch (error) { - throw new Error( - `Failed to load schema for topic ${topicName}`, - { cause: error } - ); - } - } - return schema; - } /** * Retrieves an event schema from the cache based on its ID. * If it's not cached, fetches the shema with the gRPC client. @@ -1019,11 +985,17 @@ var PubSubApiClient = class { reject(topicError); } else { const { schemaId } = response; - const schemaInfo = await this.#fetchEventSchemaFromIdWithClient( - schemaId + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${schemaId}` ); - this.#logger.info(`Topic schema loaded: ${topicName}`); - resolve(schemaInfo); + let schema = this.#schemaChache.getFromId(schemaId); + if (!schema) { + schema = await this.#fetchEventSchemaFromIdWithClient( + schemaId + ); + } + this.#schemaChache.set(schema); + resolve(schema); } } ); diff --git a/dist/client.d.ts b/dist/client.d.ts index 0b8901d..839e9ee 100644 --- a/dist/client.d.ts +++ b/dist/client.d.ts @@ -11,7 +11,8 @@ export default class PubSubApiClient { */ 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 */ diff --git a/dist/client.d.ts.map b/dist/client.d.ts.map index 3aff746..e27b830 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":"AA4GA;;;;GAIG;AACH;IA+BI;;;;OAIG;IACH,oBAHW,aAAa,WACb,MAAM,EAehB;IAED;;;;OAIG;IACH,WAHa,OAAO,CAAC,IAAI,CAAC,CAgEzB;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;IAqND;;;;OAIG;IACH,mCAHW,MAAM,gBACN,MAAM,QAqBhB;IAED;;;;;;;OAOG;IACH,mBANW,MAAM,iCAEN,MAAM,GACJ,OAAO,CAAC,aAAa,CAAC,CAwClC;IAED;;;OAGG;IACH,cAMC;;CAiGJ;;cAnpBa,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;;kCAzFc,eAAe;8BAKT,0BAA0B;;;;6BAUxD,MAAM;;;;;;;;;yBAVwB,0BAA0B"} \ 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"} \ No newline at end of file diff --git a/dist/client.js b/dist/client.js index 52161d9..61fdf0a 100644 --- a/dist/client.js +++ b/dist/client.js @@ -15,14 +15,8 @@ var SchemaCache = class { * @type {Map} */ #schemaChache; - /** - * Map of schemas IDs indexed by topic name - * @type {Map} - */ - #topicNameCache; constructor() { this.#schemaChache = /* @__PURE__ */ new Map(); - this.#topicNameCache = /* @__PURE__ */ new Map(); } /** * Retrieves a schema based on its ID @@ -32,18 +26,6 @@ var SchemaCache = class { getFromId(schemaId) { return this.#schemaChache.get(schemaId); } - /** - * Retrieves a schema based on a topic name - * @param {string} topicName - * @returns {Schema} schema or undefined if not found - */ - getFromTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - return this.getFromId(schemaId); - } - return void 0; - } /** * Caches a schema * @param {Schema} schema @@ -51,26 +33,6 @@ var SchemaCache = class { set(schema) { this.#schemaChache.set(schema.id, schema); } - /** - * Caches a schema with a topic name - * @param {string} topicName - * @param {Schema} schema - */ - setWithTopicName(topicName, schema) { - this.#topicNameCache.set(topicName, schema.id); - this.set(schema); - } - /** - * Delete a schema based on the topic name - * @param {string} topicName - */ - deleteWithTopicName(topicName) { - const schemaId = this.#topicNameCache.get(topicName); - if (schemaId) { - this.#schemaChache.delete(schemaId); - } - this.#topicNameCache.delete(topicName); - } }; // src/utils/eventParseError.js @@ -353,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"); @@ -384,21 +355,31 @@ var SalesforceAuth = class { * @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) { + constructor(config, logger) { this.#config = config; + this.#logger = logger; } /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} */ async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); switch (this.#config.authType) { case AuthType.USER_SUPPLIED: - return null; + 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: @@ -561,20 +542,22 @@ 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 (this.#config.authType !== AuthType.USER_SUPPLIED) { try { - const auth = new SalesforceAuth(this.#config); + 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} as ${conMetadata.username}` + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); } catch (error) { throw new Error("Failed to authenticate with Salesforce", { @@ -583,9 +566,10 @@ var PubSubApiClient = class { } } try { + 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); @@ -681,6 +665,9 @@ var PubSubApiClient = class { * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { let isInfiniteEventRequest = false; @@ -709,8 +696,18 @@ var PubSubApiClient = class { throw new Error("Pub/Sub API client is not connected."); } let subscription = this.#subscriptions.get(topicName); - let grpcSubscription = subscription?.grpcSubscription; - if (!subscription) { + 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: { @@ -729,32 +726,19 @@ var PubSubApiClient = class { 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}` ); - this.#logger.info(JSON.stringify(data.events)); for (const event of data.events) { try { - let schema; - if (topicName.endsWith("__chn")) { - schema = await this.#getEventSchemaFromId( - event.event.schemaId - ); - } else { - schema = await this.#getEventSchemaFromTopicName( - topicName - ); - if (schema.id !== event.event.schemaId) { - this.#logger.info( - `Event schema changed (${schema.id} != ${event.event.schemaId}), reloading: ${topicName}` - ); - this.#schemaChache.deleteWithTopicName( - topicName - ); - schema = await this.#getEventSchemaFromTopicName( - topicName - ); - } - } + 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( @@ -763,7 +747,9 @@ var PubSubApiClient = class { } subscription2.info.receivedEventCount++; const parsedEvent = parseEvent(schema, event); - this.#logger.debug(parsedEvent); + this.#logger.debug( + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` + ); subscribeCallback( subscription2.info, SubscribeCallbackType.EVENT, @@ -806,7 +792,7 @@ var PubSubApiClient = class { } } else { this.#logger.debug( - `Received keepalive message. Latest replay ID: ${latestReplayId}` + `${topicName} - Received keepalive message. Latest replay ID: ${latestReplayId}` ); data.latestReplayId = latestReplayId; subscribeCallback( @@ -817,12 +803,12 @@ var PubSubApiClient = class { }); grpcSubscription.on("end", () => { this.#subscriptions.delete(topicName); - this.#logger.info("gRPC stream ended"); + this.#logger.info(`${topicName} - gRPC stream ended`); subscribeCallback(subscription.info, SubscribeCallbackType.END); }); grpcSubscription.on("error", (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` ); subscribeCallback( subscription.info, @@ -832,7 +818,7 @@ var PubSubApiClient = class { }); grpcSubscription.on("status", (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` ); subscribeCallback( subscription.info, @@ -842,7 +828,7 @@ var PubSubApiClient = class { }); grpcSubscription.write(subscribeRequest); this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` + `${topicName} - Subscribe request sent for ${numRequested} events` ); } catch (error) { throw new Error( @@ -870,7 +856,7 @@ var PubSubApiClient = class { numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } /** @@ -883,10 +869,13 @@ 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."); } - const schema = await this.#getEventSchemaFromTopicName(topicName); + const schema = await this.#fetchEventSchemaFromTopicNameWithClient(topicName); const id = correlationKey ? correlationKey : crypto2.randomUUID(); const response = await new Promise((resolve, reject) => { this.#client.Publish( @@ -929,29 +918,6 @@ var PubSubApiClient = class { this.#logger.info("Closing gRPC stream"); this.#client.close(); } - /** - * Retrieves an event schema from the cache based on a topic name. - * If it's not cached, fetches the shema with the gRPC client. - * @param {string} topicName name of the topic that we're fetching - * @returns {Promise} Promise holding parsed event schema - */ - async #getEventSchemaFromTopicName(topicName) { - let schema = this.#schemaChache.getFromTopicName(topicName); - if (!schema) { - try { - schema = await this.#fetchEventSchemaFromTopicNameWithClient( - topicName - ); - this.#schemaChache.setWithTopicName(topicName, schema); - } catch (error) { - throw new Error( - `Failed to load schema for topic ${topicName}`, - { cause: error } - ); - } - } - return schema; - } /** * Retrieves an event schema from the cache based on its ID. * If it's not cached, fetches the shema with the gRPC client. @@ -986,11 +952,17 @@ var PubSubApiClient = class { reject(topicError); } else { const { schemaId } = response; - const schemaInfo = await this.#fetchEventSchemaFromIdWithClient( - schemaId + this.#logger.debug( + `${topicName} - Retrieving schema ID: ${schemaId}` ); - this.#logger.info(`Topic schema loaded: ${topicName}`); - resolve(schemaInfo); + let schema = this.#schemaChache.getFromId(schemaId); + if (!schema) { + schema = await this.#fetchEventSchemaFromIdWithClient( + schemaId + ); + } + 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 ddda340..66f3ed0 100644 --- a/dist/utils/auth.d.ts +++ b/dist/utils/auth.d.ts @@ -9,8 +9,9 @@ export default class SalesforceAuth { /** * Builds a new Pub/Sub API client * @param {Configuration} config the client configuration + * @param {Logger} logger a logger */ - constructor(config: Configuration); + constructor(config: Configuration, logger: Logger); /** * Authenticates with the auth mode specified in configuration * @returns {ConnectionMetadata} diff --git a/dist/utils/auth.d.ts.map b/dist/utils/auth.d.ts.map index c1b6dfc..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;IAOI;;;OAGG;IACH,oBAFW,aAAa,EAIvB;IAED;;;OAGG;IACH,gBAFa,kBAAkB,CAiB9B;;CAyGJ;;iBAjJa,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/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/schemaCache.d.ts b/dist/utils/schemaCache.d.ts index e0366f0..aebf86f 100644 --- a/dist/utils/schemaCache.d.ts +++ b/dist/utils/schemaCache.d.ts @@ -11,28 +11,11 @@ export default class SchemaCache { * @returns {Schema} schema or undefined if not found */ getFromId(schemaId: string): Schema; - /** - * Retrieves a schema based on a topic name - * @param {string} topicName - * @returns {Schema} schema or undefined if not found - */ - getFromTopicName(topicName: string): Schema; /** * Caches a schema * @param {Schema} schema */ set(schema: Schema): void; - /** - * Caches a schema with a topic name - * @param {string} topicName - * @param {Schema} schema - */ - setWithTopicName(topicName: string, schema: Schema): void; - /** - * Delete a schema based on the topic name - * @param {string} topicName - */ - deleteWithTopicName(topicName: string): void; #private; } export type Schema = { diff --git a/dist/utils/schemaCache.d.ts.map b/dist/utils/schemaCache.d.ts.map index a6be072..c20fa4c 100644 --- a/dist/utils/schemaCache.d.ts.map +++ b/dist/utils/schemaCache.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"schemaCache.d.ts","sourceRoot":"","sources":["../../src/utils/schemaCache.js"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;IAkBI;;;;OAIG;IACH,oBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;;OAIG;IACH,4BAHW,MAAM,GACJ,MAAM,CAQlB;IAED;;;OAGG;IACH,YAFW,MAAM,QAIhB;IAED;;;;OAIG;IACH,4BAHW,MAAM,UACN,MAAM,QAKhB;IAED;;;OAGG;IACH,+BAFW,MAAM,QAQhB;;CACJ;;QA1Ea,MAAM"} \ No newline at end of file +{"version":3,"file":"schemaCache.d.ts","sourceRoot":"","sources":["../../src/utils/schemaCache.js"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH;IAWI;;;;OAIG;IACH,oBAHW,MAAM,GACJ,MAAM,CAIlB;IAED;;;OAGG;IACH,YAFW,MAAM,QAIhB;;CACJ;;QAhCa,MAAM"} \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js index a7edcf7..bbd90d7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,5 @@ import js from '@eslint/js'; +import jasmine from 'eslint-plugin-jasmine'; import globals from 'globals'; export default [ @@ -10,5 +11,9 @@ export default [ ...globals.node } } + }, + { + files: ['spec/**/*.js'], + plugins: [jasmine] } ]; diff --git a/package.json b/package.json index e4086c6..11f6852 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salesforce-pubsub-api-client", - "version": "4.1.3", + "version": "5.0.0", "type": "module", "description": "A node client for the Salesforce Pub/Sub API", "author": "pozil", @@ -15,6 +15,7 @@ }, "scripts": { "build": "tsup && tsc", + "test": "jasmine", "prettier": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", "lint": "eslint src", "prepare": "husky || true", @@ -22,22 +23,24 @@ "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.1", - "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}": [ 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/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..a9ead4e --- /dev/null +++ b/spec/integration/client.spec.js @@ -0,0 +1,328 @@ +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'; + +// 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'; +const CHANGE_EVENT_TOPIC = '/data/AccountChangeEvent'; + +async function sleep(duration) { + return new Promise((resolve) => setTimeout(() => resolve(), duration)); +} + +async function waitFor(timeoutDuration, checkFunction) { + return new Promise((resolve) => { + let checkInterval; + const waitTimeout = setTimeout(() => { + clearInterval(checkInterval); + reject(); + }, timeoutDuration); + checkInterval = setInterval(() => { + if (checkFunction()) { + clearTimeout(waitTimeout); + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); +} + +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); + }); + + 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(3000, () => 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 + ); + }); + + 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 sleep(1000); + + // 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); + }); + + 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 sleep(1000); + + // 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); + }); + + it('supports JWT OAuth flow with platform event', async function () { + let receivedEvent, receivedSub; + + // Read private key + const privateKey = fs.readFileSync( + process.env.SALESFORCE_PRIVATE_KEY_FILE + ); + + // 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 sleep(1000); + + // 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); + }); +}); diff --git a/spec/integration/clientFailures.spec.js b/spec/integration/clientFailures.spec.js new file mode 100644 index 0000000..cd8c837 --- /dev/null +++ b/spec/integration/clientFailures.spec.js @@ -0,0 +1,111 @@ +import * as dotenv from 'dotenv'; +import jsforce from 'jsforce'; +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'; + +// 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'; + +async function sleep(duration) { + return new Promise((resolve) => setTimeout(() => resolve(), duration)); +} + +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 + await sleep(500); + + // 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 + await sleep(1000); + + // 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 6d4f80b..7072cac 100644 --- a/src/client.js +++ b/src/client.js @@ -174,13 +174,14 @@ export default class PubSubApiClient { if (this.#config.authType !== AuthType.USER_SUPPLIED) { // Connect to Salesforce to obtain an access token try { - const auth = new SalesforceAuth(this.#config); + 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} as ${conMetadata.username}` + `Connected to Salesforce org ${conMetadata.instanceUrl} (${this.#config.organizationId}) as ${conMetadata.username}` ); } catch (error) { throw new Error('Failed to authenticate with Salesforce', { @@ -191,6 +192,7 @@ export default class PubSubApiClient { // Connect to Pub/Sub API try { + this.#logger.debug(`Connecting to Pub/Sub API`); // Read certificates const rootCert = fs.readFileSync(certifi); @@ -311,6 +313,9 @@ export default class PubSubApiClient { * @param {SubscribeCallback} subscribeCallback callback function for handling subscription events */ #subscribe(subscribeRequest, subscribeCallback) { + this.#logger.debug( + `Preparing subscribe request: ${JSON.stringify(subscribeRequest)}` + ); let { topicName, numRequested } = subscribeRequest; try { // Check number of requested events @@ -344,10 +349,21 @@ export default class PubSubApiClient { // Check for an existing subscription let subscription = this.#subscriptions.get(topicName); - let grpcSubscription = subscription?.grpcSubscription; - - // Send subscription request - if (!subscription) { + 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: { @@ -368,14 +384,17 @@ export default class PubSubApiClient { 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( - `Raw event: ${toJsonString(event)}` + `${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 ); @@ -391,7 +410,7 @@ export default class PubSubApiClient { // Parse event thanks to schema const parsedEvent = parseEvent(schema, event); this.#logger.debug( - `Parsed event: ${toJsonString(parseEvent)}` + `${topicName} - Parsed event: ${toJsonString(parsedEvent)}` ); subscribeCallback( subscription.info, @@ -447,7 +466,7 @@ 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 subscribeCallback( @@ -458,12 +477,12 @@ export default class PubSubApiClient { }); grpcSubscription.on('end', () => { this.#subscriptions.delete(topicName); - this.#logger.info('gRPC stream ended'); + this.#logger.info(`${topicName} - gRPC stream ended`); subscribeCallback(subscription.info, SubscribeCallbackType.END); }); grpcSubscription.on('error', (error) => { this.#logger.error( - `gRPC stream error: ${JSON.stringify(error)}` + `${topicName} - gRPC stream error: ${JSON.stringify(error)}` ); subscribeCallback( subscription.info, @@ -473,7 +492,7 @@ export default class PubSubApiClient { }); grpcSubscription.on('status', (status) => { this.#logger.info( - `gRPC stream status: ${JSON.stringify(status)}` + `${topicName} - gRPC stream status: ${JSON.stringify(status)}` ); subscribeCallback( subscription.info, @@ -484,7 +503,7 @@ export default class PubSubApiClient { grpcSubscription.write(subscribeRequest); this.#logger.info( - `Subscribe request sent for ${numRequested} events from ${topicName}...` + `${topicName} - Subscribe request sent for ${numRequested} events` ); } catch (error) { throw new Error( @@ -516,7 +535,7 @@ export default class PubSubApiClient { numRequested: numRequested }); this.#logger.debug( - `Resubscribing to a batch of ${numRequested} events for: ${topicName}` + `${topicName} - Resubscribing to a batch of ${numRequested} events` ); } @@ -530,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.'); } @@ -617,6 +639,9 @@ export default class PubSubApiClient { } 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) { @@ -626,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 0c22860..fa282ba 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -18,12 +18,20 @@ export default class SalesforceAuth { */ #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) { + constructor(config, logger) { this.#config = config; + this.#logger = logger; } /** @@ -31,9 +39,12 @@ export default class SalesforceAuth { * @returns {ConnectionMetadata} */ async authenticate() { + this.#logger.debug(`Authenticating with ${this.#config.authType} mode`); switch (this.#config.authType) { case AuthType.USER_SUPPLIED: - return null; // No op + 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: 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. | From c5ff1a926ec8af109564a9288b4850a2e34eebd7 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 13:08:48 +0200 Subject: [PATCH 05/33] fix: broken linting --- eslint.config.js | 14 +++++++++----- package.json | 7 ++++--- spec/integration/client.spec.js | 2 +- spec/integration/clientFailures.spec.js | 1 - 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/eslint.config.js b/eslint.config.js index bbd90d7..775d7c6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -4,16 +4,20 @@ 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' } - }, - { - files: ['spec/**/*.js'], - plugins: [jasmine] } ]; diff --git a/package.json b/package.json index 11f6852..ce2561c 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,9 @@ "scripts": { "build": "tsup && tsc", "test": "jasmine", - "prettier": "prettier --write '**/*.{css,html,js,json,md,yaml,yml}'", - "lint": "eslint src", + "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" @@ -46,7 +47,7 @@ "**/src/*.{css,html,js,json,md,yaml,yml}": [ "prettier --write" ], - "**/src/**/*.js": [ + "**/{src,spec}/**/*.js": [ "eslint" ] }, diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index a9ead4e..2c5d4f4 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -31,7 +31,7 @@ async function sleep(duration) { } async function waitFor(timeoutDuration, checkFunction) { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { let checkInterval; const waitTimeout = setTimeout(() => { clearInterval(checkInterval); diff --git a/spec/integration/clientFailures.spec.js b/spec/integration/clientFailures.spec.js index cd8c837..660f483 100644 --- a/spec/integration/clientFailures.spec.js +++ b/spec/integration/clientFailures.spec.js @@ -1,5 +1,4 @@ import * as dotenv from 'dotenv'; -import jsforce from 'jsforce'; import PubSubApiClient from '../../src/client.js'; import { AuthType } from '../../src/utils/configuration.js'; import SimpleFileLogger from '../helper/simpleFileLogger.js'; From 0cad4778deca28189692bd1358c225bcd9b8cb4e Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 13:10:59 +0200 Subject: [PATCH 06/33] ci: initial build --- .github/workflows/ci-pr.yml | 49 +++++++ .github/workflows/ci.yml | 50 ++++++++ README.md | 10 +- sample-v4.js | 217 +++++++++++++++++++++++++++++++ sample-v5.js | 250 ++++++++++++++++++++++++++++++++++++ 5 files changed, 572 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/ci-pr.yml create mode 100644 .github/workflows/ci.yml create mode 100644 sample-v4.js create mode 100644 sample-v5.js diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml new file mode 100644 index 0000000..ca8b97f --- /dev/null +++ b/.github/workflows/ci-pr.yml @@ -0,0 +1,49 @@ +# 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 + 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 + + # Integration tests + - name: 'Integration tests' + run: npm run test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..294dabd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,50 @@ +# 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 + 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 + + # Integration tests + - name: 'Integration tests' + run: npm run test diff --git a/README.md b/README.md index c0583d9..bbd47ab 100644 --- a/README.md +++ b/README.md @@ -36,12 +36,14 @@ See the [official Pub/Sub API repo](https://github.com/developerforce/pub-sub-ap ### Configuration and Connection In v4 and earlier versions of this client: -- you had to specify configuration in a `.env` file with specific property names. -- you connect with either the `connect()` or `connectWithAuth()` method depending on the authentication flow. + +- you had to specify configuration in a `.env` file with specific property names. +- you connect with either the `connect()` or `connectWithAuth()` method depending on the authentication flow. In v5: -- 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. + +- 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. ### Event handling diff --git a/sample-v4.js b/sample-v4.js new file mode 100644 index 0000000..32d1d2f --- /dev/null +++ b/sample-v4.js @@ -0,0 +1,217 @@ +import PubSubApiClient from './src/client.js'; +import jsforce from 'jsforce'; + +process.on('uncaughtException', (exception) => { + console.error('uncaughtException: ', exception); +}); +process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason); +}); + +const CHANGE_FIELD_NAME_SHORT = 'BillingCity'; +const CHANGE_FIELD_NAME_FULL = 'BillingAddress.City'; +const CHANGE_FIELD_VALUE = 'SFO' + Math.random(); + +async function run() { + try { + const client = new PubSubApiClient(); + await client.connect(); + + // Subscribe to account change events + const eventEmitter = await client.subscribe( + //'/data/AccountChangeEvent', + '/event/Sample__e', + 1 + ); + + // Handle incoming events + eventEmitter.on('data', (event) => { + if (event.payload?.ChangeEventHeader) { + // Change event + console.log( + `Handling ${event.payload.ChangeEventHeader.entityName} change event ${event.replayId}` + ); + } else { + // Platform event + console.log( + `Handling platform event ` + + `with ID ${event.replayId} ` + + `on channel ${eventEmitter.getTopicName()}` + ); + } + + // 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 + ) + ); + }); + + // Publish PE + await publishPlatformEvent(client); + } catch (error) { + console.error(error); + } +} + +/* +async function run() { + try { + // Test for user supplied auth + //const sfConnection = await connectWithUserSuppliedAuth(); + const client = new PubSubApiClient(); + + // Connect + await client.connect(); + + //await subscribeToChangeEventFromReplayId(client, subscribeCallback, 2202810, 1); + //await subscribeToChangeEventFromRetentionWindow(client, subscribeCallback, 5); + //await subscribeToChangeEvent(client, subscribeCallback, 1); + await subscribeToPlatformEvent(client, subscribeCallback, 1); + + + console.log('Wait for subscription...'); + await new Promise((resolve) => setTimeout(() => resolve(), 1000)); + console.log('Resuming.'); + + //await updateRecord(sfConnection); + await publishPlatformEvent(client); + } catch (error) { + console.error(error); + } +} + */ + +async function connectWithUserSuppliedAuth() { + console.log('Connect with jsForce'); + const sfConnection = new jsforce.Connection({ + loginUrl: process.env.loginUrl + }); + await sfConnection.login( + process.env.username, + process.env.userToken + ? process.env.password + process.env.userToken + : process.env.password + ); + return sfConnection; +} + +async function subscribeToPlatformEventWithReplayId(client, replayId) { + // Subscribe to platform events + console.log('Subscribing...'); + const eventEmitter = await client.subscribeFromReplayId( + '/event/Sample__e', + null, + replayId + ); + console.log('Subscribed.'); + // Handle events + eventEmitter.on('data', (event) => { + console.log( + `Handling event ` + + `with ID ${event.replayId} ` + + `on channel ${eventEmitter.getTopicName()} ` + + `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + + `events received so far)` + ); + console.log( + JSON.stringify( + event, + (key, value) => + typeof value === 'bigint' ? value.toString() : value, // return everything else unchanged + 2 + ) + ); + }); + return eventEmitter; +} + +async function subscribeToPlatformEvent( + client, + subscribeCallback, + eventCount = 1 +) { + console.log(`Subscribing to ${eventCount} platform events.`); + await client.subscribe('/event/Sample__e', subscribeCallback, eventCount); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEvent( + client, + subscribeCallback, + eventCount = 1 +) { + console.log(`Subscribing to ${eventCount} change events.`); + await client.subscribe( + //'/data/AccountChangeEvent', + '/data/Account_Channel__chn', + subscribeCallback, + eventCount + ); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEventFromRetentionWindow( + client, + subscribeCallback, + eventCount = 1 +) { + console.log( + `Subscribing to ${eventCount} change events from retention window.` + ); + await client.subscribeFromEarliestEvent( + '/data/AccountChangeEvent', + subscribeCallback, + eventCount + ); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEventFromReplayId( + client, + subscribeCallback, + replayId, + eventCount = 1 +) { + console.log( + `Subscribing to ${eventCount} change events from replay ID ${replayId}.` + ); + await client.subscribeFromReplayId( + '/data/AccountChangeEvent', + subscribeCallback, + eventCount, + replayId + ); + console.log(`Subscribed.`); +} + +async function updateRecord(sfConnection) { + console.log('Updating record...'); + const newValues = { + Id: '0014H00002LbR7QQAV' + }; + newValues[CHANGE_FIELD_NAME_SHORT] = CHANGE_FIELD_VALUE; + sfConnection.sobject('Account').update(newValues, (err, ret) => { + if (err || !ret.success) { + return console.error(err, ret); + } + console.log('Record updated'); + }); +} + +async function publishPlatformEvent(client) { + 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('/event/Sample__e', payload); + console.log('Published platform event: ', JSON.stringify(publishResult)); +} + +run(); diff --git a/sample-v5.js b/sample-v5.js new file mode 100644 index 0000000..64e9f36 --- /dev/null +++ b/sample-v5.js @@ -0,0 +1,250 @@ +import * as dotenv from 'dotenv'; +import fs from 'fs'; + +import PubSubApiClient from './src/client.js'; +import jsforce from 'jsforce'; +import { AuthType } from './src/utils/configuration.js'; + +process.on('uncaughtException', (exception) => { + console.error('uncaughtException: ', exception); +}); +process.on('unhandledRejection', (reason, p) => { + console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason); +}); + +const CHANGE_FIELD_NAME_SHORT = 'BillingCity'; +const CHANGE_FIELD_NAME_FULL = 'BillingAddress.City'; +const CHANGE_FIELD_VALUE = 'SFO' + Math.random(); + +async function run() { + try { + // Load config from .env file + dotenv.config(); + + // Test for user supplied auth + const sfConnection = await connectWithUserSuppliedAuth(); + const client = new PubSubApiClient({ + authType: AuthType.USER_SUPPLIED, + accessToken: sfConnection.accessToken, + instanceUrl: sfConnection.instanceUrl, + organizationId: sfConnection.userInfo.organizationId + }); + + // Connect + await client.connect(); + + const subscribeCallback = (subscription, callbackType, data) => { + switch (callbackType) { + case 'event': + if (data.payload?.ChangeEventHeader) { + // Change event + console.log( + `Handling ${data.payload.ChangeEventHeader.entityName} change event ${data.replayId}` + ); + console.log(JSON.stringify(data, null, 2)); + + const { changedFields } = + data.payload.ChangeEventHeader; + if (!changedFields.includes(CHANGE_FIELD_NAME_FULL)) { + console.error( + `TEST FAILED: expected to find ${CHANGE_FIELD_NAME_FULL} in ${changedFields}` + ); + } else { + console.log('TEST SUCCESS'); + } + } else { + // Platform event + console.log( + `Handling platform event ` + + `with ID ${data.replayId} ` + + `on channel ${subscription.topicName} ` + + `(${subscription.receivedEventCount}/${subscription.requestedEventCount} ` + + `events received so far)` + ); + console.log( + JSON.stringify( + data, + (key, value) => + typeof value === 'bigint' + ? value.toString() + : value, // return everything else unchanged + 2 + ) + ); + } + break; + case 'lastEvent': + console.log( + `Reached last requested event on channel ${subscription.topicName}. Closing connection.` + ); + client.close(); + break; + case 'end': + console.log('Shut down gracefully.'); + process.exit(0); + break; + case 'error': + console.log( + 'Received gRPC error: ' + data, + JSON.stringify(data) + ); + break; + case 'grpcStatus': + console.log('Received gRPC status: ', JSON.stringify(data)); + break; + case 'grpcKeepAlive': + console.log('Received subscription keepalive.'); + break; + default: + console.log( + `Unsupported event callback type: ${callbackType}`, + JSON.stringify(data) + ); + break; + } + }; + + //await subscribeToChangeEventFromReplayId(client, subscribeCallback, 2202810, 1); + //await subscribeToChangeEventFromRetentionWindow(client, subscribeCallback, 5); + //await subscribeToChangeEvent(client, subscribeCallback, 1); + await subscribeToPlatformEvent(client, subscribeCallback, 1); + + console.log('Wait for subscription...'); + await new Promise((resolve) => setTimeout(() => resolve(), 1000)); + console.log('Resuming.'); + + //await updateRecord(sfConnection); + await publishPlatformEvent(client); + } catch (error) { + console.error(error); + } +} + +async function connectWithUserSuppliedAuth() { + console.log('Connect with jsForce'); + const 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; +} + +async function subscribeToPlatformEventWithReplayId(client, replayId) { + // Subscribe to platform events + console.log('Subscribing...'); + const eventEmitter = await client.subscribeFromReplayId( + '/event/Sample__e', + null, + replayId + ); + console.log('Subscribed.'); + // Handle events + eventEmitter.on('data', (event) => { + console.log( + `Handling event ` + + `with ID ${event.replayId} ` + + `on channel ${eventEmitter.getTopicName()} ` + + `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + + `events received so far)` + ); + console.log( + JSON.stringify( + event, + (key, value) => + typeof value === 'bigint' ? value.toString() : value, // return everything else unchanged + 2 + ) + ); + }); + return eventEmitter; +} + +async function subscribeToPlatformEvent( + client, + subscribeCallback, + eventCount = 1 +) { + console.log(`Subscribing to ${eventCount} platform events.`); + await client.subscribe('/event/Sample__e', subscribeCallback, eventCount); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEvent( + client, + subscribeCallback, + eventCount = 1 +) { + console.log(`Subscribing to ${eventCount} change events.`); + await client.subscribe( + //'/data/AccountChangeEvent', + '/data/Account_Channel__chn', + subscribeCallback, + eventCount + ); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEventFromRetentionWindow( + client, + subscribeCallback, + eventCount = 1 +) { + console.log( + `Subscribing to ${eventCount} change events from retention window.` + ); + await client.subscribeFromEarliestEvent( + '/data/AccountChangeEvent', + subscribeCallback, + eventCount + ); + console.log(`Subscribed.`); +} + +async function subscribeToChangeEventFromReplayId( + client, + subscribeCallback, + replayId, + eventCount = 1 +) { + console.log( + `Subscribing to ${eventCount} change events from replay ID ${replayId}.` + ); + await client.subscribeFromReplayId( + '/data/AccountChangeEvent', + subscribeCallback, + eventCount, + replayId + ); + console.log(`Subscribed.`); +} + +async function updateRecord(sfConnection) { + console.log('Updating record...'); + const newValues = { + Id: '0014H00002LbR7QQAV' + }; + newValues[CHANGE_FIELD_NAME_SHORT] = CHANGE_FIELD_VALUE; + sfConnection.sobject('Account').update(newValues, (err, ret) => { + if (err || !ret.success) { + return console.error(err, ret); + } + console.log('Record updated'); + }); +} + +async function publishPlatformEvent(client) { + 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('/event/Sample__e', payload); + console.log('Published platform event: ', JSON.stringify(publishResult)); +} + +run(); From 9e1ed16ae54ac1c1a490a63966f15c9b89d4e0cd Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 13:20:07 +0200 Subject: [PATCH 07/33] ci: tweat test for CI config --- spec/integration/client.spec.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 2c5d4f4..7aa599b 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -274,9 +274,14 @@ describe('Client', function () { let receivedEvent, receivedSub; // Read private key - const privateKey = fs.readFileSync( - process.env.SALESFORCE_PRIVATE_KEY_FILE - ); + let privateKey; + if (process.env.SALESFORCE_PRIVATE_KEY) { + privateKey = process.env.SALESFORCE_PRIVATE_KEY; + } else { + privateKey = fs.readFileSync( + process.env.SALESFORCE_PRIVATE_KEY_PATH + ); + } // Build PubSub client client = new PubSubApiClient( From 5db31538c59bc241bcf71cf1368c8ed8128895e2 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 13:21:47 +0200 Subject: [PATCH 08/33] build: add package lock to enable CI build --- .gitignore | 1 - package-lock.json | 4187 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 4187 insertions(+), 1 deletion(-) create mode 100644 package-lock.json diff --git a/.gitignore b/.gitignore index 8384bbe..afdc551 100644 --- a/.gitignore +++ b/.gitignore @@ -13,7 +13,6 @@ node_modules jsconfig.json .vscode .idea -package-lock.json # MacOS system files .DS_Store 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" + } + } + } +} From 83f5c58c5f625578133914f40bed4f9e8b03e348 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 14:36:53 +0200 Subject: [PATCH 09/33] ci: config --- .github/workflows/ci-pr.yml | 21 +++++++++++++++++++++ .github/workflows/ci.yml | 21 +++++++++++++++++++++ spec/integration/client.spec.js | 11 +++-------- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index ca8b97f..eab7964 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -44,6 +44,27 @@ jobs: - name: 'Lint' run: npm run lint + # Configure test env + - name: 'Configure test environment' + run: | + touch .env + echo SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }} >> .env + echo SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }} >> .env + echo SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }} >> .env + echo SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }} >> .env + echo SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }} >> .env + echo SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }} >> .env + echo SALESFORCE_PRIVATE_KEY_PATH="server.key" >> .env + echo SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }} >> .env + echo SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }} >> .env + touch server.key + echo ${{ secrets.SALESFORCE_PRIVATE_KEY }} >> server.key + # Integration tests - name: 'Integration tests' run: npm run test + + # Housekeeping + - name: 'Delete test environment configuration' + if: always() + run: rm .env server.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 294dabd..d98a1a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,6 +45,27 @@ jobs: - name: 'Lint' run: npm run lint + # Configure test env + - name: 'Configure test environment' + run: | + touch .env + echo SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }} >> .env + echo SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }} >> .env + echo SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }} >> .env + echo SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }} >> .env + echo SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }} >> .env + echo SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }} >> .env + echo SALESFORCE_PRIVATE_KEY_PATH="server.key" >> .env + echo SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }} >> .env + echo SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }} >> .env + touch server.key + echo ${{ secrets.SALESFORCE_PRIVATE_KEY }} >> server.key + # Integration tests - name: 'Integration tests' run: npm run test + + # Housekeeping + - name: 'Delete test environment configuration' + if: always() + run: rm .env server.key diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 7aa599b..9ce0a25 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -274,14 +274,9 @@ describe('Client', function () { let receivedEvent, receivedSub; // Read private key - let privateKey; - if (process.env.SALESFORCE_PRIVATE_KEY) { - privateKey = process.env.SALESFORCE_PRIVATE_KEY; - } else { - privateKey = fs.readFileSync( - process.env.SALESFORCE_PRIVATE_KEY_PATH - ); - } + const privateKey = fs.readFileSync( + process.env.SALESFORCE_PRIVATE_KEY_PATH + ); // Build PubSub client client = new PubSubApiClient( From 821f7b16a80a7fd6bb5ef54024c9943f887233f1 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 14:46:49 +0200 Subject: [PATCH 10/33] ci: config --- .github/workflows/ci-pr.yml | 20 +-- .github/workflows/ci.yml | 20 +-- .gitignore | 2 +- sample-v4.js | 217 ------------------------------- sample-v5.js | 250 ------------------------------------ 5 files changed, 21 insertions(+), 488 deletions(-) delete mode 100644 sample-v4.js delete mode 100644 sample-v5.js diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index eab7964..ae8cbff 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -48,17 +48,17 @@ jobs: - name: 'Configure test environment' run: | touch .env - echo SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }} >> .env - echo SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }} >> .env - echo SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }} >> .env - echo SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }} >> .env - echo SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }} >> .env - echo SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }} >> .env - echo SALESFORCE_PRIVATE_KEY_PATH="server.key" >> .env - echo SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }} >> .env - echo SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }} >> .env + echo "SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }}" >> .env + echo "SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }}" >> .env + echo "SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }}" >> .env + echo "SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }}" >> .env + echo "SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }}" >> .env + echo "SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }}" >> .env + echo "SALESFORCE_PRIVATE_KEY_PATH=server.key" >> .env + echo "SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }}" >> .env + echo "SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }}" >> .env touch server.key - echo ${{ secrets.SALESFORCE_PRIVATE_KEY }} >> server.key + echo "${{ secrets.SALESFORCE_PRIVATE_KEY }}" >> server.key # Integration tests - name: 'Integration tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d98a1a6..20b6bc6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,17 +49,17 @@ jobs: - name: 'Configure test environment' run: | touch .env - echo SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }} >> .env - echo SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }} >> .env - echo SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }} >> .env - echo SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }} >> .env - echo SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }} >> .env - echo SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }} >> .env - echo SALESFORCE_PRIVATE_KEY_PATH="server.key" >> .env - echo SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }} >> .env - echo SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }} >> .env + echo "SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }}" >> .env + echo "SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }}" >> .env + echo "SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }}" >> .env + echo "SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }}" >> .env + echo "SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }}" >> .env + echo "SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }}" >> .env + echo "SALESFORCE_PRIVATE_KEY_PATH=server.key" >> .env + echo "SALESFORCE_JWT_LOGIN_URL=${{ secrets.SALESFORCE_JWT_LOGIN_URL }}" >> .env + echo "SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }}" >> .env touch server.key - echo ${{ secrets.SALESFORCE_PRIVATE_KEY }} >> server.key + echo "${{ secrets.SALESFORCE_PRIVATE_KEY }}" >> server.key # Integration tests - name: 'Integration tests' diff --git a/.gitignore b/.gitignore index afdc551..2858896 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ ehthumbs.db $RECYCLE.BIN/ # Sample code -sample.js +sample* \ No newline at end of file diff --git a/sample-v4.js b/sample-v4.js deleted file mode 100644 index 32d1d2f..0000000 --- a/sample-v4.js +++ /dev/null @@ -1,217 +0,0 @@ -import PubSubApiClient from './src/client.js'; -import jsforce from 'jsforce'; - -process.on('uncaughtException', (exception) => { - console.error('uncaughtException: ', exception); -}); -process.on('unhandledRejection', (reason, p) => { - console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason); -}); - -const CHANGE_FIELD_NAME_SHORT = 'BillingCity'; -const CHANGE_FIELD_NAME_FULL = 'BillingAddress.City'; -const CHANGE_FIELD_VALUE = 'SFO' + Math.random(); - -async function run() { - try { - const client = new PubSubApiClient(); - await client.connect(); - - // Subscribe to account change events - const eventEmitter = await client.subscribe( - //'/data/AccountChangeEvent', - '/event/Sample__e', - 1 - ); - - // Handle incoming events - eventEmitter.on('data', (event) => { - if (event.payload?.ChangeEventHeader) { - // Change event - console.log( - `Handling ${event.payload.ChangeEventHeader.entityName} change event ${event.replayId}` - ); - } else { - // Platform event - console.log( - `Handling platform event ` + - `with ID ${event.replayId} ` + - `on channel ${eventEmitter.getTopicName()}` - ); - } - - // 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 - ) - ); - }); - - // Publish PE - await publishPlatformEvent(client); - } catch (error) { - console.error(error); - } -} - -/* -async function run() { - try { - // Test for user supplied auth - //const sfConnection = await connectWithUserSuppliedAuth(); - const client = new PubSubApiClient(); - - // Connect - await client.connect(); - - //await subscribeToChangeEventFromReplayId(client, subscribeCallback, 2202810, 1); - //await subscribeToChangeEventFromRetentionWindow(client, subscribeCallback, 5); - //await subscribeToChangeEvent(client, subscribeCallback, 1); - await subscribeToPlatformEvent(client, subscribeCallback, 1); - - - console.log('Wait for subscription...'); - await new Promise((resolve) => setTimeout(() => resolve(), 1000)); - console.log('Resuming.'); - - //await updateRecord(sfConnection); - await publishPlatformEvent(client); - } catch (error) { - console.error(error); - } -} - */ - -async function connectWithUserSuppliedAuth() { - console.log('Connect with jsForce'); - const sfConnection = new jsforce.Connection({ - loginUrl: process.env.loginUrl - }); - await sfConnection.login( - process.env.username, - process.env.userToken - ? process.env.password + process.env.userToken - : process.env.password - ); - return sfConnection; -} - -async function subscribeToPlatformEventWithReplayId(client, replayId) { - // Subscribe to platform events - console.log('Subscribing...'); - const eventEmitter = await client.subscribeFromReplayId( - '/event/Sample__e', - null, - replayId - ); - console.log('Subscribed.'); - // Handle events - eventEmitter.on('data', (event) => { - console.log( - `Handling event ` + - `with ID ${event.replayId} ` + - `on channel ${eventEmitter.getTopicName()} ` + - `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + - `events received so far)` - ); - console.log( - JSON.stringify( - event, - (key, value) => - typeof value === 'bigint' ? value.toString() : value, // return everything else unchanged - 2 - ) - ); - }); - return eventEmitter; -} - -async function subscribeToPlatformEvent( - client, - subscribeCallback, - eventCount = 1 -) { - console.log(`Subscribing to ${eventCount} platform events.`); - await client.subscribe('/event/Sample__e', subscribeCallback, eventCount); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEvent( - client, - subscribeCallback, - eventCount = 1 -) { - console.log(`Subscribing to ${eventCount} change events.`); - await client.subscribe( - //'/data/AccountChangeEvent', - '/data/Account_Channel__chn', - subscribeCallback, - eventCount - ); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEventFromRetentionWindow( - client, - subscribeCallback, - eventCount = 1 -) { - console.log( - `Subscribing to ${eventCount} change events from retention window.` - ); - await client.subscribeFromEarliestEvent( - '/data/AccountChangeEvent', - subscribeCallback, - eventCount - ); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEventFromReplayId( - client, - subscribeCallback, - replayId, - eventCount = 1 -) { - console.log( - `Subscribing to ${eventCount} change events from replay ID ${replayId}.` - ); - await client.subscribeFromReplayId( - '/data/AccountChangeEvent', - subscribeCallback, - eventCount, - replayId - ); - console.log(`Subscribed.`); -} - -async function updateRecord(sfConnection) { - console.log('Updating record...'); - const newValues = { - Id: '0014H00002LbR7QQAV' - }; - newValues[CHANGE_FIELD_NAME_SHORT] = CHANGE_FIELD_VALUE; - sfConnection.sobject('Account').update(newValues, (err, ret) => { - if (err || !ret.success) { - return console.error(err, ret); - } - console.log('Record updated'); - }); -} - -async function publishPlatformEvent(client) { - 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('/event/Sample__e', payload); - console.log('Published platform event: ', JSON.stringify(publishResult)); -} - -run(); diff --git a/sample-v5.js b/sample-v5.js deleted file mode 100644 index 64e9f36..0000000 --- a/sample-v5.js +++ /dev/null @@ -1,250 +0,0 @@ -import * as dotenv from 'dotenv'; -import fs from 'fs'; - -import PubSubApiClient from './src/client.js'; -import jsforce from 'jsforce'; -import { AuthType } from './src/utils/configuration.js'; - -process.on('uncaughtException', (exception) => { - console.error('uncaughtException: ', exception); -}); -process.on('unhandledRejection', (reason, p) => { - console.error('Unhandled Rejection at: Promise ', p, ' reason: ', reason); -}); - -const CHANGE_FIELD_NAME_SHORT = 'BillingCity'; -const CHANGE_FIELD_NAME_FULL = 'BillingAddress.City'; -const CHANGE_FIELD_VALUE = 'SFO' + Math.random(); - -async function run() { - try { - // Load config from .env file - dotenv.config(); - - // Test for user supplied auth - const sfConnection = await connectWithUserSuppliedAuth(); - const client = new PubSubApiClient({ - authType: AuthType.USER_SUPPLIED, - accessToken: sfConnection.accessToken, - instanceUrl: sfConnection.instanceUrl, - organizationId: sfConnection.userInfo.organizationId - }); - - // Connect - await client.connect(); - - const subscribeCallback = (subscription, callbackType, data) => { - switch (callbackType) { - case 'event': - if (data.payload?.ChangeEventHeader) { - // Change event - console.log( - `Handling ${data.payload.ChangeEventHeader.entityName} change event ${data.replayId}` - ); - console.log(JSON.stringify(data, null, 2)); - - const { changedFields } = - data.payload.ChangeEventHeader; - if (!changedFields.includes(CHANGE_FIELD_NAME_FULL)) { - console.error( - `TEST FAILED: expected to find ${CHANGE_FIELD_NAME_FULL} in ${changedFields}` - ); - } else { - console.log('TEST SUCCESS'); - } - } else { - // Platform event - console.log( - `Handling platform event ` + - `with ID ${data.replayId} ` + - `on channel ${subscription.topicName} ` + - `(${subscription.receivedEventCount}/${subscription.requestedEventCount} ` + - `events received so far)` - ); - console.log( - JSON.stringify( - data, - (key, value) => - typeof value === 'bigint' - ? value.toString() - : value, // return everything else unchanged - 2 - ) - ); - } - break; - case 'lastEvent': - console.log( - `Reached last requested event on channel ${subscription.topicName}. Closing connection.` - ); - client.close(); - break; - case 'end': - console.log('Shut down gracefully.'); - process.exit(0); - break; - case 'error': - console.log( - 'Received gRPC error: ' + data, - JSON.stringify(data) - ); - break; - case 'grpcStatus': - console.log('Received gRPC status: ', JSON.stringify(data)); - break; - case 'grpcKeepAlive': - console.log('Received subscription keepalive.'); - break; - default: - console.log( - `Unsupported event callback type: ${callbackType}`, - JSON.stringify(data) - ); - break; - } - }; - - //await subscribeToChangeEventFromReplayId(client, subscribeCallback, 2202810, 1); - //await subscribeToChangeEventFromRetentionWindow(client, subscribeCallback, 5); - //await subscribeToChangeEvent(client, subscribeCallback, 1); - await subscribeToPlatformEvent(client, subscribeCallback, 1); - - console.log('Wait for subscription...'); - await new Promise((resolve) => setTimeout(() => resolve(), 1000)); - console.log('Resuming.'); - - //await updateRecord(sfConnection); - await publishPlatformEvent(client); - } catch (error) { - console.error(error); - } -} - -async function connectWithUserSuppliedAuth() { - console.log('Connect with jsForce'); - const 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; -} - -async function subscribeToPlatformEventWithReplayId(client, replayId) { - // Subscribe to platform events - console.log('Subscribing...'); - const eventEmitter = await client.subscribeFromReplayId( - '/event/Sample__e', - null, - replayId - ); - console.log('Subscribed.'); - // Handle events - eventEmitter.on('data', (event) => { - console.log( - `Handling event ` + - `with ID ${event.replayId} ` + - `on channel ${eventEmitter.getTopicName()} ` + - `(${eventEmitter.getReceivedEventCount()}/${eventEmitter.getRequestedEventCount()} ` + - `events received so far)` - ); - console.log( - JSON.stringify( - event, - (key, value) => - typeof value === 'bigint' ? value.toString() : value, // return everything else unchanged - 2 - ) - ); - }); - return eventEmitter; -} - -async function subscribeToPlatformEvent( - client, - subscribeCallback, - eventCount = 1 -) { - console.log(`Subscribing to ${eventCount} platform events.`); - await client.subscribe('/event/Sample__e', subscribeCallback, eventCount); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEvent( - client, - subscribeCallback, - eventCount = 1 -) { - console.log(`Subscribing to ${eventCount} change events.`); - await client.subscribe( - //'/data/AccountChangeEvent', - '/data/Account_Channel__chn', - subscribeCallback, - eventCount - ); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEventFromRetentionWindow( - client, - subscribeCallback, - eventCount = 1 -) { - console.log( - `Subscribing to ${eventCount} change events from retention window.` - ); - await client.subscribeFromEarliestEvent( - '/data/AccountChangeEvent', - subscribeCallback, - eventCount - ); - console.log(`Subscribed.`); -} - -async function subscribeToChangeEventFromReplayId( - client, - subscribeCallback, - replayId, - eventCount = 1 -) { - console.log( - `Subscribing to ${eventCount} change events from replay ID ${replayId}.` - ); - await client.subscribeFromReplayId( - '/data/AccountChangeEvent', - subscribeCallback, - eventCount, - replayId - ); - console.log(`Subscribed.`); -} - -async function updateRecord(sfConnection) { - console.log('Updating record...'); - const newValues = { - Id: '0014H00002LbR7QQAV' - }; - newValues[CHANGE_FIELD_NAME_SHORT] = CHANGE_FIELD_VALUE; - sfConnection.sobject('Account').update(newValues, (err, ret) => { - if (err || !ret.success) { - return console.error(err, ret); - } - console.log('Record updated'); - }); -} - -async function publishPlatformEvent(client) { - 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('/event/Sample__e', payload); - console.log('Published platform event: ', JSON.stringify(publishResult)); -} - -run(); From 00266506b0b89d2818f643be86fe32e3e5d59b1e Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 14:54:38 +0200 Subject: [PATCH 11/33] ci: config --- .github/workflows/ci-pr.yml | 27 ++++++++++++++++++--------- .github/workflows/ci.yml | 27 ++++++++++++++++++--------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index ae8cbff..833662d 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -48,17 +48,26 @@ jobs: - name: 'Configure test environment' run: | touch .env - echo "SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }}" >> .env - echo "SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }}" >> .env - echo "SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }}" >> .env - echo "SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }}" >> .env - echo "SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }}" >> .env - echo "SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }}" >> .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=${{ secrets.SALESFORCE_JWT_LOGIN_URL }}" >> .env - echo "SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }}" >> .env + echo "SALESFORCE_JWT_LOGIN_URL=$SALESFORCE_JWT_LOGIN_URL" >> .env + echo "SALESFORCE_JWT_CLIENT_ID=$SALESFORCE_JWT_CLIENT_ID" >> .env touch server.key - echo "${{ secrets.SALESFORCE_PRIVATE_KEY }}" >> server.key + echo "$SALESFORCE_PRIVATE_KEY" >> server.key + 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 }} # Integration tests - name: 'Integration tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20b6bc6..7bb4215 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,17 +49,26 @@ jobs: - name: 'Configure test environment' run: | touch .env - echo "SALESFORCE_LOGIN_URL=${{ secrets.SALESFORCE_LOGIN_URL }}" >> .env - echo "SALESFORCE_USERNAME=${{ secrets.SALESFORCE_USERNAME }}" >> .env - echo "SALESFORCE_PASSWORD=${{ secrets.SALESFORCE_PASSWORD }}" >> .env - echo "SALESFORCE_TOKEN=${{ secrets.SALESFORCE_TOKEN }}" >> .env - echo "SALESFORCE_CLIENT_ID=${{ secrets.SALESFORCE_CLIENT_ID }}" >> .env - echo "SALESFORCE_CLIENT_SECRET=${{ secrets.SALESFORCE_CLIENT_SECRET }}" >> .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=${{ secrets.SALESFORCE_JWT_LOGIN_URL }}" >> .env - echo "SALESFORCE_JWT_CLIENT_ID=${{ secrets.SALESFORCE_JWT_CLIENT_ID }}" >> .env + echo "SALESFORCE_JWT_LOGIN_URL=$SALESFORCE_JWT_LOGIN_URL" >> .env + echo "SALESFORCE_JWT_CLIENT_ID=$SALESFORCE_JWT_CLIENT_ID" >> .env touch server.key - echo "${{ secrets.SALESFORCE_PRIVATE_KEY }}" >> server.key + echo "$SALESFORCE_PRIVATE_KEY" >> server.key + 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 }} # Integration tests - name: 'Integration tests' From 8fb9b9553d31b2d18786ef9ccf0aa63660d5844f Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 15:06:57 +0200 Subject: [PATCH 12/33] ci: config --- .github/workflows/ci-pr.yml | 39 +++++++++++++++++++------------------ .github/workflows/ci.yml | 39 +++++++++++++++++++------------------ 2 files changed, 40 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 833662d..b478c55 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -11,6 +11,16 @@ on: 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 }} + steps: # Checkout the source code - name: 'Checkout source code' @@ -48,26 +58,17 @@ jobs: - 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_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 touch server.key - echo "$SALESFORCE_PRIVATE_KEY" >> server.key - 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 }} + echo $SALESFORCE_PRIVATE_KEY >> server.key # Integration tests - name: 'Integration tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7bb4215..ad9345a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,16 @@ on: 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 }} + steps: # Checkout the source code - name: 'Checkout source code' @@ -49,26 +59,17 @@ jobs: - 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_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 touch server.key - echo "$SALESFORCE_PRIVATE_KEY" >> server.key - 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 }} + echo $SALESFORCE_PRIVATE_KEY >> server.key # Integration tests - name: 'Integration tests' From ca429ef39b881d6253ad672d8db7c4e962c09c1a Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 15:27:55 +0200 Subject: [PATCH 13/33] ci: extended test timeout --- spec/integration/client.spec.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 9ce0a25..b26b0a0 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -23,6 +23,7 @@ if (process.env.TEST_LOGGER === 'simpleFileLogger') { logger = console; } +const EXTENDED_JASMINE_TIMEOUT = 10000; const PLATFORM_EVENT_TOPIC = '/event/Sample__e'; const CHANGE_EVENT_TOPIC = '/data/AccountChangeEvent'; @@ -108,7 +109,7 @@ describe('Client', function () { 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; @@ -167,7 +168,7 @@ describe('Client', function () { expect(receivedEvent.payload.BillingAddress.City).toBe( account.BillingCity ); - }); + }, EXTENDED_JASMINE_TIMEOUT); it('supports usermame/password auth with platform event', async function () { let receivedEvent, receivedSub; @@ -218,7 +219,7 @@ describe('Client', function () { 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; @@ -268,7 +269,7 @@ describe('Client', function () { 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; @@ -324,5 +325,5 @@ describe('Client', function () { expect(receivedSub?.topicName).toBe(PLATFORM_EVENT_TOPIC); expect(receivedSub?.receivedEventCount).toBe(1); expect(receivedSub?.requestedEventCount).toBe(1); - }); + }, EXTENDED_JASMINE_TIMEOUT); }); From 70fa0732775299230eae995f6918df3116ef3886 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 15:30:22 +0200 Subject: [PATCH 14/33] fix: test format --- package.json | 2 +- spec/integration/client.spec.js | 556 +++++++++++++++++--------------- 2 files changed, 289 insertions(+), 269 deletions(-) diff --git a/package.json b/package.json index ce2561c..bc1bf53 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "typescript": "^5.6.3" }, "lint-staged": { - "**/src/*.{css,html,js,json,md,yaml,yml}": [ + "**/{src,spec}/*.{css,html,js,json,md,yaml,yml}": [ "prettier --write" ], "**/{src,spec}/**/*.js": [ diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index b26b0a0..669b9c4 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -58,272 +58,292 @@ describe('Client', function () { } }); - 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(3000, () => 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 sleep(1000); - - // 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 sleep(1000); - - // 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 - 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 sleep(1000); - - // 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 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(3000, () => 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 sleep(1000); + + // 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 sleep(1000); + + // 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 + 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 sleep(1000); + + // 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 + ); }); From b718f923656105283604bdb8b5028cf9da0a5ea9 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 15:39:38 +0200 Subject: [PATCH 15/33] feat: improved async tests --- spec/helper/asyncUtilities.js | 20 ++++++++++++++++++++ spec/integration/client.spec.js | 22 +--------------------- spec/integration/clientFailures.spec.js | 13 +++++-------- 3 files changed, 26 insertions(+), 29 deletions(-) create mode 100644 spec/helper/asyncUtilities.js diff --git a/spec/helper/asyncUtilities.js b/spec/helper/asyncUtilities.js new file mode 100644 index 0000000..4c60a8d --- /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(); + }, timeoutDuration); + checkInterval = setInterval(() => { + if (checkFunction()) { + clearTimeout(waitTimeout); + clearInterval(checkInterval); + resolve(); + } + }, 100); + }); +} diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 669b9c4..e6cb7e5 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -9,6 +9,7 @@ import { } 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(); @@ -27,27 +28,6 @@ const EXTENDED_JASMINE_TIMEOUT = 10000; const PLATFORM_EVENT_TOPIC = '/event/Sample__e'; const CHANGE_EVENT_TOPIC = '/data/AccountChangeEvent'; -async function sleep(duration) { - return new Promise((resolve) => setTimeout(() => resolve(), duration)); -} - -async function waitFor(timeoutDuration, checkFunction) { - return new Promise((resolve, reject) => { - let checkInterval; - const waitTimeout = setTimeout(() => { - clearInterval(checkInterval); - reject(); - }, timeoutDuration); - checkInterval = setInterval(() => { - if (checkFunction()) { - clearTimeout(waitTimeout); - clearInterval(checkInterval); - resolve(); - } - }, 100); - }); -} - describe('Client', function () { var client; diff --git a/spec/integration/clientFailures.spec.js b/spec/integration/clientFailures.spec.js index 660f483..1c8ecd9 100644 --- a/spec/integration/clientFailures.spec.js +++ b/spec/integration/clientFailures.spec.js @@ -3,6 +3,7 @@ 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(); @@ -19,10 +20,6 @@ if (process.env.TEST_LOGGER === 'simpleFileLogger') { const PLATFORM_EVENT_TOPIC = '/event/Sample__e'; -async function sleep(duration) { - return new Promise((resolve) => setTimeout(() => resolve(), duration)); -} - describe('Client failures', function () { var client; @@ -61,8 +58,8 @@ describe('Client failures', function () { }; client.subscribe(PLATFORM_EVENT_TOPIC, callback, 1); - // Wait for subscribe to be effective - await sleep(500); + // 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); @@ -99,8 +96,8 @@ describe('Client failures', function () { }; client.subscribe('/event/INVALID', callback, 1); - // Wait for subscribe to be effective - await sleep(1000); + // 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); From 9c434da52a720c6f458359dcbe288b4f3ce7eff4 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Mon, 14 Oct 2024 15:45:39 +0200 Subject: [PATCH 16/33] feat: added timeout error message --- spec/helper/asyncUtilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/helper/asyncUtilities.js b/spec/helper/asyncUtilities.js index 4c60a8d..7d007a8 100644 --- a/spec/helper/asyncUtilities.js +++ b/spec/helper/asyncUtilities.js @@ -7,7 +7,7 @@ export async function waitFor(timeoutDuration, checkFunction) { let checkInterval; const waitTimeout = setTimeout(() => { clearInterval(checkInterval); - reject(); + reject(`waitFor timed out after ${timeoutDuration} ms`); }, timeoutDuration); checkInterval = setInterval(() => { if (checkFunction()) { From d63523d3376d0f8aee922ba3c2bea36d560d2e09 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Tue, 15 Oct 2024 16:30:50 +0200 Subject: [PATCH 17/33] ci: fix private key --- .github/workflows/ci-pr.yml | 4 ++-- .github/workflows/ci.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index b478c55..8562fcb 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -20,6 +20,7 @@ jobs: 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 @@ -67,8 +68,7 @@ jobs: 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 - touch server.key - echo $SALESFORCE_PRIVATE_KEY >> server.key + echo $SALESFORCE_PRIVATE_KEY > server.key # Integration tests - name: 'Integration tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ad9345a..4136284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: 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 @@ -68,8 +69,7 @@ jobs: 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 - touch server.key - echo $SALESFORCE_PRIVATE_KEY >> server.key + echo $SALESFORCE_PRIVATE_KEY > server.key # Integration tests - name: 'Integration tests' From f6b44a3f4c731874b98ae534117c947f576fcef1 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Tue, 15 Oct 2024 16:35:36 +0200 Subject: [PATCH 18/33] fix: made client close safer in case client failed to log --- src/client.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/client.js b/src/client.js index 7072cac..9c96a51 100644 --- a/src/client.js +++ b/src/client.js @@ -599,7 +599,7 @@ export default class PubSubApiClient { this.#subscriptions.clear(); this.#logger.info('Closing gRPC stream'); - this.#client.close(); + this.#client?.close(); } /** From bab272f8ba8a4e00ca3877e1c3a13556049d3f4d Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Tue, 15 Oct 2024 16:43:04 +0200 Subject: [PATCH 19/33] fix: better async support --- spec/integration/client.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index e6cb7e5..151cb27 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -202,7 +202,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await sleep(1000); + await waitFor(3000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); @@ -256,7 +256,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await sleep(1000); + await waitFor(3000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); @@ -316,7 +316,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await sleep(1000); + await waitFor(3000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); From 4315bf4e43800fa7f8d6ed77b9a4d2c61921da39 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Wed, 16 Oct 2024 13:36:42 +0200 Subject: [PATCH 20/33] fix: openssl issue --- .github/workflows/ci-pr.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 8562fcb..4e47ce8 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -72,7 +72,7 @@ jobs: # Integration tests - name: 'Integration tests' - run: npm run test + run: export NODE_OPTIONS=--openssl-legacy-provider && npm run test # Housekeeping - name: 'Delete test environment configuration' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4136284..6564a57 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: # Integration tests - name: 'Integration tests' - run: npm run test + run: export NODE_OPTIONS=--openssl-legacy-provider && npm run test # Housekeeping - name: 'Delete test environment configuration' From a512eff6abb8c90f73c61139fc6338cdf08f6284 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Wed, 16 Oct 2024 13:47:05 +0200 Subject: [PATCH 21/33] ci: revert script change --- .github/workflows/ci-pr.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 4e47ce8..8562fcb 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -72,7 +72,7 @@ jobs: # Integration tests - name: 'Integration tests' - run: export NODE_OPTIONS=--openssl-legacy-provider && npm run test + run: npm run test # Housekeeping - name: 'Delete test environment configuration' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6564a57..4136284 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,7 +73,7 @@ jobs: # Integration tests - name: 'Integration tests' - run: export NODE_OPTIONS=--openssl-legacy-provider && npm run test + run: npm run test # Housekeeping - name: 'Delete test environment configuration' From 52b2da7f9fc41f94e8f22a93e2fcb1d1360645a3 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Wed, 16 Oct 2024 14:02:43 +0200 Subject: [PATCH 22/33] feat: check private key format --- src/utils/auth.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/utils/auth.js b/src/utils/auth.js index fa282ba..d52dfba 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -96,6 +96,11 @@ export default class SalesforceAuth { */ 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({ From c480b63ddfffa510e4b2babbae3d8eefb9fd8356 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Wed, 16 Oct 2024 14:05:53 +0200 Subject: [PATCH 23/33] ci: hide remove error in case of failure --- .github/workflows/ci-pr.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 8562fcb..64cb4a1 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -77,4 +77,4 @@ jobs: # Housekeeping - name: 'Delete test environment configuration' if: always() - run: rm .env server.key + run: rm -f .env server.key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4136284..bb3bade 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,4 +78,4 @@ jobs: # Housekeeping - name: 'Delete test environment configuration' if: always() - run: rm .env server.key + run: rm -f .env server.key From 390da52d51f167a03f07a7cc1c1f22a375f99532 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Wed, 16 Oct 2024 14:06:06 +0200 Subject: [PATCH 24/33] chore: format --- src/utils/auth.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/utils/auth.js b/src/utils/auth.js index d52dfba..528c696 100644 --- a/src/utils/auth.js +++ b/src/utils/auth.js @@ -96,7 +96,12 @@ export default class SalesforceAuth { */ async #authWithJwtBearer() { const { clientId, username, loginUrl, privateKey } = this.#config; - if (!privateKey.toString().trim().startsWith('-----BEGIN RSA PRIVATE KEY-----')) { + if ( + !privateKey + .toString() + .trim() + .startsWith('-----BEGIN RSA PRIVATE KEY-----') + ) { throw new Error( `Private key is missing -----BEGIN RSA PRIVATE KEY----- header` ); From fdfc0ac5b5916b3926d23c44f9ea0d2fa60257dd Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 22:50:31 +0200 Subject: [PATCH 25/33] fix: invalid chars in key --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bbd47ab..3a7c46a 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ This is the most secure authentication option. Recommended for production use. ```js // Read private key file -const privateKey = fs.readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE); +const privateKey = fs.readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE).replace(/\\n/g, '\n'); // Build PubSub client const client = new PubSubApiClient({ From 7b4ca9f3c31885b94367ac5ebd0a0dd39be5150f Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 22:53:50 +0200 Subject: [PATCH 26/33] fix: format and pre-commit script --- README.md | 4 +++- package.json | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3a7c46a..9d2cd26 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,9 @@ This is the most secure authentication option. Recommended for production use. ```js // Read private key file -const privateKey = fs.readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE).replace(/\\n/g, '\n'); +const privateKey = fs + .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE) + .replace(/\\n/g, '\n'); // Build PubSub client const client = new PubSubApiClient({ diff --git a/package.json b/package.json index bc1bf53..2125f34 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "typescript": "^5.6.3" }, "lint-staged": { - "**/{src,spec}/*.{css,html,js,json,md,yaml,yml}": [ + "**/{src,spec}/**/*.{css,html,js,json,md,yaml,yml}": [ "prettier --write" ], "**/{src,spec}/**/*.js": [ From a1856df54a8551e84c91694a8f52df66901c2952 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 22:56:07 +0200 Subject: [PATCH 27/33] fix: formatting --- README.md | 3 +-- spec/integration/client.spec.js | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 9d2cd26..3687de9 100644 --- a/README.md +++ b/README.md @@ -111,8 +111,7 @@ This is the most secure authentication option. Recommended for production use. ```js // Read private key file const privateKey = fs - .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE) - .replace(/\\n/g, '\n'); + .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE); // Build PubSub client const client = new PubSubApiClient({ diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 151cb27..e12c356 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -273,9 +273,9 @@ describe('Client', function () { let receivedEvent, receivedSub; // Read private key - const privateKey = fs.readFileSync( - process.env.SALESFORCE_PRIVATE_KEY_PATH - ); + const privateKey = fs + .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_PATH) + .replace(/\\n/g, '\n'); // Build PubSub client client = new PubSubApiClient( From ac0be3149620457ad96a3e9057daa15af292a157 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 22:57:36 +0200 Subject: [PATCH 28/33] doc: format --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3687de9..bbd47ab 100644 --- a/README.md +++ b/README.md @@ -110,8 +110,7 @@ This is the most secure authentication option. Recommended for production use. ```js // Read private key file -const privateKey = fs - .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE); +const privateKey = fs.readFileSync(process.env.SALESFORCE_PRIVATE_KEY_FILE); // Build PubSub client const client = new PubSubApiClient({ From f9b68135d629f758290a1a43c0179a0cdb79a4f9 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 23:03:18 +0200 Subject: [PATCH 29/33] fix: invalid chars in key --- spec/integration/client.spec.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index e12c356..dc37385 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -272,10 +272,14 @@ describe('Client', function () { async function () { let receivedEvent, receivedSub; - // Read private key - const privateKey = fs - .readFileSync(process.env.SALESFORCE_PRIVATE_KEY_PATH) + // Read private key and remove potential invalid characters from key + const privateKeyBuffer = fs.readFileSync( + process.env.SALESFORCE_PRIVATE_KEY_PATH + ); + const privateKeyString = privateKeyBuffer + .toString() .replace(/\\n/g, '\n'); + const privateKey = Buffer.from(privateKeyString, 'utf-8'); // Build PubSub client client = new PubSubApiClient( From e6e457a5e2d04e5f0680095e605b7ba4a8b20890 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 23:06:21 +0200 Subject: [PATCH 30/33] fix: rollback key format --- spec/integration/client.spec.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index dc37385..968e9e5 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -273,13 +273,9 @@ describe('Client', function () { let receivedEvent, receivedSub; // Read private key and remove potential invalid characters from key - const privateKeyBuffer = fs.readFileSync( + const privateKey = fs.readFileSync( process.env.SALESFORCE_PRIVATE_KEY_PATH ); - const privateKeyString = privateKeyBuffer - .toString() - .replace(/\\n/g, '\n'); - const privateKey = Buffer.from(privateKeyString, 'utf-8'); // Build PubSub client client = new PubSubApiClient( From 48fb4f69c2f5f548225e2bbee88457954656db27 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 23:22:02 +0200 Subject: [PATCH 31/33] ci: fix key format --- .github/workflows/ci-pr.yml | 2 +- .github/workflows/ci.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-pr.yml b/.github/workflows/ci-pr.yml index 64cb4a1..bf2cad0 100644 --- a/.github/workflows/ci-pr.yml +++ b/.github/workflows/ci-pr.yml @@ -68,7 +68,7 @@ jobs: 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 + echo "$SALESFORCE_PRIVATE_KEY" > server.key # Integration tests - name: 'Integration tests' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bb3bade..2a5f00e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -69,7 +69,7 @@ jobs: 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 + echo "$SALESFORCE_PRIVATE_KEY" > server.key # Integration tests - name: 'Integration tests' From e368d6e21d2ff896d65d27dac48609c3227ad76b Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 23:29:13 +0200 Subject: [PATCH 32/33] fix: increase test timeout --- spec/integration/client.spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/integration/client.spec.js b/spec/integration/client.spec.js index 968e9e5..5a531e2 100644 --- a/spec/integration/client.spec.js +++ b/spec/integration/client.spec.js @@ -133,7 +133,7 @@ describe('Client', function () { await updateSampleAccount(account); // Wait for event to be received - await waitFor(3000, () => receivedEvent !== undefined); + await waitFor(5000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBeDefined(); @@ -202,7 +202,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await waitFor(3000, () => receivedEvent !== undefined); + await waitFor(5000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); @@ -256,7 +256,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await waitFor(3000, () => receivedEvent !== undefined); + await waitFor(5000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); @@ -316,7 +316,7 @@ describe('Client', function () { const publishedReplayId = publishResult.replayId; // Wait for event to be received - await waitFor(3000, () => receivedEvent !== undefined); + await waitFor(5000, () => receivedEvent !== undefined); // Check received event and subcription info expect(receivedEvent?.replayId).toBe(publishedReplayId); From 29e86be1f2da34ef66aa41b4699d01a2905c11c2 Mon Sep 17 00:00:00 2001 From: Philippe Ozil Date: Sun, 20 Oct 2024 23:46:11 +0200 Subject: [PATCH 33/33] doc: improved migration doc --- README.md | 27 ++++++++++++++++++++++++--- package.json | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index bbd47ab..442c17f 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See the [official Pub/Sub API repo](https://github.com/developerforce/pub-sub-ap In v4 and earlier versions of this client: -- you had to specify configuration in a `.env` file with specific property names. +- 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. In v5: @@ -47,9 +47,30 @@ In v5: ### Event handling -In v4 and earlier versions of this client you used an asynchronous `EventEmitter` to receive updates such as incoming messages or lifecycle events. +In v4 and earlier versions of this client you use an asynchronous `EventEmitter` to receive updates such as incoming messages or lifecycle events: -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. +```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 +}): +``` + +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. + +```js +const subscribeCallback = (subscription, callbackType, data) => { + // Event handling logic goes here +}; + +// Subscribe to account change events +await client.subscribe('/data/AccountChangeEvent', subscribeCallback); +``` ## Installation and Configuration diff --git a/package.json b/package.json index 2125f34..f369ef8 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ "typescript": "^5.6.3" }, "lint-staged": { - "**/{src,spec}/**/*.{css,html,js,json,md,yaml,yml}": [ + "**/*.{css,html,js,json,md,yaml,yml}": [ "prettier --write" ], "**/{src,spec}/**/*.js": [