From 1070967870a907a1f8a2e458aa92f2869e1e9cef Mon Sep 17 00:00:00 2001 From: jmcqueen Date: Wed, 1 Jul 2026 15:23:20 -0400 Subject: [PATCH] Initial commit: Webex CC + Jira + xAI service Core capabilities: - Jira ticket lifecycle: status, update, comment, transitions, close - JSM Store Support request creation with Assets object resolution - Assets AQL diagnostic probe endpoint with schema/type introspection - Webex transcript ingestion (audio + JSON + human-readable) with restricted-visibility summary comments - Grok-powered single-ticket and open-tickets-by-reporter summaries Repo hygiene: - .gitignore covering .env, node_modules, logs, IDE dirs - .env.example documenting every env var - discover-ss-*.js scripts refactored to read credentials from .env - README covering setup, endpoints, and the Assets scope-vs-role gotcha Co-authored-by: Cursor --- .dockerignore | 17 + .env.example | 50 + .gitignore | 54 + Dockerfile | 58 + README.md | 84 ++ discover-ss-fields.js | 51 + discover-ss-request-types.js | 80 ++ docker-compose.yml | 15 + package-lock.json | 1872 +++++++++++++++++++++++++++ package.json | 26 + src/app.js | 134 ++ src/config/index.js | 67 + src/prompts/openTicketsSummary.txt | 30 + src/prompts/singleTicketSummary.txt | 30 + src/routes/wxccRoutes.js | 574 ++++++++ src/services/grokService.js | 135 ++ src/services/healthService.js | 106 ++ src/services/jiraService.js | 1425 ++++++++++++++++++++ src/utilities/adfToPlainText.js | 30 + src/utilities/logger.js | 60 + ss-fields-266.json | 89 ++ ss-fields-267.json | 115 ++ ss-fields-268.json | 202 +++ ss-fields-269.json | 89 ++ ss-fields-270.json | 105 ++ ss-fields-271.json | 89 ++ ss-fields-272.json | 75 ++ ss-fields-273.json | 103 ++ ss-fields-274.json | 61 + ss-fields-275.json | 105 ++ ss-fields-426.json | 103 ++ ss-fields-493.json | 216 ++++ ss-request-types-clean.json | 209 +++ ss-request-types.json | 643 +++++++++ 34 files changed, 7102 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100644 discover-ss-fields.js create mode 100644 discover-ss-request-types.js create mode 100644 docker-compose.yml create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/app.js create mode 100644 src/config/index.js create mode 100644 src/prompts/openTicketsSummary.txt create mode 100644 src/prompts/singleTicketSummary.txt create mode 100644 src/routes/wxccRoutes.js create mode 100644 src/services/grokService.js create mode 100644 src/services/healthService.js create mode 100644 src/services/jiraService.js create mode 100644 src/utilities/adfToPlainText.js create mode 100644 src/utilities/logger.js create mode 100644 ss-fields-266.json create mode 100644 ss-fields-267.json create mode 100644 ss-fields-268.json create mode 100644 ss-fields-269.json create mode 100644 ss-fields-270.json create mode 100644 ss-fields-271.json create mode 100644 ss-fields-272.json create mode 100644 ss-fields-273.json create mode 100644 ss-fields-274.json create mode 100644 ss-fields-275.json create mode 100644 ss-fields-426.json create mode 100644 ss-fields-493.json create mode 100644 ss-request-types-clean.json create mode 100644 ss-request-types.json diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..0cf7409 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +node_modules +npm-debug.log +Dockerfile +.dockerignore +.git +.gitignore +.gitattributes +README.md +.env +.env.* +!.env.example +logs +*.log +.continue +.vscode +.idea +*.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..c7048d6 --- /dev/null +++ b/.env.example @@ -0,0 +1,50 @@ +# ============================================= +# .env.example +# Copy to .env and fill in real values. DO NOT commit .env. +# ============================================= + +# --- Server --- +PORT=1866 +NODE_ENV=development + +# --- Jira --- +# Preferred: use the JIRA_CLOUD_ID gateway form. When set, requests go to +# https://api.atlassian.com/ex/jira/{cloudId}. If unset, JIRA_BASE_URL is used +# directly (e.g. https://your-site.atlassian.net). +JIRA_CLOUD_ID= +JIRA_BASE_URL=https://your-site.atlassian.net + +# Auth. 'basic' = email + API token (Atlassian API tokens). +# 'bearer' = OAuth bearer token in Authorization header. +JIRA_AUTH_TYPE=basic +JIRA_EMAIL=service-account@example.com +JIRA_API_TOKEN=REPLACE_ME + +# JSM Service Desk (Store Support). Numeric service desk id. +JIRA_SERVICE_DESK_ID=170 + +# Role name used to restrict visibility on comments posted by this service. +# Common values: "Administrators", "Service Desk Team". +JIRA_COMMENT_VISIBILITY_ROLE=Service Desk Team + +# --- Jira Assets (Store Number -> Assets object resolution) --- +# Workspace id for Jira Assets. If unset, the app tries to auto-discover via +# /rest/servicedeskapi/assets/workspace, but setting it explicitly is safer +# on tenants with more than one Assets workspace. +JIRA_ASSETS_WORKSPACE_ID= + +# Numeric object schema and object type id for the Stores schema in Assets. +JIRA_ASSETS_STORE_SCHEMA_ID=68 +JIRA_ASSETS_STORE_OBJECT_TYPE_ID=109 + +# The attribute name (as shown in the Assets UI) holding the store number. +JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE=Store Number +# Optional. If set, the app will also try attribute[]=... form in AQL. +JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE_ID= + +# The custom field on the JSM request that holds the Store Assets reference. +JIRA_STORE_CUSTOM_FIELD_ID=customfield_10261 + +# --- xAI (Grok) --- +XAI_API_KEY=REPLACE_ME +XAI_BASE_URL=https://api.x.ai/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f7dff63 --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# ============================================= +# Secrets — NEVER commit these +# ============================================= +.env +.env.* +!.env.example +*.pem +*.key +*.p12 +*.pfx +credentials* +secrets* + +# ============================================= +# Node +# ============================================= +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.npm/ +.yarn/ +.pnp.* + +# ============================================= +# Runtime / build artifacts +# ============================================= +logs/ +*.log +dist/ +build/ +coverage/ +.nyc_output/ +tmp/ +.tmp/ + +# ============================================= +# Editor / IDE / OS +# ============================================= +.vscode/ +.idea/ +.continue/ +*.swp +*.swo +*~ +.DS_Store +Thumbs.db +Desktop.ini + +# ============================================= +# Docker +# ============================================= +docker-compose.override.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..cd3db1a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# syntax=docker/dockerfile:1 + +# ============================================= +# Stage 1: Builder (install deps + prepare app) +# ============================================= +FROM node:22-alpine AS builder + +WORKDIR /app + +# Copy package files first for better layer caching +COPY package*.json ./ + +# Install all dependencies (including dev if needed) +RUN npm ci --ignore-scripts + +# Copy source code +COPY . . + +# Optional: If you ever add a build step (e.g. TypeScript), run it here +# RUN npm run build + +# ============================================= +# Stage 2: Production (lean runtime image) +# ============================================= +FROM node:22-alpine AS production + +# Create non-root user for security +RUN addgroup -g 1001 -S nodejs && \ + adduser -S -u 1001 -G nodejs nodejs + +WORKDIR /app + +# Copy package files +COPY package*.json ./ + +# Install ONLY production dependencies +RUN npm ci --only=production --ignore-scripts && \ + npm cache clean --force + +# Copy built/ready files from builder stage +COPY --from=builder /app/src ./src +COPY --from=builder /app/node_modules ./node_modules + +# Change ownership to non-root user +RUN chown -R nodejs:nodejs /app + +# Switch to non-root user +USER nodejs + +# Expose the port your app listens on (from your config) +EXPOSE 1866 + +# Detailed healthcheck using the new /health endpoint +HEALTHCHECK --interval=30s --timeout=10s --start-period=20s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:1866/health || exit 1 + +# Start the app directly with node (better than npm start in containers) +CMD ["node", "src/app.js"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfa49ac --- /dev/null +++ b/README.md @@ -0,0 +1,84 @@ +# 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. + +### 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`. + +## 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. diff --git a/discover-ss-fields.js b/discover-ss-fields.js new file mode 100644 index 0000000..93c64a6 --- /dev/null +++ b/discover-ss-fields.js @@ -0,0 +1,51 @@ +// discover-ss-fields.js +// One-off helper to dump JSM request-type field definitions for the Store +// Support service desk. Output files (ss-fields-.json) are used to keep +// REQUEST_TYPE_MAP in src/services/jiraService.js in sync with what JSM +// actually accepts. +// +// Usage: node discover-ss-fields.js +// Reads credentials from .env — never commit real tokens into this file. + +import 'dotenv/config'; +import fetch from 'node-fetch'; +import fs from 'fs/promises'; + +const BASE_URL = (process.env.JIRA_BASE_URL || 'https://your-site.atlassian.net').replace(/\/$/, ''); +const SERVICE_DESK_ID = process.env.JIRA_SERVICE_DESK_ID || '170'; +const EMAIL = process.env.JIRA_EMAIL; +const TOKEN = process.env.JIRA_API_TOKEN; + +if (!EMAIL || !TOKEN) { + console.error('JIRA_EMAIL and JIRA_API_TOKEN must be set in .env'); + process.exit(1); +} + +const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64') +}; + +// Request-type ids that we care about (matches REQUEST_TYPE_MAP in jiraService.js). +const importantTypes = [269, 266, 273, 274, 267, 275, 272, 268, 271, 270, 426, 493]; + +async function getFields(requestTypeId) { + console.log(`Fetching fields for request type ${requestTypeId}...`); + const res = await fetch(`${BASE_URL}/rest/servicedeskapi/servicedesk/${SERVICE_DESK_ID}/requesttype/${requestTypeId}/field`, { headers }); + const data = await res.json(); + + if (res.ok) { + await fs.writeFile(`ss-fields-${requestTypeId}.json`, JSON.stringify(data, null, 2)); + console.log(` saved ss-fields-${requestTypeId}.json`); + } else { + console.error(' error:', data); + } + return data; +} + +for (const id of importantTypes) { + await getFields(id); +} + +console.log('\nDone. Check the generated ss-fields-*.json files.'); diff --git a/discover-ss-request-types.js b/discover-ss-request-types.js new file mode 100644 index 0000000..6835b26 --- /dev/null +++ b/discover-ss-request-types.js @@ -0,0 +1,80 @@ +// discover-ss-request-types.js +// One-off helper to list all JSM request types on the Store Support service +// desk. Produces ss-request-types.json (raw) and ss-request-types-clean.json +// (id/name/issueTypeId only) — both useful for maintaining REQUEST_TYPE_MAP +// in src/services/jiraService.js. +// +// Usage: node discover-ss-request-types.js +// Reads credentials from .env — never commit real tokens into this file. + +import 'dotenv/config'; +import fetch from 'node-fetch'; +import fs from 'fs/promises'; + +const BASE_URL = (process.env.JIRA_BASE_URL || 'https://your-site.atlassian.net').replace(/\/$/, ''); +const SERVICE_DESK_ID = process.env.JIRA_SERVICE_DESK_ID || '170'; +const EMAIL = process.env.JIRA_EMAIL; +const TOKEN = process.env.JIRA_API_TOKEN; + +if (!EMAIL || !TOKEN) { + console.error('JIRA_EMAIL and JIRA_API_TOKEN must be set in .env'); + process.exit(1); +} + +const headers = { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + 'Authorization': 'Basic ' + Buffer.from(`${EMAIL}:${TOKEN}`).toString('base64') +}; + +async function getRequestTypes() { + console.log(`Fetching request types for service desk ${SERVICE_DESK_ID}...\n`); + + const res = await fetch(`${BASE_URL}/rest/servicedeskapi/servicedesk/${SERVICE_DESK_ID}/requesttype?limit=100`, { headers }); + const data = await res.json(); + + if (!res.ok) { + console.error('Error:', data); + return; + } + + console.log(`Found ${data.values.length} request types.\n`); + + await fs.writeFile('ss-request-types.json', JSON.stringify(data, null, 2)); + console.log('Full list saved to ss-request-types.json'); + + const mapping = data.values.map(rt => ({ + id: rt.id, + name: rt.name, + defaultName: rt.defaultName, + description: rt.description, + issueTypeId: rt.issueTypeId, + groupIds: rt.groupIds, + canCreateRequest: rt.canCreateRequest + })); + + await fs.writeFile('ss-request-types-clean.json', JSON.stringify(mapping, null, 2)); + console.log('Clean mapping saved to ss-request-types-clean.json'); +} + +async function getFieldsForRequestType(requestTypeId) { + console.log(`\nFetching fields for request type ${requestTypeId}...`); + const res = await fetch(`${BASE_URL}/rest/servicedeskapi/servicedesk/${SERVICE_DESK_ID}/requesttype/${requestTypeId}/field`, { headers }); + const data = await res.json(); + + if (!res.ok) { + console.error('Error:', data); + return null; + } + + await fs.writeFile(`fields-requesttype-${requestTypeId}.json`, JSON.stringify(data, null, 2)); + console.log(`Fields saved to fields-requesttype-${requestTypeId}.json`); + return data; +} + +await getRequestTypes(); + +// Fetch fields for specific request types by uncommenting below. +// await getFieldsForRequestType(269); // Register Not Functioning Properly +// await getFieldsForRequestType(266); // Broken Device / Hardware +// await getFieldsForRequestType(426); // UKG Pro / Workforce Management Issues diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..63cf095 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,15 @@ +services: + wxcc-jira-grok: + build: . + ports: + - "1866:1866" + env_file: + - .env + restart: unless-stopped + # Optional: healthcheck at compose level + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:1866/health"] + interval: 30s + timeout: 8s + start_period: 15s + retries: 3 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..698a46a --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1872 @@ +{ + "name": "wxcc-ai", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wxcc-ai", + "version": "1.0.0", + "dependencies": { + "axios": "^1.8.0", + "axios-retry": "^4.5.0", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "form-data": "^4.0.5", + "node-fetch": "^3.3.2", + "openai": "^4.85.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", + "license": "MIT", + "dependencies": { + "@so-ric/colorspace": "^1.1.6", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/axios-retry": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/axios-retry/-/axios-retry-4.5.0.tgz", + "integrity": "sha512-aR99oXhpEDGo0UuAlYcn2iGRds30k366Zfa05XWScR9QaQD4JYiP3/1Qt1u7YlefUOK+cn0CcwoL1oefavQUlQ==", + "license": "Apache-2.0", + "dependencies": { + "is-retry-allowed": "^2.2.0" + }, + "peerDependencies": { + "axios": "0.x || 1.x" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/color": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", + "integrity": "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.1.3", + "color-string": "^2.1.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/color-convert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.3.tgz", + "integrity": "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/color-name": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.1.0.tgz", + "integrity": "sha512-1bPaDNFm0axzE4MEAzKPuqKWeRaT43U/hyxKPBdqTfmPF+d6n7FSoTFxLVULUJOmiLp01KjhIPPH+HrXZJN4Rg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/color-string": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", + "integrity": "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", + "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", + "license": "MIT" + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/fetch-blob/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", + "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "statuses": "~2.0.2", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-2.2.0.tgz", + "integrity": "sha512-XVm7LOeLpTW4jV19QSH38vkswxoLud8sQ57YwJVTPWdiaI9I8keEhGFpBlslyVsgdQy4Opg8QOLb8YRgsyZiQg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/logform/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/nodemon": { + "version": "3.1.14", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.14.tgz", + "integrity": "sha512-jakjZi93UtB3jHMWsXL68FXSAosbLfY0In5gtKq3niLSkrWznrVBzXFNOEMJUfc9+Ke7SHWoAZsiMkNP3vq6Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^10.2.1", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/nodemon/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/openai/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz", + "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", + "license": "MIT" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/winston": { + "version": "3.19.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", + "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.8", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..aecaa55 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "wxcc-ai", + "version": "1.0.0", + "description": "Webex Contact Center Jira summarizer powered by xAI Grok", + "main": "src/app.js", + "type": "module", + "scripts": { + "start": "node src/app.js", + "dev": "nodemon src/app.js", + "docker:build": "docker build -t wxcc-jira-grok:latest .", + "docker:run": "docker run -p 1866:1866 --env-file .env --rm wxcc-jira-grok:latest" + }, + "dependencies": { + "axios": "^1.8.0", + "axios-retry": "^4.5.0", + "dotenv": "^16.4.5", + "express": "^4.21.0", + "form-data": "^4.0.5", + "node-fetch": "^3.3.2", + "openai": "^4.85.0", + "winston": "^3.17.0" + }, + "devDependencies": { + "nodemon": "^3.1.0" + } +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..96e442c --- /dev/null +++ b/src/app.js @@ -0,0 +1,134 @@ +import express from 'express'; +import config from './config/index.js'; +import wxccRoutes from './routes/wxccRoutes.js'; +import { getDetailedHealth } from './services/healthService.js'; +import logger from './utilities/logger.js'; + +const app = express(); + +// Early request logging (BEFORE body parsers). +// This guarantees we see evidence of requests reaching the app (even if JSON +// parsing fails, wrong method, or 404). Critical for debugging webhooks from +// external systems like Webex CC where the request may never reach our +// route handler. +app.use((req, res, next) => { + const isApi = req.path.startsWith('/api/') || req.url.startsWith('/api/'); + const isTranscript = req.path.includes('/issueTranscript') || req.url.includes('/issueTranscript'); + + if (isApi || isTranscript) { + const logInfo = { + method: req.method, + url: req.originalUrl || req.url, + ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress, + contentType: req.headers['content-type'], + contentLength: req.headers['content-length'], + userAgent: req.headers['user-agent'] + }; + + // Direct console for maximum visibility in docker logs / platform stdout + if (isTranscript) { + console.log('TRANSCRIPT WEBHOOK REQUEST:', JSON.stringify(logInfo)); + } else { + console.log('API REQUEST:', JSON.stringify(logInfo)); + } + + // Also via structured logger (goes to app.log + console per config) + logger.info('Incoming API request', logInfo); + } + next(); +}); + +// Special handling for the Webex transcript webhook. +// The upstream Java client sometimes includes unescaped control characters +// (newlines, etc.) inside the Summaries strings, which breaks strict JSON.parse. +// We use express.raw() for this specific path so we always get the buffer, +// then we sanitize and parse inside the route handler. +app.use('/api/wxccai/issueTranscript', express.raw({ + type: 'application/json', + limit: '10mb', + verify: (req, res, buf) => { + req.rawBody = buf; // keep raw for possible future needs + } +})); + +// Middleware - normal JSON parser for all other routes +app.use(express.json({ + limit: '10mb', + verify: (req, res, buf) => { + req.rawBody = buf; // Store raw buffer for fallback parsing + } +})); + +// ======================== +// Health Check Routes +// ======================== +app.get('/health', async (req, res) => { + try { + const health = await getDetailedHealth(); + const statusCode = health.status === 'unhealthy' ? 503 : 200; + res.status(statusCode).json(health); + } catch (err) { + logger.error('Health check failed:', err); + res.status(503).json({ + status: 'unhealthy', + message: 'Health check failed', + error: err.message + }); + } +}); + +app.get('/health/detailed', async (req, res) => { + try { + const health = await getDetailedHealth(); + res.json(health); + } catch (err) { + res.status(503).json({ + status: 'unhealthy', + message: 'Detailed health check failed', + error: err.message + }); + } +}); + +// Simple welcome route at root +app.get('/', (req, res) => { + res.json({ + message: "Webex Contact Center + Jira + xAI Grok API service is running", + version: process.env.npm_package_version || "1.0.0", + endpoints: { + health: "/health", + singleTicket: "/api/wxccai/getticket?jiraKey=CS-1234 (Grok-summarized)", + openTicketsByReporter: "/api/wxccai/open-tickets-by-reporter?email=user@example.com", + ticketStatus: "GET /api/wxccai/ticket/:key/status (raw fields, no Grok)", + updateTicket: "PATCH /api/wxccai/ticket/:key body: { summary?, description?, priority?, labels?, assigneeAccountId?, additional? }", + addComment: "POST /api/wxccai/ticket/:key/comment body: { text, internal? }", + transitions: "GET /api/wxccai/ticket/:key/transitions", + closeTicket: "POST /api/wxccai/ticket/:key/close body: { transitionName?, resolution?, comment?, internal? }", + createSSRequest: "POST /api/wxccai/createSSRequest (store support ticket creator; storeNumber auto-resolved via Assets)", + ssRequestTypes: "GET /api/wxccai/ssRequestTypes (lists exact subType values + config notes)", + assetsProbe: "GET /api/wxccai/debug/assetsProbe?storeNumber=305 (non-production only)" + } + }); +}); + +// ======================== +// Main API Routes (mounted under /api) +// ======================== +app.use('/api', wxccRoutes); + +// 404 handler +app.use((req, res) => { + logger.warn('Route not found', { + method: req.method, + url: req.originalUrl || req.url, + ip: req.headers['x-forwarded-for'] || req.socket?.remoteAddress + }); + res.status(404).json({ + error: "Route not found", + message: "Available endpoints are under /api/wxccai/..." + }); +}); + +app.listen(config.port, () => { + logger.info(`Server running on port ${config.port} in ${config.nodeEnv} mode`); +}); \ No newline at end of file diff --git a/src/config/index.js b/src/config/index.js new file mode 100644 index 0000000..df0d20f --- /dev/null +++ b/src/config/index.js @@ -0,0 +1,67 @@ +import dotenv from 'dotenv'; +dotenv.config(); + +const cloudId = process.env.JIRA_CLOUD_ID?.trim(); +let baseUrl = process.env.JIRA_BASE_URL?.trim(); + +if (cloudId) { + // When JIRA_CLOUD_ID is provided, use the api.atlassian.com/ex/jira gateway form. + // This is the current style for addressing a specific Jira Cloud site (e.g. for + // /rest/servicedeskapi/... and /rest/api/3/... calls). Falls back to JIRA_BASE_URL + // for legacy site-based URLs like https://your-site.atlassian.net when no cloud ID is set. + baseUrl = `https://api.atlassian.com/ex/jira/${cloudId}`; +} else if (baseUrl) { + // Normalize legacy site base (remove trailing slash) + baseUrl = baseUrl.replace(/\/$/, ''); +} + +// Validate required environment variables +const requiredEnvVars = [ + 'JIRA_EMAIL', + 'JIRA_API_TOKEN', + 'XAI_API_KEY' +]; + +const missingVars = requiredEnvVars.filter(env => !process.env[env]); +if (missingVars.length > 0) { + console.warn(`Missing required environment variables: ${missingVars.join(', ')}`); +} + +export const config = { + port: process.env.PORT || 1866, + nodeEnv: process.env.NODE_ENV || 'development', + + jira: { + baseUrl, + cloudId: cloudId || null, + authType: (process.env.JIRA_AUTH_TYPE || 'basic').toLowerCase(), // 'basic' or 'bearer' + email: process.env.JIRA_EMAIL, + apiToken: process.env.JIRA_API_TOKEN, + serviceDeskId: process.env.JIRA_SERVICE_DESK_ID, + // Role used for restricted/internal visibility on summary comments. + // Common values: "Administrators", or a project role you have defined. + commentVisibilityRole: process.env.JIRA_COMMENT_VISIBILITY_ROLE || 'Administrators', + + // Assets / CMDB configuration for Store Number object references. + // These are used for the "service object" / Assets object lookup when creating SS tickets. + // The lookup now uses the api.atlassian.com workspace-scoped endpoint: + // https://api.atlassian.com/jsm/assets/workspace/{workspaceId}/v1/object/aql + assetsWorkspaceId: process.env.JIRA_ASSETS_WORKSPACE_ID, + // (optional) numeric schema or object type details + assetsStoreSchemaId: process.env.JIRA_ASSETS_STORE_SCHEMA_ID, + assetsStoreObjectTypeId: process.env.JIRA_ASSETS_STORE_OBJECT_TYPE_ID || '109', + assetsStoreNumberAttribute: process.env.JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE || 'Store Number', + // If you know the attribute ID (e.g. 635 from schema), we can try "attribute[635]" form in AQL + assetsStoreNumberAttributeId: process.env.JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE_ID || null, + + // The custom field ID on the request that holds the Store Assets reference + storeCustomFieldId: process.env.JIRA_STORE_CUSTOM_FIELD_ID || 'customfield_10261', + }, + + xai: { + apiKey: process.env.XAI_API_KEY, + baseUrl: process.env.XAI_BASE_URL || 'https://api.x.ai/v1', + } +}; + +export default config; \ No newline at end of file diff --git a/src/prompts/openTicketsSummary.txt b/src/prompts/openTicketsSummary.txt new file mode 100644 index 0000000..4c11ce6 --- /dev/null +++ b/src/prompts/openTicketsSummary.txt @@ -0,0 +1,30 @@ +You are assisting a Webex Contact Center agent. Return ONLY a valid JSON array with one object per ticket. No extra text, no markdown, no explanations. + +For each ticket, create a short, neutral, spoken-friendly summary (maximum 2 sentences) that includes: +- The initial problem / request from the customer (from the description) +- Key actions that have been taken so far (from description and public comments) +- Current state and any blockers or next steps (based especially on the most recent public notes/comments) + +Rules: +- Do NOT mention any names or people +- Be factual and concise — easy to read aloud +- Prioritize the latest public comments to determine current status + +Each object must have exactly these fields: +- jiraKey: string +- currentStatus: the exact current status name from Jira (e.g. "In Progress", "Waiting for Customer", "To Do") +- summary: string (the spoken summary described above) +- lastUpdated: ISO timestamp string from the ticket or null + +Example: +[ + { + "jiraKey": "CS-1234", + "currentStatus": "In Progress", + "summary": "Customer reported slow performance when generating reports. Backend indexing was optimized and a new cache layer was added last week. Final performance testing is now underway.", + "lastUpdated": "2026-04-13T08:38:00-05:00" + } +] + +Tickets context: +{{context}} \ No newline at end of file diff --git a/src/prompts/singleTicketSummary.txt b/src/prompts/singleTicketSummary.txt new file mode 100644 index 0000000..dcab98e --- /dev/null +++ b/src/prompts/singleTicketSummary.txt @@ -0,0 +1,30 @@ +You are assisting a Webex Contact Center agent. Return ONLY a valid JSON array with one object per ticket. No extra text, no markdown, no explanations. + +For each ticket, create a short, neutral, spoken-friendly summary (maximum 2 sentences) that includes: +- The initial problem / request from the customer (from the description) +- Key actions that have been taken so far (from description and public comments) +- Current state and any blockers or next steps (based especially on the most recent public notes/comments) + +Rules: +- Do NOT mention any names or people +- Be factual and concise — easy to read aloud +- Prioritize the latest public comments to determine current status + +Each object must have exactly these fields: +- jiraKey: string +- currentStatus: the exact current status name from Jira (e.g. "In Progress", "Waiting for Customer", "To Do") +- summary: string (the spoken summary described above) +- lastUpdated: ISO timestamp string from the ticket or null + +Example: +[ + { + "jiraKey": "CS-1234", + "currentStatus": "In Progress", + "summary": "Customer reported slow performance when generating reports. Backend indexing was optimized and a new cache layer was added last week. Final performance testing is now underway.", + "lastUpdated": "2026-04-13T08:38:00-05:00" + } +] + +Tickets context: +{{context}} diff --git a/src/routes/wxccRoutes.js b/src/routes/wxccRoutes.js new file mode 100644 index 0000000..fc732f7 --- /dev/null +++ b/src/routes/wxccRoutes.js @@ -0,0 +1,574 @@ +import express from 'express'; +import axios from 'axios'; +import axiosRetry from 'axios-retry'; +import { logger, webexLogger } from '../utilities/logger.js'; +import * as jiraService from '../services/jiraService.js'; +import grokService from '../services/grokService.js'; +import config from '../config/index.js'; + +// Configure retry for transient failures +axiosRetry(axios, { + retries: 3, + retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, + retryCondition: (error) => { + return axiosRetry.isNetworkOrIdempotentRequestError(error) || + error.response?.status === 429 || + error.response?.status >= 500; + } +}); + +const router = express.Router(); + +// ======================== +// Single Ticket Lookup +// ======================== +router.get('/wxccai/getticket', async (req, res) => { + const jiraKey = req.query.jiraKey?.trim().toUpperCase(); + + if (!jiraKey || !/^[A-Z0-9]+-\d+$/.test(jiraKey)) { + return res.status(400).json({ + jiraKey: jiraKey || "missing", + assignedTo: "Error", + status: "Error", + summary: "Invalid or missing Jira key" + }); + } + + try { + const [issueData, description, comments] = await Promise.all([ + jiraService.fetchJiraIssue(jiraKey).catch(() => ({})), + jiraService.fetchPlainDescription(jiraKey).catch(() => "No description."), + jiraService.fetchPublicComments(jiraKey).catch(() => []) + ]); + + const fields = issueData.fields || {}; + const assignedTo = fields.assignee?.displayName || fields.assignee?.name || "Unassigned"; + const status = fields.status?.name || "Unknown"; + const title = fields.summary || "No title"; + + let commentsText = ""; + if (Array.isArray(comments) && comments.length > 0) { + const recent = comments.slice(-5); + commentsText = "\nRecent comments:\n" + recent.map(c => { + let dateStr = "unknown-date"; + if (typeof c.created === 'string') { + dateStr = c.created.slice(0, 10); + } else if (c.created?.iso8601) { + dateStr = c.created.iso8601.slice(0, 10); + } else if (c.created?.jira) { + dateStr = c.created.jira.slice(0, 10); + } + + const bodyPreview = typeof c.body === 'string' + ? c.body.substring(0, 250) + (c.body.length > 250 ? '...' : '') + : '[No body]'; + + return `[${dateStr}] ${c.author || 'Unknown'}: ${bodyPreview}`; + }).join('\n'); + } + + const context = ` +Ticket: ${jiraKey} +Title: ${title} +Status: ${status} +Assigned: ${assignedTo} +Description: ${description} +${commentsText} +`.trim(); + + const summary = await grokService.generateTicketSummary(context); + + res.json({ + jiraKey, + assignedTo, + status, + summary + }); + + } catch (error) { + logger.error(`Ticket lookup failed for ${jiraKey}:`, error); + res.status(200).json({ + jiraKey, + assignedTo: "Error", + status: "Lookup failed", + summary: `Unable to fetch ticket details: ${error.message.substring(0, 150)}` + }); + } +}); + +// ======================== +// Open Tickets by Reporter +// ======================== +router.get('/wxccai/open-tickets-by-reporter', async (req, res) => { + const email = req.query.email?.trim().toLowerCase(); + + if (!email || !email.includes('@')) { + return res.status(400).json({ + error: "Invalid or missing email address", + message: "Please provide a valid email address to search for open tickets" + }); + } + + try { + const issues = await jiraService.searchOpenTicketsByReporterEmail(email); + const ticketArray = await grokService.generateOpenTicketsSummary(issues); + + res.status(200).json(ticketArray); + + } catch (error) { + logger.error(`Open tickets by reporter failed for ${email}:`, error); + // Return safe empty response on runtime failures (Jira/Grok down, etc.) + // to match the degrade-gracefully + 200 pattern used by /getticket and the + // webhook endpoint. 400 is still used above for clear input validation errors. + res.status(200).json([]); + } +}); + +// ======================== +// Store Support (SS) Ticket Creation +// ======================== +// GET /api/wxccai/ssRequestTypes → list of exact subType strings you can use +// POST /api/wxccai/createSSRequest with body (see comment in handler) +router.get('/wxccai/ssRequestTypes', (req, res) => { + try { + const types = jiraService.getSupportedSSSubTypes ? jiraService.getSupportedSSSubTypes() : []; + res.json({ + serviceDeskId: config.jira.serviceDeskId || '170', + subTypes: types, + note: "storeNumber padded to 5 digits. Uses api.atlassian.com workspace-scoped Assets AQL (https://api.atlassian.com/jsm/assets/workspace/{id}/v1/object/aql) because this is a service object.", + assetsConfig: { + objectTypeId: config.jira.assetsStoreObjectTypeId || '109', + attribute: config.jira.assetsStoreNumberAttribute || 'Store Number', + storeCustomFieldId: config.jira.storeCustomFieldId || 'customfield_10261', + workspaceId: config.jira.assetsWorkspaceId || '(auto-discovered via /rest/servicedeskapi/assets/workspace)', + note: 'Always uses https://api.atlassian.com/jsm/assets/workspace/.../v1/object/aql (required for service objects)', + requiredEnv: [ + "JIRA_ASSETS_WORKSPACE_ID (recommended; auto-discovered if possible)", + "JIRA_ASSETS_STORE_OBJECT_TYPE_ID (default 109)", + "JIRA_STORE_CUSTOM_FIELD_ID (default customfield_10261)" + ] + }, + example: { + subType: types[0] || "Register Not functioning properly", + onBehalfOf: "user@ae.com", + summary: "Register 3 is frozen at login screen", + description: "Detailed description (optional)", + storeNumber: "00305", // looked up → [{ "objectId": "82288" }] + additional: { + customfield_10294: "2026-06-18T14:30:00.000+0000" + } + } + }); + } catch (e) { + res.status(500).json({ error: 'Failed to list types' }); + } +}); + +// POST body example: +// { +// "subType": "Register Not functioning properly", +// "onBehalfOf": "user@ae.com", +// "summary": "Register 3 is frozen", +// "description": "Customer reports...", +// "storeNumber": "00305", // 5 digits; resolved via api.atlassian.com Assets endpoint to { "objectId": "..." } +// "additional": { +// "customfield_10294": "2026-06-18T14:30:00.000+0000", +// "customfield_10557": "Transaction failed" +// } +// } +// subType must match exactly one of the keys in the REQUEST_TYPE_MAP. +router.post('/wxccai/createSSRequest', async (req, res) => { + const body = req.body || {}; + const timestamp = new Date().toISOString(); + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + + logger.info('Create SS Request received', { + timestamp, + ip, + subType: body.subType, + storeNumber: body.storeNumber, + hasOnBehalfOf: !!body.onBehalfOf + }); + + if (!body.subType || !body.summary) { + return res.status(400).json({ + success: false, + error: 'subType and summary are required' + }); + } + + try { + const result = await jiraService.createSSRequest(body); + + logger.info('Create SS Request succeeded', { + issueKey: result?.issueKey, + subType: body.subType + }); + + res.status(200).json({ + success: true, + issueKey: result?.issueKey, + issueId: result?.issueId, + requestTypeId: result?.requestType?.id, + raw: result // full response for debugging / future use + }); + } catch (error) { + logger.error('Create SS Request failed', { + subType: body.subType, + storeNumber: body.storeNumber, + error: error.message, + details: error.details || error.response?.data + }); + + res.status(400).json({ + success: false, + error: error.message, + details: error.details || error.response?.data || null + }); + } +}); + +// ======================== +// Ticket lifecycle: status / update / comment / transitions / close +// These are the raw operations (no Grok). Use /wxccai/getticket for the +// Grok-summarized variant. +// ======================== + +const KEY_RE = /^[A-Z]+-\d+$/; + +router.get('/wxccai/ticket/:key/status', async (req, res) => { + const key = req.params.key?.trim().toUpperCase(); + if (!key || !KEY_RE.test(key)) { + return res.status(400).json({ error: 'Invalid Jira key' }); + } + try { + const status = await jiraService.getTicketStatus(key); + res.json(status); + } catch (err) { + logger.error('status route failed', { key, error: err.message, details: err.details }); + res.status(err.status && err.status < 500 ? err.status : 500).json({ + error: err.message, details: err.details || null + }); + } +}); + +// Body: { summary?, description?, priority?, labels?, assigneeAccountId?, additional? } +router.patch('/wxccai/ticket/:key', async (req, res) => { + const key = req.params.key?.trim().toUpperCase(); + if (!key || !KEY_RE.test(key)) { + return res.status(400).json({ error: 'Invalid Jira key' }); + } + try { + const result = await jiraService.updateTicket(key, req.body || {}); + res.json({ success: true, ...result }); + } catch (err) { + logger.error('update route failed', { key, error: err.message, details: err.details }); + res.status(err.status && err.status < 500 ? err.status : 500).json({ + success: false, error: err.message, details: err.details || null + }); + } +}); + +// Body: { text: "...", internal?: boolean } +router.post('/wxccai/ticket/:key/comment', async (req, res) => { + const key = req.params.key?.trim().toUpperCase(); + if (!key || !KEY_RE.test(key)) { + return res.status(400).json({ error: 'Invalid Jira key' }); + } + const { text, internal } = req.body || {}; + try { + const result = await jiraService.addComment(key, text, { internal: !!internal }); + res.json({ success: true, ...result }); + } catch (err) { + logger.error('comment route failed', { key, error: err.message, details: err.details }); + res.status(err.status && err.status < 500 ? err.status : 500).json({ + success: false, error: err.message, details: err.details || null + }); + } +}); + +router.get('/wxccai/ticket/:key/transitions', async (req, res) => { + const key = req.params.key?.trim().toUpperCase(); + if (!key || !KEY_RE.test(key)) { + return res.status(400).json({ error: 'Invalid Jira key' }); + } + try { + const transitions = await jiraService.getTransitions(key); + res.json({ key, transitions }); + } catch (err) { + logger.error('transitions route failed', { key, error: err.message }); + res.status(500).json({ error: err.message }); + } +}); + +// Body: { transitionName?, resolution?, comment?, internal? } +// If transitionName is omitted, we auto-pick the first "done" transition. +router.post('/wxccai/ticket/:key/close', async (req, res) => { + const key = req.params.key?.trim().toUpperCase(); + if (!key || !KEY_RE.test(key)) { + return res.status(400).json({ error: 'Invalid Jira key' }); + } + const { transitionName, resolution, comment, internal } = req.body || {}; + try { + const result = await jiraService.closeTicket(key, { + transitionName, + resolution: resolution || 'Done', + comment, + internal: !!internal + }); + res.json({ success: true, ...result }); + } catch (err) { + logger.error('close route failed', { key, error: err.message, details: err.details }); + res.status(err.status && err.status < 500 ? err.status : 500).json({ + success: false, error: err.message, details: err.details || null + }); + } +}); + +// ======================== +// Assets diagnostic (non-production only) +// GET /api/wxccai/debug/assetsProbe?storeNumber=305 +// Runs several AQL variants against the Assets workspace so we can see which +// query shape actually matches (attribute name vs Name field, padded vs +// unpadded, etc.) and dumps the real attribute names + values from any +// returned objects. This is intentionally verbose; do not expose in prod. +// ======================== +router.get('/wxccai/debug/assetsProbe', async (req, res) => { + if ((config.nodeEnv || '').toLowerCase() === 'production') { + return res.status(404).json({ error: 'Not found' }); + } + + const storeNumber = (req.query.storeNumber ?? '').toString().trim(); + + try { + const result = await jiraService.probeAssetsForStore(storeNumber); + res.json(result); + } catch (err) { + logger.error('Assets probe failed', { storeNumber, error: err.message }); + res.status(500).json({ error: err.message }); + } +}); + +// ======================== +// Webex → Jira Transcript + Audio + Summary Comment +// ======================== +router.post('/wxccai/issueTranscript/:jiraKey', async (req, res) => { + const jiraKey = req.params.jiraKey?.trim().toUpperCase(); + const timestamp = new Date().toISOString(); + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress; + + let payload = req.body || {}; + + // If we hit the raw() parser for this webhook (because of potential bad JSON + // from transcription text), sanitize control characters and parse ourselves. + if (Buffer.isBuffer(req.body)) { + const rawBodyStr = req.body.toString('utf8'); + try { + let rawStr = rawBodyStr; + + // Trim any leading control characters that would make the top-level JSON invalid. + rawStr = rawStr.replace(/^[\u0000-\u001F]+/, ''); + + // Only escape control characters that appear *inside* JSON string values. + // Global replacement would turn structural newlines/tabs (the pretty-printing + // in the JSON itself) into literal "\n" text, breaking the top-level structure + // (which is exactly what caused "Expected property name or '}' at position 1"). + // + // This regex finds "..." strings (handling escaped inner quotes) and cleans + // only the content inside them. Structural whitespace stays intact so JSON.parse + // still sees valid tokens. + rawStr = rawStr.replace(/"((?:[^"\\]|\\.)*)"/g, (fullMatch, content) => { + const cleaned = content.replace(/[\u0000-\u001F]/g, (ch) => { + switch (ch) { + case '\n': return '\\n'; + case '\r': return '\\r'; + case '\t': return '\\t'; + case '\b': return '\\b'; + case '\f': return '\\f'; + default: + return '\\u' + ch.charCodeAt(0).toString(16).padStart(4, '0'); + } + }); + return '"' + cleaned + '"'; + }); + + payload = JSON.parse(rawStr); + } catch (parseErr) { + // Make replay easy: a one-liner using base64 so special chars/newlines in the payload don't break the log line + const base64 = req.body.toString('base64'); + const replayCmd = `echo '${base64}' | base64 -d | curl -X POST -H 'Content-Type: application/json' --data-binary @- 'http://localhost:1866${req.originalUrl || req.url}'`; + + logger.error('Failed to parse transcript webhook payload even after sanitization', { + jiraKey, + error: parseErr.message, + rawBody: rawBodyStr, + rawBodyBase64: base64, + replayCurl: replayCmd, + // The body after our trimming + escaping attempt (truncated for log size if huge) + sanitizedAttempt: rawBodyStr.substring(0, 2000) + }); + + // Last-resort fallback: extract fields with regex so we can still attempt + // the attachments and summary comment even if the overall JSON is badly broken + // (common when transcription text contains unescaped quotes, newlines, etc.) + try { + const get = (name) => { + // Simple string value extractor that tolerates some bad escaping inside the value + const re = new RegExp(`"${name}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`, 'i'); + const m = rawBodyStr.match(re); + if (!m) return ''; + return m[1] + .replace(/\\"/g, '"') + .replace(/\\n/g, '\n') + .replace(/\\r/g, '\r') + .replace(/\\t/g, '\t') + .replace(/\\\\/g, '\\'); + }; + + payload = { + orgId: get('orgId'), + taskId: get('taskId'), + audioFileName: get('audioFileName'), + audioFilePath: get('audioFilePath'), + textFileName: get('textFileName'), + textFilePath: get('textFilePath'), + Summaries: { + intialContactReason: get('intialContactReason'), + additionalContext: get('additionalContext'), + keyActionsTake: get('keyActionsTake'), + nextSteps: get('nextSteps'), + resolution: get('resolution') + } + }; + logger.warn('Used regex fallback extraction for malformed payload', { jiraKey }); + } catch (fallbackErr) { + logger.error('Fallback extraction also failed', { jiraKey, error: fallbackErr.message }); + return res.status(400).json({ status: "error", message: "Invalid JSON payload from upstream" }); + } + } + } + + // Convenience unwrap for Postman / local testing. + // When users copy the "payload" value (or the whole meta object) from our previous + // WEBEX_TRANSCRIPT or error logs into Postman, the body often arrives wrapped as + // { jiraKey, ip, payload: , level }. + // Auto-unwrap so the rest of the handler sees the real fields (audioFilePath, Summaries etc.) + // and actually performs the attachments + comment. + if (payload && typeof payload === 'object' && payload.payload && + (payload.payload.orgId || payload.payload.audioFilePath || payload.payload.Summaries)) { + logger.info('Unwrapped log meta / copied-from-log payload for testing', { jiraKey }); + payload = payload.payload; + } + + logger.info('Webex Transcript Webhook Received', { + timestamp, jiraKey, ip, payloadKeys: Object.keys(payload) + }); + + webexLogger.info('WEBEX_TRANSCRIPT', { timestamp, jiraKey, ip, payload }); + + if (!jiraKey || !/^[A-Z0-9]+-\d+$/.test(jiraKey)) { + return res.status(400).json({ status: "error", message: "Invalid Jira key" }); + } + + const results = { + attachments: [], + commentPosted: false, + errors: [] + }; + + // Process each step independently so one failure (e.g. 401 scope) doesn't prevent others. + // Always return 200 to the caller; use "partial_error" status if anything failed. + // This prevents the webhook from "crashing" the processing for non-fatal issues. + + // 1. Attach Audio File (if present) + if (payload.audioFilePath) { + try { + await jiraService.attachFileToJira(jiraKey, payload.audioFilePath, payload.audioFileName || `audio-${jiraKey}.wav`); + results.attachments.push('audio'); + } catch (error) { + logger.error('Audio file attach failed', { + jiraKey, + error: error.response?.data?.message || error.message, + status: error.response?.status, + responseData: error.response?.data + }); + results.errors.push({ + step: 'audio', + error: error.response?.data?.message || error.message, + status: error.response?.status, + details: error.response?.data + }); + } + } + + // 2. Attach Text Transcript (only if it actually exists) + if (payload.textFilePath && payload.textFilePath.trim() !== '') { + try { + await jiraService.attachFileToJira(jiraKey, payload.textFilePath, payload.textFileName || `transcript-${jiraKey}.json`); + results.attachments.push('transcript'); + + // Convert the JSON transcript to human-readable text and upload as well + const readableAttached = await jiraService.attachReadableTranscript(jiraKey, payload.textFilePath, payload.textFileName); + if (readableAttached) { + results.attachments.push('transcript-readable'); + } + } catch (error) { + logger.error('Text transcript attach failed', { + jiraKey, + error: error.response?.data?.message || error.message, + status: error.response?.status, + responseData: error.response?.data + }); + results.errors.push({ + step: 'text', + error: error.response?.data?.message || error.message, + status: error.response?.status, + details: error.response?.data + }); + } + } + + // 3. Post Summaries as a clean Jira comment (restricted/internal via role) + if (payload.Summaries && Object.keys(payload.Summaries).length > 0) { + try { + // Map internal attachment keys to friendly names for the comment body + const friendly = results.attachments.map(a => { + if (a === 'audio') return 'audio recording'; + if (a === 'transcript') return 'JSON transcript'; + if (a === 'transcript-readable') return 'human-readable transcript'; + return a; + }); + await jiraService.postWebexSummaryComment(jiraKey, payload.Summaries, friendly); + results.commentPosted = true; + } catch (error) { + logger.error('Summary comment post failed', { + jiraKey, + error: error.response?.data?.message || error.message, + status: error.response?.status, + responseData: error.response?.data + }); + results.errors.push({ + step: 'comment', + error: error.response?.data?.message || error.message, + status: error.response?.status, + details: error.response?.data + }); + } + } + + const overallStatus = results.errors.length > 0 ? 'partial_error' : 'success'; + const message = overallStatus === 'success' ? 'Processing completed' : 'Some operations failed (see errors)'; + + logger.info('Webex webhook processed', { jiraKey, results, overallStatus }); + + res.status(200).json({ + status: overallStatus, + message, + jiraKey, + attachments: results.attachments, + commentPosted: results.commentPosted, + errors: results.errors.length ? results.errors : undefined + }); +}); + +export default router; diff --git a/src/services/grokService.js b/src/services/grokService.js new file mode 100644 index 0000000..dcc8ac4 --- /dev/null +++ b/src/services/grokService.js @@ -0,0 +1,135 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import OpenAI from 'openai'; +import config from '../config/index.js'; +import logger from '../utilities/logger.js'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PROMPTS_DIR = path.join(__dirname, '../prompts'); + +const client = new OpenAI({ + apiKey: config.xai.apiKey, + baseURL: config.xai.baseUrl, +}); + +// Helper to load prompt from file +async function loadPrompt(templateName, variables = {}) { + const filePath = path.join(PROMPTS_DIR, `${templateName}.txt`); + let prompt = await fs.readFile(filePath, 'utf8'); + + Object.keys(variables).forEach(key => { + const placeholder = new RegExp(`{{${key}}}`, 'g'); + prompt = prompt.replace(placeholder, variables[key]); + }); + + return prompt; +} + +// Single ticket summary (your original /getticket endpoint) +export async function generateTicketSummary(context) { + try { + const prompt = await loadPrompt('singleTicketSummary', { context }); + + const completion = await client.chat.completions.create({ + model: "grok-4.20-non-reasoning", + messages: [{ role: "user", content: prompt }], + temperature: 0.2, + max_tokens: 400, + }); + + return completion.choices[0]?.message?.content?.trim() || "Summary unavailable at this time."; + } catch (err) { + logger.error(`Grok single ticket summary failed: ${err.message}`); + return "AI summary is temporarily unavailable. Please review the ticket directly in Jira."; + } +} + +// Multi-ticket summary (your /open-tickets-by-reporter endpoint) +export async function generateOpenTicketsSummary(tickets) { + if (!tickets || tickets.length === 0) { + return []; + } + + let context = `Open tickets reported by the customer:\n`; + + tickets.forEach(ticket => { + const fields = ticket.fields || {}; + const key = ticket.key; + const status = fields.status?.name || "Unknown"; + const updated = fields.updated || null; + const title = fields.summary || "No title"; + + const notes = ticket.enrichedNotes || {}; + const description = notes.description || "No description available."; + + let commentsText = ""; + if (Array.isArray(notes.publicComments) && notes.publicComments.length > 0) { + const recent = notes.publicComments.slice(-8).reverse(); + commentsText = "\nRecent public comments (most recent first):\n" + recent.map(c => { + const date = c.createdIso ? c.createdIso.slice(0, 10) : "unknown"; + const body = c.body ? c.body.substring(0, 300) : ""; + return `[${date}] ${body}${body.length === 300 ? '...' : ''}`; + }).join("\n"); + } + + context += `\nTicket: ${key} +Status: ${status} +Last updated: ${updated || "unknown"} +Title: ${title} +Initial Problem / Description: ${description} +${commentsText} +`; + }); + + try { + const prompt = await loadPrompt('openTicketsSummary', { context }); + + const completion = await client.chat.completions.create({ + model: "grok-4.20-non-reasoning", + messages: [{ role: "user", content: prompt }], + temperature: 0.3, + max_tokens: 900, + }); + + const rawOutput = completion.choices[0]?.message?.content?.trim(); + + if (!rawOutput) { + return tickets.map(t => createFallbackTicket(t)); + } + + let parsed; + try { + parsed = JSON.parse(rawOutput); + } catch (e) { + const jsonMatch = rawOutput.match(/\[[\s\S]*\]/); + if (jsonMatch) { + parsed = JSON.parse(jsonMatch[0]); + } else { + parsed = null; + } + } + + return Array.isArray(parsed) ? parsed : tickets.map(t => createFallbackTicket(t)); + + } catch (err) { + logger.error(`Grok multi-ticket summary failed: ${err.message}`); + return tickets.map(t => createFallbackTicket(t)); + } +} + +// Fallback helper +function createFallbackTicket(ticket) { + const fields = ticket.fields || {}; + return { + jiraKey: ticket.key, + currentStatus: fields.status?.name || "Unknown", + summary: `${fields.status?.name || "Unknown"} - ${fields.summary || "No details available. Check public notes for latest updates."}`, + lastUpdated: fields.updated || null + }; +} + +export default { + generateTicketSummary, + generateOpenTicketsSummary +}; \ No newline at end of file diff --git a/src/services/healthService.js b/src/services/healthService.js new file mode 100644 index 0000000..f7fbf51 --- /dev/null +++ b/src/services/healthService.js @@ -0,0 +1,106 @@ +import axios from 'axios'; +import { jiraClient } from './jiraService.js'; +import config from '../config/index.js'; +import logger from '../utilities/logger.js'; + +export async function getDetailedHealth() { + const startTime = Date.now(); + const health = { + status: 'healthy', + uptime: process.uptime(), + timestamp: new Date().toISOString(), + environment: config.nodeEnv || 'development', + version: process.env.npm_package_version || '1.0.0', + checks: {} + }; + + // Self check + health.checks.self = { + status: 'healthy', + responseTimeMs: Date.now() - startTime + }; + + // Jira Check (critical) - uses the shared jiraClient so it automatically + // gets the correct base URL (including api.atlassian.com/ex/jira/{cloudId} form) + // and the configured auth (basic kept). + try { + const jiraStart = Date.now(); + await jiraClient.get('/rest/api/3/myself', { timeout: 6000 }); + + health.checks.jira = { + status: 'healthy', + responseTimeMs: Date.now() - jiraStart, + message: 'Jira authentication successful' + }; + } catch (err) { + const status = err.response?.status || 'unknown'; + health.checks.jira = { + status: (status === 401 || status === 403) ? 'unhealthy' : 'degraded', + responseTimeMs: Date.now() - startTime, + message: status === 401 ? 'Jira authentication failed (check credentials)' : `Jira error (${status})` + }; + if (health.status === 'healthy') health.status = 'degraded'; + } + + // xAI Check - Tolerant + tries both common endpoints + let xaiSuccess = false; + const xaiStart = Date.now(); + + // Try 1: Legacy chat/completions (still widely used) + try { + await axios.post(`${config.xai.baseUrl}/v1/chat/completions`, { + model: "grok-4.20-non-reasoning", + messages: [{ role: 'user', content: 'Say \'ok\'.' }], + max_tokens: 5, + temperature: 0 + }, { + headers: { + Authorization: `Bearer ${config.xai.apiKey}`, + 'Content-Type': 'application/json' + }, + timeout: 7000 + }); + xaiSuccess = true; + } catch (_) {} + + // Try 2: New Responses API (if legacy fails) + if (!xaiSuccess) { + try { + await axios.post(`${config.xai.baseUrl}/v1/responses`, { + model: "grok-4.20-non-reasoning", + input: [{ role: 'user', content: 'Say \'ok\'.' }], + max_tokens: 5, + temperature: 0 + }, { + headers: { + Authorization: `Bearer ${config.xai.apiKey}`, + 'Content-Type': 'application/json' + }, + timeout: 7000 + }); + xaiSuccess = true; + } catch (_) {} + } + + if (xaiSuccess) { + health.checks.xai = { + status: 'healthy', + responseTimeMs: Date.now() - xaiStart, + message: 'xAI API reachable' + }; + } else { + health.checks.xai = { + status: 'degraded', + responseTimeMs: Date.now() - xaiStart, + message: 'xAI API test failed (summaries still working)' + }; + // Do NOT degrade overall status for xAI issues + } + + // Overall status only fails if Jira is unhealthy + if (health.checks.jira?.status === 'unhealthy') { + health.status = 'unhealthy'; + } + + return health; +} diff --git a/src/services/jiraService.js b/src/services/jiraService.js new file mode 100644 index 0000000..a18a8bd --- /dev/null +++ b/src/services/jiraService.js @@ -0,0 +1,1425 @@ +import path from 'path'; +import axios from 'axios'; +import axiosRetry from 'axios-retry'; +import FormData from 'form-data'; +import config from '../config/index.js'; +import logger from '../utilities/logger.js'; +import { adfToPlainText } from '../utilities/adfToPlainText.js'; + +// Create a reusable Jira client with flexible auth (kept as basic per current requirements). +// We deliberately do NOT put a default 'Content-Type' on the instance because we +// need to support both JSON bodies and multipart/form-data (for attachments). +// Content-Type is set explicitly on the calls that need it. +const createJiraClient = () => { + const headers = {}; + const effectiveBase = config.jira.baseUrl || '(not configured)'; + logger.debug(`[jira] baseUrl=${effectiveBase} authType=${config.jira.authType}`); + + if (config.jira.authType === 'bearer') { + headers.Authorization = `Bearer ${config.jira.apiToken}`; + logger.info('Using Jira Bearer Token authentication'); + } else { + // Classic Basic Auth (email:apiToken) + const authStr = `${config.jira.email}:${config.jira.apiToken}`; + headers.Authorization = `Basic ${Buffer.from(authStr).toString('base64')}`; + logger.info('Using Jira Basic Auth'); + } + + const client = axios.create({ + baseURL: config.jira.baseUrl, + headers, + }); + + // Apply retry policy to this instance (keep in sync with the global axios-retry + // configured in wxccRoutes.js for the S3 downloads and any other raw calls). + // Using exponential backoff to match the current policy in routes. + axiosRetry(client, { + retries: 3, + retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, + retryCondition: (error) => { + return axiosRetry.isNetworkOrIdempotentRequestError(error) || + error.response?.status === 429 || + error.response?.status >= 500; + } + }); + + return client; +}; + +export const jiraClient = createJiraClient(); + +// ======================== +// Existing Functions (updated to use jiraClient where possible) +// ======================== + +export async function fetchJiraIssue(key) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + + const url = `/rest/api/3/issue/${key}?fields=summary,status,assignee,created,updated,priority`; + + try { + const response = await jiraClient.get(url); + return response.data; + } catch (error) { + logger.error('Fetch Jira issue failed:', error.response?.data || error.message); + throw new Error(`Issue fetch failed: ${error.message}`); + } +} + +export async function fetchPlainDescription(key) { + const payload = { expression: "issue.description.plainText", context: { issue: { key } } }; + try { + const response = await jiraClient.post('/rest/api/3/expression/evaluate', payload, { + headers: { 'Content-Type': 'application/json' } + }); + return response.data.value || 'No description available.'; + } catch (error) { + logger.warn('Failed to fetch plain description:', error.message); + return 'No description available.'; + } +} + +export async function fetchPublicComments(key) { + // Use standard /rest/api/3/issue/{key}/comment (switched from servicedeskapi + // because the latter can require different auth/permissions under the current + // cloudId + Basic auth setup). We still normalize the shape for downstream + // consumers in wxccRoutes and grokService (author string, plain-text body, + // consistent date fields). + const url = `/rest/api/3/issue/${key}/comment`; + try { + const response = await jiraClient.get(url); + const values = response.data?.values || []; + return values.map(comment => ({ + author: comment.author?.displayName || comment.author?.name || 'Unknown', + body: adfToPlainText(comment.body), + created: comment.created, + createdIso: typeof comment.created === 'string' ? comment.created : (comment.created?.iso8601 || comment.created || null) + })); + } catch (error) { + logger.warn('Failed to fetch public comments:', error.message); + return []; + } +} + +async function getAccountIdFromEmail(email) { + if (!email) { + throw new Error('Email is required'); + } + + const url = `/rest/api/3/user/search?query=${encodeURIComponent(email)}&maxResults=10`; + try { + const response = await jiraClient.get(url); + const users = response.data || []; + + if (users.length === 0) { + throw new Error(`No users found matching "${email}"`); + } + + let user = users.find(u => u.emailAddress?.toLowerCase() === email.toLowerCase()); + if (!user && users.length > 0) { + user = users[0]; + } + + if (!user?.accountId) { + throw new Error(`No usable accountId found for "${email}"`); + } + + logger.info(`Using accountId ${user.accountId} for email "${email}"`); + return user.accountId; + } catch (err) { + throw new Error(`User lookup failed: ${err.message}`); + } +} + +// Search functions (updated to use jiraClient) +// Updated: Search open tickets by REPORTER using email directly in JQL (no user lookup needed) +export async function searchOpenTicketsByReporterEmail(email) { + if (!email || typeof email !== 'string' || email.trim() === '') { + throw new Error("Email is required"); + } + + const jql = `project in (CS, SS, SUPPORT) + AND reporter = "${email}" + AND statusCategory != Done + ORDER BY updated DESC`; + + try { + const response = await jiraClient.post('/rest/api/3/search/jql', { + jql: jql, + maxResults: 8, + fields: ["key", "summary", "status", "updated", "description"], + expand: "comments" + }, { + headers: { 'Content-Type': 'application/json' } + }); + + const issues = response.data.issues || []; + + // Enrich with description + public comments + const enrichedIssues = await Promise.all( + issues.map(async (issue) => { + const key = issue.key; + try { + const [plainDesc, publicComments] = await Promise.all([ + fetchPlainDescription(key).catch(() => "No description available."), + fetchPublicComments(key).catch(() => []) + ]); + + issue.enrichedNotes = { + description: plainDesc, + publicComments: publicComments.slice(-6) + }; + } catch (err) { + logger.warn(`Failed to enrich notes for ${key}:`, err.message); + issue.enrichedNotes = { description: "Notes unavailable.", publicComments: [] }; + } + return issue; + }) + ); + + return enrichedIssues; + } catch (error) { + logger.error('Jira reporter search failed:', error.response?.data || error.message); + throw new Error(`Failed to search tickets reported by ${email}: ${error.message}`); + } +} + +// ======================== +// Jira helper operations (extracted from wxccRoutes for attachments + Webex transcript summaries) +// These centralize Jira interactions so all calls go through the shared client (correct +// cloudId base URL via ex/jira/{cloudId}, Basic auth, and retry policy). +// Uses core /rest/api/3/issue/.../attachments (per working curl) + X-Atlassian-Token: no-check. +// ======================== + +/** + * Shared helper: POST a Buffer as multipart attachment to core Jira attachments endpoint. + * - Uses the configured jiraClient (correct base + auth inherited). + * - Properly spreads form.getHeaders() so boundary is set. + * - Cleans any charset from content-type (prevents 415). + * - Retry loop only around the API call (download must be done by caller). + * - Fail-fast on 401/403 (scope/perms). + */ +async function attachBufferToJira(jiraKey, fileBuffer, fileName) { + for (let attempt = 1; attempt <= 3; attempt++) { + try { + const form = new FormData(); + form.append('file', fileBuffer, fileName); + + const formHeaders = form.getHeaders(); + // Remove charset if present (Atlassian often rejects or causes 415 on multipart+charset) + if (formHeaders['content-type']) { + formHeaders['content-type'] = formHeaders['content-type'].replace(/;\s*charset=[^;]*/i, ''); + } + + // Relative path: jiraClient already has the correct baseURL (ex/jira/{cloudId}) + const uploadPath = `/rest/api/3/issue/${jiraKey}/attachments`; + + await jiraClient.post(uploadPath, form, { + headers: { + 'X-Atlassian-Token': 'no-check', + ...formHeaders + }, + timeout: 15000 + }); + + logger.info('File attached successfully', { jiraKey, fileName, attempt }); + return; + } catch (err) { + const status = err.response?.status; + logger.error('Jira file attach attempt failed', { + jiraKey, + fileName, + attempt, + status, + responseData: err.response?.data, + responseHeaders: err.response?.headers ? Object.fromEntries( + Object.entries(err.response.headers).filter(([k]) => !k.toLowerCase().includes('auth')) + ) : undefined + }); + // Fail fast on auth/permission errors (scope mismatch etc.) + if (status === 401 || status === 403) { + throw err; + } + if (attempt === 3) throw err; + await new Promise(r => setTimeout(r, attempt * 1500)); + } + } +} + +/** + * Download a file from the given URL (S3 pre-signed) *once* and attach using core + * Jira attachments API (/rest/api/3/issue/{key}/attachments). + * Download happens outside the retry loop because the signed URL expires (~1800s). + */ +export async function attachFileToJira(jiraKey, fileUrl, fileName) { + let fileBuffer; + try { + const dl = await axios.get(fileUrl, { + responseType: 'arraybuffer', + timeout: 20000 + }); + fileBuffer = Buffer.from(dl.data); + } catch (dlErr) { + logger.error('Failed to download file from S3 for attachment (URL likely expired on replay)', { + jiraKey, + fileName, + url: fileUrl, + status: dlErr.response?.status, + message: dlErr.message + }); + throw dlErr; + } + + await attachBufferToJira(jiraKey, fileBuffer, fileName); +} + +/** + * Convert a Webex-style JSON transcript to human-readable text. + */ +function formatTranscriptToHumanReadable(data) { + if (!data || !Array.isArray(data.responseContents)) return null; + const lines = []; + lines.push(`Transcript`); + if (data.interactionId) lines.push(`Interaction ID: ${data.interactionId}`); + if (data.languageCode) lines.push(`Language: ${data.languageCode}`); + lines.push(''); + for (const entry of data.responseContents) { + const res = entry.recognitionResult; + if (!res || !res.alternatives || !res.alternatives[0]) continue; + const role = (res.role || 'UNKNOWN').toUpperCase(); + const alt = res.alternatives[0]; + const transcript = (alt.transcript || '').trim(); + if (!transcript) continue; + let ts = ''; + const words = alt.words || []; + if (words.length > 0) { + const start = words[0].start_time || {}; + const totalSec = (start.seconds || 0) + Math.floor((start.nanos || 0) / 1e9); + const min = Math.floor(totalSec / 60); + const sec = Math.floor(totalSec % 60); + ts = `[${String(min).padStart(2,'0')}:${String(sec).padStart(2,'0')}] `; + } + lines.push(`${ts}${role}: ${transcript}`); + } + return lines.join('\n'); +} + +async function fetchAndConvertTranscript(url) { + try { + const resp = await axios.get(url, { timeout: 10000 }); + return formatTranscriptToHumanReadable(resp.data); + } catch (e) { + logger.warn(`Failed to fetch/convert transcript: ${e.message}`); + return null; + } +} + +/** + * Download the JSON transcript, convert to human readable, and attach as *-readable.txt + * (e.g. Transcript_...-readable.txt). Uses shared attach helper for correct headers/retry. + * Failures here are swallowed so they don't mark the original JSON text attach as failed. + */ +export async function attachReadableTranscript(jiraKey, transcriptUrl, originalFileName = null) { + const readable = await fetchAndConvertTranscript(transcriptUrl); + if (!readable) { + logger.warn('Readable transcript conversion yielded no content (check transcript JSON shape or URL)', { jiraKey }); + return false; + } + const base = (originalFileName || `transcript-${jiraKey}`).replace(/\.json$/i, ''); + const fileName = `${base}-readable.txt`; + + try { + await attachBufferToJira(jiraKey, Buffer.from(readable, 'utf8'), fileName); + return true; + } catch (err) { + logger.error('Readable transcript attach failed (non-fatal)', { + jiraKey, + fileName, + error: err.response?.data?.message || err.message, + status: err.response?.status + }); + return false; + } +} + +/** + * Build a clean ADF comment from the Webex CC AI summaries object and post it + * to the Jira issue. The comment is posted with role visibility (restricted/internal) + * so only users with that role see the summary + attachment references. + */ +export async function postWebexSummaryComment(jiraKey, summaries, attachedFiles = []) { + const items = [ + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: `Initial Contact Reason: ${summaries.intialContactReason || 'N/A'}` }] }] }, + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: `Additional Context: ${summaries.additionalContext || 'N/A'}` }] }] }, + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: `Key Actions Taken: ${summaries.keyActionsTake || 'N/A'}` }] }] }, + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: `Next Steps: ${summaries.nextSteps || 'N/A'}` }] }] }, + { type: "listItem", content: [{ type: "paragraph", content: [{ type: "text", text: `Resolution: ${summaries.resolution || 'N/A'}` }] }] } + ]; + + const content = [ + { + type: "heading", + attrs: { level: 3 }, + content: [{ type: "text", text: "Webex Contact Center Summary" }] + }, + { type: "bulletList", content: items }, + { + type: "paragraph", + content: [{ type: "text", text: `Posted via Webex Integration — ${new Date().toISOString()}` }] + } + ]; + + if (Array.isArray(attachedFiles) && attachedFiles.length > 0) { + content.push({ + type: "paragraph", + content: [{ + type: "text", + text: `Attached files (internal): ${attachedFiles.join(', ')}` + }] + }); + } + + const commentPayload = { + body: { + version: 1, + type: "doc", + content + }, + // Restrict visibility so summary + attachment notes are internal only. + // Controlled by JIRA_COMMENT_VISIBILITY_ROLE (or defaults to Administrators). + visibility: { + type: "role", + value: config.jira.commentVisibilityRole || 'Service Desk Team' + } + }; + + await jiraClient.post( + `/rest/api/3/issue/${jiraKey}/comment`, + commentPayload, + { + headers: { + 'Content-Type': 'application/json' + } + } + ); + + logger.info('Clean summary comment posted (restricted)', { jiraKey, attachedFiles }); +} + +// ======================== +// Ticket lifecycle: status, update, comment, close +// These are project-agnostic (work for any project the token can see) and use +// the core /rest/api/3/issue/... endpoints. They are separate from the JSM +// Service Desk create flow further below. +// ======================== + +/** + * Convert a plain string to a minimal ADF document (single paragraph). + * ADF is what /rest/api/3/... expects for description/comment bodies. + */ +function plainTextToAdf(text) { + const safe = (text ?? '').toString(); + if (!safe) { + return { version: 1, type: 'doc', content: [] }; + } + // Split on blank lines → separate paragraphs; single newlines become hardBreak. + const paragraphs = safe.split(/\n{2,}/).map(block => { + const parts = block.split('\n'); + const content = []; + parts.forEach((line, idx) => { + if (line.length) content.push({ type: 'text', text: line }); + if (idx < parts.length - 1) content.push({ type: 'hardBreak' }); + }); + return { type: 'paragraph', content }; + }); + return { version: 1, type: 'doc', content: paragraphs }; +} + +/** + * Compact, purpose-built status view — no Grok, no comment enrichment. + * Use this when a caller just wants "where is this ticket right now?". + */ +export async function getTicketStatus(key) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + + const url = `/rest/api/3/issue/${key}?fields=summary,status,assignee,reporter,priority,resolution,created,updated,labels`; + try { + const { data } = await jiraClient.get(url); + const f = data.fields || {}; + return { + key: data.key, + summary: f.summary || null, + status: f.status?.name || null, + statusCategory: f.status?.statusCategory?.key || null, + assignee: f.assignee?.displayName || f.assignee?.emailAddress || null, + reporter: f.reporter?.displayName || f.reporter?.emailAddress || null, + priority: f.priority?.name || null, + resolution: f.resolution?.name || null, + labels: f.labels || [], + created: f.created || null, + updated: f.updated || null + }; + } catch (err) { + logger.error('getTicketStatus failed', { key, status: err.response?.status, details: err.response?.data }); + const e = new Error(`Failed to fetch status for ${key}: ${err.message}`); + e.status = err.response?.status; + e.details = err.response?.data; + throw e; + } +} + +/** + * Partial issue update. Accepts flat, friendly fields: + * { summary, description, priority, labels, assigneeAccountId, additional } + * `additional` is merged raw into the `fields` object (e.g. customfield_* values). + * `description` is a plain string; we convert to ADF. + */ +export async function updateTicket(key, updates = {}) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + + const fields = {}; + if (updates.summary !== undefined) fields.summary = String(updates.summary); + if (updates.description !== undefined) fields.description = plainTextToAdf(updates.description); + if (updates.priority) fields.priority = { name: String(updates.priority) }; + if (Array.isArray(updates.labels)) fields.labels = updates.labels.map(String); + if (updates.assigneeAccountId) fields.assignee = { accountId: String(updates.assigneeAccountId) }; + if (updates.additional && typeof updates.additional === 'object') Object.assign(fields, updates.additional); + + if (Object.keys(fields).length === 0) { + throw new Error('updateTicket called with no updatable fields'); + } + + try { + await jiraClient.put(`/rest/api/3/issue/${key}`, { fields }, { + headers: { 'Content-Type': 'application/json' } + }); + logger.info('Ticket updated', { key, fieldKeys: Object.keys(fields) }); + return { key, updated: Object.keys(fields) }; + } catch (err) { + logger.error('updateTicket failed', { key, status: err.response?.status, details: err.response?.data }); + const e = new Error(err.response?.data?.errorMessages?.join('; ') || err.message); + e.status = err.response?.status; + e.details = err.response?.data; + throw e; + } +} + +/** + * Add a comment to an issue. `text` is plain; converted to ADF. + * If `internal: true`, restricts visibility to `config.jira.commentVisibilityRole` + * (same behavior as postWebexSummaryComment). + */ +export async function addComment(key, text, { internal = false } = {}) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + if (!text || !String(text).trim()) { + throw new Error('Comment text is required'); + } + + const payload = { body: plainTextToAdf(String(text)) }; + if (internal) { + payload.visibility = { type: 'role', value: config.jira.commentVisibilityRole || 'Service Desk Team' }; + } + + try { + const { data } = await jiraClient.post(`/rest/api/3/issue/${key}/comment`, payload, { + headers: { 'Content-Type': 'application/json' } + }); + logger.info('Comment posted', { key, commentId: data?.id, internal }); + return { key, commentId: data?.id, internal }; + } catch (err) { + logger.error('addComment failed', { key, status: err.response?.status, details: err.response?.data }); + const e = new Error(err.response?.data?.errorMessages?.join('; ') || err.message); + e.status = err.response?.status; + e.details = err.response?.data; + throw e; + } +} + +/** + * Fetch available workflow transitions for an issue. Useful for both the + * client picking a transition manually and for closeTicket() below. + */ +export async function getTransitions(key) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + try { + const { data } = await jiraClient.get(`/rest/api/3/issue/${key}/transitions`); + return (data.transitions || []).map(t => ({ + id: t.id, + name: t.name, + to: { id: t.to?.id, name: t.to?.name, statusCategory: t.to?.statusCategory?.key }, + hasScreen: !!t.hasScreen + })); + } catch (err) { + logger.error('getTransitions failed', { key, status: err.response?.status, details: err.response?.data }); + throw new Error(`Failed to fetch transitions for ${key}: ${err.message}`); + } +} + +/** + * Execute a specific transition. Optionally set a resolution and/or append a + * comment in the same call (both are fields the transition screen can accept). + */ +export async function transitionTicket(key, transitionId, { resolution, comment, internal = false, additionalFields } = {}) { + if (!key || !/^[A-Z]+-\d+$/.test(key)) { + throw new Error(`Invalid ticket key: "${key}"`); + } + if (!transitionId) throw new Error('transitionId is required'); + + const payload = { transition: { id: String(transitionId) } }; + + const fields = { ...(additionalFields || {}) }; + if (resolution) fields.resolution = { name: String(resolution) }; + if (Object.keys(fields).length) payload.fields = fields; + + if (comment) { + const commentEntry = { add: { body: plainTextToAdf(String(comment)) } }; + if (internal) { + commentEntry.add.visibility = { type: 'role', value: config.jira.commentVisibilityRole || 'Service Desk Team' }; + } + payload.update = { comment: [commentEntry] }; + } + + try { + await jiraClient.post(`/rest/api/3/issue/${key}/transitions`, payload, { + headers: { 'Content-Type': 'application/json' } + }); + logger.info('Ticket transitioned', { key, transitionId, resolution }); + return { key, transitionId, resolution: resolution || null }; + } catch (err) { + logger.error('transitionTicket failed', { + key, transitionId, status: err.response?.status, details: err.response?.data + }); + const e = new Error(err.response?.data?.errorMessages?.join('; ') || err.message); + e.status = err.response?.status; + e.details = err.response?.data; + throw e; + } +} + +/** + * Convenience: find a "closing" transition and execute it. + * Prefers explicit `transitionName` if provided, otherwise picks the first + * transition whose target status is in category "done" (Jira's canonical + * category for closed/resolved/completed), falling back to a name-based match. + */ +export async function closeTicket(key, { transitionName, resolution = 'Done', comment, internal = false } = {}) { + const transitions = await getTransitions(key); + if (transitions.length === 0) { + throw new Error(`No workflow transitions available for ${key} (check assignee/permissions)`); + } + + let chosen = null; + if (transitionName) { + chosen = transitions.find(t => t.name.toLowerCase() === transitionName.toLowerCase()); + if (!chosen) { + throw new Error(`Transition "${transitionName}" not available for ${key}. Available: ${transitions.map(t => t.name).join(', ')}`); + } + } else { + chosen = transitions.find(t => t.to?.statusCategory === 'done') + || transitions.find(t => /done|closed|resolved|complete/i.test(t.name)); + if (!chosen) { + throw new Error(`Could not find a closing transition for ${key}. Available: ${transitions.map(t => t.name).join(', ')}`); + } + } + + return transitionTicket(key, chosen.id, { resolution, comment, internal }); +} + +// ======================== +// Store Support (SS) Ticket Creation via Service Desk API +// Uses /rest/servicedeskapi/request to create proper JSM customer requests +// with request types. This is separate from corporate tickets (future). +// Consistent base URL from config (supports ex/jira/{cloudId} or direct site). +// ======================== + +const REQUEST_TYPE_MAP = { + // Point of Sale + 'Register Not functioning properly': 269, + 'Unable to login': 275, + 'Business report issue': 267, + 'Broken device / hardware': 266, + + // Hardware + 'Broken Device / Hardware': 266, + 'Report Missing Hardware': 273, + 'Request Additional Hardware': 274, + + // Technology + 'Business Report Issue': 267, + 'Report an Issue with Sterling Application': 272, + 'Omni Turn Off / On': 268, + 'Report a Traffic Counter Issue': 271, + 'Report a Technology issue': 270, + 'UKG Pro / Workforce Management Issues': 426, + 'Store Transportation Request': 493, +}; + +/** + * Create a Store Support ticket (JSM request) using the Service Desk API. + * @param {Object} params + * @param {string} params.subType - Exact key from REQUEST_TYPE_MAP (e.g. "Register Not functioning properly") + * @param {string} [params.onBehalfOf] - email or accountId (becomes raiseOnBehalfOf) + * @param {string} params.summary + * @param {string} [params.description] + * @param {string|number} [params.storeNumber] + * @param {Object} [params.additional] - extra customfield_* values to merge into requestFieldValues + */ +export function getSupportedSSSubTypes() { + return Object.keys(REQUEST_TYPE_MAP); +} + +export { REQUEST_TYPE_MAP }; + +/** + * Resolve a store number (e.g. "00305" or 305) to the proper Assets object reference + * for the Store custom field in a JSM request. + * + * This is a "service object" / Assets object and therefore uses the + * api.atlassian.com workspace-scoped endpoint: + * POST https://api.atlassian.com/jsm/assets/workspace/{workspaceId}/v1/object/aql + * { "qlQuery": "objectTypeId = 109 AND \"Store Number\" = \"00305\"", ... } + * + * References are passed as: + * "customfield_10261": [ { "objectId": "82288" } ] + * + * Workspace ID can be provided via JIRA_ASSETS_WORKSPACE_ID or auto-discovered + * from /rest/servicedeskapi/assets/workspace . + */ +async function getAssetsWorkspaceId() { + if (config.jira.assetsWorkspaceId) { + return config.jira.assetsWorkspaceId; + } + + const list = await listAssetsWorkspacesRaw(); + const first = list.workspaces[0]; + if (first?.workspaceId) { + logger.info(`Discovered Assets workspaceId via /rest/servicedeskapi/assets/workspace: ${first.workspaceId}`); + if (list.workspaces.length > 1) { + logger.warn(`Multiple Assets workspaces are visible to this account (${list.workspaces.length}); using the first. Set JIRA_ASSETS_WORKSPACE_ID explicitly to disambiguate.`, { + workspaces: list.workspaces.map(w => w.workspaceId) + }); + } + return first.workspaceId; + } + + throw new Error('JIRA_ASSETS_WORKSPACE_ID is required for Assets object lookup (or ensure /rest/servicedeskapi/assets/workspace is accessible).'); +} + +/** + * Return the full list of Assets workspaces the current account can see, plus + * the raw payload for diagnostics. Never throws. + */ +async function listAssetsWorkspacesRaw() { + try { + const resp = await jiraClient.get('/rest/servicedeskapi/assets/workspace'); + const data = resp.data; + + let entries = []; + if (Array.isArray(data)) entries = data; + else if (Array.isArray(data?.values)) entries = data.values; + else if (Array.isArray(data?.workspaces)) entries = data.workspaces; + else if (data && typeof data === 'object') entries = [data]; + + const workspaces = entries + .map(e => ({ workspaceId: e.workspaceId || e.id || e.key || e.workspaceID || null })) + .filter(w => w.workspaceId); + + return { httpStatus: resp.status, workspaces, raw: data }; + } catch (e) { + return { + httpStatus: e.response?.status || 'network', + workspaces: [], + raw: e.response?.data || null, + error: e.message + }; + } +} + +/** + * Filter out response headers that would leak scope/tenant/session identifiers + * or noise (cookies, tracing tokens, CORS bookkeeping). We keep just the + * Atlassian informational headers, which are the ones useful for debugging + * unexpected empty results (rate-limit, tracing, deprecation, request id). + */ +function pickInterestingHeaders(headers = {}) { + const wanted = new Set([ + 'content-type', 'content-length', + 'x-request-id', 'x-arequestid', 'x-arequest-id', + 'x-ratelimit-limit', 'x-ratelimit-remaining', 'x-ratelimit-reset', + 'x-atlassian-request-id', 'x-atlassian-trace-id', + 'atl-traceid', 'atl-request-id', + 'x-atlassian-server-status', 'x-atlassian-cursor', + 'x-content-type-options', 'x-frame-options', + 'deprecation', 'sunset', 'warning', 'retry-after' + ]); + const out = {}; + for (const [k, v] of Object.entries(headers)) { + if (wanted.has(k.toLowerCase())) out[k] = v; + } + return out; +} + +/** + * Low-level AQL POST helper. Never throws on non-2xx. + * Returns { status, statusText, data, headers, requestUrl, requestBody, workspaceId, error }. + */ +async function runAssetsAql(qlQuery, { resultPerPage = 5, includeAttributes = true, extraBody = {} } = {}) { + const workspaceId = await getAssetsWorkspaceId(); + const aqlUrl = `https://api.atlassian.com/jsm/assets/workspace/${workspaceId}/v1/object/aql`; + + const authHeader = jiraClient.defaults.headers.Authorization + || jiraClient.defaults.headers.common?.Authorization; + + const body = { qlQuery, resultPerPage, includeAttributes, ...extraBody }; + + logger.debug('Assets AQL request', { aqlUrl, qlQuery, resultPerPage }); + + try { + const resp = await axios.post(aqlUrl, body, { + headers: { + 'Authorization': authHeader, + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, + timeout: 15000, + validateStatus: () => true + }); + return { + status: resp.status, + statusText: resp.statusText, + data: resp.data ?? null, + headers: pickInterestingHeaders(resp.headers || {}), + requestUrl: aqlUrl, + requestBody: body, + workspaceId + }; + } catch (err) { + return { + status: err.response?.status || 'network', + statusText: err.response?.statusText || err.code || 'error', + data: err.response?.data || null, + headers: pickInterestingHeaders(err.response?.headers || {}), + requestUrl: aqlUrl, + requestBody: body, + workspaceId, + error: err.message + }; + } +} + +/** + * Low-level GET helper against api.atlassian.com Assets endpoints. + * Same shape as runAssetsAql. Use for schema/objecttype introspection. + */ +async function runAssetsGet(path) { + const workspaceId = await getAssetsWorkspaceId(); + const url = `https://api.atlassian.com/jsm/assets/workspace/${workspaceId}/v1${path}`; + + const authHeader = jiraClient.defaults.headers.Authorization + || jiraClient.defaults.headers.common?.Authorization; + + try { + const resp = await axios.get(url, { + headers: { 'Authorization': authHeader, 'Accept': 'application/json' }, + timeout: 15000, + validateStatus: () => true + }); + return { + status: resp.status, + statusText: resp.statusText, + data: resp.data ?? null, + headers: pickInterestingHeaders(resp.headers || {}), + requestUrl: url, + workspaceId + }; + } catch (err) { + return { + status: err.response?.status || 'network', + statusText: err.response?.statusText || err.code || 'error', + data: err.response?.data || null, + headers: pickInterestingHeaders(err.response?.headers || {}), + requestUrl: url, + workspaceId, + error: err.message + }; + } +} + +/** + * List all Assets schemas visible to the current token. Critical diagnostic: + * if this returns zero schemas, the token has no Assets access at all + * (regardless of what workspace id is used). + */ +export async function listAssetsSchemas() { + return runAssetsGet('/objectschema/list'); +} + +/** + * Fetch a single Assets object type (id, name, attributes). If HTTP 200, + * the token can see the type — and the attribute names in the response are + * authoritative for AQL queries. + */ +export async function getAssetsObjectType(objectTypeId) { + const [detail, attributes] = await Promise.all([ + runAssetsGet(`/objecttype/${objectTypeId}`), + runAssetsGet(`/objecttype/${objectTypeId}/attributes`) + ]); + return { detail, attributes }; +} + +/** + * Flatten one Assets AQL "value" (object entry) into a compact shape suitable + * for humans debugging attribute names/values. Different Assets tenants return + * subtly different envelopes (attributes[].objectTypeAttribute vs typeAttribute, + * objectAttributeValues[].value vs displayValue), so we're defensive. + */ +function summarizeAssetsObject(obj) { + if (!obj || typeof obj !== 'object') return null; + + const attributes = Array.isArray(obj.attributes) ? obj.attributes.map(attr => { + const meta = attr.objectTypeAttribute || attr.typeAttribute || {}; + const rawValues = Array.isArray(attr.objectAttributeValues) ? attr.objectAttributeValues : []; + const values = rawValues.map(v => v.displayValue ?? v.value ?? v.searchValue ?? null).filter(v => v !== null); + return { + id: attr.objectTypeAttributeId || meta.id || null, + name: meta.name || null, + values + }; + }) : []; + + return { + id: obj.id || null, + objectKey: obj.objectKey || null, + name: obj.label || obj.name || null, + objectType: obj.objectType?.name || null, + objectTypeId: obj.objectType?.id || null, + attributes + }; +} + +async function resolveStoreAssetReference(rawStoreNumber) { + if (!rawStoreNumber) return null; + + const normalized = String(rawStoreNumber).padStart(5, '0'); + const objectTypeId = config.jira.assetsStoreObjectTypeId || '109'; + const attribute = config.jira.assetsStoreNumberAttribute || 'Store Number'; + const attrId = config.jira.assetsStoreNumberAttributeId; + + const queries = [ + `objectTypeId = ${objectTypeId} AND "${attribute}" = "${normalized}"` + ]; + if (attrId) { + queries.push(`objectTypeId = ${objectTypeId} AND attribute[${attrId}] = "${normalized}"`); + } + + let lastResult = null; + + for (const qlQuery of queries) { + logger.info('Assets AQL lookup for store number', { qlQuery, storeNumber: normalized }); + + const result = await runAssetsAql(qlQuery, { resultPerPage: 1, includeAttributes: true }); + lastResult = result; + + if (result.status !== 200) { + logger.error('Assets AQL variant failed', { + qlQuery, + status: result.status, + error: result.error, + // data may contain useful "code"/"message" from Atlassian; safe to log + atlassianError: result.data + }); + continue; + } + + const data = result.data || {}; + const values = Array.isArray(data.values) ? data.values : []; + const total = typeof data.total === 'number' ? data.total : values.length; + + if (total === 0 || values.length === 0) { + logger.warn('Assets AQL returned zero results for variant', { qlQuery, total, storeNumber: normalized }); + continue; + } + + const objectId = extractObjectIdFromResponse(data); + if (!objectId) { + logger.warn('Assets AQL returned results but no extractable id', { + qlQuery, + storeNumber: normalized, + valuesSample: values[0] + }); + continue; + } + + logger.info('Resolved store to Assets object', { storeNumber: normalized, objectId: String(objectId) }); + return [{ objectId: String(objectId) }]; + } + + const total = lastResult?.data?.total ?? 'unknown'; + throw new Error( + `Failed to resolve Store Number ${normalized} via Assets (objectTypeId=${objectTypeId}). ` + + `No results or unparseable id across all variants. Last total=${total}` + ); +} + +/** + * Diagnostic helper: run several AQL variants for a given store number and + * return each result side-by-side, plus workspace/schema/object-type + * introspection and a plain-English diagnosis. Intended for a dev-only debug + * endpoint. + */ +export async function probeAssetsForStore(rawStoreNumber, { extraVariants = [] } = {}) { + const raw = String(rawStoreNumber ?? '').trim(); + const padded = raw ? raw.padStart(5, '0') : ''; + const unpadded = raw.replace(/^0+/, '') || raw; + + const objectTypeId = config.jira.assetsStoreObjectTypeId || '109'; + const schemaId = config.jira.assetsStoreSchemaId; + const attribute = config.jira.assetsStoreNumberAttribute || 'Store Number'; + const attrId = config.jira.assetsStoreNumberAttributeId; + + // -------- Introspection -------- + + // A. Workspace discovery — did we get one at all? What are ALL visible workspaces? + const workspacesList = await listAssetsWorkspacesRaw(); + let workspaceIdResolved = null; + let workspaceIdError = null; + try { + workspaceIdResolved = await getAssetsWorkspaceId(); + } catch (e) { + workspaceIdError = e.message; + } + + // B. Schemas the token can actually see. If empty, permissions are the issue. + let schemasProbe = null; + if (workspaceIdResolved) { + const schemasResp = await listAssetsSchemas(); + let visibleSchemas = []; + const data = schemasResp.data; + // Response shape varies: sometimes an array, sometimes { values: [...] }, sometimes { objectschemas: [...] } + const list = Array.isArray(data) ? data + : Array.isArray(data?.values) ? data.values + : Array.isArray(data?.objectschemas) ? data.objectschemas + : Array.isArray(data?.objectSchemas) ? data.objectSchemas + : []; + visibleSchemas = list.map(s => ({ + id: s.id ?? null, + name: s.name ?? null, + objectSchemaKey: s.objectSchemaKey ?? s.key ?? null + })); + schemasProbe = { + httpStatus: schemasResp.status, + requestUrl: schemasResp.requestUrl, + count: visibleSchemas.length, + schemas: visibleSchemas, + raw: schemasResp.status === 200 ? undefined : schemasResp.data, + headers: schemasResp.headers + }; + } + + // C. Object type detail — is object type 109 visible? What are its attributes actually called? + let objectTypeProbe = null; + if (workspaceIdResolved) { + const { detail, attributes } = await getAssetsObjectType(objectTypeId); + const attrList = Array.isArray(attributes.data) ? attributes.data + : Array.isArray(attributes.data?.values) ? attributes.data.values + : []; + objectTypeProbe = { + detail: { + httpStatus: detail.status, + requestUrl: detail.requestUrl, + name: detail.data?.name ?? null, + objectSchemaId: detail.data?.objectSchemaId ?? null, + raw: detail.status === 200 ? { id: detail.data?.id, name: detail.data?.name, objectSchemaId: detail.data?.objectSchemaId, description: detail.data?.description } : detail.data, + headers: detail.headers + }, + attributes: { + httpStatus: attributes.status, + requestUrl: attributes.requestUrl, + count: attrList.length, + names: attrList.map(a => ({ + id: a.id ?? null, + name: a.name ?? null, + type: a.type ?? a.defaultType?.name ?? null, + system: a.system ?? null + })), + raw: attributes.status === 200 ? undefined : attributes.data, + headers: attributes.headers + } + }; + } + + // -------- AQL variants -------- + + const variants = []; + + if (schemaId) { + variants.push({ label: 'schema_probe', qlQuery: `objectSchemaId = ${schemaId}`, resultPerPage: 5 }); + } + variants.push({ label: 'object_type_probe', qlQuery: `objectTypeId = ${objectTypeId}`, resultPerPage: 5 }); + variants.push({ label: 'object_type_by_name', qlQuery: `objectType = "Store"`, resultPerPage: 5 }); + + if (raw) { + variants.push({ label: 'attr_name_padded', qlQuery: `objectTypeId = ${objectTypeId} AND "${attribute}" = "${padded}"`, resultPerPage: 3 }); + variants.push({ label: 'attr_name_unpadded', qlQuery: `objectTypeId = ${objectTypeId} AND "${attribute}" = "${unpadded}"`, resultPerPage: 3 }); + variants.push({ label: 'attr_name_like_padded', qlQuery: `objectTypeId = ${objectTypeId} AND "${attribute}" LIKE "${padded}"`, resultPerPage: 3 }); + + variants.push({ label: 'name_padded', qlQuery: `objectTypeId = ${objectTypeId} AND Name = "${padded}"`, resultPerPage: 3 }); + variants.push({ label: 'name_unpadded', qlQuery: `objectTypeId = ${objectTypeId} AND Name = "${unpadded}"`, resultPerPage: 3 }); + variants.push({ label: 'name_like_padded', qlQuery: `objectTypeId = ${objectTypeId} AND Name LIKE "${padded}"`, resultPerPage: 3 }); + + // Schema-only variants (no objectTypeId filter) in case the type filter is what's dropping results. + if (schemaId) { + variants.push({ label: 'schema_name_padded', qlQuery: `objectSchemaId = ${schemaId} AND Name = "${padded}"`, resultPerPage: 3 }); + variants.push({ label: 'schema_name_unpadded', qlQuery: `objectSchemaId = ${schemaId} AND Name = "${unpadded}"`, resultPerPage: 3 }); + } + + if (attrId) { + variants.push({ label: 'attr_id_padded', qlQuery: `objectTypeId = ${objectTypeId} AND attribute[${attrId}] = "${padded}"`, resultPerPage: 3 }); + variants.push({ label: 'attr_id_unpadded', qlQuery: `objectTypeId = ${objectTypeId} AND attribute[${attrId}] = "${unpadded}"`, resultPerPage: 3 }); + } + } + + for (const v of extraVariants) { + variants.push({ label: v.label || 'custom', qlQuery: v.qlQuery, resultPerPage: v.resultPerPage ?? 5 }); + } + + const results = []; + for (const v of variants) { + const r = await runAssetsAql(v.qlQuery, { resultPerPage: v.resultPerPage, includeAttributes: true }); + const values = Array.isArray(r.data?.values) ? r.data.values : []; + results.push({ + variant: v.label, + qlQuery: v.qlQuery, + httpStatus: r.status, + statusText: r.statusText, + total: r.data?.total ?? values.length, + objects: values.map(summarizeAssetsObject), + atlassianError: r.status === 200 ? undefined : r.data, + headers: r.headers, + // Truncated raw body so we can see *everything* Atlassian sent back + // (some tenants surface hints in "hasMoreResults", "objectTypeAttributes", etc.) + rawBody: r.data && typeof r.data === 'object' + ? JSON.parse(JSON.stringify(r.data)) // deep copy so we don't mutate + : r.data + }); + } + + // -------- Diagnosis -------- + + const diagnosis = buildAssetsProbeDiagnosis({ + workspaceIdResolved, + workspaceIdError, + workspacesList, + schemasProbe, + objectTypeProbe, + variantResults: results, + configuredAttribute: attribute, + configuredAttributeId: attrId, + configuredSchemaId: schemaId, + configuredObjectTypeId: objectTypeId, + jiraEmail: config.jira.email + }); + + return { + input: { raw, padded, unpadded }, + config: { + workspaceId: workspaceIdResolved || `error: ${workspaceIdError}`, + objectTypeId, + schemaId: schemaId || null, + attribute, + attrId: attrId || null, + storeCustomFieldId: config.jira.storeCustomFieldId || 'customfield_10261', + authType: config.jira.authType, + jiraEmail: config.jira.email ? maskEmail(config.jira.email) : null + }, + workspace: { + resolvedId: workspaceIdResolved, + error: workspaceIdError, + allVisible: workspacesList.workspaces, + httpStatus: workspacesList.httpStatus + }, + schemas: schemasProbe, + objectType: objectTypeProbe, + variants: results, + diagnosis + }; +} + +function maskEmail(email) { + if (!email || !email.includes('@')) return email || null; + const [local, domain] = email.split('@'); + const shown = local.length <= 3 ? local[0] : `${local.slice(0, 3)}…`; + return `${shown}@${domain}`; +} + +function buildAssetsProbeDiagnosis({ + workspaceIdResolved, + workspaceIdError, + workspacesList, + schemasProbe, + objectTypeProbe, + variantResults, + configuredAttribute, + configuredAttributeId, + configuredSchemaId, + configuredObjectTypeId, + jiraEmail +}) { + const notes = []; + const suggestions = []; + let likelyCause = 'unknown'; + + const visibleWorkspaces = workspacesList?.workspaces || []; + const email = jiraEmail || '(JIRA_EMAIL)'; + + if (!workspaceIdResolved) { + likelyCause = 'workspace_not_discovered'; + notes.push(`Could not discover Assets workspace id (${workspaceIdError}).`); + suggestions.push('Set JIRA_ASSETS_WORKSPACE_ID explicitly, or ensure /rest/servicedeskapi/assets/workspace is reachable.'); + return { likelyCause, notes, suggestions }; + } + + if (visibleWorkspaces.length > 1) { + notes.push(`Account can see ${visibleWorkspaces.length} Assets workspaces: ${visibleWorkspaces.map(w => w.workspaceId).join(', ')}. Using ${workspaceIdResolved}.`); + suggestions.push('If the Store schema lives in a different workspace, set JIRA_ASSETS_WORKSPACE_ID explicitly.'); + } + + const schemaHttp = schemasProbe?.httpStatus; + const schemaCount = schemasProbe?.count ?? 0; + + if (schemaHttp && schemaHttp !== 200) { + likelyCause = 'schema_list_error'; + notes.push(`GET /objectschema/list returned HTTP ${schemaHttp}. The token cannot list schemas.`); + if (schemaHttp === 401 || schemaHttp === 403) { + suggestions.push(`Add ${email} to an Object Schema role on the target schema in Jira → Assets → Object schemas → Configure → Roles. In Assets, API-token scopes (read:cmdb-*:jira) are NOT sufficient on their own; the user still needs schema-level role membership.`); + } + return { likelyCause, notes, suggestions }; + } + + if (schemaCount === 0) { + likelyCause = 'no_schema_visibility'; + notes.push('GET /objectschema/list returned HTTP 200 with 0 schemas — this account has no visibility to any Assets schema, so every AQL against it returns total=0.'); + suggestions.push(`Ask a Jira Assets admin to add ${email} to a role on the Store schema in Jira → Assets → Object schemas → Configure → Roles. "Object Schema Viewer" or "Object Schema User" is enough to look up store objects; "Developer" is needed to create/update objects.`); + suggestions.push('The four read:cmdb-* / write:cmdb-* scopes on the token are necessary but not sufficient — Assets enforces a separate per-schema role check on top of the OAuth scopes.'); + return { likelyCause, notes, suggestions }; + } + + const visibleSchemaIds = (schemasProbe?.schemas || []).map(s => String(s.id)); + const visibleSchemaSummary = (schemasProbe?.schemas || []).map(s => `${s.id}:${s.name}`).join(', '); + notes.push(`Account can see ${schemaCount} schema(s): ${visibleSchemaSummary}.`); + + if (configuredSchemaId && !visibleSchemaIds.includes(String(configuredSchemaId))) { + likelyCause = 'schema_not_visible'; + notes.push(`Configured JIRA_ASSETS_STORE_SCHEMA_ID=${configuredSchemaId} is NOT in the list of schemas this account can see. AQL against schema ${configuredSchemaId} will always return total=0.`); + suggestions.push(`Ask a Jira Assets admin to add ${email} to a role on the Store schema (id ${configuredSchemaId}) in Jira → Assets → Object schemas → Configure → Roles. "Object Schema Viewer" is enough for read; "Developer" for writes.`); + suggestions.push('Reminder: Jira Assets enforces per-schema role membership on top of OAuth scopes. Granting the token the read:cmdb-* / write:cmdb-* scopes is necessary but NOT sufficient — the underlying user must also be in a role on the schema.'); + if (visibleSchemaIds.length === 1) { + suggestions.push(`Right now the account is only in a role on schema ${visibleSchemaIds[0]} (${visibleSchemaSummary}). Same admin action needs to happen for the Store schema.`); + } + return { likelyCause, notes, suggestions }; + } + + const otDetailStatus = objectTypeProbe?.detail?.httpStatus; + const otAttrStatus = objectTypeProbe?.attributes?.httpStatus; + + if (otDetailStatus && otDetailStatus !== 200) { + if (otDetailStatus === 403) { + likelyCause = 'object_type_forbidden'; + notes.push(`GET /objecttype/${configuredObjectTypeId} returned HTTP 403. The account can list schemas but cannot see this object type — almost always because it is missing an Object Schema role on the Store schema.`); + suggestions.push(`Add ${email} to an Object Schema role on the Store schema in Jira → Assets → Object schemas → Configure → Roles.`); + } else { + likelyCause = 'object_type_not_visible'; + notes.push(`GET /objecttype/${configuredObjectTypeId} returned HTTP ${otDetailStatus}. The configured objectTypeId is either wrong or not visible to this account.`); + suggestions.push(`Verify JIRA_ASSETS_STORE_OBJECT_TYPE_ID matches the actual Store type id in Jira Assets. Note the .env has a typo: IRA_ASSETS_STORE_OBJECT_TYPE_ID (missing leading J) — the app currently defaults to 109.`); + } + return { likelyCause, notes, suggestions }; + } + + if (otDetailStatus === 200) { + notes.push(`Object type is visible: ${objectTypeProbe.detail.name} (schema ${objectTypeProbe.detail.objectSchemaId}).`); + } + + if (otAttrStatus === 200 && objectTypeProbe.attributes.count > 0) { + const attrNames = objectTypeProbe.attributes.names.map(a => a.name).filter(Boolean); + const attrMatch = attrNames.find(n => n.toLowerCase() === (configuredAttribute || '').toLowerCase()); + if (!attrMatch) { + likelyCause = 'attribute_name_mismatch'; + notes.push(`The configured attribute "${configuredAttribute}" is NOT among the object type's attributes. Actual attribute names: ${attrNames.join(', ')}.`); + const guess = attrNames.find(n => /store|number|store\s*id/i.test(n)); + if (guess) suggestions.push(`Set JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE="${guess}" (or use the id form via JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE_ID).`); + else suggestions.push('Pick the correct attribute from the list above and set JIRA_ASSETS_STORE_NUMBER_ATTRIBUTE (or ...ATTRIBUTE_ID) accordingly.'); + return { likelyCause, notes, suggestions }; + } + notes.push(`Attribute "${configuredAttribute}" exists on the object type.`); + } + + const anyHits = variantResults.some(v => v.total > 0); + if (!anyHits) { + likelyCause = 'value_format_mismatch'; + notes.push('Schema and object type are visible but no AQL variant returned rows. Attribute values are likely stored in a form none of the variants matched.'); + suggestions.push('Re-run the probe without a storeNumber to sample real Store objects: curl "http://localhost:1866/api/wxccai/debug/assetsProbe" — the object_type_probe row will show up to 5 real Store objects with their actual attribute values, so you can see how Store Number is stored (leading zeros, prefix, etc.).'); + } else { + const winners = variantResults.filter(v => v.total > 0).map(v => v.variant); + likelyCause = 'success'; + notes.push(`These variants returned rows: ${winners.join(', ')}. Lock resolveStoreAssetReference to the first one.`); + } + + return { likelyCause, notes, suggestions }; +} + +// Helper kept outside the loop +function extractObjectIdFromResponse(respData) { + if (!respData || typeof respData !== 'object') return null; + + if (respData.id) return respData.id; + if (respData.objectId) return respData.objectId; + + const listKeys = ['values', 'objectEntries', 'objects', 'objectList', 'results', 'items']; + for (const key of listKeys) { + const list = respData[key]; + if (Array.isArray(list)) { + for (const item of list) { + if (item && typeof item === 'object') { + if (item.id) return item.id; + if (item.objectId) return item.objectId; + if (item.object && item.object.id) return item.object.id; + if (item.attributes && item.attributes.id) return item.attributes.id; + } + } + } + } + + function deepFind(obj, depth = 0) { + if (depth > 6 || obj == null || typeof obj !== 'object') return null; + if (obj.id && (typeof obj.id === 'string' || typeof obj.id === 'number')) return obj.id; + if (obj.objectId && (typeof obj.objectId === 'string' || typeof obj.objectId === 'number')) return obj.objectId; + if (Array.isArray(obj)) { + for (const el of obj) { + const found = deepFind(el, depth + 1); + if (found) return found; + } + } else { + for (const k of Object.keys(obj)) { + const found = deepFind(obj[k], depth + 1); + if (found) return found; + } + } + return null; + } + + return deepFind(respData); +} + +export async function createSSRequest(params = {}) { + const { + subType, + onBehalfOf, + summary, + description, + storeNumber, + additional = {} + } = params; + + if (!subType || !summary) { + throw new Error('subType and summary are required'); + } + + const requestTypeId = REQUEST_TYPE_MAP[subType]; + if (!requestTypeId) { + throw new Error(`Unknown subType: "${subType}". Must be one of the supported values.`); + } + + const serviceDeskId = config.jira.serviceDeskId || '170'; + + // Build requestFieldValues. Store Number is special because it is an Assets object. + const storeCustomField = config.jira.storeCustomFieldId || 'customfield_10261'; + + const requestFieldValues = { + summary, + description: description || summary, + ...additional + }; + + if (storeNumber) { + // Resolve to proper Assets object reference: [ { "objectId": "82288" } ] + const storeRef = await resolveStoreAssetReference(storeNumber); + if (storeRef) { + requestFieldValues[storeCustomField] = storeRef; + } + } + + const payload = { + serviceDeskId: String(serviceDeskId), + requestTypeId: String(requestTypeId), + requestFieldValues + }; + + if (onBehalfOf) { + payload.raiseOnBehalfOf = onBehalfOf; + } + + try { + const response = await jiraClient.post( + '/rest/servicedeskapi/request', + payload, + { + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + } + } + ); + + const data = response.data; + logger.info('SS ticket created successfully', { + issueKey: data?.issueKey, + subType, + storeNumber: String(storeNumber || '').padStart(5, '0') + }); + return data; + } catch (err) { + const errData = err.response?.data || {}; + const message = errData.errorMessage || errData.message || err.message || 'Unknown error creating SS request'; + logger.error('Failed to create SS request', { + subType, + storeNumber, + status: err.response?.status, + details: errData + }); + const error = new Error(message); + error.status = err.response?.status; + error.details = errData; + throw error; + } +} + +// Export everything +export default { + fetchJiraIssue, + fetchPlainDescription, + fetchPublicComments, + searchOpenTicketsByReporterEmail, + attachFileToJira, + attachReadableTranscript, + postWebexSummaryComment, + createSSRequest, + getSupportedSSSubTypes, + probeAssetsForStore, + getTicketStatus, + updateTicket, + addComment, + getTransitions, + transitionTicket, + closeTicket, + jiraClient // export the client so other files can use it +}; diff --git a/src/utilities/adfToPlainText.js b/src/utilities/adfToPlainText.js new file mode 100644 index 0000000..6b7a506 --- /dev/null +++ b/src/utilities/adfToPlainText.js @@ -0,0 +1,30 @@ +export function adfToPlainText(node) { + if (!node || typeof node !== 'object') return ''; + let text = ''; + + if (node.type === 'text' && node.text) { + text += node.text; + if (node.marks) { + node.marks.forEach(mark => { + if (mark.type === 'link' && mark.attrs?.href) { + text += ` (${mark.attrs.href})`; + } + }); + } + } + + if (node.content && Array.isArray(node.content)) { + node.content.forEach(child => { + const childText = adfToPlainText(child); + if (childText) { + text += childText; + if (['paragraph', 'heading', 'bulletList', 'orderedList', 'listItem'].includes(child.type)) { + text += '\n\n'; + } else { + text += ' '; + } + } + }); + } + return text.trim(); +} \ No newline at end of file diff --git a/src/utilities/logger.js b/src/utilities/logger.js new file mode 100644 index 0000000..90b723d --- /dev/null +++ b/src/utilities/logger.js @@ -0,0 +1,60 @@ +import winston from 'winston'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const LOG_DIR = path.join(__dirname, '../../logs'); + +// Ensure logs directory exists +import fs from 'fs'; +if (!fs.existsSync(LOG_DIR)) { + fs.mkdirSync(LOG_DIR, { recursive: true }); +} + +const logger = winston.createLogger({ + level: process.env.NODE_ENV === 'production' ? 'info' : 'debug', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.json() + ), + transports: [ + // Console (for development) + new winston.transports.Console({ + format: winston.format.simple() + }), + // Main application log file + new winston.transports.File({ + filename: path.join(LOG_DIR, 'app.log'), + maxsize: 10 * 1024 * 1024, // 10MB + maxFiles: 5, + tailable: true + }) + ] +}); + +// Dedicated transport for Webex callbacks (human-readable + JSON) +// IMPORTANT: This now also goes to console (stdout) so that `docker logs` and +// container platforms can see the WEBEX_TRANSCRIPT events without needing +// to inspect files inside the container. +const webexLogger = winston.createLogger({ + level: 'info', + format: winston.format.combine( + winston.format.timestamp(), + winston.format.printf(({ timestamp, message, ...meta }) => { + return `[${timestamp}] ${message} | ${JSON.stringify(meta)}`; + }) + ), + transports: [ + // Console so webhook activity is visible in docker logs / platform logs + new winston.transports.Console(), + new winston.transports.File({ + filename: path.join(LOG_DIR, 'webex-callbacks.log'), + maxsize: 20 * 1024 * 1024, // 20MB + maxFiles: 10, + tailable: true + }) + ] +}); + +export { logger, webexLogger }; +export default logger; \ No newline at end of file diff --git a/ss-fields-266.json b/ss-fields-266.json new file mode 100644 index 0000000..9c59f89 --- /dev/null +++ b/ss-fields-266.json @@ -0,0 +1,89 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Type in short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click on it to select.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Please provide as much detail as possible (ie: What is broken with the device? Is it a specific cord that needs to be replaced? Include all relevant information. )", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10544", + "name": "Affected Device", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10544 + }, + "visible": true + }, + { + "fieldId": "customfield_10294", + "name": "Date/Time Issue Occured", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "datetime", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:datetime", + "customId": 10294 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-267.json b/ss-fields-267.json new file mode 100644 index 0000000..58ea119 --- /dev/null +++ b/ss-fields-267.json @@ -0,0 +1,115 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert Short Description of Issue", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click on it to select.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "customfield_10381", + "name": "Report System", + "description": "Click on the drop down and select how this report is accessed.", + "required": false, + "defaultValues": [], + "validValues": [ + { + "value": "11381", + "label": "Cognos", + "children": [] + }, + { + "value": "11382", + "label": "Metric Insight", + "children": [] + }, + { + "value": "11383", + "label": "Tableau", + "children": [] + }, + { + "value": "11384", + "label": "Stores Productivity Report", + "children": [] + }, + { + "value": "11385", + "label": "Insights to Action (ITA)", + "children": [] + } + ], + "jiraSchema": { + "type": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:select", + "customId": 10381 + }, + "visible": true + }, + { + "fieldId": "customfield_10480", + "name": "Report Name", + "description": "What is the name of the report?", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10480 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Describe the issue you are having. The more info you provide will help us get you help faster!", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-268.json b/ss-fields-268.json new file mode 100644 index 0000000..2376768 --- /dev/null +++ b/ss-fields-268.json @@ -0,0 +1,202 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and select", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "customfield_10548", + "name": "Reason for Omni Request", + "description": "Click the 1st drop down to select what needs enabled / disabled. Click the 2nd drop down to select reason code.", + "required": true, + "defaultValues": [], + "validValues": [ + { + "value": "13461", + "label": "BOSS Enable", + "children": [ + { + "value": "13467", + "label": "Printer Issues Resolved", + "children": [] + }, + { + "value": "13468", + "label": "NSO/Maintenance Completed", + "children": [] + }, + { + "value": "13469", + "label": "Other", + "children": [] + } + ] + }, + { + "value": "13462", + "label": "BOSS Disable", + "children": [ + { + "value": "13470", + "label": "Printer Issues", + "children": [] + }, + { + "value": "13471", + "label": "NSO/Maintenance", + "children": [] + }, + { + "value": "13472", + "label": "Other", + "children": [] + } + ] + }, + { + "value": "13463", + "label": "Pickup Enable", + "children": [ + { + "value": "13473", + "label": "Printer Issues Resolved", + "children": [] + }, + { + "value": "13474", + "label": "NSO/Maintenance Completed", + "children": [] + }, + { + "value": "13475", + "label": "Other", + "children": [] + } + ] + }, + { + "value": "13464", + "label": "Pickup Disable", + "children": [ + { + "value": "13476", + "label": "Printer Issues", + "children": [] + }, + { + "value": "13477", + "label": "NSO/Maintenance", + "children": [] + }, + { + "value": "13478", + "label": "Other", + "children": [] + } + ] + }, + { + "value": "13465", + "label": "BOSS and Pickup Enable", + "children": [ + { + "value": "13479", + "label": "Printer Issues Resolved", + "children": [] + }, + { + "value": "13480", + "label": "NSO/Maintenance Completed", + "children": [] + }, + { + "value": "13481", + "label": "Other", + "children": [] + } + ] + }, + { + "value": "13466", + "label": "BOSS and Pickup Disable", + "children": [ + { + "value": "13482", + "label": "Printer Issues", + "children": [] + }, + { + "value": "13483", + "label": "NSO/Maintenance", + "children": [] + }, + { + "value": "13484", + "label": "Other", + "children": [] + } + ] + } + ], + "jiraSchema": { + "type": "option-with-child", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:cascadingselect", + "customId": 10548 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "If you selected \"Other\" as the Reason Code above please provide detail here. Otherwise please briefly describe why you need Omni Turn Off / On Completed.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-269.json b/ss-fields-269.json new file mode 100644 index 0000000..060b2b6 --- /dev/null +++ b/ss-fields-269.json @@ -0,0 +1,89 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert Short Description of Issue", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click on it to select.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "What is the issue? Give examples of what is happening? What troubleshooting have you tried?", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10294", + "name": "Date/Time Issue Occured", + "description": "Date and Time of Incident.", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "datetime", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:datetime", + "customId": 10294 + }, + "visible": true + }, + { + "fieldId": "customfield_10557", + "name": "Transaction Detail", + "description": "Provide transaction details to ensure the correct information is reviewed.", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10557 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-270.json b/ss-fields-270.json new file mode 100644 index 0000000..6fc1dc8 --- /dev/null +++ b/ss-fields-270.json @@ -0,0 +1,105 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click on it to select.", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Describe the issue you are having. The more info you provide will help us get you help faster!", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10454", + "name": "Preferred Contact Method", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [ + { + "value": "11325", + "label": "Email", + "children": [] + }, + { + "value": "11326", + "label": "Phone", + "children": [] + }, + { + "value": "11327", + "label": "Webex", + "children": [] + } + ], + "jiraSchema": { + "type": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:select", + "customId": 10454 + }, + "visible": true + }, + { + "fieldId": "customfield_10426", + "name": "Phone", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10426 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-271.json b/ss-fields-271.json new file mode 100644 index 0000000..a5cbbe0 --- /dev/null +++ b/ss-fields-271.json @@ -0,0 +1,89 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click on it to select.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "When did you start noticing this issue? Is this an issue daily? Does traffic tend to “catch up” after some time?", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10294", + "name": "Date/Time Issue Occured", + "description": "What date / time did you first notice that there was a traffic issue?", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "datetime", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:datetime", + "customId": 10294 + }, + "visible": true + }, + { + "fieldId": "customfield_10544", + "name": "Affected Device", + "description": "Do you have two traffic counters? Which traffic counter is having an issue?", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10544 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-272.json b/ss-fields-272.json new file mode 100644 index 0000000..10cd00e --- /dev/null +++ b/ss-fields-272.json @@ -0,0 +1,75 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and select", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Please provide detailed description of Sterling Application issue. Please make sure to include any order information available", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10294", + "name": "Date/Time Issue Occured", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "datetime", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:datetime", + "customId": 10294 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "Please include a screenshot of what you are showing related to your Sterling Issue", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-273.json b/ss-fields-273.json new file mode 100644 index 0000000..7bcb674 --- /dev/null +++ b/ss-fields-273.json @@ -0,0 +1,103 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and click to select it from the list.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Please provide as much detail as possible regarding the missing hardware.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10544", + "name": "Affected Device", + "description": "What device / hardware are you missing?", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10544 + }, + "visible": true + }, + { + "fieldId": "customfield_10549", + "name": "Police Report Number", + "description": "A Police Report needs to be filed for any lost/stolen hardware. Please make sure to include this.", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10549 + }, + "visible": true + }, + { + "fieldId": "customfield_10294", + "name": "Date / Time Hardware Last Seen or Used", + "description": "When is the last day / time this device was used or in your store?", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "datetime", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:datetime", + "customId": 10294 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-274.json b/ss-fields-274.json new file mode 100644 index 0000000..bbb98be --- /dev/null +++ b/ss-fields-274.json @@ -0,0 +1,61 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of request.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and select", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Please provide as much detail as possible for why your store needs additional hardware.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-275.json b/ss-fields-275.json new file mode 100644 index 0000000..5312b97 --- /dev/null +++ b/ss-fields-275.json @@ -0,0 +1,105 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "Insert short description of issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "Type in your store number and select", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Description", + "description": "Please provide as much detail as possible about your login issue.", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "customfield_10454", + "name": "Preferred Contact Method", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [ + { + "value": "11325", + "label": "Email", + "children": [] + }, + { + "value": "11326", + "label": "Phone", + "children": [] + }, + { + "value": "11327", + "label": "Webex", + "children": [] + } + ], + "jiraSchema": { + "type": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:select", + "customId": 10454 + }, + "visible": true + }, + { + "fieldId": "customfield_10426", + "name": "Phone", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textfield", + "customId": 10426 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-426.json b/ss-fields-426.json new file mode 100644 index 0000000..03d1459 --- /dev/null +++ b/ss-fields-426.json @@ -0,0 +1,103 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "customfield_10563", + "name": "Description", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:textarea", + "customId": 10563 + }, + "visible": true + }, + { + "fieldId": "customfield_10264", + "name": "Urgency", + "description": "", + "required": false, + "defaultValues": [ + { + "value": "10480", + "label": "Medium", + "children": [] + } + ], + "validValues": [ + { + "value": "10478", + "label": "Critical", + "children": [] + }, + { + "value": "10479", + "label": "High", + "children": [] + }, + { + "value": "10480", + "label": "Medium", + "children": [] + }, + { + "value": "10481", + "label": "Low", + "children": [] + } + ], + "jiraSchema": { + "type": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:select", + "customId": 10264 + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-fields-493.json b/ss-fields-493.json new file mode 100644 index 0000000..ea003f8 --- /dev/null +++ b/ss-fields-493.json @@ -0,0 +1,216 @@ +{ + "requestTypeFields": [ + { + "fieldId": "summary", + "name": "Summary", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "summary" + }, + "visible": true + }, + { + "fieldId": "customfield_10261", + "name": "Store Number", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "cmdb-object-field", + "custom": "com.atlassian.jira.plugins.cmdb:cmdb-object-cftype", + "customId": 10261 + }, + "visible": true + }, + { + "fieldId": "customfield_11688", + "name": "Reason for Transportation Request", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [ + { + "value": "13706", + "label": "Urgent Store Closure", + "children": [ + { + "value": "13711", + "label": "Power Outage", + "children": [] + }, + { + "value": "13712", + "label": "Fire", + "children": [] + }, + { + "value": "13713", + "label": "Mall Closure", + "children": [] + }, + { + "value": "13714", + "label": "Weather Hazard", + "children": [] + }, + { + "value": "13715", + "label": "Unexpected Closure", + "children": [] + } + ] + }, + { + "value": "13707", + "label": "Unprofessional Driver Issue", + "children": [ + { + "value": "13716", + "label": "Unsafe / Hostile Work Environment", + "children": [] + }, + { + "value": "13717", + "label": "Profanity / Offensive Language", + "children": [] + }, + { + "value": "13718", + "label": "Receiving shipment anywhere but back room stock door", + "children": [] + } + ] + }, + { + "value": "13708", + "label": "Carrier Issue", + "children": [ + { + "value": "13719", + "label": "Delivering outside of delivery window (includes after 5p & Saturday)", + "children": [] + }, + { + "value": "13720", + "label": "Unprofessional Driver", + "children": [] + }, + { + "value": "13721", + "label": "Damaged Carton (if issue is consistently happening)", + "children": [] + }, + { + "value": "13722", + "label": "Delivery not meeting company standard / policies", + "children": [] + }, + { + "value": "13723", + "label": "Picking up outside of pickup window provided", + "children": [] + } + ] + }, + { + "value": "13709", + "label": "Missed Pickup", + "children": [ + { + "value": "13724", + "label": "BOSS FedEx Express", + "children": [] + }, + { + "value": "13725", + "label": "BOSS FedEx Ground", + "children": [] + }, + { + "value": "13726", + "label": "BOSS USPS", + "children": [] + }, + { + "value": "13727", + "label": "FedEx Ground (Transfer)", + "children": [] + }, + { + "value": "13728", + "label": "FedEx Express (Transfer)", + "children": [] + }, + { + "value": "13729", + "label": "CA Post (BOSS)", + "children": [] + }, + { + "value": "13730", + "label": "Purolator (BOSS)", + "children": [] + }, + { + "value": "13731", + "label": "UPS", + "children": [] + } + ] + }, + { + "value": "13710", + "label": "Ship Manager Issue", + "children": [ + { + "value": "13732", + "label": "Unable to Login - Fedex Ship Manager or UPS Campus Ship Manager (CANADA ONLY)", + "children": [] + } + ] + } + ], + "jiraSchema": { + "type": "option-with-child", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:cascadingselect", + "customId": 11688 + }, + "visible": true + }, + { + "fieldId": "description", + "name": "Detailed Description", + "description": "", + "required": true, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "string", + "system": "description" + }, + "visible": true + }, + { + "fieldId": "attachment", + "name": "Attachment", + "description": "", + "required": false, + "defaultValues": [], + "validValues": [], + "jiraSchema": { + "type": "array", + "items": "attachment", + "system": "attachment" + }, + "visible": true + } + ], + "canRaiseOnBehalfOf": true, + "canAddRequestParticipants": true +} \ No newline at end of file diff --git a/ss-request-types-clean.json b/ss-request-types-clean.json new file mode 100644 index 0000000..4257cb0 --- /dev/null +++ b/ss-request-types-clean.json @@ -0,0 +1,209 @@ +[ + { + "id": "266", + "name": "Broken Device / Hardware", + "defaultName": "Broken Device / Hardware", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "156", + "157" + ], + "canCreateRequest": true + }, + { + "id": "267", + "name": "Business Report Issue", + "defaultName": "Business Report Issue", + "description": "Business Report not updating or showing incorrect data? Click here", + "issueTypeId": "10085", + "groupIds": [ + "156", + "158" + ], + "canCreateRequest": true + }, + { + "id": "268", + "name": "Omni Turn Off / On", + "defaultName": "Omni Turn Off / On", + "description": "", + "issueTypeId": "10082", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "269", + "name": "Register Not Functioning Properly", + "defaultName": "Register Not Functioning Properly", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "156" + ], + "canCreateRequest": true + }, + { + "id": "270", + "name": "Report a Technology Issue", + "defaultName": "Report a Technology Issue", + "description": "Select this option if something you use for work that usually functions, is not functioning as expected.", + "issueTypeId": "10085", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "271", + "name": "Report a Traffic Counter Issue", + "defaultName": "Report a Traffic Counter Issue", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "272", + "name": "Report an Issue with Sterling Application", + "defaultName": "Report an Issue with Sterling Application", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "273", + "name": "Report Missing Hardware", + "defaultName": "Report Missing Hardware", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "157" + ], + "canCreateRequest": true + }, + { + "id": "274", + "name": "Request Additional Hardware", + "defaultName": "Request Additional Hardware", + "description": "", + "issueTypeId": "10082", + "groupIds": [ + "157" + ], + "canCreateRequest": true + }, + { + "id": "493", + "name": "Store Transportation Request", + "defaultName": "Store Transportation Request", + "description": "Click here to report an issue / request assistance with store transportation.", + "issueTypeId": "10085", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "426", + "name": "UKG Pro / Workforce Management Issues", + "defaultName": "UKG Pro / Workforce Management Issues", + "description": "", + "issueTypeId": "10085", + "groupIds": [ + "158" + ], + "canCreateRequest": true + }, + { + "id": "275", + "name": "Unable to Login", + "defaultName": "Unable to Login", + "description": "Try the AEO2GO.com password reset and [unlock|https://aeo.onelogin.com/login2#action=unlock_account] options!\n[Forgot Password| https://aeo.onelogin.com/login2#action=password_reset]\n[Instructions| https://wiki.ae.com/display/KB/AEO2GO+-+How+to+Unlock+Your+SSO+Network+Account]\n\nIt takes seconds to regain access! If that does not work, select this option.", + "issueTypeId": "10085", + "groupIds": [ + "156", + "158" + ], + "canCreateRequest": true + }, + { + "id": "276", + "name": "Bug Resolution", + "defaultName": "Bug Resolution", + "description": "", + "issueTypeId": "10079", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "263", + "name": "Emailed request", + "defaultName": "Emailed request", + "description": "Request received from your email support channel.", + "issueTypeId": "10082", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "359", + "name": "Hardware Request", + "defaultName": "Hardware Request", + "description": "", + "issueTypeId": "10091", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "393", + "name": "Payment Table Validation ", + "defaultName": "Payment Table Validation ", + "description": "", + "issueTypeId": "10082", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "277", + "name": "Process Problem", + "defaultName": "Process Problem", + "description": "", + "issueTypeId": "10079", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "494", + "name": "Questions", + "defaultName": "Questions", + "description": "Request type used by live chat for fallback, only when other request types don't work.", + "issueTypeId": "10008", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "278", + "name": "Recurring Incident", + "defaultName": "Recurring Incident", + "description": "", + "issueTypeId": "10079", + "groupIds": [], + "canCreateRequest": true + }, + { + "id": "280", + "name": "SolarWinds Alert", + "defaultName": "SolarWinds Alert", + "description": "", + "issueTypeId": "10085", + "groupIds": [], + "canCreateRequest": true + } +] \ No newline at end of file diff --git a/ss-request-types.json b/ss-request-types.json new file mode 100644 index 0000000..78939c2 --- /dev/null +++ b/ss-request-types.json @@ -0,0 +1,643 @@ +{ + "_expands": [ + "field" + ], + "size": 20, + "start": 0, + "limit": 100, + "isLastPage": true, + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype?limit=100", + "base": "https://aeo.atlassian.net", + "context": "" + }, + "values": [ + { + "_expands": [ + "field" + ], + "id": "266", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/266" + }, + "name": "Broken Device / Hardware", + "description": "", + "helpText": "", + "defaultName": "Broken Device / Hardware", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "156", + "157" + ], + "icon": { + "id": "10472", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "267", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/267" + }, + "name": "Business Report Issue", + "description": "Business Report not updating or showing incorrect data? Click here", + "helpText": "", + "defaultName": "Business Report Issue", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "156", + "158" + ], + "icon": { + "id": "10492", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10492?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10492?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10492?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10492?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "268", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/268" + }, + "name": "Omni Turn Off / On", + "description": "", + "helpText": "", + "defaultName": "Omni Turn Off / On", + "issueTypeId": "10082", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10487", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10487?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10487?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10487?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10487?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "269", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/269" + }, + "name": "Register Not Functioning Properly", + "description": "", + "helpText": "", + "defaultName": "Register Not Functioning Properly", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "156" + ], + "icon": { + "id": "10484", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10484?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10484?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10484?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10484?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "270", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/270" + }, + "name": "Report a Technology Issue", + "description": "Select this option if something you use for work that usually functions, is not functioning as expected.", + "helpText": "", + "defaultName": "Report a Technology Issue", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10470", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10470?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10470?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10470?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10470?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "271", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/271" + }, + "name": "Report a Traffic Counter Issue", + "description": "", + "helpText": "", + "defaultName": "Report a Traffic Counter Issue", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10648", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10648?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10648?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10648?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10648?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "272", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/272" + }, + "name": "Report an Issue with Sterling Application", + "description": "", + "helpText": "", + "defaultName": "Report an Issue with Sterling Application", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10469", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10469?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10469?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10469?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10469?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "273", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/273" + }, + "name": "Report Missing Hardware", + "description": "", + "helpText": "", + "defaultName": "Report Missing Hardware", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "157" + ], + "icon": { + "id": "10489", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10489?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10489?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10489?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10489?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "274", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/274" + }, + "name": "Request Additional Hardware", + "description": "", + "helpText": "", + "defaultName": "Request Additional Hardware", + "issueTypeId": "10082", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "157" + ], + "icon": { + "id": "10511", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "493", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/493" + }, + "name": "Store Transportation Request", + "description": "Click here to report an issue / request assistance with store transportation.", + "helpText": "", + "defaultName": "Store Transportation Request", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10664", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10664?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10664?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10664?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10664?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "426", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/426" + }, + "name": "UKG Pro / Workforce Management Issues", + "description": "", + "helpText": "", + "defaultName": "UKG Pro / Workforce Management Issues", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "158" + ], + "icon": { + "id": "10499", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10499?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10499?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10499?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10499?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "275", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/275" + }, + "name": "Unable to Login", + "description": "Try the AEO2GO.com password reset and [unlock|https://aeo.onelogin.com/login2#action=unlock_account] options!\n[Forgot Password| https://aeo.onelogin.com/login2#action=password_reset]\n[Instructions| https://wiki.ae.com/display/KB/AEO2GO+-+How+to+Unlock+Your+SSO+Network+Account]\n\nIt takes seconds to regain access! If that does not work, select this option.", + "helpText": "", + "defaultName": "Unable to Login", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [ + "156", + "158" + ], + "icon": { + "id": "10610", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10610?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10610?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10610?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10610?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "276", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/276" + }, + "name": "Bug Resolution", + "description": "", + "helpText": "", + "defaultName": "Bug Resolution", + "issueTypeId": "10079", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10476", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10476?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10476?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10476?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10476?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "263", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/263" + }, + "name": "Emailed request", + "description": "Request received from your email support channel.", + "helpText": "", + "defaultName": "Emailed request", + "issueTypeId": "10082", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10527", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10527?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10527?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10527?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10527?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "359", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/359" + }, + "name": "Hardware Request", + "description": "", + "helpText": "", + "defaultName": "Hardware Request", + "issueTypeId": "10091", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10511", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "393", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/393" + }, + "name": "Payment Table Validation ", + "description": "", + "helpText": "", + "defaultName": "Payment Table Validation ", + "issueTypeId": "10082", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10471", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10471?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10471?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10471?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10471?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "277", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/277" + }, + "name": "Process Problem", + "description": "", + "helpText": "", + "defaultName": "Process Problem", + "issueTypeId": "10079", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10472", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10472?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "494", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/494" + }, + "name": "Questions", + "description": "Request type used by live chat for fallback, only when other request types don't work.", + "helpText": "", + "defaultName": "Questions", + "issueTypeId": "10008", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10466", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10466?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10466?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10466?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10466?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "278", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/278" + }, + "name": "Recurring Incident", + "description": "", + "helpText": "", + "defaultName": "Recurring Incident", + "issueTypeId": "10079", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10507", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10507?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10507?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10507?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10507?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + }, + { + "_expands": [ + "field" + ], + "id": "280", + "_links": { + "self": "https://aeo.atlassian.net/rest/servicedeskapi/servicedesk/170/requesttype/280" + }, + "name": "SolarWinds Alert", + "description": "", + "helpText": "", + "defaultName": "SolarWinds Alert", + "issueTypeId": "10085", + "serviceDeskId": "170", + "portalId": "170", + "groupIds": [], + "icon": { + "id": "10511", + "_links": { + "iconUrls": { + "48x48": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=large", + "24x24": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=small", + "16x16": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=xsmall", + "32x32": "https://aeo.atlassian.net/rest/api/2/universal_avatar/view/type/SD_REQTYPE/avatar/10511?size=medium" + } + } + }, + "restrictionStatus": "OPEN", + "canCreateRequest": true + } + ] +} \ No newline at end of file