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

144 lines
6.4 KiB
Markdown

# 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
```bash
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`](./.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:
```bash
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:
```bash
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:
```bash
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:
```bash
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
```bash
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 `.gitignore`d.
- 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.