Webex Contact Center AI Agent Helper Bot
Find a file
jmcqueen b4bc6462a3 Fix Assets sync: correct AQL pagination + Jira Cloud CMDB field format
Two bugs found during end-to-end smoke test on the stores cache
workaround (Forgejo #8):

1. Wrong pagination params on POST /object/aql. The endpoint uses
   startAt/maxResults as URL query params, not page/resultPerPage in the
   body. Passing the wrong param names caused the API to silently fall
   back to the default maxResults=25, so the first sync only cached 25
   stores. After fix: 2,661 stores paginated across 6 pages in 5.3s.

2. Wrong request-field shape for CMDB-object custom fields on Jira
   Cloud. resolveStoreAssetReference was returning
     [{ objectId: "75974" }]
   which is the legacy Data Center / Server shape. Cloud requires
     [{ id: "<workspaceId>:<objectId>" }]
   The old shape is silently accepted (HTTP 204) by REST and by
   POST /rest/servicedeskapi/request, but the field is never actually
   persisted \u2014 verified via direct REST GET showing customfield_10261:[].
   After fix: SS-20948 shows the store correctly populated.

- assetsSyncClient.js: use URLSearchParams to pass
  startAt/maxResults/includeAttributes; drop the old page/resultPerPage
  opts; jsdoc updated with pointer to Atlassian Assets API v1 docs.
- storesCache.js: pagination loop now advances by startAt +=
  values.length and trusts isLast (Atlassian caps `total` at 1000 as a
  hint on this endpoint, so we can't rely on it).
- assets.js: new buildStoreFieldRef(objectId) helper produces the
  correct Cloud shape from config.jira.assetsWorkspaceId; all three
  resolution paths (cache / live PAT / service-account fallback) route
  through it. Falls back to legacy shape with a loud error log if
  workspaceId is unset (misconfig).

End-to-end verified: SS-20948 was created for store 00782 (objectId
75974) via cache-only lookup and shows the field populated on both the
JSM request echo and a direct REST GET.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-07 10:38:28 -04:00
bin Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path 2026-07-07 10:22:40 -04:00
src Fix Assets sync: correct AQL pagination + Jira Cloud CMDB field format 2026-07-07 10:38:28 -04:00
.dockerignore Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
.env.example Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path 2026-07-07 10:22:40 -04:00
.gitignore Workaround #1: cache Assets store lookups via personal PAT sync 2026-07-07 10:18:28 -04:00
discover-ss-fields.js Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
discover-ss-request-types.js Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
docker-compose.yml Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
Dockerfile Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
package-lock.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
package.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
README.md Docs: prefer .env for ASSETS_SYNC_TOKEN; Keychain is optional dev path 2026-07-07 10:22:40 -04:00
ss-fields-266.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-267.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-268.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-269.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-270.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-271.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-272.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-273.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-274.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-275.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-426.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-fields-493.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-request-types-clean.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00
ss-request-types.json Initial commit: Webex CC + Jira + xAI service 2026-07-01 15:23:20 -04:00

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.