Skip to content

Audit fixes: revive the API, close the token leak, make self-hosting the default - #19

Merged
Bogzx merged 14 commits into
mainfrom
audit/fixes-and-features
Aug 17, 2026
Merged

Audit fixes: revive the API, close the token leak, make self-hosting the default#19
Bogzx merged 14 commits into
mainfrom
audit/fixes-and-features

Conversation

@Bogzx

@Bogzx Bogzx commented Aug 17, 2026

Copy link
Copy Markdown
Owner

Addresses the LearnLoop findings from repo-audit.html. Every claim below was verified by running the thing, not by reading it.

Verification (all run on this branch at final commit):

Check Result
npm ci clean, 315 packages
npm run typecheck exit 0, 0 errors, 7 workspaces
npm test exit 0 — 139 tests, 139 pass, 0 fail (baseline was 89)
npm run build --workspace=apps/dashboard exit 0, ✓ Generating static pages (7/7)

Blocker fixes

tracedGenerate called itselfapps/api/src/gemini.ts:57 invoked tracedGenerate(params) instead of ai.models.generateContent(params). Every LLM call unwound into RangeError: Maximum call stack size exceeded before any network I/O, so all ten exported functions were dead and /score, /coach, /diff, /improve with them. The recursion was type-valid, which is why tsc and CI stayed green.

apps/api had no test script and zero tests — precisely why this shipped. Added apps/api/src/gemini.test.ts: seven tests that stub globalThis.fetch and assert a request actually reaches the transport, which is what infinite recursion prevents. Verified as a real regression guard: reintroducing the self-call turns the suite red with RangeError, not merely a failed assertion.

Two defects in the pre-existing uncommitted work were found and fixed before building on it: the test asserted five dimension names that do not exist (constraints/output_shape/verification vs the real specificity/constraint_articulation/output_specification), so 3 of 7 tests failed; and apps/api/package.json pointed test at two files that did not exist. It had never been run.

/coach failed open silentlyapps/api/src/index.ts:471-483 answered a scoring failure with { proceed: true, text: '', overall: 0 }. The MCP tool (apps/mcp-server/src/tools.ts:339) renders an empty text on a proceed: true score-mode response as (coach overall: 0/10 — no coaching needed) — byte-identical to a flawless prompt. Whenever Gemini was down the tool ran forever: never coaching, never erroring, reporting success.

proceed stays true (a coaching sidecar outage must not block real work), but the response now carries degraded/error and a non-empty text naming the failure, and the MCP tool renders that instead of the success string. Helper extracted to apps/api/src/coach-degraded.ts so it is testable without booting an HTTP listener and a Postgres pool; 8 tests pin the contract.

No LICENSE, no license field in any of nine manifests — also hard-blocks vsce package. Added MIT LICENSE (Copyright (c) 2026 Bogdan Truta) and license: "MIT" to all nine manifests, plus repository on the two publishable ones.

The dead production hosttrailheadapi-production.up.railway.app returns 404 and was the hardcoded default in ten source files. Rather than re-point at another host that can die, self-hosting is now the default: every surface defaults to http://localhost:3000 (the port apps/api actually listens on) and names the exact env var / setting when it cannot reach it. Changed: apps/browser-ext/src/config.ts:3, apps/vscode-ext/package.json:44, apps/vscode-ext/src/extension.ts:19, apps/dashboard/src/lib/api.ts:20, apps/mcp-server/bin/cli.mjs:56, apps/mcp-server/src/reset-cli.ts:67, apps/mcp-server/src/bootstrap-cli.ts:164, apps/mcp-server/src/smoke-test.mjs:28, apps/mcp-server/src/verify-all-tools.mjs:14, apps/browser-ext/manifest.json:10, apps/browser-ext/scripts/smoke.sh:8.

The browser popup gains an API server row that shows, edits and probes the URL. The dashboard fails at request time (never at module scope) so next build still produces its 7 routes.

Security

GET /teams returned every tenant's token, unauthenticated, with CORS * (apps/api/src/index.ts:127, 1557-1567). That token is the only credential in the system — read on the wiki (which summarises private source code) and write on everything — so one GET from any web page compromised every tenant at once.

