const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager"); const { defaultProvider } = require("@aws-sdk/credential-provider-node"); const { isString, isEmpty } = require("lodash"); const CHATTER_BASE_URL = process.env.CHATTER_API_BASE_URL || "https://api.chatterresearch.com"; const AWS_REGION = process.env.AWS_REGION || "ca-central-1"; // Configure SecretsManager client with localstack support const secretsClientOptions = { region: AWS_REGION, credentials: defaultProvider() }; const isLocal = isString(process.env?.LOCALSTACK_HOSTNAME) && !isEmpty(process.env?.LOCALSTACK_HOSTNAME); if (isLocal) { secretsClientOptions.endpoint = `http://${process.env.LOCALSTACK_HOSTNAME}:4566`; } const secretsClient = new SecretsManagerClient(secretsClientOptions); /** * Chatter API Client for making requests to the Chatter API */ class ChatterApiClient { constructor({ baseUrl, apiToken }) { if (!apiToken) throw new Error("ChatterApiClient requires apiToken"); this.baseUrl = String(baseUrl || "").replace(/\/+$/, ""); this.apiToken = apiToken; } async createLocation(companyId, payload) { return this.request(`/api/v1/companies/${companyId}/locations`, { method: "POST", body: payload }); } async postInteraction(companyId, payload) { return this.request(`/api/v1/companies/${companyId}/solicitation/interaction`, { method: "POST", body: payload }); } async request(path, { method = "GET", body } = {}) { const res = await fetch(this.baseUrl + path, { method, headers: { "Api-Token": this.apiToken, Accept: "application/json", ...(body ? { "Content-Type": "application/json" } : {}) }, body: body ? JSON.stringify(body) : undefined }); const text = await res.text(); const data = text ? safeJson(text) : null; if (!res.ok) { const err = new Error(`Chatter API error ${res.status} | ${data?.message}`); err.status = res.status; err.data = data; const retryAfterMs = parseRetryAfterMs(res.headers.get("retry-after")); if (retryAfterMs != null) err.retryAfterMs = retryAfterMs; throw err; } return data; } } /** * Safely parse JSON, returning original text if parsing fails */ function safeJson(text) { try { return JSON.parse(text); } catch { return text; } } function parseRetryAfterMs(value) { if (!value) return null; const sec = Number(value); if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec * 1000); const dateMs = Date.parse(value); if (!Number.isFinite(dateMs)) return null; return Math.max(0, dateMs - Date.now()); } /** * Fetches Chatter API token from AWS Secrets Manager * SecretId format: CHATTER_COMPANY_KEY_ * * @param {string|number} companyId - The company ID * @returns {Promise} The API token */ async function getChatterApiToken(companyId) { const key = String(companyId ?? "").trim(); if (!key) throw new Error("getChatterApiToken: companyId is required"); // Optional override for development/testing if (process.env.CHATTER_API_TOKEN) return process.env.CHATTER_API_TOKEN; const secretId = `CHATTER_COMPANY_KEY_${key}`; const command = new GetSecretValueCommand({ SecretId: secretId }); const { SecretString, SecretBinary } = await secretsClient.send(command); const token = (SecretString && SecretString.trim()) || (SecretBinary && Buffer.from(SecretBinary, "base64").toString("ascii").trim()) || ""; if (!token) throw new Error(`Chatter API token secret is empty: ${secretId}`); return token; } /** * Creates a Chatter API client instance * * @param {string|number} companyId - The company ID * @param {string} [baseUrl] - Optional base URL override * @returns {Promise} Configured API client */ async function createChatterClient(companyId, baseUrl = CHATTER_BASE_URL) { const apiToken = await getChatterApiToken(companyId); return new ChatterApiClient({ baseUrl, apiToken }); } module.exports = { ChatterApiClient, getChatterApiToken, createChatterClient, safeJson, CHATTER_BASE_URL };