The service account is silently filtered out of Object Type 109 (Store
Address / Hierarchy) despite having schema-level read on schema 68, so
every AQL against the store type returns total=0. Until that permission
is granted, resolve store numbers from a local cache populated by a
personal PAT (different auth path, different account, has the role).
- new: src/services/jira/assetsSyncClient.js — Basic-auth axios against
api.atlassian.com/jsm/assets/workspace/{ws}/v1, credentials sourced
from ASSETS_SYNC_EMAIL / ASSETS_SYNC_TOKEN (loaded from Keychain by
bin/load-assets-sync-secret.sh so the PAT never touches .env)
- new: src/services/jira/storesCache.js — in-memory Map + on-disk JSON
at data/stores.json (gitignored), atomic write, paginated full sync
via AQL (objectTypeId=N), boot-time load + background refresh if
stale, periodic setInterval every STORES_CACHE_REFRESH_HOURS
- new: bin/load-assets-sync-secret.sh — Keychain -> env var wrapper
(security find-generic-password -s jira-assets-sync -a <email>)
- change: resolveStoreAssetReference now tries cache -> live PAT -> the
existing service-account AQL, in that order; the fallback path is
preserved so this cleanly deactivates once the permission on #1 is
fixed. Error message names all three routes and points at the refresh
endpoint.
- new admin routes: GET /api/wxccai/admin/storesCache/status,
POST /api/wxccai/admin/storesCache/refresh
- app.js kicks off storesCache.init() after listen()
- config: STORES_CACHE_ENABLED / _PATH / _REFRESH_HOURS /
_STALE_AFTER_HOURS / _PAGE_SIZE / _MAX_PAGES, plus intFromEnv /
boolFromEnv helpers
- .gitignore adds data/; .env.example documents the new vars; README
adds an "Admin" endpoints section and a "Stores cache" setup guide
Co-authored-by: Cursor <cursoragent@cursor.com>
122 lines
5.5 KiB
Markdown
122 lines
5.5 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).
|
|
|
|
**One-time setup:**
|
|
|
|
```bash
|
|
# Store your PAT in macOS Keychain (never touches disk in cleartext)
|
|
security add-generic-password \
|
|
-s jira-assets-sync \
|
|
-a you@ae.com \
|
|
-w '<paste-your-atlassian-api-token>' \
|
|
-U
|
|
|
|
# Export the account name via .env / your shell
|
|
echo 'ASSETS_SYNC_EMAIL=you@ae.com' >> .env
|
|
```
|
|
|
|
**Run the server:**
|
|
|
|
```bash
|
|
# Wrapper loads the PAT from Keychain into ASSETS_SYNC_TOKEN before exec
|
|
./bin/load-assets-sync-secret.sh npm start
|
|
```
|
|
|
|
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.
|