It now requires X-Team-Token and returns only the caller's own team, as { name, id } where id is a truncated SHA-256 of the token: opaque, stable, safe to render, not replayable. TeamSummary no longer carries token, so the type system enforces this at every call site.

Client consequences, all deliberate: the popup's pick-a-team list is gone (switching teams means entering that team's token, which the popup resolves to a display name); the popup's reachability probe moved to GET /, the genuine unauthenticated endpoint; the dashboard shows the team its own NEXT_PUBLIC_TEAM_TOKEN resolves to and no longer prints the token into the page.

TRAILHEAD_AUTO_CREATE_TEAMS defaulted on (apps/api/src/index.ts:81) — any string any stranger sent silently provisioned a tenant row. Now defaults off; opt in with =true.

A live tenant token was committed in scripts/list-team-prompts.mjs:18 and scripts/delete-prompt.mjs:18. All three scripts/*.mjs were unreferenced one-offs and are deleted. See Not done below — the token still needs rotating by hand.

Compose ports bind to 127.0.0.1. docker-compose.yml sets TRAILHEAD_AUTO_CREATE_TEAMS=true so trailhead-mcp init's derived per-repo token is accepted; that combination must not be reachable from the network, so neither published port listens on 0.0.0.0.

Honesty pass

Every claim checked against code, not against the spec:

  • README.md:99,108,320-323 — the debounced 250 ms scoring, the 5-second nudge and auto-send-on-timeout were all deliberately removed; apps/browser-ext/src/send-intercept.ts:17-21 lists them as gone. The extension scores on send and never auto-fires.
  • README.md:84-85,288,371 — claimed Gemini 2.5 Flash / 2.5 Pro. packages/scoring/src/models.mjs uses gemini-3-flash-preview and gemma-4-31b-it. Nothing uses 2.5 Pro.
  • README.md:123 — "Four hero tools"; apps/mcp-server/src/tools.ts registers five. Added the missing wiki_proven_prompts row, plus a note that npx trailhead-mcp does not work (private: true, neither name on npm).
  • apps/vscode-ext/README.md:12Cmd+Shift+K articulation scaffold. The manifest contributes no keybindings at all and one command (trailhead.refresh). Moved to an explicit "specced but never built" section.
  • apps/dashboard/README.md:47 — "All four routes prerender as static". There are five app routes and none prerender: / is force-dynamic, the other four read searchParams. Only the framework's /_not-found is static, which is why the build reports 7. (My own first correction here said "four are static" — caught it by reading the build output instead of trusting the audit.)
  • apps/browser-ext/README.md:47 — pointed at a PINNED_CHROME.txt that was never created.
  • packages/db/schema.sql:2 — "Six tables"; there are eight (wiki_jobs and wiki_job_paths landed with rich bootstrap).

New features

Self-host docker-composedocker-compose.yml + apps/api/Dockerfile + .dockerignore + SELFHOSTING.md + rewritten .env.example. Postgres 16 with a named volume and pg_isready healthcheck, schema auto-applied on first boot via docker-entrypoint-initdb.d, API gated on service_healthy. A stranger needs only a Gemini key.

Wiki export to markdownGET /wiki/export returns the whole team wiki as one markdown document (?drafts=true, ?format=json). The renderer is pure, so 14 tests cover the output contract. The case worth calling out: prompt templates routinely contain their own triple-backtick fences, so fence width is computed from the longest backtick run in the body — a hardcoded three-backtick fence closes early and silently truncates the export, the kind of corruption nobody notices until they need the backup. The tree query moved to apps/api/src/wiki-tree.ts so /wiki/tree and /wiki/export share one query instead of two copies that drift.

Remaining MEDIUM/LOW

  • Bubble classifier over-matching (apps/browser-ext/src/selectors.ts:133-141, content.ts:78-91) — classifyBubble matched the node or any descendant, and the walker went outermost-first and let the outermost win, so one wrapper div swallowed the whole thread and every per-message widget mounted on the wrong element. Now matches the element itself only. The walker also stopped testing every div, article, li (thousands of elements per streaming token) in favour of querying the hint selectors directly. 9 tests.
  • Duplicated types (apps/mcp-server/src/api-client.ts:19-43, apps/vscode-ext/src/api.ts:9-26) — four shared types re-declared locally while their neighbours in the same import block came from @trailhead/shared. Both now import and re-export. Also added SearchItem/SearchResponse to shared and applied them to GET /search, which returned c.json({ items: rows }) with no declared contract at all.
  • Unverified .d.mts declarations (packages/scoring/tsconfig.json) — include was ["src/**/*.ts"], matching neither .mts nor .mjs, so nothing typechecked the implementations. Enabled allowJs/checkJs and widened the include (it immediately caught real implicit-any). More importantly, nothing compared a declaration to its implementation: declarations-match.test.mjs parses each .d.mts's value-level exports, imports the .mjs, and asserts both sets match in both directions. All ten pairs are in sync. Verified it fails: a phantom export declare const PHANTOM_MODEL turns it red naming the file and the symbol.
  • Missing indexes (packages/db/schema.sql:71-81,96) — captures had nothing beyond its PK yet /team/metrics filters it on (team_token, created_at) and the dashboard polls every 30s per viewer. The /score dedup probe filters skill_observations on (team_token, user_id, dimension, prompt_hash, ts) but the only index led with (team_token, dimension) and carried neither user_id nor prompt_hash — and that probe runs once per dimension, five times per /score. Dropped idx_nodes_team_token_path, which duplicated the UNIQUE (team_token, path) constraint. Migration: packages/db/migrations/2026-08-17-hot-path-indexes.sql.
  • Packagingcontributes.viewsContainers used the $(rocket) codicon where VS Code requires a file path, which fails vsce package. Added a real SVG at apps/vscode-ext/media/trailhead.svg.
  • Clutter — deleted c.txt (five space characters, unreferenced), moved pitch-before-after.html into archive/, switched the landing page from react.development.js to the production builds.
  • Naming split — left as-is. README.md:9-10 already explains that Trailhead is the internal codename and LearnLoop the product name; renaming nine packages is a large, risky change for little gain.

