MAXIMUM_STATE_MAX_AGE) {
+ // This method is always called from ProductClient, so this should be fine.
+ throw new ProductClientConstructionError(`stateCookieMaxAge cannot be greater than ${MAXIMUM_STATE_MAX_AGE.toString()} seconds.`);
+ }
this.installerOptions = updatedOptions;
return updatedOptions;
}
@@ -577,9 +589,6 @@ class HttpReceiver {
server;
logger;
constructor(options) {
- if (!options.webhooksSecretToken) {
- throw new HTTPReceiverConstructionError("webhooksSecretToken is a required constructor option.");
- }
this.options = mergeDefaultOptions(options, { endpoints: HttpReceiver.DEFAULT_ENDPOINT });
this.options.endpoints = prependSlashes(this.options.endpoints);
this.logger =
@@ -593,6 +602,19 @@ class HttpReceiver {
canInstall() {
return true;
}
+ buildDeletedStateCookieHeader(name) {
+ return `${name}=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/; Secure;`;
+ }
+ buildStateCookieHeader(name, value, maxAge) {
+ return `${name}=${value}; HttpOnly; Max-Age=${maxAge.toString()}; Path=/; Secure;`;
+ }
+ getRequestCookie(req, name) {
+ return req.headers.cookie
+ ?.split(";")
+ .find((cookie) => cookie.trim().startsWith(name))
+ ?.split("=")[1]
+ ?.trim();
+ }
getServerCreator() {
return this.hasSecureOptions() ? createServer : createServer$1;
}
@@ -607,8 +629,20 @@ class HttpReceiver {
this.eventEmitter = eventEmitter;
this.interactiveAuth = interactiveAuth;
}
+ setResponseCookie(res, cookie) {
+ const existingCookies = res.getHeader("Set-Cookie") ?? [];
+ const cookiesArray = Array.isArray(existingCookies) ? existingCookies
+ : typeof existingCookies === "string" ? [existingCookies]
+ : [existingCookies.toString()];
+ res.setHeader("Set-Cookie", [...cookiesArray, cookie]);
+ }
+ areNormalizedUrlsEqual(firstUrl, secondUrl) {
+ const normalizedFirstUrl = firstUrl.endsWith("/") ? firstUrl.slice(0, -1) : firstUrl;
+ const normalizedSecondUrl = secondUrl.endsWith("/") ? secondUrl.slice(0, -1) : secondUrl;
+ return normalizedFirstUrl == normalizedSecondUrl;
+ }
start(port) {
- if (typeof port !== "number" && isNaN(Number(port)) && !this.options.port) {
+ if (typeof port !== "number" && isNaN(Number(port)) && !this.options.port && this.options.port !== 0) {
const errorMessage = "HTTP receiver must have number-coercible port found in constructor option or method call.";
this.logger.error(errorMessage);
throw new HTTPReceiverPortNotNumberError(errorMessage);
@@ -625,69 +659,84 @@ class HttpReceiver {
// Handle interactive OAuth flow, if user is going to installPath or redirectUriPath
if (interactiveAuth && interactiveAuth instanceof InteractiveAuth && interactiveAuth.installerOptions) {
const { installerOptions } = interactiveAuth;
- if (pathname == installerOptions.installPath) {
- const authUrl = await Promise.resolve(interactiveAuth.getAuthorizationUrl());
+ if (this.areNormalizedUrlsEqual(pathname, installerOptions.installPath)) {
+ const { fullUrl, generatedState } = await interactiveAuth.getAuthorizationUrl();
+ const stateCookie = this.buildStateCookieHeader(installerOptions.stateCookieName, generatedState, installerOptions.stateCookieMaxAge);
await (installerOptions.directInstall ?
- this.writeTemporaryRedirect(res, authUrl)
- : this.writeResponse(res, StatusCode.OK, defaultInstallTemplate(authUrl)));
+ this.writeTemporaryRedirect(res, fullUrl, stateCookie)
+ : this.writeResponse(res, StatusCode.OK, defaultInstallTemplate(fullUrl), stateCookie));
return;
}
// The user has navigated to the redirect page; init the code
- if (pathname === installerOptions.redirectUriPath) {
- const authCode = searchParams.get("code");
- const stateCode = searchParams.get("state");
+ if (this.areNormalizedUrlsEqual(pathname, installerOptions.redirectUriPath)) {
+ const authCodeParam = searchParams.get("code");
+ const stateCodeParam = searchParams.get("state");
+ const stateCodeCookie = this.getRequestCookie(req, installerOptions.stateCookieName);
try {
- if (!authCode || !stateCode) {
+ // Can't proceed if no auth code or state code in search parameters
+ if (!authCodeParam || !stateCodeParam) {
const errorMessage = "OAuth callback did not include code and/or state in request.";
this.logger.error(errorMessage);
throw new ReceiverOAuthFlowError(errorMessage);
}
- // Wrapped in `await Promise.resolve(...)`, as method may return a `Promise` or may not.
- await Promise.resolve(installerOptions.stateStore.verifyState(stateCode));
- await Promise.resolve(interactiveAuth.initRedirectCode(authCode));
- await this.writeResponse(res, StatusCode.OK, defaultCallbackSuccessTemplate());
+ // Ensure that the state token is verified, according to our state store
+ await installerOptions.stateStore.verifyState(stateCodeParam);
+ // Ensure that the state token we received (in search parameters) IS THE SAME as the state cookie
+ if (!stateCodeCookie || stateCodeCookie !== stateCodeParam) {
+ const errorMessage = "The state parameter is not from this browser session.";
+ this.logger.error(errorMessage);
+ throw new ReceiverOAuthFlowError(errorMessage);
+ }
+ await interactiveAuth.initRedirectCode(authCodeParam);
+ const deletionStateCookie = this.buildDeletedStateCookieHeader(installerOptions.stateCookieName);
+ await this.writeResponse(res, StatusCode.OK, defaultCallbackSuccessTemplate(), deletionStateCookie);
return;
}
catch (err) {
const htmlTemplate = isCoreError(err) ?
defaultCallbackKnownErrorTemplate(err.name, err.message)
: defaultCallbackUnknownErrorTemplate();
- await this.writeResponse(res, StatusCode.INTERNAL_SERVER_ERROR, htmlTemplate);
+ const deletionStateCookie = this.buildDeletedStateCookieHeader(installerOptions.stateCookieName);
+ await this.writeResponse(res, StatusCode.INTERNAL_SERVER_ERROR, htmlTemplate, deletionStateCookie);
return;
}
}
}
- // We currently only support a single endpoint, though this will change in the future.
- if (!this.hasEndpoint(pathname)) {
- await this.writeResponse(res, StatusCode.NOT_FOUND);
- return;
- }
- // We currently only support POST requests, as that's what Zoom sends.
- if (req.method !== "post" && req.method !== "POST") {
- await this.writeResponse(res, StatusCode.METHOD_NOT_ALLOWED);
- return;
- }
- try {
- const { webhooksSecretToken } = this.options;
- const request = await CommonHttpRequest.buildFromIncomingMessage(req, webhooksSecretToken);
- const processedEvent = request.processEvent();
- if (isHashedUrlValidation(processedEvent)) {
- await this.writeResponse(res, StatusCode.OK, processedEvent);
+ // This section is only applicable if we have a webhooks secret token—if we don't, then this
+ // receiver is, in effect, just for OAuth usage, meaning installing and validating.
+ if (this.options.webhooksSecretToken) {
+ // We currently only support a single endpoint, though this will change in the future.
+ if (!this.hasEndpoint(pathname)) {
+ await this.writeResponse(res, StatusCode.NOT_FOUND);
+ return;
}
- else {
- await this.eventEmitter?.emit(processedEvent.event, processedEvent);
- await this.writeResponse(res, StatusCode.OK, { message: "Zoom event processed successfully." });
+ // We currently only support POST requests, as that's what Zoom sends.
+ if (req.method !== "post" && req.method !== "POST") {
+ await this.writeResponse(res, StatusCode.METHOD_NOT_ALLOWED);
+ return;
}
- }
- catch (err) {
- if (isCoreError(err, "CommonHttpRequestError")) {
- await this.writeResponse(res, StatusCode.BAD_REQUEST, { error: err.message });
+ try {
+ const { webhooksSecretToken } = this.options;
+ const request = await CommonHttpRequest.buildFromIncomingMessage(req, webhooksSecretToken);
+ const processedEvent = request.processEvent();
+ if (isHashedUrlValidation(processedEvent)) {
+ await this.writeResponse(res, StatusCode.OK, processedEvent);
+ }
+ else {
+ await this.eventEmitter?.emit(processedEvent.event, processedEvent);
+ await this.writeResponse(res, StatusCode.OK, { message: "Zoom event processed successfully." });
+ }
}
- else {
- console.error(err);
- await this.writeResponse(res, StatusCode.INTERNAL_SERVER_ERROR, {
- error: "An unknown error occurred. Please try again later."
- });
+ catch (err) {
+ if (isCoreError(err, "CommonHttpRequestError")) {
+ await this.writeResponse(res, StatusCode.BAD_REQUEST, { error: err.message });
+ }
+ else {
+ console.error(err);
+ await this.writeResponse(res, StatusCode.INTERNAL_SERVER_ERROR, {
+ error: "An unknown error occurred. Please try again later."
+ });
+ }
}
}
})());
@@ -721,18 +770,24 @@ class HttpReceiver {
resolve();
});
}
- writeTemporaryRedirect(res, location) {
+ writeTemporaryRedirect(res, location, setCookie) {
return new Promise((resolve) => {
+ if (setCookie) {
+ this.setResponseCookie(res, setCookie);
+ }
res.writeHead(StatusCode.TEMPORARY_REDIRECT, { Location: location });
res.end(() => {
resolve();
});
});
}
- writeResponse(res, statusCode, bodyContent) {
+ writeResponse(res, statusCode, bodyContent, setCookie) {
return new Promise((resolve) => {
const mimeType = typeof bodyContent === "object" ? "application/json" : "text/html";
bodyContent = typeof bodyContent === "object" ? JSON.stringify(bodyContent) : bodyContent;
+ if (setCookie) {
+ this.setResponseCookie(res, setCookie);
+ }
res.writeHead(statusCode, { "Content-Type": mimeType });
res.end(bodyContent, () => {
resolve();
@@ -792,7 +847,9 @@ class ProductClient {
// Only create an instance of `this.receiver` if the developer did not explicitly disable it.
if (!isReceiverDisabled(options)) {
// Throw error if receiver enabled, but no explicit receiver or a webhooks secret token provided.
- if (!hasExplicitReceiver(options) && !hasWebhooksSecretToken(options)) {
+ // This is mainly applicable for products where we expect webhooks to be used; in events where webhooks are not
+ // expected, then it's perfectly fine for the developer to not provide a receiver of a webhooks secret token.
+ if (this.webEventConsumer && !hasExplicitReceiver(options) && !hasWebhooksSecretToken(options)) {
throw new ProductClientConstructionError("Options must include a custom receiver, or a webhooks secret token.");
}
this.receiver = (hasExplicitReceiver(options) ?
@@ -813,7 +870,7 @@ class ProductClient {
}
async start() {
if (!this.receiver) {
- throw new ReceiverInconsistentStateError("Receiver not constructed. Was disableReceiver set to true?");
+ throw new ReceiverInconsistentStateError("Receiver failed to construct. Was disableReceiver set to true?");
}
// Method call is wrapped in `await` and `Promise.resolve()`, as the call
// may or may not return a promise. This is not required when implementing `Receiver`.
@@ -821,87 +878,13 @@ class ProductClient {
}
}
-const type = "module";
-const name = "@zoom/rivet";
-const author = "Zoom Communications, Inc.";
-const contributors = [
- {
- name: "James Coon",
- email: "james.coon@zoom.us",
- url: "https://www.npmjs.com/~jcoon97"
- },
- {
- name: "Will Ezrine",
- email: "will.ezrine@zoom.us",
- url: "https://www.npmjs.com/~wezrine"
- },
- {
- name: "Tommy Gaessler",
- email: "tommy.gaessler@zoom.us",
- url: "https://www.npmjs.com/~tommygaessler"
- }
-];
-const packageManager = "pnpm@9.9.0";
-const version = "0.2.2";
-const scripts = {
- test: "vitest",
- "test:coverage": "vitest --coverage",
- "export": "rollup --config ./rollup.config.mjs",
- prepare: "husky",
- lint: "eslint './packages/**/*.ts' --ignore-pattern '**/*{Endpoints,EventProcessor}.ts' --ignore-pattern '**/*.{spec,test,test-d}.ts'"
-};
-const devDependencies = {
- "@eslint/js": "^9.12.0",
- "@rollup/plugin-commonjs": "^28.0.0",
- "@rollup/plugin-json": "^6.1.0",
- "@rollup/plugin-node-resolve": "^15.3.0",
- "@rollup/plugin-typescript": "^12.1.0",
- "@tsconfig/recommended": "^1.0.7",
- "@tsconfig/strictest": "^2.0.5",
- "@types/eslint__js": "^8.42.3",
- "@types/node": "^22.7.5",
- "@types/semver": "^7.5.8",
- "@types/supertest": "^6.0.2",
- "@vitest/coverage-v8": "2.1.3",
- dotenv: "^16.4.5",
- eslint: "^9.12.0",
- "eslint-plugin-n": "^17.11.1",
- "eslint-plugin-promise": "^7.1.0",
- "get-port": "^7.1.0",
- husky: "^9.1.6",
- "lint-staged": "^15.2.10",
- nock: "^13.5.5",
- prettier: "^3.3.3",
- "prettier-plugin-organize-imports": "^4.1.0",
- rollup: "^4.24.0",
- "rollup-plugin-copy": "^3.5.0",
- "rollup-plugin-dts": "^6.1.1",
- semver: "^7.6.3",
- supertest: "^7.0.0",
- "ts-node": "^10.9.2",
- tslib: "^2.7.0",
- typescript: "^5.6.3",
- "typescript-eslint": "^8.8.1",
- vitest: "2.1.3"
-};
+const version = "0.4.0";
var packageJson = {
- type: type,
- name: name,
- author: author,
- contributors: contributors,
- packageManager: packageManager,
- version: version,
- scripts: scripts,
- devDependencies: devDependencies,
- "lint-staged": {
- "*": "prettier --ignore-unknown --write",
- "*.ts !*{Endpoints,EventProcessor}.ts !*.{spec,test,test-d}.ts": [
- "eslint --fix",
- "eslint"
- ]
-}
-};
+ version: version};
+// eslint-disable-next-line no-control-regex
+const ASCII_CONTROL_CHARACTERS_PATTERN = /[\x00-\x1F\x7F]/;
+const NON_ASCII_CHARACTERS_PATTERN = /[^\x20-\x7E]/;
class WebEndpoints {
/** @internal */
static DEFAULT_BASE_URL = "https://api.zoom.us/v2";
@@ -930,10 +913,23 @@ class WebEndpoints {
return (async ({ path, body, query }) => await this.makeRequest(method, baseUrlOverride, urlPathBuilder(path), requestMimeType ?? WebEndpoints.DEFAULT_MIME_TYPE, body, query)).bind(this);
}
buildUserAgent() {
- return (`rivet/${packageJson.version} ` +
+ const customUserAgentName = this.getCustomUserAgentName();
+ const userAgent = `rivet/${packageJson.version}${customUserAgentName ? ` (${customUserAgentName})` : ""}`;
+ return (`${userAgent} ` +
`${basename(process.title)}/${process.version.replace("v", "")} ` +
`${os.platform()}/${os.release()}`);
}
+ getCustomUserAgentName() {
+ const { userAgentName } = this.options;
+ if (!userAgentName || typeof userAgentName !== "string") {
+ return null;
+ }
+ return userAgentName
+ .replace(new RegExp(ASCII_CONTROL_CHARACTERS_PATTERN, "g"), "")
+ .replace(new RegExp(NON_ASCII_CHARACTERS_PATTERN, "g"), "")
+ .trim()
+ .slice(0, 100);
+ }
getHeaders(bearerToken, contentType) {
return {
Accept: "application/json",
diff --git a/commerce/commerce.cjs b/commerce/commerce.cjs
new file mode 100644
index 0000000..58a3c91
--- /dev/null
+++ b/commerce/commerce.cjs
@@ -0,0 +1,1039 @@
+'use strict';
+
+var node_crypto = require('node:crypto');
+var node_http = require('node:http');
+var node_https = require('node:https');
+var axios = require('axios');
+var dayjs = require('dayjs');
+var node_buffer = require('node:buffer');
+var jose = require('jose');
+var FormData = require('form-data');
+var os = require('node:os');
+var node_path = require('node:path');
+
+/**
+ * Guard if an object implements the {@link StateStore} interface — most notably,
+ * `generateState()` and `verifyState(state: string)`.
+ */
+const isStateStore = (obj) => typeof obj.generateState === "function" && typeof obj.verifyState === "function";
+
+const createRivetErrors = (errors) => ({
+ createError: (errorCode) => class extends Error {
+ errorCode = errors[errorCode];
+ constructor(message, options) {
+ const errorMessage = (message ??
+ (options?.cause instanceof Error ? options.cause.message : errorCode));
+ super(errorMessage, options);
+ this.name = errorCode;
+ Object.setPrototypeOf(this, new.target.prototype);
+ }
+ },
+ isError: (obj, key) => key ?
+ Object.keys(errors).some((code) => code === key) &&
+ typeof obj.errorCode === "string" &&
+ obj.errorCode === errors[key]
+ : typeof obj.errorCode === "string"
+});
+
+const coreErrors = {
+ ApiResponseError: "zoom_rivet_api_response_error",
+ AwsReceiverRequestError: "zoom_rivet_aws_receiver_request_error",
+ ClientCredentialsRawResponseError: "zoom_rivet_client_credentials_raw_response_error",
+ S2SRawResponseError: "zoom_rivet_s2s_raw_response_error",
+ CommonHttpRequestError: "zoom_rivet_common_http_request_error",
+ ReceiverInconsistentStateError: "zoom_rivet_receiver_inconsistent_state_error",
+ ReceiverOAuthFlowError: "zoom_rivet_receiver_oauth_flow_error",
+ HTTPReceiverConstructionError: "zoom_rivet_http_receiver_construction_error",
+ HTTPReceiverPortNotNumberError: "zoom_rivet_http_receiver_port_not_number_error",
+ HTTPReceiverRequestError: "zoom_rivet_http_receiver_request_error",
+ OAuthInstallerNotInitializedError: "zoom_rivet_oauth_installer_not_initialized_error",
+ OAuthTokenDoesNotExistError: "zoom_rivet_oauth_does_not_exist_error",
+ OAuthTokenFetchFailedError: "zoom_rivet_oauth_token_fetch_failed_error",
+ OAuthTokenRawResponseError: "zoom_rivet_oauth_token_raw_response_error",
+ OAuthTokenRefreshFailedError: "zoom_rivet_oauth_token_refresh_failed_error",
+ OAuthStateVerificationFailedError: "zoom_rivet_oauth_state_verification_failed_error",
+ ProductClientConstructionError: "zoom_rivet_product_client_construction_error"
+};
+const { createError: createCoreError, isError: isCoreError } = createRivetErrors(coreErrors);
+const ApiResponseError = createCoreError("ApiResponseError");
+const AwsReceiverRequestError = createCoreError("AwsReceiverRequestError");
+const ClientCredentialsRawResponseError = createCoreError("ClientCredentialsRawResponseError");
+const S2SRawResponseError = createCoreError("S2SRawResponseError");
+const CommonHttpRequestError = createCoreError("CommonHttpRequestError");
+const ReceiverInconsistentStateError = createCoreError("ReceiverInconsistentStateError");
+const ReceiverOAuthFlowError = createCoreError("ReceiverOAuthFlowError");
+const HTTPReceiverConstructionError = createCoreError("HTTPReceiverConstructionError");
+const HTTPReceiverPortNotNumberError = createCoreError("HTTPReceiverPortNotNumberError");
+const HTTPReceiverRequestError = createCoreError("HTTPReceiverRequestError");
+const OAuthInstallerNotInitializedError = createCoreError("OAuthInstallerNotInitializedError");
+const OAuthTokenDoesNotExistError = createCoreError("OAuthTokenDoesNotExistError");
+const OAuthTokenFetchFailedError = createCoreError("OAuthTokenFetchFailedError");
+const OAuthTokenRawResponseError = createCoreError("OAuthTokenRawResponseError");
+const OAuthTokenRefreshFailedError = createCoreError("OAuthTokenRefreshFailedError");
+const OAuthStateVerificationFailedError = createCoreError("OAuthStateVerificationFailedError");
+const ProductClientConstructionError = createCoreError("ProductClientConstructionError");
+
+exports.LogLevel = void 0;
+(function (LogLevel) {
+ LogLevel["ERROR"] = "error";
+ LogLevel["WARN"] = "warn";
+ LogLevel["INFO"] = "info";
+ LogLevel["DEBUG"] = "debug";
+})(exports.LogLevel || (exports.LogLevel = {}));
+class ConsoleLogger {
+ level;
+ name;
+ static labels = (() => {
+ const entries = Object.entries(exports.LogLevel);
+ const map = entries.map(([key, value]) => [value, `[${key}] `]);
+ return new Map(map);
+ })();
+ static severity = {
+ [exports.LogLevel.ERROR]: 400,
+ [exports.LogLevel.WARN]: 300,
+ [exports.LogLevel.INFO]: 200,
+ [exports.LogLevel.DEBUG]: 100
+ };
+ constructor() {
+ this.level = exports.LogLevel.INFO;
+ this.name = "";
+ }
+ getLevel() {
+ return this.level;
+ }
+ setLevel(level) {
+ this.level = level;
+ }
+ setName(name) {
+ this.name = name;
+ }
+ debug(...msg) {
+ if (ConsoleLogger.isMoreOrEqualSevere(exports.LogLevel.DEBUG, this.level)) {
+ console.debug(ConsoleLogger.labels.get(exports.LogLevel.DEBUG), this.name, ...msg);
+ }
+ }
+ info(...msg) {
+ if (ConsoleLogger.isMoreOrEqualSevere(exports.LogLevel.INFO, this.level)) {
+ console.info(ConsoleLogger.labels.get(exports.LogLevel.INFO), this.name, ...msg);
+ }
+ }
+ warn(...msg) {
+ if (ConsoleLogger.isMoreOrEqualSevere(exports.LogLevel.WARN, this.level)) {
+ console.warn(ConsoleLogger.labels.get(exports.LogLevel.WARN), this.name, ...msg);
+ }
+ }
+ error(...msg) {
+ if (ConsoleLogger.isMoreOrEqualSevere(exports.LogLevel.ERROR, this.level)) {
+ console.error(ConsoleLogger.labels.get(exports.LogLevel.ERROR), this.name, ...msg);
+ }
+ }
+ static isMoreOrEqualSevere(a, b) {
+ return ConsoleLogger.severity[a] >= ConsoleLogger.severity[b];
+ }
+}
+
+/** @internal */
+const hashUrlValidationEvent = ({ payload: { plainToken } }, webhooksSecretToken) => ({
+ encryptedToken: node_crypto.createHmac("sha256", webhooksSecretToken).update(plainToken).digest("hex"),
+ plainToken
+});
+const isHashedUrlValidation = (obj) => typeof obj.encryptedToken === "string" &&
+ typeof obj.plainToken === "string";
+const isRawUrlValidationEvent = (obj) => obj.event === "endpoint.url_validation" && typeof obj.payload.plainToken === "string";
+const isSkeletonEvent = (obj) => typeof obj.event === "string";
+class CommonHttpRequest {
+ headers;
+ payload;
+ webhooksSecretToken;
+ constructor(headers, payload, webhooksSecretToken) {
+ this.headers = headers;
+ this.payload = payload;
+ this.webhooksSecretToken = webhooksSecretToken;
+ }
+ static buildFromAwsEvent({ body, headers, isBase64Encoded }, webhooksSecretToken) {
+ try {
+ const rawBody = body ?? "";
+ const decodedBody = isBase64Encoded ? Buffer.from(rawBody, "base64").toString("ascii") : rawBody;
+ const payload = JSON.parse(decodedBody);
+ return new CommonHttpRequest(headers, payload, webhooksSecretToken);
+ }
+ catch (err) {
+ throw err instanceof SyntaxError ?
+ new CommonHttpRequestError("Failed to parse payload string to JSON.", err)
+ : err;
+ }
+ }
+ static async buildFromIncomingMessage(incomingMessage, webhooksSecretToken) {
+ const bufferAsString = () => {
+ return new Promise((resolve, reject) => {
+ const body = [];
+ incomingMessage.on("data", (chunk) => body.push(chunk));
+ incomingMessage.on("error", (err) => {
+ reject(err);
+ });
+ incomingMessage.on("end", () => {
+ resolve(Buffer.concat(body).toString());
+ });
+ });
+ };
+ try {
+ const payload = JSON.parse(await bufferAsString());
+ return new CommonHttpRequest(incomingMessage.headers, payload, webhooksSecretToken);
+ }
+ catch (err) {
+ if (err instanceof SyntaxError) {
+ throw new CommonHttpRequestError("Failed to parse payload string to JSON.", err);
+ }
+ throw err;
+ }
+ }
+ isEventVerified() {
+ const { signature, requestTimestamp } = this.parseHeaders();
+ const messageToVerify = `v0:${requestTimestamp.toString()}:${JSON.stringify(this.payload)}`;
+ const hashToVerify = node_crypto.createHmac("sha256", this.webhooksSecretToken).update(messageToVerify).digest("hex");
+ const signatureToVerify = `v0=${hashToVerify}`;
+ return signatureToVerify === signature;
+ }
+ parseHeaders() {
+ const findHeader = (header) => {
+ const foundHeader = Object.keys(this.headers).find((key) => key.toLowerCase() === header.toLowerCase());
+ return foundHeader && this.headers[foundHeader];
+ };
+ const headerSignature = findHeader("x-zm-signature");
+ const headerRequestTimestamp = findHeader("x-zm-request-timestamp");
+ if (!headerSignature && !headerRequestTimestamp) {
+ throw new CommonHttpRequestError("Request payload must have signature and request timestamp from Zoom.");
+ }
+ return {
+ signature: headerSignature,
+ requestTimestamp: Number(headerRequestTimestamp)
+ };
+ }
+ processEvent() {
+ if (!isSkeletonEvent(this.payload)) {
+ throw new CommonHttpRequestError("Request payload structure does not match expected from Zoom.");
+ }
+ if (!this.isEventVerified()) {
+ throw new CommonHttpRequestError("Failed to verify event originated from Zoom.");
+ }
+ if (isRawUrlValidationEvent(this.payload)) {
+ return hashUrlValidationEvent(this.payload, this.webhooksSecretToken);
+ }
+ return this.payload;
+ }
+}
+
+exports.StatusCode = void 0;
+(function (StatusCode) {
+ StatusCode[StatusCode["OK"] = 200] = "OK";
+ StatusCode[StatusCode["TEMPORARY_REDIRECT"] = 302] = "TEMPORARY_REDIRECT";
+ StatusCode[StatusCode["BAD_REQUEST"] = 400] = "BAD_REQUEST";
+ StatusCode[StatusCode["NOT_FOUND"] = 404] = "NOT_FOUND";
+ StatusCode[StatusCode["METHOD_NOT_ALLOWED"] = 405] = "METHOD_NOT_ALLOWED";
+ StatusCode[StatusCode["INTERNAL_SERVER_ERROR"] = 500] = "INTERNAL_SERVER_ERROR";
+})(exports.StatusCode || (exports.StatusCode = {}));
+
+class AwsLambdaReceiver {
+ eventEmitter;
+ webhooksSecretToken;
+ constructor({ webhooksSecretToken }) {
+ this.webhooksSecretToken = webhooksSecretToken;
+ }
+ buildResponse(statusCode, body) {
+ return {
+ body: JSON.stringify(body),
+ headers: { "Content-Type": "application/json" },
+ statusCode
+ };
+ }
+ canInstall() {
+ return false;
+ }
+ init({ eventEmitter }) {
+ this.eventEmitter = eventEmitter;
+ }
+ start() {
+ return async (event, context) => {
+ console.debug("Processing Lambda event ", JSON.stringify(event), " with context ", JSON.stringify(context));
+ try {
+ const request = CommonHttpRequest.buildFromAwsEvent(event, this.webhooksSecretToken);
+ const processedEvent = request.processEvent();
+ if (isHashedUrlValidation(processedEvent)) {
+ return this.buildResponse(exports.StatusCode.OK, processedEvent);
+ }
+ else {
+ await this.eventEmitter?.emit(processedEvent.event, processedEvent);
+ return this.buildResponse(exports.StatusCode.OK, { message: "Zoom event processed successfully." });
+ }
+ }
+ catch (err) {
+ if (isCoreError(err, "CommonHttpRequestError")) {
+ return this.buildResponse(exports.StatusCode.BAD_REQUEST, { error: err.message });
+ }
+ else {
+ console.error(err);
+ return this.buildResponse(exports.StatusCode.INTERNAL_SERVER_ERROR, {
+ error: "An unknown error occurred. Please try again later."
+ });
+ }
+ }
+ };
+ }
+ async stop() {
+ return Promise.resolve();
+ }
+}
+
+const prependSlashes = (strs) => {
+ const rawStrs = Array.isArray(strs) ? strs : [strs];
+ const mappedStrs = rawStrs.map((rawStr) => (rawStr.startsWith("/") ? rawStr : `/${rawStr}`));
+ return (Array.isArray(strs) ? mappedStrs : mappedStrs[0]);
+};
+
+class TokenMemoryStore {
+ currentToken;
+ getLatestToken() {
+ return this.currentToken;
+ }
+ storeToken(token) {
+ this.currentToken = token;
+ }
+}
+
+/** @internal */
+const EXPIRATION_DELTA_SECONDS = 60;
+/** @internal */
+const OAUTH_BASE_URL = "https://zoom.us";
+/** @internal */
+const OAUTH_TOKEN_PATH = "/oauth/token";
+/**
+ * {@link Auth} is the base implementation of authentication for Zoom's APIs.
+ *
+ * It only requires a `clientId` and `tokenStore`, as these options are shared across
+ * all authentication implementations, namely OAuth and server-to-server auth (client
+ * credentials, JWT, and server-to-server OAuth.)
+ */
+class Auth {
+ clientId;
+ clientSecret;
+ tokenStore;
+ logger;
+ constructor({ clientId, clientSecret, tokenStore, logger }) {
+ this.clientId = clientId;
+ this.clientSecret = clientSecret;
+ this.tokenStore = tokenStore ?? new TokenMemoryStore();
+ this.logger = logger;
+ }
+ getBasicAuthorization() {
+ const clientCredentials = `${this.clientId}:${this.clientSecret}`;
+ return node_buffer.Buffer.from(clientCredentials).toString("base64");
+ }
+ isAlmostExpired(isoTime) {
+ const currentDate = dayjs();
+ return dayjs(isoTime).diff(currentDate, "seconds") <= EXPIRATION_DELTA_SECONDS;
+ }
+ async makeOAuthTokenRequest(grantType, payload) {
+ return await axios({
+ method: "POST",
+ url: new URL(OAUTH_TOKEN_PATH, OAUTH_BASE_URL).toString(),
+ headers: {
+ Authorization: `Basic ${this.getBasicAuthorization()}`,
+ "Content-Type": "application/x-www-form-urlencoded"
+ },
+ data: new URLSearchParams({ grant_type: grantType, ...payload }),
+ validateStatus: (status) => status >= 200 && status <= 299
+ });
+ }
+}
+
+const DEFAULT_EXPIRATION_SECONDS = 300; // 5 minutes
+/** @internal */
+const ISSUER_URN = "urn:zoom:rivet-sdk";
+class JwtStateStore {
+ encodedSecret;
+ expirationSeconds;
+ constructor({ expirationSeconds, stateSecret }) {
+ this.encodedSecret = new TextEncoder().encode(stateSecret);
+ this.expirationSeconds = expirationSeconds ?? DEFAULT_EXPIRATION_SECONDS;
+ }
+ async generateState() {
+ const issuedTime = dayjs();
+ const expirationTime = issuedTime.add(this.expirationSeconds, "seconds");
+ return await new jose.SignJWT({ random: node_crypto.randomBytes(8).toString("hex") })
+ .setProtectedHeader({ alg: "HS256", typ: "JWT" })
+ .setExpirationTime(expirationTime.toDate())
+ .setIssuedAt(issuedTime.toDate())
+ .setIssuer(ISSUER_URN)
+ .sign(this.encodedSecret);
+ }
+ async verifyState(state) {
+ try {
+ await jose.jwtVerify(state, this.encodedSecret, {
+ algorithms: ["HS256"],
+ issuer: ISSUER_URN,
+ typ: "JWT"
+ });
+ }
+ catch (err) {
+ throw new OAuthStateVerificationFailedError(`Failed to verify OAuth state: ${err.name}.`, {
+ cause: err
+ });
+ }
+ }
+}
+
+const DEFAULT_INSTALL_PATH = "/zoom/oauth/install";
+const DEFAULT_CALLBACK_PATH = "/zoom/oauth/callback";
+const DEFAULT_STATE_COOKIE_NAME = "zoom-oauth-state";
+const DEFAULT_STATE_COOKIE_MAX_AGE = 600; // 10 minutes in seconds
+const MAXIMUM_STATE_MAX_AGE = 3600; // 1 hour in seconds
+const OAUTH_AUTHORIZE_PATH = "/oauth/authorize";
+/**
+ * {@link InteractiveAuth}, an extension of {@link Auth}, is designed for use cases where authentication
+ * is initiated server-side, but requires manual authorization from a user, by redirecting the user to Zoom.
+ *
+ * In addition to all required fields from {@link AuthOptions}, this class requires a `redirectUri`, as this
+ * value is appended to the authorization URL when the user is redirected to Zoom and subsequently redirected
+ * back to an endpoint on this server.
+ *
+ * @see {@link https://developers.zoom.us/docs/integrations/oauth/ | OAuth - Zoom Developers}
+ */
+class InteractiveAuth extends Auth {
+ installerOptions;
+ async getAuthorizationUrl() {
+ if (!this.installerOptions?.stateStore) {
+ throw new OAuthInstallerNotInitializedError("Cannot generate authorization URL, state store not initialized.");
+ }
+ const authUrl = new URL(OAUTH_AUTHORIZE_PATH, OAUTH_BASE_URL);
+ const generatedState = await Promise.resolve(this.installerOptions.stateStore.generateState());
+ const { searchParams } = authUrl;
+ searchParams.set("client_id", this.clientId);
+ searchParams.set("redirect_uri", this.getFullRedirectUri());
+ searchParams.set("response_type", "code");
+ searchParams.set("state", generatedState);
+ return {
+ fullUrl: authUrl.toString(),
+ generatedState
+ };
+ }
+ getFullRedirectUri() {
+ if (!this.installerOptions?.redirectUri || !this.installerOptions.redirectUriPath) {
+ throw new OAuthInstallerNotInitializedError("Cannot generate full redirect URI, redirect URI or redirect URI path not initialized.");
+ }
+ return new URL(this.installerOptions.redirectUriPath, this.installerOptions.redirectUri).toString();
+ }
+ // Don't return a type; we want it to be as narrow as possible (used for ReturnType).
+ // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
+ setInstallerOptions({ directInstall, installPath, redirectUri, redirectUriPath, stateStore, stateCookieName, stateCookieMaxAge }) {
+ const updatedOptions = {
+ directInstall: Boolean(directInstall),
+ installPath: installPath ? prependSlashes(installPath) : DEFAULT_INSTALL_PATH,
+ redirectUri,
+ redirectUriPath: redirectUriPath ? prependSlashes(redirectUriPath) : DEFAULT_CALLBACK_PATH,
+ stateStore: isStateStore(stateStore) ? stateStore : new JwtStateStore({ stateSecret: stateStore }),
+ stateCookieName: stateCookieName ?? DEFAULT_STATE_COOKIE_NAME,
+ stateCookieMaxAge: stateCookieMaxAge ?? DEFAULT_STATE_COOKIE_MAX_AGE
+ };
+ if (updatedOptions.stateCookieMaxAge > MAXIMUM_STATE_MAX_AGE) {
+ // This method is always called from ProductClient, so this should be fine.
+ throw new ProductClientConstructionError(`stateCookieMaxAge cannot be greater than ${MAXIMUM_STATE_MAX_AGE.toString()} seconds.`);
+ }
+ this.installerOptions = updatedOptions;
+ return updatedOptions;
+ }
+}
+
+const mergeDefaultOptions = (options, defaultOptions) => ({ ...defaultOptions, ...options });
+
+const withDefaultTemplate = (cardContent, buttonContent) => `
+
+
+ Zoom Rivet
+
+
+
+
+
+
+
+
+
+
+ Zoom Rivet
+
+
+
+ ${cardContent}
+
+ ${buttonContent ?
+ `
`
+ : ""}
+
+
+
+
+`;
+/**
+ * Get the default HTML template that is shown to the developer/user when they visit the
+ * `installPath` endpoint, if Rivet currently has OAuth enabled.
+ *
+ * If `directInstall` is set to `true`, this function is not called; instead, the developer
+ * is directly redirected to Zoom's OAuth page.
+ */
+const defaultInstallTemplate = (authUrl) => withDefaultTemplate(`Click the button below to navigate to Zoom to authorize your application for use with Rivet.
`, { href: authUrl, text: "Authorize with Zoom" });
+/**
+ * Get the default HTML template that is shown to the developer/user when they successfully
+ * authorize Rivet with a Zoom application. This is shown once they have already been redirected
+ * to Zoom, and the authorization attempt with Rivet was successful.
+ */
+const defaultCallbackSuccessTemplate = () => withDefaultTemplate(`Your application has been successfully authorized with Rivet!
+ You may now close this page, or click the button below to redirect to Zoom's Marketplace.
`, { href: "https://marketplace.zoom.us", text: "Go to Marketplace" });
+/**
+ * Get the default HTML template that is shown to the developer when a known error occurs, meaning
+ * that the error is a core Rivet error.
+ */
+const defaultCallbackKnownErrorTemplate = (errName, errMessage) => withDefaultTemplate(`An error occurred authorizing Rivet with Zoom.
+ [${errName}]: ${errMessage}
`);
+/**
+ * Get the default HTML template that is shown to the developer when an unknown error occurs,
+ * meaning that the error is not known to be a core Rivet error and was thrown and not wrapped elsewhere.
+ */
+const defaultCallbackUnknownErrorTemplate = () => withDefaultTemplate(`An unknown error occurred authorizing Rivet with Zoom. Please see stacktrace for details.
+ Please see stacktrace for further details.
`);
+
+const secureServerOptionKeys = [
+ "ALPNProtocols",
+ "clientCertEngine",
+ "enableTrace",
+ "handshakeTimeout",
+ "rejectUnauthorized",
+ "requestCert",
+ "sessionTimeout",
+ "SNICallback",
+ "ticketKeys",
+ "pskCallback",
+ "pskIdentityHint",
+ "ca",
+ "cert",
+ "sigalgs",
+ "ciphers",
+ "clientCertEngine",
+ "crl",
+ "dhparam",
+ "ecdhCurve",
+ "honorCipherOrder",
+ "key",
+ "privateKeyEngine",
+ "privateKeyIdentifier",
+ "maxVersion",
+ "minVersion",
+ "passphrase",
+ "pfx",
+ "secureOptions",
+ "secureProtocol",
+ "sessionIdContext"
+];
+class HttpReceiver {
+ /** @internal */
+ static DEFAULT_ENDPOINT = "/zoom/events";
+ eventEmitter;
+ interactiveAuth;
+ /** @internal */
+ options;
+ server;
+ logger;
+ constructor(options) {
+ this.options = mergeDefaultOptions(options, { endpoints: HttpReceiver.DEFAULT_ENDPOINT });
+ this.options.endpoints = prependSlashes(this.options.endpoints);
+ this.logger =
+ options.logger ??
+ (() => {
+ const defaultLogger = new ConsoleLogger();
+ defaultLogger.setLevel(options.logLevel ?? exports.LogLevel.ERROR);
+ return defaultLogger;
+ })();
+ }
+ canInstall() {
+ return true;
+ }
+ buildDeletedStateCookieHeader(name) {
+ return `${name}=deleted; Expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly; Path=/; Secure;`;
+ }
+ buildStateCookieHeader(name, value, maxAge) {
+ return `${name}=${value}; HttpOnly; Max-Age=${maxAge.toString()}; Path=/; Secure;`;
+ }
+ getRequestCookie(req, name) {
+ return req.headers.cookie
+ ?.split(";")
+ .find((cookie) => cookie.trim().startsWith(name))
+ ?.split("=")[1]
+ ?.trim();
+ }
+ getServerCreator() {
+ return this.hasSecureOptions() ? node_https.createServer : node_http.createServer;
+ }
+ hasEndpoint(pathname) {
+ const { endpoints } = this.options;
+ return Array.isArray(endpoints) ? endpoints.includes(pathname) : endpoints === pathname;
+ }
+ hasSecureOptions() {
+ return Object.keys(this.options).some((option) => secureServerOptionKeys.includes(option));
+ }
+ init({ eventEmitter, interactiveAuth }) {
+ this.eventEmitter = eventEmitter;
+ this.interactiveAuth = interactiveAuth;
+ }
+ setResponseCookie(res, cookie) {
+ const existingCookies = res.getHeader("Set-Cookie") ?? [];
+ const cookiesArray = Array.isArray(existingCookies) ? existingCookies
+ : typeof existingCookies === "string" ? [existingCookies]
+ : [existingCookies.toString()];
+ res.setHeader("Set-Cookie", [...cookiesArray, cookie]);
+ }
+ areNormalizedUrlsEqual(firstUrl, secondUrl) {
+ const normalizedFirstUrl = firstUrl.endsWith("/") ? firstUrl.slice(0, -1) : firstUrl;
+ const normalizedSecondUrl = secondUrl.endsWith("/") ? secondUrl.slice(0, -1) : secondUrl;
+ return normalizedFirstUrl == normalizedSecondUrl;
+ }
+ start(port) {
+ if (typeof port !== "number" && isNaN(Number(port)) && !this.options.port && this.options.port !== 0) {
+ const errorMessage = "HTTP receiver must have number-coercible port found in constructor option or method call.";
+ this.logger.error(errorMessage);
+ throw new HTTPReceiverPortNotNumberError(errorMessage);
+ }
+ const listenPort = port ?? this.options.port;
+ return new Promise((resolve, reject) => {
+ this.server = this.getServerCreator()(this.options, (req, res) => void (async () => {
+ // `req.headers.host` should be used with care, as clients can manipulate this value.
+ // However, for this use case, the value is completely discarded and only `pathname`
+ // is used, which is why there's no further validation occurring.
+ const { pathname, searchParams } = new URL(req.url ?? "", `http://${req.headers.host ?? "localhost"}`);
+ const { interactiveAuth } = this;
+ this.logger.debug([pathname, searchParams]);
+ // Handle interactive OAuth flow, if user is going to installPath or redirectUriPath
+ if (interactiveAuth && interactiveAuth instanceof InteractiveAuth && interactiveAuth.installerOptions) {
+ const { installerOptions } = interactiveAuth;
+ if (this.areNormalizedUrlsEqual(pathname, installerOptions.installPath)) {
+ const { fullUrl, generatedState } = await interactiveAuth.getAuthorizationUrl();
+ const stateCookie = this.buildStateCookieHeader(installerOptions.stateCookieName, generatedState, installerOptions.stateCookieMaxAge);
+ await (installerOptions.directInstall ?
+ this.writeTemporaryRedirect(res, fullUrl, stateCookie)
+ : this.writeResponse(res, exports.StatusCode.OK, defaultInstallTemplate(fullUrl), stateCookie));
+ return;
+ }
+ // The user has navigated to the redirect page; init the code
+ if (this.areNormalizedUrlsEqual(pathname, installerOptions.redirectUriPath)) {
+ const authCodeParam = searchParams.get("code");
+ const stateCodeParam = searchParams.get("state");
+ const stateCodeCookie = this.getRequestCookie(req, installerOptions.stateCookieName);
+ try {
+ // Can't proceed if no auth code or state code in search parameters
+ if (!authCodeParam || !stateCodeParam) {
+ const errorMessage = "OAuth callback did not include code and/or state in request.";
+ this.logger.error(errorMessage);
+ throw new ReceiverOAuthFlowError(errorMessage);
+ }
+ // Ensure that the state token is verified, according to our state store
+ await installerOptions.stateStore.verifyState(stateCodeParam);
+ // Ensure that the state token we received (in search parameters) IS THE SAME as the state cookie
+ if (!stateCodeCookie || stateCodeCookie !== stateCodeParam) {
+ const errorMessage = "The state parameter is not from this browser session.";
+ this.logger.error(errorMessage);
+ throw new ReceiverOAuthFlowError(errorMessage);
+ }
+ await interactiveAuth.initRedirectCode(authCodeParam);
+ const deletionStateCookie = this.buildDeletedStateCookieHeader(installerOptions.stateCookieName);
+ await this.writeResponse(res, exports.StatusCode.OK, defaultCallbackSuccessTemplate(), deletionStateCookie);
+ return;
+ }
+ catch (err) {
+ const htmlTemplate = isCoreError(err) ?
+ defaultCallbackKnownErrorTemplate(err.name, err.message)
+ : defaultCallbackUnknownErrorTemplate();
+ const deletionStateCookie = this.buildDeletedStateCookieHeader(installerOptions.stateCookieName);
+ await this.writeResponse(res, exports.StatusCode.INTERNAL_SERVER_ERROR, htmlTemplate, deletionStateCookie);
+ return;
+ }
+ }
+ }
+ // This section is only applicable if we have a webhooks secret token—if we don't, then this
+ // receiver is, in effect, just for OAuth usage, meaning installing and validating.
+ if (this.options.webhooksSecretToken) {
+ // We currently only support a single endpoint, though this will change in the future.
+ if (!this.hasEndpoint(pathname)) {
+ await this.writeResponse(res, exports.StatusCode.NOT_FOUND);
+ return;
+ }
+ // We currently only support POST requests, as that's what Zoom sends.
+ if (req.method !== "post" && req.method !== "POST") {
+ await this.writeResponse(res, exports.StatusCode.METHOD_NOT_ALLOWED);
+ return;
+ }
+ try {
+ const { webhooksSecretToken } = this.options;
+ const request = await CommonHttpRequest.buildFromIncomingMessage(req, webhooksSecretToken);
+ const processedEvent = request.processEvent();
+ if (isHashedUrlValidation(processedEvent)) {
+ await this.writeResponse(res, exports.StatusCode.OK, processedEvent);
+ }
+ else {
+ await this.eventEmitter?.emit(processedEvent.event, processedEvent);
+ await this.writeResponse(res, exports.StatusCode.OK, { message: "Zoom event processed successfully." });
+ }
+ }
+ catch (err) {
+ if (isCoreError(err, "CommonHttpRequestError")) {
+ await this.writeResponse(res, exports.StatusCode.BAD_REQUEST, { error: err.message });
+ }
+ else {
+ console.error(err);
+ await this.writeResponse(res, exports.StatusCode.INTERNAL_SERVER_ERROR, {
+ error: "An unknown error occurred. Please try again later."
+ });
+ }
+ }
+ }
+ })());
+ this.server.on("close", () => (this.server = undefined));
+ this.server.on("error", (err) => {
+ this.logger.error(err.message);
+ reject(err);
+ });
+ this.server.listen(listenPort, () => {
+ if (!this.server) {
+ throw new ReceiverInconsistentStateError();
+ }
+ const { port: listeningPort } = this.server.address();
+ this.logger.info(`Listening on port ${listeningPort.toString()}`);
+ resolve(this.server);
+ });
+ });
+ }
+ stop() {
+ if (!this.server) {
+ throw new ReceiverInconsistentStateError();
+ }
+ return new Promise((resolve, reject) => {
+ this.server?.close((err) => {
+ if (err) {
+ this.logger.error(err.message);
+ reject(err);
+ }
+ });
+ this.server = undefined;
+ resolve();
+ });
+ }
+ writeTemporaryRedirect(res, location, setCookie) {
+ return new Promise((resolve) => {
+ if (setCookie) {
+ this.setResponseCookie(res, setCookie);
+ }
+ res.writeHead(exports.StatusCode.TEMPORARY_REDIRECT, { Location: location });
+ res.end(() => {
+ resolve();
+ });
+ });
+ }
+ writeResponse(res, statusCode, bodyContent, setCookie) {
+ return new Promise((resolve) => {
+ const mimeType = typeof bodyContent === "object" ? "application/json" : "text/html";
+ bodyContent = typeof bodyContent === "object" ? JSON.stringify(bodyContent) : bodyContent;
+ if (setCookie) {
+ this.setResponseCookie(res, setCookie);
+ }
+ res.writeHead(statusCode, { "Content-Type": mimeType });
+ res.end(bodyContent, () => {
+ resolve();
+ });
+ });
+ }
+}
+
+const version = "0.4.0";
+var packageJson = {
+ version: version};
+
+// eslint-disable-next-line no-control-regex
+const ASCII_CONTROL_CHARACTERS_PATTERN = /[\x00-\x1F\x7F]/;
+const NON_ASCII_CHARACTERS_PATTERN = /[^\x20-\x7E]/;
+class WebEndpoints {
+ /** @internal */
+ static DEFAULT_BASE_URL = "https://api.zoom.us/v2";
+ /** @internal */
+ static DEFAULT_MIME_TYPE = "application/json";
+ /** @internal */
+ static DEFAULT_TIMEOUT = 0;
+ /** @internal */
+ static GENERIC_ERROR_MESSAGE = "Request was unsuccessful with no further context";
+ /** @internal */
+ static TRACKING_ID_HEADER = "x-zm-trackingid";
+ /** @internal */
+ options;
+ constructor(options) {
+ this.options = mergeDefaultOptions(options, {
+ baseUrl: WebEndpoints.DEFAULT_BASE_URL,
+ hasCustomBaseUrl: typeof options.baseUrl !== "undefined",
+ timeout: WebEndpoints.DEFAULT_TIMEOUT
+ });
+ }
+ buildEndpoint({ method, baseUrlOverride, urlPathBuilder, requestMimeType }) {
+ // @ts-expect-error: Some arguments may not be present, but we pass them to makeRequest() anyway.
+ // prettier-ignore
+ // Next AST node is ignored by Prettier, even though it exceed maximum line length, because TypeScript
+ // won't allow ts-expect-error directive on multiple lines (https://github.com/Microsoft/TypeScript/issues/19573).
+ return (async ({ path, body, query }) => await this.makeRequest(method, baseUrlOverride, urlPathBuilder(path), requestMimeType ?? WebEndpoints.DEFAULT_MIME_TYPE, body, query)).bind(this);
+ }
+ buildUserAgent() {
+ const customUserAgentName = this.getCustomUserAgentName();
+ const userAgent = `rivet/${packageJson.version}${customUserAgentName ? ` (${customUserAgentName})` : ""}`;
+ return (`${userAgent} ` +
+ `${node_path.basename(process.title)}/${process.version.replace("v", "")} ` +
+ `${os.platform()}/${os.release()}`);
+ }
+ getCustomUserAgentName() {
+ const { userAgentName } = this.options;
+ if (!userAgentName || typeof userAgentName !== "string") {
+ return null;
+ }
+ return userAgentName
+ .replace(new RegExp(ASCII_CONTROL_CHARACTERS_PATTERN, "g"), "")
+ .replace(new RegExp(NON_ASCII_CHARACTERS_PATTERN, "g"), "")
+ .trim()
+ .slice(0, 100);
+ }
+ getHeaders(bearerToken, contentType) {
+ return {
+ Accept: "application/json",
+ Authorization: `Bearer ${bearerToken}`,
+ "Content-Type": contentType,
+ "User-Agent": this.buildUserAgent()
+ };
+ }
+ getRequestBody(args, mimeType) {
+ if (mimeType === "multipart/form-data") {
+ const formData = new FormData();
+ Object.entries(args).forEach(([key, value]) => {
+ formData.append(key, value);
+ });
+ return formData;
+ }
+ return args;
+ }
+ isOk(response) {
+ return response.status >= 200 && response.status <= 299;
+ }
+ isZoomResponseError(obj) {
+ return (typeof obj.code !== "undefined" &&
+ typeof obj.message !== "undefined");
+ }
+ async makeRequest(method, baseUrlOverride, url, requestContentType, bodyArgs, queryArgs) {
+ const { auth, baseUrl, doubleEncodeUrl, hasCustomBaseUrl, timeout } = this.options;
+ const bearerToken = await Promise.resolve(auth.getToken());
+ const urlToSend = doubleEncodeUrl ? encodeURIComponent(encodeURIComponent(url)) : url;
+ const response = await axios({
+ url: urlToSend,
+ method,
+ baseURL: hasCustomBaseUrl ? baseUrl : (baseUrlOverride ?? baseUrl),
+ headers: this.getHeaders(bearerToken, requestContentType),
+ params: queryArgs,
+ data: bodyArgs && this.getRequestBody(bodyArgs, requestContentType),
+ timeout: timeout,
+ beforeRedirect: (options) => {
+ options.headers = {
+ ...this.getHeaders(bearerToken, requestContentType),
+ ...options.headers
+ };
+ },
+ validateStatus: () => true // All responses are valid, not just 2xx
+ });
+ if (!this.isOk(response)) {
+ const { status: statusCode } = response;
+ if (this.isZoomResponseError(response.data)) {
+ const { code: errorCode, message: errorMessage } = response.data;
+ throw new ApiResponseError(`[${statusCode.toString()}/${errorCode.toString()}]: "${errorMessage}"`);
+ }
+ throw new ApiResponseError(`[${statusCode.toString()}]: ${WebEndpoints.GENERIC_ERROR_MESSAGE}`);
+ }
+ return {
+ data: response.data,
+ statusCode: response.status,
+ trackingId: response.headers[WebEndpoints.TRACKING_ID_HEADER]
+ };
+ }
+}
+
+class CommerceEndpoints extends WebEndpoints {
+ accountManagement = {
+ createEndCustomerAccount: this.buildEndpoint({ method: "POST", urlPathBuilder: () => `/commerce/account` }),
+ addContactsToExistingEndCustomerOrYourOwnAccount: this.buildEndpoint({ method: "POST", urlPathBuilder: ({ accountKey }) => `/commerce/account/${accountKey}/contacts` }),
+ getsListOfAllAccountsAssociatedWithZoomPartnerSubResellerByAccountType: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/accounts` }),
+ getAccountDetailsForZoomPartnerSubResellerEndCustomer: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ accountKey }) => `/commerce/accounts/${accountKey}` })
+ };
+ billing = {
+ getsAllBillingDocumentsForDistributorOrReseller: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/billing_documents` }),
+ getsPDFDocumentForBillingDocumentID: this.buildEndpoint({
+ method: "GET",
+ urlPathBuilder: ({ documentNumber }) => `/commerce/billing_documents/${documentNumber}/document`
+ }),
+ getDetailedInformationAboutSpecificInvoiceForDistributorOrReseller: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ invoiceNumber }) => `/commerce/invoices/${invoiceNumber}` })
+ };
+ dealRegistration = {
+ retrievesAllValidZoomCampaignsWhichDealRegistrationCanBeAssociatedWith: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/campaigns` }),
+ createsNewDealRegistrationForPartner: this.buildEndpoint({ method: "POST", urlPathBuilder: () => `/commerce/deal_registration` }),
+ getsAllValidDealRegistrationsForPartner: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/deal_registrations` }),
+ getsDetailsForDealRegistrationByDealRegistrationNumber: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ dealRegKey }) => `/commerce/deal_registrations/${dealRegKey}` }),
+ updatesExistingDealRegistration: this.buildEndpoint({ method: "PATCH", urlPathBuilder: ({ dealRegKey }) => `/commerce/deal_registrations/${dealRegKey}` })
+ };
+ order = {
+ createsSubscriptionOrderForZoomPartner: this.buildEndpoint({ method: "POST", urlPathBuilder: () => `/commerce/order` }),
+ previewDeltaOrderMetricsAndSubscriptionsInOrder: this.buildEndpoint({ method: "POST", urlPathBuilder: () => `/commerce/order/preview` }),
+ getsAllOrdersForZoomPartner: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/orders` }),
+ getsOrderDetailsByOrderReferenceID: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ orderReferenceId }) => `/commerce/orders/${orderReferenceId}` })
+ };
+ productCatalog = {
+ getsZoomProductCatalogForZoomPartner: this.buildEndpoint({ method: "POST", urlPathBuilder: () => `/commerce/catalog` }),
+ getsDetailsForZoomProductOrOffer: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ offerId }) => `/commerce/catalog/${offerId}` }),
+ getsPricebookInDownloadableFile: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/pricebooks` })
+ };
+ subscription = {
+ getsSubscriptionsForZoomPartner: this.buildEndpoint({ method: "GET", urlPathBuilder: () => `/commerce/subscriptions` }),
+ getsSubscriptionDetailsForGivenSubscriptionNumber: this.buildEndpoint({ method: "GET", urlPathBuilder: ({ subscriptionNumber }) => `/commerce/subscriptions/${subscriptionNumber}` }),
+ getsSubscriptionChangesVersionsForGivenSubscriptionNumber: this.buildEndpoint({
+ method: "GET",
+ urlPathBuilder: ({ subscriptionNumber }) => `/commerce/subscriptions/${subscriptionNumber}/versions`
+ })
+ };
+}
+
+class S2SAuth extends Auth {
+ accountId;
+ constructor({ accountId, ...restOptions }) {
+ super(restOptions);
+ this.accountId = accountId;
+ }
+ assertRawToken(obj) {
+ if (typeof obj.access_token !== "string" ||
+ typeof obj.expires_in !== "number" ||
+ typeof obj.scope !== "string") {
+ throw new S2SRawResponseError(`Failed to match raw response ${JSON.stringify(obj)} to expected shape.`);
+ }
+ }
+ async fetchAccessToken() {
+ const response = await this.makeOAuthTokenRequest("account_credentials", {
+ account_id: this.accountId
+ });
+ this.assertRawToken(response.data);
+ return this.mapAccessToken(response.data);
+ }
+ async getToken() {
+ const { tokenStore } = this;
+ const currentToken = await Promise.resolve(tokenStore.getLatestToken());
+ if (currentToken && !this.isAlmostExpired(currentToken.expirationTimeIso)) {
+ return currentToken.accessToken;
+ }
+ const token = await this.fetchAccessToken();
+ await Promise.resolve(tokenStore.storeToken(token));
+ return token.accessToken;
+ }
+ mapAccessToken({ access_token, expires_in, scope }) {
+ return {
+ accessToken: access_token,
+ expirationTimeIso: dayjs().add(expires_in, "seconds").toISOString(),
+ scopes: scope.includes(" ") ? scope.split(" ") : [scope]
+ };
+ }
+}
+
+// Utility functions for determining if client options include custom receiver, or, if not,
+// a webhooks secret token, as one of those is required!
+const hasExplicitReceiver = (obj) => typeof obj.receiver !== "undefined";
+const hasWebhooksSecretToken = (obj) => typeof obj.webhooksSecretToken !== "undefined";
+const isReceiverDisabled = (options) => typeof options.disableReceiver !== "undefined" && options.disableReceiver;
+const DEFAULT_HTTP_RECEIVER_PORT = 8080;
+const DEFAULT_LOGLEVEL = exports.LogLevel.ERROR;
+class ProductClient {
+ auth;
+ endpoints;
+ webEventConsumer;
+ receiver;
+ constructor(options) {
+ this.auth = this.initAuth(options);
+ this.endpoints = this.initEndpoints(this.auth, options);
+ this.webEventConsumer = this.initEventProcessor(this.endpoints, options);
+ // Only create an instance of `this.receiver` if the developer did not explicitly disable it.
+ if (!isReceiverDisabled(options)) {
+ // Throw error if receiver enabled, but no explicit receiver or a webhooks secret token provided.
+ // This is mainly applicable for products where we expect webhooks to be used; in events where webhooks are not
+ // expected, then it's perfectly fine for the developer to not provide a receiver of a webhooks secret token.
+ if (this.webEventConsumer && !hasExplicitReceiver(options) && !hasWebhooksSecretToken(options)) {
+ throw new ProductClientConstructionError("Options must include a custom receiver, or a webhooks secret token.");
+ }
+ this.receiver = (hasExplicitReceiver(options) ?
+ options.receiver
+ : this.initDefaultReceiver(options));
+ this.receiver.init({
+ eventEmitter: this.webEventConsumer,
+ interactiveAuth: this.auth instanceof InteractiveAuth ? this.auth : undefined
+ });
+ }
+ }
+ initDefaultReceiver({ port, webhooksSecretToken, logLevel }) {
+ return new HttpReceiver({
+ port: port ?? DEFAULT_HTTP_RECEIVER_PORT,
+ webhooksSecretToken,
+ logLevel: logLevel ?? DEFAULT_LOGLEVEL
+ });
+ }
+ async start() {
+ if (!this.receiver) {
+ throw new ReceiverInconsistentStateError("Receiver failed to construct. Was disableReceiver set to true?");
+ }
+ // Method call is wrapped in `await` and `Promise.resolve()`, as the call
+ // may or may not return a promise. This is not required when implementing `Receiver`.
+ return (await Promise.resolve(this.receiver.start()));
+ }
+}
+
+class CommerceS2SAuthClient extends ProductClient {
+ initAuth({ accountId, clientId, clientSecret, tokenStore }) {
+ return new S2SAuth({ accountId, clientId, clientSecret, tokenStore });
+ }
+ initEndpoints(auth, options) {
+ return new CommerceEndpoints({ auth, ...options });
+ }
+ initEventProcessor() {
+ return undefined;
+ }
+}
+
+exports.ApiResponseError = ApiResponseError;
+exports.AwsLambdaReceiver = AwsLambdaReceiver;
+exports.AwsReceiverRequestError = AwsReceiverRequestError;
+exports.ClientCredentialsRawResponseError = ClientCredentialsRawResponseError;
+exports.CommerceEndpoints = CommerceEndpoints;
+exports.CommerceS2SAuthClient = CommerceS2SAuthClient;
+exports.CommonHttpRequestError = CommonHttpRequestError;
+exports.ConsoleLogger = ConsoleLogger;
+exports.HTTPReceiverConstructionError = HTTPReceiverConstructionError;
+exports.HTTPReceiverPortNotNumberError = HTTPReceiverPortNotNumberError;
+exports.HTTPReceiverRequestError = HTTPReceiverRequestError;
+exports.HttpReceiver = HttpReceiver;
+exports.OAuthInstallerNotInitializedError = OAuthInstallerNotInitializedError;
+exports.OAuthStateVerificationFailedError = OAuthStateVerificationFailedError;
+exports.OAuthTokenDoesNotExistError = OAuthTokenDoesNotExistError;
+exports.OAuthTokenFetchFailedError = OAuthTokenFetchFailedError;
+exports.OAuthTokenRawResponseError = OAuthTokenRawResponseError;
+exports.OAuthTokenRefreshFailedError = OAuthTokenRefreshFailedError;
+exports.ProductClientConstructionError = ProductClientConstructionError;
+exports.ReceiverInconsistentStateError = ReceiverInconsistentStateError;
+exports.ReceiverOAuthFlowError = ReceiverOAuthFlowError;
+exports.S2SRawResponseError = S2SRawResponseError;
+exports.isCoreError = isCoreError;
+exports.isStateStore = isStateStore;
diff --git a/commerce/commerce.d.ts b/commerce/commerce.d.ts
new file mode 100644
index 0000000..f4deb91
--- /dev/null
+++ b/commerce/commerce.d.ts
@@ -0,0 +1,2444 @@
+import { AxiosResponse } from 'axios';
+import { LambdaFunctionURLResult, LambdaFunctionURLHandler } from 'aws-lambda';
+import { Server } from 'node:http';
+import { ServerOptions } from 'node:https';
+
+declare enum LogLevel {
+ ERROR = "error",
+ WARN = "warn",
+ INFO = "info",
+ DEBUG = "debug"
+}
+interface Logger {
+ /**
+ * Output debug message
+ * @param msg any data to be logged
+ */
+ debug(...msg: unknown[]): void;
+ /**
+ * Output info message
+ * @param msg any data to be logged
+ */
+ info(...msg: unknown[]): void;
+ /**
+ * Output warn message
+ * @param msg any data to be logged
+ */
+ warn(...msg: unknown[]): void;
+ /**
+ * Output error message
+ * @param msg any data to be logged
+ */
+ error(...msg: unknown[]): void;
+ /**
+ * Disables all logging below the given level
+ * @param level as a string, 'error' | 'warn' | 'info' | 'debug'
+ */
+ setLevel(level: LogLevel): void;
+ /**
+ * Return the current LogLevel.
+ */
+ getLevel(): LogLevel;
+ /**
+ * Name the instance so that it can be filtered when many loggers are sending output
+ * to the same destination.
+ * @param name as a string
+ */
+ setName(name: string): void;
+}
+declare class ConsoleLogger implements Logger {
+ private level;
+ private name;
+ private static labels;
+ private static severity;
+ constructor();
+ getLevel(): LogLevel;
+ setLevel(level: LogLevel): void;
+ setName(name: string): void;
+ debug(...msg: unknown[]): void;
+ info(...msg: unknown[]): void;
+ warn(...msg: unknown[]): void;
+ error(...msg: unknown[]): void;
+ private static isMoreOrEqualSevere;
+}
+
+type AllPropsOptional = Exclude<{
+ [P in keyof T]: undefined extends T[P] ? True : False;
+}[keyof T], undefined> extends True ? True : False;
+type Constructor = new (...args: any[]) => T;
+type MaybeArray = T | T[];
+type MaybePromise = T | Promise;
+type StringIndexed = Record;
+
+interface TokenStore {
+ getLatestToken(): MaybePromise