security: encrypt credentials at rest, publish the posture page, and release 0.10.0 (Phase 3) - #324
Conversation
Per D4: once a stored value's first colon-separated segment matches a version tag (^v\d+$), it is classified as an envelope claim. Every way that claim can be malformed - wrong segment count, unrecognised version, bad IV, failed authentication - resolves to undecryptable, never to plaintext. Only a value whose first segment is not version-tag-shaped is plaintext. Per D3, the accepted cost is that an existing plaintext password shaped exactly ^v\d+: becomes unreadable after this ships.
…ntial-bearing ones
…t rotating the key costs
There was a problem hiding this comment.
Pull request overview
Adds server-side credential encryption, publishes a verified security posture, and prepares release 0.10.0.
Changes:
- Encrypts six credential fields using AES-256-GCM.
- Adds security posture documentation and CI drift checks.
- Updates application, Helm chart, and operator versions.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
.env.example |
Documents the encryption key. |
.github/workflows/ci.yml |
Adds the posture CI gate. |
SECURITY.md |
Updates support and storage security guidance. |
charts/libredb-studio/Chart.yaml |
Bumps chart and application versions. |
charts/libredb-studio/README.md |
Documents Helm encryption configuration. |
docs/BACKLOG.md |
Records encryption residual risks. |
docs/SECURITY.md |
Adds the security posture inventory. |
docs/STORAGE.md |
Documents encryption behavior and recovery. |
operator/bundle/manifests/libredb-studio-operator.clusterserviceversion.yaml |
Updates the operator bundle version. |
operator/config/manager/kustomization.yaml |
Updates the controller image tag. |
operator/helm-charts/libredb-studio/Chart.yaml |
Updates the operator-packaged chart. |
operator/helm-charts/libredb-studio/README.md |
Documents operator Helm configuration. |
package.json |
Bumps to 0.10.0 and adds the check command. |
scripts/security-check.mjs |
Implements posture drift validation. |
src/lib/storage/connection-secrets.ts |
Classifies and transforms credential fields. |
src/lib/storage/encryption.ts |
Implements encryption envelopes and key derivation. |
src/lib/storage/encrypting-provider.ts |
Decorates storage providers with encryption. |
src/lib/storage/factory.ts |
Installs encryption at the provider factory. |
tests/isolated/factory-singleton.test.ts |
Verifies factory encryption wiring. |
tests/security/credential-at-rest.test.ts |
Tests the at-rest threat boundary. |
tests/unit/lib/storage/connection-secrets.test.ts |
Tests field classification and transforms. |
tests/unit/lib/storage/encrypting-provider.test.ts |
Tests decorator behavior and warnings. |
tests/unit/lib/storage/encryption.test.ts |
Tests encryption, tampering, and keys. |
tests/unit/lib/storage/providers/postgres.test.ts |
Verifies PostgreSQL serialization. |
tests/unit/lib/storage/providers/sqlite.test.ts |
Verifies SQLite serialization. |
tests/unit/security-check.test.ts |
Tests posture-check failure modes. |
Suppressed comments (1)
docs/SECURITY.md:42
- Control 3.1 omits the factory choke point and its wiring test.
credential-at-rest.test.tswraps the capture provider directly, so it remains green ifgetStorageProvider()later returns a bare provider—the exact regression the PR saysfactory-singleton.test.tsguards mechanically. Link bothfactory.tsand that isolated test so the published posture covers installation of the decorator, not only its behavior in isolation.
| 3.1 | Credentials are encrypted at rest in the server-side store | Implemented | [`src/lib/storage/encryption.ts`](../src/lib/storage/encryption.ts), [`src/lib/storage/connection-secrets.ts`](../src/lib/storage/connection-secrets.ts), [`src/lib/storage/encrypting-provider.ts`](../src/lib/storage/encrypting-provider.ts) | [`tests/security/credential-at-rest.test.ts`](../tests/security/credential-at-rest.test.ts) |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
… pass it through sealIfPlaintext only encrypted the 'plaintext' outcome of readSecret and returned the other two outcomes verbatim. That is correct for 'decrypted' (already-sealed, skip re-encryption) but wrong for 'undecryptable': a real password shaped like an envelope claim, e.g. literally v2:hunter2, classifies as undecryptable and was being written to SQLite/PostgreSQL in the clear - exactly what this phase promises cannot happen. Fix: only skip encryption for a value PROVEN to open under the current key (kind === decrypted). Plaintext and anything merely envelope-shaped, including a corrupted three-segment claim, are both sealed. The write path is fed from localStorage through useStorageSync, which holds plaintext by design, so a value reaching here is overwhelmingly a real credential rather than corruption in transit. Adds regression tests for both shapes (two-segment and three-segment lookalikes) and corrects the encryptConnections and sealIfPlaintext doc comments to state what is now actually true.
…licate control ids isExecuted treated every non-test path as executed, so a control could name the checker script or the policy document itself as its own Verified by link and the drift guard would accept it - 0.4 and 0.5 both did exactly this. Added requireTest to isExecuted and apply it only to the Verified by column (the Enforced in column legitimately links source files); a non-test target there now fails as 'not a test'. Added real verifier tests for both claims: tests/unit/security-check.test.ts gained a CLI-level case that runs the checker against the actual repository (the real proof behind 0.4, 'the security policy states only what the code does'), and tests/security/vulnerability-disclosure.test.ts reads the root SECURITY.md to verify 0.5's reporting-channel and response-time claim. Both control rows now link these instead of a non-test path. Also detects a duplicate control id: the exact-control-set check only compared presence, so a page carrying all 16 ids plus a second 0.1 produced neither a missing nor an extra id and passed. Added an explicit duplicate check plus a sabotage test. Finally, scoped control 3.2's claim to authoritative (server-generated) audit events: the row claimed every audit event reaches stdout, contradicting the note directly below it that documents POST /api/admin/audit deliberately writing only to the ring buffer, because its body is fully client-supplied.
Both chart copies carried artifacthub.io/containsSecurityUpdates: "false" on a release that is four phases of security work. Set to "true" in the source chart and re-ran chart:bump to refresh the operator's vendored copy.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 27 out of 27 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
operator/helm-charts/libredb-studio/README.md:256
- This vendored README has the same invalid chart reference: the configured repository alias is
libredb, notlibredb-studio, so this install command cannot resolve the chart. Keep the generated copy aligned with the source README's corrected command.
helm install libredb-studio libredb-studio/libredb-studio \
tests/unit/lib/storage/encrypting-provider.test.ts:55
- These warning tests assume encryption falls back to
JWT_SECRET, but the setup leaves any ambientSTORAGE_ENCRYPTION_KEYin place. In such an environment changingJWT_SECRETat line 115 does not change the encryption key, so the expected undecryptable warning never occurs. Snapshot, clear, and restore the explicit key as the neighboring encryption suites do.
beforeEach(() => {
snapshot.JWT_SECRET = process.env.JWT_SECRET;
process.env.JWT_SECRET = "encrypting-provider-test-jwt-secret-32";
resetStorageEncryptionKey();
charts/libredb-studio/README.md:256
- This command uses the repository alias
libredb-studio, but this README tells users to add the repository aslibredb(line 16), and every other install example useslibredb/libredb-studio. As written, the copy-paste command fails with an unknown repository; use the documented alias.
helm install libredb-studio libredb-studio/libredb-studio \
…ually holds docs/SECURITY.md and docs/STORAGE.md both claimed credential encryption at rest protects a stolen backup or volume snapshot. For STORAGE_PROVIDER=sqlite with no STORAGE_ENCRYPTION_KEY set, that is false: the fallback key derives from JWT_SECRET, persisted in auth-bootstrap.json beside the SQLite file (both resolve through the same getDataDir(), and the Helm chart mounts that one directory as a single /app/data volume), so a snapshot of it carries the ciphertext and the key that opens it side by side. Narrowed both claims to a stolen database file or dump on its own, and stated plainly what closes the gap: set STORAGE_ENCRYPTION_KEY from outside the mounted volume (a Kubernetes Secret, an environment variable). postgres deployments do not share this exposure by default, since the key material lives in the app's own filesystem, a volume separate from the database being backed up. STORAGE_ENCRYPTION_KEY stays optional; the zero-config default is unchanged. Also corrected the same claim in the encryption.ts module docstring, and added the same actionable note to the chart README's key-separation section (ran chart:bump afterward to refresh the operator's vendored copy). Pinned the underlying fact rather than just the prose: two new tests in tests/security/credential-at-rest.test.ts assert that the auth-bootstrap file and the SQLite store resolve to the same directory, so a future change that separates them fails the test - the signal that the docs can be widened back.
…s, and test isolation
Second-reviewer batch on #324, all four verified independently:
1. Control 3.1's Verified by named only tests/security/credential-at-rest.test.ts,
which constructs withCredentialEncryption(inner) directly and cannot fail if
the factory ever stopped wrapping the provider it hands out - the phase's
central choke point. Linked the existing tests/isolated/factory-singleton.test.ts,
which does guard the factory, and src/lib/storage/factory.ts in Enforced in.
Chose linking the existing test over writing a new one: it already exercises
exactly this claim and a second copy would just be a second thing to drift.
2. charts/libredb-studio/README.md told the reader to
'helm install libredb-studio libredb-studio/libredb-studio', but the repo
alias documented two lines above (and used by every other example in the
file) is 'libredb', not 'libredb-studio' - the command as written does not
resolve. Fixed to match every other example in the file. Verified with git
blame: introduced on this branch, absent from origin/main. Edited the source
chart, then ran chart:bump to refresh the operator's vendored copy.
3. tests/unit/lib/storage/encrypting-provider.test.ts snapshotted and restored
only JWT_SECRET; an ambient STORAGE_ENCRYPTION_KEY (a developer's
.env.local) takes precedence in inputKeyMaterial(), so rotating JWT_SECRET
in that case would not change the derived key and the undecryptable-warning
tests would stop testing what they claim to. Matched the sibling file's
isolation (tests/security/credential-at-rest.test.ts already snapshots
both). No new test added: the key-precedence behavior itself is already
covered correctly in tests/unit/lib/storage/encryption.test.ts - this was
purely a test-isolation gap, not a missing behavioral test.
4. eslint.config.mjs's type-aware layer (no-floating-promises,
no-misused-promises, await-thenable) was scoped to src/app/api/** and
src/lib/db/**, never reaching src/lib/storage/**, where this phase's
async-heavy credential-encryption decorator lives. Added
src/lib/storage/**/*.ts to the scope. Verified both directions: a floating
promise injected into encrypting-provider.ts is now caught (confirmed, then
reverted), and src/lib/db/providers/document/couchbase/introspect.ts:253:23: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/monitoring/tabs/PerformanceTab.tsx:21:59: warning react(no-object-type-as-default-prop): Do not use an array literal as default prop value. Use a stable reference instead. help: Default values are re-created on every render and break referential equality, causing unnecessary re-renders. Move the value out of the component or memoize it.
src/components/monitoring/tabs/PerformanceTab.tsx:265:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/OverviewTab.tsx:21:56: warning react(no-object-type-as-default-prop): Do not use an array literal as default prop value. Use a stable reference instead. help: Default values are re-created on every render and break referential equality, causing unnecessary re-renders. Move the value out of the component or memoize it.
src/components/monitoring/tabs/OverviewTab.tsx:247:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
tests/helpers/mock-fetch.ts:33:15: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/monitoring/tabs/SessionsTab.tsx:267:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/SessionsTab.tsx:284:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/lib/storage/providers/postgres.ts:119:11: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/utils/pool-manager.ts:168:14: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/utils/pool-manager.ts:180:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/monitoring/tabs/TablesTab.tsx:246:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/TablesTab.tsx:263:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/StorageTab.tsx:279:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/StorageTab.tsx:296:20: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/QueriesTab.tsx:229:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/monitoring/tabs/QueriesTab.tsx:246:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/hooks/use-ai-chat.ts:143:49: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/QuerySafetyDialog.tsx:183:35: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/QuerySafetyDialog.tsx:270:23: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/ResultsGrid.tsx:467:21: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DataImportModal.tsx:437:27: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DataImportModal.tsx:440:29: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/QueryEditor.tsx:112:16: warning react(no-object-type-as-default-prop): Do not use an array literal as default prop value. Use a stable reference instead. help: Default values are re-created on every render and break referential equality, causing unnecessary re-renders. Move the value out of the component or memoize it.
tests/components/QuerySafetyDialog.test.tsx:275:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/admin/tabs/OperationsTab.tsx:352:29: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/admin/tabs/OperationsTab.tsx:456:29: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/AIAutopilotPanel.tsx:98:33: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/CreateTableModal.tsx:161:19: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/NL2SQLPanel.tsx:135:35: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/NL2SQLPanel.tsx:208:16: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/admin/tabs/OverviewTab.tsx:658:18: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:81:33: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/DatabaseDocs.tsx:131:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:137:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:143:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:150:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:157:14: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DatabaseDocs.tsx:162:19: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/lib/db/factory.ts:169:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/factory.ts:176:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/PivotTable.tsx:259:21: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:415:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:427:29: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:449:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:459:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:477:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/SchemaDiff.tsx:486:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:331:23: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:426:27: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:510:33: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/VisualExplain.tsx:540:18: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:580:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:586:15: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:592:16: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:600:16: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:606:28: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:609:14: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:625:19: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:632:17: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:864:21: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/VisualExplain.tsx:928:22: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DataCharts.tsx:228:12: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/DataCharts.tsx:961:27: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
tests/security/rate-limit-routes.test.ts:62:17: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:78:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:115:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:130:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:153:17: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:197:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:201:26: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/rate-limit-routes.test.ts:214:21: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:124:15: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:136:15: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:144:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:150:15: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:157:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:197:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:213:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/login-enumeration.test.ts:248:36: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/api/storage/storage-routes.test.ts:307:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/api/storage/storage-routes.test.ts:322:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/llm/ollama-provider.test.ts:45:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/llm/custom-provider.test.ts:52:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/llm/gemini-provider.test.ts:55:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/helpers/mock-next.ts:74:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/hooks/use-inline-editing.ts:152:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/keyvalue/redis.ts:330:36: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/keyvalue/redis.ts:344:28: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/llm/openai-provider.test.ts:52:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/hooks/use-inline-editing.test.ts:602:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/admin/tabs/AuditTab.tsx:163:25: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/admin/tabs/AuditTab.tsx:308:27: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/lib/db/providers/document/mongodb.ts:466:24: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:471:23: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:478:26: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:482:25: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:663:17: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:685:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:748:27: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:895:27: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:928:32: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/document/mongodb.ts:931:25: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/DataProfiler.tsx:151:35: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/DataProfiler.tsx:310:35: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
tests/api/auth/oidc-login.test.ts:108:9: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/app/api/db/multi-query/route.ts:128:23: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/sql/mysql.ts:563:31: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/sql/mysql.ts:568:26: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/sql/mysql.ts:573:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/llm/utils/retry.ts:62:14: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/llm/utils/retry.ts:82:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/ui/toggle-group.tsx:44:36: warning react(jsx-no-constructed-context-values): The Context `value` prop should not be constructed. help: Wrap the `value` prop in useMemo() or useCallback(), or use a constant value to prevent unnecessary re-renders. Alternatively, move the value outside the render function if it doesn't depend on props or state.
tests/security/csrf-origin.test.ts:175:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/security/route-auth.test.ts:143:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/instrumentation.test.ts:27:5: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/db/providers/sql/oracle.ts:737:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/llm/streaming.test.ts:12:29: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/lib/llm/utils/streaming.ts:168:37: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/app/api/db/profile/route.ts:116:26: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/ui/carousel.tsx:107:7: warning react(jsx-no-constructed-context-values): The Context `value` prop should not be constructed. help: Wrap the `value` prop in useMemo() or useCallback(), or use a constant value to prevent unnecessary re-renders. Alternatively, move the value outside the render function if it doesn't depend on props or state.
e2e/functional-smoke.spec.ts:69:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/api/auth/login.test.ts:158:19: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/api/auth/login.test.ts:316:7: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
src/components/ui/field.tsx:194:67: warning react(no-array-index-key): Usage of Array index in keys is not allowed help: Use a unique data-dependent key to avoid unnecessary rerenders
src/components/ui/form.tsx:37:32: warning react(jsx-no-constructed-context-values): The Context `value` prop should not be constructed. help: Wrap the `value` prop in useMemo() or useCallback(), or use a constant value to prevent unnecessary re-renders. Alternatively, move the value outside the render function if it doesn't depend on props or state.
src/components/ui/form.tsx:76:31: warning react(jsx-no-constructed-context-values): The Context `value` prop should not be constructed. help: Wrap the `value` prop in useMemo() or useCallback(), or use a constant value to prevent unnecessary re-renders. Alternatively, move the value outside the render function if it doesn't depend on props or state.
src/components/ui/chart.tsx:48:28: warning react(jsx-no-constructed-context-values): The Context `value` prop should not be constructed. help: Wrap the `value` prop in useMemo() or useCallback(), or use a constant value to prevent unnecessary re-renders. Alternatively, move the value outside the render function if it doesn't depend on props or state.
tests/unit/lib/auth.test.ts:268:20: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
tests/unit/lib/auth.test.ts:273:20: warning eslint(no-await-in-loop): Unexpected `await` inside a loop. help: Collect all promises into an array and use `Promise.all()` to run them in parallel, rather than awaiting each one sequentially inside the loop.
/home/cevheri/projects/libredb/libredb-studio/.remember/tmp/last-ndc.ts
1:1 warning Expected an assignment or function call and instead saw an expression @typescript-eslint/no-unused-expressions
/home/cevheri/projects/libredb/libredb-studio/bin/studio.js
159:7 warning Unused eslint-disable directive (no problems were reported from 'no-await-in-loop')
167:7 warning Unused eslint-disable directive (no problems were reported from 'no-await-in-loop')
/home/cevheri/projects/libredb/libredb-studio/src/app/admin/error.tsx
27:15 warning Do not use `window.location.href` to navigate to internal Next.js pages. Use `redirect()` in the render phase, or `useRouter().push()` in Client Components' event handlers instead. See: https://nextjs.org/docs/messages/no-location-assign-relative-destination @next/next/no-location-assign-relative-destination
/home/cevheri/projects/libredb/libredb-studio/src/app/api/connections/managed/route.ts
19:17 warning 'password' is assigned a value but never used @typescript-eslint/no-unused-vars
19:27 warning 'connectionString' is assigned a value but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/app/login/login-form.tsx
226:23 warning Do not use `window.location.href` to navigate to internal Next.js pages. Use `redirect()` in the render phase, or `useRouter().push()` in Client Components' event handlers instead. See: https://nextjs.org/docs/messages/no-location-assign-relative-destination @next/next/no-location-assign-relative-destination
/home/cevheri/projects/libredb/libredb-studio/src/components/QueryEditor.tsx
146:9 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/QueryEditor.tsx:146:9
144 | const saved = localStorage.getItem("editor-line-numbers");
145 | if (saved !== null) {
> 146 | setShowLineNumbers(saved === "true");
| ^^^^^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
147 | }
148 | setLineNumbersPreferenceReady(true);
149 | }, []); react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/QueryHistory.tsx
49:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/QueryHistory.tsx:49:5
47 | // Refresh history when refreshTrigger changes (replaces key-based re-mount)
48 | useEffect(() => {
> 49 | setHistory(storage.getHistory());
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
50 | }, [refreshTrigger]);
51 |
52 | const filteredHistory = useMemo(() => { react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/ResultsGrid.tsx
412:17 warning Compilation Skipped: Use of incompatible library
This API returns functions which cannot be memoized without leading to stale UI. To prevent this, by default React Compiler will skip memoizing this component/hook. However, you may see issues if values from this API are passed to other components/hooks that are memoized.
/home/cevheri/projects/libredb/libredb-studio/src/components/ResultsGrid.tsx:412:17
410 | ]);
411 |
> 412 | const table = useReactTable({
| ^^^^^^^^^^^^^ TanStack Table's `useReactTable()` API returns functions that cannot be memoized safely
413 | data: filteredRows,
414 | columns,
415 | state: { react-hooks/incompatible-library
/home/cevheri/projects/libredb/libredb-studio/src/components/SavedQueries.tsx
23:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/SavedQueries.tsx:23:5
21 | // Refresh queries when refreshTrigger changes (replaces key-based re-mount)
22 | useEffect(() => {
> 23 | setQueries(storage.getSavedQueries());
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
24 | }, [refreshTrigger]);
25 |
26 | const filteredQueries = queries.filter((q) => { react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/VisualExplain.tsx
757:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/VisualExplain.tsx:757:5
755 | useEffect(() => {
756 | if (kind === null) return;
> 757 | setActiveTab((current) => {
| ^^^^^^^^^^^^ Avoid calling setState() directly within an effect
758 | const available: readonly ExplainTab[] = kind === "tree" ? TREE_TABS : POSTGRES_TABS;
759 | return available.includes(current) ? current : available[0];
760 | }); react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/AuditTab.tsx
236:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/AuditTab.tsx:236:5
234 |
235 | useEffect(() => {
> 236 | setHistory(storage.getHistory());
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
237 | }, []);
238 |
239 | const filteredHistory = useMemo(() => { react-hooks/set-state-in-effect
352:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/AuditTab.tsx:352:5
350 |
351 | useEffect(() => {
> 352 | setHistory(storage.getHistory());
| ^^^^^^^^^^ Avoid calling setState() directly within an effect
353 | }, []);
354 |
355 | const stats = useMemo(() => { react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/OverviewTab.tsx
155:7 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/OverviewTab.tsx:155:7
153 | prevTarget.current = target;
154 | if (target === 0) {
> 155 | setValue(0);
| ^^^^^^^^ Avoid calling setState() directly within an effect
156 | return;
157 | }
158 | react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/SecurityTab.tsx
138:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/admin/tabs/SecurityTab.tsx:138:5
136 |
137 | useEffect(() => {
> 138 | setThresholds(storage.getThresholdConfig());
| ^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
139 | }, []);
140 |
141 | const updateThreshold = (index: number, field: "warning" | "critical", value: number) => { react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/monitoring/MonitoringDashboard.tsx
78:5 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/components/monitoring/MonitoringDashboard.tsx:78:5
76 | useEffect(() => {
77 | if (allConns.length === 0) return;
> 78 | setConnections(allConns);
| ^^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
79 |
80 | setSelectedConnection((prev) => {
81 | if (prev) return prev; react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/components/schema-explorer/SchemaExplorer.tsx
7:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/components/schema-explorer/TableItem.tsx
17:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/components/sidebar/ConnectionItem.tsx
5:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/components/sidebar/Sidebar.tsx
7:10 warning 'Button' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/components/ui/sidebar.tsx
570:26 warning Error: Cannot call impure function during render
`Math.random` is an impure function. Calling an impure function can produce unstable results that update unpredictably when the component happens to re-render. (https://react.dev/reference/rules/components-and-hooks-must-be-pure#components-and-hooks-must-be-idempotent).
/home/cevheri/projects/libredb/libredb-studio/src/components/ui/sidebar.tsx:570:26
568 | // Random width between 50 to 90%.
569 | const width = React.useMemo(() => {
> 570 | return `${Math.floor(Math.random() * 40) + 50}%`;
| ^^^^^^^^^^^^^ Cannot call impure function
571 | }, []);
572 |
573 | return ( react-hooks/purity
/home/cevheri/projects/libredb/libredb-studio/src/hooks/use-connection-form.ts
293:6 warning React Hook useCallback has an unnecessary dependency: 'type'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
/home/cevheri/projects/libredb/libredb-studio/src/hooks/use-provider-metadata.ts
22:7 warning Error: Calling setState synchronously within an effect can trigger cascading renders
Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:
* Update external systems with the latest state from React.
* Subscribe for updates from some external system, calling setState in a callback function when external state changes.
Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).
/home/cevheri/projects/libredb/libredb-studio/src/hooks/use-provider-metadata.ts:22:7
20 | useEffect(() => {
21 | if (!connection) {
> 22 | setMetadata(null);
| ^^^^^^^^^^^ Avoid calling setState() directly within an effect
23 | lastConnectionId.current = null;
24 | return;
25 | } react-hooks/set-state-in-effect
/home/cevheri/projects/libredb/libredb-studio/src/hooks/use-tab-manager.ts
160:6 warning React Hook useCallback has an unnecessary dependency: 'activeConnection'. Either exclude it or remove the dependency array react-hooks/exhaustive-deps
/home/cevheri/projects/libredb/libredb-studio/src/lib/db/providers/sql/druid/http-transport.ts
264:7 warning 'UNDESCRIBED' is assigned a value but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/lib/db/providers/sql/sqlite-driver.ts
99:33 warning '_options' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/src/workspace/StudioWorkspace.tsx
278:5 warning React Hook useCallback has a missing dependency: 'conn.activeConnection?.type'. Either include it or remove the dependency array react-hooks/exhaustive-deps
/home/cevheri/projects/libredb/libredb-studio/tests/api/db/maintenance.test.ts
32:29 warning '_event' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/DataCharts.test.tsx
59:33 warning '_el' is defined but never used @typescript-eslint/no-unused-vars
59:48 warning '_options' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/QueryEditor.test.tsx
19:37 warning '_a' is defined but never used @typescript-eslint/no-unused-vars
20:34 warning '_a' is defined but never used @typescript-eslint/no-unused-vars
311:37 warning '_a' is defined but never used @typescript-eslint/no-unused-vars
312:34 warning '_a' is defined but never used @typescript-eslint/no-unused-vars
1820:26 warning '_a' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/QuerySafetyDialog.test.tsx
399:41 warning '_params' is defined but never used @typescript-eslint/no-unused-vars
428:41 warning '_params' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/SchemaDiagram.test.tsx
136:40 warning '_options' is defined but never used @typescript-eslint/no-unused-vars
339:50 warning '_options' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/StudioWorkspace.test.tsx
46:25 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
48:35 warning '_blob' is defined but never used @typescript-eslint/no-unused-vars
303:37 warning '_query' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/sidebar/ConnectionItem.test.tsx
13:7 warning Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-unused-vars')
18:13 warning 'initial' is defined but never used @typescript-eslint/no-unused-vars
19:13 warning 'animate' is defined but never used @typescript-eslint/no-unused-vars
20:13 warning 'exit' is defined but never used @typescript-eslint/no-unused-vars
21:13 warning 'variants' is defined but never used @typescript-eslint/no-unused-vars
22:13 warning 'whileHover' is defined but never used @typescript-eslint/no-unused-vars
23:13 warning 'whileTap' is defined but never used @typescript-eslint/no-unused-vars
24:13 warning 'layoutId' is defined but never used @typescript-eslint/no-unused-vars
25:13 warning 'transition' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/components/sidebar/ConnectionsList.test.tsx
13:7 warning Unused eslint-disable directive (no problems were reported from '@typescript-eslint/no-unused-vars')
18:13 warning 'initial' is defined but never used @typescript-eslint/no-unused-vars
19:13 warning 'animate' is defined but never used @typescript-eslint/no-unused-vars
20:13 warning 'exit' is defined but never used @typescript-eslint/no-unused-vars
21:13 warning 'variants' is defined but never used @typescript-eslint/no-unused-vars
22:13 warning 'whileHover' is defined but never used @typescript-eslint/no-unused-vars
23:13 warning 'whileTap' is defined but never used @typescript-eslint/no-unused-vars
24:13 warning 'layoutId' is defined but never used @typescript-eslint/no-unused-vars
25:13 warning 'transition' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/hooks/use-ai-chat.test.ts
628:14 warning '_params' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/integration/db/oracle-provider.test.ts
14:38 warning '_opts' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/isolated/factory-singleton.test.ts
38:52 warning 'getStorageProviderType' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/isolated/use-storage-sync.test.ts
444:13 warning 'fetchMock' is assigned a value but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/unit/lib/connection-string-parser.test.ts
654:5 warning Unused eslint-disable directive (no problems were reported from 'no-extend-native')
662:7 warning Unused eslint-disable directive (no problems were reported from 'no-extend-native')
/home/cevheri/projects/libredb/libredb-studio/tests/unit/lib/storage/providers/postgres.test.ts
9:34 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
238:41 warning 'sql' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/unit/lib/storage/providers/sqlite.test.ts
10:17 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
12:27 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
13:29 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
115:30 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
136:26 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
164:30 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
198:30 warning '_args' is defined but never used @typescript-eslint/no-unused-vars
/home/cevheri/projects/libredb/libredb-studio/tests/unit/schema-diagram/layout-engine.test.ts
29:15 warning '_url' is defined but never used @typescript-eslint/no-unused-vars
126:19 warning '_url' is defined but never used @typescript-eslint/no-unused-vars
✖ 78 problems (0 errors, 78 warnings)
0 errors and 6 warnings potentially fixable with the `--fix` option. surfaces zero new errors against the existing
code in that directory, so nothing pre-existing needed silencing.
Also attempted, as requested: a real end-to-end round trip against a genuine
STORAGE_PROVIDER=sqlite file. tests/integration/storage/ now has a Node-run
harness (bundled with bun build v1.3.14 (0d9b296a)
error: Missing entrypoints. What would you like to bundle?
Usage:
$ bun build <entrypoint> [...<entrypoints>] [...flags]
To see full documentation:
$ bun build --help and
executed under real , since Bun cannot load better-sqlite3) that writes
a canary password through the same withCredentialEncryption() decorator the
factory uses, checkpoints the WAL, and reads the RAW BYTES ON DISK plus the
read-back after a simulated key rotation. Confirmed it catches a real
regression: reverting the connection-secrets.ts fix from the previous commit
turns this test red (verified locally, then re-fixed). The one obstacle noted
in advance - better-sqlite3 needing real node_modules resolution when the
bundle lives outside the project tree - is worked around with a symlink into
the temp build directory rather than moving output into the repo.
docs/SECURITY.md's control 3.1 row now lists three verifiers instead of one.
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 30 out of 30 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
src/lib/storage/encryption.ts:79
- An explicitly configured empty value bypasses the minimum-length validation because it is falsy, so
STORAGE_ENCRYPTION_KEY=""silently falls back toJWT_SECRET. This defeats an operator's intended key separation and contradicts the documented rule that configured values shorter than 32 characters are rejected. Distinguish an absent variable from an empty one and validate the latter.
const explicit = process.env.STORAGE_ENCRYPTION_KEY;
if (explicit) {
SECURITY.md:98
- This backup claim is still too broad. In the zero-configuration SQLite case,
JWT_SECRETis persisted inauth-bootstrap.jsonbeside the database, so a backup of that data directory contains both ciphertext and the key; the newdocs/STORAGE.mdand posture page correctly document this exception. Narrow this paragraph too so the published disclosure policy does not contradict them.
still identify what a dump contains. A leaked database file or backup is therefore not by itself
enough to read the credentials — but anyone who can read the server's environment still can.
Rotating the key makes stored credentials unreadable; see
[docs/STORAGE.md](docs/STORAGE.md#credential-encryption-at-rest).
SECURITY.md:10
- The support table now declares every pre-0.10 release unsupported, but this same document's image-SBOM commands at lines 215 and 220 still resolve
:0.9.67. Copying the published verification example would therefore inspect an unsupported old image rather than this 0.10.0 release; update those two occurrences to0.10.0(or a version placeholder).
| 0.10.x | :white_check_mark: |
| < 0.10.0 | :x: |



Phase 3, the last of the security maturity programme: credentials encrypted at rest, a published security posture, and the 0.10.0 release that carries all four phases.
Phases 0 (#320), 1 (#321) and 2 (#322) are merged, plus the Next.js bump (#323). None of them changed the version — this pull request does, once, for the whole programme.
What this adds
STORAGE_ENCRYPTION_KEYwhen set, otherwise derived fromJWT_SECRETthrough HKDF-SHA256Record<keyof DatabaseConnection, FieldClass>— a new field failstypecheckuntil someone classifies itdocs/SECURITY.mdbun run security:checkWhat is encrypted, and what is not
Six fields, all inside
connections:password,connectionString,ssl.clientKey,sshTunnel.password,sshTunnel.privateKey,sshTunnel.passphrase.host,port,useranddatabasestay readable on purpose, so an operator can still answer "which database is in this dump".ssl.caCertandssl.clientCertare public certificates.This is not a vault. Anyone who can read the process environment can read the credentials. It protects a leaked database file or dump, and that is the whole claim.
The browser's
localStoragecopy stays plaintext, by deliberate product decision. That is what lets Studio work without a master password, and it is why Phase 0's cross-site-scripting work carries the weight it does.Operator notes — read before upgrading
Nothing is required of you.
STORAGE_ENCRYPTION_KEYis optional and stays optional; all 27 distribution channels depend on that. With nothing set, the key derives fromJWT_SECRET, which the auth bootstrap already generates and persists on first run.Rotating
JWT_SECRETinvalidates every stored credential when no explicitSTORAGE_ENCRYPTION_KEYis set. If you rotate a JWT secret for an unrelated reason, stored passwords stop opening. SetSTORAGE_ENCRYPTION_KEYfirst if you expect to rotate.A credential that cannot be decrypted is omitted, not fatal. The record survives, one warning is logged per read with a count, and the user sees an empty password box to retype. Throwing instead would take query history, saved queries and charts down with the passwords; dropping the record would be worse still, because
useStorageSyncis a write-through cache and a dropped connection is persisted as deleted on the next push.Migration is lazy. Reads accept plaintext and enveloped values; writes always produce enveloped values. There is no migration command, no backfill and no version column.
Only the server-side store is affected —
STORAGE_PROVIDER=sqlite|postgres.STORAGE_PROVIDER=localhas no server store at all.The plan was wrong six times, and that is worth saying
This phase was executed from a written plan whose test code had never been run. Implementers stopped and asked rather than making the tests pass, and found six defects in it. Two mattered:
A tamper test that failed on correct code. It flipped the last base64url character of the sealed body. For a 25-byte body that character encodes only the low 2 bits of the final byte, so it can only be
A,Q,gorw— andAtoBdecodes to an identical byte string. Enumerating all 256 final-byte values: 64 of 256, exactly 25.0 percent, change nothing, matching the 24.2 percent measured over 5000 runs. It would have reddened CI intermittently while looking like a real tamper-detection failure. Replaced with two deterministic byte-level tests — one flipping a ciphertext byte, one a tag byte, because those are different properties and neither stands in for the other.The plan contradicted its own design decision. Decision D4 says a malformed
v1:value isundecryptable, never plaintext. The task text mandated the opposite for a value with the wrong segment count, and shipped a test asserting it. Sov1:abcandv2:abcwere returned verbatim as passwords. The rule is now: if the first colon-segment matches^v\d+$the value is an envelope claim, and any malformation of that claim isundecryptable.The other four: an off-by-one that stripped a 10-character prefix with
slice(11)and silently dropped three fields from a derived list; a unit test asserting exactlogger.warncall counts, which fails deterministically becausebun run testruns unit, api and integration in one process and fire-and-forget warnings from unrelated route tests land inside the spy window; and three hand-counted numbers that were simply wrong.A programme about claim accuracy should say when its own plan was inaccurate.
What review then found in the shipped code
Two independent reviewers went over this branch after it was pushed. Eleven findings between them; every one was valid, and two were real holes in the guarantee this pull request makes.
The backup claim was false for the default deployment. The documentation said encryption protects "a stolen database file, dump, backup or volume snapshot". But
getDataDir()is defined aspath.dirname(STORAGE_SQLITE_PATH), soauth-bootstrap.json— which holds the generatedJWT_SECRETthe key derives from when nothing is configured — sits beside the store by construction, and the chart mounts/app/dataas one volume. A snapshot therefore contained the ciphertext and its key. Narrowed to what holds: the database file or dump on its own is protected always; a backup or snapshot only onceSTORAGE_ENCRYPTION_KEYcomes from outside the volume. Two new tests pinresolveBootstrapPath()'s directory togetDataDir(), so if a future change separates them the tests fail and the claim can be widened deliberately rather than by accident.The write path did not encrypt everything it claimed to.
sealIfPlaintextencrypted only values classifiedplaintext, and passed anundecryptableone through unchanged. So a user whose password happens to match^v\d+:—v2:hunter2, say — had it written to the store in the clear, while the function directly below it claimed "Never returns a connection with a plaintext credential in it." Fixed to pass through only what actually opens and encrypt everything else, which also repairs that user's password: it now round-trips instead of being classified undecryptable and dropped.The posture page's own guard had the shape this phase exists to eliminate — three times. Two controls named a script and a policy document rather than a test in their
Verified bycolumn, andsecurity:checkaccepted both. Requiring a test there fixed those two but not the third: control 3.1, "credentials are encrypted at rest", was verified by a test containing zero references togetStorageProvider— it wraps the decorator directly, so it stays green even if the factory handed out a bare provider. Requiring a test turned out to be necessary and not sufficient; the test also has to exercise the claim beside it. The factory guard is now linked from that row.The remaining findings:
security:checkaccepted a duplicate control ID; the audit row contradicted the exception documented directly beneath it; both chart copies marked a four-phase security release ascontainsSecurityUpdates: "false"; a Helm install example referencedlibredb-studio/libredb-studiowhen the repo alias this branch documents islibredb, so the copy-paste command failed (introduced here —origin/mainhas no such line); a unit test snapshotted onlyJWT_SECRET, so an ambientSTORAGE_ENCRYPTION_KEYwould silently defeat its own rotation cases while its sibling file snapshotted both; and the type-aware ESLint layer coveredsrc/app/apiandsrc/lib/dbbut notsrc/lib/storage, where every file this phase added lives — including a decorator that wraps async provider methods, which is exactly what those floating-promise rules are for.One suggestion was declined on inspection rather than implemented: a request to document
STORAGE_ENCRYPTION_KEY's 32-character minimum in.env.example, which already states it.Verification
Every gate green at
dea4f0b, including the two this phase deliberately carried red until the end:The encryption was verified against a real store, not only against a fake
The threat suite proves the decorator seals before handing anything to a provider, but it does so through an in-memory capture provider — so it demonstrates the boundary, not the file on disk. The end-to-end suite does not close that gap either: it runs with
STORAGE_PROVIDERunset, which returnsnullbefore any provider is constructed, so the decorator never executes during it.That gap is now closed in CI, not only by hand.
tests/integration/storage/sqlite-credential-encryption.test.tsdrives a canary password throughwithCredentialEncryption(new SQLiteStorageProvider(...))against a real file, checkpoints the WAL, asserts against the raw bytes on disk, then simulates a key rotation on that same file. It runs under realnodethrough a bundled harness, following the existingsqlite-node-harness.tspattern, becausebetter-sqlite3cannot load underbun— which is why this could not live intests/security/. Disabling the seal makes it fail; restoring it makes it pass.The same round trip was also driven by hand through the real HTTP API before that test existed:
store.dbor its WALv1:F2gpBx_6sERd6sES:waeMRgr69ZS…hoststill readable in the filepasswordabsent — not null, not the raw envelopeThat last row is the one worth reading twice. A rotated key does not throw, does not drop the record, and does not hand the envelope to a driver as a password. The operator sees an empty password box; query history, saved queries and charts are untouched.
Each task carried a sabotage step: the control is not delivered until something fails when it is broken. Three results are worth naming, and I re-ran each rather than accepting the report.
The choke point.
getStorageProviderhas exactly three call sites, all undersrc/app/api/storage/; the factory constructs exactly two providers and both are wrapped;STORAGE_PROVIDER=localreturnsnullbefore any provider exists. Unwrapping the factory reddens a wiring test, so the guard is mechanical rather than a comment.The compile-time classification. Adding
vaultToken?: stringtoDatabaseConnectionproduceserror TS2741: Property 'vaultToken' is missing ... but required in type 'Record<keyof DatabaseConnection, FieldClass>', pointing at the classification map. A new field is opt out, not opt in.The posture guard fails in both directions. Renaming one security test produced two errors: the control row now links a file that does not exist, and the renamed test is verified by no control row. A one-directional check would have caught only the first, and the second is the one that catches a page drifting from reality.
Deferred
docs/BACKLOG.mdcarries this phase's residuals rather than leaving them in review transcripts. Thesecurity:checkscript sits outside the coverage universe by design —scripts/merge-lcov.mjsfilters tosrc/— so it is unit-tested on thereadme-checkprecedent instead.