Not done and why

  • 🔑 The leaked tenant token must be rotated by the repo owner, by hand. repo_dbab62ba8d72ca37 is removed from the working tree but remains in git history, and history was not rewritten (out of scope, and it would break every existing clone). Until it is rotated it remains a valid credential for that team. This needs the owner's account — it cannot be done from here.
  • The Docker stack has never actually been run. Docker CLI 28.0.4 and Compose v2.34.0 are installed on this machine but the daemon is not running. docker compose config validates (exit 0 with GEMINI_API_KEY set, exit 1 with a readable message without it), so schema, interpolation, volumes and healthcheck wiring are checked — but docker compose up was never executed. Unproven at runtime: that the image builds, that npm ci succeeds in-container, that the API connects to Postgres, and that the schema auto-applies. These are reasoned from source, not observed. Please run it once before advertising it.
  • npx trailhead-mcp still does not work. The package remains private: true and unpublished; renaming, unprivating, building to dist/ and bundling the workspace deps is a publishing decision for the owner, not a code fix. The README now says so plainly instead of advertising it.
  • No browser-extension icons, no zip/crx step, no store listing. The vsce blocker is fixed, but producing icon art and store listings is a design/publishing task.
  • The landing page still compiles JSX in-browser via @babel/standalone, and still uses the cdn.tailwindcss.com dev CDN. Both need a real build step, which also turns the Vercel deploy from "serve static files" into "run a build" — a bigger change than this pass, and documented inline in index.html.
  • apps/landing-page/assets/logo-full.png is 488 KB and could be an order of magnitude smaller. Re-encoding a brand asset without the owner eyeballing the result is not a call to make from a script.
  • The audit's "two files committed as UTF-16" could not be reproduced — a scan of every tracked file found no BOM-prefixed or null-heavy UTF-16 content. Either already fixed, or the finding was inaccurate.
  • apps/landing-page is not actually a workspace (it has no package.json) despite the root workspaces: ["apps/*"] implying it. Left alone; changing it affects the deploy.
  • Not attempted from the audit's feature list: local/Ollama model support, weekly Slack/Discord digest, score trend over time, PR description scoring, prompt diff replay.

🤖 Generated with Claude Code

