Skip to content

Add auth repair, browser login, identity profiles, doctor bundle, and readiness contract#248

Merged
markus-lassfolk merged 9 commits into
mainfrom
claude/open-issues-features-2bqotj
Jul 18, 2026
Merged

Add auth repair, browser login, identity profiles, doctor bundle, and readiness contract#248
markus-lassfolk merged 9 commits into
mainfrom
claude/open-issues-features-2bqotj

Conversation

@markus-lassfolk

Copy link
Copy Markdown
Owner

Summary

Implements all five open feature requests, all centered on making delegated auth diagnosable and safe for assistant-driven workflows:

  • auth repair (Feature: auth repair wizard for revoked/expired delegated auth #243) — diagnoses broken delegated auth (revoked/expired refresh grant, interaction required, MFA/conditional access, missing credentials, malformed cache, tenant/client mismatch) and prints one safe recovery command. AADSTS50173 (TokensValidFrom after grant issue) is classified as a tenant-side revoked grant, not local cache corruption. --start-login launches device-code login immediately when repair is needed. Never prints raw token/refresh material.
  • login --browser (Feature: browser-based OAuth login mode with PKCE #244) — OAuth2 authorization-code + PKCE login through the system browser, alongside the existing device-code flow. Binds to 127.0.0.1 only, uses PKCE (no client secret), never logs the authorization code or tokens, times out the local listener, and supports --no-open, --localhost-port, --callback-timeout.
  • Identity profiles and wrong-account guardrails (Feature: identity profiles and wrong-account guardrails #245) — profiles list/show/set-default/delete bind a friendly name to a token-cache identity slot and record the last-verified signed-in account; set-default becomes the fallback identity everywhere. New root flags --require-identity <upn> and --as-delegate-of <upn> (with --mailbox) fail closed before any command runs unless the signed-in identity matches — an unverifiable identity is treated as a mismatch, not a pass-through. Both login flows gained --identity/--force-identity-switch: re-logging into an already-verified slug under a different account is refused unless forced.
  • doctor / doctor --redacted-bundle (Feature: redacted diagnostic bundle for auth and mailbox failures #246) — non-secret diagnostic summary safe to attach to an issue: CLI/Node/platform versions, config/cache file presence/size/mtime (never contents), classified auth failure, capability summary. --redacted-bundle [path] writes a real .zip (hand-rolled minimal ZIP writer, no new dependency, cross-validated against unzip and Python's zipfile), or --format dir --output <path>. Every field passes through a deep-redaction pass as defense in depth.
  • readiness --json (Feature: machine-readable readiness contract for mail/calendar operations #247) — stable, versioned JSON contract for "can I safely send mail/read calendar right now?": ready, signedInAs, authHealth, cacheHealth, capabilities/missingCapabilities (via repeatable --require <capability>), optional mailboxAccess, optional --expect-identity mismatch check, and — whenever not ready — recommendedAction/safeCommand. Exits 0 whenever the CLI itself ran successfully.

Shared foundation: src/lib/auth-diagnostics.ts (failure classification reused by auth repair, readiness, and doctor) and src/lib/redact.ts (secret-shaped value detection, tested against false positives on file paths/filenames).

This branch was rebased onto main after #242 (login --json NDJSON automation events) landed concurrently; login.ts was manually reconciled so both feature sets coexist — device-code --json events, --browser PKCE login, and the new --identity/--force-identity-switch guardrails all work together.

Test plan

  • bun run typecheck clean
  • bun run biome:check clean (0 errors; 1 pre-existing unrelated warning in cli.integration.test.ts, not touched by this PR)
  • bun run build + smoke-tested the compiled CLI (doctor, readiness, profiles, auth repair, login --json, login --browser all manually exercised end-to-end, including a real node:http loopback server + real fetch round trip for the browser flow)
  • Full suite: 975 pass, 0 fail (bun test --isolate)
  • Coverage gates: 60.1% overall / 60.8% lib-only (both above the 50% CI floor)
  • graph:inventory:check and docs:graph-permission-matrix:check both pass
  • knip clean
  • doctor --redacted-bundle zip output cross-validated with unzip and Python's zipfile module
  • Docs updated: README.md, docs/AUTHENTICATION.md, docs/CLI_REFERENCE.md, CHANGELOG.md, bundled skill (skills/m365-agent-cli/SKILL.md)

🤖 Generated with Claude Code


Generated by Claude Code

claude added 6 commits July 18, 2026 20:40
… readiness contract

Implements the five open feature requests (#243-#247), all centered on making
delegated auth diagnosable and safe for assistant-driven workflows:

- auth repair: classifies broken auth (revoked/expired grant, interaction
  required, MFA, missing creds, tenant/client mismatch) and prints one safe
  recovery command; AADSTS50173 is correctly classified as a revoked grant,
  not cache corruption. --start-login launches recovery immediately.
- login --browser: authorization-code + PKCE flow through the system browser
  as an alternative to device-code login, with a loopback-only listener,
  --no-open/--localhost-port/--callback-timeout.
- Identity profiles (profiles list/show/set-default/delete) plus global
  --require-identity/--as-delegate-of guardrails that fail closed before any
  command runs on a mismatched or unverifiable identity. Both login flows
  gained --identity/--force-identity-switch to catch wrong-account re-logins.
- doctor / doctor --redacted-bundle: non-secret diagnostic summary (versions,
  file presence/size/mtime, classified failure, capability summary) with a
  hand-rolled minimal ZIP writer (no new dependency) and a deep-redaction
  pass as defense in depth.
- readiness --json: a stable, versioned "can I safely send mail/read
  calendar right now" contract with ready/missingCapabilities/
  recommendedAction/safeCommand, exiting 0 whenever the CLI itself ran.

Shared foundation: src/lib/auth-diagnostics.ts (failure classification used
by all three diagnostic commands) and src/lib/redact.ts (secret-shaped value
detection, tested against false positives on file paths/filenames).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
The runBrowserLoginFlow success test stood up a real loopback HTTP server and
polled captured console.log for the auth URL, which could exceed its 15s
timeout under heavy parallel `bun test --isolate` load (97 files spawning
subprocesses) — flaky in the full run though reliable in isolation.

runBrowserLoginFlow now accepts an internal `_runBrowserLogin` injection
(mirroring the existing openBrowser injection in browser-login.ts) so the
command wrapper's env-persist + identity-binding wiring is tested
deterministically. The real loopback + PKCE round trip remains fully covered
by browser-login.test.ts (which passes reliably under load); the command test
keeps one real-network case for the failure/timeout path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
…load

All 5 real-loopback tests in browser-login.test.ts (the dedicated unit tests
for runBrowserLogin, which must exercise the real server/PKCE/token-exchange
logic) failed under the full `bun test --isolate` run (97 files), timing out
even the one test that already had an explicit 15s allowance.

Root cause: each test synchronized on the captured auth URL via a `setTimeout`
poll loop (5ms interval). Polling needs many event-loop ticks to "catch" a
state change, and becomes increasingly unreliable as the scheduler falls
behind under heavy concurrent load — exactly the full-suite scenario.

Replaced polling with a directly-resolved promise (`authUrlWaiter()`): it
resolves on the very next tick after `onAuthorizationUrl` fires, however
delayed that tick is, with no repeated-polling dependency. Also raised each
test's explicit timeout to 20s as a belt-and-suspenders margin. Verified
passing in isolation; this is the last of the load-sensitive tests introduced
in this batch (the command-level equivalent in login.test.ts was already
made deterministic via dependency injection in the prior commit).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
Root-caused the remaining full-suite flakiness: browser-login.test.ts's
real-socket tests (server.listen + an outbound fetch back to 127.0.0.1) hung
for their full timeout specifically when run as part of the complete 97-file
`bun test --isolate` suite, even after removing the setTimeout-polling
pattern and raising timeouts to 20s. In isolation or small groups they always
passed quickly and reliably — this was a property of real OS socket I/O
competing with 96 other files' state over several minutes, not a bug in the
production code (confirmed separately with a standalone script that exercises
the real node:http server + a real loopback fetch end-to-end, which passes
cleanly).

runBrowserLogin now accepts an injectable `_createLoopbackServer` (defaulting
to a real `node:http` server, unchanged for production/the CLI), typed
against a new minimal `LoopbackServerLike` interface. browser-login.test.ts
uses a small in-memory `FakeLoopbackServer` (an EventEmitter) to drive the
exact same PKCE/state-validation/token-exchange logic by emitting synthetic
`request` events — no real socket, no dependency on how the scheduler treats
it under load. This removes the last real-network dependency from the
automated suite for this feature while covering 100% of the actual decision
logic; real HTTP delivery to a `request` event is a Node.js standard-library
guarantee this repo doesn't need to keep re-verifying under load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
'bun run biome:check' (biome check, which includes the "assist" organize-
imports rules) is the actual CI gate — I had only been running 'biome format'
and 'biome lint' separately, which don't cover import ordering. Applied
biome's safe auto-fix across the 9 files this batch touched; left the one
pre-existing, unrelated warning in cli.integration.test.ts (a file this PR
never touches) alone, since it requires an unsafe fix.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
…on work

TypeScript's control-flow analysis does not treat a bare `await someAsyncFn()`
call as flow-terminating even when someAsyncFn is typed Promise<never> (this
works reliably for synchronous never-returning calls like process.exit, but
not consistently through an await). runBrowserLoginFlow's catch block calls
fatalJson (Promise<never>, always calls process.exit) without an explicit
return/throw after it, which is fine at runtime but left `result` flagged as
possibly-unassigned at every read after the try/catch.

Assign the (never-typed, hence assignable-to-anything) await result to
`result` — satisfies the type checker without changing runtime behavior; the
line is unreachable in practice since fatalJson always exits the process.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
@github-actions

Copy link
Copy Markdown

Coverage after merging claude/open-issues-features-2bqotj into main will be

45.33%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/commands
   approvals.ts34.75%100%33.33%34.80%100–106, 109, 114–115, 121–135, 141–160, 171–200, 213–241, 245–254, 258–263, 42–43, 46–47, 60–61, 66–81, 90–91, 96–99
   auth.ts79.22%100%100%78.67%45, 71–84, 86
   auto-reply.ts18.90%100%100%17.90%100–104, 106–116, 118–119, 122–125, 127–132, 134–135, 137–140, 142–143, 145–150, 152–153, 155–159, 161–181, 185–190, 192, 28, 32–40, 42–43, 46–51, 53–54, 57–66, 68–78, 80–82, 84–87, 90–95, 97–98
   bookings.ts38.87%100%9.52%40.17%100, 1000–1009, 101, 1010–1019, 102–103, 1033–1039, 104, 1040–1047, 105–106, 1062, 1076–1096, 116–128, 139–149, 160–175, 186–201, 212–227, 237–251, 262–270, 281–298, 309–323, 336–348, 360–374, 386–398, 412–432, 444–458, 471–483, 495–510, 523–537, 551–567, 581–595, 609–623, 638–649, 65, 650–653, 66, 667–669, 67, 670–679, 68, 680–687, 69–70, 701–709, 71, 710–719, 72, 720–721, 73, 736–739, 74, 740–749, 75, 750–759, 76, 760–761, 773–788, 802–816, 831, 845–865, 879–893, 908, 922–939, 94, 940–942, 956–970, 985, 99, 999
   calendar.ts27.07%100%50%26.34%100, 1006, 101, 1010–1014, 1016–1019, 1021–1033, 1035–1041, 1043–1051, 1060–1064, 1066, 1072–1077, 1079, 1093, 1106–1112, 151–170, 181–182, 184–185, 187–188, 204–212, 227–231, 234–236, 258, 260–264, 266–268, 270–274, 276–278, 280–291, 294–295, 297–301, 304–316, 321–327, 331–368, 379, 389, 39, 394–396, 399, 40, 400–401, 403–408, 41, 411–412, 414–416, 419, 42, 420–421, 424–427, 43, 430–444, 48–51, 516–519, 52, 520–521, 523, 526–529, 53, 533–539, 54, 540–549, 55, 550–559, 56, 560–569, 57, 570–579, 58, 580–589, 59, 590–591, 596–599, 60, 600–606, 608–609, 61, 610–616, 618–619, 62, 620–621, 623–628, 63, 630–631, 633, 635–639, 64, 640–649, 65, 650–659, 66, 660–662, 664, 666–667, 67, 671–679, 68, 680–689, 69, 690–699, 70, 700–709, 71, 710–719, 72, 720–729, 73, 730–739, 74, 740–749, 75, 750–759, 76, 760–769, 77, 770–782, 784–790, 792–800, 802–805, 807–809, 81, 810–812, 814–815, 817–819, 82, 820–829, 83, 830–839, 84, 840–849, 85, 850–859, 86, 860–869, 87, 870–879, 88, 880–882, 884–889, 89, 890–899, 90, 900–901, 903, 91, 913, 92, 929, 93–96, 962, 964–967, 969, 97, 970–979, 98, 980–982, 984–989, 99, 990–995, 998–999
   contacts.ts36.68%100%9.09%37.54%1000–1025, 1041–1071, 1086–1100, 1115–1129, 1144–1163, 1178–1187, 1200–1216, 1228–1250, 134–144, 158–179, 194–211, 225–243, 257–270, 310, 323–329, 336–355, 363–369, 384–399, 415–438, 453–470, 484–502, 519–536, 557–604, 627–656, 674–707, 71–72, 726–729, 73, 730–739, 74, 740–749, 75, 750–757, 76–77, 774–779, 78, 780–803, 820–851, 86–87, 870–879, 88, 880, 89, 894–899, 90, 900–909, 91, 910, 92, 924–929, 93, 930–939, 94, 940–942, 95–96, 961–969, 97, 970–974, 98–99, 991–999
   copilot.ts49.61%100%0.83%52.28%1000–1005, 1017–1020, 1034–1040, 1053–1056, 1066–1069, 1080–1085, 1098–1101, 1112–1116, 1137–1167, 1186–1216, 1232–1235, 1250–1260, 1277–1295, 1310–1319, 132, 1320, 133, 1335–1339, 134, 1340–1343, 135–136, 1360–1369, 137, 1370–1386, 1402–1409, 141, 1410–1425, 1442–1453, 1466–1477, 1490–1501, 1514–1518, 1532–1539, 154–155, 1554–1560, 1578–1581, 1591–1596, 1606–1609, 1620–1626, 1636–1641, 1651–1656, 1667–1672, 1683–1688, 1700–1716, 1728–1734, 1752–1755, 176, 1769, 177, 1770–1779, 178, 1780–1785, 179, 1798–1799, 180, 1800–1803, 181, 1815–1818, 182–183, 1830–1833, 184, 1845–1849, 185, 1850, 186, 1864–1869, 187, 1870, 188, 1886–1889, 189, 1890–1899, 190, 1900–1903, 191, 1918–1919, 192, 1920–1927, 193–194, 1940–1943, 195, 1955–1958,

Comment thread src/lib/browser-login.test.ts Fixed
…st.ts

url.includes('login.microsoftonline.com') would also match an attacker-
controlled URL containing that string anywhere (e.g. an evil.example URL
with the real host as a path segment). Replaced with an exact hostname
comparison via new URL(url).hostname === 'login.microsoftonline.com'.

Test-only code (a fetch mock deciding whether to return a fixture response),
but worth being precise regardless — flagged as a high-severity CodeQL
alert on the PR.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
@github-actions

Copy link
Copy Markdown

Coverage after merging claude/open-issues-features-2bqotj into main will be

45.33%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/commands
   approvals.ts34.75%100%33.33%34.80%100–106, 109, 114–115, 121–135, 141–160, 171–200, 213–241, 245–254, 258–263, 42–43, 46–47, 60–61, 66–81, 90–91, 96–99
   auth.ts79.22%100%100%78.67%45, 71–84, 86
   auto-reply.ts18.90%100%100%17.90%100–104, 106–116, 118–119, 122–125, 127–132, 134–135, 137–140, 142–143, 145–150, 152–153, 155–159, 161–181, 185–190, 192, 28, 32–40, 42–43, 46–51, 53–54, 57–66, 68–78, 80–82, 84–87, 90–95, 97–98
   bookings.ts38.87%100%9.52%40.17%100, 1000–1009, 101, 1010–1019, 102–103, 1033–1039, 104, 1040–1047, 105–106, 1062, 1076–1096, 116–128, 139–149, 160–175, 186–201, 212–227, 237–251, 262–270, 281–298, 309–323, 336–348, 360–374, 386–398, 412–432, 444–458, 471–483, 495–510, 523–537, 551–567, 581–595, 609–623, 638–649, 65, 650–653, 66, 667–669, 67, 670–679, 68, 680–687, 69–70, 701–709, 71, 710–719, 72, 720–721, 73, 736–739, 74, 740–749, 75, 750–759, 76, 760–761, 773–788, 802–816, 831, 845–865, 879–893, 908, 922–939, 94, 940–942, 956–970, 985, 99, 999
   calendar.ts27.07%100%50%26.34%100, 1006, 101, 1010–1014, 1016–1019, 1021–1033, 1035–1041, 1043–1051, 1060–1064, 1066, 1072–1077, 1079, 1093, 1106–1112, 151–170, 181–182, 184–185, 187–188, 204–212, 227–231, 234–236, 258, 260–264, 266–268, 270–274, 276–278, 280–291, 294–295, 297–301, 304–316, 321–327, 331–368, 379, 389, 39, 394–396, 399, 40, 400–401, 403–408, 41, 411–412, 414–416, 419, 42, 420–421, 424–427, 43, 430–444, 48–51, 516–519, 52, 520–521, 523, 526–529, 53, 533–539, 54, 540–549, 55, 550–559, 56, 560–569, 57, 570–579, 58, 580–589, 59, 590–591, 596–599, 60, 600–606, 608–609, 61, 610–616, 618–619, 62, 620–621, 623–628, 63, 630–631, 633, 635–639, 64, 640–649, 65, 650–659, 66, 660–662, 664, 666–667, 67, 671–679, 68, 680–689, 69, 690–699, 70, 700–709, 71, 710–719, 72, 720–729, 73, 730–739, 74, 740–749, 75, 750–759, 76, 760–769, 77, 770–782, 784–790, 792–800, 802–805, 807–809, 81, 810–812, 814–815, 817–819, 82, 820–829, 83, 830–839, 84, 840–849, 85, 850–859, 86, 860–869, 87, 870–879, 88, 880–882, 884–889, 89, 890–899, 90, 900–901, 903, 91, 913, 92, 929, 93–96, 962, 964–967, 969, 97, 970–979, 98, 980–982, 984–989, 99, 990–995, 998–999
   contacts.ts36.68%100%9.09%37.54%1000–1025, 1041–1071, 1086–1100, 1115–1129, 1144–1163, 1178–1187, 1200–1216, 1228–1250, 134–144, 158–179, 194–211, 225–243, 257–270, 310, 323–329, 336–355, 363–369, 384–399, 415–438, 453–470, 484–502, 519–536, 557–604, 627–656, 674–707, 71–72, 726–729, 73, 730–739, 74, 740–749, 75, 750–757, 76–77, 774–779, 78, 780–803, 820–851, 86–87, 870–879, 88, 880, 89, 894–899, 90, 900–909, 91, 910, 92, 924–929, 93, 930–939, 94, 940–942, 95–96, 961–969, 97, 970–974, 98–99, 991–999
   copilot.ts49.61%100%0.83%52.28%1000–1005, 1017–1020, 1034–1040, 1053–1056, 1066–1069, 1080–1085, 1098–1101, 1112–1116, 1137–1167, 1186–1216, 1232–1235, 1250–1260, 1277–1295, 1310–1319, 132, 1320, 133, 1335–1339, 134, 1340–1343, 135–136, 1360–1369, 137, 1370–1386, 1402–1409, 141, 1410–1425, 1442–1453, 1466–1477, 1490–1501, 1514–1518, 1532–1539, 154–155, 1554–1560, 1578–1581, 1591–1596, 1606–1609, 1620–1626, 1636–1641, 1651–1656, 1667–1672, 1683–1688, 1700–1716, 1728–1734, 1752–1755, 176, 1769, 177, 1770–1779, 178, 1780–1785, 179, 1798–1799, 180, 1800–1803, 181, 1815–1818, 182–183, 1830–1833, 184, 1845–1849, 185, 1850, 186, 1864–1869, 187, 1870, 188, 1886–1889, 189, 1890–1899, 190, 1900–1903, 191, 1918–1919, 192, 1920–1927, 193–194, 1940–1943, 195, 1955–1958,

Move the Unreleased changelog entries (auth repair, browser login,
identity profiles, doctor bundle, readiness contract) under a
2026.7.19 release header, since 2026.7.18 is already tagged/released.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
@github-actions

Copy link
Copy Markdown

Coverage after merging claude/open-issues-features-2bqotj into main will be

45.33%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/commands
   approvals.ts34.75%100%33.33%34.80%100–106, 109, 114–115, 121–135, 141–160, 171–200, 213–241, 245–254, 258–263, 42–43, 46–47, 60–61, 66–81, 90–91, 96–99
   auth.ts79.22%100%100%78.67%45, 71–84, 86
   auto-reply.ts18.90%100%100%17.90%100–104, 106–116, 118–119, 122–125, 127–132, 134–135, 137–140, 142–143, 145–150, 152–153, 155–159, 161–181, 185–190, 192, 28, 32–40, 42–43, 46–51, 53–54, 57–66, 68–78, 80–82, 84–87, 90–95, 97–98
   bookings.ts38.87%100%9.52%40.17%100, 1000–1009, 101, 1010–1019, 102–103, 1033–1039, 104, 1040–1047, 105–106, 1062, 1076–1096, 116–128, 139–149, 160–175, 186–201, 212–227, 237–251, 262–270, 281–298, 309–323, 336–348, 360–374, 386–398, 412–432, 444–458, 471–483, 495–510, 523–537, 551–567, 581–595, 609–623, 638–649, 65, 650–653, 66, 667–669, 67, 670–679, 68, 680–687, 69–70, 701–709, 71, 710–719, 72, 720–721, 73, 736–739, 74, 740–749, 75, 750–759, 76, 760–761, 773–788, 802–816, 831, 845–865, 879–893, 908, 922–939, 94, 940–942, 956–970, 985, 99, 999
   calendar.ts27.07%100%50%26.34%100, 1006, 101, 1010–1014, 1016–1019, 1021–1033, 1035–1041, 1043–1051, 1060–1064, 1066, 1072–1077, 1079, 1093, 1106–1112, 151–170, 181–182, 184–185, 187–188, 204–212, 227–231, 234–236, 258, 260–264, 266–268, 270–274, 276–278, 280–291, 294–295, 297–301, 304–316, 321–327, 331–368, 379, 389, 39, 394–396, 399, 40, 400–401, 403–408, 41, 411–412, 414–416, 419, 42, 420–421, 424–427, 43, 430–444, 48–51, 516–519, 52, 520–521, 523, 526–529, 53, 533–539, 54, 540–549, 55, 550–559, 56, 560–569, 57, 570–579, 58, 580–589, 59, 590–591, 596–599, 60, 600–606, 608–609, 61, 610–616, 618–619, 62, 620–621, 623–628, 63, 630–631, 633, 635–639, 64, 640–649, 65, 650–659, 66, 660–662, 664, 666–667, 67, 671–679, 68, 680–689, 69, 690–699, 70, 700–709, 71, 710–719, 72, 720–729, 73, 730–739, 74, 740–749, 75, 750–759, 76, 760–769, 77, 770–782, 784–790, 792–800, 802–805, 807–809, 81, 810–812, 814–815, 817–819, 82, 820–829, 83, 830–839, 84, 840–849, 85, 850–859, 86, 860–869, 87, 870–879, 88, 880–882, 884–889, 89, 890–899, 90, 900–901, 903, 91, 913, 92, 929, 93–96, 962, 964–967, 969, 97, 970–979, 98, 980–982, 984–989, 99, 990–995, 998–999
   contacts.ts36.68%100%9.09%37.54%1000–1025, 1041–1071, 1086–1100, 1115–1129, 1144–1163, 1178–1187, 1200–1216, 1228–1250, 134–144, 158–179, 194–211, 225–243, 257–270, 310, 323–329, 336–355, 363–369, 384–399, 415–438, 453–470, 484–502, 519–536, 557–604, 627–656, 674–707, 71–72, 726–729, 73, 730–739, 74, 740–749, 75, 750–757, 76–77, 774–779, 78, 780–803, 820–851, 86–87, 870–879, 88, 880, 89, 894–899, 90, 900–909, 91, 910, 92, 924–929, 93, 930–939, 94, 940–942, 95–96, 961–969, 97, 970–974, 98–99, 991–999
   copilot.ts49.61%100%0.83%52.28%1000–1005, 1017–1020, 1034–1040, 1053–1056, 1066–1069, 1080–1085, 1098–1101, 1112–1116, 1137–1167, 1186–1216, 1232–1235, 1250–1260, 1277–1295, 1310–1319, 132, 1320, 133, 1335–1339, 134, 1340–1343, 135–136, 1360–1369, 137, 1370–1386, 1402–1409, 141, 1410–1425, 1442–1453, 1466–1477, 1490–1501, 1514–1518, 1532–1539, 154–155, 1554–1560, 1578–1581, 1591–1596, 1606–1609, 1620–1626, 1636–1641, 1651–1656, 1667–1672, 1683–1688, 1700–1716, 1728–1734, 1752–1755, 176, 1769, 177, 1770–1779, 178, 1780–1785, 179, 1798–1799, 180, 1800–1803, 181, 1815–1818, 182–183, 1830–1833, 184, 1845–1849, 185, 1850, 186, 1864–1869, 187, 1870, 188, 1886–1889, 189, 1890–1899, 190, 1900–1903, 191, 1918–1919, 192, 1920–1927, 193–194, 1940–1943, 195, 1955–1958,

An 8-angle multi-pass review (line-by-line, removed-behavior, cross-file
tracing, reuse, simplification, efficiency, altitude) followed by
independent verification surfaced 32 confirmed issues across auth repair,
browser login, identity profiles/guardrails, doctor, and readiness. All
are fixed here, with regression tests for the highest-severity ones.

Correctness:
- classifyAuthFailure checked the generic `interaction_required` branch
  before the more specific AADSTS50076/50079 MFA check, so real
  conditional-MFA responses (which usually carry both signals) were
  misclassified and recommended the wrong login flow.
- A healthy EWS-only diagnosis always returned `capabilities: []`
  (EWS.AccessAsUser.All isn't representable in Graph scp/roles), making
  `readiness --require mail.read` falsely report the capability missing.
  Now maps onto the equivalent Graph-shaped capability ids.
- `--as-delegate-of`'s target was never checked when `--require-identity`
  was also set (`requireIdentity ?? asDelegateOf` only verified one).
  Now both are checked independently.
- The root `preAction` identity guard applied to `login`/`auth
  repair`/`profiles` too, so `login --require-identity X` on a machine
  with no verifiable identity yet could never succeed. These commands are
  now exempted — they have their own finer-grained wrong-account check.
- `auth repair --start-login` passed the raw (possibly undefined)
  `opts.identity` instead of the resolved identity, silently bypassing
  the wrong-account guardrail when relying on the default profile.
- Device-code login extracted the UPN ad hoc instead of via the shared
  `getJwtPayloadUpn` helper, missing `preferred_username` and letting
  `bindLoginIdentityOrThrow` skip its mismatch check for such tokens.
- Login bound/committed the identity profile before the refresh token was
  durably persisted; a later unrelated failure (disk full) could leave
  profiles.json falsely claiming a fresh, verified login. Split into
  assert (pre-write) then commit (post-persist) phases.
- `readiness`/`doctor` unconditionally re-resolved Graph for the mailbox
  check even when the healthy diagnosis came from the EWS fallback,
  producing a false "could not obtain a token" failure. Gated on the new
  `authBackend` field instead.
- `readiness`'s `profile` field used `getProfile(identity)`, which looks
  up by profile *name* — always `null` when a profile's bound identity
  differs from its name. Added `getProfileByIdentity`.
- `getJwtPayloadUpn` only trimmed the UPN claim, dropping the embedded
  CR/LF stripping the old device-code path had — a crafted claim could
  forge extra output lines. Centralized in `upnFromJwtPayload`.
- `redact.ts`'s value-shape heuristic redacted legitimate long,
  mixed-case identity/profile names (e.g. "ContosoProdAcct2024") in
  `doctor`/`profiles`/`readiness` JSON output. Added a `safeKeys`
  exemption for known display-identifier fields.
- `browser-login.ts`'s token-exchange error text skipped the control-char
  sanitization `auth.ts`/`graph-auth.ts` apply to the same kind of error.
- MCP server excluded the entire `auth` command (hiding the safe,
  read-only `auth repair` diagnostic) instead of just the interactive
  `--start-login` flag. Now excludes only that flag from the tool schema.

Reuse/simplification/efficiency:
- Extracted `resolveIdentitySlug`/`getProfilesSnapshot` in
  identity-profiles.ts to remove 4x duplication of the
  `opts.identity || defaultProfileIdentity || 'default'` fallback and a
  double read of profiles.json in `profiles list/show`.
- Exported `capabilitiesFromToken` from auth-diagnostics.ts so
  `profiles.ts` reuses it instead of reimplementing JWT capability
  decoding; `resolveSignedInUpn` now respects `M365_EXCHANGE_BACKEND`
  like `diagnoseAuth` does.
- `diagnoseAuth` decodes each token payload once instead of three times
  (UPN/tenant/capabilities) via a shared `decodeJwtPayload`.
- Removed dead code in `classifyAuthFailure`, collapsed duplicate
  `safeCommandFor` switch cases, de-duplicated `profiles.ts`'s
  JSON-error-and-exit block into one `failWith` helper, and parallelized
  independent awaits in `readiness.ts`/`doctor-bundle.ts`.

Regenerated docs/GRAPH_PATH_INVENTORY.json (line-number drift from the
fixes above) and docs/CLI_SCRIPTING_INVENTORY.md (pre-existing drift).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FhSLSPoBW86bnXc7HFt832
@github-actions

Copy link
Copy Markdown

Coverage after merging claude/open-issues-features-2bqotj into main will be

45.45%

Coverage Report
FileStmtsBranchesFuncsLinesUncovered Lines
src/commands
   approvals.ts34.75%100%33.33%34.80%100–106, 109, 114–115, 121–135, 141–160, 171–200, 213–241, 245–254, 258–263, 42–43, 46–47, 60–61, 66–81, 90–91, 96–99
   auth.ts79.22%100%100%78.67%45, 71–74, 79–88, 90
   auto-reply.ts18.90%100%100%17.90%100–104, 106–116, 118–119, 122–125, 127–132, 134–135, 137–140, 142–143, 145–150, 152–153, 155–159, 161–181, 185–190, 192, 28, 32–40, 42–43, 46–51, 53–54, 57–66, 68–78, 80–82, 84–87, 90–95, 97–98
   bookings.ts38.87%100%9.52%40.17%100, 1000–1009, 101, 1010–1019, 102–103, 1033–1039, 104, 1040–1047, 105–106, 1062, 1076–1096, 116–128, 139–149, 160–175, 186–201, 212–227, 237–251, 262–270, 281–298, 309–323, 336–348, 360–374, 386–398, 412–432, 444–458, 471–483, 495–510, 523–537, 551–567, 581–595, 609–623, 638–649, 65, 650–653, 66, 667–669, 67, 670–679, 68, 680–687, 69–70, 701–709, 71, 710–719, 72, 720–721, 73, 736–739, 74, 740–749, 75, 750–759, 76, 760–761, 773–788, 802–816, 831, 845–865, 879–893, 908, 922–939, 94, 940–942, 956–970, 985, 99, 999
   calendar.ts27.07%100%50%26.34%100, 1006, 101, 1010–1014, 1016–1019, 1021–1033, 1035–1041, 1043–1051, 1060–1064, 1066, 1072–1077, 1079, 1093, 1106–1112, 151–170, 181–182, 184–185, 187–188, 204–212, 227–231, 234–236, 258, 260–264, 266–268, 270–274, 276–278, 280–291, 294–295, 297–301, 304–316, 321–327, 331–368, 379, 389, 39, 394–396, 399, 40, 400–401, 403–408, 41, 411–412, 414–416, 419, 42, 420–421, 424–427, 43, 430–444, 48–51, 516–519, 52, 520–521, 523, 526–529, 53, 533–539, 54, 540–549, 55, 550–559, 56, 560–569, 57, 570–579, 58, 580–589, 59, 590–591, 596–599, 60, 600–606, 608–609, 61, 610–616, 618–619, 62, 620–621, 623–628, 63, 630–631, 633, 635–639, 64, 640–649, 65, 650–659, 66, 660–662, 664, 666–667, 67, 671–679, 68, 680–689, 69, 690–699, 70, 700–709, 71, 710–719, 72, 720–729, 73, 730–739, 74, 740–749, 75, 750–759, 76, 760–769, 77, 770–782, 784–790, 792–800, 802–805, 807–809, 81, 810–812, 814–815, 817–819, 82, 820–829, 83, 830–839, 84, 840–849, 85, 850–859, 86, 860–869, 87, 870–879, 88, 880–882, 884–889, 89, 890–899, 90, 900–901, 903, 91, 913, 92, 929, 93–96, 962, 964–967, 969, 97, 970–979, 98, 980–982, 984–989, 99, 990–995, 998–999
   contacts.ts36.68%100%9.09%37.54%1000–1025, 1041–1071, 1086–1100, 1115–1129, 1144–1163, 1178–1187, 1200–1216, 1228–1250, 134–144, 158–179, 194–211, 225–243, 257–270, 310, 323–329, 336–355, 363–369, 384–399, 415–438, 453–470, 484–502, 519–536, 557–604, 627–656, 674–707, 71–72, 726–729, 73, 730–739, 74, 740–749, 75, 750–757, 76–77, 774–779, 78, 780–803, 820–851, 86–87, 870–879, 88, 880, 89, 894–899, 90, 900–909, 91, 910, 92, 924–929, 93, 930–939, 94, 940–942, 95–96, 961–969, 97, 970–974, 98–99, 991–999
   copilot.ts49.61%100%0.83%52.28%1000–1005, 1017–1020, 1034–1040, 1053–1056, 1066–1069, 1080–1085, 1098–1101, 1112–1116, 1137–1167, 1186–1216, 1232–1235, 1250–1260, 1277–1295, 1310–1319, 132, 1320, 133, 1335–1339, 134, 1340–1343, 135–136, 1360–1369, 137, 1370–1386, 1402–1409, 141, 1410–1425, 1442–1453, 1466–1477, 1490–1501, 1514–1518, 1532–1539, 154–155, 1554–1560, 1578–1581, 1591–1596, 1606–1609, 1620–1626, 1636–1641, 1651–1656, 1667–1672, 1683–1688, 1700–1716, 1728–1734, 1752–1755, 176, 1769, 177, 1770–1779, 178, 1780–1785, 179, 1798–1799, 180, 1800–1803, 181, 1815–1818, 182–183, 1830–1833, 184, 1845–1849, 185, 1850, 186, 1864–1869, 187, 1870, 188, 1886–1889, 189, 1890–1899, 190, 1900–1903, 191, 1918–1919, 192, 1920–1927, 193–194, 1940–1943, 195,

@markus-lassfolk
markus-lassfolk merged commit 3077afc into main Jul 18, 2026
5 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.

3 participants