diff --git a/README.it.md b/README.it.md index 5799dc9..5b68568 100644 --- a/README.it.md +++ b/README.it.md @@ -25,11 +25,11 @@ Il selettore di lingua offre **inglese, italiano, francese, tedesco e spagnolo** ## Database -13 profili: **SQLite, PostgreSQL, Aurora PostgreSQL, CockroachDB, Redshift, MySQL, MariaDB, Aurora MySQL, Databricks SQL, SQL Server, ClickHouse, MongoDB e Redis**. Gli adapter usano driver reali; Aurora usa il protocollo del proprio engine. Databricks, Redshift e ClickHouse sono in lettura. MongoDB e Redis usano operazioni JSON consentite, con scritture revisionate e senza rollback comune. +14 profili: **SQLite, Amazon Athena, PostgreSQL, Aurora PostgreSQL, CockroachDB, Redshift, MySQL, MariaDB, Aurora MySQL, Databricks SQL, SQL Server, ClickHouse, MongoDB e Redis**. Gli adapter usano driver reali; Aurora usa il protocollo del proprio engine. Athena usa il profilo AWS locale, un ruolo IAM facoltativo e un workgroup. Athena, Databricks, Redshift e ClickHouse sono in lettura. MongoDB e Redis usano operazioni JSON consentite, con scritture revisionate e senza rollback comune. SQLite, PostgreSQL, MySQL, MongoDB, Redis e ClickHouse hanno prove su fixture locali reali. Account cloud Databricks/Aurora/Redshift e server CockroachDB, MariaDB e SQL Server richiedono una verifica specifica. Consulta [capacità e limiti](docs/DATABASES.md) e [validazione](docs/VALIDATION.md). -Le nuove connessioni sono in lettura. TLS verifica i certificati remoti quando attivo; i ruoli nel database restano l'autorità sul server. Questa versione non implementa tunnel SSH, IAM/OAuth dei database, Oracle, Snowflake, BigQuery, Cassandra, DuckDB, Elasticsearch, DDL, backup/restore o importazioni massive. +Le nuove connessioni sono in lettura. Prima di usare Athena completa il login AWS SSO per il profilo scelto. TLS verifica i certificati remoti quando attivo; i ruoli nel database restano l'autorità sul server. Questa versione non implementa tunnel SSH, IAM/OAuth per gli altri database, Oracle, Snowflake, BigQuery, Cassandra, DuckDB, Elasticsearch, DDL, backup/restore o importazioni massive. ## Assistente diff --git a/README.md b/README.md index 87a9828..7680669 100644 --- a/README.md +++ b/README.md @@ -29,12 +29,12 @@ Open **Local demo** to use a real SQLite database with 120 customers, 36 product | PostgreSQL | PostgreSQL, Aurora PostgreSQL, CockroachDB | Reviewed transactional writes | | MySQL | MySQL, MariaDB, Aurora MySQL | Reviewed transactional writes; InnoDB required | | SQL Server | SQL Server | Reviewed transactional writes | -| Analytics | Databricks SQL, Redshift, ClickHouse | Read-only | +| Analytics | Amazon Athena, Databricks SQL, Redshift, ClickHouse | Read-only | | Document / key-value | MongoDB, Redis | Reviewed allowlisted JSON operations; no shared rollback | Adapters use real client libraries. Aurora profiles use their engine's protocol. The catalog exposes each adapter's capabilities; it does not imply live cloud certification. Local fixture checks cover SQLite, PostgreSQL, MySQL, MongoDB, Redis and ClickHouse. Live Databricks/Aurora/Redshift accounts, CockroachDB, MariaDB and SQL Server need separate acceptance tests. See [database capabilities](docs/DATABASES.md) and [validation](docs/VALIDATION.md). -New connections start read-only. TLS verifies remote certificates when enabled. Database principals remain the server-side authority. SSH tunnels, database IAM/OAuth, Oracle, Snowflake, BigQuery, Cassandra, DuckDB, Elasticsearch, DDL, backup/restore and bulk import are not implemented in this release. +New connections start read-only. Athena uses a local AWS profile, an optional assumed role, and a selected workgroup; complete AWS SSO login before connecting. TLS verifies remote certificates when enabled. Database principals remain the server-side authority. SSH tunnels, database IAM/OAuth for other engines, Oracle, Snowflake, BigQuery, Cassandra, DuckDB, Elasticsearch, DDL, backup/restore and bulk import are not implemented in this release. ## Assistant diff --git a/THIRD_PARTY_NOTICES.md b/THIRD_PARTY_NOTICES.md index c6d5a7f..c747a52 100644 --- a/THIRD_PARTY_NOTICES.md +++ b/THIRD_PARTY_NOTICES.md @@ -53,9 +53,9 @@ The application uses the following main open-source packages. Their source distr | ClickHouse JavaScript client | Apache-2.0 | | AWS SDK for JavaScript v3 | Apache-2.0 | | Google Auth Library for Node.js | Apache-2.0 | -| Databricks native SQL kernel and FlatBuffers | Apache-2.0 | +| FlatBuffers JavaScript runtime | Apache-2.0 | | LZ4 N-API native bindings | MIT | | TypeScript, Playwright | Apache-2.0; build and test tooling | | Vite, electron-builder | MIT; build tooling | -`package-lock.json` identifies exact dependency versions. The installed runtime dependency license inventory uses MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause and 0BSD, including the Apache-2.0 text in FlatBuffers' `LICENSE`. Platform-specific optional packages differ by installation. Transitive packages keep their distributed copyright and license notices; Electron's Chromium notices must remain in distributed bundles. No third-party service grants or account rights are implied by the integration adapters. +`package-lock.json` identifies exact dependency versions. The installed runtime dependency license inventory uses MIT, Apache-2.0, ISC, BSD-2-Clause, BSD-3-Clause and 0BSD, including the Apache-2.0 text in FlatBuffers' `LICENSE`. The Databricks SQL JavaScript Thrift backend is included; its optional native SQL kernel binary and platform package are excluded from Tableline 0.2.2. Platform-specific optional packages differ by installation. Transitive packages keep their distributed copyright and license notices; Electron's Chromium notices must remain in distributed bundles. No third-party service grants or account rights are implied by the integration adapters. diff --git a/assets/electron-notices.json b/assets/electron-notices.json index 619f6aa..78fde12 100644 --- a/assets/electron-notices.json +++ b/assets/electron-notices.json @@ -1,82 +1,82 @@ { "version": 2, - "electronVersion": "44.5.1", + "electronVersion": "44.10.0", "distributions": { "darwin-arm64": { - "upstreamArchiveSha256": "1d75703019bb16461ae65f3081d7e6f5c0b11e901d0ccb5c343bcf7bcdd6435c", + "upstreamArchiveSha256": "8e9f6041d3aa62229332b033d0390fd213a9b10cd078163f3f5fe988ee91baf7", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "a62dabd1c6ef1327365b2a3fdffb806222684a746dcb8f4afd1c1f690eba5535", - "bytes": 20111209 + "sha256": "3375e2a9bd7bc631738fde10be6548a76efe8cf5e4b8826b8140ba8b0c4d6353", + "bytes": 20115518 } } }, "darwin-x64": { - "upstreamArchiveSha256": "e567d13833d0e161d7749727355b98643461df3395b537cfa7bdddf8a8bfedff", + "upstreamArchiveSha256": "0d52465c24865981a9a68621c7068f124ef9f2a4caf5119afde62b04fffea66d", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "a62dabd1c6ef1327365b2a3fdffb806222684a746dcb8f4afd1c1f690eba5535", - "bytes": 20111209 + "sha256": "3375e2a9bd7bc631738fde10be6548a76efe8cf5e4b8826b8140ba8b0c4d6353", + "bytes": 20115518 } } }, "linux-arm64": { - "upstreamArchiveSha256": "ee1790d743af1abd6a7e3971589dcd8635b58dab51ca1359123ba5216ce3a453", + "upstreamArchiveSha256": "558ae899d98b70236325d81da44bb6b2c988dd9a7bd16be17cea25f06e56cc14", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "a62dabd1c6ef1327365b2a3fdffb806222684a746dcb8f4afd1c1f690eba5535", - "bytes": 20111209 + "sha256": "3375e2a9bd7bc631738fde10be6548a76efe8cf5e4b8826b8140ba8b0c4d6353", + "bytes": 20115518 } } }, "linux-x64": { - "upstreamArchiveSha256": "5bcd217611d6843ececd6c9e9c1fcd1da3ab066c43d8b1a9e4b44689a1fba6f5", + "upstreamArchiveSha256": "62df9375ef035f2835a18cd7f9bff1959a185da22f2ecba1daa37cd9caee1dc5", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "a62dabd1c6ef1327365b2a3fdffb806222684a746dcb8f4afd1c1f690eba5535", - "bytes": 20111209 + "sha256": "3375e2a9bd7bc631738fde10be6548a76efe8cf5e4b8826b8140ba8b0c4d6353", + "bytes": 20115518 } } }, "win32-arm64": { - "upstreamArchiveSha256": "6ba86d5d5cfdf4ce61d51e2d270eaca464a7f88c143d14c6d9b3f43bb0760e9e", + "upstreamArchiveSha256": "331a59951738402c25525806e515a49d37d39c5a813d72f572a5653428bd57bd", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "7b328b8c7463ac9bfc7dc648c751533517c8441a0b5b21047d6c0b2620e60d70", - "bytes": 20472830 + "sha256": "e6e6b1f9cb0f32235412be113de7716333e88121f9159a0ea0ff3be652021af7", + "bytes": 20477229 } } }, "win32-x64": { - "upstreamArchiveSha256": "9b382492dcfee91f8f9e92c91f7972550a1b95d2299cac72279dab33a600d7db", + "upstreamArchiveSha256": "64c2d874fea1bd4c8d2e1f2df3a547497044159a6b853dee7fcdff357ff0e3cd", "licenses": { "LICENSE.electron.txt": { "sha256": "5154e165bd6c2cc0cfbcd8916498c7abab0497923bafcd5cb07673fe8480087d", "bytes": 1096 }, "LICENSES.chromium.html": { - "sha256": "7b328b8c7463ac9bfc7dc648c751533517c8441a0b5b21047d6c0b2620e60d70", - "bytes": 20472830 + "sha256": "e6e6b1f9cb0f32235412be113de7716333e88121f9159a0ea0ff3be652021af7", + "bytes": 20477229 } } } diff --git a/docs/DATABASES.md b/docs/DATABASES.md index f8d80e9..f26c369 100644 --- a/docs/DATABASES.md +++ b/docs/DATABASES.md @@ -3,6 +3,7 @@ | Profilo | Driver reale | Query | Scritture | Verifica | | ----------------- | -------------------------------- | -------------------------- | --------------------------------------------- | -------------------------------------------------------- | | SQLite | `node:sqlite`, processo dedicato | SQL | INSERT, UPDATE, DELETE con anteprima e commit | File reale, servizio e app desktop | +| Amazon Athena | `@aws-sdk/client-athena` | SQL, AWS SSO e workgroup | Disabilitate | SDK e account AWS da verificare | | PostgreSQL | `pg` | SQL, transazione READ ONLY | Proposta non eseguita, transazione al commit | Server Docker isolato | | Aurora PostgreSQL | `pg` | SQL | Come PostgreSQL | Contratto; cluster AWS non verificato | | CockroachDB | `pg` | SQL | Come PostgreSQL | Contratto; server non verificato | @@ -64,3 +65,5 @@ MongoDB e Redis mostrano una stima o lo stato attuale. Le scritture non hanno ro I profili nuovi sono in lettura. TLS verifica i certificati remoti; per i server locali di test puoi configurare esplicitamente una connessione senza TLS. Non vengono modificati i ruoli nel database. SQL Server non offre una transazione READ ONLY: usa un principal di sola lettura per applicare la policy anche sul server. Databricks richiede un warehouse, host, HTTP path e token. Aurora usa host/porta/database/utente del cluster e TLS; IAM e tunnel SSH non sono implementati. I provider LLM e i database hanno vault namespace separati. + +Athena usa il profilo AWS locale, la regione e un workgroup. Puoi indicare un ARN di ruolo IAM: Tableline lo assume con le credenziali del profilo e rinnova automaticamente le credenziali temporanee. Per i profili SSO devi prima completare il login con `aws sso login --profile `; quando scade, ripeti il login. Il database predefinito è facoltativo: se lo lasci vuoto, lo schema elenca i database visibili al ruolo. I permessi AWS, il limite di scansione e la cifratura dei risultati devono essere configurati nel workgroup e nel ruolo. Tableline esegue solo letture e non salva copie delle tabelle; esportazioni, cronologia SQL e richieste esplicite all'assistente possono comunque contenere dati. diff --git a/docs/PLATFORMS.md b/docs/PLATFORMS.md index 9c02f15..be5ed70 100644 --- a/docs/PLATFORMS.md +++ b/docs/PLATFORMS.md @@ -11,7 +11,7 @@ Tableline 0.2.2 targets six native desktop combinations. Compatibility is tied t | Windows | x64 | Windows Server 2025 | | Windows | arm64 | Windows 11 ARM | -Electron 44.5.1 requires macOS 13 or later and supports 64-bit desktop targets. The runtime's [versioned platform requirements](https://github.com/electron/electron/blob/v44.5.1/README.md) are a minimum; the table identifies the systems actually exercised by CI. Linux checks use Ubuntu 24.04 with glibc and X11/Xvfb. GPU drivers and Wayland configurations are not certified. Alpine/musl, 32-bit systems, FreeBSD and old unsupported OS releases are outside this matrix. Windows Server CI does not establish every Windows desktop build or enterprise policy configuration. +Electron 44.10.0 requires macOS 13 or later and supports 64-bit desktop targets. The runtime's [versioned platform requirements](https://github.com/electron/electron/blob/v44.10.0/README.md) are a minimum; the table identifies the systems actually exercised by CI. Linux checks use Ubuntu 24.04 with glibc and X11/Xvfb. GPU drivers and Wayland configurations are not certified. Alpine/musl, 32-bit systems, FreeBSD and old unsupported OS releases are outside this matrix. Windows Server CI does not establish every Windows desktop build or enterprise policy configuration. ## Required checks diff --git a/docs/PUBLIC_RELEASE.md b/docs/PUBLIC_RELEASE.md index 921b16c..244e162 100644 --- a/docs/PUBLIC_RELEASE.md +++ b/docs/PUBLIC_RELEASE.md @@ -33,3 +33,5 @@ Download the published archive, verify its checksum, extract it and inspect its Before publishing the first GPL binary after v0.2.0, review the exact native dependency sources as well as Tableline's tagged archive. The optional Databricks kernel's exact compiled source was unavailable during the 0.2.1 audit. From 0.2.2, packages exclude that unused kernel and verify its absence. Tableline retains the connector's JavaScript Thrift backend; SEA/Reyden warehouses that require the kernel are explicitly unsupported. The supported route and native LZ4 binding must pass packaged runtime checks on the release target. LZ4 NAPI 2.10.0 identifies its source commit as [`ec869f7de3aab4d07cadfd6796d06af30e386fdc`](https://github.com/antoniomuso/lz4-napi/tree/ec869f7de3aab4d07cadfd6796d06af30e386fdc). Keep this exact source and its build dependencies, the pinned Electron/runtime sources and other required dependency sources accessible beside a release download. An npm tarball containing only native code is not a substitute for its source. Do not silently add a GPL exception or replace historical MIT assets to work around this requirement. A signed local build and a published source fix are separate from a public binary release. + +The [0.2.2 source references](releases/0.2.2-sources.md) identify the pinned upstream sources, build workflow and binary provenance, and disclose the upstream LZ4 Rust lockfile limitation. Link that manifest from the GitHub release notes along with the exact Tableline source tag. diff --git a/docs/VALIDATION.md b/docs/VALIDATION.md index df09408..69d0e16 100644 --- a/docs/VALIDATION.md +++ b/docs/VALIDATION.md @@ -66,6 +66,8 @@ The renderer is sandboxed with context isolation, no Node access, a restrictive On 5 October 2026, both the complete dependency audit and production audit recorded zero advisories after removing the build-only `got → cacheable-request → http-cache-semantics` chain. The override is limited to `app-builder-lib` 26.15.3's downloader, using the official `@electron/get` 5.1.0 already used by Electron. A build-process preload preserves request deadlines, proxy/NO_PROXY routing, progress and retry error codes while retaining upstream artifact caching and SHA-256 checks. TLS verification remains required. CI audits development dependencies as well as production dependencies. +On 31 October 2026, the new [minimatch advisory GHSA-g3hv-75wg-m249](https://github.com/isaacs/minimatch/security/advisories/GHSA-g3hv-75wg-m249) caused `npm audit` to report 15 moderate findings through the development-only Electron Builder dependency tree; `npm audit --omit=dev` still reports zero. The patched minimatch 11 API is incompatible with several legacy build-tool callers, so Tableline's audit gate permits only this advisory and its development-only transitive chain through 30 November 2026. `npm run audit` still runs both full and production audits, prints the findings and fails on any production finding, any other advisory, a higher severity, an unexpected dependency path or expiration. The exception does not apply to bundled application dependencies. + The published `http-cache-semantics` 4.3.0 still reproduced the synthetic cross-user `max-stale` behavior; moving beyond the advisory's version range alone was not accepted as remediation. The affected packages are absent from the resolved lockfile and installed tree. See [GHSA-ch52-4w7c-c8xp](https://github.com/advisories/GHSA-ch52-4w7c-c8xp) and the [upstream report](https://github.com/kornelski/http-cache-semantics/issues/56). Dependency licenses and public source/history scans are reviewed before publishing; generated reports and user data are excluded from Git. Local macOS arm64 acceptance after this change passed 328 unit tests (two optional tests skipped), an actual cold Electron download matched the reviewed upstream SHA-256, and the packaged app passed all nine platform checks, 35 locale checks and nine AI selection checks. The eleven loopback download regressions cover checksums, cache repair, HTTP 503 retry, stalled responses, cancellation, authenticated proxy routing, redirects and isolation between synthetic users. A separate temporary HTTPS fixture confirmed rejection of a self-signed certificate. These checks use synthetic credentials, make no macOS Keychain calls and do not establish Developer ID signing or public installer trust. diff --git a/docs/releases/0.2.2-sources.md b/docs/releases/0.2.2-sources.md new file mode 100644 index 0000000..37d124d --- /dev/null +++ b/docs/releases/0.2.2-sources.md @@ -0,0 +1,17 @@ +# Tableline 0.2.2 source references + +This manifest identifies source locations for the planned macOS arm64 release. It is not evidence that a binary has been notarized, published or built from the final tag. The release notes must link the exact `v0.2.2` tag and the final binary checksum before publication. + +| Component in the macOS arm64 app | Pinned source | Build and license information | +| --- | --- | --- | +| Tableline 0.2.2 (`GPL-3.0-only`) | [`Hexecu/tableline` tag `v0.2.2`](https://github.com/Hexecu/tableline/tree/v0.2.2) and its [source archive](https://github.com/Hexecu/tableline/archive/refs/tags/v0.2.2.tar.gz), once the tag is published | The tagged tree contains `package-lock.json`, `package.json`, `scripts/`, [`docs/MACOS.md`](../MACOS.md), [`docs/PUBLIC_RELEASE.md`](../PUBLIC_RELEASE.md), `LICENSE`, `COPYRIGHT` and `THIRD_PARTY_NOTICES.md`. The release must use that tree's contents. | +| Electron 44.10.0, including its macOS runtime | [Electron source commit `dce120a548ef263f34ad5952e1fc701981172209`](https://github.com/electron/electron/tree/dce120a548ef263f34ad5952e1fc701981172209) ([release `v44.10.0`](https://github.com/electron/electron/releases/tag/v44.10.0)) | Electron's release identifies Chromium 152.0.7977.169 and Node.js 24.22.1. The packaged Electron MIT license and Chromium notices are retained in `Tableline.app/Contents/Resources/licenses/`. | +| Chromium 152.0.7977.169 | [Chromium source tag `152.0.7977.169`](https://chromium.googlesource.com/chromium/src/+/refs/tags/152.0.7977.169) and its [`DEPS`](https://chromium.googlesource.com/chromium/src/+/refs/tags/152.0.7977.169/DEPS) | Use the tag and dependency manifest for the Chromium source included by Electron; its component notices remain in the app. | +| Node.js 24.22.1 | [Node.js source archive](https://nodejs.org/dist/v24.22.1/node-v24.22.1.tar.gz) and [release](https://nodejs.org/en/blog/release/v24.22.1) | Version identified by Electron 44.10.0's release notes. | +| LZ4 N-API 2.10.0 macOS arm64 binding (`MIT`) | [Upstream source commit `ec869f7de3aab4d07cadfd6796d06af30e386fdc`](https://github.com/antoniomuso/lz4-napi/tree/ec869f7de3aab4d07cadfd6796d06af30e386fdc) ([source archive](https://github.com/antoniomuso/lz4-napi/archive/ec869f7de3aab4d07cadfd6796d06af30e386fdc.tar.gz), [tag `v2.10.0`](https://github.com/antoniomuso/lz4-napi/releases/tag/v2.10.0)) | The source contains `Cargo.toml`, `src/`, `package.json`, `yarn.lock` and the [upstream build workflow](https://github.com/antoniomuso/lz4-napi/blob/ec869f7de3aab4d07cadfd6796d06af30e386fdc/.github/workflows/CI.yaml). The prebuilt npm `@antoniomuso/lz4-napi-darwin-arm64` package contains the binary, not its Rust source. | + +The native file installed by npm has SHA-256 `1fad8f56742362a9a7101ff19753d13dc429baee5009f2acf39756460535cab5`, matching both the [upstream `v2.10.0` macOS arm64 release asset](https://github.com/antoniomuso/lz4-napi/releases/download/v2.10.0/lz4-napi.darwin-arm64.node) and the [upstream CI run's arm64 build artifact](https://github.com/antoniomuso/lz4-napi/actions/runs/31935185318) byte for byte. The npm archive is pinned in Tableline's `package-lock.json` with integrity `sha512-rrP5B4CJkSgHBAZ8jP/xGdXULDjDMF/DiUPxtBdo23CfkIOvf2VYGDF2vWO+1LDFMqlV4Mu4dZWwvOWklP4t4Q==`. That CI run built from the pinned source commit with Node.js 24 and Rust stable, using `yarn install` and `yarn build:release --target aarch64-apple-darwin`. Tableline's signing step subsequently changes the native file's bytes; the npm-installed file is the verified matching input. + +To obtain the LZ4 source, download the commit archive above or check out that exact commit from the upstream repository. Its `Cargo.toml` identifies Rust dependencies and its `yarn.lock` identifies the JavaScript build dependencies; the linked workflow gives the build commands. The source tree does not contain `Cargo.lock`, and `Cargo.toml` allows version ranges for some Rust crates. The upstream CI logs show some resolved versions but do not record a complete Rust lockfile, so this manifest does not claim the exact Rust dependency resolution or a byte-identical rebuild of the prebuilt binding. The release notes should point to this manifest beside the binary download and retain access to these exact source references for as long as the binary is offered. + +The Databricks SQL JavaScript Thrift backend and FlatBuffers JavaScript runtime are included. Its optional native SQL kernel binary and platform package are excluded; `scripts/verify-package.cjs` checks the package inventory, and `scripts/platform-smoke.cjs` tests the supported backend and LZ4 binding in the packaged app. Other JavaScript dependency versions and npm package integrity hashes are fixed by Tableline's `package-lock.json`; their licenses and source locations remain under their package metadata and `THIRD_PARTY_NOTICES.md`. diff --git a/electron/ai-vault.cjs b/electron/ai-vault.cjs index cc39262..20a0e82 100644 --- a/electron/ai-vault.cjs +++ b/electron/ai-vault.cjs @@ -425,9 +425,11 @@ class AIVault { if (error.code === "SECURE_STORAGE_TIMEOUT") throw timeoutError(); if (error.code === "SECURE_STORAGE_BLOCKED") throw blockedError(); if (error.code === "SECURE_STORAGE_INSECURE") throw insecureError(); - throw new Error( + const unreadable = new Error( "Impossibile sbloccare le credenziali AI. Importale nuovamente nel profilo.", ); + unreadable.code = "SECURE_STORAGE_UNREADABLE"; + throw unreadable; } } async set(id, credentials) { diff --git a/electron/ai.cjs b/electron/ai.cjs index 3c76275..6a984e6 100644 --- a/electron/ai.cjs +++ b/electron/ai.cjs @@ -106,6 +106,18 @@ function profileId(id) { throw new Error("ID profilo non valido."); return id; } +function completeCredentialReplacement(profile, credentials) { + const mode = profile.authMode || + (profile.provider === "vertex" ? "adc" + : profile.provider === "bedrock" ? (credentials.apiKey ? "apiKey" : "aws") + : ["ollama", "compatible"].includes(profile.provider) ? "none" : "apiKey"); + if (mode === "apiKey") return !!credentials.apiKey; + if (mode === "bearer") return !!credentials.bearerToken; + if (mode === "aws") + return !!credentials.accessKeyId && !!credentials.secretAccessKey; + if (mode === "serviceAccount") return !!credentials.serviceAccount; + return false; +} function loopback(hostname) { const host = hostname.toLowerCase().replace(/\.$/, ""); return ( @@ -424,7 +436,7 @@ class AIService { this.remember(result); return result; } - async backupCredentials(id, readCredentials = true) { + async backupCredentials(id, readCredentials = true, allowUnreadableReplacement = false) { const key = this.vaultId(id); const encryptedRollback = typeof this.vault.snapshot === "function" && typeof this.vault.restore === "function"; @@ -435,9 +447,12 @@ class AIService { if (readCredentials || !encryptedRollback) { try { credentials = await this.credentials(id); } catch (error) { - if (error.code !== "SECURE_STORAGE_INSECURE" || !encryptedRollback) + if (!encryptedRollback || ( + error.code !== "SECURE_STORAGE_INSECURE" && + !(allowUnreadableReplacement && error.code === "SECURE_STORAGE_UNREADABLE") + )) throw error; - // An explicit replacement cannot retain a secret from unsafe ciphertext. + // A complete replacement cannot retain a secret from unreadable ciphertext. } } return { key, credentials, encryptedRollback, ciphertext }; @@ -483,7 +498,11 @@ class AIService { const clean = this.cleanCredentials(credentials); // Empty form fields are a metadata-only edit; an explicit {} clears. if (Object.keys(clean).length || !Object.keys(credentials).length) { - backup = await this.backupCredentials(profile.id, Object.keys(clean).length > 0); + backup = await this.backupCredentials( + profile.id, + Object.keys(clean).length > 0, + completeCredentialReplacement(profile, clean), + ); expected = Object.keys(clean).length ? await this.vault.set(backup.key, { ...backup.credentials, ...clean }) : await this.vault.delete(backup.key); diff --git a/electron/database.cjs b/electron/database.cjs index 58e73b7..34845ce 100644 --- a/electron/database.cjs +++ b/electron/database.cjs @@ -8,6 +8,7 @@ const crypto = require("node:crypto"); const { performance } = require("node:perf_hooks"); const { guardSql, quoteIdentifier } = require("./sql-guard.cjs"); const { SQLiteDriver } = require("./drivers/sqlite.cjs"); +const { AthenaDriver } = require("./drivers/athena.cjs"); const { PostgresDriver, MySQLDriver, @@ -63,6 +64,22 @@ const NETWORK_FIELDS = [ ]; const CATALOG = [ descriptor("sqlite", "SQLite", "sqlite", null, ["filePath"]), + descriptor( + "athena", + "Amazon Athena", + "athena", + null, + ["awsProfile", "region", "roleArn", "workgroup", "database"], + { + write: false, + guardedWrites: false, + transactions: false, + transactional: false, + cancel: true, + writePreview: "unsupported", + parameters: "?", + }, + ), descriptor("postgres", "PostgreSQL", "postgres", 5432, NETWORK_FIELDS), descriptor( "aurora-postgresql", @@ -154,6 +171,7 @@ const CATALOG = [ ]; const DRIVER_TYPES = { sqlite: SQLiteDriver, + athena: AthenaDriver, postgres: PostgresDriver, mysql: MySQLDriver, sqlserver: SQLServerDriver, @@ -163,7 +181,7 @@ const DRIVER_TYPES = { redis: RedisDriver, }; const PROFILE_KEYS = new Set( - "id name driver host port database username filePath httpPath catalog schema ssl tls sslCA color readOnly".split( + "id name driver host port database username filePath httpPath catalog schema ssl tls sslCA color readOnly awsProfile region roleArn workgroup".split( " ", ), ); diff --git a/electron/drivers/athena.cjs b/electron/drivers/athena.cjs new file mode 100644 index 0000000..4d781fe --- /dev/null +++ b/electron/drivers/athena.cjs @@ -0,0 +1,336 @@ +// Copyright (c) 2026 Davide Leopardi +// SPDX-License-Identifier: GPL-3.0-only + +"use strict"; + +const { guardSql, quoteIdentifier } = require("../sql-guard.cjs"); +const { boundedSql } = require("./sql.cjs"); + +const CATALOG = "AwsDataCatalog"; +const QUERY_TIMEOUT_MS = 90000; +const RESULT_BYTES_LIMIT = 10 * 1024 * 1024; + +function executionParameters(params) { + if (!Array.isArray(params)) + throw new Error("Athena uses positional parameters (?)."); + return params.map((value) => { + if (value === null) return "NULL"; + if (typeof value === "boolean") return value ? "TRUE" : "FALSE"; + if (typeof value === "number" && Number.isFinite(value)) + return String(value); + if (typeof value === "string" && !value.includes("\0")) { + const quoted = "'" + value.replaceAll("'", "''") + "'"; + if (quoted.length <= 1024) return quoted; + } + throw new Error("Athena parameters must be short scalar values."); + }); +} + +class AthenaDriver { + constructor(profile, credentials = {}, sdk) { + this.profile = profile; + this.credentials = credentials; + this.sdk = sdk; + this.dialect = "athena"; + this.transactional = false; + this.active = new Map(); + this.inFlight = new Set(); + this.cachedSchema = null; + this.timeoutMs = QUERY_TIMEOUT_MS; + } + + async connect() { + const { awsProfile, region, roleArn, workgroup } = this.profile; + if (!awsProfile || !region || !workgroup) + throw new Error("Athena requires an AWS profile, region, and workgroup."); + const sdk = this.sdk || { + ...require("@aws-sdk/client-athena"), + ...require("@aws-sdk/credential-providers"), + }; + const masterCredentials = sdk.fromIni({ + profile: awsProfile, + clientConfig: { region }, + }); + const credentials = roleArn + ? sdk.fromTemporaryCredentials({ + masterCredentials, + clientConfig: { region }, + params: { RoleArn: roleArn, RoleSessionName: "tableline-athena" }, + }) + : masterCredentials; + this.sdk = sdk; + this.client = new sdk.AthenaClient({ + region, + credentials, + maxAttempts: 2, + logger: { trace() {}, debug() {}, info() {}, warn() {}, error() {} }, + }); + try { + const result = await this.client.send( + new sdk.GetWorkGroupCommand({ WorkGroup: workgroup }), + ); + if (result.WorkGroup?.State === "DISABLED") + throw new Error("Athena workgroup is disabled."); + } catch (error) { + this.client.destroy(); + this.client = null; + throw error; + } + } + + async schema() { + if (!this.client) throw new Error("Athena is not connected."); + const { ListDatabasesCommand, ListTableMetadataCommand } = this.sdk; + const workgroup = this.profile.workgroup; + const databases = []; + if (this.profile.database) databases.push(this.profile.database); + else { + let next; + do { + const result = await this.client.send( + new ListDatabasesCommand({ + CatalogName: CATALOG, + WorkGroup: workgroup, + MaxResults: 50, + ...(next ? { NextToken: next } : {}), + }), + ); + databases.push(...(result.DatabaseList || []).map((db) => db.Name)); + if (databases.length > 100) + throw new Error("Athena schema has too many databases."); + next = result.NextToken; + } while (next); + } + const tables = []; + for (const database of databases) { + let next; + do { + let result; + try { + result = await this.client.send( + new ListTableMetadataCommand({ + CatalogName: CATALOG, + DatabaseName: database, + WorkGroup: workgroup, + MaxResults: 50, + ...(next ? { NextToken: next } : {}), + }), + ); + } catch (error) { + // An unrestricted database list can include databases outside a + // narrow read role. A specifically selected database must still fail. + if (!this.profile.database && /accessdenied/i.test(error.name || "")) + break; + throw error; + } + for (const table of result.TableMetadataList || []) { + const columns = []; + for (const column of [ + ...(table.Columns || []), + ...(table.PartitionKeys || []), + ]) { + if (!columns.some((current) => current.name === column.Name)) + columns.push({ + name: column.Name, + type: column.Type || "string", + nullable: true, + primaryKey: false, + }); + } + tables.push({ + name: table.Name, + schema: database, + kind: /view/i.test(table.TableType || "") ? "view" : "table", + columns, + }); + } + if (tables.length > 5000) + throw new Error("Athena schema has too many tables."); + next = result.NextToken; + } while (next); + } + this.cachedSchema = tables; + return tables; + } + + async read(sql, params = [], limit = 500, options = {}) { + const statement = guardSql(sql, "read", "athena").sql; + return this.executeRead(statement, params, limit, options); + } + + async executeRead(sql, params = [], limit = 500, { requestId, alreadyBound = false } = {}) { + if (!this.client) throw new Error("Athena is not connected."); + if (!Number.isInteger(limit) || limit < 1 || limit > 10000) + throw new Error("Athena result limit must be between 1 and 10000."); + const bound = executionParameters(params); + const sdk = this.sdk; + const response = await this.client.send( + new sdk.StartQueryExecutionCommand({ + QueryString: alreadyBound ? sql : boundedSql(sql, "athena", limit), + WorkGroup: this.profile.workgroup, + QueryExecutionContext: { + Catalog: CATALOG, + Database: this.profile.database || "default", + }, + ...(bound.length ? { ExecutionParameters: bound } : {}), + }), + ); + const id = response.QueryExecutionId; + if (!id) throw new Error("Athena did not return a query ID."); + this.inFlight.add(id); + if (requestId) this.active.set(requestId, id); + const deadline = Date.now() + this.timeoutMs; + try { + let delayMs = 300; + while (true) { + const result = await this.client.send( + new sdk.GetQueryExecutionCommand({ QueryExecutionId: id }), + ); + const state = result.QueryExecution?.Status?.State; + if (state === "SUCCEEDED") break; + if (state === "FAILED" || state === "CANCELLED") + throw new Error( + `Athena query ${state.toLowerCase()}: ${result.QueryExecution?.Status?.StateChangeReason || "unknown reason"}`, + ); + if (Date.now() >= deadline) { + const stopped = await this.stop(id); + throw new Error(stopped + ? "Athena query timed out and was stopped." + : "Athena query timed out; it could not be stopped."); + } + await new Promise((resolve) => setTimeout(resolve, delayMs)); + delayMs = Math.min(Math.round(delayMs * 1.5), 2000); + } + return await this.results(id, limit + 1); + } finally { + this.inFlight.delete(id); + if (requestId && this.active.get(requestId) === id) + this.active.delete(requestId); + } + } + + async results(id, maxRows) { + const columns = []; + const rows = []; + let next; + let firstPage = true; + let bytes = 0; + do { + const result = await this.client.send( + new this.sdk.GetQueryResultsCommand({ + QueryExecutionId: id, + MaxResults: Math.min(1000, maxRows - rows.length + (firstPage ? 1 : 0)), + ...(next ? { NextToken: next } : {}), + }), + ); + if (firstPage) { + for (const column of result.ResultSet?.ResultSetMetadata?.ColumnInfo || []) + columns.push({ name: column.Name, type: column.Type }); + } + const pageRows = result.ResultSet?.Rows || []; + for (const row of firstPage && columns.length ? pageRows.slice(1) : pageRows) { + const value = {}; + columns.forEach((column, index) => { + value[column.name] = row.Data?.[index]?.VarCharValue ?? null; + }); + bytes += Buffer.byteLength(JSON.stringify(value), "utf8"); + if (bytes > RESULT_BYTES_LIMIT) + throw new Error("Result exceeds 10 MB. Select fewer columns or rows."); + rows.push(value); + if (rows.length >= maxRows) break; + } + firstPage = false; + next = result.NextToken; + } while (next && rows.length < maxRows); + return { columns, rows }; + } + + async browse({ + table, + schema, + search = "", + sortColumn, + sortDirection = "asc", + offset = 0, + limit = 100, + } = {}) { + if (!Number.isInteger(offset) || offset < 0 || offset > 1000000) + throw new Error("Offset must be between 0 and 1000000."); + if (!Number.isInteger(limit) || limit < 1 || limit > 1000) + throw new Error("Limit must be between 1 and 1000."); + if (typeof search !== "string" || search.length > 500) + throw new Error("Search must be text of up to 500 characters."); + if (!["asc", "desc"].includes(sortDirection)) + throw new Error("Choose ascending or descending sort."); + const tables = this.cachedSchema || (await this.schema()); + const selected = tables.find( + (candidate) => candidate.name === table && (!schema || candidate.schema === schema), + ); + if (!selected) throw new Error("Table not found. Refresh the schema."); + if (sortColumn && !selected.columns.some((column) => column.name === sortColumn)) + throw new Error("Sort column not found."); + const q = (name) => quoteIdentifier(name, "athena"); + const fullName = `${q(selected.schema)}.${q(selected.name)}`; + const searchable = selected.columns + .filter((column) => /^(?:varchar|char|string|text)\b/i.test(column.type)) + .slice(0, 20); + if (search && !searchable.length) + return { + columns: selected.columns.map(({ name, type }) => ({ name, type })), + rows: [], + }; + const where = search + ? " WHERE " + searchable.map((column) => + `strpos(lower(${q(column.name)}), lower(?)) > 0`, + ).join(" OR ") + : ""; + const ordering = sortColumn + ? ` ORDER BY ${q(sortColumn)} ${sortDirection.toUpperCase()}` + : ""; + const sql = `SELECT * FROM ${fullName}${where}${ordering} OFFSET ${offset} LIMIT ${limit + 1}`; + const raw = await this.executeRead( + sql, + search ? searchable.map(() => search) : [], + limit, + { alreadyBound: true }, + ); + raw.columns = raw.columns.length + ? raw.columns.map((column) => ({ + ...column, + type: selected.columns.find((item) => item.name === column.name)?.type || column.type, + })) + : selected.columns.map(({ name, type }) => ({ name, type })); + return raw; + } + + async stop(id) { + if (!this.client) return false; + try { + await this.client.send( + new this.sdk.StopQueryExecutionCommand({ QueryExecutionId: id }), + ); + return true; + } catch { + return false; + } + } + + async cancel(requestId) { + const id = this.active.get(requestId); + if (!id) return false; + return this.stop(id); + } + + async abortReads() { + await Promise.all([...this.inFlight].map((id) => this.stop(id))); + } + + async close() { + await this.abortReads(); + this.client?.destroy(); + this.client = null; + this.cachedSchema = null; + } +} + +module.exports = { AthenaDriver, executionParameters }; diff --git a/locales/de.json b/locales/de.json index 866a6d0..0ca8347 100644 --- a/locales/de.json +++ b/locales/de.json @@ -56,6 +56,11 @@ "Esporta CSV": "CSV exportieren", "Esporta JSON": "JSON exportieren", "Dettaglio riga": "Zeilendetails", + "Visualizza dato": "Wert anzeigen", + "Copia valore": "Wert kopieren", + "Copia riga come JSON": "Zeile als JSON kopieren", + "Valore della cella": "Zellenwert", + "Chiudi valore": "Wertansicht schließen", "Query non riuscita": "Abfrage fehlgeschlagen", "Chiudi errore": "Fehler schließen", "Apro": "Öffnen von", @@ -522,5 +527,23 @@ "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.": "Die Abfrage hat keine Zeilen zurückgegeben. Prüfe den Filter, bevor du daraus schließt, dass die gesuchten Daten fehlen.", "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.": "Dieses Databricks-Warehouse benötigt den nativen SEA-Kernel. Tableline unterstützt Thrift-SQL-Warehouses; wählen Sie ein kompatibles Warehouse.", "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.": "Die Verschlüsselung der Zugangsdaten unter Linux ist nicht sicher. Entsperren Sie den Schlüsselbund, öffnen Sie die App erneut und importieren Sie die Zugangsdaten noch einmal.", - "Formato archivio credenziali non valido.": "Ungültiges Format des Zugangsdatenarchivs." + "Formato archivio credenziali non valido.": "Ungültiges Format des Zugangsdatenarchivs.", + "Workgroup Athena": "Athena-Arbeitsgruppe", + "ARN ruolo IAM (opzionale)": "IAM-Rollen-ARN (optional)", + "Database predefinito (opzionale)": "Standarddatenbank (optional)", + "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.": "Verwendet Ihre lokale AWS-SSO-Anmeldung. Temporäre Anmeldedaten werden automatisch erneuert.", + "Athena uses positional parameters (?).": "Athena verwendet positionsgebundene Parameter (?).", + "Athena parameters must be short scalar values.": "Athena-Parameter müssen kurze skalare Werte sein.", + "Athena requires an AWS profile, region, and workgroup.": "Athena erfordert ein AWS-Profil, eine Region und eine Arbeitsgruppe.", + "Athena workgroup is disabled.": "Die Athena-Arbeitsgruppe ist deaktiviert.", + "Athena is not connected.": "Athena ist nicht verbunden.", + "Athena schema has too many databases.": "Das Athena-Schema enthält zu viele Datenbanken.", + "Athena schema has too many tables.": "Das Athena-Schema enthält zu viele Tabellen.", + "Athena result limit must be between 1 and 10000.": "Das Athena-Ergebnislimit muss zwischen 1 und 10000 liegen.", + "Athena did not return a query ID.": "Athena hat keine Abfrage-ID zurückgegeben.", + "Athena query {state}: {reason}": "Athena-Abfrage {state}: {reason}", + "Athena query timed out and was stopped.": "Die Athena-Abfrage hat das Zeitlimit überschritten und wurde gestoppt.", + "Athena query timed out; it could not be stopped.": "Die Athena-Abfrage hat das Zeitlimit überschritten; sie konnte nicht gestoppt werden.", + "Choose ascending or descending sort.": "Wählen Sie aufsteigende oder absteigende Sortierung.", + "Limit must be between 1 and 1000.": "Das Limit muss zwischen 1 und 1000 liegen." } diff --git a/locales/en.json b/locales/en.json index 970a103..f83b77b 100644 --- a/locales/en.json +++ b/locales/en.json @@ -56,6 +56,11 @@ "Esporta CSV": "Export CSV", "Esporta JSON": "Export JSON", "Dettaglio riga": "Row details", + "Visualizza dato": "View value", + "Copia valore": "Copy value", + "Copia riga come JSON": "Copy row as JSON", + "Valore della cella": "Cell value", + "Chiudi valore": "Close value", "Query non riuscita": "Query failed", "Chiudi errore": "Dismiss error", "Apro": "Opening", @@ -522,5 +527,23 @@ "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.": "The query returned no rows. Check the filter before concluding that the requested data is absent.", "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.": "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.", "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.": "Linux credential encryption is not secure. Unlock the keyring, reopen the app and import the credentials again.", - "Formato archivio credenziali non valido.": "Invalid credential archive format." + "Formato archivio credenziali non valido.": "Invalid credential archive format.", + "Workgroup Athena": "Athena workgroup", + "ARN ruolo IAM (opzionale)": "IAM role ARN (optional)", + "Database predefinito (opzionale)": "Default database (optional)", + "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.": "Uses your local AWS SSO login. Temporary credentials refresh automatically.", + "Athena uses positional parameters (?).": "Athena uses positional parameters (?).", + "Athena parameters must be short scalar values.": "Athena parameters must be short scalar values.", + "Athena requires an AWS profile, region, and workgroup.": "Athena requires an AWS profile, region, and workgroup.", + "Athena workgroup is disabled.": "Athena workgroup is disabled.", + "Athena is not connected.": "Athena is not connected.", + "Athena schema has too many databases.": "Athena schema has too many databases.", + "Athena schema has too many tables.": "Athena schema has too many tables.", + "Athena result limit must be between 1 and 10000.": "Athena result limit must be between 1 and 10000.", + "Athena did not return a query ID.": "Athena did not return a query ID.", + "Athena query {state}: {reason}": "Athena query {state}: {reason}", + "Athena query timed out and was stopped.": "Athena query timed out and was stopped.", + "Athena query timed out; it could not be stopped.": "Athena query timed out; it could not be stopped.", + "Choose ascending or descending sort.": "Choose ascending or descending sort.", + "Limit must be between 1 and 1000.": "Limit must be between 1 and 1000." } diff --git a/locales/es.json b/locales/es.json index 9c73e35..aa10f4d 100644 --- a/locales/es.json +++ b/locales/es.json @@ -56,6 +56,11 @@ "Esporta CSV": "Exportar CSV", "Esporta JSON": "Exportar JSON", "Dettaglio riga": "Detalles de fila", + "Visualizza dato": "Ver valor", + "Copia valore": "Copiar valor", + "Copia riga come JSON": "Copiar fila como JSON", + "Valore della cella": "Valor de la celda", + "Chiudi valore": "Cerrar valor", "Query non riuscita": "Consulta fallida", "Chiudi errore": "Cerrar error", "Apro": "Abriendo", @@ -522,5 +527,23 @@ "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.": "La consulta no devolvió filas. Revisa el criterio antes de concluir que los datos solicitados no existen.", "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.": "Este almacén Databricks requiere el núcleo nativo SEA. Tableline admite almacenes SQL Thrift; elige un almacén compatible.", "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.": "El cifrado de credenciales de Linux no es seguro. Desbloquea el llavero, vuelve a abrir la aplicación e importa las credenciales de nuevo.", - "Formato archivio credenziali non valido.": "Formato de archivo de credenciales no válido." + "Formato archivio credenziali non valido.": "Formato de archivo de credenciales no válido.", + "Workgroup Athena": "Grupo de trabajo de Athena", + "ARN ruolo IAM (opzionale)": "ARN del rol IAM (opcional)", + "Database predefinito (opzionale)": "Base de datos predeterminada (opcional)", + "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.": "Usa el inicio de sesión AWS SSO local. Las credenciales temporales se renuevan automáticamente.", + "Athena uses positional parameters (?).": "Athena usa parámetros posicionales (?).", + "Athena parameters must be short scalar values.": "Los parámetros de Athena deben ser valores escalares cortos.", + "Athena requires an AWS profile, region, and workgroup.": "Athena requiere un perfil de AWS, una región y un grupo de trabajo.", + "Athena workgroup is disabled.": "El grupo de trabajo de Athena está deshabilitado.", + "Athena is not connected.": "Athena no está conectado.", + "Athena schema has too many databases.": "El esquema de Athena contiene demasiadas bases de datos.", + "Athena schema has too many tables.": "El esquema de Athena contiene demasiadas tablas.", + "Athena result limit must be between 1 and 10000.": "El límite de resultados de Athena debe estar entre 1 y 10000.", + "Athena did not return a query ID.": "Athena no devolvió un ID de consulta.", + "Athena query {state}: {reason}": "Consulta de Athena {state}: {reason}", + "Athena query timed out and was stopped.": "La consulta de Athena agotó el tiempo y se detuvo.", + "Athena query timed out; it could not be stopped.": "La consulta de Athena agotó el tiempo; no se pudo detener.", + "Choose ascending or descending sort.": "Elige orden ascendente o descendente.", + "Limit must be between 1 and 1000.": "El límite debe estar entre 1 y 1000." } diff --git a/locales/fr.json b/locales/fr.json index ca029de..13e8971 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -56,6 +56,11 @@ "Esporta CSV": "Exporter en CSV", "Esporta JSON": "Exporter en JSON", "Dettaglio riga": "Détails de la ligne", + "Visualizza dato": "Afficher la valeur", + "Copia valore": "Copier la valeur", + "Copia riga come JSON": "Copier la ligne en JSON", + "Valore della cella": "Valeur de la cellule", + "Chiudi valore": "Fermer la valeur", "Query non riuscita": "Échec de la requête", "Chiudi errore": "Fermer l’erreur", "Apro": "Ouverture de", @@ -522,5 +527,23 @@ "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.": "La requête n’a renvoyé aucune ligne. Vérifiez le critère avant de conclure que les données demandées sont absentes.", "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.": "Cet entrepôt Databricks nécessite le noyau natif SEA. Tableline prend en charge les entrepôts SQL Thrift ; choisissez un entrepôt compatible.", "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.": "Le chiffrement des identifiants sous Linux n’est pas sûr. Déverrouillez le trousseau, rouvrez l’application et importez à nouveau les identifiants.", - "Formato archivio credenziali non valido.": "Format de l’archive des identifiants non valide." + "Formato archivio credenziali non valido.": "Format de l’archive des identifiants non valide.", + "Workgroup Athena": "Groupe de travail Athena", + "ARN ruolo IAM (opzionale)": "ARN du rôle IAM (facultatif)", + "Database predefinito (opzionale)": "Base de données par défaut (facultatif)", + "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.": "Utilise votre connexion AWS SSO locale. Les identifiants temporaires sont renouvelés automatiquement.", + "Athena uses positional parameters (?).": "Athena utilise des paramètres positionnels (?).", + "Athena parameters must be short scalar values.": "Les paramètres Athena doivent être de courtes valeurs scalaires.", + "Athena requires an AWS profile, region, and workgroup.": "Athena nécessite un profil AWS, une région et un groupe de travail.", + "Athena workgroup is disabled.": "Le groupe de travail Athena est désactivé.", + "Athena is not connected.": "Athena n’est pas connecté.", + "Athena schema has too many databases.": "Le schéma Athena contient trop de bases de données.", + "Athena schema has too many tables.": "Le schéma Athena contient trop de tables.", + "Athena result limit must be between 1 and 10000.": "La limite des résultats Athena doit être comprise entre 1 et 10000.", + "Athena did not return a query ID.": "Athena n’a pas renvoyé d’identifiant de requête.", + "Athena query {state}: {reason}": "Requête Athena {state} : {reason}", + "Athena query timed out and was stopped.": "La requête Athena a expiré et a été arrêtée.", + "Athena query timed out; it could not be stopped.": "La requête Athena a expiré ; impossible de l’arrêter.", + "Choose ascending or descending sort.": "Choisissez un tri croissant ou décroissant.", + "Limit must be between 1 and 1000.": "La limite doit être comprise entre 1 et 1000." } diff --git a/locales/it.json b/locales/it.json index 7da3e15..e31ded7 100644 --- a/locales/it.json +++ b/locales/it.json @@ -56,6 +56,11 @@ "Esporta CSV": "Esporta CSV", "Esporta JSON": "Esporta JSON", "Dettaglio riga": "Dettaglio riga", + "Visualizza dato": "Visualizza dato", + "Copia valore": "Copia valore", + "Copia riga come JSON": "Copia riga come JSON", + "Valore della cella": "Valore della cella", + "Chiudi valore": "Chiudi valore", "Query non riuscita": "Query non riuscita", "Chiudi errore": "Chiudi errore", "Apro": "Apro", @@ -522,5 +527,23 @@ "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.": "La query non ha restituito righe. Verifica il criterio prima di concludere che i dati richiesti siano assenti.", "This Databricks warehouse requires the SEA native kernel. Tableline supports Thrift SQL warehouses; choose a compatible warehouse.": "Questo warehouse Databricks richiede il kernel nativo SEA. Tableline supporta i warehouse SQL Thrift; scegli un warehouse compatibile.", "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.": "La cifratura credenziali di Linux non è sicura. Sblocca il portachiavi, riapri l’app e importa nuovamente le credenziali.", - "Formato archivio credenziali non valido.": "Formato archivio credenziali non valido." + "Formato archivio credenziali non valido.": "Formato archivio credenziali non valido.", + "Workgroup Athena": "Workgroup Athena", + "ARN ruolo IAM (opzionale)": "ARN ruolo IAM (opzionale)", + "Database predefinito (opzionale)": "Database predefinito (opzionale)", + "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.": "Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.", + "Athena uses positional parameters (?).": "Athena usa parametri posizionali (?).", + "Athena parameters must be short scalar values.": "I parametri Athena devono essere valori scalari brevi.", + "Athena requires an AWS profile, region, and workgroup.": "Athena richiede un profilo AWS, una regione e un workgroup.", + "Athena workgroup is disabled.": "Il workgroup Athena è disabilitato.", + "Athena is not connected.": "Athena non è connesso.", + "Athena schema has too many databases.": "Lo schema Athena contiene troppi database.", + "Athena schema has too many tables.": "Lo schema Athena contiene troppe tabelle.", + "Athena result limit must be between 1 and 10000.": "Il limite dei risultati Athena deve essere tra 1 e 10000.", + "Athena did not return a query ID.": "Athena non ha restituito un ID query.", + "Athena query {state}: {reason}": "Query Athena {state}: {reason}", + "Athena query timed out and was stopped.": "La query Athena è scaduta ed è stata fermata.", + "Athena query timed out; it could not be stopped.": "La query Athena è scaduta; non è stato possibile fermarla.", + "Choose ascending or descending sort.": "Scegli l’ordinamento crescente o decrescente.", + "Limit must be between 1 and 1000.": "Il limite deve essere tra 1 e 1000." } diff --git a/package-lock.json b/package-lock.json index 0ce9be3..0b8e127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.2.2", "license": "GPL-3.0-only", "dependencies": { + "@aws-sdk/client-athena": "^3.1161.0", "@aws-sdk/client-bedrock": "^3.1145.0", "@aws-sdk/client-bedrock-runtime": "^3.1145.0", "@aws-sdk/credential-providers": "^3.1145.0", @@ -28,12 +29,12 @@ "@types/node": "^24.0.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", - "electron": "44.5.1", + "electron": "44.10.0", "electron-builder": "^26.0.12", "playwright": "^1.56.0", "typescript": "^5.9.3", - "undici": "7.30.0", - "vite": "8.3.2" + "undici": "7.30.1", + "vite": "8.3.5" }, "engines": { "node": ">=22.13.0" @@ -341,6 +342,25 @@ "node": ">= 10" } }, + "node_modules/@aws-sdk/client-athena": { + "version": "3.1161.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-athena/-/client-athena-3.1161.0.tgz", + "integrity": "sha512-cmqvur4c4xEXSI2KHLDfW6xn0dPWjZh/MDshyxlArBb9u5esSnEllarREZxKuN2qB6YvkDwRiIStppJO020bpg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/credential-provider-node": "^3.972.92", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", + "@smithy/fetch-http-handler": "^5.8.0", + "@smithy/node-http-handler": "^4.14.0", + "@smithy/types": "^4.19.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, "node_modules/@aws-sdk/client-bedrock": { "version": "3.1146.0", "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock/-/client-bedrock-3.1146.0.tgz", @@ -385,16 +405,16 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.978.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.1.tgz", - "integrity": "sha512-LbY9aGsEiznDWmUc30Nwv3aIX/+dbwTx8KfS0yOC3NPYMO+O91e6jkT1azf34FwjOndq8/Q+RcVVZz5xnerwdg==", + "version": "3.979.1", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.979.1.tgz", + "integrity": "sha512-ayOlV1VhLNMiq8PW9ZkVobG4gkldnUoHjG69yy2b8j+VYFlhz/uAL7ZdYQAoQ+eShtGUdNDnnG5rxDg6yUJrOg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.974.6", - "@aws-sdk/xml-builder": "^3.972.41", + "@aws-sdk/types": "^3.975.0", + "@aws-sdk/xml-builder": "^3.972.42", "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.35.0", - "@smithy/signature-v4": "^5.7.3", + "@smithy/core": "^3.38.0", + "@smithy/signature-v4": "^5.7.5", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -420,14 +440,14 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.72.tgz", - "integrity": "sha512-xTKO/FWJPozTIXbozVnVGoNBhaGba8TBcx+KyUjRVeOlXE+dUc7GTR1cLvu0uTdIdmemzaFbqqCshXeZA1fZew==", + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.80.tgz", + "integrity": "sha512-uO5TiSvb6j1w9RTe5z28OaOmsrOxVQUPaC5+wZl50RfOXcrYD/KgnS8I9lfyGXc7sGbkCopK6VnhNIAoLqfewQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -436,16 +456,16 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.74", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.74.tgz", - "integrity": "sha512-u91E/hT8f4d1xy0Jl7VG4nVKJ3lxbrZkoBTeSVoJdWBiSEUMwMS/9+e0H/aJVQV//Lt5wuzP+E69v4aRSsNTmw==", + "version": "3.972.82", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.82.tgz", + "integrity": "sha512-vQzlDHlC24Yz42p+rT4NBoP/5VdetkBoujbOLq9h6QYWC8THuZSPYnXIR9OP3XcOrdMgPBBipOrdssfxnIiWUg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/fetch-http-handler": "^5.8.0", - "@smithy/node-http-handler": "^4.12.1", + "@smithy/node-http-handler": "^4.14.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -454,21 +474,21 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.17", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.17.tgz", - "integrity": "sha512-ged4KXdBkvIC81bLvNHHuQKdKak/VXhQTR1NWYTTqW0474nlmsxy9O/vlgTIohDDWH3xpBdtVMZRyjb+DnocDA==", + "version": "3.973.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.25.tgz", + "integrity": "sha512-b5xl71uCRdWsu7P1VeGpiGuCf6zfYETsSNOv8AmYVTrsgbvTJRXdmmOPVfxkvGGVNsiKkGctClQW3jAn0SZLug==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/credential-provider-env": "^3.972.72", - "@aws-sdk/credential-provider-http": "^3.972.74", - "@aws-sdk/credential-provider-login": "^3.972.79", - "@aws-sdk/credential-provider-process": "^3.972.72", - "@aws-sdk/credential-provider-sso": "^3.973.16", - "@aws-sdk/credential-provider-web-identity": "^3.972.78", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/credential-provider-env": "^3.972.80", + "@aws-sdk/credential-provider-http": "^3.972.82", + "@aws-sdk/credential-provider-login": "^3.972.87", + "@aws-sdk/credential-provider-process": "^3.972.80", + "@aws-sdk/credential-provider-sso": "^3.973.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.86", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -478,15 +498,15 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.79", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.79.tgz", - "integrity": "sha512-L+Z85anONJd8MaiuraO4wRxATCdEejBZ3K3eymzWI5JPXa9sOS9CkIm72PBKqXKX+Z9p9NGMX5AIMXm0LEflgw==", + "version": "3.972.87", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.87.tgz", + "integrity": "sha512-C4ZFmcwM+Zj14Z8huVUk54Gv4uzT1ZdzG9cY3Gdh8FePakq7Bss1OFsgZjAhWFOkFOGpBvFBeu0OWjWf8HNKRQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -495,19 +515,19 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.84", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.84.tgz", - "integrity": "sha512-oHt854odINVwzwsh+c5x69j0ajm4DbqqqVJ+O1ECsCIZeMDAbzFpXItaqP7UZstJj/ATdTk/KFSH0LaNAgV+kA==", + "version": "3.972.92", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.92.tgz", + "integrity": "sha512-jC3zmOo7Qxlk+oBivvSsS1Ph7sFGm0QfuhqDGbqgRXi4cxUYMG+dWtBwS2XYYXgc8TIVXJiwgQ65sbNpbYupTA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.72", - "@aws-sdk/credential-provider-http": "^3.972.74", - "@aws-sdk/credential-provider-ini": "^3.973.17", - "@aws-sdk/credential-provider-process": "^3.972.72", - "@aws-sdk/credential-provider-sso": "^3.973.16", - "@aws-sdk/credential-provider-web-identity": "^3.972.78", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/credential-provider-env": "^3.972.80", + "@aws-sdk/credential-provider-http": "^3.972.82", + "@aws-sdk/credential-provider-ini": "^3.973.25", + "@aws-sdk/credential-provider-process": "^3.972.80", + "@aws-sdk/credential-provider-sso": "^3.973.24", + "@aws-sdk/credential-provider-web-identity": "^3.972.86", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" @@ -517,14 +537,14 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.72", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.72.tgz", - "integrity": "sha512-rLIp2xbMjX/k9/od7APpqq1ZgXXnV0pOL1Th3ZsL8Wu0TRtBsDTVS8iPqcfRFcHakFxPvR04OSTv2ka2qOb/2A==", + "version": "3.972.80", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.80.tgz", + "integrity": "sha512-xqfixO5aclje7tbQJDias+iNND5wp/3QZixCdZcVQpgWPTZ0V5DfTW9A804A5Z7ezu7sH3WRO+vzUjtiJBA8yg==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -533,16 +553,16 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.16.tgz", - "integrity": "sha512-IGihaJfFZYacJJr/odqILCoK7W/mvrZ7cuK7ECn3sAu4vLC6u0V8bS7mCGbdugJ8Aum2tnvqmx0F2MRFp2rn9g==", + "version": "3.973.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.24.tgz", + "integrity": "sha512-nlg3BKG03Cgzvv/dMnhxvDJPhAhQSpaN0RUdwgU7ziwkasoev7iI8el8Xy4Rd5sqzgaDUbV269fOXck5SargqA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/token-providers": "3.1138.0", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/token-providers": "3.1161.0", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -551,15 +571,15 @@ } }, "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { - "version": "3.1138.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1138.0.tgz", - "integrity": "sha512-GpyAr0DD63YOEmYFM6Df+gJuIgC92MMTiBK4FTKfxii5MJ9ge20epR7LyroulscYlG89J+ZB2ivFDPjvfQhzdw==", + "version": "3.1161.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1161.0.tgz", + "integrity": "sha512-ypbXSofNcSHwf/KA40bFCXJb0MLaAKrfT7sot4dFaA+2lsuXNah4zLRJIAU7Qmvs9PFJ6rnY0bZFEC+mE7attA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -568,15 +588,15 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.78", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.78.tgz", - "integrity": "sha512-/y9WvNtlcPBGLR0qc1a+9J/xtYZfVczvLUOuXaVWylzttH7ewsxwHtjmiJSolNrVSDorIxHGHMU61CbonRkmwA==", + "version": "3.972.86", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.86.tgz", + "integrity": "sha512-zKTyLJBEyfQBVlYqT6Jt+mYV4r8kGbWM7dn6v4Y4MRARizhYafjDXP/iMu76UMRhToSPNmIKX1XAb24gtpBFIA==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/nested-clients": "^3.997.46", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/nested-clients": "^3.997.54", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -660,17 +680,17 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.46", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.46.tgz", - "integrity": "sha512-oRxtBcka/JGHGs9l9p9IVajGoTP8vTPmoAzdHGy4Qcy9P5vPnDf6nhIeM/COQNY9k/OahImTRaLkHftoXvfcmQ==", + "version": "3.997.54", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.54.tgz", + "integrity": "sha512-/OczlavsS7U5sOodRO10Ug4R/AKFVZiXTstKipWe68EfjPkmdDFNXNl9FCJyXWkg1DG6RBm3qAuVB8FNW9O8KQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.978.1", - "@aws-sdk/signature-v4-multi-region": "^3.996.47", - "@aws-sdk/types": "^3.974.6", - "@smithy/core": "^3.35.0", + "@aws-sdk/core": "^3.979.1", + "@aws-sdk/signature-v4-multi-region": "^3.996.51", + "@aws-sdk/types": "^3.975.0", + "@smithy/core": "^3.38.0", "@smithy/fetch-http-handler": "^5.8.0", - "@smithy/node-http-handler": "^4.12.1", + "@smithy/node-http-handler": "^4.14.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -679,13 +699,13 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.47", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.47.tgz", - "integrity": "sha512-Zk08macMvQTHzQJCLJVkOlviVoqwYMrpXv4lmLN7b7sAbiMoOK7Go0NYdR5UeF+MW8LIbRmwrNy9u/5VvX1U5g==", + "version": "3.996.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.51.tgz", + "integrity": "sha512-+aHvUugVBuMPXxgkPfWnTMyGdE/yWS8GHi5+qQ37HowS0YqzrbuaDLznj+Dtmtu5jgCdqz36gXmujOToUhNNWw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.974.6", - "@smithy/signature-v4": "^5.7.3", + "@aws-sdk/types": "^3.975.0", + "@smithy/signature-v4": "^5.7.5", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -711,9 +731,9 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.974.6", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.6.tgz", - "integrity": "sha512-v/clNZzZnDxGyvpHMOGpJKVXFAExJzUNAAjaWGdcx8QAcXLGwTaOkw33p5SHAi0YAioK32xB3hWwOekRVfmfKg==", + "version": "3.975.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.975.0.tgz", + "integrity": "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.19.0", @@ -724,9 +744,9 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.41.tgz", - "integrity": "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ==", + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.42.tgz", + "integrity": "sha512-smX8/gNP8Kjq+CWTBMZQLQ912vOQjiWExzwzvMO+eujPB5RY8IbtF0AGdUUXHKN7KoDWK9dIcuW0WG269kiP6w==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.19.0", @@ -887,9 +907,9 @@ } }, "node_modules/@azure/identity": { - "version": "4.13.3", - "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.3.tgz", - "integrity": "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A==", + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.14.1.tgz", + "integrity": "sha512-HHuuFrmN1uuF9D5A6aAo5eHzNFNX4Q+P4SksZqDKse5YaJFFy/zDLG3wXHEV2FVGgetZ3QM8jGURNBtnQYf1NQ==", "license": "MIT", "dependencies": { "@azure/abort-controller": "^2.0.0", @@ -901,8 +921,8 @@ "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", - "@azure/msal-node": "^6.0.0", - "open": "^10.1.0", + "@azure/msal-node": "^7.1.0", + "open": "^11.0.0", "tslib": "^2.2.0" }, "engines": { @@ -922,18 +942,20 @@ } }, "node_modules/@azure/identity/node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/open/-/open-11.0.4.tgz", + "integrity": "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==", "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", + "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", + "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" + "powershell-utils": "^0.2.1", + "wsl-utils": "^1.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -1015,12 +1037,12 @@ } }, "node_modules/@azure/msal-node": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-6.0.1.tgz", - "integrity": "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-7.1.2.tgz", + "integrity": "sha512-52hKQLGpEQzwNufIXG1SPVU+wQ3RIe7Rp2+g+VdAmZojCAZ6yCdGEffzQEQ6RnBZGBEs40nl14rWBmfJpCoHsg==", "license": "MIT", "dependencies": { - "@azure/msal-common": "16.14.1", + "@azure/msal-common": "16.15.0", "jsonwebtoken": "^9.0.0" }, "engines": { @@ -1028,9 +1050,9 @@ } }, "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.14.1.tgz", - "integrity": "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ==", + "version": "16.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-16.15.0.tgz", + "integrity": "sha512-lwdLb1B9Q5nEhPLVIKe78XH5wFMN15fCiDkY7ylPDSWZuWcD93G3jpJajrKhO3CQld8SLKz0sHiOi24mxdvtsg==", "license": "MIT", "engines": { "node": ">=0.8.0" @@ -1637,9 +1659,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.152.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.152.0.tgz", - "integrity": "sha512-oM/5rLBm2tPkg0iBgkH/FOeR3PCDpY19GTgAZjMFM8h9WI9VW7cLgzp6nwtarYKmovavIQZ+Fe/RKX/8C8O/Rw==", + "version": "0.156.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.156.0.tgz", + "integrity": "sha512-1Duyg48TdJNdyhEB8Xn1L36bT9Pv6GUtlKjHxOys8xkaF/rQq3XJE+bAurSK/AgIz5mGFsb77cn9Pb20+WCScw==", "dev": true, "license": "MIT", "funding": { @@ -1774,9 +1796,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.12.tgz", - "integrity": "sha512-dB/a1214qKfHMXCpgqR4OZT+jS4kTyEXbQGJPqzobt5EwH5rX080pxE37alt3RzvR1bf1Yz/yGqRfrYAxuPw0A==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.16.tgz", + "integrity": "sha512-ia3DwAs0OsMGS7+IConptSiyk9pEXkOx/AbJ2n/lAGxH3QkWQus1t8d7gnAT38FCU2sQu8lUbeR6AiZFv4JxaA==", "cpu": [ "arm" ], @@ -1791,9 +1813,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.12.tgz", - "integrity": "sha512-7KHFgQ5VJxIHcLlrwrc3Xbds7oTNQT7Pgi9gQCJKrd2VGab/UksIOYp6VD8MzCstGxOKMgNamPwUCfxPdP1OHg==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.16.tgz", + "integrity": "sha512-kx5wBiMB6E7TEvpl2B9gjLU004BkvEm2Dr941BFjx/SontPwSzNuQUIoVKxHHoSjpFIfHMmcguyLOtyn0hObRg==", "cpu": [ "arm64" ], @@ -1808,9 +1830,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.12.tgz", - "integrity": "sha512-3YIhqHD96nA5SaYNRBR16HnGv4oavZvXfD/ayHM+oYZ0WD/8lBAtf6zQua4kEyAvpqrluKXl0lnOBoiNby7x9w==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.16.tgz", + "integrity": "sha512-qslZrXnKJU4cUpjoAmtAo/KQM9e2IRMlmpig6Zpwj+8KWxW8v+nkAf9216Bsek9lOC1jvg2yCjygCFo2f83msQ==", "cpu": [ "arm64" ], @@ -1825,9 +1847,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.12.tgz", - "integrity": "sha512-UuuJ35MFw4gmFOrE9pEqIV+K3syIKveph+Qc1/ljHZVdoDW4pz/JHR/eMVom+TZGl/5OOvGJOWaOCVt3ZfqhxA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.16.tgz", + "integrity": "sha512-FdF+y48ND13eAyM2mQn+85kvAqNlvRWROy2rhgA463X/0Gv4vGtazZtfaF1mRBLscYvtOlh4QnLjOWiFzX69OQ==", "cpu": [ "x64" ], @@ -1842,9 +1864,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.12.tgz", - "integrity": "sha512-uMvssit0a4W+/7D8CbHUvG719mH3R2jwXAlh/XcPvuHTE0g++LymF88DCGNX0HM2rBOn0xrzgXktIB6fLSJBTQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.16.tgz", + "integrity": "sha512-ddjqFM5e95AgLbUPtKIS7V1KTVlA0RUgCVr5mn+/IEhsdy3pr+Vuj/HZ7nlMN2FyxIwUktOH1LEPIQXO1v9y1Q==", "cpu": [ "x64" ], @@ -1859,9 +1881,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.12.tgz", - "integrity": "sha512-XcFu0R0xWnwzSf4IQgFH1rJIckPN1pLy2R+4r9IDB7Yfu/ys9cVqfa4pBrMHj7a3gl8mIR4nRNPg0e5IvEVs6g==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.16.tgz", + "integrity": "sha512-tHdKWbFkFBI687iim3LIyIvyL7j66VvQYkDPss5Stn/qyE2tWkR1YD0dXmXPFKKqT/o5ZEJVsaRsWS2meQwGOA==", "cpu": [ "arm" ], @@ -1876,9 +1898,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.12.tgz", - "integrity": "sha512-260UrKgn8tz39ak+SMDOirKzr7V04M9dWPw5llW00SwBivCZoWcRBKV1d8cXnRkUmSZA3BdiUmBHWk7734Ulpw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.16.tgz", + "integrity": "sha512-FJiLGAblPBMlBEdbOqw6kW6QcXDWAf/oJu7gYs9ZJZwmg+K+/zjFLxPTIY9izJUzxzc24FHvmseyA0IvJSl17w==", "cpu": [ "arm64" ], @@ -1893,9 +1915,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.12.tgz", - "integrity": "sha512-5YK1I9SqDkbPgc1IA8BgDl34suqUS2q0KWnBrirm0E51YjOs6eo6dV6jbQfNE/argHRSvd0QUGgtpIoYx+WWpw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.16.tgz", + "integrity": "sha512-hrJpblMsrQ58U9veWpbXzkhV5zStunds6HWLprqq1qauvgBr1E6P+24kLj6lSLI9vjRn/RYpjprhkHQPKGWpNg==", "cpu": [ "arm64" ], @@ -1910,9 +1932,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.12.tgz", - "integrity": "sha512-Rkcrmp7eFRg74yL5fXEU91JEWbdEPLevWwGtXpmhbjlD1StScbWTmO94Bhly+Mo+ketKYkdmM1vNUKeWSlx8cQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.16.tgz", + "integrity": "sha512-qJd/dC6j6CwLG9/s/RP2nb1RZ0GIgj7Ioj7x/SYa3RzjVrlzrmH0fhIOpfvqQDeIsdP0loi+kHaMhJzY1PBIaA==", "cpu": [ "ppc64" ], @@ -1927,9 +1949,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.12.tgz", - "integrity": "sha512-qvK4DuAsQc2BSjlx+Xr+IzOIvvxbGZqxFwdWfG6F518Erj0GGISyQbJ6pIappnOxlNPzNHvo/L0BwB30GZ+zVw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.16.tgz", + "integrity": "sha512-Vx7Xg5uUJuNcujJjI70OICaH0EYj80BtOLZTSN7uOYg8QOLlyKnaG0IW7Fn/J0IpJsaKzmDKMhOQv1JBM9m9vg==", "cpu": [ "s390x" ], @@ -1944,9 +1966,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.12.tgz", - "integrity": "sha512-Q9uLBO53Xd4QIq1WOycVQyPP1O4HhraEV2qqb3uTrnVw6QZih9duY4vNXOivL1xoUS1/z+W8eF4NMfl2a8Sdjw==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.16.tgz", + "integrity": "sha512-TNKhgE3uJTsTzatUSSYt0Kgo+Oa2uSlSjz4r1aEIea7HX6ULBym4fAlYUhG1i6pjNf2njfCUAfKwr6C3E9Xg1w==", "cpu": [ "x64" ], @@ -1961,9 +1983,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.12.tgz", - "integrity": "sha512-3IBxWFMjbOZskDPKv8Lf9BCnahlKuHthWkYnyIxOH/QcJrFcS4EmcenthApkwr/5+nEqZlLzeYbxeMaX7A5u4g==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.16.tgz", + "integrity": "sha512-7chXdpjXZ8qG0+mIxECN3cT9kvhZ3/oZhMX6Pp876P7CDrIy8nJswva6Mq357JR7eYXfuskwZxMeV1SzoZdHrA==", "cpu": [ "x64" ], @@ -1978,9 +2000,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.12.tgz", - "integrity": "sha512-xtX61xg4LKPkPWilZU1ynKClz5Gj4bf74LML4r3eVLWumKnGjoEr1OSHQhMdbBDoYTi+yjrujvpZe2pUnqCrrA==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.16.tgz", + "integrity": "sha512-Kwjt+7uNTJK5/MDkGWGVO2KQPnj8yAV75dMpfwyHZrJSXWHrniOB0aUEWlVYf3H1hOqFLIStLdM5QXq6bnKmOQ==", "cpu": [ "arm64" ], @@ -1995,9 +2017,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.12.tgz", - "integrity": "sha512-At7fPB6PCaIjzgIhEZFxuT+BBFqiQibJDT4d3PhiR3f4E7bbMZF4aKblbFfEM3sETRDd1YiQx/+U/g/B/ou5Ew==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.16.tgz", + "integrity": "sha512-/LH6WfjDRXXJz/p5xl4TmU+jpcnwcxbKEQBW/GZE0JHfN0lGIyTtsUEGaQ/q/GikkGJ2yLKe6pwm7AnEWO8pdw==", "cpu": [ "arm64" ], @@ -2012,9 +2034,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.12.tgz", - "integrity": "sha512-WIw2haVKwjuYdXkHaoC0mF8Le71TuCBxjrdKqLbJGctbBABj+ClfmNvtbOnzpq3RokNo5+V1qhtSzJyXorsklQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.16.tgz", + "integrity": "sha512-1lv1KvX0Z871zfk7vw+dnXMMnHNyDwMzWG8Kv3HxDXMOhzCX4SHc7HK319fInP0i+0p9D8brkfzL7pJOaGl+tw==", "cpu": [ "x64" ], @@ -2036,9 +2058,9 @@ "license": "MIT" }, "node_modules/@smithy/core": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.35.1.tgz", - "integrity": "sha512-i4YPS4B6ts7bjn7UwLnGjiZdprOvHvgGobFZsYK3GIY3E5hIqtj0rReU69BcTpGp+fvtraSNXeG1l+jtJvF55w==", + "version": "3.38.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.38.0.tgz", + "integrity": "sha512-aLyyCnK2p4yvNA2TaM+5owXqb+rZGSiUKEsNP329AzeYnERtAz7NrlTbRsaxqmZ0ooxwoID8LMm959UGQQDIsg==", "license": "Apache-2.0", "dependencies": { "@smithy/types": "^4.19.0", @@ -2077,13 +2099,13 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", - "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.14.0.tgz", + "integrity": "sha512-j24RS5V9J52u0z81RpgF7H55p4bS6UCEIxF4ZYzTYh6M9hzXvjAIAYwgLobqf7vJq3xib5VXc7nX3YesV8Rx4A==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.33.3", - "@smithy/types": "^4.18.0", + "@smithy/core": "^3.38.0", + "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, "engines": { @@ -2091,12 +2113,12 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.7.4", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.4.tgz", - "integrity": "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ==", + "version": "5.7.5", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.5.tgz", + "integrity": "sha512-WPjdr5oWOk1LArpI4pavu+fSo+/evi0Re1olxGvqwuZopd4gYR1UGCi+Soyn/IjmafqbWs+ZC+3AKxJS3ZpwUg==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.35.0", + "@smithy/core": "^3.38.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" }, @@ -3341,9 +3363,9 @@ } }, "node_modules/electron": { - "version": "44.5.1", - "resolved": "https://registry.npmjs.org/electron/-/electron-44.5.1.tgz", - "integrity": "sha512-lx7AYoFIiyvEvgvpyWO9kOC60xtNPpiPZAth6Fmg9NutzpCAMLwpUlaBY2EXfZyIgUdzEmLMj9KvK0Vrokl1yQ==", + "version": "44.10.0", + "resolved": "https://registry.npmjs.org/electron/-/electron-44.10.0.tgz", + "integrity": "sha512-ss4pCfrQKV4TWkuVYtWB4HmeysPZgB96cSYNKYZv1UVCRP4vQMGNW5oopFbvx6Bbfce1XtP5pm5RDuKehp0svQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3659,9 +3681,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", - "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", "dev": true, "funding": [ { @@ -4294,9 +4316,9 @@ "license": "ISC" }, "node_modules/ip-address": { - "version": "10.7.3", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", - "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", + "version": "10.7.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.5.tgz", + "integrity": "sha512-t2v0rkILYv9WXmSL7TpAaU3HXgaFYOPUcH3QHgwQ++gpJuH3bqyMy4Fvt/DJCTKwzH3Cf+bAp5W/mFL39KPLzA==", "license": "MIT", "engines": { "node": ">= 12" @@ -4327,6 +4349,18 @@ "node": ">=8" } }, + "node_modules/is-in-ssh": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-in-ssh/-/is-in-ssh-1.0.0.tgz", + "integrity": "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-inside-container": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", @@ -5252,9 +5286,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -5416,9 +5450,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", - "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", + "version": "6.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", + "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", "dev": true, "license": "MIT", "engines": { @@ -5815,9 +5849,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz", + "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==", "dev": true, "funding": [ { @@ -5835,9 +5869,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5900,6 +5934,18 @@ "node": ">=14.0.0" } }, + "node_modules/powershell-utils": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.2.1.tgz", + "integrity": "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -6199,13 +6245,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.12", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.12.tgz", - "integrity": "sha512-8wafseiaG80xmXSfqidUNqZcylTlhmPZZt+za2m+js2sFZ8dTNlhIOV2WcbIPx2hgwPBJpEUGFAMZ9bgBBLTSQ==", + "version": "1.2.16", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.16.tgz", + "integrity": "sha512-bWvlMpFKiTfDKfnK7FC2MPgPeWCf/Kpwsq49ApdW+FQaBZb6O6dZ4MAPbPUjp7hYQrw/RobqCzn64BRrQecGHw==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.152.0", + "@oxc-project/types": "=0.156.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6215,21 +6261,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.12", - "@rolldown/binding-android-arm64": "1.2.12", - "@rolldown/binding-darwin-arm64": "1.2.12", - "@rolldown/binding-darwin-x64": "1.2.12", - "@rolldown/binding-freebsd-x64": "1.2.12", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.12", - "@rolldown/binding-linux-arm64-gnu": "1.2.12", - "@rolldown/binding-linux-arm64-musl": "1.2.12", - "@rolldown/binding-linux-ppc64-gnu": "1.2.12", - "@rolldown/binding-linux-s390x-gnu": "1.2.12", - "@rolldown/binding-linux-x64-gnu": "1.2.12", - "@rolldown/binding-linux-x64-musl": "1.2.12", - "@rolldown/binding-openharmony-arm64": "1.2.12", - "@rolldown/binding-win32-arm64-msvc": "1.2.12", - "@rolldown/binding-win32-x64-msvc": "1.2.12" + "@rolldown/binding-android-arm-eabi": "1.2.16", + "@rolldown/binding-android-arm64": "1.2.16", + "@rolldown/binding-darwin-arm64": "1.2.16", + "@rolldown/binding-darwin-x64": "1.2.16", + "@rolldown/binding-freebsd-x64": "1.2.16", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.16", + "@rolldown/binding-linux-arm64-gnu": "1.2.16", + "@rolldown/binding-linux-arm64-musl": "1.2.16", + "@rolldown/binding-linux-ppc64-gnu": "1.2.16", + "@rolldown/binding-linux-s390x-gnu": "1.2.16", + "@rolldown/binding-linux-x64-gnu": "1.2.16", + "@rolldown/binding-linux-x64-musl": "1.2.16", + "@rolldown/binding-openharmony-arm64": "1.2.16", + "@rolldown/binding-win32-arm64-msvc": "1.2.16", + "@rolldown/binding-win32-x64-msvc": "1.2.16" } }, "node_modules/run-applescript": { @@ -6821,9 +6867,9 @@ } }, "node_modules/undici": { - "version": "7.30.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", - "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", + "version": "7.30.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.1.tgz", + "integrity": "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg==", "dev": true, "license": "MIT", "engines": { @@ -6902,16 +6948,16 @@ } }, "node_modules/vite": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.2.tgz", - "integrity": "sha512-SQr1x6W5vVSbROg7vsyXIaxK9b0G7zsT68acdWWRmnBUsgDieLCRG+Rep9WdZgcposvv/GSnr4GUUBqB3vXq6w==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.11", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "bin": { @@ -6928,7 +6974,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -7148,15 +7194,16 @@ } }, "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-1.0.1.tgz", + "integrity": "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ==", "license": "MIT", "dependencies": { - "is-wsl": "^3.1.0" + "is-wsl": "^3.1.0", + "powershell-utils": "^0.2.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index ec872da..3cf2f17 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "build": "tsc --noEmit && vite build", "start": "electron .", "test": "node scripts/test.cjs", - "audit": "npm audit --audit-level=moderate", + "audit": "node scripts/audit.cjs", "test:e2e": "node scripts/e2e.cjs", "package": "node scripts/package.cjs", "release:macos": "node scripts/release-macos.cjs", @@ -21,31 +21,32 @@ "test:platform": "node scripts/platform-smoke.cjs" }, "dependencies": { - "react": "19.3.0", - "react-dom": "19.3.0", - "lucide-react": "^0.577.0", - "pg": "^8.16.3", - "mysql2": "^3.15.0", - "@databricks/sql": "^2.2.0", - "mssql": "^12.0.0", - "mongodb": "^6.20.0", - "redis": "^5.8.0", - "@clickhouse/client": "^1.12.1", + "@aws-sdk/client-athena": "^3.1161.0", "@aws-sdk/client-bedrock": "^3.1145.0", "@aws-sdk/client-bedrock-runtime": "^3.1145.0", "@aws-sdk/credential-providers": "^3.1145.0", - "google-auth-library": "^11.1.0" + "@clickhouse/client": "^1.12.1", + "@databricks/sql": "^2.2.0", + "google-auth-library": "^11.1.0", + "lucide-react": "^0.577.0", + "mongodb": "^6.20.0", + "mssql": "^12.0.0", + "mysql2": "^3.15.0", + "pg": "^8.16.3", + "react": "19.3.0", + "react-dom": "19.3.0", + "redis": "^5.8.0" }, "devDependencies": { + "@types/node": "^24.0.0", "@types/react": "^19.2.0", "@types/react-dom": "^19.2.0", - "@types/node": "^24.0.0", - "electron": "44.5.1", + "electron": "44.10.0", "electron-builder": "^26.0.12", - "undici": "7.30.0", + "playwright": "^1.56.0", "typescript": "^5.9.3", - "vite": "8.3.2", - "playwright": "^1.56.0" + "undici": "7.30.1", + "vite": "8.3.5" }, "build": { "appId": "local.tableline.desktop", diff --git a/scripts/audit.cjs b/scripts/audit.cjs new file mode 100644 index 0000000..84423aa --- /dev/null +++ b/scripts/audit.cjs @@ -0,0 +1,106 @@ +// Copyright (c) 2026 Davide Leopardi +// SPDX-License-Identifier: GPL-3.0-only +"use strict"; + +// A short-lived exception for one upstream advisory in build-only packages. +// Production findings, other advisories and expired exceptions always fail. +const { spawnSync } = require("node:child_process"); +const path = require("node:path"); +const root = path.resolve(__dirname, ".."); +const ADVISORY_URL = "https://github.com/advisories/GHSA-g3hv-75wg-m249"; +const EXPIRES_AFTER = "2026-11-30"; + +function findings(report, label) { + if (!report || typeof report !== "object" || Array.isArray(report) || report.error || + !report.vulnerabilities || typeof report.vulnerabilities !== "object" || + Array.isArray(report.vulnerabilities) || + !Number.isSafeInteger(report.metadata?.vulnerabilities?.total) || + report.metadata.vulnerabilities.total < 0) + throw new Error(`${label} did not return a valid npm audit report.`); + const entries = Object.entries(report.vulnerabilities); + if (entries.length !== report.metadata.vulnerabilities.total) + throw new Error(`${label} vulnerability count does not match its findings.`); + return entries; +} + +function reviewAudit({ production, full, lock, now = new Date() }) { + if (!lock?.packages || typeof lock.packages !== "object") + throw new Error("The dependency lockfile is missing its package inventory."); + if (findings(production, "Production audit").length) + throw new Error("Production dependencies have audit findings."); + const entries = findings(full, "Full audit"); + if (!entries.length) return { count: 0, packages: [], exception: false }; + const date = new Date(now); + if (Number.isNaN(date.getTime()) || date >= new Date("2026-12-01T00:00:00Z")) + throw new Error(`The ${ADVISORY_URL} exception expired after ${EXPIRES_AFTER}.`); + const vulnerabilities = full.vulnerabilities; + for (const [name, entry] of entries) { + if (!entry || entry.severity !== "moderate" || !Array.isArray(entry.via) || !entry.via.length || + !Array.isArray(entry.nodes) || !entry.nodes.length) + throw new Error(`Unreviewed audit finding: ${name}.`); + for (const node of entry.nodes) + if (typeof node !== "string" || lock.packages[node]?.dev !== true) + throw new Error(`Audit finding is not confined to development dependencies: ${name}.`); + for (const via of entry.via) { + if (typeof via === "string") { + if (name === "minimatch" || !vulnerabilities[via]) + throw new Error(`Unreviewed audit finding: ${name}.`); + } else if (name !== "minimatch" || !via || via.name !== "minimatch" || + via.url !== ADVISORY_URL || via.severity !== "moderate") + throw new Error(`Unreviewed audit advisory: ${name}.`); + } + } + if (!vulnerabilities.minimatch || + vulnerabilities.minimatch.via.some(via => typeof via === "string")) + throw new Error("Audit exception has no matching minimatch finding."); + // npm can report dependency cycles (for example app-builder-lib/dmg-builder). + // Every transitive finding must still have a path to the single allowed root. + function reachesMinimatch(name, seen = new Set()) { + if (name === "minimatch") return true; + if (seen.has(name)) return false; + seen.add(name); + return vulnerabilities[name].via.some(via => typeof via === "string" && + reachesMinimatch(via, new Set(seen))); + } + for (const [name] of entries) + if (!reachesMinimatch(name)) throw new Error(`Unrelated audit dependency chain: ${name}.`); + return { count: entries.length, packages: entries.map(([name]) => name).sort(), exception: true }; +} + +function runNpmAudit(args, label) { + const npmCli = process.env.npm_execpath; + if (!npmCli) throw new Error("Run this check through npm run audit."); + const result = spawnSync(process.execPath, [npmCli, "audit", "--json", ...args], { + cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], + timeout: 120000, maxBuffer: 16 * 1024 * 1024, + }); + if (result.error || result.signal || ![0, 1].includes(result.status)) + throw new Error(`${label} could not complete.`); + let report; + try { report = JSON.parse(result.stdout); } + catch { throw new Error(`${label} returned unreadable JSON.`); } + findings(report, label); + if ((report.metadata.vulnerabilities.total === 0) !== (result.status === 0)) + throw new Error(`${label} exit status does not match its findings.`); + return report; +} + +if (require.main === module) { + try { + const production = runNpmAudit(["--omit=dev"], "Production audit"); + const full = runNpmAudit([], "Full audit"); + const lock = require(path.join(root, "package-lock.json")); + const result = reviewAudit({ production, full, lock }); + console.log(`Production dependency audit: ${production.metadata.vulnerabilities.total} findings.`); + console.log(`Full dependency audit: ${result.count} findings.`); + if (result.exception) { + console.log(`Temporary development-only exception through ${EXPIRES_AFTER}: ${ADVISORY_URL}`); + console.log(`Affected packages: ${result.packages.join(", ")}`); + } + } catch (error) { + console.error(`Dependency audit failed: ${error.message}`); + process.exitCode = 1; + } +} + +module.exports = { reviewAudit, findings, ADVISORY_URL, EXPIRES_AFTER }; diff --git a/scripts/e2e.cjs b/scripts/e2e.cjs index eca58a5..50ffd84 100644 --- a/scripts/e2e.cjs +++ b/scripts/e2e.cjs @@ -119,6 +119,15 @@ async function rowsReady() { !document.querySelector(".grid-progress"), ); } +async function waitForClipboard(expected) { + const deadline = Date.now() + 5_000; + while (Date.now() < deadline) { + if (await app.evaluate(({ clipboard }) => clipboard.readText()) === expected) + return; + await new Promise((resolve) => setTimeout(resolve, 25)); + } + throw new Error("Clipboard did not update to the expected fixture value."); +} async function readExport(filename) { const deadline = Date.now() + 10_000; while (Date.now() < deadline) { @@ -877,6 +886,52 @@ async function launch() { await firstCell(120); }, ); + await check("UI cell context menu preserves full values and row JSON", async () => { + const longValue = + "Inizio 👩‍💻 — città d'arte, 東京, مرحبا, café.\n" + + "Una riga Unicode con emoji 🧪 e accenti àèìòù. ".repeat(70) + + "\nFine ✅"; + await page.keyboard.press( + process.platform === "darwin" ? "Meta+t" : "Control+t", + ); + await page.getByLabel("Editor SQL", { exact: true }).fill( + `SELECT 7 AS id, '${longValue.replaceAll("'", "''")}' AS note, NULL AS vacant`, + ); + await page.locator(".run-button").click(); + await firstCell(7); + const cell = page.locator(".data-grid tbody tr").first().locator("td").nth(2); + const menu = page.getByTestId("cell-context-menu"); + + await cell.click({ button: "right" }); + await menu.waitFor(); + await menu.getByTestId("copy-cell-value").click(); + await menu.waitFor({ state: "hidden" }); + await waitForClipboard(longValue); + + await cell.focus(); + await cell.press("Shift+F10"); + await menu.waitFor(); + await page.keyboard.press("Escape"); + await menu.waitFor({ state: "hidden" }); + + await cell.focus(); + await cell.press("Shift+F10"); + await menu.getByRole("menuitem", { name: "Visualizza dato", exact: true }).click(); + const viewer = page.getByTestId("cell-value-viewer"); + await viewer.waitFor(); + assert.equal(await viewer.locator("pre").textContent(), longValue); + await app.evaluate(({ clipboard }) => clipboard.writeText("TABLELINE-E2E-CLIPBOARD-SENTINEL")); + await viewer.getByRole("button", { name: "Copia valore", exact: true }).click(); + await waitForClipboard(longValue); + await page.keyboard.press("Escape"); + await viewer.waitFor({ state: "hidden" }); + + await cell.focus(); + await cell.press("Shift+F10"); + await menu.getByTestId("copy-row-json").click(); + await menu.waitFor({ state: "hidden" }); + await waitForClipboard(JSON.stringify({ id: 7, note: longValue, vacant: null }, null, 2)); + }); await check("UI cell edit, discard, approval and clipboard", async () => { await page.getByRole("button", { name: "Tabelle", exact: true }).click(); await page @@ -942,10 +997,7 @@ async function launch() { .nth(2) .click(); await page.getByRole("button", { name: "Copia name", exact: true }).click(); - assert.equal( - await app.evaluate(({ clipboard }) => clipboard.readText()), - "UI approved", - ); + await waitForClipboard("UI approved"); await page .getByRole("button", { name: "Chiudi dettaglio", exact: true }) .click(); diff --git a/scripts/package.cjs b/scripts/package.cjs index 8be7580..c7a5aa1 100644 --- a/scripts/package.cjs +++ b/scripts/package.cjs @@ -78,6 +78,10 @@ function packageApp({ const selected = String(identity || env.TABLELINE_SIGN_IDENTITY || "-").replace(/^Developer ID Application:\s*/, ""); args.push(`--config.mac.identity=${selected}`, "--config.mac.notarize=false"); if (selected === "-") { + // electron-builder otherwise skips even ad-hoc signing on pull requests. + // This override is confined to the explicit no-certificate build, after + // all signing credentials have been removed from the child environment. + builderEnv.CSC_FOR_PULL_REQUEST = "true"; // Ad-hoc binaries have no Team ID; hardened library validation prevents // even Electron's framework from loading. Local CI packages never become // public release artifacts. Developer ID releases retain hardened runtime. diff --git a/scripts/platform-smoke.cjs b/scripts/platform-smoke.cjs index 22d90d1..149aa60 100644 --- a/scripts/platform-smoke.cjs +++ b/scripts/platform-smoke.cjs @@ -99,11 +99,11 @@ const check = async (name, work) => { const imported = await app.evaluate(({ app }) => { const { createRequire } = process.getBuiltinModule("module"); const appRequire = createRequire(process.getBuiltinModule("path").join(app.getAppPath(), "package.json")); - const dependencies = ["pg", "mysql2", "mssql", "mongodb", "redis", "@clickhouse/client", "@databricks/sql", + const dependencies = ["pg", "mysql2", "mssql", "mongodb", "redis", "@clickhouse/client", "@databricks/sql", "@aws-sdk/client-athena", "@aws-sdk/client-bedrock", "@aws-sdk/client-bedrock-runtime", "@aws-sdk/credential-providers", "google-auth-library"]; return dependencies.map(name => { appRequire(name); return name; }); }); - assert.equal(imported.length, 11); + assert.equal(imported.length, 12); }); await check("Native LZ4 binding roundtrips UTF-8 using the packaged Electron ABI", async () => { const result = await app.evaluate(({ app }) => { diff --git a/src/App.tsx b/src/App.tsx index ae7fc6d..02e48c0 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1025,6 +1025,7 @@ export default function App() { "postgres", "mysql", "databricks", + "athena", "sqlite", "mongodb", "redis", @@ -1043,7 +1044,7 @@ export default function App() {
- {connection.database || "demo"} + {connection.database || (connection.driver === "athena" ? "AwsDataCatalog" : "demo")} {view === "sql" ? (tab.name === "Query" ? translate("Query") : tab.name) : table?.name || translate("Database")} @@ -1425,6 +1426,8 @@ export default function App() { loading || (connection.driver === "redis" ? !output?.cursor || output.cursor === "0" + : connection.driver === "athena" + ? !output?.truncated : total !== undefined ? offset + limit >= total : (output?.rows.length || 0) < limit) @@ -1478,6 +1481,8 @@ export default function App() { {connection ? connection.driver === "sqlite" ? translate("SQLite · locale") + : connection.driver === "athena" + ? `Athena · ${connection.region || ""}` : `${connection.driver} · ${connection.host || ""}` : translate("Nessuna connessione")} {connection && ( diff --git a/src/Settings.tsx b/src/Settings.tsx index 24ec016..ef8d690 100644 --- a/src/Settings.tsx +++ b/src/Settings.tsx @@ -97,6 +97,7 @@ const driverMarks: Record = { mysql: "My", mariadb: "Ma", databricks: "Db", + athena: "At", "aurora-postgresql": "Au", "aurora-mysql": "Au", redshift: "Rs", @@ -153,6 +154,7 @@ export function Connections({ const selected = catalog.find((d) => d.id === engine); const local = engine === "sqlite"; const dbx = engine === "databricks"; + const athena = engine === "athena"; const mongo = engine === "mongodb"; function set(k: string, v: any) { setForm((f) => ({ ...f, [k]: v })); @@ -247,6 +249,7 @@ export function Connections({ ...f, port: d.defaultPort, name: d.name, + ...(d.id === "athena" ? { host: "", database: "" } : {}), })); }} > @@ -303,6 +306,47 @@ export function Connections({
+ ) : athena ? ( + <> + + + + + +
+ {translate("Usa il login AWS SSO locale. Le credenziali temporanee si rinnovano automaticamente.")} +
+ ) : ( <>
+ {menu && createPortal( +
{ + const items = Array.from(menuRef.current?.querySelectorAll("button") || []); + const current = items.indexOf(document.activeElement as HTMLButtonElement); + if (e.key === "ArrowDown" || e.key === "ArrowUp" || e.key === "Tab") { + e.preventDefault(); + const direction = e.key === "ArrowUp" || (e.key === "Tab" && e.shiftKey) ? -1 : 1; + items[(current + direction + items.length) % items.length]?.focus(); + } else if (e.key === "Home" || e.key === "End") { + e.preventDefault(); + items[e.key === "Home" ? 0 : items.length - 1]?.focus(); + } + }} + > + + + +
, + document.body, + )} + {viewer && createPortal( +
{ + if (e.target === e.currentTarget) closeViewer(); + }} + > +
{ + if (e.key !== "Tab") return; + const controls = Array.from(viewerRef.current?.querySelectorAll("button") || []); + const first = controls[0], last = controls.at(-1); + if (e.shiftKey && (document.activeElement === first || document.activeElement === viewerRef.current)) { + e.preventDefault(); + last?.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first?.focus(); + } + }} + > +
+
+ {translate("Valore della cella")} + {viewer.column} +
+ +
+
{prettyValue(viewer.row[viewer.column])}
+ +
+
, + document.body, + )} + ); } export function Structure({ table }: { table: Table | null }) { diff --git a/src/styles.css b/src/styles.css index 5edc1c0..099e606 100644 --- a/src/styles.css +++ b/src/styles.css @@ -363,6 +363,7 @@ html[data-platform="linux"] .window-space { background: var(--soft); } .engine-mark.sqlserver, +.engine-mark.athena, .engine-mark.redshift { color: #8370a0; background: var(--purple-soft); @@ -797,6 +798,101 @@ html[data-platform="linux"] .window-space { .data-grid td.selected-cell { box-shadow: inset 0 0 0 1.5px var(--accent); } +.cell-context-menu { + position: fixed; + z-index: 70; + min-width: 215px; + max-width: calc(100vw - 16px); + max-height: calc(100vh - 16px); + overflow: auto; + padding: 5px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--surface); + box-shadow: var(--shadow); +} +.cell-context-menu button { + display: block; + width: 100%; + padding: 9px 11px; + border-radius: 5px; + text-align: left; + white-space: normal; + font-size: 11px; +} +.cell-context-menu button:hover, +.cell-context-menu button:focus-visible { + background: var(--hover); + color: var(--accent); +} +.cell-value-overlay { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + align-items: center; + justify-content: center; + padding: 16px; + background: rgba(0, 0, 0, 0.42); +} +.cell-value-viewer { + width: min(720px, 100%); + max-height: min(85vh, 800px); + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); + box-shadow: var(--shadow); +} +.cell-value-viewer header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 18px; + border-bottom: 1px solid var(--border); +} +.cell-value-viewer header > div { + min-width: 0; + display: flex; + flex-direction: column; + gap: 3px; +} +.cell-value-viewer header strong { + font-size: 13px; +} +.cell-value-viewer header span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--muted); + font-size: 11px; +} +.cell-value-viewer pre { + min-height: 0; + overflow: auto; + margin: 0; + padding: 18px; + white-space: pre-wrap; + overflow-wrap: anywhere; + user-select: text; + font-size: 12px; + line-height: 1.55; +} +.cell-value-viewer footer { + display: flex; + justify-content: flex-end; + padding: 10px 18px; + border-top: 1px solid var(--border); +} +.cell-value-viewer footer button { + display: inline-flex; + align-items: center; + gap: 7px; +} .null { font-size: 9px; color: var(--muted); diff --git a/tests/ai-vault.test.cjs b/tests/ai-vault.test.cjs index 4e45de8..799ddf6 100644 --- a/tests/ai-vault.test.cjs +++ b/tests/ai-vault.test.cjs @@ -256,6 +256,10 @@ test("vault sanitizes secure-storage exceptions and preserves existing ciphertex safeStorage.decryptStringAsync = () => { throw new Error(`OS decryption failed: ${PROVIDER_ERROR}`); }; + await assert.rejects( + vault.get("connection-a"), + { code: "SECURE_STORAGE_UNREADABLE" }, + ); await rejectsWithoutSecret(() => vault.get("connection-a")); assertNoPlaintext(directory, [FIRST, SECOND, PROVIDER_ERROR]); }); diff --git a/tests/ai.test.cjs b/tests/ai.test.cjs index e0b28ca..367efb5 100644 --- a/tests/ai.test.cjs +++ b/tests/ai.test.cjs @@ -1178,3 +1178,79 @@ test("failed AI profile save preserves a concurrent credential replacement inste assert.equal(decryptions(), 0); assert.deepEqual(await vault.get("ai-weak"), { apiKey: "synthetic-concurrent-owner-key" }); }); + +async function unreadableCredentialFixture(t, profile = {}, oldCredentials = { apiKey: "synthetic-old-key", bearerToken: "synthetic-old-token" }) { + const { AIVault } = require("../electron/ai-vault.cjs"); + const directory = await fs.mkdtemp(path.join(os.tmpdir(), "tableline-ai-unreadable-recovery-")); + t.after(() => fs.rm(directory, { recursive: true, force: true })); + let unreadable = false, decryptions = 0; + const safeStorage = { + isAsyncEncryptionAvailable: async () => true, + async encryptStringAsync(value) { + const bytes = Buffer.from(value); + for (let i = 0; i < bytes.length; i++) bytes[i] ^= 0xb9; + return Buffer.concat([Buffer.from("v11"), bytes]); + }, + async decryptStringAsync(value) { + decryptions++; + if (unreadable) throw new Error("Synthetic native decryption failure"); + assert.equal(value.subarray(0, 3).toString(), "v11"); + const bytes = Buffer.from(value.subarray(3)); + for (let i = 0; i < bytes.length; i++) bytes[i] ^= 0xb9; + return { result: bytes.toString(), shouldReEncrypt: false }; + }, + }; + const vault = new AIVault({ directory, safeStorage, platform: "darwin" }); + const { service } = await fixture(t, undefined, { vault }); + await service.save({ + id: "unreadable", provider: "litellm", authMode: "apiKey", model: "fixture-model", ...profile, + }, oldCredentials); + const before = await fs.readFile(vault.file); + unreadable = true; + return { + service, vault, before, + decryptions: () => decryptions, + enableDecryption: () => { unreadable = false; }, + }; +} + +test("AI profile replaces unreadable ciphertext with a complete LiteLLM API key", async t => { + const { service, vault, before, decryptions, enableDecryption } = await unreadableCredentialFixture(t); + await service.save({ id: "unreadable", name: "Recovered" }, { apiKey: "synthetic-new-key" }); + assert.equal(decryptions(), 1); + assert.notDeepEqual(await fs.readFile(vault.file), before); + enableDecryption(); + assert.deepEqual(await vault.get("ai-unreadable"), { apiKey: "synthetic-new-key" }); + const settings = await service.settings(); + assert.equal(settings.profiles.find(p => p.id === "unreadable").name, "Recovered"); + assert.equal(settings.profiles.find(p => p.id === "unreadable").hasCredential, true); +}); + +for (const [profile, oldCredentials, replacement] of [ + [{ authMode: "bearer" }, { bearerToken: "synthetic-old-token" }, { apiKey: "synthetic-incomplete-key" }], + [{ provider: "bedrock", authMode: "aws" }, + { accessKeyId: "synthetic-old-id", secretAccessKey: "synthetic-old-secret" }, + { accessKeyId: "synthetic-new-id" }], +]) { + test(`AI profile rejects incomplete replacement of unreadable ${profile.authMode} credentials`, async t => { + const { service, vault, before, decryptions } = await unreadableCredentialFixture(t, profile, oldCredentials); + await assert.rejects( + service.save({ id: "unreadable", name: "Unpersisted rename" }, replacement), + /Impossibile sbloccare le credenziali AI/, + ); + assert.equal(decryptions(), 1); + assert.deepEqual(await fs.readFile(vault.file), before); + assert.notEqual((await service.settings()).profiles.find(p => p.id === "unreadable").name, "Unpersisted rename"); + }); +} + +test("failed AI profile save restores exact unreadable ciphertext after complete replacement", async t => { + const { service, vault, before, decryptions } = await unreadableCredentialFixture(t); + service.persist = async () => { throw new Error("Synthetic metadata persistence failure"); }; + await assert.rejects( + service.save({ id: "unreadable", name: "Unpersisted rename" }, { apiKey: "synthetic-new-key" }), + /Synthetic metadata persistence failure/, + ); + assert.equal(decryptions(), 1); + assert.deepEqual(await fs.readFile(vault.file), before); +}); diff --git a/tests/athena.test.cjs b/tests/athena.test.cjs new file mode 100644 index 0000000..03517a5 --- /dev/null +++ b/tests/athena.test.cjs @@ -0,0 +1,271 @@ +// Copyright (c) 2026 Davide Leopardi +// SPDX-License-Identifier: GPL-3.0-only + +"use strict"; + +const test = require("node:test"); +const assert = require("node:assert/strict"); +const { AthenaDriver, executionParameters } = require("../electron/drivers/athena.cjs"); + +function fakeSdk(handler) { + let client; + let ini; + let assume; + class Command { + constructor(input) { this.input = input; } + } + class GetWorkGroupCommand extends Command {} + class ListDatabasesCommand extends Command {} + class ListTableMetadataCommand extends Command {} + class StartQueryExecutionCommand extends Command {} + class GetQueryExecutionCommand extends Command {} + class GetQueryResultsCommand extends Command {} + class StopQueryExecutionCommand extends Command {} + class AthenaClient { + constructor(config) { + this.config = config; + this.calls = []; + client = this; + } + async send(command) { + this.calls.push(command); + return handler(command); + } + destroy() { this.destroyed = true; } + } + return { + AthenaClient, + GetWorkGroupCommand, + ListDatabasesCommand, + ListTableMetadataCommand, + StartQueryExecutionCommand, + GetQueryExecutionCommand, + GetQueryResultsCommand, + StopQueryExecutionCommand, + fromIni: (options) => { + ini = options; + return async () => ({ accessKeyId: "SSO" }); + }, + fromTemporaryCredentials: (options) => { + assume = options; + return async () => ({ accessKeyId: "ROLE" }); + }, + get client() { return client; }, + get ini() { return ini; }, + get assume() { return assume; }, + }; +} + +const profile = { + awsProfile: "sample-sso-profile", + region: "eu-west-1", + roleArn: "arn:aws:iam::123456789012:role/tableline-read", + workgroup: "tableline-read", + database: "analytics", +}; + +test("Athena execution parameters quote scalar SQL expressions", () => { + assert.deepEqual( + executionParameters(["O'Reilly", 42, true, false, null]), + ["'O''Reilly'", "42", "TRUE", "FALSE", "NULL"], + ); + assert.throws(() => executionParameters({ name: "x" }), /positional/); + assert.throws(() => executionParameters([{}]), /scalar/); + assert.throws(() => executionParameters([Infinity]), /scalar/); + assert.throws(() => executionParameters(["a".repeat(1024)]), /scalar/); +}); + +test("Athena uses SSO then a refreshing role provider and reads paginated rows", async () => { + const sdk = fakeSdk((command) => { + if (command instanceof sdk.GetWorkGroupCommand) + return { WorkGroup: { State: "ENABLED" } }; + if (command instanceof sdk.StartQueryExecutionCommand) + return { QueryExecutionId: "query-1" }; + if (command instanceof sdk.GetQueryExecutionCommand) + return { QueryExecution: { Status: { State: "SUCCEEDED" } } }; + if (command instanceof sdk.GetQueryResultsCommand) + return command.input.NextToken + ? { ResultSet: { Rows: [{ Data: [{ VarCharValue: "second" }, {}] }] } } + : { + ResultSet: { + ResultSetMetadata: { ColumnInfo: [ + { Name: "text", Type: "varchar" }, + { Name: "missing", Type: "varchar" }, + ] }, + Rows: [ + { Data: [{ VarCharValue: "text" }, { VarCharValue: "missing" }] }, + { Data: [{ VarCharValue: "first" }, { VarCharValue: "value" }] }, + ], + }, + NextToken: "page-2", + }; + throw new Error("Unexpected command"); + }); + const driver = new AthenaDriver(profile, {}, sdk); + await driver.connect(); + assert.equal(sdk.ini.profile, profile.awsProfile); + assert.equal(sdk.assume.params.RoleArn, profile.roleArn); + assert.equal(sdk.assume.masterCredentials instanceof Function, true); + assert.equal(sdk.client.config.credentials instanceof Function, true); + const result = await driver.read("SELECT ? AS text, ? AS missing", ["first", null], 2); + assert.deepEqual(result.columns, [ + { name: "text", type: "varchar" }, + { name: "missing", type: "varchar" }, + ]); + assert.deepEqual(result.rows, [ + { text: "first", missing: "value" }, + { text: "second", missing: null }, + ]); + const start = sdk.client.calls.find((call) => call instanceof sdk.StartQueryExecutionCommand); + assert.equal(start.input.WorkGroup, profile.workgroup); + assert.equal(start.input.QueryExecutionContext.Database, "analytics"); + assert.deepEqual(start.input.ExecutionParameters, ["'first'", "NULL"]); + assert.match(start.input.QueryString, /LIMIT 3$/); + await driver.close(); + assert.equal(sdk.client.destroyed, true); +}); + +test("Athena metadata and browser use discovered identifiers without a count scan", async () => { + const sdk = fakeSdk((command) => { + if (command instanceof sdk.GetWorkGroupCommand) + return { WorkGroup: { State: "ENABLED" } }; + if (command instanceof sdk.ListTableMetadataCommand) + return { + TableMetadataList: [{ + Name: "messaggi", + TableType: "VIRTUAL_VIEW", + Columns: [ + { Name: "testo", Type: "string" }, + { Name: "numero", Type: "bigint" }, + ], + PartitionKeys: [{ Name: "mese", Type: "string" }], + }], + }; + if (command instanceof sdk.StartQueryExecutionCommand) + return { QueryExecutionId: "browse-1" }; + if (command instanceof sdk.GetQueryExecutionCommand) + return { QueryExecution: { Status: { State: "SUCCEEDED" } } }; + if (command instanceof sdk.GetQueryResultsCommand) + return { + ResultSet: { + ResultSetMetadata: { ColumnInfo: [ + { Name: "testo", Type: "varchar" }, + { Name: "numero", Type: "bigint" }, + { Name: "mese", Type: "varchar" }, + ] }, + Rows: [ + { Data: [{ VarCharValue: "testo" }, { VarCharValue: "numero" }, { VarCharValue: "mese" }] }, + { Data: [{ VarCharValue: "hello" }, { VarCharValue: "1" }, { VarCharValue: "2026-10" }] }, + ], + }, + }; + throw new Error("Unexpected command"); + }); + const driver = new AthenaDriver(profile, {}, sdk); + await driver.connect(); + const schema = await driver.schema(); + assert.deepEqual(schema.map((table) => [table.schema, table.name, table.kind]), [ + ["analytics", "messaggi", "view"], + ]); + assert.deepEqual(schema[0].columns.map((column) => column.name), ["testo", "numero", "mese"]); + const result = await driver.browse({ + table: "messaggi", + schema: "analytics", + search: "O'Reilly", + sortColumn: "mese", + sortDirection: "desc", + offset: 20, + limit: 2, + }); + assert.equal(result.total, undefined); + assert.deepEqual(result.rows, [{ testo: "hello", numero: "1", mese: "2026-10" }]); + const start = sdk.client.calls.find((call) => call instanceof sdk.StartQueryExecutionCommand); + assert.match(start.input.QueryString, /^SELECT \* FROM "analytics"\."messaggi"/); + assert.match(start.input.QueryString, /ORDER BY "mese" DESC OFFSET 20 LIMIT 3$/); + assert.doesNotMatch(start.input.QueryString, /count\s*\(/i); + assert.deepEqual(start.input.ExecutionParameters, ["'O''Reilly'", "'O''Reilly'"]); + await driver.browse({ table: "messaggi", schema: "analytics", offset: 100, limit: 2 }); + const unsorted = sdk.client.calls.filter((call) => + call instanceof sdk.StartQueryExecutionCommand).at(-1); + assert.match(unsorted.input.QueryString, / OFFSET 100 LIMIT 3$/); + assert.doesNotMatch(unsorted.input.QueryString, /ORDER BY/); + await assert.rejects(driver.browse({ table: "messaggi", sortColumn: "not_discovered" }), /Sort column/); + await driver.close(); +}); + +test("Athena lists accessible databases and uses default SQL context when none is selected", async () => { + const names = []; + const sdk = fakeSdk((command) => { + if (command instanceof sdk.GetWorkGroupCommand) + return { WorkGroup: { State: "ENABLED" } }; + if (command instanceof sdk.ListDatabasesCommand) + return { DatabaseList: [{ Name: "analytics" }, { Name: "archive" }] }; + if (command instanceof sdk.ListTableMetadataCommand) { + names.push(command.input.DatabaseName); + return { TableMetadataList: [{ Name: "data", Columns: [{ Name: "id", Type: "string" }] }] }; + } + if (command instanceof sdk.StartQueryExecutionCommand) + return { QueryExecutionId: "connection-test" }; + if (command instanceof sdk.GetQueryExecutionCommand) + return { QueryExecution: { Status: { State: "SUCCEEDED" } } }; + if (command instanceof sdk.GetQueryResultsCommand) + return { + ResultSet: { + ResultSetMetadata: { ColumnInfo: [{ Name: "connected", Type: "integer" }] }, + Rows: [ + { Data: [{ VarCharValue: "connected" }] }, + { Data: [{ VarCharValue: "1" }] }, + ], + }, + }; + throw new Error("Unexpected command"); + }); + const driver = new AthenaDriver({ ...profile, database: "" }, {}, sdk); + await driver.connect(); + assert.deepEqual((await driver.schema()).map((table) => table.schema), ["analytics", "archive"]); + assert.deepEqual(names, ["analytics", "archive"]); + await driver.read("SELECT 1 AS connected", [], 1); + const start = sdk.client.calls.find((call) => call instanceof sdk.StartQueryExecutionCommand); + assert.equal(start.input.QueryExecutionContext.Database, "default"); + await driver.close(); +}); + +test("Athena stops a query that exceeds its polling deadline", async () => { + let stopped = false; + const sdk = fakeSdk((command) => { + if (command instanceof sdk.GetWorkGroupCommand) + return { WorkGroup: { State: "ENABLED" } }; + if (command instanceof sdk.StartQueryExecutionCommand) + return { QueryExecutionId: "slow-1" }; + if (command instanceof sdk.GetQueryExecutionCommand) + return { QueryExecution: { Status: { State: "QUEUED" } } }; + if (command instanceof sdk.StopQueryExecutionCommand) { + stopped = true; + return {}; + } + throw new Error("Unexpected command"); + }); + const driver = new AthenaDriver(profile, {}, sdk); + await driver.connect(); + driver.timeoutMs = -1; + await assert.rejects(driver.read("SELECT 1", [], 1), /timed out/); + assert.equal(stopped, true); + assert.equal(driver.inFlight.size, 0); + await driver.close(); +}); + +test("Athena cancellation reports a failed stop request", async () => { + const sdk = fakeSdk((command) => { + if (command instanceof sdk.GetWorkGroupCommand) + return { WorkGroup: { State: "ENABLED" } }; + if (command instanceof sdk.StopQueryExecutionCommand) + throw new Error("Access denied"); + throw new Error("Unexpected command"); + }); + const driver = new AthenaDriver(profile, {}, sdk); + await driver.connect(); + driver.active.set("request-1", "query-1"); + assert.equal(await driver.cancel("request-1"), false); + assert.equal(await driver.cancel("unknown"), false); + await driver.close(); +}); diff --git a/tests/audit.test.cjs b/tests/audit.test.cjs new file mode 100644 index 0000000..0a74a04 --- /dev/null +++ b/tests/audit.test.cjs @@ -0,0 +1,70 @@ +// Copyright (c) 2026 Davide Leopardi +// SPDX-License-Identifier: GPL-3.0-only +"use strict"; +const assert = require("node:assert/strict"); +const { test } = require("node:test"); +const { reviewAudit, ADVISORY_URL } = require("../scripts/audit.cjs"); + +const report = (vulnerabilities = {}) => ({ + metadata: { vulnerabilities: { total: Object.keys(vulnerabilities).length } }, + vulnerabilities, +}); +const minimatch = { + severity: "moderate", nodes: ["node_modules/minimatch"], + via: [{ name: "minimatch", severity: "moderate", url: ADVISORY_URL }], +}; +const glob = { + severity: "moderate", nodes: ["node_modules/glob"], via: ["minimatch"], +}; +const lock = { packages: { + "node_modules/minimatch": { dev: true }, + "node_modules/glob": { dev: true }, +} }; +const input = (full = report({ minimatch, glob })) => ({ + production: report(), full, lock, now: "2026-10-31T00:00:00Z", +}); + +test("audit permits only the time-limited, development-only minimatch advisory chain", () => { + assert.deepEqual(reviewAudit(input()), { + count: 2, packages: ["glob", "minimatch"], exception: true, + }); + assert.deepEqual(reviewAudit(input(report())), { + count: 0, packages: [], exception: false, + }); +}); + +test("audit rejects any production finding or production dependency path", () => { + assert.throws(() => reviewAudit({ ...input(), production: report({ minimatch }) }), /Production dependencies/); + const unsafe = structuredClone(lock); + unsafe.packages["node_modules/glob"].dev = false; + assert.throws(() => reviewAudit({ ...input(), lock: unsafe }), /not confined to development/); +}); + +test("audit rejects another advisory, higher severity and an unknown dependency chain", () => { + const other = { severity: "moderate", nodes: ["node_modules/glob"], + via: [{ name: "glob", severity: "moderate", url: "https://github.com/advisories/GHSA-other" }] }; + assert.throws(() => reviewAudit(input(report({ minimatch, glob: other }))), /Unreviewed audit advisory/); + assert.throws(() => reviewAudit(input(report({ minimatch: { ...minimatch, severity: "high" } }))), /Unreviewed audit finding/); + assert.throws(() => reviewAudit(input(report({ glob }))), /Unreviewed audit finding/); +}); + +test("audit permits a transitive dependency cycle only when it reaches minimatch", () => { + const cyclic = report({ minimatch, glob: { ...glob, via: ["minimatch", "temp"] }, + temp: { severity: "moderate", nodes: ["node_modules/temp"], via: ["glob"] } }); + const withTemp = structuredClone(lock); + withTemp.packages["node_modules/temp"] = { dev: true }; + assert.equal(reviewAudit({ ...input(cyclic), lock: withTemp }).count, 3); + const disconnected = report({ ...cyclic.vulnerabilities, + unrelated: { severity: "moderate", nodes: ["node_modules/unrelated"], via: ["other"] }, + other: { severity: "moderate", nodes: ["node_modules/other"], via: ["unrelated"] } }); + withTemp.packages["node_modules/unrelated"] = { dev: true }; + withTemp.packages["node_modules/other"] = { dev: true }; + assert.throws(() => reviewAudit({ ...input(disconnected), lock: withTemp }), /Unrelated audit dependency chain/); +}); + +test("audit rejects an expired exception and malformed audit counts", () => { + assert.throws(() => reviewAudit({ ...input(), now: "2026-12-01T00:00:00Z" }), /expired/); + const malformed = report({ minimatch }); + malformed.metadata.vulnerabilities.total = 0; + assert.throws(() => reviewAudit(input(malformed)), /count does not match/); +}); diff --git a/tests/build-download.test.cjs b/tests/build-download.test.cjs index 6f701a8..d65727d 100644 --- a/tests/build-download.test.cjs +++ b/tests/build-download.test.cjs @@ -83,7 +83,7 @@ test("build graph excludes the vulnerable cache chain and keeps official Electro ); assert.equal(get.downloadArtifact.name, "downloadArtifact"); assert.equal(lock.packages["node_modules/@electron/get"].version, "5.1.0"); - assert.equal(lock.packages["node_modules/undici"].version, "7.30.0"); + assert.equal(lock.packages["node_modules/undici"].version, "7.30.1"); }); test("official artifact download verifies cold, warm and tampered caches and preserves progress", async (t) => { diff --git a/tests/database.test.cjs b/tests/database.test.cjs index e4c1142..b8a6590 100644 --- a/tests/database.test.cjs +++ b/tests/database.test.cjs @@ -595,8 +595,8 @@ for (const code of ["SECURE_STORAGE_TIMEOUT", "SECURE_STORAGE_BLOCKED", "CORRUPT } test("catalog reports unverified rollback engines honestly", async () => { const catalog = await db.catalog(); - assert.equal(catalog.length, 13); - for (const id of ["databricks", "clickhouse", "redshift"]) + assert.equal(catalog.length, 14); + for (const id of ["athena", "databricks", "clickhouse", "redshift"]) assert.equal(catalog.find((c) => c.id === id).capabilities.write, false); for (const id of ["mongodb", "redis"]) assert.equal( diff --git a/tests/platform-package.test.cjs b/tests/platform-package.test.cjs index b96fe79..47fd10f 100644 --- a/tests/platform-package.test.cjs +++ b/tests/platform-package.test.cjs @@ -29,12 +29,12 @@ test("reviewed upstream Electron snapshots cover exactly six supported desktop t assert.deepEqual(Object.keys(manifest.distributions).sort(), expected); for (const target of expected) { const [platform, arch] = target.split("-"); - const snapshot = noticeSnapshot(root, platform, arch, "44.5.1"); + const snapshot = noticeSnapshot(root, platform, arch, "44.10.0"); assert.match(snapshot.upstreamArchiveSha256, /^[a-f\d]{64}$/); assert.equal(Object.keys(snapshot.licenses).length, 2); } - assert.throws(() => noticeSnapshot(root, "linux", "ia32", "44.5.1")); - assert.throws(() => noticeSnapshot(root, "darwin", "x64", "44.5.2")); + assert.throws(() => noticeSnapshot(root, "linux", "ia32", "44.10.0")); + assert.throws(() => noticeSnapshot(root, "darwin", "x64", "44.10.1")); }); test("distribution inventory rejects native kernel but retains Databricks Thrift", () => { const thrift = "/node_modules/@databricks/sql/dist/thrift-backend/ThriftBackend.js"; @@ -60,10 +60,19 @@ test("local Mac packages launch without hardened ad-hoc validation while signed for (const cache of [undefined, path.join(temporary, "builder-cache")]) { calls.length = 0; mocked.exports.packageMac({ platform: "darwin", arch: "arm64", identity, outputDir: temporary, - env: cache ? { ELECTRON_BUILDER_CACHE: cache } : {}, - run: (file, args) => calls.push({ file, args }), log: () => {} }); + env: { ...(cache ? { ELECTRON_BUILDER_CACHE: cache } : {}), + CSC_FOR_PULL_REQUEST: "true", CSC_LINK: "fixture-private-certificate", + APPLE_API_KEY: "fixture-private-key", AWS_SECRET_ACCESS_KEY: "fixture-private-secret" }, + run: (file, args, options) => calls.push({ file, args, options }), log: () => {} }); assert.equal(calls.every(call => call.file === process.execPath), true); const args = calls.at(-1).args; + const childEnv = calls.at(-1).options.env; + assert.equal(childEnv.CSC_FOR_PULL_REQUEST, identity === "-" ? "true" : undefined); + assert.equal(childEnv.CSC_IDENTITY_AUTO_DISCOVERY, "false"); + assert.equal(args.includes("--config.mac.identity=-"), identity === "-"); + assert.equal(args.includes("--config.forceCodeSigning=true"), identity !== "-"); + for (const key of ["CSC_LINK", "APPLE_API_KEY", "AWS_SECRET_ACCESS_KEY"]) + assert.equal(childEnv[key], undefined); assert.ok(args.includes(identity === "-" ? "--config.mac.hardenedRuntime=false" : "--config.mac.hardenedRuntime=true")); assert.equal(args[args.indexOf("--publish") + 1], "never"); assert.equal(args.some(arg => arg.startsWith("--config.electronDownload.cache=")), !!cache);