fix: row editing wrote to the wrong row, and eight other defects measured on live engines - #967
yusuf-gundogdu wants to merge 18 commits into
Conversation
… one row The key an inline edit builds its WHERE on is a guess: the first field called id or ending in _id. On a result that carries a foreign key rather than the table's own key - SELECT category_id, product_name FROM products - the guess lands on category_id, and the UPDATE rewrites every product in that category. Measured on PostgreSQL 16: one cell edited, fifteen rows changed, one statement reported as accepted. Two questions now stand between an edit and a write, and both had to be asked. IS IT THE TABLE'S COLUMN. A name is not a provenance. SELECT ROW_NUMBER() OVER (ORDER BY product_name) AS product_id, product_name FROM products puts 1, 2, 3 in a field called product_id; products really has a product_id; and the UPDATEs land on whichever products those are, not on the rows on screen. Measured, two cells edited, two rows written, neither of them visible, both reported accepted. SELECT sku AS product_id is the same thing spelled shorter. selectsPlainColumn reads the select list and requires the field to be the column itself - a star, or a reference whose alias, if any, names what it already names. DOES IT ADDRESS ONE ROW PER VALUE. A grouped count over the distinct keys, bound rather than interpolated: as many distinct keys as rows on screen, as many groups back as distinct keys, every group holding one row. Each clause replaced a version measured letting a write through. Counting per row sent IN (87, 87, 87) for three rows sharing order_id 87 and wrote all three to all three. An ungrouped total passed abc and ABC on MySQL's case-insensitive collation, where WHERE k = 'abc' writes to both. String() alone collapsed SQLite's text '1' and integer 1, and two MySQL BIGINTs past 2^53 that mysql2 rounds to the same number. The check goes to /api/db/transaction when a transaction is open, because that is where the UPDATEs go: asked on a pooled connection it cannot see a row the transaction has not committed, and the apply refuses for ever with a sentence that is false about a row the user is looking at. It asks for one more row than there are keys, because the default page is 500 and groups that fell off would read as rows that are gone. A key that cannot be read as text at all - a value with a null prototype - is refused before anything is built, where it used to throw as the statement was assembled and take the apply down with no write and no message. A null key is refused too. Fewer rows than keys gets its own sentence, because it says nothing about whether the column tells them apart. A check that could not be run is not a check that passed, and the pending edits are kept in every case.
… a diff is asked for #884 moved Current Schema off the explorer's cached copy and onto a read of the connection, but that read sits in an effect keyed on [connection] alone, so it happens once and not again while the panel stays open. The sequence the Diff tab exists for - snapshot, change the database, compare - still answered "No differences found". Measured against PostgreSQL 16 with the panel left open. Two moments read the connection now, and between them they are the sequence: a snapshot reads it, and choosing a target reads it again, because that is when a person asks to be told the difference. Freshening only the snapshot was tried first and is not enough - the other side stays at the moment of the snapshot. Two snapshots compared against each other read nothing: neither side is the database. Three reads can be in flight at once, so they are sequenced through useReadGeneration, which the repository already states once for this problem. It replaces comparing the connection OBJECT, which is not safe: activeConnection is a useMemo over a prop the embedded host supplies, so a host handing over a fresh array per render produces a fresh object per render and a read would be discarded on a connection that never changed. The rest is what an awaited read in a click handler needs and did not have: a re-entrancy guard in a ref, because two Enter presses land in the same tick; setSnapshotting(false) in a finally, or a superseded read leaves the button reading Reading... for the life of the panel; the label input and Cancel disabled while the read is out; the storage write inside the try, because a localStorage quota refusal is an ordinary outcome for a whole schema; and a snapshot that was overtaken saying so rather than returning quietly, which saved nothing and said nothing while the button went back to Save. The failure banner carries the connection it was about, so a failure on a database the user has left does not sit over the one they are looking at, and a later read of that database that works clears it.
Eleven of them, all copy-and-run, and two guards so the twelfth cannot get in quietly. The Koyeb deploy button prefilled ADMIN_PASSWORD and USER_PASSWORD with set_a_real_password. That reads as a placeholder and works as a password: measured against a running container, the standard user account signed in with it. There is no placeholder that fixes it, so the button carries neither, and USER_EMAIL goes with them. Unset, the two behave differently and both answers are safe: ADMIN_PASSWORD is generated on first run and printed to the log, while USER_PASSWORD is never generated - getAuthUsers adds that account only when it is set, so without it there is no second account at all. .env.example was the worst of them, because README says to copy it and run it: ADMIN_PASSWORD, USER_PASSWORD and a 36-character JWT_SECRET, which clears the 32-character minimum and is also what saved connection passwords are sealed with when STORAGE_ENCRYPTION_KEY is unset. README's own docker run block had LibreDB.2026 twice with the same string underneath as the login to use, and the same 36-character secret, which was in docs/DISTRIBUTION.md twice and in packaging/linux/env, installed to /etc/libredb-studio/env by every .deb and .rpm. DOCKERHUB.md is the Docker Hub landing page and carried a password through both its examples. docs/SEED_CONNECTIONS.md carried MyAdmin123 and MyUser123 through both of its. README's env block, docs/MFA.md, docs/DISTRIBUTION.md and CONTRIBUTING.md carried your_secure_admin_password and admin123, which are the same thing wearing politer names. All of them are empty now rather than replaced, and each block says where the generated password is printed. The one secret that remains is under the minimum, so a deployment left as it stands stops at boot and says why. Two guards, because three of these were removed by hand this week and nothing stopped the next: published-credentials.test.ts fails on any ADMIN_PASSWORD or USER_PASSWORD assignment and any JWT_SECRET of 32 characters or more across README.md, DOCKERHUB.md, docs/ and deploy/; and the Koyeb test now parses every Koyeb URL in the file with URL rather than reading the first matching line with a regular expression over one spelling. It caught one of four ways of putting a password back before that and catches all four now. Not touched: --set secrets.adminPassword=MyAdmin123 in twenty-one places of Helm documentation. The chart's values are empty and the template requires them, so nothing is carried into a deployment - the reader types it and can see they are typing a password.
… row mysql2 hands any integer beyond 2^53 to JavaScript as a rounded number. Two rows whose ids differ in the last digit therefore arrive in the browser as the same value, and the inline editor's key guard is satisfied by that: it asks the engine about the rounded id, gets one row back, and sends the UPDATE. Measured against MySQL 8.4.11 with the real provider: the statement landed on the neighbouring row. buildPoolConfig now sets supportBigNumbers on the base config, so it covers both the form-fields path and a pasted connection string. Only values mysql2 judges too large come back as strings, and its threshold is anything above Number.MAX_SAFE_INTEGER, so 2^53 exactly is a string too even though its value was never in doubt. Everything narrower is untouched, measured one at a time: a small INT, COUNT(*), SUM over small values, a BIGINT holding a small value, an AUTO_INCREMENT id, DECIMAL, and 2^53 - 1. One other shape changed, and it was already broken: an UNSIGNED BIGINT at the top of its range read back as 18446744073709552000, which is not the stored value. It is now the digits the row holds. bigNumberStrings was deliberately not set: it would turn every BIGINT into a string, including the small ones.
The same hole as MySQL, and worse on the driver the Docker image runs. Measured with the real provider on both: bun:sqlite silently rounded 9007199254740993 to ...992 and the inline editor then updated the neighbouring row, while node:sqlite threw ERR_OUT_OF_RANGE and refused to read the row at all. Two spellings of the same defect, one of them silent. Turning on the drivers' big-integer flag alone is not the fix. Measured: every integer becomes a BigInt, including 1 and COUNT(*), rows are sent to the browser as JSON, JSON.stringify refuses BigInt, and the connection stops opening. 180 passing became 149 passing with 31 failures, 16 of them the database failing to open. So the flag is on for both drivers, spelled differently for each, and the conversion happens at the provider boundary: a value that fits comes back as a number, a value that does not comes back as its decimal string, and no BigInt leaves the provider. The boundary is Number.MAX_SAFE_INTEGER, which is what the libsql driver already uses. Small integers are unchanged, including COUNT(*), rowid, length(), CAST and PRAGMA columns, measured one at a time. Reading correctly then opened the other half. A column with no declared type, or declared BLOB, has no affinity, and SQLite never compares a string equal to an integer, so sending that id back matched nothing and the user was told no rows changed. toSQLiteBindValue makes the bind side the exact inverse of the read side: only a digit string the read side could itself have produced goes back as a 64-bit integer. A leading zero, a leading plus, whitespace, a trailing .0, exponent form, the empty string, anything inside the safe range and anything wider than 64 bits all stay text, so a genuinely textual key still behaves as text.
The read side was already right here: decodeInteger returns a value outside the safe range as a string, and the transport's own type admits it. The send side was not. encodeValue passed that string through as text, and a column with no affinity never compares text equal to an integer, so the row could be read and then not updated. Measured against sqld 0.24.33 in a container, with the real provider: reading 9007199254740993 and sending it back changed one row in an INTEGER column and one in a TEXT column, and zero rows in a BLOB column and in a column with no declaration at all. The user was told nothing had changed. encodeValue now mirrors decodeInteger through isDecodedInteger: only a digit string the read side could itself have produced is sent as an integer. Measured on the live server, a leading zero, a leading plus, whitespace, a trailing .0, exponent form, the empty string, a value inside the safe range and one wider than 64 bits all stay text, and so does a genuine text key that happens to be digits.
…e guard .env.example carried STORAGE_ENCRYPTION_KEY as a commented line reading your_32_character_random_string_here. It looks like a placeholder and it is thirty-six characters, which clears the thirty-two-character minimum, so it works. A copy left untouched is not affected, because the line is commented out. What the file invites is uncommenting it, which is what a commented example is for, and the reader who does that seals every saved connection password with a key published in this repository. It now reads too-short-generate-your-own, twenty-seven characters. With server-side storage on - STORAGE_PROVIDER set to sqlite or postgres - uncommenting it as it stands stops the server at startup and says the key is too short. The file ships STORAGE_PROVIDER=local, where the key is never read, so on the settings it ships with nothing changes either way. The comment above the line states that condition and gives the command to generate a real key. The guard that was added with the credentials removal only looked at README.md, DOCKERHUB.md, docs/ and deploy/. Measured against nine ways of putting a credential back, it missed most of them. It now also reads render.yaml, the root Dockerfile and .github/workflows, accepts .txt as well, catches the camelCase adminPassword and userPassword spellings, catches STORAGE_ENCRYPTION_KEY, catches a value followed by another flag or a comment on the same line, and catches a key and value split across two lines. All nine attempts are caught and ten innocent shapes stay silent.
…y edited
Every refusal the inline editor can show was read against the situation that
fires it. Four of them were saying something that was not true of what was in
front of the user, and all four were measured live on PostgreSQL 16.15 and MySQL
8.4.11 through the real provider.
A key holding binary data got "some of the rows you edited are no longer in the
table. Run the query again." The rows were still there, and running the query
again produced the same unusable key, so the advice looped forever. A bytea, a
BINARY(16) and the {type:"Buffer"} shape a driver puts on the wire are now
refused before the engine is asked at all, with a sentence about the value
itself.
A date-time key was fixed only on the path the browser does not use. Over
/api/db/query a Date becomes an ISO string, the check saw a string and let it
through, and the old false sentence came back with count(*) sitting at 2. The
check now reads the column type the result already carries rather than the look
of the value, so a column that genuinely holds the text of a date still works,
proven by editing one end to end.
A fractional key was refused as a value that "does not reach the table as the row
holds it". Measured: 0.30000000000000004 in a double precision column reached the
table exactly, and WHERE found the row. It is now written, and what is still
refused is refused with a sentence that is true of it.
A large-magnitude float was refused as an out-of-range integer. 1e21 sits exactly
in a 64-bit float and both engines matched the row. The out-of-range refusal is
right for an integer column, where past 2^53 the browser holds a different value
than the row does, so the decision is now made on the declared column type rather
than the size of the number.
One refusal was also half-blind: when the engine answered for fewer keys than
were sent it said only that the key does not tell the rows apart, and dropped the
more important half. It now says both, that a row may have gone since the read
and that the engine may fold two keys into one, without asserting either.
Eight defects in the comparison panel, each measured before it was changed and each pinned by a test that fails without the fix. A snapshot whose read failed used to clear its own warning on the next render, so the user saw the panel go quiet and had no way to know nothing had been saved. The warning now stays until it is dismissed, and there is a Dismiss button to do it with. Leaving the Diff tab unmounts the panel, and a read still in flight went on to write its snapshot afterwards. The cleanup supersedes both counters now, and four state writes that ran after an await are guarded, so nothing the panel started touches storage or its own state once it is gone. Pressing Enter twice was guarded but the guard was measured by nothing: removing it left every test green. Without it the database is read twice, one snapshot is saved, and a "Press Save again" banner appears that the user did nothing to earn. Both halves are now tested. Choosing the target that was already chosen re-rendered nothing and so re-read nothing, and there was no refresh control anywhere, which left leaving the tab and coming back as the only way to see current data. There is a Refresh button, its read goes through the same generation counter, and two quick clicks start one read rather than two. Snapshot ids came from Date.now(), so two taken in the same millisecond shared an id: React warned about duplicate keys and deleting one deleted both. Worse, the store keeps fifty, and when a selected snapshot fell off the end the lookup returned nothing and an empty array was handed to the diff, which then reported that every table in the database had been removed. Ids come from the repository's own generator now, and a snapshot that cannot be found says so instead of being drawn as an empty database. The remote fetch was sequenced for its writes but not for the busy indicator: a superseded read could take "Fetching..." down while a newer one was still outstanding. And choosing a stored snapshot did not supersede a remote read at all, so a fetch the user had abandoned could land afterwards and take the target back, with the indicator left hanging. Both orderings now end with the panel showing the thing the user chose last.
…ched A failed remote fetch said nothing at all. "Fetching..." disappeared, the target stayed where it was, and the error went to the log. The comparison on screen was still the old one, and nothing told the reader it was not the database they had just asked for. The failure now appears in its own strip, built the same way as the snapshot failure strip and dismissed the same way. It keeps its own state rather than sharing the snapshot one, because that strip is tied to the connection on screen while this is about a different database, and pressing Save should not clear it. Only the fetch the user is waiting on can raise it: a superseded fetch that fails late stays silent, which is checked by the same generation counter that already guards the writes. The guard on the mount-time read was measured before this change and caught by nothing.
An earlier commit on this branch removed SQL Server from the list of engines agent mode reads. That was wrong and it was unrelated to what the commit was for. mssql.ts implements queryReadOnly, and engine-support.ts lists four engines, not three, which is what the engine-support test reads from the real providers. It was also applied unevenly, so the documents contradicted each other: README, DOCKERHUB and FEATURES said three engines while AGENT_GUIDE and the Chinese, Japanese and Hindi READMEs still said four. FEATURES went further and listed three engines in one sentence and then called them "those two engines". The English text is restored and the sentence that counted them is right again. The four-layer read-only profile SQL Server needs, which has no read-only transaction of its own, is described where the other three engines' profiles are. README also carried four source references with line numbers that do not hold those symbols - postgres.ts:915, sqlite.ts:537, duckdb/index.ts:525 and runtime.ts:199. The line numbers are gone rather than corrected, because they go stale on the next edit and the file name alone is enough to find the symbol. The Spanish and Urdu READMEs never carried this claim, so nothing changed there.
The coverage gate was red at 59092 of 59094 lines. The two uncovered lines were the scalar-bigint and typed-array branches of the driver's record normalizer, both added by the 64-bit integer commit on this branch, and the repository requires every line. The seam they sit on is typed unknown deliberately: the statement interface declares all() as unknown[] and get() as unknown, so what arrives is whatever the injected driver returns. Measured against both shipped drivers, for all(), get(), run(), a miss and a PRAGMA read, every answer is a row object, a null miss or run()'s info object, and a BLOB is a cell inside a row rather than the record itself. So neither branch is on a path those two take today. They are what stops a driver that answers with a bare cell - a values mode, or the future row-returning method the module's own comment warns about - from handing a BigInt out of the provider or walking a typed array cell by cell. They are driven through the injectable constructor the module already exposes for exactly this, so no driver is mocked and no source changed. Each test states what it kills: removing the scalar branch sends a BigInt through JSON.stringify, which refuses it outright, and removing the typed-array branch writes converted cells back into a typed array that will not hold them.
…e four holes in the guard A reviewer put working passwords into this repository four ways and the guard that is supposed to stop that stayed green each time. Every one is now caught, with the nearest innocent shape written beside it so the next person it stops does not delete it. A Markdown table row was the worst of them, because a variable table is how these files list their settings: the name is one cell and the value is the next, and nothing between them is an assignment. Only a value cell written as a code span with no space in it counts, which is what tells the value column from the description column beside it. The other three were one rule being too strict. `NAME=value` had to be followed by a comment, a pipe, another flag or the end of the line, so `docker run -e ADMIN_PASSWORD=Secret123 imagename` and `export ADMIN_PASSWORD=Secret123 && echo` both read as prose. A sentence does not write an equals sign, so an `=` no longer asks what follows it; `NAME: value` still does, which is what keeps `ADMIN_PASSWORD: generated on first run` a sentence. And a quoted value was read to the first space, so a password with a space in it looked like one word followed by prose - the same hole hid a secret at full length, and a secret is judged by its length. One value had to be excused: deploy/azure/src/install.sh writes `printf 'ADMIN_PASSWORD=%s\n'` and pours the password in from a variable. A run made entirely of format specifiers is the hole, not the value. docs/API_DOCS.md was still handing readers admin123 in two login examples, a cURL block and a fetch block, which is the same string removed from CONTRIBUTING.md by hand earlier. They now carry a placeholder and a line saying the real password is generated on first run and printed to the log. The guard reads login bodies now, found by the `email` key beside the password. Connection bodies are deliberately left alone: the password in one of those is the reader's own database, sampled as postgres or password123, and nothing here is reachable with it.
… its integer column
Every other provider sends the declared column types alongside the rows. The local
SQLite provider never did, and until this branch nothing noticed: the exporter
falls back to the JavaScript type of the value, and a SQLite integer used to
arrive as a number.
The 64-bit fix on this branch changed that. An id past 2^53 now arrives as a
string, so a table carrying one exported as `"id" TEXT` with the value in quotes,
and a user moving that table to another engine got a text column where they had an
integer one. Measured: exported, restored, and `typeof(id)` came back `text`.
libsql was unaffected the whole time, because it reports its declarations.
This follows the libsql path exactly, down to omitting the key when the map is
empty rather than sending `{}`. The two drivers spell the declarations
differently, so one read-only accessor is bridged in sqlite-driver.ts the way
`safeIntegers` and `inTransaction` already are, and nothing new reaches a shared
surface.
It is read after the rows, not before, because bun's `declaredTypes` raises until
the statement has run. bun also publishes `columnTypes`, which is not this: it is
the runtime class of the value, so a REAL column reads FLOAT and an undeclared one
reads INTEGER, and it raises on `PRAGMA journal_mode`. Measured on Bun 1.4.0 and
not used.
An absent declaration stays absent. An expression, a literal, an aggregate, a
function call, a PRAGMA column and a column declared with no type all get no
entry, because two decisions on this branch now read this map and a guess in it
would be worse than nothing.
Not fixed here: a SQLite REAL key is still refused by the row editor, which asks
whether the column is a 64-bit float and does not count `real` because on
PostgreSQL and Trino it is 32 bits. On SQLite it is 64. That answer belongs in the
editor, with the engine in hand.
…there The editor asks whether a fractional key's column is a 64-bit float, because a value read at a different width than the row holds can miss the row or write to the wrong one. `real` and `float` were left out of the safe list, and that was right for the engines it was written against. SQLite has no 32-bit float. REAL is the only float width it has, and FLOAT, DOUBLE and DOUBLE PRECISION are accepted spellings of the same storage class. Measured live: all four report the same decltype, `typeof()` answers `real` for each, 0.30000000000000004 reads back exactly, and `WHERE k_id = 1.5` matched one row while the editor was refusing that same edit. The sentence it gave - that nothing said the column was a 64-bit float - was false, because the declaration said so. This only became visible now that the SQLite provider reports its declarations. So the decision reads the engine as well as the name, from the connection the caller already holds. Nothing new is threaded through the module. Each engine was measured rather than assumed. PostgreSQL `real` and `float4` are four bytes by `pg_column_size` and `0.1::real::float8` is 0.10000000149011612, so they stay refused. MySQL FLOAT compared false against 0.1 in the same row where DOUBLE compared true, so FLOAT stays refused. DuckDB hands 32-bit values over as 0.10000000149011612, so REAL and FLOAT stay refused. libsql is SQLite and behaves as SQLite, checked against a running server. Trino REAL and SQL Server `float` could not be reached from here, so both keep the refusal: an unmeasured engine gets the closed side, which is what this rule chose in the first place.
… body as it is written
An adversarial reviewer broke this guard three ways and proved each by reproduction.
It read only the cell immediately after the name, and only a backticked span. The
real tables here are shaped Variable, Required, Description, so the value lives in
the third cell, written inside the description as "(default: LibreDB.2026)". Seven
shapes of that got past, including the plain and bolded spellings.
Reading more cells is what made the second finding possible: docs/HELM_CHART.md
has a Source column whose cells are chart value paths, so a row there reported
secrets.adminPassword as a published password, and
secrets.jwtSecret.fromExistingSecret is thirty-seven characters, which would have
failed the secret-length test outright. A guard that fires on innocent text is the
one that gets deleted by the next person it stops.
So position is not what decides a cell any more: the table's own header does. A
column headed Value, Default or Example holds values; a "default: x" written in
words is an assignment wherever it appears; a two-column table's single marked
cell is a value; with no header at all the cell after the name is read, as before.
A value that spells the variable's own name back - secrets.adminPassword,
admin-password - is a reference, not a value.
The third was the JSON rule requiring double quotes around the email key. The
file it was written for does not use them: docs/API_DOCS.md wrote
`{ email: 'admin@libredb.org', password: 'admin123' }`, a bare key and single
quotes, and that one escaped while its sibling was caught. The test that claimed
to cover "the two shapes in the file" had hand-converted this one to double
quotes - a line that never existed there. It now uses the real shape.
Measured rather than argued: a credential line injected into README.md,
DOCKERHUB.md, docs/API_DOCS.md, deploy/railway, deploy/koyeb and docs/HELM_CHART
is caught in all six, and six innocent rows added to the real HELM_CHART table
stay silent in all six. Eight tests became ten and none of the eight was weakened.
… now does docs/providers/README.md says the code, the document and the provider's integration test move together in the same pull request. This branch changed three providers and left their documents behind, and one of them now said something false. mysql.md said a BIGINT written as 9007199254740993 comes back as 9007199254740992. That was true until supportBigNumbers was set. Measured again on MySQL 8.4.11 with mysql2 3.24.4, both protocols: without the flag both rows read back 9007199254740992 and the unsigned ceiling read 18446744073709552000; with it they are the digits the rows hold. INT, COUNT(*), AUTO_INCREMENT and 2^53 - 1 are still numbers, and 2^53 exactly is a string, because mysql2's threshold sits above the safe range. sqlite.md had nothing about either change, and one of them is a new capability. The document now carries the 64-bit integer work in both directions - bun:sqlite rounded silently while node:sqlite raised, the boundary is the safe-integer limit, and a text bind matched no row in a BLOB or untyped column where an integer bind matches one - and the declared column types this provider never used to report, including why they are read after the rows rather than before and why bun's columnTypes is not the same thing as its declaredTypes. libsql.md described only the reading half of section 3.4. The send side is measured against a live sqld 0.24.33 and written beside it: the round trip changes one row in all four column types now, where a text bind used to change none in a BLOB or untyped column. Every claim was measured for this document rather than copied from the commit that made the change. BACKLOG's count of the drivers that fill columnTypes is updated with it, since SQLite is now one of them.
The guard that stops a working password being published has to contain password-shaped strings, because that is its subject. It carried six that read like real ones, and a secret scanner on the pull request reported all six. They are renamed to values that say what they are - example-not-a-real-password, example-fake-login and the rest - and each was measured afterwards to confirm the guard still reads it as a value rather than as a stand-in. That distinction is the whole risk in this change: the guard deliberately ignores a leading dollar sign, braces, angle brackets and a `your-` prefix, so a fixture that drifted into one of those shapes would leave the test green while measuring nothing. Turning one fixture into a stand-in was tried and drove a test red, which is what the renaming had to preserve. The two length-dependent assertions are untouched and still exact at forty and thirty-two characters, because a secret is judged by its length. Two sentences elsewhere spelled a removed password out while explaining that a value reading as a placeholder still works as one - one in the Koyeb deploy notes and one in its test. The sentences are gone; the paragraph around them and the assertion they described are unchanged. Ten tests before, ten after, ninety-nine assertions in both.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| 36754985 | Triggered | Generic Password | 3d000a4 | tests/unit/published-credentials.test.ts | View secret |
| 36754985 | Triggered | Generic Password | 917a257 | tests/unit/published-credentials.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secrets safely. Learn here the best practices.
- Revoke and rotate these secrets.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
|
Superseded by #968, which carries the same content with the credential-shaped test fixtures renamed in every commit rather than only the last. |



