wxccai/README.md
jmcqueen 66255a7b0c Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path
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>
2026-07-07 10:22:40 -04:00

6.4 KiB

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 when transitionName omitted).

Store Support (JSM requests)

  • GET /ssRequestTypes — supported subType values + Assets config summary.
  • POST /createSSRequest — body includes subType, 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 when NODE_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:

  1. OAuth scopes on the API token: read:cmdb-schema:jira, read:cmdb-type:jira, read:cmdb-object:jira, read:cmdb-attribute:jira (and the write: equivalents for updates).
  2. 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-*.js helper scripts read from .env — never hardcode credentials in them.
  • The debug logger no longer echoes the outbound Authorization header. Rotate any token that appears in older logs/*.log files.