fix(driver-turso)!: refuse timeout beside an UPPERCASE WSS:// url in forced remote mode - #16848
Conversation
…ASE WSS:// url with a timeout is refused too
`@libsql/client` routes on a scheme `expandConfig` has already lowercased, so
`WSS://` reaches the WebSocket arm — which carries no window — while the
driver's literal-prefix predicate did not, leaving
new TursoDriver({ url: 'WSS://…', mode: 'remote', timeout: 30000 })
constructing with a window that reaches nothing. Fold case in the window
predicate only; `detectMode` stays case-sensitive on purpose and the docblock
now says why, so the uppercase -> 'local' fall-through is not deleted as a
tidy-up.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
…case-scheme pin `@objectstack/driver-turso`'s tsconfig lib target does not carry `String.prototype.replaceAll` (TS2550) -- which is also the proof that this package's typecheck program really does reach the new `*.test.ts`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ADLdAs2pVcH17h9tZKWMBg
📓 Docs Drift Check1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run. What this run could not see
Coarse fallback — 6 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 6e42bb901d548ecb25e7b074c2ef4d3166283d97 && git checkout 6e42bb901d548ecb25e7b074c2ef4d3166283d97
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5ea982d971cb2550d35a845694a6cbc2575ea52 011101865aa0baa36331d3fae6d137f65f02ef84 && git checkout -B drift-repro c5ea982d971cb2550d35a845694a6cbc2575ea52 && git merge --no-ff 011101865aa0baa36331d3fae6d137f65f02ef84
node scripts/docs-audit/affected-docs.mjs --json c5ea982d971cb2550d35a845694a6cbc2575ea52 |
⛔ HELD — the mandated contract review could not be run, and it will NOT be downgraded
⇒ no review was performed. This is NOT a pass, and it is not an inconclusive review either — it is an absent one.
Consequence, stated plainly: ⛔ this PR does not go ready, ⛔ does not get auto-merge, ⛔ does not land, and the carrier stays on — until the review runs at its own tier. The blocker is quota, not the work: nothing in this diff has been found wanting, because nothing has been examined. PR #16796 (card #16702) is behind the same wall for the same reason. The verification the dev did stands on its own record and is not a substitute — an author's evidence is what the review reads, ⛔ not what it replaces. Generated by Claude Code |
Contract review (
|
State change recorded — the carrier was cleared and this PR flipped ready by another actor, ⛔ not by this seatMeasured just now, ⛔ not inferred: CI is fully green on head ⛔ This seat's own landing pre-check ① remains UNSATISFIED, and an external actor's action does not satisfy it — 「an EXTERNAL audit PASS does not satisfy an in-seat contract-tier PASS」. So: this seat will not land this PR, and does not treat the cleared label as a verdict. What would let this land: a contract-review verdict on record for this diff, from a reviewer at the mandated tier, stated where a seat can read it. Until then this seat abstains rather than blocks — ⛔ it will not merge, and it will not undo another seat's action. The same clearing happened on #16840, #16844 and #16846 in the same window. ⛔ Reported, not acted on. Generated by Claude Code |
⛔ Correction — the review this seat said was missing was already on recordThe seat's comment The seat had measured the labels and inferred the review's absence from the cleared carrier, without reading the comments on the PR it was writing on. ⛔ An absence asserted from the wrong instrument. Withdrawn. Its one condition is now satisfied. F4 made the verdict conditional on ⭐ Worth recording, because it is the review catching something this seat's own machinery cannot: F1 (the non-existent ⇒ this seat's landing pre-check ① is satisfied on this head: a contract-tier verdict for this diff, at the mandated tier, on record and readable. ⛔ Its earlier claim to the contrary is withdrawn. Remaining before it lands: the seat's own ② (both carriers, machine-read with firing controls) and ③ (every check green — already measured green above), then ready + auto-merge.
Generated by Claude Code |
Fixes #16637
Folds case in
ridesWebSocketTransportalone — the triage ruling's option (b), and only (b).TursoDriver.detectModeis not touched.Clause-②: yes
Deliverable 1 — the
expandConfigreading REPRODUCESTriage recorded that the card's case-folding reading was a single execution it had not re-run (the package was not in its
node_modules). Re-run here against@libsql/core@0.17.4, resolved out of this worktree's own install:The premise holds. The mechanism is one line —
lib-esm/config.js:const originalUriScheme = uri.scheme.toLowerCase();— and the last two rows are the control that makes the first two a reading rather than a coincidence: the same call observably answers something other than the input's own letters.And the chain that makes it reachability rather than trivia, read on this tree:
@libsql/client@0.17.4's node entry is_createClient(expandConfig(config, true)), and only thenif (config.scheme === "wss" || config.scheme === "ws") return _createWsClient(config). So the switch that matches the literal lowercase never sees the original casing.⭐ This is the correction triage carried forward from the director seat's review note on the earlier card. The observation "the switch matches the literal lowercase" is true and survives re-checking; the conclusion drawn from it — that an uppercase url cannot reach the WebSocket arm — is false, because a step upstream of the switch was missed. Nothing in this PR reasons from that switch to reachability.
Red before, green after — both legs, measured
Same pin file, same command, same box. The only difference between the two columns is the two-line body of
ridesWebSocketTransport.new TursoDriver({ url: 'WSS://…', mode: 'remote', timeout: 30000 })transportMode: 'remote', no envelope to readVALIDATION_ERROR/ 400The six red assertions before the fix were exactly the refusal ones, and all fourteen controls were already green — which is what makes the green column mean "the corner was closed" rather than "that combination never reached there anyway":
The explicit
mode: 'remote'is load-bearing and stays in the repro: without it an uppercase url falls throughdetectModeto'local', which is pre-existing behaviour and out of scope here.Reverse verification, re-run from the committed state (so the numbers come off a tree that really contains the implementation). Direction predicted before running: the six refusal cases go red, all three controls stay green.
Two notes on that block. The mutation was proved to reach disk by counting the injected and the deleted text separately, plus a blob-hash change — an editor's exit code proves nothing. And the ablation going red without a rebuild is itself the evidence that this suite resolves its subject through
srcand not through the package's builtdist/, so the dist-preflight hazard has no purchase here; that was measured, not assumed. The restore leg is proved by hash equality against theHEADblob together with an emptygit diff HEAD, never by an exit code.The three negative controls, all measured
detectModeis completely unchanged.WSS://,Ws://,HTTPS://andLIBSQL://with no explicitmodestill detect as'local', with and without atimeout.turso-driver.test.tsalready pins this. On this tree it does not — that file pins only:memory:andfile:for local, and a repo-wide search finds no test anywhere asserting the uppercase fall-through. So the control is now written down rather than assumed; without it, an accidental option (a) in a later PR was more likely than the ruling supposed, not less.turso-driver-ws-timeout-refusal.test.tsis green unchanged, and the two lowercase cases are repeated in the new file as the immediate neighbour of the widened predicate.https://remote +timeoutis still ACCEPTED — in every casing (https://,HTTPS://,Https://,LIBSQL://,HTTP://), plustimeout: 0and the no-timeoutcase on an uppercase WebSocket url. An implementation that refusedtimeouton every remote would have turned the red-to-green leg green while deleting the whole option; these controls are what forbid it.The deliberate inconsistency, written into the code
Triage item 3, verbatim as it now stands on
ridesWebSocketTransport:Fossil read before the guard moved, and what it said
ridesWebSocketTransportwas itself put there by the earlier PR, and its docblock ended:That sentence is the falsified claim — the same missed-upstream-step conclusion, recorded in the code. It does not say the case-sensitivity is deliberate and give a reason, so the fossil does not outrank this order; it is an instance of the very error the order warns about. It is corrected in place, in the docblock of the function being changed, because leaving a sentence that asserts the opposite of the new code would be worse than writing no comment at all.
The neighbouring contract,
refuseWebSocketTimeout's "a caller-suppliedclientis not consulted", is aboutclientand not about casing; it is untouched and still holds.Subject sweep — no recorded ruling governs
detectMode's scheme handling. Checked, all on this tree:docs/adr/fordetectMode, "transport mode" and "scheme" (zero hits on this subject); the five ADRs that mention turso or libsql at all, fordetectMode/ transport mode /wss/ scheme / case-sensitivity (zero hits in every one); andscripts/adr-anchors/for any anchor naming turso (none). ADR-0049 is the governing decision for the refusal itself and says nothing about mode detection.Changeset —
minor, and the text rejected.changeset/driver-turso-uppercase-ws-scheme-timeout-refusal.md,@objectstack/driver-turso: minor, with a BREAKING banner and the ADR-0087 disposition markernot-required (no-migration-prescription).Governing text:
.github/workflows/pr-automation.yml, the WHICH LEVEL block (around lines 660-690) — "The commit type may raise a bump but never lower it below what the act requires ... During the launch windowmajorstays refused bycheck-changeset-no-majorand breaking-ness is carried by the BREAKING banner plus the ADR-0087 disposition, not by the level."Text rejected, named: the changeset floor in
AGENTS.md:1033-1036— "A bug fix in a released package takes apatchchangeset". That line is a floor, not a ceiling, and the act here is above it: a construction-time refusal narrows the accept set of a published constructor, so this is not "afix(that changes no public surface".majoris refused during the launch window, which leavesminor. Also rejected: theskip-changesetlabel — this diff publishes from a released package, which is exactly what that label is not for.Same disposition as the just-landed sibling PR, and it is owed here. That PR shipped the identical shape — an accept-set narrowing at this same constructor — as
minor+ BREAKING banner +not-required (no-migration-prescription). Read, not copied: the disposition argument is re-derived for this change and lands the same way, because nothing here adds, removes or renames a key, spec symbol, Zod schema, object definition or stored representation.TursoDriverConfig.timeout,urlandmodekeep their names and types andTursoConfigSchemais untouched; one predicate now compares the scheme case-insensitively, exactly as@libsql/clientitself does before routing. Soobjectstack migrate metahas nothing to visit and there is no tombstone to mint.Verification
All under
scripts/pm/os-verify-lock.sh; verdicts read off the wrapper's ownVERDICT command-exitline, never a bare shell status.pnpm --filter '@objectstack/driver-turso^...' build—VERDICT command-exit 0.pnpm --filter @objectstack/driver-turso test— 50 files, 1231 tests passed.pnpm --filter @objectstack/driver-turso typecheck— clean. It really does reach the new test file: its first run failed withTS2550: Property 'replaceAll' does not exist, from inside that file. Rewritten assplit(...).join(...)for this package's lib target.pnpm lint(eslint . --no-inline-config), repo-wide and unnarrowed — exit 0.Gate families, derived from the settled diff at
011101865aand reconciled.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 57 commands — the newpnpm check:object-def-param-keysfamily among them, and it ran over this diff. Reconciliation line, verbatim:pnpm check:dual-build-cjs-loadsfirst exited 3 — PREREQUISITE NOT MET (79 packages had nodist/). NOT MEASURED, never a pass. Its named closure was built (pnpm build,VERDICT command-exit 0, 7m58s) and it then exited 0.pnpm check:type-check-debtfirst exited 3 for the same missing closure, then exited 3 a second time for a different reason: with the closure built, its re-measure ran out of heap at--max-old-space-size=4096, and the gate's own output notes that "a measurement is only as portable as the ceiling it ran under". Re-run at--max-old-space-size=8192it exited 0.验收备注
out_of_scope_findings, noted and not filed: the ruling's statement thatturso-driver.test.tsalready pins "uppercase url with no explicit mode ⇒'local'" is not true on this tree. It is an inaccuracy in a triage note, not a reproducible defect, a contract violation or an authoring trap, so no card. The gap it left is closed by CONTROL 1 in the new pin file.expandConfigreading is recorded as prose in the pin file's docblock and above, not as an executable assertion.@libsql/coreis not a declared dependency of@objectstack/driver-turso(only@libsql/clientis), so pinning its internals would introduce an undeclared dependency; the earlier PR recorded its own libsql readings the same way, and that precedent is followed.`WSS://`) rather than normalising it, on purpose: an operator greps their config for what they typed. The pin asserts sameness by construction — the uppercase message must equal the lowercase one with the echoed scheme swapped — so "the same message" is measured, not re-typed.TursoDriverConfig.timeouthas no seam on the WebSocket transport (wss:///ws://) in @libsql/client 0.17.4 — documented as unbounded, not delivered #16378, fix(driver-turso)!: refusetimeoutbeside awss:///ws://url at construction (ADR-0049 enforce-or-remove) #16616,TursoDriverConfig.timeoutreaches nothing in remote mode when a pre-configuredclientis supplied — the HTTP-arm window is applied only bycreateRemoteClient#16617 and PR fix(driver-turso)!: refusetimeoutbeside a pre-configuredclientat construction (ADR-0049 enforce-or-remove) #16757.Generated by Claude Code