From 66a55d83e32c05c7a5add46b76941361c15c87f4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 15 Aug 2026 04:36:23 +0000 Subject: [PATCH] docs(types): correct error-leak.ts's false MySQL dialect claim, and pin the uncovered dialect `DIALECT_LEAK_PHRASINGS` said MySQL/MSSQL/Oracle "nobody here runs them". Measurably false for MySQL on the same tree: driver-sql branches on mysql/mysql2, CI stands up a live mysql:8.0 for the required Temporal Conformance check, and live MySQL 8.0.46 measurements landed driver fixes (#8621, #8622). The claim is a security-reasoning input and was consumed as one on PR #8737, so the correction replaces the reachability claim with a COVERAGE statement and adds the pins that keep it honest. Behaviour is unchanged: no keyword is added, and whether MySQL is a supported deployment target or a tested dialect stays an open product question on #8739. Part of #8739 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NaS1PAHJcPfAA2acnV53Tn --- packages/types/src/error-leak.test.ts | 66 +++++++++++++++++++++++++-- packages/types/src/error-leak.ts | 52 +++++++++++++++++---- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/packages/types/src/error-leak.test.ts b/packages/types/src/error-leak.test.ts index 3dff23db28..dce6f7617d 100644 --- a/packages/types/src/error-leak.test.ts +++ b/packages/types/src/error-leak.test.ts @@ -96,9 +96,13 @@ describe('looksLikeInternalErrorLeak', () => { * FALSE and shipped a physical table name from every boundary that applies the * predicate. * - * Scope is deliberately the dialects this repo actually RUNS (SQLite/libsql and - * Postgres, via `driver-sql`), not a census of MySQL/MSSQL/Oracle spellings - * nobody here has met — the unbounded-list trap the module note argues against. + * Scope is deliberately the two dialects the list COVERS (SQLite/libsql and + * Postgres, via `driver-sql`), not a census of every dialect's spelling — the + * unbounded-list trap the module note argues against. ⚠️ Covered is not the + * same as reachable: #8739 measured that MySQL runs here (a live `mysql:8.0` + * behind a required check) while this list does not cover it, and the + * "uncovered dialects, uncovered on purpose" block below pins that gap so the + * scope sentence cannot quietly go false again. * * The negative half is the load-bearing half. A bare `includes('does not * exist')` would have matched "user does not exist" and started replacing @@ -162,6 +166,62 @@ describe('looksLikeInternalErrorLeak — shipped-dialect phrasings (#8132)', () }); }); +/** + * [#8739] The uncovered dialect, pinned as a MEASUREMENT rather than a + * paragraph. + * + * The module used to say nobody here runs MySQL, and a reviewer sizing a + * disclosure residual on PR #8737 quoted it in good faith. The claim was false: + * `driver-sql` branches on `mysql`/`mysql2`, CI stands up a live `mysql:8.0` + * for a required check, and live MySQL 8.0.46 measurements landed driver fixes + * (#8621, #8622). What survived correction is the narrower, still-true fact — + * this predicate does not COVER MySQL — and that is exactly the fact a reader + * needs and cannot get from a comment they might not read. + * + * ⛔ These assert `false`, and a `false` here is NOT a verdict that the text is + * safe: it is the predicate being SILENT on a dialect it never learned. The + * phrasing-independent answer is {@link declaresServerFault}, which is dialect- + * blind by construction. + * + * ⛔ **If a future PR teaches the list MySQL's spellings, these go red — that is + * the tripwire, not a broken test.** Do not delete the case to make it green. + * Come back here, and to `DIALECT_LEAK_PHRASINGS`' note, and update both to say + * what is then true. Extending the list is a behaviour change at three + * boundaries (it moves what gets suppressed), and #8739 leaves it parked behind + * an open product question: is MySQL a supported deployment target, or merely a + * tested dialect? Same shape as `metadata-protocol`'s + * `protocol.driver-text-disclosure.test.ts`, which pins its unmet dialects for + * the same reason. + */ +describe('looksLikeInternalErrorLeak — dialects the list does NOT cover (#8739)', () => { + it.each([ + // The tail PR #8737 keeps verbatim in the write-path log. Names an + // IDENTIFIER on MySQL, as SQLite's and Postgres' spellings do — which is + // why that PR's conclusion held even though its stated reason did not. + ['mysql unknown column', "Unknown column 'zzz_nonexistent_field' in 'field list'"], + // The same condition the Postgres/SQLite limbs above DO catch. + ['mysql missing table', "Table 'crm.sys_metadata' doesn't exist"], + // Value-bearing, and the reason "uncovered" is worth pinning: MySQL puts + // a CALLER'S VALUE in this diagnostic. `isUniqueViolationError` + // (`unique-violation.ts`) is the predicate that does recognise it; this + // one does not, and the two answer different questions on purpose. + ['mysql duplicate entry', "Duplicate entry 'acme@example.com' for key 'idx_email_unique'"], + ])('is silent on %s — false here means UNCOVERED, never "safe"', (_label, message) => { + expect(looksLikeInternalErrorLeak(message)).toBe(false); + }); + + /** + * The other half of the same measurement: the dialect is uncovered, but the + * declaration channel is not. A boundary that also asks + * {@link declaresServerFault} withholds the identical text. + */ + it('withholds the same uncovered text through the declaration channel', () => { + const mysqlDump = { status: 500, code: 'DATABASE_ERROR', message: "Table 'crm.sys_metadata' doesn't exist" }; + expect(looksLikeInternalErrorLeak(mysqlDump.message)).toBe(false); + expect(declaresServerFault(mysqlDump)).toBe(true); + }); +}); + /** * [#5811] The declaration half. `looksLikeInternalErrorLeak` asks whether a * message SOUNDS internal; this asks whether the producer SAID it was a server diff --git a/packages/types/src/error-leak.ts b/packages/types/src/error-leak.ts index 779690732d..f813509823 100644 --- a/packages/types/src/error-leak.ts +++ b/packages/types/src/error-leak.ts @@ -36,8 +36,11 @@ export const INTERNAL_ERROR_MESSAGE = 'Internal server error'; /** - * [#8132] The phrasings of the dialects this repo actually RUNS, each anchored - * on the driver's own errmsg template rather than on its tail. + * [#8132] The dialect phrasings this list COVERS — the SQLite family and + * Postgres — each anchored on the driver's own errmsg template rather than on + * its tail. Coverage, not a census of what this repo runs: see "What this list + * covers, and what it does not" below, which is the load-bearing half for + * anyone sizing a disclosure residual. * * The gap that forced these: the keyword set below caught SQLite's * `SQLITE_ERROR: no such table: sys_metadata` through the `sqlite_` limb, while @@ -53,13 +56,39 @@ export const INTERNAL_ERROR_MESSAGE = 'Internal server error'; * not — a quoted identifier, or the trailing colon of SQLite's template. The * negative cases in `error-leak.test.ts` pin that distinction. * - * **Why the list stops here.** The module note above argues against growing a - * driver taxonomy, and it is right that the list is unbounded *across dialects* - * — MySQL/MSSQL/Oracle each phrase all of this differently and nobody here runs - * them. These are not a census: they are the two engines `driver-sql`, - * `driver-turso` and `driver-sqlite-wasm` actually reach. A dialect this repo - * does not run gets no entry, and {@link declaresServerFault} remains the - * answer that does not depend on phrasing at all. + * **What this list covers, and what it does not.** The module note above argues + * against growing a driver taxonomy, and that reason still holds on its own: a + * phrasing list is unbounded *across dialects*, because every dialect spells + * every one of these conditions its own way. So these entries are a COVERAGE + * statement, not a census — they are the two spellings #8132 measured the gap + * on, and {@link declaresServerFault} remains the answer that does not depend + * on phrasing at all. + * + * ⚠️ **This list's silence is NOT evidence that a dialect is unreachable.** + * Until #8739 this paragraph said "nobody here runs" MySQL/MSSQL/Oracle, and a + * reviewer sizing a disclosure residual read it as one. It was false for MySQL, + * measurably, on the same tree: + * + * - `driver-sql` branches on `mysql`/`mysql2` — the `isMysql` getter, + * `withUtcSession`, and the `dialect === 'mysql'` arms of + * `textMatchPredicate` / `likePatternPredicate`. + * - CI stands up a live `mysql:8.0` service for the job named + * `Temporal Conformance (live PG + MySQL)`, which IS a required check, and + * its `OS_EXPECT_LIVE_DIALECT_MATRIX` flag turns a missing MySQL URL into a + * named red rather than a quiet skip. + * - Live MySQL 8.0.46 measurements produced merged driver fixes (#8621, + * #8622), and `unique-violation.ts` — one file over — names sqlite / + * postgres / mysql as the three dialect families `sql-driver.ts` recognises. + * + * Whether MySQL is a **supported deployment target** or merely a **tested + * dialect** is an open product question (#8739); this file does not answer it, + * and neither answer changes the rule a reader needs. What is true either way, + * and is the only thing to carry away: on a MySQL deployment this predicate is + * SILENT, not clearing. Its phrasings of these same conditions — + * `Unknown column 'c' in 'field list'`, `Table 'app.t' doesn't exist`, + * `Duplicate entry 'x' for key 'i'` — all return FALSE here, pinned in + * `error-leak.test.ts`. Adding them would change what gets suppressed at three + * boundaries, so it belongs to that decision, not to a comment. * * ⚠️ Related but NOT reusable: `relation-sub-object.ts` owns the same Postgres * sentence for two other questions (which column? / is this a sub-object?), and @@ -94,7 +123,10 @@ const DIALECT_LEAK_PHRASINGS: readonly RegExp[] = [ * (a message that *starts* as `SELECT`/`INSERT INTO`/`UPDATE`/`DELETE FROM` — * drivers prefix the offending SQL to their message), constraint-violation * dumps, which name physical tables and columns, and the - * {@link DIALECT_LEAK_PHRASINGS} of the engines this repo ships. + * {@link DIALECT_LEAK_PHRASINGS} the list covers — the SQLite family and + * Postgres. A dialect outside that coverage (MySQL is reachable here, and is + * not covered) makes this return FALSE without meaning the text is safe; read + * {@link DIALECT_LEAK_PHRASINGS}' note before sizing anything on a `false`. * * Does NOT match ordinary business or validation messages, which is why the * statement forms are anchored with `startsWith` and the dialect phrasings on