Fix P1s: shared-save null bypass, push cleanup, invite secret leak

Merged
TP
21d ago
+293-32

Addresses the two cubic P1s from #39 plus the invite credential leak:

  1. Shared-save null bypass (routes/files.ts, routes/github-repos.ts): an explicit baseVersion: null skipped both the shared 409 guard (=== undefined check) and the atomic compare (checkBaseVersion treats null as no-check) — a silent teammate overwrite. Null is now normalized to absent at both save endpoints, so it hits the same 409 reload prompt as an omitted version.
  2. Push cleanup deletes possible commits (github.ts repoHasCommits + create/promote catch): on push failure the shell is probed first (commits endpoint; 409 = confirmed empty). Only a confirmed-empty shell is deleted; a repo that may hold commits or can't be verified is kept, with a message saying exactly which.
  3. Invite leaks memberSecret (routes/share.ts): the JSON body spread the full createInvite result including the owner's member credential (readable by page JS despite the HttpOnly cookie). The body now carries only code + expiresAt + playgroundId; the credential still travels in the cookie, which the client never needed from JSON.

Tests: null-baseVersion 409s for both save endpoints, invite body has no secret, repoHasCommits unit cases (commits/empty/denied/network-down), and a new tests/repo-create.test.ts covering create ×3 probe outcomes + promote kept-commits. 358/358 pass, tsc clean.


Summary by cubic

Fixes three P1 issues: a shared-save null bypass, unsafe repo cleanup on failed push, and a leaked invite credential.

  • Explicit baseVersion: null now behaves like an omitted version on both /api/files and /api/file, so shared saves hit the same stale-version 409 instead of silently overwriting a teammate's edits.
  • Failed initial pushes (create and promote) delete the new repo only when the commits endpoint returns the exact empty-repo 409. Other 409s, anomalous 200s, and failed probes keep the repo; a confirmed-empty repo whose deletion fails is reported explicitly instead of claimed unverifiable.
  • POST /api/share/invite no longer spreads the full invite record into the JSON body; it returns only code, expiresAt, and playgroundId. The owner's member credential travels only in the HttpOnly cookie.