{"owner":"sahat","repo":"hackathon-starter","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md","CLAUDE.md"],"skills":{"AGENTS.md":"# AGENTS.md\n\nThis file is for coding agents (and anyone doing a quick pass with one). It is a\n**dependency map**, not a tutorial — for narrative setup instructions, API key\nregistration links, and \"how it works\" walkthroughs, see [README.md](README.md)\nand [test/TESTING.md](test/TESTING.md).\n\nHackathon Starter is boilerplate. Most people who clone it will delete large\nparts of it — most `/api/*` and `/ai/*` examples, and most OAuth providers —\nand keep only what their project needs. That deletion is the single most\ncommon task an agent will be asked to do in this repo, and it's easy to do\nincompletely: routes, views, config blocks, env vars, npm packages, and tests\nare declared in five different files per feature, and nothing enforces that\nthey're removed together. The tables below exist so an agent (or a human)\ndoesn't have to reverse-engineer those relationships from scratch, feature by\nfeature, and doesn't leave orphaned code behind.\n\n**If you can't verify a claim below against the current code, don't trust it\n— re-derive it.** This file can drift out of date; the code is ground truth.\n\n## Core vs. optional, in one paragraph\n\nCore = auth, account management, session/security middleware, the base\nlayout. Removing or breaking this makes the app not work. Optional = every\nindividual OAuth provider and every `/api/*` and `/ai/*` example — each one\nis a self-contained demo of integrating some third-party service, included so\nyou can copy the pattern, then delete the ones you don't need. A few\nproviders are **cross-cutting** (used for both login and an API example) —\nthose are flagged explicitly below because deleting them breaks more than one\nfeature.\n\n## Core system (edit carefully, don't delete)\n\n| Area                                                               | Files                                                                                                                                                 |\n| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Entry point / route table                                          | `app.js` — every route in the app is registered here, in one file, on purpose (see README \"Why do you have all routes defined in app.js?\")            |\n| Session, CSRF, rate limiting, redirect-back-after-login            | `app.js` middleware stack (top of file)                                                                                                               |\n| Local auth + session + 2FA + account CRUD                          | `controllers/user.js`, `models/User.js`, `models/Session.js`                                                                                          |\n| Passport strategy registration, OAuth linking logic, token refresh | `config/passport.js`                                                                                                                                  |\n| OAuth token revocation on unlink/delete                            | `config/token-revocation.js`                                                                                                                          |\n| Passkey / WebAuthn login and registration                          | `controllers/webauthn.js`                                                                                                                             |\n| Flash messages                                                     | `config/flash.js`, `views/partials/flash.pug`                                                                                                         |\n| Request logging                                                    | `config/morgan.js`                                                                                                                                    |\n| Email sending                                                      | `config/nodemailer.js`                                                                                                                                |\n| Cache-busted static asset URLs                                     | `config/cacheBust.js`, used via `app.locals.getFileHash` in `views/layout.pug`                                                                        |\n| Base layout, nav, footer                                           | `views/layout.pug`, `views/partials/header.pug`, `views/partials/footer.pug`                                                                          |\n| Auth views                                                         | `views/account/login.pug`, `signup.pug`, `forgot.pug`, `reset.pug`, `two-factor.pug`, `totp-setup.pug`, `webauthn-login.pug`, `webauthn-register.pug` |\n| Account management view                                            | `views/account/profile.pug` — **also lists every OAuth provider's link/unlink block**; see OAuth table below                                          |\n| Contact form                                                       | `controllers/contact.js`, `views/contact.pug` — technically optional (SMTP-dependent), but small/self-contained enough that most projects keep it     |\n\n`views/account/login.pug` and `views/account/profile.pug` are **shared,\nhand-edited files**, not generated from a list — each OAuth provider has a\nhardcoded button/link block in one or both of these templates. Removing a\nprovider means deleting its specific block, not deleting the file.\n\n## OAuth providers\n\nEvery provider follows the same shape in `config/passport.js`: a\n`passport.use(...)` call under a `/** Sign in with X */`-style comment,\nregistered under a **provider key** (the string passed to `passport.use` /\n`passport.authorize`, and the field name stored on `User.tokens[].kind` and\n`User[providerKey]`). That provider key is what to grep for across the repo\nwhen removing a provider — it appears in `app.js` routes, `passport.js`,\n`config/token-revocation.js`, and both account views.\n\n| Provider key  | Env vars                                           | Sign-in button (`login.pug`)                | Link/unlink block (`profile.pug`) | Revocation configured | Also used for an API example?                                                                                                                                                                                                                                       |\n| ------------- | -------------------------------------------------- | ------------------------------------------- | --------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `google`      | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — Drive (`/api/google/drive`) and Sheets (`/api/google/sheets`) reuse the same Google OAuth token/scopes.** Removing Google login breaks those two API examples too; removing just those two examples doesn't affect login.                                   |\n| `facebook`    | `FACEBOOK_ID`, `FACEBOOK_SECRET`                   | yes                                         | yes                               | yes                   | **Yes — `/api/facebook`** reuses the Facebook login token. Don't confuse `FACEBOOK_ID`/`FACEBOOK_SECRET` (OAuth) with `FACEBOOK_PIXEL_ID` (analytics, unrelated, keep — see below).                                                                                 |\n| `twitch`      | `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — `/api/twitch`** reuses the Twitch login token.                                                                                                                                                                                                              |\n| `github`      | `GITHUB_ID`, `GITHUB_SECRET`                       | yes                                         | yes                               | yes                   | No — `/api/github` (GitHub search example) is unauthenticated and uses `@octokit/rest` independently; it is a _different feature_ that happens to share a name. Removing \"Sign in with GitHub\" does not require removing the `/api/github` example, and vice versa. |\n| `microsoft`   | `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`   | yes                                         | yes                               | no                    | No                                                                                                                                                                                                                                                                  |\n| `linkedin`    | `LINKEDIN_ID`, `LINKEDIN_SECRET`                   | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `x` (Twitter) | `X_KEY`, `X_SECRET`                                | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `discord`     | `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`       | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `tumblr`      | `TUMBLR_KEY`, `TUMBLR_SECRET`                      | no (link-only, from `/api/tumblr` view)     | yes                               | no                    | This _is_ the API example — `/api/tumblr` (`apiController.getTumblr`) is the only reason this provider exists.                                                                                                                                                      |\n| `steam`       | `STEAM_KEY`                                        | no (link-only, from `/api/steam` view)      | yes                               | no                    | Same pattern — `/api/steam` is the feature.                                                                                                                                                                                                                         |\n| `trakt`       | `TRAKT_ID`, `TRAKT_SECRET`                         | no (link-only, from `/api/trakt` view)      | yes                               | yes                   | Same pattern — `/api/trakt` is the feature.                                                                                                                                                                                                                         |\n| `quickbooks`  | `QUICKBOOKS_CLIENT_ID`, `QUICKBOOKS_CLIENT_SECRET` | no (link-only, from `/api/quickbooks` view) | yes                               | yes                   | Same pattern — `/api/quickbooks` is the feature.                                                                                                                                                                                                                    |\n\nNotes:\n\n- \"Link/unlink block\" for all twelve providers lives in `views/account/profile.pug`; only eight of them (the ones marked \"yes\" in the sign-in column) also get a button in `views/account/login.pug`.\n- `passport-oauth2-refresh` (`refresh.use(...)` calls in `passport.js`) is only wired up for providers with a refresh token: `google`, `linkedin`, `microsoft`, `twitch`, `quickbooks`, `trakt`, `discord`. Removing one of those providers means removing its `refresh.use(...)` call too, not just `passport.use(...)`.\n- `config/token-revocation.js` is driven entirely by `passport.js`'s exported `providerRevocationConfig` object — removing a provider's entry there is enough; there's no separate per-provider file.\n- `/auth/failure` (`app.js`) is a single shared failure-redirect route for _all_ providers — never remove it while any provider remains.\n\n## API examples (`/api/*`)\n\nEvery route below is registered in `app.js` under `/** API examples routes. */`,\nimplemented in `controllers/api.js`, and rendered from `views/api/<name>.pug`\nunless noted. Feature tokens match the e2e test/fixture naming convention\n(`test/e2e/<token>.e2e.test.js`, `test/fixtures/*<token>*` — fixtures are\nnamed after the sanitized outbound request URL, so the token match is\nsubstring, not exact).\n\n| Feature                          | Route(s)                                                   | Controller export                                     | Env vars                                           | npm package(s)                       | e2e test                                                                                                     |\n| -------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| API browser index                | `/api`                                                     | `getApi`                                              | —                                                  | —                                    | —                                                                                                            |\n| Last.fm                          | `/api/lastfm`                                              | `getLastfm`                                           | `LASTFM_KEY`, `LASTFM_SECRET`                      | `lastfm`                             | `test/e2e-nokey/lastfm.e2e.test.js`                                                                          |\n| New York Times                   | `/api/nyt`                                                 | `getNewYorkTimes`                                     | `NYT_KEY`                                          | —                                    | `test/e2e/nyt.e2e.test.js`                                                                                   |\n| Stripe                           | `/api/stripe` (GET/POST)                                   | `getStripe`, `postStripe`                             | `STRIPE_SKEY`, `STRIPE_PKEY`                       | `stripe`                             | none                                                                                                         |\n| Web scraping                     | `/api/scraping`                                            | `getScraping`                                         | —                                                  | `cheerio`                            | `test/e2e-nokey/scraping.e2e.test.js`                                                                        |\n| Twilio                           | `/api/twilio` (GET/POST)                                   | `getTwilio`, `postTwilio`                             | `TWILIO_SID`, `TWILIO_TOKEN`, `TWILIO_FROM_NUMBER` | `twilio`                             | `test/e2e/twilio.e2e.test.js`                                                                                |\n| Foursquare                       | `/api/foursquare`                                          | `getFoursquare`                                       | `FOURSQUARE_APIKEY`                                | —                                    | `test/e2e/foursquare.e2e.test.js`                                                                            |\n| PayPal                           | `/api/paypal`, `/api/paypal/success`, `/api/paypal/cancel` | `getPayPal`, `getPayPalSuccess`, `getPayPalCancel`    | `PAYPAL_ID`, `PAYPAL_SECRET`                       | —                                    | none                                                                                                         |\n| File upload                      | `/api/upload` (GET/POST)                                   | `getFileUpload`, `postFileUpload`, `uploadMiddleware` | —                                                  | `multer`                             | `test/e2e-nokey/upload.e2e.test.js`                                                                          |\n| HERE Maps                        | `/api/here-maps`                                           | `getHereMaps`                                         | `HERE_API_KEY`                                     | —                                    | `test/e2e/here-maps.e2e.test.js`                                                                             |\n| Google Maps                      | `/api/google-maps`                                         | `getGoogleMaps`                                       | `GOOGLE_MAP_API_KEY`                               | —                                    | `test/e2e/google-maps.e2e.test.js`                                                                           |\n| Chart (Alpha Vantage + Chart.js) | `/api/chart`                                               | `getChart`                                            | `ALPHA_VANTAGE_KEY`                                | `chart.js`                           | `test/e2e/chart.e2e.test.js` — also remove the `/js/lib/chart.umd.min.js` entry in `app.js`'s `libFiles` map |\n| Lob                              | `/api/lob`                                                 | `getLob`                                              | `LOB_KEY`                                          | `@lob/lob-typescript-sdk`            | `test/e2e/lob.e2e.test.js`                                                                                   |\n| Trakt                            | `/api/trakt`                                               | `getTrakt`                                            | see OAuth table (`trakt`)                          | —                                    | `test/e2e/trakt.e2e.test.js`                                                                                 |\n| PubChem                          | `/api/pubchem`                                             | `getPubChem`                                          | —                                                  | —                                    | `test/e2e-nokey/pubchem.e2e.test.js`                                                                         |\n| Wikipedia                        | `/api/wikipedia`                                           | `getWikipedia`                                        | —                                                  | —                                    | `test/e2e-nokey/wikipedia.e2e.test.js`                                                                       |\n| GIPHY                            | `/api/giphy`                                               | `getGiphy`                                            | `GIPHY_API_KEY`                                    | —                                    | `test/e2e/giphy.e2e.test.js`                                                                                 |\n| GitHub search                    | `/api/github`                                              | `getGithub`                                           | `GITHUB_ID`, `GITHUB_SECRET`*                      | `@octokit/rest`                      | `test/e2e-nokey/github-api.e2e.test.js`                                                                      |\n| Google Drive                     | `/api/google/drive`                                        | `getGoogleDrive`                                      | see OAuth table (`google`)                         | `@googleapis/drive`                  | none (needs live user OAuth session)                                                                         |\n| Google Sheets                    | `/api/google/sheets`                                       | `getGoogleSheets`                                     | see OAuth table (`google`)                         | `@googleapis/sheets`                 | none                                                                                                         |\n| Tumblr                           | `/api/tumblr`                                              | `getTumblr`                                           | see OAuth table (`tumblr`)                         | —                                    | none                                                                                                         |\n| Facebook                         | `/api/facebook`                                            | `getFacebook`                                         | see OAuth table (`facebook`)                       | —                                    | none                                                                                                         |\n| Twitch                           | `/api/twitch`                                              | `getTwitch`                                           | see OAuth table (`twitch`)                         | `twitch-passport` (also OAuth)       | none                                                                                                         |\n| Steam                            | `/api/steam`                                               | `getSteam`                                            | see OAuth table (`steam`)                          | `passport-steam-openid` (also OAuth) | none                                                                                                         |\n| QuickBooks                       | `/api/quickbooks`                                          | `getQuickbooks`                                       | see OAuth table (`quickbooks`)                     | —                                    | none                                                                                                         |\n\n\\* `/api/github` (unauthenticated search) and \"Sign in with GitHub\" both read\n`GITHUB_ID`/`GITHUB_SECRET` from env, but for different purposes — verify\nwhich one(s) a given project still needs before removing these vars.\n\n## AI examples (`/ai/*`)\n\nRegistered in `app.js` under `/** AI Integrations and Boilerplate example\nroutes. */`, implemented across `controllers/ai.js` and\n`controllers/ai-agent.js`, views under `views/ai/`.\n\n| Feature                                    | Route(s)                                                  | Controller                                                       | Env vars                                                                                               | npm package(s)                                                                                                  | e2e test                                                         |\n| ------------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| AI examples index                          | `/ai`                                                     | `ai.js: getAi`                                                   | —                                                                                                      | —                                                                                                               | —                                                                |\n| LLM text classifier                        | `/ai/llm-classifier` (GET/POST)                           | `ai.js: getLLMClassifier`, `postLLMClassifier`                   | `GROQ_API_KEY`, `GROQ_MODEL_PROMPT_GUARD`                                                              | `@langchain/groq`, `langchain`                                                                                  | `test/e2e/llm-classifier.e2e.test.js`                            |\n| LLM camera / image                         | `/ai/llm-camera` (GET/POST)                               | `ai.js: getLLMCamera`, `postLLMCamera`, `imageUploadMiddleware`  | `GROQ_API_KEY`, `GROQ_VISION`                                                                          | `@langchain/groq`, `multer`                                                                                     | none                                                             |\n| RAG (retrieval-augmented generation)       | `/ai/rag`, `/ai/rag/ingest`, `/ai/rag/ask`                | `ai.js: getRag`, `postRagIngest`, `postRagAsk`                   | `HUGGINGFACE_KEY`, `HUGGINGFACE_EMBEDDING_MODEL`, `HUGGINGFACE_PROVIDER`, `GROQ_API_KEY`, `GROQ_MODEL` | `@langchain/textsplitters`, `@langchain/mongodb`, `@huggingface/inference`, `pdfjs-dist`, `keyv`, `@keyv/mongo` | `test/e2e/rag.e2e.test.js`                                       |\n| AI Agent (ReAct, tool calling, guardrails) | `/ai/ai-agent`, `/ai/ai-agent/chat`, `/ai/ai-agent/reset` | `ai-agent.js: getAIAgent`, `postAIAgentChat`, `postAIAgentReset` | `GROQ_API_KEY`, `GROQ_MODEL`                                                                           | `@langchain/groq`, `@langchain/core`, `langchain`, `@langchain/langgraph-checkpoint-mongodb`, `zod`             | none (see `test/*.test.js` for unit coverage of session cleanup) |\n\nNotes:\n\n- `ai-agent.js` also exports `cleanupOrphanedTempSessions`, called once from\n  `app.js` on Mongo connection open, and `deleteUserAIAgentData`, called from\n  `controllers/user.js` on account deletion — **both are cross-links from\n  core account code into this \"optional\" feature.** If you remove the AI\n  Agent, remove those two call sites too, not just the routes/controller.\n- `OPENAI_API_KEY` is present in `.env.example` but **not referenced by any\n  current code path** — don't assume it maps to a feature; verify with grep\n  before acting on it either way.\n- `MongoDBAtlasVectorSearch`/`MongoDBAtlasSemanticCache` (RAG) and\n  `MongoDBSaver` (AI Agent) both write to MongoDB collections beyond the\n  `User`/`Session` models — check `controllers/ai.js` / `controllers/ai-agent.js`\n  for the exact collection names if you need to also drop data, not just code.\n\n## Removing a feature: checklist\n\nFor any feature (an OAuth provider, an `/api/*` example, an `/ai/*` example):\n\n1. Route(s) in `app.js`.\n2. Controller export(s) in `controllers/*.js` (delete the function; delete the file only if nothing else in it is still used).\n3. View(s) in `views/`.\n4. Any hardcoded block in a _shared_ view — check `views/account/login.pug`, `views/account/profile.pug`, `views/partials/header.pug` by grepping the provider key or feature name; don't assume there's a clean per-feature file to delete.\n5. `config/passport.js` — `passport.use(...)`, matching `refresh.use(...)` if present, and its entry in `providerRevocationConfig` at the bottom of the file.\n6. Env vars in `.env.example` and `test/.env.test` — **verify with grep that nothing else references the var before removing it** (see the `GITHUB_ID`/`OPENAI_API_KEY` notes above for why this matters).\n7. npm packages — remove with `npm uninstall <package>` (don't hand-edit `package.json`/`package-lock.json`), and only after confirming via grep that no other retained feature imports the same package (several packages, e.g. `passport-steam-openid`, `twitch-passport`, do double duty as both the OAuth strategy and the API example).\n8. Static lib mapping in `app.js`'s `libFiles` map, if the feature has a dedicated client-side library entry (currently only `chart.js`, for the Chart/Alpha Vantage example).\n9. Tests: unit test file if one exists (`test/*.test.js`), the whole `test/e2e/<feature>.e2e.test.js` or `test/e2e-nokey/<feature>.e2e.test.js` file, and its fixtures — fixtures are named after the _outbound request URL_, so match by substring (e.g. everything containing `trakt.tv`), not by an exact feature-name filename.\n10. Run the checks in the next section before considering the removal done — an incomplete removal usually shows up as a lint error (unused import), a failing test (missing fixture / removed route still referenced), or a broken view (undefined local).\n\n## Commands\n\n- `npm run lint` — ESLint + Prettier, should pass with zero warnings.\n- `npm test` — Mocha unit/integration tests (`test/*.test.js`), runs against an in-memory MongoDB.\n- `npm run test:e2e:replay` — Playwright e2e tests against recorded fixtures (no live API keys needed).\n- `npm run test:e2e:custom -- --project=chromium-nokey-live` — Playwright e2e tests that hit live APIs not requiring keys.\n- `npm run test:e2e:live` — full Playwright e2e suite against live third-party APIs (needs real keys in `.env`/`test/.env.test`).\n- `npm start` — builds SCSS then runs the app.\n\n## Testing conventions\n\nSee [test/TESTING.md](test/TESTING.md) for the full picture. Short version:\n`test/*.test.js` are **core** tests (auth, security, session) and must always\npass — they're what protects you from breaking the framework while\ncustomizing it. `test/e2e/` and `test/e2e-nokey/` are **per-integration**\ntests; each one only matters if you're keeping that integration, and should\nbe deleted alongside it.\n","CLAUDE.md":"# CLAUDE.md\n\nSee [AGENTS.md](AGENTS.md) for project instructions. This file exists only so\nthat tools which don't yet read `AGENTS.md` natively pick up the same\nguidance.\n"},"files":{"AGENTS.md":"# AGENTS.md\n\nThis file is for coding agents (and anyone doing a quick pass with one). It is a\n**dependency map**, not a tutorial — for narrative setup instructions, API key\nregistration links, and \"how it works\" walkthroughs, see [README.md](README.md)\nand [test/TESTING.md](test/TESTING.md).\n\nHackathon Starter is boilerplate. Most people who clone it will delete large\nparts of it — most `/api/*` and `/ai/*` examples, and most OAuth providers —\nand keep only what their project needs. That deletion is the single most\ncommon task an agent will be asked to do in this repo, and it's easy to do\nincompletely: routes, views, config blocks, env vars, npm packages, and tests\nare declared in five different files per feature, and nothing enforces that\nthey're removed together. The tables below exist so an agent (or a human)\ndoesn't have to reverse-engineer those relationships from scratch, feature by\nfeature, and doesn't leave orphaned code behind.\n\n**If you can't verify a claim below against the current code, don't trust it\n— re-derive it.** This file can drift out of date; the code is ground truth.\n\n## Core vs. optional, in one paragraph\n\nCore = auth, account management, session/security middleware, the base\nlayout. Removing or breaking this makes the app not work. Optional = every\nindividual OAuth provider and every `/api/*` and `/ai/*` example — each one\nis a self-contained demo of integrating some third-party service, included so\nyou can copy the pattern, then delete the ones you don't need. A few\nproviders are **cross-cutting** (used for both login and an API example) —\nthose are flagged explicitly below because deleting them breaks more than one\nfeature.\n\n## Core system (edit carefully, don't delete)\n\n| Area                                                               | Files                                                                                                                                                 |\n| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Entry point / route table                                          | `app.js` — every route in the app is registered here, in one file, on purpose (see README \"Why do you have all routes defined in app.js?\")            |\n| Session, CSRF, rate limiting, redirect-back-after-login            | `app.js` middleware stack (top of file)                                                                                                               |\n| Local auth + session + 2FA + account CRUD                          | `controllers/user.js`, `models/User.js`, `models/Session.js`                                                                                          |\n| Passport strategy registration, OAuth linking logic, token refresh | `config/passport.js`                                                                                                                                  |\n| OAuth token revocation on unlink/delete                            | `config/token-revocation.js`                                                                                                                          |\n| Passkey / WebAuthn login and registration                          | `controllers/webauthn.js`                                                                                                                             |\n| Flash messages                                                     | `config/flash.js`, `views/partials/flash.pug`                                                                                                         |\n| Request logging                                                    | `config/morgan.js`                                                                                                                                    |\n| Email sending                                                      | `config/nodemailer.js`                                                                                                                                |\n| Cache-busted static asset URLs                                     | `config/cacheBust.js`, used via `app.locals.getFileHash` in `views/layout.pug`                                                                        |\n| Base layout, nav, footer                                           | `views/layout.pug`, `views/partials/header.pug`, `views/partials/footer.pug`                                                                          |\n| Auth views                                                         | `views/account/login.pug`, `signup.pug`, `forgot.pug`, `reset.pug`, `two-factor.pug`, `totp-setup.pug`, `webauthn-login.pug`, `webauthn-register.pug` |\n| Account management view                                            | `views/account/profile.pug` — **also lists every OAuth provider's link/unlink block**; see OAuth table below                                          |\n| Contact form                                                       | `controllers/contact.js`, `views/contact.pug` — technically optional (SMTP-dependent), but small/self-contained enough that most projects keep it     |\n\n`views/account/login.pug` and `views/account/profile.pug` are **shared,\nhand-edited files**, not generated from a list — each OAuth provider has a\nhardcoded button/link block in one or both of these templates. Removing a\nprovider means deleting its specific block, not deleting the file.\n\n## OAuth providers\n\nEvery provider follows the same shape in `config/passport.js`: a\n`passport.use(...)` call under a `/** Sign in with X */`-style comment,\nregistered under a **provider key** (the string passed to `passport.use` /\n`passport.authorize`, and the field name stored on `User.tokens[].kind` and\n`User[providerKey]`). That provider key is what to grep for across the repo\nwhen removing a provider — it appears in `app.js` routes, `passport.js`,\n`config/token-revocation.js`, and both account views.\n\n| Provider key  | Env vars                                           | Sign-in button (`login.pug`)                | Link/unlink block (`profile.pug`) | Revocation configured | Also used for an API example?                                                                                                                                                                                                                                       |\n| ------------- | -------------------------------------------------- | ------------------------------------------- | --------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `google`      | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — Drive (`/api/google/drive`) and Sheets (`/api/google/sheets`) reuse the same Google OAuth token/scopes.** Removing Google login breaks those two API examples too; removing just those two examples doesn't affect login.                                   |\n| `facebook`    | `FACEBOOK_ID`, `FACEBOOK_SECRET`                   | yes                                         | yes                               | yes                   | **Yes — `/api/facebook`** reuses the Facebook login token. Don't confuse `FACEBOOK_ID`/`FACEBOOK_SECRET` (OAuth) with `FACEBOOK_PIXEL_ID` (analytics, unrelated, keep — see below).                                                                                 |\n| `twitch`      | `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — `/api/twitch`** reuses the Twitch login token.                                                                                                                                                                                                              |\n| `github`      | `GITHUB_ID`, `GITHUB_SECRET`                       | yes                                         | yes                               | yes                   | No — `/api/github` (GitHub search example) is unauthenticated and uses `@octokit/rest` independently; it is a _different feature_ that happens to share a name. Removing \"Sign in with GitHub\" does not require removing the `/api/github` example, and vice versa. |\n| `microsoft`   | `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`   | yes                                         | yes                               | no                    | No                                                                                                                                                                                                                                                                  |\n| `linkedin`    | `LINKEDIN_ID`, `LINKEDIN_SECRET`                   | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `x` (Twitter) | `X_KEY`, `X_SECRET`                                | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `discord`     | `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`       | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `tumblr`      | `TUMBLR_KEY`, `TUMBLR_SECRET`                      | no (link-only, from `/api/tumblr` view)     | yes                               | no                    | This _is_ the API example — `/api/tumblr` (`apiController.getTumblr`) is the only reason this provider exists.                                                                                                                                                      |\n| `steam`       | `STEAM_KEY`                                        | no (link-only, from `/api/steam` view)      | yes                               | no                    | Same pattern — `/api/steam` is the feature.                                                                                                                                                                                                                         |\n| `trakt`       | `TRAKT_ID`, `TRAKT_SECRET`                         | no (link-only, from `/api/trakt` view)      | yes                               | yes                   | Same pattern — `/api/trakt` is the feature.                                                                                                                                                                                                                         |\n| `quickbooks`  | `QUICKBOOKS_CLIENT_ID`, `QUICKBOOKS_CLIENT_SECRET` | no (link-only, from `/api/quickbooks` view) | yes                               | yes                   | Same pattern — `/api/quickbooks` is the feature.                                                                                                                                                                                                                    |\n\nNotes:\n\n- \"Link/unlink block\" for all twelve providers lives in `views/account/profile.pug`; only eight of them (the ones marked \"yes\" in the sign-in column) also get a button in `views/account/login.pug`.\n- `passport-oauth2-refresh` (`refresh.use(...)` calls in `passport.js`) is only wired up for providers with a refresh token: `google`, `linkedin`, `microsoft`, `twitch`, `quickbooks`, `trakt`, `discord`. Removing one of those providers means removing its `refresh.use(...)` call too, not just `passport.use(...)`.\n- `config/token-revocation.js` is driven entirely by `passport.js`'s exported `providerRevocationConfig` object — removing a provider's entry there is enough; there's no separate per-provider file.\n- `/auth/failure` (`app.js`) is a single shared failure-redirect route for _all_ providers — never remove it while any provider remains.\n\n## API examples (`/api/*`)\n\nEvery route below is registered in `app.js` under `/** API examples routes. */`,\nimplemented in `controllers/api.js`, and rendered from `views/api/<name>.pug`\nunless noted. Feature tokens match the e2e test/fixture naming convention\n(`test/e2e/<token>.e2e.test.js`, `test/fixtures/*<token>*` — fixtures are\nnamed after the sanitized outbound request URL, so the token match is\nsubstring, not exact).\n\n| Feature                          | Route(s)                                                   | Controller export                                     | Env vars                                           | npm package(s)                       | e2e test                                                                                                     |\n| -------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| API browser index                | `/api`                                                     | `getApi`                                              | —                                                  | —                                    | —                                                                                                            |\n| Last.fm                          | `/api/lastfm`                                              | `getLastfm`                                           | `LASTFM_KEY`, `LASTFM_SECRET`                      | `lastfm`                             | `test/e2e-nokey/lastfm.e2e.test.js`                                                                          |\n| New York Times                   | `/api/nyt`                                                 | `getNewYorkTimes`                                     | `NYT_KEY`                                          | —                                    | `test/e2e/nyt.e2e.test.js`                                                                                   |\n| Stripe                           | `/api/stripe` (GET/POST)                                   | `getStripe`, `postStripe`                             | `STRIPE_SKEY`, `STRIPE_PKEY`                       | `stripe`                             | none                                                                                                         |\n| Web scraping                     | `/api/scraping`                                            | `getScraping`                                         | —                                                  | `cheerio`                            | `test/e2e-nokey/scraping.e2e.test.js`                                                                        |\n| Twilio                           | `/api/twilio` (GET/POST)                                   | `getTwilio`, `postTwilio`                             | `TWILIO_SID`, `TWILIO_TOKEN`, `TWILIO_FROM_NUMBER` | `twilio`                             | `test/e2e/twilio.e2e.test.js`                                                                                |\n| Foursquare                       | `/api/foursquare`                                          | `getFoursquare`                                       | `FOURSQUARE_APIKEY`                                | —                                    | `test/e2e/foursquare.e2e.test.js`                                                                            |\n| PayPal                           | `/api/paypal`, `/api/paypal/success`, `/api/paypal/cancel` | `getPayPal`, `getPayPalSuccess`, `getPayPalCancel`    | `PAYPAL_ID`, `PAYPAL_SECRET`                       | —                                    | none                                                                                                         |\n| File upload                      | `/api/upload` (GET/POST)                                   | `getFileUpload`, `postFileUpload`, `uploadMiddleware` | —                                                  | `multer`                             | `test/e2e-nokey/upload.e2e.test.js`                                                                          |\n| HERE Maps                        | `/api/here-maps`                                           | `getHereMaps`                                         | `HERE_API_KEY`                                     | —                                    | `test/e2e/here-maps.e2e.test.js`                                                                             |\n| Google Maps                      | `/api/google-maps`                                         | `getGoogleMaps`                                       | `GOOGLE_MAP_API_KEY`                               | —                                    | `test/e2e/google-maps.e2e.test.js`                                                                           |\n| Chart (Alpha Vantage + Chart.js) | `/api/chart`                                               | `getChart`                                            | `ALPHA_VANTAGE_KEY`                                | `chart.js`                           | `test/e2e/chart.e2e.test.js` — also remove the `/js/lib/chart.umd.min.js` entry in `app.js`'s `libFiles` map |\n| Lob                              | `/api/lob`                                                 | `getLob`                                              | `LOB_KEY`                                          | `@lob/lob-typescript-sdk`            | `test/e2e/lob.e2e.test.js`                                                                                   |\n| Trakt                            | `/api/trakt`                                               | `getTrakt`                                            | see OAuth table (`trakt`)                          | —                                    | `test/e2e/trakt.e2e.test.js`                                                                                 |\n| PubChem                          | `/api/pubchem`                                             | `getPubChem`                                          | —                                                  | —                                    | `test/e2e-nokey/pubchem.e2e.test.js`                                                                         |\n| Wikipedia                        | `/api/wikipedia`                                           | `getWikipedia`                                        | —                                                  | —                                    | `test/e2e-nokey/wikipedia.e2e.test.js`                                                                       |\n| GIPHY                            | `/api/giphy`                                               | `getGiphy`                                            | `GIPHY_API_KEY`                                    | —                                    | `test/e2e/giphy.e2e.test.js`                                                                                 |\n| GitHub search                    | `/api/github`                                              | `getGithub`                                           | `GITHUB_ID`, `GITHUB_SECRET`*                      | `@octokit/rest`                      | `test/e2e-nokey/github-api.e2e.test.js`                                                                      |\n| Google Drive                     | `/api/google/drive`                                        | `getGoogleDrive`                                      | see OAuth table (`google`)                         | `@googleapis/drive`                  | none (needs live user OAuth session)                                                                         |\n| Google Sheets                    | `/api/google/sheets`                                       | `getGoogleSheets`                                     | see OAuth table (`google`)                         | `@googleapis/sheets`                 | none                                                                                                         |\n| Tumblr                           | `/api/tumblr`                                              | `getTumblr`                                           | see OAuth table (`tumblr`)                         | —                                    | none                                                                                                         |\n| Facebook                         | `/api/facebook`                                            | `getFacebook`                                         | see OAuth table (`facebook`)                       | —                                    | none                                                                                                         |\n| Twitch                           | `/api/twitch`                                              | `getTwitch`                                           | see OAuth table (`twitch`)                         | `twitch-passport` (also OAuth)       | none                                                                                                         |\n| Steam                            | `/api/steam`                                               | `getSteam`                                            | see OAuth table (`steam`)                          | `passport-steam-openid` (also OAuth) | none                                                                                                         |\n| QuickBooks                       | `/api/quickbooks`                                          | `getQuickbooks`                                       | see OAuth table (`quickbooks`)                     | —                                    | none                                                                                                         |\n\n\\* `/api/github` (unauthenticated search) and \"Sign in with GitHub\" both read\n`GITHUB_ID`/`GITHUB_SECRET` from env, but for different purposes — verify\nwhich one(s) a given project still needs before removing these vars.\n\n## AI examples (`/ai/*`)\n\nRegistered in `app.js` under `/** AI Integrations and Boilerplate example\nroutes. */`, implemented across `controllers/ai.js` and\n`controllers/ai-agent.js`, views under `views/ai/`.\n\n| Feature                                    | Route(s)                                                  | Controller                                                       | Env vars                                                                                               | npm package(s)                                                                                                  | e2e test                                                         |\n| ------------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| AI examples index                          | `/ai`                                                     | `ai.js: getAi`                                                   | —                                                                                                      | —                                                                                                               | —                                                                |\n| LLM text classifier                        | `/ai/llm-classifier` (GET/POST)                           | `ai.js: getLLMClassifier`, `postLLMClassifier`                   | `GROQ_API_KEY`, `GROQ_MODEL_PROMPT_GUARD`                                                              | `@langchain/groq`, `langchain`                                                                                  | `test/e2e/llm-classifier.e2e.test.js`                            |\n| LLM camera / image                         | `/ai/llm-camera` (GET/POST)                               | `ai.js: getLLMCamera`, `postLLMCamera`, `imageUploadMiddleware`  | `GROQ_API_KEY`, `GROQ_VISION`                                                                          | `@langchain/groq`, `multer`                                                                                     | none                                                             |\n| RAG (retrieval-augmented generation)       | `/ai/rag`, `/ai/rag/ingest`, `/ai/rag/ask`                | `ai.js: getRag`, `postRagIngest`, `postRagAsk`                   | `HUGGINGFACE_KEY`, `HUGGINGFACE_EMBEDDING_MODEL`, `HUGGINGFACE_PROVIDER`, `GROQ_API_KEY`, `GROQ_MODEL` | `@langchain/textsplitters`, `@langchain/mongodb`, `@huggingface/inference`, `pdfjs-dist`, `keyv`, `@keyv/mongo` | `test/e2e/rag.e2e.test.js`                                       |\n| AI Agent (ReAct, tool calling, guardrails) | `/ai/ai-agent`, `/ai/ai-agent/chat`, `/ai/ai-agent/reset` | `ai-agent.js: getAIAgent`, `postAIAgentChat`, `postAIAgentReset` | `GROQ_API_KEY`, `GROQ_MODEL`                                                                           | `@langchain/groq`, `@langchain/core`, `langchain`, `@langchain/langgraph-checkpoint-mongodb`, `zod`             | none (see `test/*.test.js` for unit coverage of session cleanup) |\n\nNotes:\n\n- `ai-agent.js` also exports `cleanupOrphanedTempSessions`, called once from\n  `app.js` on Mongo connection open, and `deleteUserAIAgentData`, called from\n  `controllers/user.js` on account deletion — **both are cross-links from\n  core account code into this \"optional\" feature.** If you remove the AI\n  Agent, remove those two call sites too, not just the routes/controller.\n- `OPENAI_API_KEY` is present in `.env.example` but **not referenced by any\n  current code path** — don't assume it maps to a feature; verify with grep\n  before acting on it either way.\n- `MongoDBAtlasVectorSearch`/`MongoDBAtlasSemanticCache` (RAG) and\n  `MongoDBSaver` (AI Agent) both write to MongoDB collections beyond the\n  `User`/`Session` models — check `controllers/ai.js` / `controllers/ai-agent.js`\n  for the exact collection names if you need to also drop data, not just code.\n\n## Removing a feature: checklist\n\nFor any feature (an OAuth provider, an `/api/*` example, an `/ai/*` example):\n\n1. Route(s) in `app.js`.\n2. Controller export(s) in `controllers/*.js` (delete the function; delete the file only if nothing else in it is still used).\n3. View(s) in `views/`.\n4. Any hardcoded block in a _shared_ view — check `views/account/login.pug`, `views/account/profile.pug`, `views/partials/header.pug` by grepping the provider key or feature name; don't assume there's a clean per-feature file to delete.\n5. `config/passport.js` — `passport.use(...)`, matching `refresh.use(...)` if present, and its entry in `providerRevocationConfig` at the bottom of the file.\n6. Env vars in `.env.example` and `test/.env.test` — **verify with grep that nothing else references the var before removing it** (see the `GITHUB_ID`/`OPENAI_API_KEY` notes above for why this matters).\n7. npm packages — remove with `npm uninstall <package>` (don't hand-edit `package.json`/`package-lock.json`), and only after confirming via grep that no other retained feature imports the same package (several packages, e.g. `passport-steam-openid`, `twitch-passport`, do double duty as both the OAuth strategy and the API example).\n8. Static lib mapping in `app.js`'s `libFiles` map, if the feature has a dedicated client-side library entry (currently only `chart.js`, for the Chart/Alpha Vantage example).\n9. Tests: unit test file if one exists (`test/*.test.js`), the whole `test/e2e/<feature>.e2e.test.js` or `test/e2e-nokey/<feature>.e2e.test.js` file, and its fixtures — fixtures are named after the _outbound request URL_, so match by substring (e.g. everything containing `trakt.tv`), not by an exact feature-name filename.\n10. Run the checks in the next section before considering the removal done — an incomplete removal usually shows up as a lint error (unused import), a failing test (missing fixture / removed route still referenced), or a broken view (undefined local).\n\n## Commands\n\n- `npm run lint` — ESLint + Prettier, should pass with zero warnings.\n- `npm test` — Mocha unit/integration tests (`test/*.test.js`), runs against an in-memory MongoDB.\n- `npm run test:e2e:replay` — Playwright e2e tests against recorded fixtures (no live API keys needed).\n- `npm run test:e2e:custom -- --project=chromium-nokey-live` — Playwright e2e tests that hit live APIs not requiring keys.\n- `npm run test:e2e:live` — full Playwright e2e suite against live third-party APIs (needs real keys in `.env`/`test/.env.test`).\n- `npm start` — builds SCSS then runs the app.\n\n## Testing conventions\n\nSee [test/TESTING.md](test/TESTING.md) for the full picture. Short version:\n`test/*.test.js` are **core** tests (auth, security, session) and must always\npass — they're what protects you from breaking the framework while\ncustomizing it. `test/e2e/` and `test/e2e-nokey/` are **per-integration**\ntests; each one only matters if you're keeping that integration, and should\nbe deleted alongside it.\n","CLAUDE.md":"# CLAUDE.md\n\nSee [AGENTS.md](AGENTS.md) for project instructions. This file exists only so\nthat tools which don't yet read `AGENTS.md` natively pick up the same\nguidance.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"# AGENTS.md\n\nThis file is for coding agents (and anyone doing a quick pass with one). It is a\n**dependency map**, not a tutorial — for narrative setup instructions, API key\nregistration links, and \"how it works\" walkthroughs, see [README.md](README.md)\nand [test/TESTING.md](test/TESTING.md).\n\nHackathon Starter is boilerplate. Most people who clone it will delete large\nparts of it — most `/api/*` and `/ai/*` examples, and most OAuth providers —\nand keep only what their project needs. That deletion is the single most\ncommon task an agent will be asked to do in this repo, and it's easy to do\nincompletely: routes, views, config blocks, env vars, npm packages, and tests\nare declared in five different files per feature, and nothing enforces that\nthey're removed together. The tables below exist so an agent (or a human)\ndoesn't have to reverse-engineer those relationships from scratch, feature by\nfeature, and doesn't leave orphaned code behind.\n\n**If you can't verify a claim below against the current code, don't trust it\n— re-derive it.** This file can drift out of date; the code is ground truth.\n\n## Core vs. optional, in one paragraph\n\nCore = auth, account management, session/security middleware, the base\nlayout. Removing or breaking this makes the app not work. Optional = every\nindividual OAuth provider and every `/api/*` and `/ai/*` example — each one\nis a self-contained demo of integrating some third-party service, included so\nyou can copy the pattern, then delete the ones you don't need. A few\nproviders are **cross-cutting** (used for both login and an API example) —\nthose are flagged explicitly below because deleting them breaks more than one\nfeature.\n\n## Core system (edit carefully, don't delete)\n\n| Area                                                               | Files                                                                                                                                                 |\n| ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Entry point / route table                                          | `app.js` — every route in the app is registered here, in one file, on purpose (see README \"Why do you have all routes defined in app.js?\")            |\n| Session, CSRF, rate limiting, redirect-back-after-login            | `app.js` middleware stack (top of file)                                                                                                               |\n| Local auth + session + 2FA + account CRUD                          | `controllers/user.js`, `models/User.js`, `models/Session.js`                                                                                          |\n| Passport strategy registration, OAuth linking logic, token refresh | `config/passport.js`                                                                                                                                  |\n| OAuth token revocation on unlink/delete                            | `config/token-revocation.js`                                                                                                                          |\n| Passkey / WebAuthn login and registration                          | `controllers/webauthn.js`                                                                                                                             |\n| Flash messages                                                     | `config/flash.js`, `views/partials/flash.pug`                                                                                                         |\n| Request logging                                                    | `config/morgan.js`                                                                                                                                    |\n| Email sending                                                      | `config/nodemailer.js`                                                                                                                                |\n| Cache-busted static asset URLs                                     | `config/cacheBust.js`, used via `app.locals.getFileHash` in `views/layout.pug`                                                                        |\n| Base layout, nav, footer                                           | `views/layout.pug`, `views/partials/header.pug`, `views/partials/footer.pug`                                                                          |\n| Auth views                                                         | `views/account/login.pug`, `signup.pug`, `forgot.pug`, `reset.pug`, `two-factor.pug`, `totp-setup.pug`, `webauthn-login.pug`, `webauthn-register.pug` |\n| Account management view                                            | `views/account/profile.pug` — **also lists every OAuth provider's link/unlink block**; see OAuth table below                                          |\n| Contact form                                                       | `controllers/contact.js`, `views/contact.pug` — technically optional (SMTP-dependent), but small/self-contained enough that most projects keep it     |\n\n`views/account/login.pug` and `views/account/profile.pug` are **shared,\nhand-edited files**, not generated from a list — each OAuth provider has a\nhardcoded button/link block in one or both of these templates. Removing a\nprovider means deleting its specific block, not deleting the file.\n\n## OAuth providers\n\nEvery provider follows the same shape in `config/passport.js`: a\n`passport.use(...)` call under a `/** Sign in with X */`-style comment,\nregistered under a **provider key** (the string passed to `passport.use` /\n`passport.authorize`, and the field name stored on `User.tokens[].kind` and\n`User[providerKey]`). That provider key is what to grep for across the repo\nwhen removing a provider — it appears in `app.js` routes, `passport.js`,\n`config/token-revocation.js`, and both account views.\n\n| Provider key  | Env vars                                           | Sign-in button (`login.pug`)                | Link/unlink block (`profile.pug`) | Revocation configured | Also used for an API example?                                                                                                                                                                                                                                       |\n| ------------- | -------------------------------------------------- | ------------------------------------------- | --------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| `google`      | `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — Drive (`/api/google/drive`) and Sheets (`/api/google/sheets`) reuse the same Google OAuth token/scopes.** Removing Google login breaks those two API examples too; removing just those two examples doesn't affect login.                                   |\n| `facebook`    | `FACEBOOK_ID`, `FACEBOOK_SECRET`                   | yes                                         | yes                               | yes                   | **Yes — `/api/facebook`** reuses the Facebook login token. Don't confuse `FACEBOOK_ID`/`FACEBOOK_SECRET` (OAuth) with `FACEBOOK_PIXEL_ID` (analytics, unrelated, keep — see below).                                                                                 |\n| `twitch`      | `TWITCH_CLIENT_ID`, `TWITCH_CLIENT_SECRET`         | yes                                         | yes                               | yes                   | **Yes — `/api/twitch`** reuses the Twitch login token.                                                                                                                                                                                                              |\n| `github`      | `GITHUB_ID`, `GITHUB_SECRET`                       | yes                                         | yes                               | yes                   | No — `/api/github` (GitHub search example) is unauthenticated and uses `@octokit/rest` independently; it is a _different feature_ that happens to share a name. Removing \"Sign in with GitHub\" does not require removing the `/api/github` example, and vice versa. |\n| `microsoft`   | `MICROSOFT_CLIENT_ID`, `MICROSOFT_CLIENT_SECRET`   | yes                                         | yes                               | no                    | No                                                                                                                                                                                                                                                                  |\n| `linkedin`    | `LINKEDIN_ID`, `LINKEDIN_SECRET`                   | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `x` (Twitter) | `X_KEY`, `X_SECRET`                                | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `discord`     | `DISCORD_CLIENT_ID`, `DISCORD_CLIENT_SECRET`       | yes                                         | yes                               | yes                   | No                                                                                                                                                                                                                                                                  |\n| `tumblr`      | `TUMBLR_KEY`, `TUMBLR_SECRET`                      | no (link-only, from `/api/tumblr` view)     | yes                               | no                    | This _is_ the API example — `/api/tumblr` (`apiController.getTumblr`) is the only reason this provider exists.                                                                                                                                                      |\n| `steam`       | `STEAM_KEY`                                        | no (link-only, from `/api/steam` view)      | yes                               | no                    | Same pattern — `/api/steam` is the feature.                                                                                                                                                                                                                         |\n| `trakt`       | `TRAKT_ID`, `TRAKT_SECRET`                         | no (link-only, from `/api/trakt` view)      | yes                               | yes                   | Same pattern — `/api/trakt` is the feature.                                                                                                                                                                                                                         |\n| `quickbooks`  | `QUICKBOOKS_CLIENT_ID`, `QUICKBOOKS_CLIENT_SECRET` | no (link-only, from `/api/quickbooks` view) | yes                               | yes                   | Same pattern — `/api/quickbooks` is the feature.                                                                                                                                                                                                                    |\n\nNotes:\n\n- \"Link/unlink block\" for all twelve providers lives in `views/account/profile.pug`; only eight of them (the ones marked \"yes\" in the sign-in column) also get a button in `views/account/login.pug`.\n- `passport-oauth2-refresh` (`refresh.use(...)` calls in `passport.js`) is only wired up for providers with a refresh token: `google`, `linkedin`, `microsoft`, `twitch`, `quickbooks`, `trakt`, `discord`. Removing one of those providers means removing its `refresh.use(...)` call too, not just `passport.use(...)`.\n- `config/token-revocation.js` is driven entirely by `passport.js`'s exported `providerRevocationConfig` object — removing a provider's entry there is enough; there's no separate per-provider file.\n- `/auth/failure` (`app.js`) is a single shared failure-redirect route for _all_ providers — never remove it while any provider remains.\n\n## API examples (`/api/*`)\n\nEvery route below is registered in `app.js` under `/** API examples routes. */`,\nimplemented in `controllers/api.js`, and rendered from `views/api/<name>.pug`\nunless noted. Feature tokens match the e2e test/fixture naming convention\n(`test/e2e/<token>.e2e.test.js`, `test/fixtures/*<token>*` — fixtures are\nnamed after the sanitized outbound request URL, so the token match is\nsubstring, not exact).\n\n| Feature                          | Route(s)                                                   | Controller export                                     | Env vars                                           | npm package(s)                       | e2e test                                                                                                     |\n| -------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------- | -------------------------------------------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------ |\n| API browser index                | `/api`                                                     | `getApi`                                              | —                                                  | —                                    | —                                                                                                            |\n| Last.fm                          | `/api/lastfm`                                              | `getLastfm`                                           | `LASTFM_KEY`, `LASTFM_SECRET`                      | `lastfm`                             | `test/e2e-nokey/lastfm.e2e.test.js`                                                                          |\n| New York Times                   | `/api/nyt`                                                 | `getNewYorkTimes`                                     | `NYT_KEY`                                          | —                                    | `test/e2e/nyt.e2e.test.js`                                                                                   |\n| Stripe                           | `/api/stripe` (GET/POST)                                   | `getStripe`, `postStripe`                             | `STRIPE_SKEY`, `STRIPE_PKEY`                       | `stripe`                             | none                                                                                                         |\n| Web scraping                     | `/api/scraping`                                            | `getScraping`                                         | —                                                  | `cheerio`                            | `test/e2e-nokey/scraping.e2e.test.js`                                                                        |\n| Twilio                           | `/api/twilio` (GET/POST)                                   | `getTwilio`, `postTwilio`                             | `TWILIO_SID`, `TWILIO_TOKEN`, `TWILIO_FROM_NUMBER` | `twilio`                             | `test/e2e/twilio.e2e.test.js`                                                                                |\n| Foursquare                       | `/api/foursquare`                                          | `getFoursquare`                                       | `FOURSQUARE_APIKEY`                                | —                                    | `test/e2e/foursquare.e2e.test.js`                                                                            |\n| PayPal                           | `/api/paypal`, `/api/paypal/success`, `/api/paypal/cancel` | `getPayPal`, `getPayPalSuccess`, `getPayPalCancel`    | `PAYPAL_ID`, `PAYPAL_SECRET`                       | —                                    | none                                                                                                         |\n| File upload                      | `/api/upload` (GET/POST)                                   | `getFileUpload`, `postFileUpload`, `uploadMiddleware` | —                                                  | `multer`                             | `test/e2e-nokey/upload.e2e.test.js`                                                                          |\n| HERE Maps                        | `/api/here-maps`                                           | `getHereMaps`                                         | `HERE_API_KEY`                                     | —                                    | `test/e2e/here-maps.e2e.test.js`                                                                             |\n| Google Maps                      | `/api/google-maps`                                         | `getGoogleMaps`                                       | `GOOGLE_MAP_API_KEY`                               | —                                    | `test/e2e/google-maps.e2e.test.js`                                                                           |\n| Chart (Alpha Vantage + Chart.js) | `/api/chart`                                               | `getChart`                                            | `ALPHA_VANTAGE_KEY`                                | `chart.js`                           | `test/e2e/chart.e2e.test.js` — also remove the `/js/lib/chart.umd.min.js` entry in `app.js`'s `libFiles` map |\n| Lob                              | `/api/lob`                                                 | `getLob`                                              | `LOB_KEY`                                          | `@lob/lob-typescript-sdk`            | `test/e2e/lob.e2e.test.js`                                                                                   |\n| Trakt                            | `/api/trakt`                                               | `getTrakt`                                            | see OAuth table (`trakt`)                          | —                                    | `test/e2e/trakt.e2e.test.js`                                                                                 |\n| PubChem                          | `/api/pubchem`                                             | `getPubChem`                                          | —                                                  | —                                    | `test/e2e-nokey/pubchem.e2e.test.js`                                                                         |\n| Wikipedia                        | `/api/wikipedia`                                           | `getWikipedia`                                        | —                                                  | —                                    | `test/e2e-nokey/wikipedia.e2e.test.js`                                                                       |\n| GIPHY                            | `/api/giphy`                                               | `getGiphy`                                            | `GIPHY_API_KEY`                                    | —                                    | `test/e2e/giphy.e2e.test.js`                                                                                 |\n| GitHub search                    | `/api/github`                                              | `getGithub`                                           | `GITHUB_ID`, `GITHUB_SECRET`*                      | `@octokit/rest`                      | `test/e2e-nokey/github-api.e2e.test.js`                                                                      |\n| Google Drive                     | `/api/google/drive`                                        | `getGoogleDrive`                                      | see OAuth table (`google`)                         | `@googleapis/drive`                  | none (needs live user OAuth session)                                                                         |\n| Google Sheets                    | `/api/google/sheets`                                       | `getGoogleSheets`                                     | see OAuth table (`google`)                         | `@googleapis/sheets`                 | none                                                                                                         |\n| Tumblr                           | `/api/tumblr`                                              | `getTumblr`                                           | see OAuth table (`tumblr`)                         | —                                    | none                                                                                                         |\n| Facebook                         | `/api/facebook`                                            | `getFacebook`                                         | see OAuth table (`facebook`)                       | —                                    | none                                                                                                         |\n| Twitch                           | `/api/twitch`                                              | `getTwitch`                                           | see OAuth table (`twitch`)                         | `twitch-passport` (also OAuth)       | none                                                                                                         |\n| Steam                            | `/api/steam`                                               | `getSteam`                                            | see OAuth table (`steam`)                          | `passport-steam-openid` (also OAuth) | none                                                                                                         |\n| QuickBooks                       | `/api/quickbooks`                                          | `getQuickbooks`                                       | see OAuth table (`quickbooks`)                     | —                                    | none                                                                                                         |\n\n\\* `/api/github` (unauthenticated search) and \"Sign in with GitHub\" both read\n`GITHUB_ID`/`GITHUB_SECRET` from env, but for different purposes — verify\nwhich one(s) a given project still needs before removing these vars.\n\n## AI examples (`/ai/*`)\n\nRegistered in `app.js` under `/** AI Integrations and Boilerplate example\nroutes. */`, implemented across `controllers/ai.js` and\n`controllers/ai-agent.js`, views under `views/ai/`.\n\n| Feature                                    | Route(s)                                                  | Controller                                                       | Env vars                                                                                               | npm package(s)                                                                                                  | e2e test                                                         |\n| ------------------------------------------ | --------------------------------------------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------- |\n| AI examples index                          | `/ai`                                                     | `ai.js: getAi`                                                   | —                                                                                                      | —                                                                                                               | —                                                                |\n| LLM text classifier                        | `/ai/llm-classifier` (GET/POST)                           | `ai.js: getLLMClassifier`, `postLLMClassifier`                   | `GROQ_API_KEY`, `GROQ_MODEL_PROMPT_GUARD`                                                              | `@langchain/groq`, `langchain`                                                                                  | `test/e2e/llm-classifier.e2e.test.js`                            |\n| LLM camera / image                         | `/ai/llm-camera` (GET/POST)                               | `ai.js: getLLMCamera`, `postLLMCamera`, `imageUploadMiddleware`  | `GROQ_API_KEY`, `GROQ_VISION`                                                                          | `@langchain/groq`, `multer`                                                                                     | none                                                             |\n| RAG (retrieval-augmented generation)       | `/ai/rag`, `/ai/rag/ingest`, `/ai/rag/ask`                | `ai.js: getRag`, `postRagIngest`, `postRagAsk`                   | `HUGGINGFACE_KEY`, `HUGGINGFACE_EMBEDDING_MODEL`, `HUGGINGFACE_PROVIDER`, `GROQ_API_KEY`, `GROQ_MODEL` | `@langchain/textsplitters`, `@langchain/mongodb`, `@huggingface/inference`, `pdfjs-dist`, `keyv`, `@keyv/mongo` | `test/e2e/rag.e2e.test.js`                                       |\n| AI Agent (ReAct, tool calling, guardrails) | `/ai/ai-agent`, `/ai/ai-agent/chat`, `/ai/ai-agent/reset` | `ai-agent.js: getAIAgent`, `postAIAgentChat`, `postAIAgentReset` | `GROQ_API_KEY`, `GROQ_MODEL`                                                                           | `@langchain/groq`, `@langchain/core`, `langchain`, `@langchain/langgraph-checkpoint-mongodb`, `zod`             | none (see `test/*.test.js` for unit coverage of session cleanup) |\n\nNotes:\n\n- `ai-agent.js` also exports `cleanupOrphanedTempSessions`, called once from\n  `app.js` on Mongo connection open, and `deleteUserAIAgentData`, called from\n  `controllers/user.js` on account deletion — **both are cross-links from\n  core account code into this \"optional\" feature.** If you remove the AI\n  Agent, remove those two call sites too, not just the routes/controller.\n- `OPENAI_API_KEY` is present in `.env.example` but **not referenced by any\n  current code path** — don't assume it maps to a feature; verify with grep\n  before acting on it either way.\n- `MongoDBAtlasVectorSearch`/`MongoDBAtlasSemanticCache` (RAG) and\n  `MongoDBSaver` (AI Agent) both write to MongoDB collections beyond the\n  `User`/`Session` models — check `controllers/ai.js` / `controllers/ai-agent.js`\n  for the exact collection names if you need to also drop data, not just code.\n\n## Removing a feature: checklist\n\nFor any feature (an OAuth provider, an `/api/*` example, an `/ai/*` example):\n\n1. Route(s) in `app.js`.\n2. Controller export(s) in `controllers/*.js` (delete the function; delete the file only if nothing else in it is still used).\n3. View(s) in `views/`.\n4. Any hardcoded block in a _shared_ view — check `views/account/login.pug`, `views/account/profile.pug`, `views/partials/header.pug` by grepping the provider key or feature name; don't assume there's a clean per-feature file to delete.\n5. `config/passport.js` — `passport.use(...)`, matching `refresh.use(...)` if present, and its entry in `providerRevocationConfig` at the bottom of the file.\n6. Env vars in `.env.example` and `test/.env.test` — **verify with grep that nothing else references the var before removing it** (see the `GITHUB_ID`/`OPENAI_API_KEY` notes above for why this matters).\n7. npm packages — remove with `npm uninstall <package>` (don't hand-edit `package.json`/`package-lock.json`), and only after confirming via grep that no other retained feature imports the same package (several packages, e.g. `passport-steam-openid`, `twitch-passport`, do double duty as both the OAuth strategy and the API example).\n8. Static lib mapping in `app.js`'s `libFiles` map, if the feature has a dedicated client-side library entry (currently only `chart.js`, for the Chart/Alpha Vantage example).\n9. Tests: unit test file if one exists (`test/*.test.js`), the whole `test/e2e/<feature>.e2e.test.js` or `test/e2e-nokey/<feature>.e2e.test.js` file, and its fixtures — fixtures are named after the _outbound request URL_, so match by substring (e.g. everything containing `trakt.tv`), not by an exact feature-name filename.\n10. Run the checks in the next section before considering the removal done — an incomplete removal usually shows up as a lint error (unused import), a failing test (missing fixture / removed route still referenced), or a broken view (undefined local).\n\n## Commands\n\n- `npm run lint` — ESLint + Prettier, should pass with zero warnings.\n- `npm test` — Mocha unit/integration tests (`test/*.test.js`), runs against an in-memory MongoDB.\n- `npm run test:e2e:replay` — Playwright e2e tests against recorded fixtures (no live API keys needed).\n- `npm run test:e2e:custom -- --project=chromium-nokey-live` — Playwright e2e tests that hit live APIs not requiring keys.\n- `npm run test:e2e:live` — full Playwright e2e suite against live third-party APIs (needs real keys in `.env`/`test/.env.test`).\n- `npm start` — builds SCSS then runs the app.\n\n## Testing conventions\n\nSee [test/TESTING.md](test/TESTING.md) for the full picture. Short version:\n`test/*.test.js` are **core** tests (auth, security, session) and must always\npass — they're what protects you from breaking the framework while\ncustomizing it. `test/e2e/` and `test/e2e-nokey/` are **per-integration**\ntests; each one only matters if you're keeping that integration, and should\nbe deleted alongside it.\n","category":"root","tokens":7668},{"name":"CLAUDE.md","path":"CLAUDE.md","title":"CLAUDE.md","content":"# CLAUDE.md\n\nSee [AGENTS.md](AGENTS.md) for project instructions. This file exists only so\nthat tools which don't yet read `AGENTS.md` natively pick up the same\nguidance.\n","category":"root","tokens":43}]}