claude added 13 commits August 17, 2026 19:05
tracedGenerate (apps/api/src/gemini.ts:41) called itself at line 57
instead of ai.models.generateContent(params). Every LLM call unwound
into RangeError: Maximum call stack size exceeded before any network
I/O, so all ten exported functions were dead — /score, /coach, /diff
and /improve with them. The recursion was type-valid, so tsc --noEmit
and CI stayed green the whole time.

apps/api had no test script and zero tests, which is exactly why this
shipped. Add gemini.test.ts: seven tests that stub globalThis.fetch
and assert a request actually reaches the transport, which is
precisely what the recursion prevented. Verified as a real regression
guard — reintroducing the self-call turns the suite red with
RangeError rather than merely failing an assertion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…en; add LICENSE

/coach used to answer a scoring failure with { proceed: true, text: '',
overall: 0 }. The MCP coach tool renders an empty text on a proceed=true
score-mode response as "(coach overall: 0/10 - no coaching needed)",
byte-identical to what it prints for a flawless prompt. So whenever
Gemini was down or returned unparseable output, the tool ran forever:
never coaching, never erroring, reporting success the whole time.

proceed stays true - a coaching sidecar outage must not block anyone's
real work - but the response now carries degraded/error and a non-empty
text saying what failed, and the MCP tool renders that instead of the
success string. The helper moved to coach-degraded.ts so it can be
tested without booting an HTTP listener and a Postgres pool; eight tests
pin the contract.

TRAILHEAD_AUTO_CREATE_TEAMS now defaults OFF (index.ts:81). It defaulted
on, so any string any stranger sent as X-Team-Token silently provisioned
a real tenant row - unauthenticated tenant creation and an unbounded
write amplifier. Opt in with =true for open demo deploys.

Delete scripts/{list-team-prompts,delete-prompt,find-prompt-to-delete}.mjs.
Two embedded a live non-demo tenant token; all three were unreferenced
one-offs. NOTE: the token remains in git history and MUST be rotated by
the repo owner - that needs their account and cannot be done from here.

Add MIT LICENSE (Copyright (c) 2026 Bogdan Truta). Its absence also hard
-blocks vsce package. Manifest license fields follow in a later commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Teams will not pour knowledge into a store they cannot get it back out
of. Until now there was no export path at all, which makes the wiki a
roach motel and a fair reason to refuse to adopt it. Export is both the
trust signal and the backup story.

GET /wiki/export returns the whole team wiki as one markdown document
(text/markdown, content-disposition attachment). ?drafts=true includes
draft learnings; ?format=json returns { filename, markdown } for browser
clients that want to trigger their own download.

