The bug: SESSION_COOKIE_OPTIONS had httpOnly: true, so js/app.js could
not see the `id` cookie set by /oauth. Every page load thought the user
was signed out, redirected to Webex, minted a fresh token, set another
invisible cookie, redirected back, and looped -- until Webex's Common
Token Store hit its per-user limit and returned
error=tokenlimit_reached on the next callback.
Fix:
- httpOnly is now explicitly false with a comment explaining why: the
app's design has always relied on the client reading the id and
displayName cookies via document.cookie.
- sameSite tightened change from 'strict' to 'lax' so the cookie
reliably survives the webex.com -> /oauth -> /sendMessage.html
redirect chain across all browsers (some treat continuations of a
cross-site navigation as cross-site for strict cookies).
- Stop setting access_token, refresh_token, avatar, email, orgId on
the response. `req.cookies.*` grep confirms the server never reads
any of them, and the client uses id/displayName only. Removing the
token cookies also eliminates a would-be XSS foothold.
Existing broken sessions: setting a new cookie with the same name and
path replaces the old one regardless of httpOnly flag, so a single
completed OAuth after this deploy repairs the browser state.
Co-authored-by: Cursor <cursoragent@cursor.com>
The nav link race:
- Every page's header used `<a id="jobLink" href=''>` and the real
destination was only assigned later, after /info returned. An empty
href resolves to the current document URL, so clicking "Monitor Jobs"
on sendMessage before /info completed silently reloaded sendMessage.
- Fixed structurally: nav links now use static relative hrefs baked
into the HTML ("./sendMessage.html", "./monitorJobs.html"), so the
destination is correct the moment the DOM parses.
Shared UI:
- New html/css/app.css: design tokens (palette, radius, shadow, font),
sticky compact top header (bot avatar + label on the left, nav pills
in the middle, current user on the right, active-page highlight via
aria-current), card containers, form styling with focus rings,
DataTables theme overrides, status pills, modal, and responsive
breakpoints.
- New html/js/app.js: shared browser bootstrap. Parses appName from
the URL, redirects to OAuth if the id cookie is missing, fetches
/info once, populates the header, applies aria-current to the
active nav link, and invokes a per-page onReady callback with
{ info, appName }. Also exports getCookie, escapeHtml, and formatDate
helpers so each page stops shipping its own copy.
Per-page rewrites:
- sendMessage.html/.js: form now lives in a card, image preview only
shows when a file is attached, EasyMDE + VirtualSelect styled to
match the theme, submit is a primary button, confirmation modal
redesigned. All bootstrap code deleted (delegated to app.js).
- monitorJobs.html/.js: three cards (Running / Scheduled / Completed)
with themed DataTables. Completed table sorts by start time desc,
paginates, and searches; message column truncates HTML previews to
~120 chars. Empty-state text per table. `jobId` and running-row
"view" links go to jobDetail via safe relative URLs.
- jobDetail.html/.js: same shared header + card layout; summary grid,
message preview, and recipient table styled to match the new
palette.
Sanity checks:
- All 43 helper tests still pass.
- Server boots cleanly on port 3001.
- Curl of sendMessage/monitorJobs/jobDetail all return 200 with the
shared header markup.
- /CollabCentral/:app/css/app.css and /CollabCentral/:app/js/app.js
both serve 200 (shared static mount is per-bot as expected).
- No local href in any page is empty; every nav target resolves at
parse time.
- /info and requireBot 404 gate unchanged.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Move buildingKey, jobsForApp, getBotToken, isBotEnabled, getBotConfig,
isAuthorized, getOAuthRedirectUri, buildAuthUrl, cleanCompletedJobs,
and msToTime into lib/helpers.js as state-free functions that accept
config, botTokens, or env as parameters. COMPLETED_RETENTION_DAYS also
lives there so callers and tests share the constant.
- Replace the bodies in index.js with thin wrappers that pass the module-
level state into the pure helpers. Call sites and behavior are
unchanged; index.js shrinks by ~60 lines.
- Move the cleanCompletedJobs logging into the cron caller so the pure
helper returns a result object (jobs, removed, cutoff) that tests can
assert on without capturing stdout.
- Add test/helpers.test.js with 43 assertions across 10 suites covering
the enable/disable gating, per-bot draft isolation, authorization,
OAuth URL construction, retention filter (including endTime -> startTime
-> created fallback and the safety default for jobs missing a
timestamp), and the duration formatter.
- Wire `npm test` to `node --test test/*.test.js` (no new deps, uses the
built-in node:test runner) and document it in the README.
Smoke test confirms unchanged HTTP behavior for /info (known + unknown
bots), the requireBot 404 gate, and the 401 path on jobs/list/completed.
Co-authored-by: Cursor <cursoragent@cursor.com>
- Add README with local setup, add-a-bot walkthrough, Docker run
recipe, and a rundown of committed vs. runtime state.
- Wrap the cron schedules with an explicit timezone (default
America/New_York, override via CRON_TIMEZONE) so cadence is
independent of the host/container clock.
- Route saveServiceAccountToken through a try/catch so a disk hiccup
no longer bubbles up as an unhandled exception during token refresh,
and keep the in-memory copy usable even on write failure.
- Silence per-message queue.on('active') / on('completed') logs that
also registered a new listener on every job invocation (accumulating
on the shared queue over time).
- Fix logIt reference (undefined; would have thrown if
checkScheduledJobs ever rejected) and a broken JSON.stringify.result
debug log that always evaluated to undefined.
- Dockerfile: switch to node:20-slim, install from the lockfile via
npm ci --omit=dev, set NODE_ENV=production, and document that env
and volumes are supplied at runtime.
Co-authored-by: Cursor <cursoragent@cursor.com>
Extends the single-Novi codebase into a multi-bot mass-messenger where
each bot has its own token, avatar, label, and per-user authorization.
- Secrets moved out of config.json: per-bot tokens in gitignored
config/botTokens.json (with enabled flag), service-account OAuth in
gitignored config/token.json (rewritten by refresh cron), integration
and Google keys in .env.
- Single Webex integration handles OAuth for all bots via a per-app
redirect URI derived from OAUTH_CALLBACK_URL_TEMPLATE.
- New requireBot middleware and getBotConfig helper reject requests for
unknown or disabled bots at the /CollabCentral/:app boundary.
- New /info endpoint plus dynamic frontend loading (sendMessage,
monitorJobs) so pages self-describe per bot, including bot avatar
fetched from Webex /people/me at startup.
- Job draft state keyed by cookieId + appName so each bot has its own
building queue; job list/detail endpoints filter by appName so users
only see jobs from bots they are authorized on.
- New jobDetail page for a readable per-job view; completed jobs are
retained for 30 days by the cleanup cron.
- Completion adaptive cards use per-bot avatar and label.
- Miscellaneous fixes: off-by-two in the send loop, removed three dead
send/process variants, added defensive init for jobs.* on load,
dropped the deprecated crypto npm shim, and cleaned up stray logger
labels and typos.
Co-authored-by: Cursor <cursoragent@cursor.com>