Every defect here was reproduced on a running engine before it was changed, and each fix is held by a test that fails without it.
The row editor wrote to the wrong row
Three drivers handed integers past 2^53 to JavaScript rounded, so two rows whose ids differ in the last digit arrived in the browser as the same value. The inline editor's key guard asked the engine about the rounded id, got one row back, and sent the UPDATE to the neighbour. Measured on MySQL 8.4.11 and on both SQLite drivers;
node:sqliteraisedERR_OUT_OF_RANGEinstead, which is the same defect wearing a louder spelling.MySQL now sets
supportBigNumbers. SQLite and libsql convert at the provider boundary: a value that fits comes back a number, one that does not comes back its decimal string, and no BigInt leaves a provider. The bind side is the exact inverse, so only a digit string the read side could itself have produced goes back as a 64-bit integer, and a leading zero, a leading plus, a trailing.0, exponent form and anything wider than 64 bits all stay text.Six refusal messages were untrue of what fired them
The editor refuses an edit it cannot address safely, and six of its sentences said something false about the value in front of the user. A binary key was told its rows were gone from the table while they were still there, with advice that looped forever. A date-time key was fixed only on the path the browser does not use. A fractional key was refused as unreachable when the engine matched it exactly. A large float was refused as an out-of-range integer. A grouped-count answer reported half of what it knew.
The decisions now read the declared column type carried with the result, and the engine as well as the type name, because
realis 32 bits on PostgreSQL and 64 on SQLite. Each engine was measured rather than assumed; the ones that could not be reached keep the refusal.The schema diff panel
Eight defects: a snapshot failure that cleared itself silently, a read that wrote after the panel was unmounted, a double-Enter guard nothing measured, no way to re-read without leaving the tab,
Date.now()snapshot ids that collided so deleting one deleted two, a missing snapshot drawn as an empty database so the panel reported every table removed, a busy indicator a superseded read could clear, and a remote fetch that could take the target back after the user had chosen something else. A failed remote fetch now says so instead of going quiet.Published credentials
Eleven working credentials were removed from the documentation, and the example env file's encryption key is now below the minimum so uncommenting it stops the server rather than sealing saved passwords with a published key. A guard fails the build on the next one: measured against a credential injected into six real files, and silent on six innocent rows of the real chart table.
Provider documents
docs/providers/{mysql,sqlite,libsql}.mdare updated in the same PR, asdocs/providers/README.mdrequires, with the measurements taken for those documents rather than copied.Checks
typecheck,format,lint,knip,build,build:lib,attw,chart:check,readme:check,security:check,channels:showcase:check,distribution:checkall pass. 569 test files, 18511 passing, and line coverage is 100 percent at 59114 of 59114.Three independent reviewers read earlier states of this branch and rejected it three times, for a red coverage gate, a documentation regression, two commit messages asserting measurements that did not hold, and four shapes of credential the guard let through. Those are the reasons the branch looks the way it does.