fix: only warn on endpoint-less sessionAuth when it carries more than refreshOn (#148) - #149
Conversation
… refreshOn (#148) An endpoint-less `sessionAuth: { refreshOn: [...] }` block is a deliberate, supported config (#135) - refreshOn is sourced from the raw merge precisely so it survives the mergedSessionAuth narrowing. The warning added in #147 fired for that case too, so a correctly-configured project saw it on every invocation. Now it only fires when the block carries other keys (the signature of a typo'd handshake key like `sessions:` for `session:`), and names the offending keys so the typo is easy to spot.
…sionAuth (#148) bin/apijack.ts and run-routine.ts inject onChallenge from .apijack/auth.ts into the sessionAuth object, so a refreshOn-only block with a defined onChallenge warns and names onChallenge in foundKeys. That's intentional, not a leak of the bug just fixed: onChallenge is only consumed by SessionAuthStrategy, which is never constructed without a session.endpoint, so the hook is genuinely dead in that config and the warning points at a real mistake.
garretpremo
left a comment
There was a problem hiding this comment.
🤖 Automated review by claude — generated by the
review-issueskill. Treat as advisory; a human still owns the merge decision.
The gate is correct and every acceptance criterion in #148 is met: the refreshOn-only block goes silent, the typo'd-handshake-key block still warns and now names what it found, refreshOn is pinned in both directions, and the JSDoc purity claim is gone. Scope is tight — two files, no drive-by edits — and all 11 checks are green on 55c0284.
I verified the two load-bearing claims in the description independently:
deepMergeSessionAuthreally does always assignonChallenge(src/auth/config-merge.ts:24,result.onChallenge = overrideFn ?? baseFn), andstructuredClone({ ...base, onChallenge: undefined })preserves the key with anundefinedvalue — soObject.keysseesonChallengeon every merged block and the!== undefinedclause is genuinely required, not padding.- The
onChallengeinjection atbin/apijack.tsandsrc/run-routine.tsis guarded byif (sessionAuth && projectOnChallenge), so it only lands when a project actually exports the hook. Acceptance criterion 1 therefore holds on the real shared-binary path, not just in the unit test.
Non-blocking observations
- The
Found: onChallengecase is reasoned about well and pinned by a test, but the message a user actually sees names a key they never typed into their config — they wroterefreshOnand a.apijack/auth.tsexport, and got toldsessionAuth is set but missing session.endpoint ... Found: onChallenge. The diagnostic is pointing at a real dead hook, so keeping it is the right call; the phrasing just doesn't connect the key back to where it came from. A future tweak could special-case it (Found: onChallenge (injected from .apijack/auth.ts)). Not worth blocking on, and not in #148's scope. foundKeysfilters on!== undefinedonly, so a JSON env config with an explicit"cookies": nullcounts as a found key and gets named. That's arguably correct — an explicit null is still something the user wrote — but it's a slightly different notion of "present" than theundefinedcase the filter was written for.- No caller asserts on the new
Found:suffix outside the unit test;tests/cli-builder-refresh-wiring.integration.test.tscovers the wiring but not the message. Fine as-is given the message is a diagnostic rather than API surface.
Nitpicks
- The message ends at
Found: sessions, cookies.without a hint at the fix. For the typo case the found-key list is usually enough to spotsessionsvssession, so this is marginal. - The description's mutation-testing note is a good signal to have in the PR body — worth keeping that habit, since it's the part of the test suite that's hardest to verify from the diff alone.
garretpremo
left a comment
There was a problem hiding this comment.
🤖 Final review by claude — generated by the
final-reviewskill. CI is green, no blockers, soak window elapsed. Marking approved and merging.
Approved.
Outstanding observations from first-pass review
- The
Found: onChallengecase is reasoned about well and pinned by a test, but the message a user actually sees names a key they never typed into their config — they wroterefreshOnand a.apijack/auth.tsexport, and got toldsessionAuth is set but missing session.endpoint ... Found: onChallenge. The diagnostic is pointing at a real dead hook, so keeping it is the right call; the phrasing just doesn't connect the key back to where it came from. A future tweak could special-case it (Found: onChallenge (injected from .apijack/auth.ts)). Not worth blocking on, and not in #148's scope. (#150) foundKeysfilters on!== undefinedonly, so a JSON env config with an explicit"cookies": nullcounts as a found key and gets named. That's arguably correct — an explicit null is still something the user wrote — but it's a slightly different notion of "present" than theundefinedcase the filter was written for. (#150)- No caller asserts on the new
Found:suffix outside the unit test;tests/cli-builder-refresh-wiring.integration.test.tscovers the wiring but not the message. Fine as-is given the message is a diagnostic rather than API surface.
Closes #148
Summary
The
session.endpointwarning added in #147 was over-broad. It fired wheneverrawSessionAuthexisted withoutsession.endpoint— but an endpoint-lesssessionAuth: { refreshOn: [401] }block is a configuration #135 deliberately supports, sincerefreshOnis sourced fromrawSessionAuthprecisely so it survives themergedSessionAuthnarrowing. A correctly-configured project on that route printed the warning on every single CLI invocation.The warning's actual target is the typo case — a block carrying handshake keys with no reachable endpoint (
sessions:forsession:). Gating on "carries some key other thanrefreshOn" keeps that diagnostic and goes quiet for the intentional config.What changes
src/auth/refresh-wiring.ts— gate the warning, and say what was foundThe
!== undefinedclause is load-bearing, not defensive padding:deepMergeSessionAuthalways assignsonChallenge = overrideFn ?? baseFn, so the key is present-but-undefinedon every merged block. Without the clause, a refreshOn-only config would still warn.Naming the found keys is what makes the diagnostic actionable — a
sessions:typo now reportsFound: sessions, cookiesinstead of leaving the user to diff their config against the docs.JSDoc no longer claims purity
The function grew an I/O side effect in #147 while its docstring still read "Kept pure so both sites stay in lockstep". The lockstep rationale stands and is kept; the purity claim is replaced with a note about the diagnostic.
Acceptance criteria
sessionAuth: { refreshOn: [...] }block produces no warningsession.endpoint(the typo case) still warnsrefreshOnbehavior from sessionAuth.refreshOn is unreachable for projects using a custom AuthStrategy #135 is unchanged in both casesrefresh-wiring.tsJSDoc no longer claims purityTest plan
bun test— 1043 pass / 0 fail (up from 1041 ondev)bun run lint— 0 errors (118 pre-existing warnings, unchanged)New coverage in
tests/auth/refresh-wiring.test.ts:sessionsandcookiesbut notrefreshOn('sessions'isn't a substring of'session.endpoint', so that assertion is real rather than incidental)mergedSessionAuth === undefinedandrefreshOn === [401], so sessionAuth.refreshOn is unreachable for projects using a custom AuthStrategy #135's behavior is pinned in both directionsonChallengecase (see below)Every clause of the new filter was mutation-tested during review — reverting the gate, inverting it, dropping
!== undefined, or droppingkey !== 'refreshOn'each fails at least one test, and dropping therefreshOnexclusion fails both.Known behavior, deliberately kept
bin/apijack.ts:130-132andsrc/run-routine.ts:140-142injectonChallengefrom.apijack/auth.tsinto the sessionAuth object. A project with a refreshOn-only block and a customonChallengeexport therefore still warns, reportingFound: onChallenge— a key the user never wrote into that block. This is left as-is and pinned by a test:onChallengeis consumed only bySessionAuthStrategy, which is never constructed without an endpoint, so in that configuration the hook is genuinely dead and the warning is pointing at a real mistake. Excluding it fromfoundKeyswould discard a legitimate diagnostic.