The bot runs on a Linux host where macOS Keychain isn't available, so .env is the default supported storage for the personal PAT. Both paths land in the same process.env slot, but the previous README framing implied Keychain was mandatory. - .env.example: promote ASSETS_SYNC_TOKEN from a comment to a real REPLACE_ME field; note chmod 600 and rotation guidance - README: split the setup section into "Setup A - production/Linux (.env)" and "Setup B - local dev on macOS (Keychain)"; clarify that the wrapper is a no-op if ASSETS_SYNC_TOKEN is already exported - bin/load-assets-sync-secret.sh: soften the header comment to match Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| bin | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| discover-ss-fields.js | ||
| discover-ss-request-types.js | ||
| docker-compose.yml | ||
| Dockerfile | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| ss-fields-266.json | ||
| ss-fields-267.json | ||
| ss-fields-268.json | ||
| ss-fields-269.json | ||
| ss-fields-270.json | ||
| ss-fields-271.json | ||
| ss-fields-272.json | ||
| ss-fields-273.json | ||
| ss-fields-274.json | ||
| ss-fields-275.json | ||
| ss-fields-426.json | ||
| ss-fields-493.json | ||
| ss-request-types-clean.json | ||
| ss-request-types.json | ||
wxcc-ai
Node/Express service that wires Webex Contact Center summaries, Jira tickets (core + JSM Store Support), and xAI Grok summarization into a single set of internal HTTP endpoints.
Setup
npm install
cp .env.example .env
# fill in JIRA_*, XAI_*, and optionally the Assets values
npm run dev
The server listens on PORT (default 1866).
Environment variables
See .env.example for the full list. The important ones:
| Var | Purpose |
|---|---|
JIRA_CLOUD_ID |
If set, requests use https://api.atlassian.com/ex/jira/{cloudId}. |
JIRA_BASE_URL |
Fallback for legacy site URLs (https://your-site.atlassian.net). |
JIRA_EMAIL, JIRA_API_TOKEN |
Basic auth to Jira. JIRA_AUTH_TYPE=bearer switches to bearer. |
JIRA_SERVICE_DESK_ID |
Numeric JSM service desk id for Store Support. |
JIRA_ASSETS_WORKSPACE_ID |
Assets workspace id (auto-discovered if omitted). |
JIRA_ASSETS_STORE_SCHEMA_ID, _OBJECT_TYPE_ID |
Locate the Store schema/type. |
JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE[_ID] |
Attribute name (or id) holding the store number in Assets. |
JIRA_STORE_CUSTOM_FIELD_ID |
Custom field on the JSM request that holds the Store Assets reference. |
XAI_API_KEY, XAI_BASE_URL |
Grok credentials for summary generation. |
Endpoints
Base path: /api/wxccai.
Read
GET /getticket?jiraKey=CS-1234— Grok-summarized single ticket.GET /open-tickets-by-reporter?email=user@example.com— Grok-summarized list of open tickets a person reported.GET /ticket/:key/status— raw status fields (no Grok).GET /ticket/:key/transitions— available workflow transitions.
Write
PATCH /ticket/:key— body:{ summary?, description?, priority?, labels?, assigneeAccountId?, additional? }POST /ticket/:key/comment— body:{ text, internal? }POST /ticket/:key/close— body:{ transitionName?, resolution?, comment?, internal? }(auto-picks the first "done" transition whentransitionNameomitted).
Store Support (JSM requests)
GET /ssRequestTypes— supportedsubTypevalues + Assets config summary.POST /createSSRequest— body includessubType,summary,storeNumber(auto-resolved via Assets),onBehalfOf,description,additional.
Webex webhook
POST /issueTranscript/:jiraKey— attaches audio + JSON transcript + human-readable transcript, then posts a restricted-visibility summary comment.
Admin
GET /admin/storesCache/status— snapshot of the local Assets store cache:{ storeCount, lastSyncAt, ageSeconds, syncing, assetsSyncConfigured, ... }. Safe for health checks.POST /admin/storesCache/refresh— force an immediate resync via the personal PAT (see below). Takes a few seconds and returns the new status.
Debug (non-production only)
GET /debug/assetsProbe?storeNumber=305— runs several AQL variants against Jira Assets and returns visible schemas + object-type detail + a computed diagnosis. Returns 404 whenNODE_ENV=production.
Stores cache (Assets workaround)
createSSRequest needs to translate a store number into an Assets object id. The shared service account is silently filtered out of the Store object type, so the app maintains a local storeNumber → objectId cache that's populated from a personal Atlassian PAT (a real human account with the right Assets role). The service account is still used for everything else (creating tickets, comments, attachments).
Both storage patterns end up in the same place — process.env.ASSETS_SYNC_TOKEN — so the runtime code path is identical. Pick whichever fits the host.
Setup A — production / Linux host (.env)
Put the values directly in .env (which is gitignored) and lock the file down:
cat >> .env <<'EOF'
ASSETS_SYNC_EMAIL=you@ae.com
ASSETS_SYNC_TOKEN=<paste-your-atlassian-api-token>
EOF
chmod 600 .env # only the bot user can read it
Then start normally:
npm start
Rotate the token in Atlassian → Account → Security → API tokens whenever the trust boundary on the host changes (new operator, offboarding, suspected leak). The app reloads it on the next process start.
Setup B — local dev on macOS (Keychain)
If you're running the app on a Mac and would rather not keep the PAT in .env, use the wrapper script — it pulls the token from Keychain into ASSETS_SYNC_TOKEN before exec'ing the process:
security add-generic-password \
-s jira-assets-sync \
-a you@ae.com \
-w '<paste-your-atlassian-api-token>' \
-U
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env # email in .env, token stays in Keychain
./bin/load-assets-sync-secret.sh npm start
If ASSETS_SYNC_TOKEN is already in the process env (Setup A above), the wrapper is a no-op and skips the Keychain lookup.
Runtime behavior
On boot the app loads the on-disk cache at data/stores.json, kicks off a background refresh if the snapshot is missing or older than STORES_CACHE_STALE_AFTER_HOURS, and schedules a periodic full resync every STORES_CACHE_REFRESH_HOURS. resolveStoreAssetReference then serves lookups from memory (sub-ms) with a live PAT lookup as fallback for brand-new stores.
Force a refresh at any time:
curl -X POST http://localhost:1866/api/wxccai/admin/storesCache/refresh
Jira Assets gotcha
createSSRequest resolves a store number to an Assets object reference (customfield_10261). Two independent permission layers must both grant access, or every AQL query silently returns total: 0:
- OAuth scopes on the API token:
read:cmdb-schema:jira,read:cmdb-type:jira,read:cmdb-object:jira,read:cmdb-attribute:jira(and thewrite:equivalents for updates). - Object Schema role membership in Jira Assets itself — the underlying user needs to be added to a role on the Store schema in Jira → Assets → Object schemas → Configure → Roles.
If AQL keeps returning total: 0 with HTTP 200, run the probe endpoint above; the diagnosis field will tell you exactly which layer is missing.
Docker
npm run docker:build
npm run docker:run
The image runs as a non-root user and exposes 1866. HEALTHCHECK pings GET /health.
Repo hygiene
- Secrets live only in
.env, which is.gitignored. - The two
discover-ss-*.jshelper scripts read from.env— never hardcode credentials in them. - The debug logger no longer echoes the outbound
Authorizationheader. Rotate any token that appears in olderlogs/*.logfiles.