Skip to content

T98: setup-route credential handling - #17

Merged
LeeJMorel merged 5 commits into
mainfrom
security/t98
Sep 17, 2026
Merged

LeeJMorel merged 5 commits into
mainfrom
security/t98

Conversation

@tomj12k

@tomj12k tomj12k commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Tracked as T98. Details remain in the private security tracker; this public description records the contract and verification without reproducing sensitive review material.

Outcome

  • The setup credential is collected in a POST body rather than a URL.
  • Bare and incorrect requests never receive the GitHub App registration form.
  • A completed registration closes both GET and POST setup paths even if an operator accidentally leaves the setup token configured.
  • Test cleanup restores environment variables faithfully: keys that were originally absent are deleted rather than assigned JavaScript undefined (which Node can coerce to the string "undefined").

The route intentionally serves a credential prompt while setup is pending. That briefly reveals that setup is open, but avoids putting a durable credential in access logs, browser history, and referrers.

TDD and verification

  • The lifecycle regression covers unauthenticated GET and token-bearing POST after registration; both return 404 and disclose no registration form.
  • The environment-cleanup regression first failed because no safe restoration helper existed, then passed after the helper was introduced.
  • Targeted setup/registration/header verification: 20/20 tests passed.
  • TypeScript: tsc --noEmit passed.
  • The prior hosted head passed its complete suite. The new head must repeat hosted verification; the local database-backed suite was not used as proof because the existing local test database has a different schema fingerprint, and it was deliberately left untouched.

Boundary and tradeoff

initiative-app-kit continues to own generic setup-token comparison and state signing. The organization prompt, GitHub manifest, registration-completion condition, route behavior, and copy remain here because they are GitHub-specific. Extracting them would couple the generic kit to one provider's lifecycle without a second consumer.

tomj12k and others added 2 commits September 16, 2026 04:45
The setup route becomes two steps: a GET that renders a prompt, and a POST
that carries the value in a request body. `setupIsOpen()` names the
configuration question that `registrationForm(...)` returning null was
standing in for, so asking it no longer mints and signs a state as a side
effect.

Invariants, stated where the route is:

* the value never appears in a request line, so nothing that records one
  keeps a copy of it;
* the registration form is served only to a request that presented the
  value; every other request gets the same answer it got before;
* with no setup token configured there is no route here at all, so a
  deployment that has finished registering gives nothing away.

One previously pinned behaviour changes: a bare GET renders a prompt rather
than answering 404. The trade is written down next to the test that used to
pin it, and four new tests assert what that 404 was protecting.

See T98.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D5xdKhPXJT4HMWwUyENiu1
@tomj12k

tomj12k commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up review found two stale operator paths after the change to how the
setup value is submitted. Both were documentation and an error string, not
behaviour.

Fixed at f683130: the runtime error and the README now point at the plain
route, and a behaviour-level regression test proves an unregistered deployment
never instructs an operator down the old path. The test failed first against
the stale instruction, then passed. Focused tests (7), TypeScript checking,
build and diff checks pass locally.

Trade-off: the test boots a second server with registration values removed so
it exercises the real production response, which is heavier than a string
assertion and covers the user-visible boundary instead of the source.

Detail is tracked privately under T98.

@tomj12k

tomj12k commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Dependency follow-up at 2fe95c5: npm audit reproduced the transitive high-severity fast-uri URI/SSRF advisories through AJV. The lockfile now resolves the compatible patched 3.1.8 release without changing the declared dependency contract. Audit is clean; the focused behavior suite, type checking, build, and diff checks remain green.

@LeeJMorel

Copy link
Copy Markdown
Member

this might be better served by this kit, is this a shape that will be reused by many apps, or is it specific to github?

@tomj12k

tomj12k commented Sep 16, 2026

Copy link
Copy Markdown
Contributor Author

Exact-head review at 2fe95c5: changes required under T98.

Two earlier corrections were confirmed, along with passing hosted checks. One setup-lifecycle boundary still needed implementation work. The reproduction, impact analysis, and line-level reasoning are maintained in the private assurance record rather than repeated on this public pull request.

Follow-up remediation and verification are recorded below.

A configured setup token must not reopen registration after the app has credentials. Keep setup reachable only while registration is incomplete.
@tomj12k

tomj12k commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

T98 follow-up remediation is pushed at ad616d9.

The setup lifecycle now closes when registration is complete. A real-server regression test covered both request methods before and after the implementation change.

Verification on the committed tree: 294/294 tests, typecheck, production build, dependency audit, and whitespace checks all pass.

Trade-off: reopening setup now requires first removing the existing four-part registration configuration; restoring the setup value alone is deliberately insufficient. Local verification used Node 22.15 while the repository declares Node 24, so hosted Node 24 remains the runtime-version evidence.

Detailed reasoning is tracked privately under T98.

@tomj12k

tomj12k commented Sep 17, 2026

Copy link
Copy Markdown
Contributor Author

Following up on whether this belongs in initiative-app-kit: I kept the boundary where it is.

The kit already owns the reusable primitives—constant-time setup-token comparison and signed state. This PR's remaining behavior is provider-specific: GitHub organization selection, manifest construction, the four GitHub registration credentials, registration-completion closure, route responses, and operator copy. Moving that shape into the kit now would couple the generic package to GitHub without a second implementation demonstrating a stable abstraction.

I also fixed the test-environment cleanup issue in 0d31fb6. A new regression proves that variables absent before a test are deleted afterward instead of becoming the string "undefined". Targeted setup/registration/header tests pass 20/20 and TypeScript passes. Tradeoff: the helper adds a small test-only abstraction, but it removes duplicated unsafe cleanup and makes future setup tests less order-dependent.

@LeeJMorel
LeeJMorel merged commit 2b1f60d into main Sep 17, 2026
2 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