Repository navigation
OAUTH-001B9 — Refuse stale Strava disconnect cleanup #608
Description
Parent: #88
Officer impact: None now. After a future protected Functions release, an older disconnect may return the existing unknown-result instruction instead of erasing a newer Strava reconnect.
Officer documentation: None — this is a server-only source boundary. The existing SOURCE ONLY / NOT LIVE unknown-disconnect procedure already tells the member to refresh and stop when the result cannot be confirmed; no officer step, screen, permission, deployment path, or verified live behavior changes.
Deployment evidence: Source, tests, merge, website, runmprc.com, Firebase, Strava/provider, production data, and live behavior are all unverified at issue creation.
Problem
The trusted disconnect path reads secret version A, may wait on Strava revocation, and then blindly batch-deletes the current secret and connection records. A concurrent refresh or reconnect can commit version B during that wait. The older disconnect then erases B and returns success, losing the only local credential for provider access that may still be active.
Invariant and allowed transitions
Capture the initial Firestore secret snapshot's existence and immutable update time before provider work. After admitted provider success, or immediately when no admissible provider token exists, use one Firestore transaction to reread the secret before deleting both local records.
- Current secret exists at the exact initial update time: atomically delete the secret and connection and return success.
- Current secret exists but is newer, including byte-identical replacement: stage no delete, preserve both records, and return the existing fixed unknown result with one fixed redacted cleanup marker.
- Initial secret was absent but a secret now exists: treat it as a newer connection, preserve both records, and return the fixed unknown result.
- Current secret is absent: atomically delete any stale connection and return success; a later reconnect wins by Firestore serialization.
- Transaction conflict retry: reread the current version without repeating provider work; a newer version is preserved.
- Transaction construction, read, version access/comparison, either delete staging, commit, or acknowledgement failure: preserve the existing fixed redacted cleanup boundary. A lost acknowledgement may mean both records were deleted; retry remains convergent.
- Never log or expose tokens, snapshot versions, provider bodies, raw failures, UIDs, paths, or record data.
Atomic scope
functions/strava.js: retain only the admitted initial disconnect snapshot/version and replace the final blind batch cleanup with compare-before-delete transaction logic.functions/strava.test.js: add minimal immutable snapshot-update-time support to the Firestore fake; add a trustworthy named OAUTH-001B9 RED/race block; mechanically update only directly affected OAUTH-001B7 cleanup assertions.SECURITY.md: update only the existing RISK-024 current-truth row with the source-only B9 fence and its residuals.
Acceptance
- Record a focused test-only RED result against the exact base.
- Prove a reconnect committed while token A's revoke is pending remains byte-for-byte intact and returns the fixed unknown result.
- Prove a transaction conflict retries, observes B, and applies no delete.
- Prove a byte-identical replacement with a newer update time is preserved.
- Prove absent-to-new and malformed-to-repaired races preserve the newer pair.
- Prove unchanged, already-absent, provider-failure, pre-apply failure, and lost-acknowledgement/retry behavior.
- Prove provider work occurs at most once and never inside the transaction.
- Run focused and full relevant checks and complete independent security and backup-officer review.
- Record exact merge and deployment-surface evidence before closure.
Migration impact: None. Firestore's existing document update time is the compare-and-delete fence; no stored field, backfill, or production read is added.
Exclusions / stop conditions
Do not expand into client response-shape validation (reserved for a separate B10 child), provider-call serialization, a durable generation/tombstone/audit worker, provider/local acknowledgement reconciliation, response-body byte limits, scope/account policy, other Strava handlers, packages, Rules, workflows, officer-guide changes, Firebase deployment, provider configuration/calls outside synthetic mocks, real tokens, production data, or live verification. Do not close #88 from this child alone.
Stop if a competing timestamped claim appears, the exact disconnect hunks change first, or correctness requires a durable stored generation/schema migration instead of this bounded update-time fence.
Issue creation is not a claim.
Activity
daliu commentedon Aug 2, 2026
CLAIMED by
/rootat2026-08-02T06:36:41Z.codex/issue-608-strava-disconnect-cas24ec0e5789c0ff26cba49371d298aea5c0cdf3cefunctions/strava.js; only minimal immutable snapshot-update-time fake support, one named OAUTH-001B9 race block, and directly required OAUTH-001B7 expectation updates infunctions/strava.test.js; only the existing RISK-024 current-truth row inSECURITY.md.Officer impact: None now. A future protected Functions release may preserve a newer reconnect and return the existing unknown-result instruction instead of erasing it.
Officer documentation: None — the existing SOURCE ONLY / NOT LIVE unknown-disconnect procedure remains accurate and no officer task or verified live behavior changes.
Deployment evidence: None yet. No Firebase, Strava/provider, website, production-data, or live action is authorized by this claim.
No source edit began before this issue, branch, assignment, status label, and timestamped claim. Stop and coordinate before touching the exact owned hunks.
daliu commentedon Aug 2, 2026
RED evidence — 2026-08-02
Exact base:
24ec0e5789c0ff26cba49371d298aea5c0cdf3ceTest-only binary diff SHA-256:
1d8477963d87cc555147f7ac961ddf29ffcf696846714b14be254cd41177fe03Focused Node 20 command:
npm --prefix functions run test:run -- --runInBand strava.test.js -t "OAUTH-001B9 stale disconnect cleanup fence"Expected result: 1 intended failure, 711 unrelated tests skipped. The paused synthetic revoke observed token A; the test committed a made-up replacement secret and connection B before resolving provider success. Exact old source fulfilled instead of returning the fixed unknown result, then erased B. The failure occurred at
Expected Strava request to fail.Synthetic values and mocks only. No Firebase project, Strava/provider call, website, production data, deployment, or live action.
OAUTH-001B9 — Refuse stale Strava disconnect cleanup (#609)
daliu commentedon Aug 2, 2026
COMPLETE / CLOSED / CLAIM RELEASED
Outcome: merged PR #609 now refuses stale Strava disconnect cleanup. A newer reconnect, including a byte-identical replacement with a newer Firestore update time, is preserved and the caller receives the existing fixed unknown result.
Exact evidence:
24ec0e5789c0ff26cba49371d298aea5c0cdf3ce0baf701e6dad16f5d5a6f842001c2c3216921d88645fd89eb771011c634ee37677471a7b1fe59ba7b46d04415fbac77e1d6f005c9d8bfaae0a4f9e573053b4035954e933ae626875613e20ae3ba10b2f3b7085d06ed529486b82f6c6(identical)Officer impact: None now. This is protected Function source only and is not deployed.
Officer documentation: None — the current no-terminal Strava disconnect procedure already covers every unconfirmed result with preserve/refresh/stop/escalate steps. No officer action, visible wording, page structure, permission, data flow, account ownership, or deployment topology changed; no Mermaid update was required.
Deployment evidence checked 2026-08-02:
6a6eea20c6fa380008f2371awas ready for the exact head;published_atwas null.6a6eeae42c8dae0008c35f63stopped in error withpublished_atnull.[].runmprc.commarker remained the bounded WEB-002A — Publish bounded Shop and Events permissions containment #473 release: control9ad6837756cdd409d296009fde5082eeeae5c059, source39ab8649df411262c8109a3c81a57bc38f1e168b, treed76b496cdc5e79015bc048cb961758abbe88b9ce.Residuals remain explicit in
SECURITY.md: provider calls are unserialized; provider/local and lost-acknowledgement reconciliation, connection-only writers, client confirmation admission, runtime IAM/encryption, deployment, and live behavior remain open.Parent #88 remains open. No path or outcome remains claimed by #608.