From d77b7372ec3f24d38e962c8070f9e807a5c4df22 Mon Sep 17 00:00:00 2001 From: cb-jananivijayan Date: Tue, 8 Sep 2026 13:59:43 +0530 Subject: [PATCH] oauth sample app --- sample-apps/oauth-crm-sync/.gitignore | 6 + sample-apps/oauth-crm-sync/README.md | 73 ++++++++++ sample-apps/oauth-crm-sync/handler/handler.js | 137 ++++++++++++++++++ sample-apps/oauth-crm-sync/manifest.json | 14 ++ sample-apps/oauth-crm-sync/oauth_config.json | 13 ++ .../test_data/customer_created.json | 23 +++ .../test_data/subscription_created.json | 36 +++++ sample-apps/oauth-crm-sync/types/types.d.ts | 10 ++ 8 files changed, 312 insertions(+) create mode 100644 sample-apps/oauth-crm-sync/.gitignore create mode 100644 sample-apps/oauth-crm-sync/README.md create mode 100644 sample-apps/oauth-crm-sync/handler/handler.js create mode 100644 sample-apps/oauth-crm-sync/manifest.json create mode 100644 sample-apps/oauth-crm-sync/oauth_config.json create mode 100644 sample-apps/oauth-crm-sync/test_data/customer_created.json create mode 100644 sample-apps/oauth-crm-sync/test_data/subscription_created.json create mode 100644 sample-apps/oauth-crm-sync/types/types.d.ts diff --git a/sample-apps/oauth-crm-sync/.gitignore b/sample-apps/oauth-crm-sync/.gitignore new file mode 100644 index 0000000..87abd1b --- /dev/null +++ b/sample-apps/oauth-crm-sync/.gitignore @@ -0,0 +1,6 @@ +oauth.local.json +.oauth.key +iparams.local.json +logs/ +dist/ +*.zip diff --git a/sample-apps/oauth-crm-sync/README.md b/sample-apps/oauth-crm-sync/README.md new file mode 100644 index 0000000..43fe060 --- /dev/null +++ b/sample-apps/oauth-crm-sync/README.md @@ -0,0 +1,73 @@ +# oauth-crm-sync + +Sample Chargebee custom app demonstrating OAuth 2.0 integration. + +Listens to `customer_created` and `subscription_created` events, then syncs the data to HubSpot CRM using an OAuth-authorized access token. + +## Setup + +### 1. Create a HubSpot OAuth app + +1. Go to [HubSpot Developer Portal](https://developers.hubspot.com/) → Apps → Create app +2. Under **Auth** → **OAuth**, note your **Client ID** and **Client Secret** +3. Add redirect URI: `http://localhost:10101/oauth/callback` (or the port you run the CLI on) +4. Add scopes: `crm.objects.contacts.read crm.objects.contacts.write` + +### 2. Configure credentials + +Edit `oauth_config.json` and replace the placeholder values: + +```json +{ + "connectors": { + "hubspot": { + "client_id": "", + "client_secret": "", + ... + } + } +} +``` + +### 3. Run the tester UI + +```bash +cb-apps run --dir . +``` + +Open `http://localhost:10101` in your browser. + +### 4. Authorize via OAuth + +1. Click the **OAuth** tab in the tester UI +2. Click **Connect** next to `hubspot` +3. Complete the HubSpot authorization flow in the popup +4. The tab shows **Authorized** once the token is saved + +### 5. Test an event + +1. Select `customer_created` from the event dropdown +2. Click **Invoke** — the handler creates or updates a HubSpot contact +3. Select `subscription_created` and click **Invoke** — a note is added in HubSpot + +## How OAuth tokens work + +- Tokens are encrypted with AES-256-GCM and stored in `oauth.local.json` (gitignored) +- The encryption key lives in `.oauth.key` (also gitignored, `0600` permissions) +- On every invocation, the CLI decrypts the token and injects it as: + ```js + payload.oauth_token.hubspot.access_token // Bearer token + payload.oauth_token.hubspot.token_type // "Bearer" + ``` +- Only `access_token` and `token_type` are exposed to handler code; `refresh_token` stays encrypted on disk + +## Files + +| File | Purpose | +|------|---------| +| `manifest.json` | App metadata and event-to-handler mapping | +| `oauth_config.json` | OAuth connector credentials (fill in your client_id/secret) | +| `handler/handler.js` | Event handler code | +| `test_data/` | Sample payloads for local testing | +| `types/types.d.ts` | TypeScript type hints for the handler payload | +| `.gitignore` | Excludes secrets and generated files | diff --git a/sample-apps/oauth-crm-sync/handler/handler.js b/sample-apps/oauth-crm-sync/handler/handler.js new file mode 100644 index 0000000..2af3b16 --- /dev/null +++ b/sample-apps/oauth-crm-sync/handler/handler.js @@ -0,0 +1,137 @@ +/** + * oauth-crm-sync — sample app demonstrating OAuth 2.0 token usage. + * + * How OAuth tokens reach this handler: + * 1. Add your OAuth credentials to oauth_config.json. + * 2. Open the tester UI (cb-apps run), click the OAuth tab, and click Connect. + * 3. After authorizing, tokens are encrypted and saved locally. + * 4. On every invocation, the CLI decrypts the token and injects it as + * payload.oauth_token["hubspot"].access_token (Bearer token ready to use). + * + * Available in payload.oauth_token only when: + * - oauth_config.json exists in the app directory, AND + * - the connector has been authorized via the tester UI + */ + +'use strict'; + +const HUBSPOT_API = 'https://api.hubapi.com'; + +module.exports = { + /** + * Creates or updates a HubSpot contact when a Chargebee customer is created. + * @param {import('../types/types.d.ts').HandlerPayload} payload + */ + onCustomerCreated: async function (payload) { + const token = getToken(payload, 'hubspot'); + const customer = payload.event.content.customer; + + const [firstName, ...rest] = (customer.first_name || '').split(' '); + const lastName = customer.last_name || rest.join(' ') || ''; + + const contact = { + properties: { + email: customer.email, + firstname: firstName, + lastname: lastName, + phone: customer.phone || '', + company: customer.company || '', + chargebee_customer_id: customer.id, + }, + }; + + const existing = await hubspotGet(token, `/crm/v3/objects/contacts/${customer.email}?idProperty=email`); + + if (existing.id) { + await hubspotPatch(token, `/crm/v3/objects/contacts/${existing.id}`, contact); + console.log(`[CRM Sync] Updated HubSpot contact ${existing.id} for customer ${customer.id}`); + } else { + const created = await hubspotPost(token, '/crm/v3/objects/contacts', contact); + console.log(`[CRM Sync] Created HubSpot contact ${created.id} for customer ${customer.id}`); + } + }, + + /** + * Logs a HubSpot note when a Chargebee subscription is created. + * @param {import('../types/types.d.ts').HandlerPayload} payload + */ + onSubscriptionCreated: async function (payload) { + const token = getToken(payload, 'hubspot'); + const subscription = payload.event.content.subscription; + const customer = payload.event.content.customer; + + const note = { + properties: { + hs_note_body: [ + `Chargebee subscription created`, + `Subscription ID: ${subscription.id}`, + `Plan: ${subscription.subscription_items?.[0]?.item_price_id ?? 'N/A'}`, + `Status: ${subscription.status}`, + `Customer: ${customer?.email ?? subscription.customer_id}`, + ].join('\n'), + hs_timestamp: new Date().toISOString(), + }, + }; + + const created = await hubspotPost(token, '/crm/v3/objects/notes', note); + console.log(`[CRM Sync] Created HubSpot note ${created.id} for subscription ${subscription.id}`); + }, +}; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +/** + * Returns the Bearer token for a connector. + * Throws a descriptive error when the connector hasn't been authorized yet, + * so the developer sees a clear message in the tester UI logs. + */ +function getToken(payload, connectorName) { + const token = payload.oauth_token?.[connectorName]?.access_token; + if (!token) { + throw new Error( + `OAuth token for '${connectorName}' is not available. ` + + `Open the tester UI, go to the OAuth tab, and click Connect to authorize.` + ); + } + return token; +} + +async function hubspotGet(token, path) { + const res = await fetch(`${HUBSPOT_API}${path}`, { + headers: { Authorization: `Bearer ${token}`, Accept: 'application/json' }, + }); + if (!res.ok && res.status !== 404) { + throw new Error(`HubSpot GET ${path} failed: ${res.status} ${await res.text()}`); + } + return res.status === 404 ? {} : res.json(); +} + +async function hubspotPost(token, path, body) { + const res = await fetch(`${HUBSPOT_API}${path}`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`HubSpot POST ${path} failed: ${res.status} ${await res.text()}`); + } + return res.json(); +} + +async function hubspotPatch(token, path, body) { + const res = await fetch(`${HUBSPOT_API}${path}`, { + method: 'PATCH', + headers: { + Authorization: `Bearer ${token}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + }); + if (!res.ok) { + throw new Error(`HubSpot PATCH ${path} failed: ${res.status} ${await res.text()}`); + } + return res.json(); +} diff --git a/sample-apps/oauth-crm-sync/manifest.json b/sample-apps/oauth-crm-sync/manifest.json new file mode 100644 index 0000000..03da673 --- /dev/null +++ b/sample-apps/oauth-crm-sync/manifest.json @@ -0,0 +1,14 @@ +{ + "name": "oauth-crm-sync", + "version": "1.0.0", + "description": "Sample app: syncs Chargebee customer and subscription events to a CRM via OAuth 2.0.", + "events": { + "customer_created": { + "handler": "onCustomerCreated" + }, + "subscription_created": { + "handler": "onSubscriptionCreated" + } + }, + "dependencies": {} +} diff --git a/sample-apps/oauth-crm-sync/oauth_config.json b/sample-apps/oauth-crm-sync/oauth_config.json new file mode 100644 index 0000000..97cc5a7 --- /dev/null +++ b/sample-apps/oauth-crm-sync/oauth_config.json @@ -0,0 +1,13 @@ +{ + "connectors": { + "hubspot": { + "client_id": "", + "client_secret": "", + "authorize_url": "https://app.hubspot.com/oauth/authorize", + "token_url": "https://api.hubapi.com/oauth/v1/token", + "options": { + "scope": "crm.objects.contacts.read crm.objects.contacts.write" + } + } + } +} diff --git a/sample-apps/oauth-crm-sync/test_data/customer_created.json b/sample-apps/oauth-crm-sync/test_data/customer_created.json new file mode 100644 index 0000000..cf6c3c0 --- /dev/null +++ b/sample-apps/oauth-crm-sync/test_data/customer_created.json @@ -0,0 +1,23 @@ +{ + "id": "evt_sample_cust_001", + "api_version": "v2", + "object": "event", + "occurred_at": 1700000000, + "source": "api", + "webhook_status": "success", + "webhooks": [], + "event_type": "customer_created", + "content": { + "customer": { + "id": "cust_sample_001", + "email": "alex.sample@example.com", + "first_name": "Alex", + "last_name": "Sample", + "phone": "+1-555-0100", + "company": "Example Corp", + "created_at": 1700000000, + "updated_at": 1700000000, + "object": "customer" + } + } +} diff --git a/sample-apps/oauth-crm-sync/test_data/subscription_created.json b/sample-apps/oauth-crm-sync/test_data/subscription_created.json new file mode 100644 index 0000000..8f9a046 --- /dev/null +++ b/sample-apps/oauth-crm-sync/test_data/subscription_created.json @@ -0,0 +1,36 @@ +{ + "id": "evt_sample_sub_001", + "api_version": "v2", + "object": "event", + "occurred_at": 1700000060, + "source": "api", + "webhook_status": "success", + "webhooks": [], + "event_type": "subscription_created", + "content": { + "subscription": { + "id": "sub_sample_001", + "customer_id": "cust_sample_001", + "status": "active", + "created_at": 1700000060, + "updated_at": 1700000060, + "subscription_items": [ + { + "item_price_id": "starter-monthly-usd", + "quantity": 1, + "unit_price": 4900, + "amount": 4900, + "object": "subscription_item" + } + ], + "object": "subscription" + }, + "customer": { + "id": "cust_sample_001", + "email": "alex.sample@example.com", + "first_name": "Alex", + "last_name": "Sample", + "object": "customer" + } + } +} diff --git a/sample-apps/oauth-crm-sync/types/types.d.ts b/sample-apps/oauth-crm-sync/types/types.d.ts new file mode 100644 index 0000000..f5160e4 --- /dev/null +++ b/sample-apps/oauth-crm-sync/types/types.d.ts @@ -0,0 +1,10 @@ +interface OAuthAccessToken { + access_token: string; + token_type: string; +} + +interface HandlerPayload { + event: import('@chargebee/chargebee-apps-shared').EventRecord; + iparams?: Record; + oauth_token?: Record; +}