The renderer (wiki-export.ts) is pure - nodes in, string out, injected
clock - so the output contract is covered by fourteen real tests rather
than a smoke test around a database. The case worth calling out: prompt
templates routinely contain their own ``` blocks, so the fence width is
computed from the longest backtick run in the body. A hardcoded three-
backtick fence closes early and silently truncates the export mid-
document, which is the kind of corruption nobody notices until they need
the backup.

The tree query moved to wiki-tree.ts so /wiki/tree and /wiki/export read
the same rows through one query instead of two copies that drift.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Read the actual query predicates in apps/api/src/index.ts against the
indexes that existed:

- `captures` had nothing beyond its primary key, yet GET /team/metrics
  filters it on (team_token, created_at) and the dashboard polls that
  every 30s per open viewer. Every poll was a sequential scan.
- The /score dedup probe filters skill_observations on (team_token,
  user_id, dimension, prompt_hash, ts). The only index led with
  (team_token, dimension) and carried neither user_id nor prompt_hash,
  so it could not serve the probe - which runs once per dimension, five
  times per /score.
- GET /skill-arc and the COUNT(DISTINCT user_id) in /team/metrics filter
  on (team_token, ts) and never on dimension, so they could not use that
  index either.
- idx_nodes_team_token_path duplicated the UNIQUE (team_token, path)
  constraint on the same columns. Dropped.

schema.sql:2 claimed "Six tables"; there are eight - the wiki_jobs and
wiki_job_paths tables landed with rich bootstrap and the header was
never updated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…duct

Each of these was checked against the code, not against the spec:

- Score card "debounced 250 ms hits to /score" and the "5-second nudge /
  auto-sends as-is on timeout" describe behaviour that was deliberately
  removed. send-intercept.ts:17-21 lists both as gone. The extension now
  scores on send and never auto-fires a prompt.
- Model names: README claimed Gemini 2.5 Flash and Gemini 2.5 Pro.
  packages/scoring/src/models.mjs uses gemini-3-flash-preview (score,
  topic, diff) and gemma-4-31b-it (extract). Nothing uses 2.5 Pro.
- "Four hero tools" - tools.ts registers five (coach, wiki_lookup,
  wiki_save, wiki_bootstrap, wiki_proven_prompts). Added the missing row.
- Cmd+Shift+K (vscode-ext/README.md:12): the manifest contributes no
  keybindings at all and one command, trailhead.refresh. The articulation
  scaffold was deferred during the original build and never written.
  Moved to an explicit "specced but never built" section.
- dashboard/README.md:47 "All four routes prerender as static": there are
  five, and / is force-dynamic.
- browser-ext/README.md:47 pointed at PINNED_CHROME.txt, which was never
  created and does not pin anything.

Also noted in the MCP section that npx trailhead-mcp does not work - the
package is private:true and neither name exists on npm.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- c.txt was five space characters and referenced nowhere.
- pitch-before-after.html (28 KB) is a one-off pitch artifact, unreferenced
  by any build or doc. Moved next to the other one in archive/ rather than
  deleted, since it is presentation history.

Not done here: apps/landing-page/assets/logo-full.png is 488 KB and could
be an order of magnitude smaller, but re-encoding a brand asset without
the owner eyeballing the result is not a call to make from a script.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
ExamplesItem/ExamplesResponse and WikiRecentItem/WikiRecentResponse were
re-declared locally in both apps/mcp-server/src/api-client.ts and
apps/vscode-ext/src/api.ts, while the neighbouring types in the very same
import block came from @trailhead/shared. The local copies were
field-identical to the server's definitions but had no compile-time link
to them, so a server-side change would have typechecked cleanly on both
sides and failed only at runtime.

Both files now import the shared definitions and re-export the names, so
existing importers are unaffected.

Also added SearchItem/SearchResponse to shared and applied SearchResponse
to the GET /search handler, which previously returned c.json({ items: rows })
with no declared contract at all - the one endpoint whose response shape
nothing was checking.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
packages/scoring ships ten hand-written .d.mts declarations next to the
.mjs implementations they describe. Two gaps:

1. tsconfig include was ["src/**/*.ts"], which matches neither .mts nor
   .mjs, so with no allowJs/checkJs the implementations were never
   typechecked by anything. Enabled allowJs + checkJs and widened the
   include. It found real implicit-any in the new test, which is the
   point.

2. Nothing compared a declaration to its implementation. TypeScript
   resolves importers to the .d.mts and never looks at the .mjs, so a
   declared export that does not exist gives every consumer `undefined`
   at runtime while the whole repo still typechecks green.

declarations-match.test.mjs parses the value-level exports out of each
.d.mts, imports the .mjs, and asserts the two sets match in both
directions. All ten pairs are currently in sync. Verified it actually
fails: adding a phantom `export declare const PHANTOM_MODEL` to
models.d.mts turns it red with a message naming the file and the symbol.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The public marketing page loaded react.development.js and
react-dom.development.js - several times the size of the minified builds,
running every dev-only invariant and warning path for every visitor.

Documented the two limitations this does not fix: @babel/standalone still
compiles the JSX in-browser on every load, and cdn.tailwindcss.com is a
dev-time CDN Tailwind tells you not to ship. Both need a real build step,
which also turns the Vercel deploy from "serve static files" into "run a
build" - out of scope for this pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pairs with the MIT LICENSE added earlier. apps/vscode-ext and
apps/mcp-server follow in the next commit - they are being edited
concurrently for the self-hosting change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
classifyBubble returned a role when a hint selector matched the node
ITSELF *or* anything in its subtree (node.querySelector(sel)). The
content script walks outermost-first and deliberately lets the outermost
match win, so the first wrapper div that happened to contain a user
message classified as a user bubble and swallowed the entire thread -
every per-message widget (score badge, outcome rating, prompt diff) then
mounted once, on the wrong element.

classifyBubble now matches on the element itself only.

walkBubblesIn queried every `div, article, li` in the subtree and asked
each one. On a long conversation that is thousands of elements per
mutation batch - i.e. on every streaming token. It now queries the hint
selectors directly via BUBBLE_HINT_SELECTOR: same results, a fraction of
the work, and no ambiguity about which element in a nesting chain is the
bubble.

Nine tests cover it, including the exact regression: a container that
merely contains a user message, and a thread container holding both
roles, must both classify as 'unknown'. classifyBubble only calls
matches(), so the tests use a stub element rather than a DOM.

(content.ts also picks up the initApiUrlState() call from the
self-hosting change landing alongside this.)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…efault

## GET /teams

The endpoint was unauthenticated, returned every team on the server
together with its token, and CORS is `*`. The team token is the only
credential this system has - it grants read on the wiki (which
summarises private source code) and write on everything - so one GET
from any web page compromised every tenant at once. What paid for that
was the browser popup's convenience of pre-filling a team dropdown
before any token was configured.

/teams now requires X-Team-Token and returns only the caller's own team,
as { name, id } where id is a truncated SHA-256 of the token: opaque,
stable, safe to render, and not replayable as a credential
(index.ts:127, 1557-1567 in the old numbering). TeamSummary no longer
carries `token` at all, so the type system enforces this at every call
site.

Client consequences, all deliberate:
- The popup's pick-a-team list is gone; switching teams means entering
  that team's token, which the popup then resolves to a display name via
  the authenticated endpoint. Adopting a team you don't hold a token for
  is no longer possible, which is the point.
- The popup's reachability probe moved from /teams to GET /, the actual
  unauthenticated status endpoint.
- The dashboard shows the team its own NEXT_PUBLIC_TEAM_TOKEN resolves
  to, and no longer prints the token into the page.

## Self-hosting

trailheadapi-production.up.railway.app is deleted and returns 404, and it
was the hardcoded default in ten source files, so every client shipped
pointing at a dead server. Rather than re-point at another host that can
die, self-hosting is now the default: every surface defaults to
http://localhost:3000 (the port apps/api actually listens on) and says so
by name when it cannot reach it. The browser popup grows an "API server"
row that shows and edits the URL.

docker-compose.yml + apps/api/Dockerfile + SELFHOSTING.md bring up
Postgres and the API together, schema auto-applied on first boot, so a
stranger needs only a Gemini key. Both published ports bind to 127.0.0.1
deliberately: compose sets TRAILHEAD_AUTO_CREATE_TEAMS=true so
`trailhead-mcp init`'s derived per-repo token is accepted, and that
combination must not be reachable from the network.

## Also

- license: MIT + repository on the last two manifests (all nine now).
- contributes.viewsContainers used the "$(rocket)" codicon where VS Code
  requires a file path, which fails vsce package. Added a real SVG.
- Dated roadmap/spec docs still cite the dead host; two contain
  copy-pasteable config, so all three now open with a note saying the
  host is gone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eams

The /teams row still advertised "List all teams with tokens (unauth)" -
now an accurate description of a fixed vulnerability. Also documents the
markdown export endpoint and replaces "pre-allowlists the deployed
Railway API" (which is deleted) with the localhost default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
learnloop Ready Ready Preview Aug 17, 2026 4:53pm
polihackwinners-dashboard Ready Ready Preview Aug 17, 2026 4:53pm

…ams client

The "npx trailhead-mcp does not work" disclaimer had been added in one place
(README mcp-server section) but three other spots still told users to run it:
the quick-start (cd into a target repo, then npx trailhead-mcp init), the
deploy list, and the stack summary. The package is private/unpublished, so npx
from a target repo hits the registry and fails. Replaced all three with the
truthful clone-based invocation (node apps/mcp-server/bin/cli.mjs init), and
corrected the stale "home page lists every team" line to match the
now-authenticated, own-team-only endpoint.

Also removed the dead listTeams/fetchListTeams from the dashboard client:
nothing calls it (the home page does its own authenticated fetch), it sent no
X-Team-Token so it would 401, and its comment still claimed the teams endpoint
needs no auth -- the exact falsehood this PR set out to fix. Corrected the file
header comment to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Bogzx
Bogzx merged commit a7430a0 into main Aug 17, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants