3535/** Generic replacement text for a message that trips {@link looksLikeInternalErrorLeak}. */
3636export const INTERNAL_ERROR_MESSAGE = 'Internal server error' ;
3737
38+ /**
39+ * [#8132] The phrasings of the dialects this repo actually RUNS, each anchored
40+ * on the driver's own errmsg template rather than on its tail.
41+ *
42+ * The gap that forced these: the keyword set below caught SQLite's
43+ * `SQLITE_ERROR: no such table: sys_metadata` through the `sqlite_` limb, while
44+ * the Postgres phrasing of *the same condition* —
45+ * `relation "sys_metadata" does not exist` — matched nothing and shipped a
46+ * physical table name to the client from every boundary that applies the
47+ * predicate.
48+ *
49+ * **Why anchored, and never on the bare tail.** `does not exist` is ordinary
50+ * business English: "user does not exist", "record does not exist". Matching
51+ * that substring would replace legitimate answers with `Internal server error`,
52+ * so each pattern requires what the DRIVER always emits and prose usually does
53+ * not — a quoted identifier, or the trailing colon of SQLite's template. The
54+ * negative cases in `error-leak.test.ts` pin that distinction.
55+ *
56+ * **Why the list stops here.** The module note above argues against growing a
57+ * driver taxonomy, and it is right that the list is unbounded *across dialects*
58+ * — MySQL/MSSQL/Oracle each phrase all of this differently and nobody here runs
59+ * them. These are not a census: they are the two engines `driver-sql`,
60+ * `driver-turso` and `driver-sqlite-wasm` actually reach. A dialect this repo
61+ * does not run gets no entry, and {@link declaresServerFault} remains the
62+ * answer that does not depend on phrasing at all.
63+ *
64+ * ⚠️ Related but NOT reusable: `relation-sub-object.ts` owns the same Postgres
65+ * sentence for two other questions (which column? / is this a sub-object?), and
66+ * its note warns that its two widths must never be collapsed. Neither answers
67+ * "is this a leak", and its central problem does not arise here: a message like
68+ * `column "label" of relation "sys_team" does not exist` contains a complete
69+ * missing-TABLE phrase as a substring, which is a hazard when you are deciding
70+ * WHICH object is missing and a non-issue when the verdict is "leak" either way.
71+ * That is why this asks its own question with its own patterns.
72+ */
73+ const DIALECT_LEAK_PHRASINGS : readonly RegExp [ ] = [
74+ // Postgres 42P01 / 42703 (and, as a superstring, the `… of relation "…"`
75+ // sub-object family: 42704 and friends). The quotes are required because
76+ // Postgres always emits them here.
77+ / \b (?: r e l a t i o n | c o l u m n ) \s + [ " ' ` ] [ ^ " ' ` ] + [ " ' ` ] \s + d o e s n o t e x i s t / i,
78+ // Postgres 42501. Restricted to physical object kinds: `schema`, `view`,
79+ // `function` and `column` are all ObjectStack AUTHORING vocabulary, so a
80+ // product message could legitimately use them and a miss is the cheap
81+ // direction (the outcome is already a 5xx).
82+ / \b p e r m i s s i o n d e n i e d f o r (?: t a b l e | r e l a t i o n | s e q u e n c e | d a t a b a s e ) \b / i,
83+ // SQLite/libsql, message-only form. The `sqlite_` limb below catches these
84+ // only when the driver prefixed its code; `better-sqlite3` and libsql both
85+ // raise them bare, which is the shape measured across this repo.
86+ / \b n o s u c h (?: t a b l e | c o l u m n ) : / i,
87+ ] ;
88+
3889/**
3990 * Whether `message` looks like a raw SQL statement or driver/engine dump that
4091 * must not be returned to an API client.
4192 *
4293 * Matches: dialect error codes (`SQLSTATE`, `sqlite_*`), bare statements
4394 * (a message that *starts* as `SELECT`/`INSERT INTO`/`UPDATE`/`DELETE FROM` —
44- * drivers prefix the offending SQL to their message), and constraint-violation
45- * dumps, which name physical tables and columns.
95+ * drivers prefix the offending SQL to their message), constraint-violation
96+ * dumps, which name physical tables and columns, and the
97+ * {@link DIALECT_LEAK_PHRASINGS} of the engines this repo ships.
4698 *
4799 * Does NOT match ordinary business or validation messages, which is why the
48- * statement forms are anchored with `startsWith`: a legitimate message may
49- * *mention* "update" without being one.
100+ * statement forms are anchored with `startsWith` and the dialect phrasings on
101+ * the driver's template: a legitimate message may *mention* "update", or say
102+ * "does not exist" about a business record, without being either.
50103 */
51104export function looksLikeInternalErrorLeak ( message : string | undefined | null ) : boolean {
52105 if ( ! message ) return false ;
@@ -60,7 +113,8 @@ export function looksLikeInternalErrorLeak(message: string | undefined | null):
60113 lower . startsWith ( 'delete from ' ) ||
61114 lower . includes ( 'constraint failed' ) ||
62115 lower . includes ( 'unique constraint' ) ||
63- lower . includes ( 'foreign key' )
116+ lower . includes ( 'foreign key' ) ||
117+ DIALECT_LEAK_PHRASINGS . some ( ( pattern ) => pattern . test ( lower ) )
64118 ) ;
65119}
66120
0 commit comments