diff --git a/.gitignore b/.gitignore index 7f3f076b..c355898c 100644 --- a/.gitignore +++ b/.gitignore @@ -21,5 +21,14 @@ # # dist build output target/ +/server/bindings/ + +# Runtime capture spools and generated capture exports. +/capture-spool/ +/capture-metrics-*.csv +/capture-analysis-*/ +/server/scripts/output +/server/scripts/capture-metrics-*.csv +/server/scripts/capture-analysis-*/ # CodeChat Editor lexer: python. See TODO. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b876bbd..898d9d45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,11 @@ Changelog * No changes. +Version 0.2.0 -- 2026-Jul-20 +---------------------------- + +* Added capture functionality. + Version 0.1.61 -- 2026-Jul-11 ----------------------------- diff --git a/README.md b/README.md index 9b41a5fc..92e7af1b 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,43 @@ Install the [CodeChat Editor extension for Visual Studio code](extensions/VSCode/README.md). For developers, see [building from source](docs/design.md). +Research capture +---------------- + +The VS Code extension can record dissertation study capture events when a +participant explicitly opts in. A participant first registers in the capture +portal, which emails a capture token. In VS Code, run **Manage CodeChat Editor +Capture** or **CodeChat Editor: Enter Capture Token** from the command palette, +paste the token, then turn on consent and recording from the same capture +manager. + +The token is imported through the VS Code UI and persisted only in VS Code +SecretStorage. It is never written to workspace settings, repository files, or a +JSON configuration file. The extension asks CaptureWebService for token status; +the status item and capture manager show whether the token is accepted, +rejected, unavailable, or disabled by the portal. The participant ID used in +events comes from that status response, not from the token text. + +CodeChat no longer connects directly to the remote capture database and no +longer reads or stores database credentials. The old local JSON database-secret +configuration path has been removed. Capture events now leave CodeChat only by +calling CaptureWebService with the portal-issued bearer token; any database +writer role remains inside the service deployment. + +Events are sanitized, written to a durable local FIFO spool in VS Code's global +extension storage, then uploaded to CaptureWebService. Spooled events carry only +a non-secret token hash/service identity so events from an old token are not +uploaded under a new token. Offline +recording is allowed only after the same token and service URL have previously +been verified as capture-enabled; a token disabled by the portal remains +disabled while the service is unavailable. If the network or service is +unavailable after the token has been accepted at least once, queued events remain +in the spool and upload as soon as the matching token and service are available +again. The capture service endpoint can be changed in the user-level +`CodeChatEditor.Capture.ServiceBaseUrl` setting; workspace values are ignored +for this token-bearing endpoint. Token-bearing requests require HTTPS except for +localhost development endpoints. + Structure --------- diff --git a/builder/Cargo.lock b/builder/Cargo.lock index d1f8ea3d..c449ca69 100644 --- a/builder/Cargo.lock +++ b/builder/Cargo.lock @@ -100,9 +100,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -110,9 +110,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -129,7 +129,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -160,7 +160,7 @@ dependencies = [ "proc-macro-error2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -194,7 +194,7 @@ dependencies = [ "defmt-parser", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -279,11 +279,12 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "jiff" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "961d16382652bfdd8c6f68b223b26a8c93e0d475c672f414411db31c6c5c900e" +checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" dependencies = [ "defmt", + "jiff-core", "jiff-static", "log", "portable-atomic", @@ -291,15 +292,25 @@ dependencies = [ "serde_core", ] +[[package]] +name = "jiff-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7feca88439efe53da3754500c1851dedf3cb36c524dd5cf8225cc0794de95d09" +dependencies = [ + "defmt", +] + [[package]] name = "jiff-static" -version = "0.2.32" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0879bd39df99c4c5e2c6615ccc026391a423dde10532c573e6086eb94a802cc" +checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" dependencies = [ + "jiff-core", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -339,7 +350,7 @@ checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] @@ -356,9 +367,9 @@ checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "open" -version = "5.3.6" +version = "5.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8d3b65c44123a56e0133d2cd06ce4361bd3ca99d41198b2f25e3c3db9b8b4a" +checksum = "a0b3d059e795d52b8a72fef45658620edd4d9c359b338564aa14391ffa511ed5" dependencies = [ "is-wsl", "libc", @@ -382,9 +393,9 @@ checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" [[package]] name = "portable-atomic" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" +checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" [[package]] name = "portable-atomic-util" @@ -414,32 +425,32 @@ dependencies = [ "proc-macro-error-attr2", "proc-macro2", "quote", - "syn", + "syn 2.0.119", ] [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -449,9 +460,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -472,22 +483,22 @@ checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] @@ -498,9 +509,20 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -509,22 +531,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 3.0.2", ] [[package]] diff --git a/client/package.json5 b/client/package.json5 index abfc1de4..76c5952e 100644 --- a/client/package.json5 +++ b/client/package.json5 @@ -43,7 +43,7 @@ url: 'https://github.com/bjones1/CodeChat_editor', }, type: 'module', - version: '0.1.61', + version: '0.2.0', dependencies: { '@codemirror/commands': '^6.10.4', '@codemirror/lang-cpp': '^6.0.3', @@ -53,7 +53,7 @@ '@codemirror/lang-java': '^6.0.2', '@codemirror/lang-javascript': '^6.2.5', '@codemirror/lang-json': '^6.0.2', - '@codemirror/lang-markdown': '^6.5.0', + '@codemirror/lang-markdown': '^6.5.1', '@codemirror/lang-php': '^6.0.2', '@codemirror/lang-python': '^6.2.1', '@codemirror/lang-rust': '^6.0.2', @@ -64,13 +64,13 @@ '@codemirror/legacy-modes': '^6.5.3', '@codemirror/state': '^6.7.1', '@codemirror/view': '^6.43.6', - '@hpcc-js/wasm-graphviz': '^1.24.1', + '@hpcc-js/wasm-graphviz': '^1.27.0', '@mathjax/mathjax-newcm-font': '^4.1.3', codemirror: '^6.0.2', mathjax: '^4.1.3', mermaid: '^11.16.0', 'pdfjs-dist': '^6.1.200', - tinymce: '^8.7.0', + tinymce: '^8.8.0', 'toastify-js': '^1.12.0', }, devDependencies: { @@ -82,11 +82,11 @@ '@types/mocha': '^10.0.10', '@types/node': '^24.13.3', '@types/toastify-js': '^1.12.4', - '@typescript-eslint/eslint-plugin': '^8.63.0', - '@typescript-eslint/parser': '^8.63.0', + '@typescript-eslint/eslint-plugin': '^8.64.0', + '@typescript-eslint/parser': '^8.64.0', chai: '^6.2.2', esbuild: '^0.28.1', - eslint: '^10.6.0', + eslint: '^10.7.0', 'eslint-config-prettier': '^10.1.8', 'eslint-plugin-import': '^2.32.0', 'eslint-plugin-prettier': '^5.5.6', @@ -94,8 +94,14 @@ mocha: '^11.7.6', 'npm-check-updates': '^22.2.9', prettier: '^3.9.5', - typescript: '^6.0.3', - 'typescript-eslint': '^8.63.0', + /* Typescript 7 support for eslint is blocked per (this + issue)\[https://github.com/typescript-eslint/typescript-eslint/issues/10940\]. + Instead, see + [Running Typescript 7.0 Side-by-Side with TypeScript 6.0](https://devblogs.microsoft.com/typescript/announcing-typescript-7-0/#running-side-by-side-with-typescript-6.0) + for a workaround. */ + typescript: 'npm:@typescript/typescript6@^6.0.2', + "@typescript/native": "npm:typescript@^7.0.2", + 'typescript-eslint': '^8.64.0', }, scripts: { test: 'echo "Error: no test specified" && exit 1', diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index fd553337..16a88005 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -33,8 +33,8 @@ importers: specifier: ^6.0.2 version: 6.0.2 '@codemirror/lang-markdown': - specifier: ^6.5.0 - version: 6.5.0 + specifier: ^6.5.1 + version: 6.5.1 '@codemirror/lang-php': specifier: ^6.0.2 version: 6.0.2 @@ -66,8 +66,8 @@ importers: specifier: ^6.43.6 version: 6.43.6 '@hpcc-js/wasm-graphviz': - specifier: ^1.24.1 - version: 1.24.1 + specifier: ^1.27.0 + version: 1.27.0 '@mathjax/mathjax-newcm-font': specifier: ^4.1.3 version: 4.1.3 @@ -84,8 +84,8 @@ importers: specifier: ^6.1.200 version: 6.1.200 tinymce: - specifier: ^8.7.0 - version: 8.7.0 + specifier: ^8.8.0 + version: 8.8.0 toastify-js: specifier: ^1.12.0 version: 1.12.0 @@ -95,7 +95,7 @@ importers: version: 1.4.0 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) + version: 10.0.1(eslint@10.7.0(supports-color@8.1.1)) '@types/chai': specifier: ^5.2.3 version: 5.2.3 @@ -115,11 +115,14 @@ importers: specifier: ^1.12.4 version: 1.12.4 '@typescript-eslint/eslint-plugin': - specifier: ^8.63.0 - version: 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) '@typescript-eslint/parser': - specifier: ^8.63.0 - version: 8.63.0(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript/native': + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 chai: specifier: ^6.2.2 version: 6.2.2 @@ -127,17 +130,17 @@ importers: specifier: ^0.28.1 version: 0.28.1 eslint: - specifier: ^10.6.0 - version: 10.6.0 + specifier: ^10.7.0 + version: 10.7.0(supports-color@8.1.1) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.6.0) + version: 10.1.8(eslint@10.7.0(supports-color@8.1.1)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0) + version: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) eslint-plugin-prettier: specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.5) + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@8.1.1)))(eslint@10.7.0(supports-color@8.1.1))(prettier@3.9.5) globals: specifier: ^17.7.0 version: 17.7.0 @@ -151,11 +154,11 @@ importers: specifier: ^3.9.5 version: 3.9.5 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' typescript-eslint: - specifier: ^8.63.0 - version: 8.63.0(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) packages: @@ -195,8 +198,8 @@ packages: '@codemirror/lang-json@6.0.2': resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==} - '@codemirror/lang-markdown@6.5.0': - resolution: {integrity: sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==} + '@codemirror/lang-markdown@6.5.1': + resolution: {integrity: sha512-6re5avCNfyRMIoi3XNjbEfQM1vTeVD3JS3g/Fyegyso/eoANFM71Cyvbb66LDyYtQLMEcRFlzioywCqDo9SlLA==} '@codemirror/lang-php@6.0.2': resolution: {integrity: sha512-ZKy2v1n8Fc8oEXj0Th0PUMXzQJ0AIR6TaZU+PbDHExFwdu+guzOA4jmCHS1Nz4vbFezwD7LyBdDnddSJeScMCA==} @@ -437,8 +440,8 @@ packages: resolution: {integrity: sha512-+CNAzxglkrpNf/kKywqQfk74QjtceuOE7Qm+AF8miRvPF/wmmK5+OJOgVh3AVTT3RP2mH3+FOaxlE5v72owk0A==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - '@hpcc-js/wasm-graphviz@1.24.1': - resolution: {integrity: sha512-n2aImBtWdMm8G1kKOKK946n3DNGQiEk+aOX6qsQQj6URSSWCkwioXeQt1EeYTl3dRY3lcu09Wa5/teaVhiSJaw==} + '@hpcc-js/wasm-graphviz@1.27.0': + resolution: {integrity: sha512-akmM2zI64U2osEByxcULTQLe2PGlpsWCHQYvHPRwKA6HPEFSrnvyXtz0d2SJvlDer9RDZD91BLyFhOTJbUzp3A==} '@humanfs/core@0.19.2': resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} @@ -500,8 +503,8 @@ packages: '@lezer/lr@1.4.10': resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} - '@lezer/markdown@1.7.1': - resolution: {integrity: sha512-MEBZeFSBxgteUjEC3Wxg2Dwld5/JxRKG267L3bMFdibm8KjqSdiJYBeFw1Nt1CM8+zKMpSIEHblY8FD9z38sJQ==} + '@lezer/markdown@1.7.2': + resolution: {integrity: sha512-iTkYvoVcKt3WkeL7qUDyXHONZEwLio4wj8KTNi2dnjQEXBZKMV63BpQrPqfsM+OkvuRbiSTAcycYAsQzLhRNoQ==} '@lezer/php@1.0.5': resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} @@ -745,65 +748,189 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/eslint-plugin@8.63.0': - resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.63.0 + '@typescript-eslint/parser': ^8.64.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.63.0': - resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.63.0': - resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.63.0': - resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.63.0': - resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.63.0': - resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.63.0': - resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.63.0': - resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.63.0': - resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.63.0': - resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} + hasBin: true + '@upsetjs/venn.js@2.0.0': resolution: {integrity: sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==} @@ -1175,8 +1302,8 @@ packages: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} - dompurify@3.4.11: - resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} + dompurify@3.4.12: + resolution: {integrity: sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==} dunder-proto@1.0.1: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} @@ -1305,8 +1432,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1476,8 +1603,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} import-meta-resolve@4.2.0: @@ -2013,8 +2140,8 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} - tinymce@8.7.0: - resolution: {integrity: sha512-V3fBgEzKxKT5d41/3qkXs//2SjANGHzo1MWnU2ai8nss8YrzPprrRFlDbX2X9Z6ClyQAknojs7CzEgcsTdqhPQ==} + tinymce@8.8.0: + resolution: {integrity: sha512-rdhwXLHDjUTJ42Cvq1tKRDvS4h8oeQSiWeNGx82AKNdOm8FGAqKObeEPa6amFGobTXHcoFtPZRtyLHO5zV5mZA==} toastify-js@1.12.0: resolution: {integrity: sha512-HeMHCO9yLPvP9k0apGSdPUWrUbLnxUKNFzgUoZp1PHCLploIX/4DSQ7V8H25ef+h4iO9n0he7ImfcndnN6nDrQ==} @@ -2052,8 +2179,8 @@ packages: resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} engines: {node: '>= 0.4'} - typescript-eslint@8.63.0: - resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} + typescript-eslint@8.64.0: + resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2064,6 +2191,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2215,7 +2347,7 @@ snapshots: '@codemirror/language': 6.12.4 '@lezer/json': 1.0.3 - '@codemirror/lang-markdown@6.5.0': + '@codemirror/lang-markdown@6.5.1': dependencies: '@codemirror/autocomplete': 6.20.3 '@codemirror/lang-html': 6.4.11 @@ -2223,7 +2355,7 @@ snapshots: '@codemirror/state': 6.7.1 '@codemirror/view': 6.43.6 '@lezer/common': 1.5.2 - '@lezer/markdown': 1.7.1 + '@lezer/markdown': 1.7.2 '@codemirror/lang-php@6.0.2': dependencies: @@ -2388,14 +2520,14 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(supports-color@8.1.1))': dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@8.1.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@8.1.1)': dependencies: '@eslint/object-schema': 3.0.5 debug: 4.4.3(supports-color@8.1.1) @@ -2422,9 +2554,9 @@ snapshots: '@eslint/css-tree': 4.0.4 '@eslint/plugin-kit': 0.7.2 - '@eslint/js@10.0.1(eslint@10.6.0)': + '@eslint/js@10.0.1(eslint@10.7.0(supports-color@8.1.1))': optionalDependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@8.1.1) '@eslint/object-schema@3.0.5': {} @@ -2433,7 +2565,7 @@ snapshots: '@eslint/core': 1.2.1 levn: 0.4.1 - '@hpcc-js/wasm-graphviz@1.24.1': {} + '@hpcc-js/wasm-graphviz@1.27.0': {} '@humanfs/core@0.19.2': dependencies: @@ -2520,7 +2652,7 @@ snapshots: dependencies: '@lezer/common': 1.5.2 - '@lezer/markdown@1.7.1': + '@lezer/markdown@1.7.2': dependencies: '@lezer/common': 1.5.2 '@lezer/highlight': 1.2.3 @@ -2767,97 +2899,161 @@ snapshots: '@types/trusted-types@2.0.7': optional: true - '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 - eslint: 10.6.0 - ignore: 7.0.5 + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 10.7.0(supports-color@8.1.1) + ignore: 7.0.6 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0 - typescript: 6.0.3 + eslint: 10.7.0(supports-color@8.1.1) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.63.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.64.0 debug: 4.4.3(supports-color@8.1.1) - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.63.0': + '@typescript-eslint/scope-manager@8.64.0': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 - '@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': dependencies: - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) debug: 4.4.3(supports-color@8.1.1) - eslint: 10.6.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + eslint: 10.7.0(supports-color@8.1.1) + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.63.0': {} + '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1)': dependencies: - '@typescript-eslint/project-service': 8.63.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 debug: 4.4.3(supports-color@8.1.1) minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - eslint: 10.6.0 - typescript: 6.0.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(supports-color@8.1.1)) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + eslint: 10.7.0(supports-color@8.1.1) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.63.0': + '@typescript-eslint/visitor-keys@8.64.0': dependencies: - '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/types': 8.64.0 eslint-visitor-keys: 5.0.1 + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': + dependencies: + '@typescript/old': typescript@6.0.3 + '@upsetjs/venn.js@2.0.0': optionalDependencies: d3-selection: 3.0.0 @@ -3243,9 +3439,11 @@ snapshots: dayjs@1.11.21: {} - debug@3.2.7: + debug@3.2.7(supports-color@8.1.1): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 8.1.1 debug@4.4.3(supports-color@8.1.1): dependencies: @@ -3279,7 +3477,7 @@ snapshots: dependencies: esutils: 2.0.3 - dompurify@3.4.11: + dompurify@3.4.12: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -3422,40 +3620,40 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.6.0): + eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@8.1.1)): dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@8.1.1) - eslint-import-resolver-node@0.3.10: + eslint-import-resolver-node@0.3.10(supports-color@8.1.1): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) is-core-module: 2.16.2 resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 10.7.0(supports-color@8.1.1) + eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7 + debug: 3.2.7(supports-color@8.1.1) doctrine: 2.1.0 - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0) + eslint: 10.7.0(supports-color@8.1.1) + eslint-import-resolver-node: 0.3.10(supports-color@8.1.1) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(eslint-import-resolver-node@0.3.10(supports-color@8.1.1))(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -3467,20 +3665,20 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.5): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@8.1.1)))(eslint@10.7.0(supports-color@8.1.1))(prettier@3.9.5): dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@8.1.1) prettier: 3.9.5 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.6.0) + eslint-config-prettier: 10.1.8(eslint@10.7.0(supports-color@8.1.1)) eslint-scope@9.1.2: dependencies: @@ -3493,11 +3691,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0: + eslint@10.7.0(supports-color@8.1.1): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(supports-color@8.1.1)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 + '@eslint/config-array': 0.23.5(supports-color@8.1.1) '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -3683,7 +3881,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.6: {} import-meta-resolve@4.2.0: {} @@ -3897,7 +4095,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.21 - dompurify: 3.4.11 + dompurify: 3.4.12 es-toolkit: 1.49.0 katex: 0.16.47 khroma: 2.1.0 @@ -4284,13 +4482,13 @@ snapshots: fdir: 6.5.0(picomatch@4.0.5) picomatch: 4.0.5 - tinymce@8.7.0: {} + tinymce@8.8.0: {} toastify-js@1.12.0: {} - ts-api-utils@2.5.0(typescript@6.0.3): + ts-api-utils@2.5.0(@typescript/typescript6@6.0.2): dependencies: - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' ts-dedent@2.3.0: {} @@ -4338,19 +4536,42 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.63.0(eslint@10.6.0)(typescript@6.0.3): + typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1): dependencies: - '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 - typescript: 6.0.3 + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@8.1.1) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@8.1.1))(supports-color@8.1.1) + eslint: 10.7.0(supports-color@8.1.1) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 diff --git a/client/src/CodeChatEditor-test.mts b/client/src/CodeChatEditor-test.mts index cf142563..d90e1535 100644 --- a/client/src/CodeChatEditor-test.mts +++ b/client/src/CodeChatEditor-test.mts @@ -45,6 +45,24 @@ import { // From [SO](https://stackoverflow.com/a/39914235). const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +const RENDER_TIMEOUT_MS = 10000; +const MOCHA_TEST_TIMEOUT_MS = RENDER_TIMEOUT_MS + 5000; + +const waitFor = async ( + description: string, + predicate: () => boolean, + timeoutMs = RENDER_TIMEOUT_MS, +) => { + const start = Date.now(); + while (Date.now() - start < timeoutMs) { + if (predicate()) { + return; + } + await sleep(100); + } + assert.fail(`Timed out waiting for ${description}.`); +}; + // Tests // ----- // @@ -97,11 +115,18 @@ window.CodeChatEditor_test = () => { }); }); - test("GraphViz, Mathjax, Mermaid", async function () { - // Wait for the renderers to run. - await sleep(1500); + test("GraphViz, Mathjax, Mermaid", async function (this: Mocha.Context) { + this.timeout(MOCHA_TEST_TIMEOUT_MS); + // Make sure GraphViz includes a `div` at the top of the shadow // root, with a `svg` inside it. + const getGraphVizRoot = () => + document.getElementsByTagName("graphviz-graph")[0] + ?.shadowRoot?.children[0]; + await waitFor( + "GraphViz SVG", + () => getGraphVizRoot()?.children[0]?.tagName === "svg", + ); const gv = document.getElementsByTagName("graphviz-graph")[0] .shadowRoot!.children[0]; @@ -109,6 +134,13 @@ window.CodeChatEditor_test = () => { assert.equal(gv.children[0].tagName, "svg"); // Mermaid graphs start with a div. + const getMermaidRoot = () => + document.getElementsByTagName("wc-mermaid")[0]?.shadowRoot + ?.children[0]; + await waitFor( + "Mermaid SVG", + () => getMermaidRoot()?.children[0]?.tagName === "svg", + ); const mer = document.getElementsByTagName("wc-mermaid")[0].shadowRoot! .children[0]; @@ -116,6 +148,12 @@ window.CodeChatEditor_test = () => { assert.equal(mer.children[0].tagName, "svg"); // MathJax has its own stuff. + await waitFor( + "MathJax containers", + () => + document.getElementsByTagName("mjx-container") + .length === 2, + ); assert.equal( document.getElementsByTagName("mjx-container").length, 2, diff --git a/client/src/CodeChatEditor.mts b/client/src/CodeChatEditor.mts index f9a0cde6..3a46c87c 100644 --- a/client/src/CodeChatEditor.mts +++ b/client/src/CodeChatEditor.mts @@ -716,6 +716,9 @@ export const on_error = (event: Event) => { let err_str: string; if (event instanceof ErrorEvent) { err_str = `${event.filename}:${event.lineno}: ${event.message}`; + if (event.error?.stack) { + err_str += `\n${event.error.stack}`; + } } else if (event instanceof PromiseRejectionEvent) { const reason = event.reason; let userMessage = "An unexpected error occurred. Please try again."; @@ -731,6 +734,9 @@ export const on_error = (event: Event) => { userMessage = reason; } err_str = `Promise rejected: ${userMessage}`; + if (reason instanceof Error && reason.stack) { + err_str += `\n${reason.stack}`; + } } else { err_str = `Unexpected error ${typeof event}: ${event}`; } diff --git a/client/src/shared.mts b/client/src/shared.mts index d3550b0f..caadb971 100644 --- a/client/src/shared.mts +++ b/client/src/shared.mts @@ -43,6 +43,8 @@ import { CodeMirrorDocBlockTuple } from "./rust-types/CodeMirrorDocBlockTuple.js import { UpdateMessageContents } from "./rust-types/UpdateMessageContents.js"; import { ResultOkTypes } from "./rust-types/ResultOkTypes.js"; import { ResultErrTypes } from "./rust-types/ResultErrTypes.js"; +import { CaptureEventWire } from "./rust-types/CaptureEventWire.js"; +import { CaptureStatus } from "./rust-types/CaptureStatus.js"; // Manually define this, since `ts-rs` can't export `webserver.MessageResult`. type MessageResult = { Ok: ResultOkTypes } | { Err: ResultErrTypes }; @@ -55,6 +57,8 @@ export type { CodeMirror, CodeMirrorDiffable, CodeMirrorDocBlockTuple, + CaptureEventWire, + CaptureStatus, CodeChatForWeb, EditorMessage, EditorMessageContents, diff --git a/docs/implementation.md b/docs/implementation.md index 427831c9..fbe9c541 100644 --- a/docs/implementation.md +++ b/docs/implementation.md @@ -60,9 +60,47 @@ Inside the client: The entire VSCode interface is contained in the extension, with the NAPI-RS glue in the corresponding library. -Does this make more sense to place in the TOC? Or is it too wordy there? I think -a diagram as an overview might be helpful. Perhaps the server, client, etc. -should have its of readme files providing some of this. +### Capture path + +Dissertation capture is a web-service-only path. The VS Code extension stores +the portal-issued capture token in VS Code SecretStorage after the user enters +it through the capture manager or **CodeChat Editor: Enter Capture Token** +command. The token is never written to settings, workspace files, or a JSON +configuration file. The extension keeps only non-secret participant/instance +metadata in VS Code global state and asks CaptureWebService for token status +before recording user events. The participant ID in each event comes from that +status response. The extension also caches the last capture-enabled decision for +the same token hash and service URL, so offline fallback cannot turn a +portal-disabled token back into a recordable state. + +When consent, recording, and token status allow capture, the extension sends the +same capture event shape to the local CodeChat server through the existing +NAPI-RS bridge. The server hashes raw local file paths, removes any raw path +fields from event data before local persistence, writes every event to a durable +`capture-spool` FIFO directory under VS Code's global extension storage, and +uploads batches to +`POST /v1/capture/events` using +`Authorization: Bearer `. CodeChat does not read database credentials +from disk and does not connect directly to PostgreSQL; the database schema under +`server/scripts/` documents the server-side table used by CaptureWebService. +The previous local JSON database-secret configuration path is intentionally +removed; any database writer role or password must remain server-side in the +CaptureWebService deployment. +The service endpoint is read only from the user/application-level +`CodeChatEditor.Capture.ServiceBaseUrl` setting; workspace values are ignored so +a repository cannot redirect a stored token. Bearer-token requests require +HTTPS, with `http://localhost` and `http://127.0.0.1` allowed only for local +development. + +The upload worker deletes spooled events only after the service returns `202`. +After a token has been accepted at least once, transient service or network +failures still allow local recording and keep events queued for retry. Each +spool record includes a non-secret token hash and service URL identity; the +worker uploads only records matching the currently configured token/service, so +events queued under an old token remain local until that matching token is +configured again. Authentication failures pause upload until a valid token is +entered, malformed or oversized events are moved to the spool quarantine +directory, and blocking service calls use bounded request timeouts. Architecture ------------------------------------------ diff --git a/extensions/VSCode/.gitignore b/extensions/VSCode/.gitignore index 8c5160c5..4d024c8a 100644 --- a/extensions/VSCode/.gitignore +++ b/extensions/VSCode/.gitignore @@ -22,6 +22,7 @@ # NPM node_modules/ out/ +.test-output/ # Server support files log4rs.yml @@ -33,5 +34,5 @@ src/index.d.ts src/index.js src/codechat-editor-client.win32-x64-msvc.node .windows/ - +*.log # CodeChat Editor lexer: python. See TODO. diff --git a/extensions/VSCode/.vscodeignore b/extensions/VSCode/.vscodeignore index cd2f89a4..faaec241 100644 --- a/extensions/VSCode/.vscodeignore +++ b/extensions/VSCode/.vscodeignore @@ -27,6 +27,7 @@ src/** # Omit libraries, since `esbuild` takes care of packaging. node_modules/ +.test-output/** # Omit VSCode config. .vscode/** @@ -41,6 +42,7 @@ target/** # Misc files not needed in a package. .eslintrc.yml .gitignore +out/*.test.cjs build.rs Cargo.lock Cargo.toml diff --git a/extensions/VSCode/Cargo.lock b/extensions/VSCode/Cargo.lock index 2f4bbfb7..07d2d4db 100644 --- a/extensions/VSCode/Cargo.lock +++ b/extensions/VSCode/Cargo.lock @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -191,7 +191,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.4", + "socket2 0.6.5", "time", "tracing", "url", @@ -206,7 +206,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -306,7 +306,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b9d3370580a12f4b7a10fdcc18b28942c083ba570e3d954fe59d10951b85a2" dependencies = [ - "cssparser 0.37.0", + "cssparser", "html5ever", "maplit", "url", @@ -373,9 +373,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" @@ -410,17 +410,6 @@ dependencies = [ "walkdir", ] -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "autocfg" version = "1.5.1" @@ -454,9 +443,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" @@ -502,9 +491,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "serde_core", @@ -541,12 +530,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.12.1" @@ -576,9 +559,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -612,15 +595,16 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -628,9 +612,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -647,7 +631,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -656,12 +640,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - [[package]] name = "cobs" version = "0.3.0" @@ -673,7 +651,7 @@ dependencies = [ [[package]] name = "codechat-editor-server" -version = "0.1.61" +version = "0.2.0" dependencies = [ "actix-files", "actix-http", @@ -715,10 +693,10 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "test_utils", "thiserror", "tokio", - "tokio-postgres", "tracing", "tracing-log", "tracing-subscriber", @@ -731,7 +709,7 @@ dependencies = [ [[package]] name = "codechat-editor-vscode-extension" -version = "0.1.61" +version = "0.2.0" dependencies = [ "codechat-editor-server", "log", @@ -779,23 +757,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-str" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" -dependencies = [ - "const-str-proc-macro", -] - -[[package]] -name = "const-str-proc-macro" -version = "0.3.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "convert_case" @@ -909,63 +873,43 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "cssparser" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.11.3", - "smallvec", -] - [[package]] name = "cssparser" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ + "cssparser-macros", "dtoa-short", "itoa", + "phf 0.13.1", "smallvec", ] [[package]] name = "cssparser-color" -version = "0.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" +checksum = "bbaa233e1dcd9c13a5d3e3a8a2c0f5a727bac380398345dbcb31db4597edc86b" dependencies = [ - "cssparser 0.33.0", + "cssparser", ] [[package]] name = "cssparser-macros" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "ctor" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb22e947478ccf9dc44d8922042c677a63fbb88f2cb468521d1145816e5087cb" - -[[package]] -name = "ctutils" -version = "0.4.2" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", -] +checksum = "a394189d59f9befacce833f337f7b1eca5e9a91221bcdd4d28e0114d96e597b3" [[package]] name = "dashmap" @@ -1020,7 +964,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", "unicode-xid", ] @@ -1045,7 +989,6 @@ dependencies = [ "block-buffer", "const-oid", "crypto-common", - "ctutils", ] [[package]] @@ -1056,16 +999,15 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "dprint-core" -version = "0.67.4" +version = "0.68.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1d827947704a9495f705d6aeed270fa21a67f825f22902c28f38dc3af7a9ae" +checksum = "7eb1dc2cac6929b352e64ee15d45cecb990f081eebbec99be0444edb01e642a8" dependencies = [ - "anyhow", "bumpalo", "hashbrown 0.15.5", "indexmap", @@ -1086,15 +1028,15 @@ dependencies = [ [[package]] name = "dprint-plugin-markdown" -version = "0.21.1" -source = "git+https://github.com/bjones1/dprint-plugin-markdown.git?branch=all-fixes#edcf25b35e1702f90d0c898a01dbec69d74a775e" +version = "0.22.1" +source = "git+https://github.com/bjones1/dprint-plugin-markdown.git?branch=all-fixes#3e767b74c195fdeb88033db99b9a35cf16d4a248" dependencies = [ - "anyhow", "dprint-core", "dprint-core-macros", "pulldown-cmark 0.11.3", "regex", "serde", + "thiserror", "unicode-width", ] @@ -1168,17 +1110,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "file-id" @@ -1249,9 +1185,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1264,9 +1200,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1274,15 +1210,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1291,38 +1227,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1343,7 +1279,7 @@ checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", ] [[package]] @@ -1372,9 +1308,9 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1454,15 +1390,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest", -] - [[package]] name = "htmd" version = "0.5.4" @@ -1666,9 +1593,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7" +checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" dependencies = [ "crossbeam-deque", "globset", @@ -1794,7 +1721,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1813,7 +1740,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -1891,25 +1818,16 @@ dependencies = [ "windows-link", ] -[[package]] -name = "libredox" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" -dependencies = [ - "libc", -] - [[package]] name = "lightningcss" -version = "1.0.0-alpha.71" +version = "1.0.0-alpha.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6314c2f0590ac93c86099b98bb7ba8abcf759bfd89604ffca906472bb54937" +checksum = "d31b760f96e8fdfe1d0c295e4bf76c503d6f15d2d470d53bd8cd1f7aa8c7d934" dependencies = [ "ahash 0.8.12", "bitflags", "const-str", - "cssparser 0.33.0", + "cssparser", "cssparser-color", "dashmap", "data-encoding", @@ -2010,7 +1928,7 @@ dependencies = [ "log-mdc", "mock_instant", "parking_lot", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde-value", "serde_json", @@ -2066,16 +1984,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest", -] - [[package]] name = "memchr" version = "2.8.3" @@ -2157,13 +2065,13 @@ checksum = "659579df697b372ef9e36f02fcbb41f6d6f157dcec7db9c9618fa0f23cf0fc20" [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -2175,9 +2083,9 @@ checksum = "9bb517913cfcfb9eeda59f36020269075a152701a01606c612f547e4890be399" [[package]] name = "napi" -version = "3.10.3" +version = "3.10.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c71997d6f7ad4a756966e452426848ac27d3b37a295302d63afbbcce0270f93" +checksum = "6826e5ddc15589b2d68c8ad5321c18e85d40488e93e32962f362e572669bccf6" dependencies = [ "bitflags", "ctor", @@ -2197,36 +2105,36 @@ checksum = "c9c366d2c8c60b86fa632df75f745509b52f9128f91a6bad4c796e44abb505e1" [[package]] name = "napi-derive" -version = "3.5.9" +version = "3.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ba572deef53e2c386759a8c2014175a62679d74ff83adc205c8bc0e0285727" +checksum = "b0fe526e81c105d3640516fcde83909dd1afe757c0d7a15af58830b5bc0fb9a1" dependencies = [ "convert_case 0.11.0", "ctor", "napi-derive-backend", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "napi-derive-backend" -version = "5.1.1" +version = "5.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd961eb2aa8965e3f29722d754f3a86907eb1984e2fbcbe3fe87b9a02d6bfba" +checksum = "514281397bcddd9ea9a876c7a21a57bff2374237a000ca9a64ea0211ec1993e2" dependencies = [ "convert_case 0.11.0", "proc-macro2", "quote", "semver", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "napi-sys" -version = "3.2.2" +version = "3.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f5bcdf71abd3a50d00b49c1c2c75251cb3c913777d6139cd37dabc093a5e400" +checksum = "73e43cf2eb0bd1bf95a43c07c076ebd2da5d1e015a71c3d201faeffffcc0ecac" dependencies = [ "libloading", ] @@ -2353,15 +2261,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -2378,15 +2277,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "objc2-system-configuration" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" -dependencies = [ - "objc2-core-foundation", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -2428,9 +2318,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxc-browserslist" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373741e2febe6df186995b668f295e46fd402ee12f312ea2fda8b3b6312c0885" +checksum = "50a403a3c6be65be7bc7730d07d959ec6c143e4ff8117da485df78cd5582260a" dependencies = [ "miniz_oxide 0.9.1", "postcard", @@ -2462,7 +2352,7 @@ checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2504,7 +2394,7 @@ dependencies = [ "phf 0.13.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2789,12 +2679,12 @@ dependencies = [ [[package]] name = "parcel_selectors" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196" +checksum = "d05f71e01edca03d245ab0a9f7ce13a974ceb79baaae8faf2ba0b11de6b90913" dependencies = [ "bitflags", - "cssparser 0.33.0", + "cssparser", "log", "phf 0.11.3", "phf_codegen 0.11.3", @@ -2894,7 +2784,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2912,7 +2802,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -2965,7 +2854,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -2988,19 +2877,6 @@ dependencies = [ "phf_shared 0.14.0", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "phf_macros" version = "0.13.1" @@ -3011,7 +2887,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3024,7 +2900,7 @@ dependencies = [ "phf_shared 0.14.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3078,36 +2954,6 @@ dependencies = [ "serde", ] -[[package]] -name = "postgres-protocol" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" -dependencies = [ - "base64", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.10.2", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" -dependencies = [ - "bytes", - "chrono", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -3167,9 +3013,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3225,9 +3071,9 @@ checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae" [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3252,18 +3098,18 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3342,9 +3188,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3354,9 +3200,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3476,9 +3322,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -3494,9 +3340,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -3523,29 +3369,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "itoa", "memchr", @@ -3637,15 +3483,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -3693,9 +3539,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -3738,17 +3584,6 @@ dependencies = [ "quote", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -3768,9 +3603,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -3785,7 +3631,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3852,22 +3698,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -3891,9 +3737,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -3911,9 +3757,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -3946,9 +3792,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -3956,46 +3802,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.4", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf 0.13.1", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.10.2", - "socket2 0.6.4", - "tokio", - "tokio-util", - "whoami", + "syn 2.0.119", ] [[package]] @@ -4032,7 +3852,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4092,7 +3912,7 @@ checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "termcolor", ] @@ -4123,12 +3943,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-id-start" version = "1.4.0" @@ -4147,21 +3961,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -4227,9 +4026,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", @@ -4281,15 +4080,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -4299,15 +4089,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" -dependencies = [ - "wasi 0.14.7+wasi-0.2.4", -] - [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -4340,7 +4121,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -4391,19 +4172,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "whoami" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" -dependencies = [ - "libc", - "libredox", - "objc2-system-configuration", - "wasite", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" @@ -4488,7 +4256,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4499,7 +4267,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4751,7 +4519,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -4772,7 +4540,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4792,7 +4560,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -4826,14 +4594,14 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zstd" diff --git a/extensions/VSCode/Cargo.toml b/extensions/VSCode/Cargo.toml index 7b581c61..22ab5fd7 100644 --- a/extensions/VSCode/Cargo.toml +++ b/extensions/VSCode/Cargo.toml @@ -32,7 +32,7 @@ license = "GPL-3.0-only" name = "codechat-editor-vscode-extension" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.61" +version = "0.2.0" [lib] crate-type = ["cdylib"] diff --git a/extensions/VSCode/README.md b/extensions/VSCode/README.md index 195ac000..3e7cc0b0 100644 --- a/extensions/VSCode/README.md +++ b/extensions/VSCode/README.md @@ -35,6 +35,32 @@ Running 2. Run the extension again (close the existing window, type `Ctrl+Shift+P` then select Enable the CodeChat Editor). +Study capture +------------- + +Participants who have registered in the capture portal receive a capture token +by email. To use it, run **Manage CodeChat Editor Capture** or **CodeChat +Editor: Enter Capture Token** from the command palette, paste the token, then +turn on consent and recording. The capture status item shows whether the token +is accepted, rejected, unavailable, or disabled by the portal. + +The token is imported through the VS Code UI and persisted only in VS Code +SecretStorage. It is never written to settings, workspace files, or a JSON +configuration file. The participant identity used in capture events comes from +CaptureWebService token status, not from the token text. + +CodeChat sends capture events only to CaptureWebService and does not connect +directly to the capture database. The old JSON database-secret configuration +path is not used by the extension. Events are sanitized and written to a local +FIFO spool under VS Code's global extension storage before upload, so extension +upgrades do not remove queued events. Events recorded offline after the token +has been accepted and capture-enabled upload automatically when the matching +service is reachable again. If the service endpoint changes, update the +user-level +`CodeChatEditor.Capture.ServiceBaseUrl` setting. Workspace values are ignored +for this token-bearing endpoint. Token-bearing service requests must use HTTPS, +except for localhost development endpoints. + Additional documentation ------------------------ diff --git a/extensions/VSCode/package.json b/extensions/VSCode/package.json index 286f244a..3ab7f80f 100644 --- a/extensions/VSCode/package.json +++ b/extensions/VSCode/package.json @@ -41,10 +41,14 @@ "type": "git", "url": "https://github.com/bjones1/CodeChat_Editor" }, - "version": "0.1.61", + "version": "0.2.0", "activationEvents": [ "onCommand:extension.codeChatEditorActivate", - "onCommand:extension.codeChatEditorDeactivate" + "onCommand:extension.codeChatEditorDeactivate", + "onCommand:extension.codeChatCaptureStatus", + "onCommand:extension.codeChatCaptureEnterToken", + "onCommand:extension.codeChatCaptureValidateToken", + "onCommand:extension.codeChatCaptureClearToken" ], "contributes": { "configuration": { @@ -62,6 +66,17 @@ "In the default external web browser" ], "markdownDescription": "Select the location of the CodeChat Editor Client. After changing this value, you **must** close then restart the CodeChat Editor extension." + }, + "CodeChatEditor.Capture.ConsentEnabled": { + "type": "boolean", + "default": false, + "markdownDescription": "Record that participant consent has been given for CodeChat dissertation capture. This defaults to off and persists after setting." + }, + "CodeChatEditor.Capture.ServiceBaseUrl": { + "type": "string", + "scope": "application", + "default": "https://9m2nbv2rvc.execute-api.us-east-2.amazonaws.com/dev", + "markdownDescription": "Application-level base URL for CaptureWebService. Workspace settings are ignored for this token-bearing endpoint. Capture tokens are stored separately in VS Code SecretStorage and are never written to settings." } } }, @@ -73,6 +88,26 @@ { "command": "extension.codeChatEditorDeactivate", "title": "Disable the CodeChat Editor" + }, + { + "command": "extension.codeChatCaptureStatus", + "title": "Manage CodeChat Editor Capture" + }, + { + "command": "extension.codeChatCaptureEnterToken", + "title": "CodeChat Editor: Enter Capture Token" + }, + { + "command": "extension.codeChatCaptureValidateToken", + "title": "CodeChat Editor: Validate Capture Token" + }, + { + "command": "extension.codeChatCaptureClearToken", + "title": "CodeChat Editor: Clear Capture Token" + }, + { + "command": "extension.codeChatInsertReflectionPrompt", + "title": "CodeChat Editor: Insert Reflection Prompt" } ] }, @@ -83,31 +118,33 @@ "@emnapi/core": "^1.11.2", "@emnapi/runtime": "^1.11.2", "@eslint/js": "^10.0.1", - "@napi-rs/cli": "^3.7.2", + "@napi-rs/cli": "^3.7.3", "@tybys/wasm-util": "^0.10.3", "@types/escape-html": "^1.0.4", "@types/node": "^24.13.3", "@types/vscode": "1.61.0", - "@typescript-eslint/eslint-plugin": "^8.63.0", - "@typescript-eslint/parser": "^8.63.0", + "@typescript-eslint/eslint-plugin": "^8.64.0", + "@typescript-eslint/parser": "^8.64.0", "@vscode/vsce": "^3.9.2", "chalk": "^5.6.2", "esbuild": "^0.28.1", - "eslint": "^10.6.0", + "eslint": "^10.7.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-import": "^2.32.0", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^5.5.6", "npm-run-all2": "^9.0.2", "prettier": "^3.9.5", - "typescript": "^6.0.3", - "typescript-eslint": "^8.63.0" + "typescript": "npm:@typescript/typescript6@^6.0.2", + "@typescript/native": "npm:typescript@^7.0.2", + "typescript-eslint": "^8.64.0" }, "optionalDependencies": { "bufferutil": "^4.1.0" }, "scripts": { "compile": "cargo run --manifest-path=../../builder/Cargo.toml ext-build", + "test:capture-policy": "esbuild src/capture-policy.ts --platform=node --format=esm --bundle --outfile=.test-output/capture-policy.test.mjs && node --test src/capture-policy.test.mjs", "vscode:prepublish": "cargo run --manifest-path=../../builder/Cargo.toml ext-build --dist" } } diff --git a/extensions/VSCode/pnpm-lock.yaml b/extensions/VSCode/pnpm-lock.yaml index de6c9a22..c7fe48c6 100644 --- a/extensions/VSCode/pnpm-lock.yaml +++ b/extensions/VSCode/pnpm-lock.yaml @@ -20,10 +20,10 @@ importers: version: 1.11.2 '@eslint/js': specifier: ^10.0.1 - version: 10.0.1(eslint@10.6.0) + version: 10.0.1(eslint@10.7.0(supports-color@7.2.0)) '@napi-rs/cli': - specifier: ^3.7.2 - version: 3.7.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3) + specifier: ^3.7.3 + version: 3.7.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)(supports-color@7.2.0) '@tybys/wasm-util': specifier: ^0.10.3 version: 0.10.3 @@ -37,14 +37,17 @@ importers: specifier: 1.61.0 version: 1.61.0 '@typescript-eslint/eslint-plugin': - specifier: ^8.63.0 - version: 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) '@typescript-eslint/parser': - specifier: ^8.63.0 - version: 8.63.0(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript/native': + specifier: npm:typescript@^7.0.2 + version: typescript@7.0.2 '@vscode/vsce': specifier: ^3.9.2 - version: 3.9.2 + version: 3.9.2(supports-color@7.2.0) chalk: specifier: ^5.6.2 version: 5.6.2 @@ -52,20 +55,20 @@ importers: specifier: ^0.28.1 version: 0.28.1 eslint: - specifier: ^10.6.0 - version: 10.6.0 + specifier: ^10.7.0 + version: 10.7.0(supports-color@7.2.0) eslint-config-prettier: specifier: ^10.1.8 - version: 10.1.8(eslint@10.6.0) + version: 10.1.8(eslint@10.7.0(supports-color@7.2.0)) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0) + version: 2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) eslint-plugin-node: specifier: ^11.1.0 - version: 11.1.0(eslint@10.6.0) + version: 11.1.0(eslint@10.7.0(supports-color@7.2.0)) eslint-plugin-prettier: specifier: ^5.5.6 - version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.5) + version: 5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@7.2.0)))(eslint@10.7.0(supports-color@7.2.0))(prettier@3.9.5) npm-run-all2: specifier: ^9.0.2 version: 9.0.2 @@ -73,11 +76,11 @@ importers: specifier: ^3.9.5 version: 3.9.5 typescript: - specifier: ^6.0.3 - version: 6.0.3 + specifier: npm:@typescript/typescript6@^6.0.2 + version: '@typescript/typescript6@6.0.2' typescript-eslint: - specifier: ^8.63.0 - version: 8.63.0(eslint@10.6.0)(typescript@6.0.3) + specifier: ^8.64.0 + version: 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) optionalDependencies: bufferutil: specifier: ^4.1.0 @@ -91,48 +94,48 @@ packages: '@azu/style-format@1.0.1': resolution: {integrity: sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g==} - '@azure/abort-controller@2.1.2': - resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} - engines: {node: '>=18.0.0'} + '@azure/abort-controller@2.2.0': + resolution: {integrity: sha512-fNAjWnA/nZ2jz31kxR/AqRaUT8ewHBw/WuBIosK0moMy1C9e5ValbDfFdIxJzVOOYaYkV/b2F1S4H/aHiqfVQg==} + engines: {node: '>=22.0.0'} - '@azure/core-auth@1.10.1': - resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} - engines: {node: '>=20.0.0'} + '@azure/core-auth@1.11.0': + resolution: {integrity: sha512-IUZydyTUkDnYdstOW9pFOOUQlBjAepK5teihDE3x6yxsPJs/hsAaaYpeGxdxrgtOiJbBKSjKW7MDk7AEhb4LRg==} + engines: {node: '>=22.0.0'} - '@azure/core-client@1.10.2': - resolution: {integrity: sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==} - engines: {node: '>=20.0.0'} + '@azure/core-client@1.11.0': + resolution: {integrity: sha512-JjQWO6akOck45PH/XBrxzsQGAiKrfFl4m5iggJ0ItMIz5omRufOXWpqCPpdjKN3vKDzlSUvFjaMb7Zwf0gvAdA==} + engines: {node: '>=22.0.0'} - '@azure/core-rest-pipeline@1.24.0': - resolution: {integrity: sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==} - engines: {node: '>=20.0.0'} + '@azure/core-rest-pipeline@1.25.0': + resolution: {integrity: sha512-bMs8ekJLjX8wPV+9IPBges1SLPyuDtE9g5gLDWOpxzKcoOFQnpLGkbcT1tdw3FaAmDS1gnPmMmJ6y/T5B96kIA==} + engines: {node: '>=22.0.0'} - '@azure/core-tracing@1.3.1': - resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} - engines: {node: '>=20.0.0'} + '@azure/core-tracing@1.4.0': + resolution: {integrity: sha512-eGwxD0AtncrxeBM4tG8R55Pc3rdX1hNW2WibJAgYpCVA6E93mvvVH+LcssoVjOBrSKWS55yEIHsk0X8ctHmfOQ==} + engines: {node: '>=22.0.0'} - '@azure/core-util@1.13.1': - resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} - engines: {node: '>=20.0.0'} + '@azure/core-util@1.14.0': + resolution: {integrity: sha512-9n2pWK61veAuN0V20t9lOuoV4CFMdyAZ1ygZzvBGk/pBBJRib/PjL9PLXa/aI2CcPpyHfqVsxxqLCYl6uZlfDw==} + engines: {node: '>=22.0.0'} '@azure/identity@4.13.1': resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} engines: {node: '>=20.0.0'} - '@azure/logger@1.3.0': - resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} - engines: {node: '>=20.0.0'} + '@azure/logger@1.4.0': + resolution: {integrity: sha512-rbAE25KUfjU/s3XHUdJgceoCP5dEOpMx85J04kF+QMdta73XkuG9JGHHinch+XIoKpBdqljin+KqURpJriSzLA==} + engines: {node: '>=22.0.0'} - '@azure/msal-browser@5.17.0': - resolution: {integrity: sha512-/yTnW2TCk9Mh+2b/NOaHAN+MryUNxzRTaJD/YtrqOA9bpBWfTXn/iyReRbaLrK/btBo3stEzLyEvuWp2NZ5DuA==} + '@azure/msal-browser@5.17.1': + resolution: {integrity: sha512-zBhRGzABKSI7hfWh5EaZmril5ybZ7imBN1qEZl5sDTaelr+l8SnPjZO50Q4dnKnm347YPIlBMSnXKZyh3Yu5DQ==} engines: {node: '>=0.8.0'} - '@azure/msal-common@16.11.1': - resolution: {integrity: sha512-yPohvMwWLv1XnaWnIUyKUh8CvcVChCGqG/VluGwfGmaAfrZTNt5yQ+sIs462Sgw6+e2K83KGmMJ860p73ZSCrw==} + '@azure/msal-common@16.11.2': + resolution: {integrity: sha512-yDhtBOGDCdK9ipQ9g3+wmlMEPnZx2pXaDicDd9jYyR1L+7lEbvEohTDmF5qejZDutZY3m9pWPxeYxzNC701A2w==} engines: {node: '>=0.8.0'} - '@azure/msal-node@5.4.0': - resolution: {integrity: sha512-6EZEParwHRlnSSIikw8FNAnAzwmh71uhveUXdPNFeZFviJ9SH+rwFiurhjzXqICYTrpm3E+dj693QOwfPbJXAQ==} + '@azure/msal-node@5.4.1': + resolution: {integrity: sha512-yqgoyOIMCH7TNaSLMBTP+4LUlbMMf1zgC8nzOFG95lmW82CmsAEtUT0J93e4BdqDcnX5qle/9X+yb7A8Mw9M0g==} engines: {node: '>=20'} '@babel/code-frame@7.29.7': @@ -501,8 +504,8 @@ packages: '@types/node': optional: true - '@napi-rs/cli@3.7.2': - resolution: {integrity: sha512-shDW0Td/XZQpP04Yy+OsMt1ILMKGGkoLcy1zVAsSAK0fLfWm0Upgkmfs/NOV2ZhMQwkgpR3ZEdyHmTwgrUDQuA==} + '@napi-rs/cli@3.7.3': + resolution: {integrity: sha512-iu5BOoYjYVixp5jwE7JniHvg72XuKWXUfXteu+6Gt/XY4/mslsS+Qbipleg1+3CAUGHkWc+ebaMJj7Pc93BXSQ==} engines: {node: '>= 16'} hasBin: true peerDependencies: @@ -546,221 +549,221 @@ packages: '@napi-rs/cross-toolchain-x64-target-x86_64': optional: true - '@napi-rs/lzma-android-arm-eabi@1.4.5': - resolution: {integrity: sha512-Up4gpyw2SacmyKWWEib06GhiDdF+H+CCU0LAV8pnM4aJIDqKKd5LHSlBht83Jut6frkB0vwEPmAkv4NjQ5u//Q==} - engines: {node: '>= 10'} + '@napi-rs/lzma-android-arm-eabi@1.5.1': + resolution: {integrity: sha512-sahBe4ko2Z69NPTddaX6ZgbQZu9SDoITxw1S3dWl1gAGynZG34qHHCT8UaUMFxf3h3zMhCJjEzz4basaBxiTuQ==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm] os: [android] - '@napi-rs/lzma-android-arm64@1.4.5': - resolution: {integrity: sha512-uwa8sLlWEzkAM0MWyoZJg0JTD3BkPknvejAFG2acUA1raXM8jLrqujWCdOStisXhqQjZ2nDMp3FV6cs//zjfuQ==} - engines: {node: '>= 10'} + '@napi-rs/lzma-android-arm64@1.5.1': + resolution: {integrity: sha512-7tkQAJJuBHxAxiEBNFgSTpvrtGpbwZYYJUSOmGEK3OfbdbNeoT2rdBxpM/gY1s+itEVbtOSlpaRPPG19MnwOzA==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm64] os: [android] - '@napi-rs/lzma-darwin-arm64@1.4.5': - resolution: {integrity: sha512-0Y0TQLQ2xAjVabrMDem1NhIssOZzF/y/dqetc6OT8mD3xMTDtF8u5BqZoX3MyPc9FzpsZw4ksol+w7DsxHrpMA==} - engines: {node: '>= 10'} + '@napi-rs/lzma-darwin-arm64@1.5.1': + resolution: {integrity: sha512-XWX8gtF+GHGk3nH3Wm3QUZNcxw9QHsFVZz3MzVLhWWHhceede1J4/vD+3dj3E1iKB9G6mualaZxOoD08R3E+7g==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm64] os: [darwin] - '@napi-rs/lzma-darwin-x64@1.4.5': - resolution: {integrity: sha512-vR2IUyJY3En+V1wJkwmbGWcYiT8pHloTAWdW4pG24+51GIq+intst6Uf6D/r46citObGZrlX0QvMarOkQeHWpw==} - engines: {node: '>= 10'} + '@napi-rs/lzma-darwin-x64@1.5.1': + resolution: {integrity: sha512-CfsqUpMTI1z8enrA/b+GcHM6YDI8D0kqCiqPYEnst4rbOABQ9KZ92ybTTNnlnZ7A017WoMZKUEWc36KXDwi0xg==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [darwin] - '@napi-rs/lzma-freebsd-x64@1.4.5': - resolution: {integrity: sha512-XpnYQC5SVovO35tF0xGkbHYjsS6kqyNCjuaLQ2dbEblFRr5cAZVvsJ/9h7zj/5FluJPJRDojVNxGyRhTp4z2lw==} - engines: {node: '>= 10'} + '@napi-rs/lzma-freebsd-x64@1.5.1': + resolution: {integrity: sha512-bTyNfg90FXIgE61U7l14aMmVOqRQ6AyP5JMT3jmCStaZI18apLNPdzZ8i7yqxZfKvRMVfPjE2brXIw27c+RRgA==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [freebsd] - '@napi-rs/lzma-linux-arm-gnueabihf@1.4.5': - resolution: {integrity: sha512-ic1ZZMoRfRMwtSwxkyw4zIlbDZGC6davC9r+2oX6x9QiF247BRqqT94qGeL5ZP4Vtz0Hyy7TEViWhx5j6Bpzvw==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-arm-gnueabihf@1.5.1': + resolution: {integrity: sha512-vNE+D8nrw+eOkBsdKCsmDhowDV3pIMKXEhedvXfbgrWbrO7GlZJH+RXL+X+RYLxGwi8Ym61ZMt15sIOnNmh9Sw==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm] os: [linux] - '@napi-rs/lzma-linux-arm64-gnu@1.4.5': - resolution: {integrity: sha512-asEp7FPd7C1Yi6DQb45a3KPHKOFBSfGuJWXcAd4/bL2Fjetb2n/KK2z14yfW8YC/Fv6x3rBM0VAZKmJuz4tysg==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-arm64-gnu@1.5.1': + resolution: {integrity: sha512-csUem4WgoKGTprv/pOPm9UIWbb+hrfUwYXefpTHPAEGVFLl5behEFabisJ7FtihCa3yG2Efcl+yw25rlhhrIYw==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm64] os: [linux] libc: [glibc] - '@napi-rs/lzma-linux-arm64-musl@1.4.5': - resolution: {integrity: sha512-yWjcPDgJ2nIL3KNvi4536dlT/CcCWO0DUyEOlBs/SacG7BeD6IjGh6yYzd3/X1Y3JItCbZoDoLUH8iB1lTXo3w==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-arm64-musl@1.5.1': + resolution: {integrity: sha512-kB/xhlVN1eLvVmDJSKZEjp5Gg2xDYexNrB5jwpSMbOkeGS6N9AasByPBg5VqCpMYC+zZi7DM458DRhtWYhqXTQ==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm64] os: [linux] libc: [musl] - '@napi-rs/lzma-linux-ppc64-gnu@1.4.5': - resolution: {integrity: sha512-0XRhKuIU/9ZjT4WDIG/qnX7Xz7mSQHYZo9Gb3MP2gcvBgr6BA4zywQ9k3gmQaPn9ECE+CZg2V7DV7kT+x2pUMQ==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-ppc64-gnu@1.5.1': + resolution: {integrity: sha512-s28RW0W1yBWQc1nbPdF7tp14koqslY3ZWLVI8uaanX292Dc6ezd4NPVwxEoCNBVON/oD7BmUbWGtyFvmm7dQ5A==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [ppc64] os: [linux] libc: [glibc] - '@napi-rs/lzma-linux-riscv64-gnu@1.4.5': - resolution: {integrity: sha512-QrqDIPEUUB23GCpyQj/QFyMlr8SGxxyExeZz9OWFnHfb70kXdTLWrHS/hEI1Ru+lSbQ/6xRqeoGyQ4Aqdg+/RA==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-riscv64-gnu@1.5.1': + resolution: {integrity: sha512-+lGNwYlIN14YPMTNvYtIJJqHFevDTd6Juw/1NmXbWx/iRd/LLrjhlM/yluMX6pxs6NkOGsuuEXJJrbbEUS59OQ==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [riscv64] os: [linux] libc: [glibc] - '@napi-rs/lzma-linux-s390x-gnu@1.4.5': - resolution: {integrity: sha512-k8RVM5aMhW86E9H0QXdquwojew4H3SwPxbRVbl49/COJQWCUjGi79X6mYruMnMPEznZinUiT1jgKbFo2A00NdA==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-s390x-gnu@1.5.1': + resolution: {integrity: sha512-PB44FFWWFrLeQowhcep1hPD1YcLqKlnnY60RMU74qrxTlr4YGEyzeMItJqh2uivBfv9kQScOF/B0J9+Vab/oyw==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [s390x] os: [linux] libc: [glibc] - '@napi-rs/lzma-linux-x64-gnu@1.4.5': - resolution: {integrity: sha512-6rMtBgnIq2Wcl1rQdZsnM+rtCcVCbws1nF8S2NzaUsVaZv8bjrPiAa0lwg4Eqnn1d9lgwqT+cZgm5m+//K08Kw==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-x64-gnu@1.5.1': + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [linux] libc: [glibc] - '@napi-rs/lzma-linux-x64-musl@1.4.5': - resolution: {integrity: sha512-eiadGBKi7Vd0bCArBUOO/qqRYPHt/VQVvGyYvDFt6C2ZSIjlD+HuOl+2oS1sjf4CFjK4eDIog6EdXnL0NE6iyQ==} - engines: {node: '>= 10'} + '@napi-rs/lzma-linux-x64-musl@1.5.1': + resolution: {integrity: sha512-I3nsYrWtrW9JpeCr+mkJIVDt0HY3m6qVUBs5vTtoIvJQxwqf1PBXSy5IS7T53ksQFH2kd2UX8rLxJ7B4WISpZg==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [linux] libc: [musl] - '@napi-rs/lzma-wasm32-wasi@1.4.5': - resolution: {integrity: sha512-+VyHHlr68dvey6fXc2hehw9gHVFIW3TtGF1XkcbAu65qVXsA9D/T+uuoRVqhE+JCyFHFrO0ixRbZDRK1XJt1sA==} - engines: {node: '>=14.0.0'} + '@napi-rs/lzma-wasm32-wasi@1.5.1': + resolution: {integrity: sha512-gy3wwPBa6+XEyA4fUzq6CClrXA1ajXjuVf5zbnHytJRgoHznj+mvpU3+co2fxXwqTCmIpn6KrzqH5bRDztBPhA==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [wasm32] - '@napi-rs/lzma-win32-arm64-msvc@1.4.5': - resolution: {integrity: sha512-eewnqvIyyhHi3KaZtBOJXohLvwwN27gfS2G/YDWdfHlbz1jrmfeHAmzMsP5qv8vGB+T80TMHNkro4kYjeh6Deg==} - engines: {node: '>= 10'} + '@napi-rs/lzma-win32-arm64-msvc@1.5.1': + resolution: {integrity: sha512-dK+huOsHiyH6oJjij+cnjqFCakk2HgWmpI12Xm4pLUyPphe4ebYoJBgehaNAxprmjFqBQ7nL95YPVz9BHyqmPg==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [arm64] os: [win32] - '@napi-rs/lzma-win32-ia32-msvc@1.4.5': - resolution: {integrity: sha512-OeacFVRCJOKNU/a0ephUfYZ2Yt+NvaHze/4TgOwJ0J0P4P7X1mHzN+ig9Iyd74aQDXYqc7kaCXA2dpAOcH87Cg==} - engines: {node: '>= 10'} + '@napi-rs/lzma-win32-ia32-msvc@1.5.1': + resolution: {integrity: sha512-dGE8L+0EQ+GyU9ap9InqB/t/PmPG/bLj918q7OsJ29FuTdn8fK4OX3U4IQZhylHIA+/dQ/SXJk5n4yfah2XVvA==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [ia32] os: [win32] - '@napi-rs/lzma-win32-x64-msvc@1.4.5': - resolution: {integrity: sha512-T4I1SamdSmtyZgDXGAGP+y5LEK5vxHUFwe8mz6D4R7Sa5/WCxTcCIgPJ9BD7RkpO17lzhlaM2vmVvMy96Lvk9Q==} - engines: {node: '>= 10'} + '@napi-rs/lzma-win32-x64-msvc@1.5.1': + resolution: {integrity: sha512-EKW4t/iqdCT/xnd5t9oXLvVER/PMNAWXKqUAl3fgvUcOILeZIIht77/dVnfFcc9htA/DCBXC/6YQWdW+LusjFA==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [win32] - '@napi-rs/lzma@1.4.5': - resolution: {integrity: sha512-zS5LuN1OBPAyZpda2ZZgYOEDC+xecUdAGnrvbYzjnLXkrq/OBC3B9qcRvlxbDR3k5H/gVfvef1/jyUqPknqjbg==} - engines: {node: '>= 10'} + '@napi-rs/lzma@1.5.1': + resolution: {integrity: sha512-sgOZ89+y8cDbY+3WbzR8CtIhCuFRWotZ9/2PjPVDJHz6np5KFTAev0DrwiyTJTgFsCRDhfGlbmhMgyhHbWdZ6g==} + engines: {node: ^22.20 || ^24.12 || >=25} - '@napi-rs/tar-android-arm-eabi@1.1.0': - resolution: {integrity: sha512-h2Ryndraj/YiKgMV/r5by1cDusluYIRT0CaE0/PekQ4u+Wpy2iUVqvzVU98ZPnhXaNeYxEvVJHNGafpOfaD0TA==} + '@napi-rs/tar-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-cAhnA10cSusAUbcE9HtjQY/tZ9BH/0w2sKtRcQc94TzIlnm7QSr1htJSd/PPrbWNPtrv1orXb2CkrHlVlbnlHA==} engines: {node: '>= 10'} cpu: [arm] os: [android] - '@napi-rs/tar-android-arm64@1.1.0': - resolution: {integrity: sha512-DJFyQHr1ZxNZorm/gzc1qBNLF/FcKzcH0V0Vwan5P+o0aE2keQIGEjJ09FudkF9v6uOuJjHCVDdK6S6uHtShAw==} + '@napi-rs/tar-android-arm64@1.1.1': + resolution: {integrity: sha512-EslUWHCDBY/g5abTPBiHLsMaML4GagV0TXLm5WL9hAjx/DDtlxz9fegMb77RJ+f7nFLOIsUxF/3QWFvgOT0sMQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/tar-darwin-arm64@1.1.0': - resolution: {integrity: sha512-Zz2sXRzjIX4e532zD6xm2SjXEym6MkvfCvL2RMpG2+UwNVDVscHNcz3d47Pf3sysP2e2af7fBB3TIoK2f6trPw==} + '@napi-rs/tar-darwin-arm64@1.1.1': + resolution: {integrity: sha512-+A42/6ES5G9CQ35BOwzwA+WBjLID28r2jNPgc0dteD2hhClIhng0mva7D2ujUlXBNmgNOsr1LHn3stA4uTf4NQ==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@napi-rs/tar-darwin-x64@1.1.0': - resolution: {integrity: sha512-EI+CptIMNweT0ms9S3mkP/q+J6FNZ1Q6pvpJOEcWglRfyfQpLqjlC0O+dptruTPE8VamKYuqdjxfqD8hifZDOA==} + '@napi-rs/tar-darwin-x64@1.1.1': + resolution: {integrity: sha512-RYtE8w1dkEvj8hSJCDV5Jw0Rz2i13fsM7u893zv5O9n/4Ad5GNsw/f4RQ7/0YGSFaenkVxqPFrjmEvUHlKzsrg==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@napi-rs/tar-freebsd-x64@1.1.0': - resolution: {integrity: sha512-J0PIqX+pl6lBIAckL/c87gpodLbjZB1OtIK+RDscKC9NLdpVv6VGOxzUV/fYev/hctcE8EfkLbgFOfpmVQPg2g==} + '@napi-rs/tar-freebsd-x64@1.1.1': + resolution: {integrity: sha512-rEepBvCJUwcuvUYkY83e8aot8RsR5Jcnal4PsG3tbWGKW1yAvcXhyMXf0fN6ZGpVRZFnB+FJqDyBxvsCPEXKhw==} engines: {node: '>= 10'} cpu: [x64] os: [freebsd] - '@napi-rs/tar-linux-arm-gnueabihf@1.1.0': - resolution: {integrity: sha512-SLgIQo3f3EjkZ82ZwvrEgFvMdDAhsxCYjyoSuWfHCz0U16qx3SuGCp8+FYOPYCECHN3ZlGjXnoAIt9ERd0dEUg==} + '@napi-rs/tar-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-an1bJdfyhI5FpZYyTQ20mrqwR+a676i8GkaYc4Uy12dH/a7TJIfrK6Qa2Gm46arZvxUvx56qxoRKXbpOjUPvwA==} engines: {node: '>= 10'} cpu: [arm] os: [linux] - '@napi-rs/tar-linux-arm64-gnu@1.1.0': - resolution: {integrity: sha512-d014cdle52EGaH6GpYTQOP9Py7glMO1zz/+ynJPjjzYFSxvdYx0byrjumZk2UQdIyGZiJO2MEFpCkEEKFSgPYA==} + '@napi-rs/tar-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-w++Vtx36T2yHTKws7GVnmHHcUT1ybB59xLWSh9A8bwEpJVG4dG7Qub9mFe5cpcbfrJ+XP2mKKxC3oUJSunK3iQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@napi-rs/tar-linux-arm64-musl@1.1.0': - resolution: {integrity: sha512-L/y1/26q9L/uBqiW/JdOb/Dc94egFvNALUZV2WCGKQXc6UByPBMgdiEyW2dtoYxYYYYc+AKD+jr+wQPcvX2vrQ==} + '@napi-rs/tar-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-Rh6UFhNtj3i4deJHOBINFIeRL0072mgbeyuK5rl1HokKnNoMKx8qKIZNEzBTTqpogMfDHWGvzyTQdnVxes5dpA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@napi-rs/tar-linux-ppc64-gnu@1.1.0': - resolution: {integrity: sha512-EPE1K/80RQvPbLRJDJs1QmCIcH+7WRi0F73+oTe1582y9RtfGRuzAkzeBuAGRXAQEjRQw/RjtNqr6UTJ+8UuWQ==} + '@napi-rs/tar-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-Cp+AxFbv9zcyAXtnzQi0OzmgDnQgy2w9D4Ubr+iwzMtVgJcztzcEoCcCrN1k2ATdEB01LX2Vb49IaocGOZhC9Q==} engines: {node: '>= 10'} cpu: [ppc64] os: [linux] libc: [glibc] - '@napi-rs/tar-linux-s390x-gnu@1.1.0': - resolution: {integrity: sha512-B2jhWiB1ffw1nQBqLUP1h4+J1ovAxBOoe5N2IqDMOc63fsPZKNqF1PvO/dIem8z7LL4U4bsfmhy3gBfu547oNQ==} + '@napi-rs/tar-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-ZyscC3SYKTBWyDRYjLOKAd5TyJ7q0KACRdQ8bWrb3rgrra1CCIJD66CsGTH6Dh0AVSdfLwZ8MfIIXU6+14BMjQ==} engines: {node: '>= 10'} cpu: [s390x] os: [linux] libc: [glibc] - '@napi-rs/tar-linux-x64-gnu@1.1.0': - resolution: {integrity: sha512-tbZDHnb9617lTnsDMGo/eAMZxnsQFnaRe+MszRqHguKfMwkisc9CCJnks/r1o84u5fECI+J/HOrKXgczq/3Oww==} + '@napi-rs/tar-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-LlIv+zg4fiOQge9LQX/ieBdRWE2fhVDjCTHxnunZkbugNmdhdelxWf1RpZb/6ZujWpNF4LPu4N/MW7ygg2oYAQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@napi-rs/tar-linux-x64-musl@1.1.0': - resolution: {integrity: sha512-dV6cODlzbO8u6Anmv2N/ilQHq/AWz0xyltuXoLU3yUyXbZcnWYZuB2rL8OBGPmqNcD+x9NdScBNXh7vWN0naSQ==} + '@napi-rs/tar-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-gZBeoKLjanOVj55qk4EMu13P2i9M0SuINmlGQkOxm1niIJofexzddHUYtqO5o/5QqtyL8lADmAcZplLILMLhHA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@napi-rs/tar-wasm32-wasi@1.1.0': - resolution: {integrity: sha512-jIa9nb2HzOrfH0F8QQ9g3WE4aMH5vSI5/1NYVNm9ysCmNjCCtMXCAhlI3WKCdm/DwHf0zLqdrrtDFXODcNaqMw==} + '@napi-rs/tar-wasm32-wasi@1.1.1': + resolution: {integrity: sha512-rwtQ1Mdt/ft6g6I54fJzbUeLspl4yTwj6I3UJ6mitKnrN42soJkcDrdh3Y/FGvlpqZTad2YMQ96fGJl3EtAm2Q==} engines: {node: '>=14.0.0'} cpu: [wasm32] - '@napi-rs/tar-win32-arm64-msvc@1.1.0': - resolution: {integrity: sha512-vfpG71OB0ijtjemp3WTdmBKJm9R70KM8vsSExMsIQtV0lVzP07oM1CW6JbNRPXNLhRoue9ofYLiUDk8bE0Hckg==} + '@napi-rs/tar-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-30PVp1AehRpfwxmv5wI4cg0yj3WmWBsZ+1QnLGnvEELu7Eu/+dhNU0nrmhI7VfPgLwSRK2eg9DQTB3tP7Wv9bA==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@napi-rs/tar-win32-ia32-msvc@1.1.0': - resolution: {integrity: sha512-hGPyPW60YSpOSgzfy68DLBHgi6HxkAM+L59ZZZPMQ0TOXjQg+p2EW87+TjZfJOkSpbYiEkULwa/f4a2hcVjsqQ==} + '@napi-rs/tar-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-aI3/rmz+izUChiSeaPxcasAOxhf3FpJNuIHMXlxS/vpW+HIxUsSDR5+XV61PEG5DL4L/75iENVUxmSGM5l2yaw==} engines: {node: '>= 10'} cpu: [ia32] os: [win32] - '@napi-rs/tar-win32-x64-msvc@1.1.0': - resolution: {integrity: sha512-L6Ed1DxXK9YSCMyvpR8MiNAyKNkQLjsHsHK9E0qnHa8NzLFqzDKhvs5LfnWxM2kJ+F7m/e5n9zPm24kHb3LsVw==} + '@napi-rs/tar-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-yJsB2IsrODQVLKbm2Fg1nHiVRbEj49mSPbj4x7JPZWJI0jGVPjohE2Sif0FBbx8OxsVoUODvS0BwksZZ8jl/OA==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@napi-rs/tar@1.1.0': - resolution: {integrity: sha512-7cmzIu+Vbupriudo7UudoMRH2OA3cTw67vva8MxeoAe5S7vPFI7z0vp0pMXiA25S8IUJefImQ90FeJjl8fjEaQ==} + '@napi-rs/tar@1.1.1': + resolution: {integrity: sha512-p6q2HhUc5vwH1CNwfOcrhLoxfgn8ust8Sqlfx+sA4VzAcp1cMbvbkl99tZZlDqOjCHgQNSiTfk/yWPjl/D42qA==} engines: {node: '>= 10'} '@napi-rs/wasm-runtime@1.1.6': @@ -1019,68 +1022,192 @@ packages: '@types/vscode@1.61.0': resolution: {integrity: sha512-9k5Nwq45hkRwdfCFY+eKXeQQSbPoA114mF7U/4uJXRBJeGIO7MuJdhF1PnaDN+lllL9iKGQtd6FFXShBXMNaFg==} - '@typescript-eslint/eslint-plugin@8.63.0': - resolution: {integrity: sha512-rvwSgqT+DHpWdzfSzPatRLm02a0GlESt++9iy3hLCDY4BgkaLcl8LBi9Yh7XGFBpwcBE/K3024QuXWTpbz4FfQ==} + '@typescript-eslint/eslint-plugin@8.64.0': + resolution: {integrity: sha512-CGvQPBxN3wZLu6Rz2kFUpZeoCm78xUic92ck39KPePkO1NPOwjCqdQnm5Q87tpWw9vcBvW8XLrDXjH9PWYtJ3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.63.0 + '@typescript-eslint/parser': ^8.64.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.63.0': - resolution: {integrity: sha512-gwh4gvvlaVDKKxyfxMG+Gnu1u9X0OQBwyGLkbwB65dIzBKnxeRiJlNFqlI3zwVhNXJIs6qV7mlFCn/BIajlVig==} + '@typescript-eslint/parser@8.64.0': + resolution: {integrity: sha512-KA0OshtlcCCXmbfqyZkM5pV3/WNraJf7DkJRLpyrmwPtud57H5BDX7C3k0LPSPxpprfRL+cJDGabF10mvNCoCw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.63.0': - resolution: {integrity: sha512-e5dh0/UI0ok53AlZ5wRkXCB32z/f2jUZqPR/ygAw5WYaSw8j9EoJWlS7wQjr/dmOaqWjnPIn2m+HhVPCMWGZVQ==} + '@typescript-eslint/project-service@8.64.0': + resolution: {integrity: sha512-tk4WpOJ6IEbGrVHaNmM0YRrwAD3exZlIK3iadQNAxh4YKk6jvUQ4ecq18n+v7+meh+cJ3j+D8nbk8sRKhlwLQg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.63.0': - resolution: {integrity: sha512-uUyfMWCnDSN8bCpcrY8nGP2BLkQ9Xn0GsipcONcpIDWhwhO4ZSyHvyS14U3X75mzxWxL3I2UZIrenTzdzcJO8A==} + '@typescript-eslint/scope-manager@8.64.0': + resolution: {integrity: sha512-CXEaFdYXjSTgKhisNkwCcJwTP8Pl+fmRrEQrri4nm3vU743bALrxzLmq7fHG/7e6a5xO0lDYeURpZmBuhHk54w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.63.0': - resolution: {integrity: sha512-sUAbkulqBAsncKnbRP3+7CtQFRKicexnj7ZwNC6ddCR7EmrXvjvdCYMJbUIqMd6lwoEriZjwLo08aS5tSjVMHg==} + '@typescript-eslint/tsconfig-utils@8.64.0': + resolution: {integrity: sha512-2yo8rRNKuzbVWQp5kslhANqZ2uDAeROQHBRZNPu8JDsHmeFNj/XJJhX/FhNUWmkHHvoNsKa6+tHJiig87EzsQw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.63.0': - resolution: {integrity: sha512-Nzzh/OGxVCOjObjaj1CQF2RUasyYy2Jfuh+zZ3PjLzG2fYRriAiZLib9UKtO+CpQAS3YHiAS+ckZDclwqI1TPA==} + '@typescript-eslint/type-utils@8.64.0': + resolution: {integrity: sha512-XWG4Fmmv/6SvyS9nH8jWrKs6terwJvE8cyRt1CzYYqzp9OrPhCT4cMc/f7C6RZCwG+qMmiffJS1/qJP8G1URtg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.63.0': - resolution: {integrity: sha512-xyLtl9DUBBFrcJS4x2pIqGLH68/tC2uOa4Z7pUteW09D3bXnnXUom4dyPikzWgB7llmIc1zoeI3aoUdC4rPK/Q==} + '@typescript-eslint/types@8.64.0': + resolution: {integrity: sha512-qjhfuTfLXjA4IOzXvz0rTjT01BqEiIgPoUeMwiEjnaHKJMTNo8rH5pYW1a2L/0Dnux2fPC85AeyJoWaGa8WxTA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.63.0': - resolution: {integrity: sha512-ygBkU+B7ex5UI/gKhaqexWev79uISfIv7XQCRNYO/jmD8rGLPyWLAb3KMRT6nd8Gt9bmUBi9+iX6tBdYfOY81Q==} + '@typescript-eslint/typescript-estree@8.64.0': + resolution: {integrity: sha512-Pztpsn1aCE1oWDvDEfUk31nngvvF7vUB5SwHFEaZIFpvw7WJtqUHHL4plBZDA9HfWJJjL13BdG0YrJInTUvoVA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.63.0': - resolution: {integrity: sha512-fUKaeAvrTuQg/Tgt3nliAUSZHJM6DlCcfyEmxCvlX8kieWSStBX+5O5Fnidtc3i2JrH+9c/GL4RY2iasd/GPTA==} + '@typescript-eslint/utils@8.64.0': + resolution: {integrity: sha512-aJUGVB3+U0htrrCjoA8qukw8cm8fNCGAxK/tVoS70k8aeb7DETKeFozRiVFIwEeN9WJLsjaP3ph8I60tY2XZoQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.63.0': - resolution: {integrity: sha512-UexrHGnGTpbuQHct2ExOc2ZcFbGUS9FOesCxxqdBGcpI1BxYu/LZ6U8Aq6/72XtF/qRBk9nhuGHFJIXXMhPMdw==} + '@typescript-eslint/visitor-keys@8.64.0': + resolution: {integrity: sha512-mrtuL8Nsn6gi2H4mo5KMTp823M+3Q19Ew/i+Zlikq20tIMm99C3Ez0dCmkWWnxut20esQvTg8aUSEhMcAOXhEw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typespec/ts-http-runtime@0.3.6': - resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} - engines: {node: '>=20.0.0'} + '@typescript/typescript-aix-ppc64@7.0.2': + resolution: {integrity: sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [aix] + + '@typescript/typescript-darwin-arm64@7.0.2': + resolution: {integrity: sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [darwin] + + '@typescript/typescript-darwin-x64@7.0.2': + resolution: {integrity: sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [darwin] + + '@typescript/typescript-freebsd-arm64@7.0.2': + resolution: {integrity: sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [freebsd] + + '@typescript/typescript-freebsd-x64@7.0.2': + resolution: {integrity: sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [freebsd] + + '@typescript/typescript-linux-arm64@7.0.2': + resolution: {integrity: sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [linux] + + '@typescript/typescript-linux-arm@7.0.2': + resolution: {integrity: sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==} + engines: {node: '>=16.20.0'} + cpu: [arm] + os: [linux] + + '@typescript/typescript-linux-loong64@7.0.2': + resolution: {integrity: sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==} + engines: {node: '>=16.20.0'} + cpu: [loong64] + os: [linux] + + '@typescript/typescript-linux-mips64el@7.0.2': + resolution: {integrity: sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==} + engines: {node: '>=16.20.0'} + cpu: [mips64el] + os: [linux] + + '@typescript/typescript-linux-ppc64@7.0.2': + resolution: {integrity: sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==} + engines: {node: '>=16.20.0'} + cpu: [ppc64] + os: [linux] + + '@typescript/typescript-linux-riscv64@7.0.2': + resolution: {integrity: sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==} + engines: {node: '>=16.20.0'} + cpu: [riscv64] + os: [linux] + + '@typescript/typescript-linux-s390x@7.0.2': + resolution: {integrity: sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==} + engines: {node: '>=16.20.0'} + cpu: [s390x] + os: [linux] + + '@typescript/typescript-linux-x64@7.0.2': + resolution: {integrity: sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [linux] + + '@typescript/typescript-netbsd-arm64@7.0.2': + resolution: {integrity: sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [netbsd] + + '@typescript/typescript-netbsd-x64@7.0.2': + resolution: {integrity: sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [netbsd] + + '@typescript/typescript-openbsd-arm64@7.0.2': + resolution: {integrity: sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [openbsd] + + '@typescript/typescript-openbsd-x64@7.0.2': + resolution: {integrity: sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [openbsd] + + '@typescript/typescript-sunos-x64@7.0.2': + resolution: {integrity: sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [sunos] + + '@typescript/typescript-win32-arm64@7.0.2': + resolution: {integrity: sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==} + engines: {node: '>=16.20.0'} + cpu: [arm64] + os: [win32] + + '@typescript/typescript-win32-x64@7.0.2': + resolution: {integrity: sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==} + engines: {node: '>=16.20.0'} + cpu: [x64] + os: [win32] + + '@typescript/typescript6@6.0.2': + resolution: {integrity: sha512-mbCddXd+jm7hfx7w2YU64/Av4/NqqeG3GoRZgxPcgoTxYjhrcfJRw9ULch71SS4G+Q3bOXFhRvPqjguN0Hyp5w==} + hasBin: true + + '@typespec/ts-http-runtime@0.3.7': + resolution: {integrity: sha512-JVUD8X2tfDMWjcjLs4yVxxVrS8yR5vnh386GAXT9Qj79nBxxXSaHFQZg5FweLmT8HlPQ3kii6noUB+Z9RN7DvQ==} + engines: {node: '>=22.0.0'} '@vscode/vsce-sign-alpine-arm64@2.0.6': resolution: {integrity: sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==} @@ -1618,8 +1745,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@10.6.0: - resolution: {integrity: sha512-6lVbcqSodALYo+4ELD0heG6lFiFxnLMuLkiMi2qV8LMp54N8tE8FT1GMH+ev4Ti00nFjNze2+Su6DsV5OQW3Dg==} + eslint@10.7.0: + resolution: {integrity: sha512-GVTD7s1vdIl6UYvAfriOPeY1Df8LIZjfofLvHwde+erDHGGuHyuM6xoxRxmHiebhYuD2p1vN4wWh0XzPARSGDQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} hasBin: true peerDependencies: @@ -1674,8 +1801,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.3: - resolution: {integrity: sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==} + fast-uri@3.1.4: + resolution: {integrity: sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -1843,8 +1970,8 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - ignore@7.0.5: - resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + ignore@7.0.6: + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} engines: {node: '>= 4'} imurmurhash@0.1.4: @@ -2024,8 +2151,8 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - json-with-bigint@3.5.8: - resolution: {integrity: sha512-eq/4KP6K34kwa7TcFdtvnftvHCD9KvHOGGICWwMFc4dOOKF5t4iYqnfLK8otCRCRv06FXOzGGyqE8h8ElMvvdw==} + json-with-bigint@3.5.10: + resolution: {integrity: sha512-Vcx+JVNEBts/xfcoCS69sKrOhOk/3TVlvlT+XzUOefVKnnrbYSCKpDCm10pohsJFtsJVYnwa/cXRZ4eElzaM6w==} json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} @@ -2247,8 +2374,8 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} - obug@2.1.3: - resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + obug@2.1.4: + resolution: {integrity: sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==} engines: {node: '>=12.20.0'} once@1.4.0: @@ -2500,8 +2627,8 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - shell-quote@1.9.0: - resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} + shell-quote@1.10.0: + resolution: {integrity: sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==} engines: {node: '>= 0.4'} side-channel-list@1.0.1: @@ -2691,8 +2818,8 @@ packages: typed-rest-client@1.8.11: resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} - typescript-eslint@8.63.0: - resolution: {integrity: sha512-xgwXyzG4sK9ALkBxbyGkTMMOS+imnW65iPhxCQMK83KhxyoDNW7l+IDqEf9vMdoUidHpOoS967RCq4eMiTexwQ==} + typescript-eslint@8.64.0: + resolution: {integrity: sha512-0qg+pDNMnqYzqH9AnNK+39tejHvsShUOUUoRUgtnTGE7QuMZhiFDnozq8nHJVq+Wae6NMLKNWLg5WmkcC/ndyQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2703,6 +2830,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@7.0.2: + resolution: {integrity: sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==} + engines: {node: '>=16.20.0'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -2827,86 +2959,86 @@ snapshots: dependencies: '@azu/format-text': 1.0.2 - '@azure/abort-controller@2.1.2': + '@azure/abort-controller@2.2.0': dependencies: tslib: 2.8.1 - '@azure/core-auth@1.10.1': + '@azure/core-auth@1.11.0(supports-color@7.2.0)': dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-util': 1.13.1 + '@azure/abort-controller': 2.2.0 + '@azure/core-util': 1.14.0(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-client@1.10.2': + '@azure/core-client@1.11.0(supports-color@7.2.0)': dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-rest-pipeline': 1.24.0 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0(supports-color@7.2.0) + '@azure/core-rest-pipeline': 1.25.0(supports-color@7.2.0) + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@7.2.0) + '@azure/logger': 1.4.0(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-rest-pipeline@1.24.0': + '@azure/core-rest-pipeline@1.25.0(supports-color@7.2.0)': dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - '@typespec/ts-http-runtime': 0.3.6 + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0(supports-color@7.2.0) + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@7.2.0) + '@azure/logger': 1.4.0(supports-color@7.2.0) + '@typespec/ts-http-runtime': 0.3.7(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/core-tracing@1.3.1': + '@azure/core-tracing@1.4.0': dependencies: tslib: 2.8.1 - '@azure/core-util@1.13.1': + '@azure/core-util@1.14.0(supports-color@7.2.0)': dependencies: - '@azure/abort-controller': 2.1.2 - '@typespec/ts-http-runtime': 0.3.6 + '@azure/abort-controller': 2.2.0 + '@typespec/ts-http-runtime': 0.3.7(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/identity@4.13.1': - dependencies: - '@azure/abort-controller': 2.1.2 - '@azure/core-auth': 1.10.1 - '@azure/core-client': 1.10.2 - '@azure/core-rest-pipeline': 1.24.0 - '@azure/core-tracing': 1.3.1 - '@azure/core-util': 1.13.1 - '@azure/logger': 1.3.0 - '@azure/msal-browser': 5.17.0 - '@azure/msal-node': 5.4.0 + '@azure/identity@4.13.1(supports-color@7.2.0)': + dependencies: + '@azure/abort-controller': 2.2.0 + '@azure/core-auth': 1.11.0(supports-color@7.2.0) + '@azure/core-client': 1.11.0(supports-color@7.2.0) + '@azure/core-rest-pipeline': 1.25.0(supports-color@7.2.0) + '@azure/core-tracing': 1.4.0 + '@azure/core-util': 1.14.0(supports-color@7.2.0) + '@azure/logger': 1.4.0(supports-color@7.2.0) + '@azure/msal-browser': 5.17.1 + '@azure/msal-node': 5.4.1 open: 10.2.0 tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/logger@1.3.0': + '@azure/logger@1.4.0(supports-color@7.2.0)': dependencies: - '@typespec/ts-http-runtime': 0.3.6 + '@typespec/ts-http-runtime': 0.3.7(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color - '@azure/msal-browser@5.17.0': + '@azure/msal-browser@5.17.1': dependencies: - '@azure/msal-common': 16.11.1 + '@azure/msal-common': 16.11.2 - '@azure/msal-common@16.11.1': {} + '@azure/msal-common@16.11.2': {} - '@azure/msal-node@5.4.0': + '@azure/msal-node@5.4.1': dependencies: - '@azure/msal-common': 16.11.1 + '@azure/msal-common': 16.11.2 jsonwebtoken: 9.0.3 '@babel/code-frame@7.29.7': @@ -3008,17 +3140,17 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@10.6.0)': + '@eslint-community/eslint-utils@4.9.1(eslint@10.7.0(supports-color@7.2.0))': dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@7.2.0) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.23.5': + '@eslint/config-array@0.23.5(supports-color@7.2.0)': dependencies: '@eslint/object-schema': 3.0.5 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.5 transitivePeerDependencies: - supports-color @@ -3031,9 +3163,9 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/js@10.0.1(eslint@10.6.0)': + '@eslint/js@10.0.1(eslint@10.7.0(supports-color@7.2.0))': optionalDependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@7.2.0) '@eslint/object-schema@3.0.5': {} @@ -3177,10 +3309,10 @@ snapshots: optionalDependencies: '@types/node': 24.13.3 - '@napi-rs/cli@3.7.2(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)': + '@napi-rs/cli@3.7.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)(@types/node@24.13.3)(supports-color@7.2.0)': dependencies: '@inquirer/prompts': 8.5.2(@types/node@24.13.3) - '@napi-rs/cross-toolchain': 1.0.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + '@napi-rs/cross-toolchain': 1.0.3(supports-color@7.2.0) '@napi-rs/wasm-tools': 1.0.1(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) '@octokit/rest': 22.0.1 clipanion: 4.0.0-rc.4(typanion@3.14.0) @@ -3188,7 +3320,7 @@ snapshots: emnapi: 1.11.2 es-toolkit: 1.49.0 js-yaml: 4.3.0 - obug: 2.1.3 + obug: 2.1.4 semver: 7.8.5 typanion: 3.14.0 optionalDependencies: @@ -3209,169 +3341,159 @@ snapshots: - node-addon-api - supports-color - '@napi-rs/cross-toolchain@1.0.3(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/cross-toolchain@1.0.3(supports-color@7.2.0)': dependencies: - '@napi-rs/lzma': 1.4.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - '@napi-rs/tar': 1.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - debug: 4.4.3 + '@napi-rs/lzma': 1.5.1 + '@napi-rs/tar': 1.1.1 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - supports-color - '@napi-rs/lzma-android-arm-eabi@1.4.5': + '@napi-rs/lzma-android-arm-eabi@1.5.1': optional: true - '@napi-rs/lzma-android-arm64@1.4.5': + '@napi-rs/lzma-android-arm64@1.5.1': optional: true - '@napi-rs/lzma-darwin-arm64@1.4.5': + '@napi-rs/lzma-darwin-arm64@1.5.1': optional: true - '@napi-rs/lzma-darwin-x64@1.4.5': + '@napi-rs/lzma-darwin-x64@1.5.1': optional: true - '@napi-rs/lzma-freebsd-x64@1.4.5': + '@napi-rs/lzma-freebsd-x64@1.5.1': optional: true - '@napi-rs/lzma-linux-arm-gnueabihf@1.4.5': + '@napi-rs/lzma-linux-arm-gnueabihf@1.5.1': optional: true - '@napi-rs/lzma-linux-arm64-gnu@1.4.5': + '@napi-rs/lzma-linux-arm64-gnu@1.5.1': optional: true - '@napi-rs/lzma-linux-arm64-musl@1.4.5': + '@napi-rs/lzma-linux-arm64-musl@1.5.1': optional: true - '@napi-rs/lzma-linux-ppc64-gnu@1.4.5': + '@napi-rs/lzma-linux-ppc64-gnu@1.5.1': optional: true - '@napi-rs/lzma-linux-riscv64-gnu@1.4.5': + '@napi-rs/lzma-linux-riscv64-gnu@1.5.1': optional: true - '@napi-rs/lzma-linux-s390x-gnu@1.4.5': + '@napi-rs/lzma-linux-s390x-gnu@1.5.1': optional: true - '@napi-rs/lzma-linux-x64-gnu@1.4.5': + '@napi-rs/lzma-linux-x64-gnu@1.5.1': optional: true - '@napi-rs/lzma-linux-x64-musl@1.4.5': + '@napi-rs/lzma-linux-x64-musl@1.5.1': optional: true - '@napi-rs/lzma-wasm32-wasi@1.4.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/lzma-wasm32-wasi@1.5.1': dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' optional: true - '@napi-rs/lzma-win32-arm64-msvc@1.4.5': + '@napi-rs/lzma-win32-arm64-msvc@1.5.1': optional: true - '@napi-rs/lzma-win32-ia32-msvc@1.4.5': + '@napi-rs/lzma-win32-ia32-msvc@1.5.1': optional: true - '@napi-rs/lzma-win32-x64-msvc@1.4.5': + '@napi-rs/lzma-win32-x64-msvc@1.5.1': optional: true - '@napi-rs/lzma@1.4.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/lzma@1.5.1': optionalDependencies: - '@napi-rs/lzma-android-arm-eabi': 1.4.5 - '@napi-rs/lzma-android-arm64': 1.4.5 - '@napi-rs/lzma-darwin-arm64': 1.4.5 - '@napi-rs/lzma-darwin-x64': 1.4.5 - '@napi-rs/lzma-freebsd-x64': 1.4.5 - '@napi-rs/lzma-linux-arm-gnueabihf': 1.4.5 - '@napi-rs/lzma-linux-arm64-gnu': 1.4.5 - '@napi-rs/lzma-linux-arm64-musl': 1.4.5 - '@napi-rs/lzma-linux-ppc64-gnu': 1.4.5 - '@napi-rs/lzma-linux-riscv64-gnu': 1.4.5 - '@napi-rs/lzma-linux-s390x-gnu': 1.4.5 - '@napi-rs/lzma-linux-x64-gnu': 1.4.5 - '@napi-rs/lzma-linux-x64-musl': 1.4.5 - '@napi-rs/lzma-wasm32-wasi': 1.4.5(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - '@napi-rs/lzma-win32-arm64-msvc': 1.4.5 - '@napi-rs/lzma-win32-ia32-msvc': 1.4.5 - '@napi-rs/lzma-win32-x64-msvc': 1.4.5 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' - - '@napi-rs/tar-android-arm-eabi@1.1.0': + '@napi-rs/lzma-android-arm-eabi': 1.5.1 + '@napi-rs/lzma-android-arm64': 1.5.1 + '@napi-rs/lzma-darwin-arm64': 1.5.1 + '@napi-rs/lzma-darwin-x64': 1.5.1 + '@napi-rs/lzma-freebsd-x64': 1.5.1 + '@napi-rs/lzma-linux-arm-gnueabihf': 1.5.1 + '@napi-rs/lzma-linux-arm64-gnu': 1.5.1 + '@napi-rs/lzma-linux-arm64-musl': 1.5.1 + '@napi-rs/lzma-linux-ppc64-gnu': 1.5.1 + '@napi-rs/lzma-linux-riscv64-gnu': 1.5.1 + '@napi-rs/lzma-linux-s390x-gnu': 1.5.1 + '@napi-rs/lzma-linux-x64-gnu': 1.5.1 + '@napi-rs/lzma-linux-x64-musl': 1.5.1 + '@napi-rs/lzma-wasm32-wasi': 1.5.1 + '@napi-rs/lzma-win32-arm64-msvc': 1.5.1 + '@napi-rs/lzma-win32-ia32-msvc': 1.5.1 + '@napi-rs/lzma-win32-x64-msvc': 1.5.1 + + '@napi-rs/tar-android-arm-eabi@1.1.1': optional: true - '@napi-rs/tar-android-arm64@1.1.0': + '@napi-rs/tar-android-arm64@1.1.1': optional: true - '@napi-rs/tar-darwin-arm64@1.1.0': + '@napi-rs/tar-darwin-arm64@1.1.1': optional: true - '@napi-rs/tar-darwin-x64@1.1.0': + '@napi-rs/tar-darwin-x64@1.1.1': optional: true - '@napi-rs/tar-freebsd-x64@1.1.0': + '@napi-rs/tar-freebsd-x64@1.1.1': optional: true - '@napi-rs/tar-linux-arm-gnueabihf@1.1.0': + '@napi-rs/tar-linux-arm-gnueabihf@1.1.1': optional: true - '@napi-rs/tar-linux-arm64-gnu@1.1.0': + '@napi-rs/tar-linux-arm64-gnu@1.1.1': optional: true - '@napi-rs/tar-linux-arm64-musl@1.1.0': + '@napi-rs/tar-linux-arm64-musl@1.1.1': optional: true - '@napi-rs/tar-linux-ppc64-gnu@1.1.0': + '@napi-rs/tar-linux-ppc64-gnu@1.1.1': optional: true - '@napi-rs/tar-linux-s390x-gnu@1.1.0': + '@napi-rs/tar-linux-s390x-gnu@1.1.1': optional: true - '@napi-rs/tar-linux-x64-gnu@1.1.0': + '@napi-rs/tar-linux-x64-gnu@1.1.1': optional: true - '@napi-rs/tar-linux-x64-musl@1.1.0': + '@napi-rs/tar-linux-x64-musl@1.1.1': optional: true - '@napi-rs/tar-wasm32-wasi@1.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/tar-wasm32-wasi@1.1.1': dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' optional: true - '@napi-rs/tar-win32-arm64-msvc@1.1.0': + '@napi-rs/tar-win32-arm64-msvc@1.1.1': optional: true - '@napi-rs/tar-win32-ia32-msvc@1.1.0': + '@napi-rs/tar-win32-ia32-msvc@1.1.1': optional: true - '@napi-rs/tar-win32-x64-msvc@1.1.0': + '@napi-rs/tar-win32-x64-msvc@1.1.1': optional: true - '@napi-rs/tar@1.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + '@napi-rs/tar@1.1.1': optionalDependencies: - '@napi-rs/tar-android-arm-eabi': 1.1.0 - '@napi-rs/tar-android-arm64': 1.1.0 - '@napi-rs/tar-darwin-arm64': 1.1.0 - '@napi-rs/tar-darwin-x64': 1.1.0 - '@napi-rs/tar-freebsd-x64': 1.1.0 - '@napi-rs/tar-linux-arm-gnueabihf': 1.1.0 - '@napi-rs/tar-linux-arm64-gnu': 1.1.0 - '@napi-rs/tar-linux-arm64-musl': 1.1.0 - '@napi-rs/tar-linux-ppc64-gnu': 1.1.0 - '@napi-rs/tar-linux-s390x-gnu': 1.1.0 - '@napi-rs/tar-linux-x64-gnu': 1.1.0 - '@napi-rs/tar-linux-x64-musl': 1.1.0 - '@napi-rs/tar-wasm32-wasi': 1.1.0(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) - '@napi-rs/tar-win32-arm64-msvc': 1.1.0 - '@napi-rs/tar-win32-ia32-msvc': 1.1.0 - '@napi-rs/tar-win32-x64-msvc': 1.1.0 - transitivePeerDependencies: - - '@emnapi/core' - - '@emnapi/runtime' + '@napi-rs/tar-android-arm-eabi': 1.1.1 + '@napi-rs/tar-android-arm64': 1.1.1 + '@napi-rs/tar-darwin-arm64': 1.1.1 + '@napi-rs/tar-darwin-x64': 1.1.1 + '@napi-rs/tar-freebsd-x64': 1.1.1 + '@napi-rs/tar-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/tar-linux-arm64-gnu': 1.1.1 + '@napi-rs/tar-linux-arm64-musl': 1.1.1 + '@napi-rs/tar-linux-ppc64-gnu': 1.1.1 + '@napi-rs/tar-linux-s390x-gnu': 1.1.1 + '@napi-rs/tar-linux-x64-gnu': 1.1.1 + '@napi-rs/tar-linux-x64-musl': 1.1.1 + '@napi-rs/tar-wasm32-wasi': 1.1.1 + '@napi-rs/tar-win32-arm64-msvc': 1.1.1 + '@napi-rs/tar-win32-ia32-msvc': 1.1.1 + '@napi-rs/tar-win32-x64-msvc': 1.1.1 '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': dependencies: @@ -3504,7 +3626,7 @@ snapshots: '@octokit/request-error': 7.1.0 '@octokit/types': 16.0.0 content-type: 2.0.0 - json-with-bigint: 3.5.8 + json-with-bigint: 3.5.10 universal-user-agent: 7.0.3 '@octokit/rest@22.0.1': @@ -3526,35 +3648,35 @@ snapshots: dependencies: '@secretlint/types': 10.2.2 - '@secretlint/config-loader@10.2.2': + '@secretlint/config-loader@10.2.2(supports-color@7.2.0)': dependencies: '@secretlint/profiler': 10.2.2 '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 ajv: 8.20.0 - debug: 4.4.3 - rc-config-loader: 4.1.4 + debug: 4.4.3(supports-color@7.2.0) + rc-config-loader: 4.1.4(supports-color@7.2.0) transitivePeerDependencies: - supports-color - '@secretlint/core@10.2.2': + '@secretlint/core@10.2.2(supports-color@7.2.0)': dependencies: '@secretlint/profiler': 10.2.2 '@secretlint/types': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) structured-source: 4.0.0 transitivePeerDependencies: - supports-color - '@secretlint/formatter@10.2.2': + '@secretlint/formatter@10.2.2(supports-color@7.2.0)': dependencies: '@secretlint/resolver': 10.2.2 '@secretlint/types': 10.2.2 - '@textlint/linter-formatter': 15.7.1 + '@textlint/linter-formatter': 15.7.1(supports-color@7.2.0) '@textlint/module-interop': 15.7.1 '@textlint/types': 15.7.1 chalk: 5.6.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) pluralize: 8.0.0 strip-ansi: 7.2.0 table: 6.9.0 @@ -3562,15 +3684,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@secretlint/node@10.2.2': + '@secretlint/node@10.2.2(supports-color@7.2.0)': dependencies: - '@secretlint/config-loader': 10.2.2 - '@secretlint/core': 10.2.2 - '@secretlint/formatter': 10.2.2 + '@secretlint/config-loader': 10.2.2(supports-color@7.2.0) + '@secretlint/core': 10.2.2(supports-color@7.2.0) + '@secretlint/formatter': 10.2.2(supports-color@7.2.0) '@secretlint/profiler': 10.2.2 '@secretlint/source-creator': 10.2.2 '@secretlint/types': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) p-map: 7.0.5 transitivePeerDependencies: - supports-color @@ -3600,7 +3722,7 @@ snapshots: '@textlint/ast-node-types@15.7.1': {} - '@textlint/linter-formatter@15.7.1': + '@textlint/linter-formatter@15.7.1(supports-color@7.2.0)': dependencies: '@azu/format-text': 1.0.2 '@azu/style-format': 1.0.1 @@ -3608,7 +3730,7 @@ snapshots: '@textlint/resolver': 15.7.1 '@textlint/types': 15.7.1 chalk: 4.1.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) js-yaml: 4.3.0 lodash: 4.18.1 pluralize: 2.0.0 @@ -3651,101 +3773,165 @@ snapshots: '@types/vscode@1.61.0': {} - '@typescript-eslint/eslint-plugin@8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/type-utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 - eslint: 10.6.0 - ignore: 7.0.5 + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/type-utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/visitor-keys': 8.64.0 + eslint: 10.7.0(supports-color@7.2.0) + ignore: 7.0.6 natural-compare: 1.4.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.63.0 - debug: 4.4.3 - eslint: 10.6.0 - typescript: 6.0.3 + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0) + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.63.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 - debug: 4.4.3 - typescript: 6.0.3 + '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.63.0': + '@typescript-eslint/scope-manager@8.64.0': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 - '@typescript-eslint/tsconfig-utils@8.63.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.64.0(@typescript/typescript6@6.0.2)': dependencies: - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' - '@typescript-eslint/type-utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - debug: 4.4.3 - eslint: 10.6.0 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + debug: 4.4.3(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.63.0': {} + '@typescript-eslint/types@8.64.0': {} - '@typescript-eslint/typescript-estree@8.63.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0)': dependencies: - '@typescript-eslint/project-service': 8.63.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.63.0(typescript@6.0.3) - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/visitor-keys': 8.63.0 - debug: 4.4.3 + '@typescript-eslint/project-service': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0) + '@typescript-eslint/tsconfig-utils': 8.64.0(@typescript/typescript6@6.0.2) + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/visitor-keys': 8.64.0 + debug: 4.4.3(supports-color@7.2.0) minimatch: 10.2.5 semver: 7.8.5 tinyglobby: 0.2.17 - ts-api-utils: 2.5.0(typescript@6.0.3) - typescript: 6.0.3 + ts-api-utils: 2.5.0(@typescript/typescript6@6.0.2) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.63.0(eslint@10.6.0)(typescript@6.0.3)': + '@typescript-eslint/utils@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) - '@typescript-eslint/scope-manager': 8.63.0 - '@typescript-eslint/types': 8.63.0 - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - eslint: 10.6.0 - typescript: 6.0.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(supports-color@7.2.0)) + '@typescript-eslint/scope-manager': 8.64.0 + '@typescript-eslint/types': 8.64.0 + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.63.0': + '@typescript-eslint/visitor-keys@8.64.0': dependencies: - '@typescript-eslint/types': 8.63.0 + '@typescript-eslint/types': 8.64.0 eslint-visitor-keys: 5.0.1 - '@typespec/ts-http-runtime@0.3.6': + '@typescript/typescript-aix-ppc64@7.0.2': + optional: true + + '@typescript/typescript-darwin-arm64@7.0.2': + optional: true + + '@typescript/typescript-darwin-x64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-freebsd-x64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm64@7.0.2': + optional: true + + '@typescript/typescript-linux-arm@7.0.2': + optional: true + + '@typescript/typescript-linux-loong64@7.0.2': + optional: true + + '@typescript/typescript-linux-mips64el@7.0.2': + optional: true + + '@typescript/typescript-linux-ppc64@7.0.2': + optional: true + + '@typescript/typescript-linux-riscv64@7.0.2': + optional: true + + '@typescript/typescript-linux-s390x@7.0.2': + optional: true + + '@typescript/typescript-linux-x64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-netbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-arm64@7.0.2': + optional: true + + '@typescript/typescript-openbsd-x64@7.0.2': + optional: true + + '@typescript/typescript-sunos-x64@7.0.2': + optional: true + + '@typescript/typescript-win32-arm64@7.0.2': + optional: true + + '@typescript/typescript-win32-x64@7.0.2': + optional: true + + '@typescript/typescript6@6.0.2': dependencies: - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 + '@typescript/old': typescript@6.0.3 + + '@typespec/ts-http-runtime@0.3.7(supports-color@7.2.0)': + dependencies: + http-proxy-agent: 7.0.2(supports-color@7.2.0) + https-proxy-agent: 7.0.6(supports-color@7.2.0) tslib: 2.8.1 transitivePeerDependencies: - supports-color @@ -3789,10 +3975,10 @@ snapshots: '@vscode/vsce-sign-win32-arm64': 2.0.6 '@vscode/vsce-sign-win32-x64': 2.0.6 - '@vscode/vsce@3.9.2': + '@vscode/vsce@3.9.2(supports-color@7.2.0)': dependencies: - '@azure/identity': 4.13.1 - '@secretlint/node': 10.2.2 + '@azure/identity': 4.13.1(supports-color@7.2.0) + '@secretlint/node': 10.2.2(supports-color@7.2.0) '@secretlint/secretlint-formatter-sarif': 10.2.2 '@secretlint/secretlint-rule-no-dotenv': 10.2.2 '@secretlint/secretlint-rule-preset-recommend': 10.2.2 @@ -3812,7 +3998,7 @@ snapshots: minimatch: 10.2.5 parse-semver: 1.1.1 read: 1.0.7 - secretlint: 10.2.2 + secretlint: 10.2.2(supports-color@7.2.0) semver: 7.8.5 tmp: 0.2.7 typed-rest-client: 1.8.11 @@ -3843,7 +4029,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.3 + fast-uri: 3.1.4 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -4096,13 +4282,17 @@ snapshots: es-errors: 1.3.0 is-data-view: 1.0.2 - debug@3.2.7: + debug@3.2.7(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 - debug@4.4.3: + debug@4.4.3(supports-color@7.2.0): dependencies: ms: 2.1.3 + optionalDependencies: + supports-color: 7.2.0 decompress-response@6.0.0: dependencies: @@ -4325,46 +4515,46 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.8(eslint@10.6.0): + eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@7.2.0)): dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@7.2.0) - eslint-import-resolver-node@0.3.10: + eslint-import-resolver-node@0.3.10(supports-color@7.2.0): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) is-core-module: 2.16.2 resolve: 2.0.0-next.7 transitivePeerDependencies: - supports-color - eslint-module-utils@2.14.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0): + eslint-module-utils@2.14.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) transitivePeerDependencies: - supports-color - eslint-plugin-es@3.0.1(eslint@10.6.0): + eslint-plugin-es@3.0.1(eslint@10.7.0(supports-color@7.2.0)): dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@7.2.0) eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 array.prototype.findlastindex: 1.2.6 array.prototype.flat: 1.3.3 array.prototype.flatmap: 1.3.3 - debug: 3.2.7 + debug: 3.2.7(supports-color@7.2.0) doctrine: 2.1.0 - eslint: 10.6.0 - eslint-import-resolver-node: 0.3.10 - eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint-import-resolver-node@0.3.10)(eslint@10.6.0) + eslint: 10.7.0(supports-color@7.2.0) + eslint-import-resolver-node: 0.3.10(supports-color@7.2.0) + eslint-module-utils: 2.14.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(eslint-import-resolver-node@0.3.10(supports-color@7.2.0))(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) hasown: 2.0.4 is-core-module: 2.16.2 is-glob: 4.0.3 @@ -4376,30 +4566,30 @@ snapshots: string.prototype.trimend: 1.0.10 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack - supports-color - eslint-plugin-node@11.1.0(eslint@10.6.0): + eslint-plugin-node@11.1.0(eslint@10.7.0(supports-color@7.2.0)): dependencies: - eslint: 10.6.0 - eslint-plugin-es: 3.0.1(eslint@10.6.0) + eslint: 10.7.0(supports-color@7.2.0) + eslint-plugin-es: 3.0.1(eslint@10.7.0(supports-color@7.2.0)) eslint-utils: 2.1.0 ignore: 5.3.2 minimatch: 3.1.5 resolve: 1.22.12 semver: 6.3.1 - eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.6.0))(eslint@10.6.0)(prettier@3.9.5): + eslint-plugin-prettier@5.5.6(eslint-config-prettier@10.1.8(eslint@10.7.0(supports-color@7.2.0)))(eslint@10.7.0(supports-color@7.2.0))(prettier@3.9.5): dependencies: - eslint: 10.6.0 + eslint: 10.7.0(supports-color@7.2.0) prettier: 3.9.5 prettier-linter-helpers: 1.0.1 synckit: 0.11.13 optionalDependencies: - eslint-config-prettier: 10.1.8(eslint@10.6.0) + eslint-config-prettier: 10.1.8(eslint@10.7.0(supports-color@7.2.0)) eslint-scope@9.1.2: dependencies: @@ -4418,11 +4608,11 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@10.6.0: + eslint@10.7.0(supports-color@7.2.0): dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@10.6.0) + '@eslint-community/eslint-utils': 4.9.1(eslint@10.7.0(supports-color@7.2.0)) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.23.5 + '@eslint/config-array': 0.23.5(supports-color@7.2.0) '@eslint/config-helpers': 0.6.0 '@eslint/core': 1.2.1 '@eslint/plugin-kit': 0.7.2 @@ -4432,7 +4622,7 @@ snapshots: '@types/estree': 1.0.9 ajv: 6.15.0 cross-spawn: 7.0.6 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) escape-string-regexp: 4.0.0 eslint-scope: 9.1.2 eslint-visitor-keys: 5.0.1 @@ -4496,7 +4686,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.3: {} + fast-uri@3.1.4: {} fast-wrap-ansi@0.2.2: dependencies: @@ -4619,7 +4809,7 @@ snapshots: dependencies: '@sindresorhus/merge-streams': 2.3.0 fast-glob: 3.3.3 - ignore: 7.0.5 + ignore: 7.0.6 path-type: 6.0.0 slash: 5.1.0 unicorn-magic: 0.3.0 @@ -4665,17 +4855,17 @@ snapshots: domutils: 3.2.2 entities: 7.0.1 - http-proxy-agent@7.0.2: + http-proxy-agent@7.0.2(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color - https-proxy-agent@7.0.6: + https-proxy-agent@7.0.6(supports-color@7.2.0): dependencies: agent-base: 7.1.4 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) transitivePeerDependencies: - supports-color @@ -4692,7 +4882,7 @@ snapshots: ignore@5.3.2: {} - ignore@7.0.5: {} + ignore@7.0.6: {} imurmurhash@0.1.4: {} @@ -4862,7 +5052,7 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - json-with-bigint@3.5.8: {} + json-with-bigint@3.5.10: {} json5@1.0.2: dependencies: @@ -5051,7 +5241,7 @@ snapshots: picomatch: 4.0.5 pidtree: 1.0.0 read-package-json-fast: 6.0.0 - shell-quote: 1.9.0 + shell-quote: 1.10.0 which: 7.0.0 nth-check@2.1.1: @@ -5098,7 +5288,7 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.2 - obug@2.1.3: {} + obug@2.1.4: {} once@1.4.0: dependencies: @@ -5230,9 +5420,9 @@ snapshots: queue-microtask@1.2.3: {} - rc-config-loader@4.1.4: + rc-config-loader@4.1.4(supports-color@7.2.0): dependencies: - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) js-yaml: 4.3.0 json5: 2.2.3 require-from-string: 2.0.2 @@ -5344,13 +5534,13 @@ snapshots: sax@1.6.0: {} - secretlint@10.2.2: + secretlint@10.2.2(supports-color@7.2.0): dependencies: '@secretlint/config-creator': 10.2.2 - '@secretlint/formatter': 10.2.2 - '@secretlint/node': 10.2.2 + '@secretlint/formatter': 10.2.2(supports-color@7.2.0) + '@secretlint/node': 10.2.2(supports-color@7.2.0) '@secretlint/profiler': 10.2.2 - debug: 4.4.3 + debug: 4.4.3(supports-color@7.2.0) globby: 14.1.0 read-pkg: 9.0.1 transitivePeerDependencies: @@ -5390,7 +5580,7 @@ snapshots: shebang-regex@3.0.0: {} - shell-quote@1.9.0: {} + shell-quote@1.10.0: {} side-channel-list@1.0.1: dependencies: @@ -5573,9 +5763,9 @@ snapshots: dependencies: is-number: 7.0.0 - ts-api-utils@2.5.0(typescript@6.0.3): + ts-api-utils@2.5.0(@typescript/typescript6@6.0.2): dependencies: - typescript: 6.0.3 + typescript: '@typescript/typescript6@6.0.2' tsconfig-paths@3.15.0: dependencies: @@ -5640,19 +5830,42 @@ snapshots: tunnel: 0.0.6 underscore: 1.13.8 - typescript-eslint@8.63.0(eslint@10.6.0)(typescript@6.0.3): + typescript-eslint@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - '@typescript-eslint/eslint-plugin': 8.63.0(@typescript-eslint/parser@8.63.0(eslint@10.6.0)(typescript@6.0.3))(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/parser': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.63.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.63.0(eslint@10.6.0)(typescript@6.0.3) - eslint: 10.6.0 - typescript: 6.0.3 + '@typescript-eslint/eslint-plugin': 8.64.0(@typescript-eslint/parser@8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0))(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/parser': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + '@typescript-eslint/typescript-estree': 8.64.0(@typescript/typescript6@6.0.2)(supports-color@7.2.0) + '@typescript-eslint/utils': 8.64.0(@typescript/typescript6@6.0.2)(eslint@10.7.0(supports-color@7.2.0))(supports-color@7.2.0) + eslint: 10.7.0(supports-color@7.2.0) + typescript: '@typescript/typescript6@6.0.2' transitivePeerDependencies: - supports-color typescript@6.0.3: {} + typescript@7.0.2: + optionalDependencies: + '@typescript/typescript-aix-ppc64': 7.0.2 + '@typescript/typescript-darwin-arm64': 7.0.2 + '@typescript/typescript-darwin-x64': 7.0.2 + '@typescript/typescript-freebsd-arm64': 7.0.2 + '@typescript/typescript-freebsd-x64': 7.0.2 + '@typescript/typescript-linux-arm': 7.0.2 + '@typescript/typescript-linux-arm64': 7.0.2 + '@typescript/typescript-linux-loong64': 7.0.2 + '@typescript/typescript-linux-mips64el': 7.0.2 + '@typescript/typescript-linux-ppc64': 7.0.2 + '@typescript/typescript-linux-riscv64': 7.0.2 + '@typescript/typescript-linux-s390x': 7.0.2 + '@typescript/typescript-linux-x64': 7.0.2 + '@typescript/typescript-netbsd-arm64': 7.0.2 + '@typescript/typescript-netbsd-x64': 7.0.2 + '@typescript/typescript-openbsd-arm64': 7.0.2 + '@typescript/typescript-openbsd-x64': 7.0.2 + '@typescript/typescript-sunos-x64': 7.0.2 + '@typescript/typescript-win32-arm64': 7.0.2 + '@typescript/typescript-win32-x64': 7.0.2 + uc.micro@2.1.0: {} unbox-primitive@1.1.0: diff --git a/extensions/VSCode/src/capture-policy.test.mjs b/extensions/VSCode/src/capture-policy.test.mjs new file mode 100644 index 00000000..a6127fe4 --- /dev/null +++ b/extensions/VSCode/src/capture-policy.test.mjs @@ -0,0 +1,242 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + appendSerializedCaptureOperation, + captureRefreshStillCurrentSnapshot, + captureStatusFailureClearsIdentity, + captureTokenCanRecord, + captureTokenClearedState, + captureTokenSnapshotStillCurrent, + captureTokenStatusForStatusFailure, + normalizeCaptureServiceBaseUrl, + trustedCaptureServiceBaseUrl, +} from "../.test-output/capture-policy.test.mjs"; + +test("capture operations run serially and recover from rejection", async () => { + const calls = []; + let releaseFirst; + const firstCanFinish = new Promise((resolve) => { + releaseFirst = resolve; + }); + + let queue = appendSerializedCaptureOperation( + Promise.resolve(), + async () => { + calls.push("first-start"); + await firstCanFinish; + calls.push("first-end"); + }, + (error) => calls.push(`unexpected: ${String(error)}`), + ); + queue = appendSerializedCaptureOperation( + queue, + async () => { + calls.push("second"); + throw new Error("expected failure"); + }, + (error) => calls.push(error.message), + ); + queue = appendSerializedCaptureOperation( + queue, + async () => { + calls.push("third"); + }, + (error) => calls.push(`unexpected: ${String(error)}`), + ); + + await Promise.resolve(); + assert.deepEqual(calls, ["first-start"]); + releaseFirst(); + await queue; + assert.deepEqual(calls, [ + "first-start", + "first-end", + "second", + "expected failure", + "third", + ]); +}); + +test("capture service URL normalization strips known routes", () => { + assert.equal( + normalizeCaptureServiceBaseUrl( + "https://capture.example/dev/v1/capture/events/", + ), + "https://capture.example/dev", + ); + assert.equal( + normalizeCaptureServiceBaseUrl( + "http://localhost:8787/v1/capture/status", + ), + "http://localhost:8787", + ); +}); + +test("capture service URL normalization rejects unsafe token destinations", () => { + assert.throws( + () => normalizeCaptureServiceBaseUrl("http://capture.example/dev"), + /https:\/\/ except for localhost/, + ); + assert.throws( + () => normalizeCaptureServiceBaseUrl("http://localhost.evil/dev"), + /https:\/\/ except for localhost/, + ); + assert.throws( + () => normalizeCaptureServiceBaseUrl("postgres://capture.example/dev"), + /https:\/\/ except for localhost/, + ); + assert.throws( + () => + normalizeCaptureServiceBaseUrl("https://user:pass@example.com/dev"), + /must not include credentials/, + ); +}); + +test("offline recording requires a cached capture-enabled token", () => { + assert.equal(captureTokenCanRecord("participant", true, "accepted"), true); + assert.equal( + captureTokenCanRecord("participant", true, "service_unavailable"), + true, + ); + assert.equal( + captureTokenCanRecord("participant", false, "service_unavailable"), + false, + ); + assert.equal( + captureTokenCanRecord("participant", undefined, "service_unavailable"), + false, + ); + assert.equal(captureTokenCanRecord("", true, "accepted"), false); +}); + +test("capture status HTTP failures map to service contract states", () => { + assert.equal(captureTokenStatusForStatusFailure(401), "rejected"); + assert.equal(captureTokenStatusForStatusFailure(403), "capture_disabled"); + assert.equal( + captureTokenStatusForStatusFailure(500), + "service_unavailable", + ); + assert.equal( + captureTokenStatusForStatusFailure(undefined), + "service_unavailable", + ); + assert.equal(captureStatusFailureClearsIdentity(401), true); + assert.equal(captureStatusFailureClearsIdentity(403), true); + assert.equal(captureStatusFailureClearsIdentity(500), false); + assert.equal(captureStatusFailureClearsIdentity(undefined), false); +}); + +test("capture service URL selection ignores workspace values", () => { + assert.equal( + trustedCaptureServiceBaseUrl( + { + globalValue: "https://trusted.example/dev", + workspaceValue: "https://workspace.example/dev", + workspaceFolderValue: "https://folder.example/dev", + }, + "https://default.example/dev", + ), + "https://trusted.example/dev", + ); + assert.equal( + trustedCaptureServiceBaseUrl( + { workspaceValue: "https://workspace.example/dev" }, + "https://default.example/dev", + ), + "https://default.example/dev", + ); +}); + +test("capture state changes invalidate an in-flight refresh", () => { + const inFlightGeneration = 7; + const generationAfterClear = 8; + + assert.equal( + captureRefreshStillCurrentSnapshot( + inFlightGeneration, + generationAfterClear, + "token-hash", + "token-hash", + "https://trusted.example/dev", + "https://trusted.example/dev", + ), + false, + ); + assert.equal( + captureRefreshStillCurrentSnapshot( + generationAfterClear, + generationAfterClear, + undefined, + "token-hash", + "https://trusted.example/dev", + "https://trusted.example/dev", + ), + false, + ); + assert.equal( + captureRefreshStillCurrentSnapshot( + generationAfterClear, + generationAfterClear, + "token-hash", + "token-hash", + "https://other.example/dev", + "https://trusted.example/dev", + ), + false, + ); + assert.deepEqual(captureTokenClearedState(), { + tokenStatus: "missing", + participantId: "", + instanceId: "", + studyId: "", + captureEnabled: false, + lastError: "Capture token is not configured.", + }); +}); + +test("refresh churn does not invalidate a token mutation", () => { + assert.equal( + captureTokenSnapshotStillCurrent( + { mutationGeneration: 3 }, + 3, + 10, + "token-hash", + "token-hash", + "https://trusted.example/dev", + "https://trusted.example/dev", + ), + true, + ); + assert.equal( + captureTokenSnapshotStillCurrent({ mutationGeneration: 3 }, 4, 10), + false, + ); +}); + +test("new refreshes and token mutations invalidate older refreshes", () => { + assert.equal( + captureTokenSnapshotStillCurrent( + { mutationGeneration: 3, refreshGeneration: 7 }, + 3, + 8, + ), + false, + ); + assert.equal( + captureTokenSnapshotStillCurrent( + { mutationGeneration: 3, refreshGeneration: 8 }, + 4, + 8, + ), + false, + ); + assert.equal( + captureTokenSnapshotStillCurrent( + { mutationGeneration: 3, refreshGeneration: 8 }, + 3, + 8, + ), + true, + ); +}); diff --git a/extensions/VSCode/src/capture-policy.ts b/extensions/VSCode/src/capture-policy.ts new file mode 100644 index 00000000..6411ba18 --- /dev/null +++ b/extensions/VSCode/src/capture-policy.ts @@ -0,0 +1,196 @@ +// Copyright (C) 2025 Bryan A. Jones. +// +// This file is part of the CodeChat Editor. + +export type CaptureTokenPolicyStatus = + | "missing" + | "unverified" + | "accepted" + | "rejected" + | "capture_disabled" + | "service_unavailable"; + +export interface CaptureServiceBaseUrlInspection { + globalValue?: unknown; +} + +export interface CaptureTokenClearedState { + tokenStatus: "missing"; + participantId: ""; + instanceId: ""; + studyId: ""; + captureEnabled: false; + lastError: string; +} + +export interface CaptureTokenCurrentnessSnapshot { + mutationGeneration: number; + refreshGeneration?: number; +} + +const CAPTURE_SERVICE_ROUTE_SUFFIXES = [ + "/v1/capture/events", + "/v1/capture/status", + "/v1/health", +]; + +function optionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : undefined; +} + +export function trustedCaptureServiceBaseUrl( + inspected: CaptureServiceBaseUrlInspection | undefined, + defaultBaseUrl: string, +): string { + return optionalString(inspected?.globalValue) ?? defaultBaseUrl; +} + +function isLocalHttpCaptureService(url: URL): boolean { + const hostname = url.hostname.toLowerCase(); + return ( + url.protocol === "http:" && + (hostname === "localhost" || + hostname === "127.0.0.1" || + hostname === "::1" || + hostname === "[::1]") + ); +} + +export function normalizeCaptureServiceBaseUrl(value: string): string { + let rawUrl = value.trim().replace(/\/+$/, ""); + if (rawUrl.length === 0) { + throw new Error("capture service URL must not be empty"); + } + + for (const suffix of CAPTURE_SERVICE_ROUTE_SUFFIXES) { + if (rawUrl.endsWith(suffix)) { + rawUrl = rawUrl.slice(0, -suffix.length).replace(/\/+$/, ""); + break; + } + } + + let url: URL; + try { + url = new URL(rawUrl); + } catch { + throw new Error("capture service URL must be an absolute URL"); + } + + if (url.username.length > 0 || url.password.length > 0) { + throw new Error("capture service URL must not include credentials"); + } + if (url.protocol !== "https:" && !isLocalHttpCaptureService(url)) { + throw new Error( + "capture service URL must use https:// except for localhost", + ); + } + + url.hash = ""; + url.search = ""; + url.pathname = url.pathname.replace(/\/+$/, ""); + return url.toString().replace(/\/+$/, ""); +} + +export function captureTokenCanRecord( + participantId: string, + captureEnabled: boolean | undefined, + tokenStatus: CaptureTokenPolicyStatus, +): boolean { + return ( + participantId.length > 0 && + captureEnabled === true && + (tokenStatus === "accepted" || tokenStatus === "service_unavailable") + ); +} + +export function captureTokenStatusForStatusFailure( + statusCode: number | undefined, +): CaptureTokenPolicyStatus { + switch (statusCode) { + case 401: + return "rejected"; + case 403: + return "capture_disabled"; + default: + return "service_unavailable"; + } +} + +export function captureStatusFailureClearsIdentity( + statusCode: number | undefined, +): boolean { + return statusCode === 401 || statusCode === 403; +} + +export function captureRefreshStillCurrentSnapshot( + refreshGeneration: number, + currentGeneration: number, + storedTokenHash?: string, + expectedTokenHash?: string, + currentBaseUrl?: string, + expectedBaseUrl?: string, +): boolean { + return captureTokenSnapshotStillCurrent( + { + mutationGeneration: refreshGeneration, + refreshGeneration, + }, + currentGeneration, + currentGeneration, + storedTokenHash, + expectedTokenHash, + currentBaseUrl, + expectedBaseUrl, + ); +} + +export function captureTokenSnapshotStillCurrent( + snapshot: CaptureTokenCurrentnessSnapshot, + currentMutationGeneration: number, + currentRefreshGeneration: number, + storedTokenHash?: string, + expectedTokenHash?: string, + currentBaseUrl?: string, + expectedBaseUrl?: string, +): boolean { + if (snapshot.mutationGeneration !== currentMutationGeneration) { + return false; + } + if ( + snapshot.refreshGeneration !== undefined && + snapshot.refreshGeneration !== currentRefreshGeneration + ) { + return false; + } + if ( + expectedTokenHash !== undefined && + storedTokenHash !== expectedTokenHash + ) { + return false; + } + if (expectedBaseUrl !== undefined && currentBaseUrl !== expectedBaseUrl) { + return false; + } + return true; +} + +export function appendSerializedCaptureOperation( + queue: Promise, + operation: () => Promise, + onFailure: (error: unknown) => void, +): Promise { + return queue.then(operation).catch(onFailure); +} + +export function captureTokenClearedState(): CaptureTokenClearedState { + return { + tokenStatus: "missing", + participantId: "", + instanceId: "", + studyId: "", + captureEnabled: false, + lastError: "Capture token is not configured.", + }; +} diff --git a/extensions/VSCode/src/extension.ts b/extensions/VSCode/src/extension.ts index a5461cbd..02fea4f2 100644 --- a/extensions/VSCode/src/extension.ts +++ b/extensions/VSCode/src/extension.ts @@ -3,7 +3,8 @@ // This file is part of the CodeChat Editor. The CodeChat Editor is free // software: you can redistribute it and/or modify it under the terms of the GNU // General Public License as published by the Free Software Foundation, either -// version 3 of the License, or (at your option) any later version. +// version 3 of the License, or (at your option) any later version of the GNU +// General Public License. // // The CodeChat Editor is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or @@ -40,6 +41,8 @@ import { CodeChatEditorServer, initServer } from "./index.js"; // ### Local packages import { auto_update_timeout_ms, + CaptureEventWire, + CaptureStatus, EditorMessage, EditorMessageContents, KeysOfRustEnum, @@ -53,6 +56,22 @@ import { console_log, } from "../../../client/src/debug_enabled.mjs"; import { ResultErrTypes } from "../../../client/src/rust-types/ResultErrTypes.js"; +import { + appendSerializedCaptureOperation, + captureStatusFailureClearsIdentity, + captureTokenCanRecord, + captureTokenClearedState, + CaptureTokenCurrentnessSnapshot, + CaptureTokenPolicyStatus, + captureTokenSnapshotStillCurrent, + captureTokenStatusForStatusFailure, + normalizeCaptureServiceBaseUrl, + trustedCaptureServiceBaseUrl, +} from "./capture-policy.js"; + +import * as crypto from "crypto"; +import * as http from "http"; +import * as https from "https"; // Globals // ------- @@ -61,6 +80,9 @@ enum CodeChatEditorClientLocation { browser, } +// Create a unique session ID for logging +const CAPTURE_SESSION_ID = crypto.randomUUID(); + // True on Windows, false on OS X / Linux. const is_windows = process.platform === "win32"; @@ -112,13 +134,1974 @@ let codeChatEditorServer: CodeChatEditorServer | undefined; initServer(ext.extensionPath); } +// --- +// +// CAPTURE (Dissertation instrumentation) +// -------------------------------------- + +// Capture uses these helpers only for documentation-like files. Source files +// classify directly as code; Markdown/RST get a finer split so prose edits count +// as documentation activity while embedded snippets count as code activity. +function markdownFenceMarker(text: string): "`" | "~" | undefined { + // Markdown fences may be indented up to three spaces. Do not trim, since a + // blockquoted fence (`> ````) should not toggle the outer document state. + const match = /^(?: {0,3})(`{3,}|~{3,})/.exec(text); + if (match === null) { + return undefined; + } + return match[1].startsWith("`") ? "`" : "~"; +} + +function isInMarkdownCodeFence( + doc: vscode.TextDocument, + line: number, +): boolean { + // The fence delimiter itself is Markdown markup, not code content. + if (markdownFenceMarker(doc.lineAt(line).text) !== undefined) { + return false; + } + + let activeFence: "`" | "~" | undefined; + for (let i = 0; i < line; i++) { + const marker = markdownFenceMarker(doc.lineAt(i).text); + if (marker === undefined) { + continue; + } + if (activeFence === undefined) { + activeFence = marker; + } else if (activeFence === marker) { + activeFence = undefined; + } + } + return activeFence !== undefined; +} + +function isInRstCodeBlock(doc: vscode.TextDocument, line: number): boolean { + // Heuristic: find the most recent ".. code-block::" (or "::") and see if + // the current line belongs to its immediately following indented region. + // A later non-indented paragraph closes the region, so don't keep scanning + // past it and accidentally classify later indented prose as code. + const cur = doc.lineAt(line).text; + if (cur.trim().length === 0 || !/^\s+/.test(cur)) { + return false; + } + + for (let i = line - 1; i >= 0; i--) { + const t = doc.lineAt(i).text; + const tt = t.trim(); + if (tt.startsWith(".. code-block::") || tt === "::") { + return true; + } + if (tt.length === 0 || /^\s+/.test(t)) { + continue; + } + return false; + } + return false; +} + +function classifyAtPosition( + doc: vscode.TextDocument, + pos: vscode.Position, +): ActivityKind { + // These helpers are only for documentation-like documents that may embed + // source snippets. Plain source files skip this branch and classify as + // code. + if (DOC_LANG_IDS.has(doc.languageId)) { + if (doc.languageId === "markdown") { + return isInMarkdownCodeFence(doc, pos.line) ? "code" : "doc"; + } + if (doc.languageId === "restructuredtext") { + return isInRstCodeBlock(doc, pos.line) ? "code" : "doc"; + } + // Other doc types: default to doc + return "doc"; + } + return "code"; +} + +// Event-specific payload attached to a capture event. Study metadata such as +// group, course, assignment, and condition is intentionally excluded from the +// student-facing capture settings; analysis can join those values later from a +// researcher-managed participant/date mapping. +type CaptureEventData = Record; + +// Event names are generated from the Rust `CaptureEventType` enum, keeping the +// extension and server in sync without re-declaring the string union here. +type CaptureEventType = CaptureEventWire["event_type"]; + +// Student-facing capture settings. The setup is intentionally small: students +// give local consent, toggle recording, and paste a portal-issued capture +// token. The participant UUID comes from CaptureWebService token status. +interface StudySettings { + // True when the student wants capture enabled for the current work session. + enabled: boolean; + // True after the student has consented to study capture. + consentEnabled: boolean; + // Pseudonymous UUID returned by CaptureWebService token status. + participantId: string; + // True only after CaptureWebService accepts the bearer token and capture is + // allowed for that participant/instance. + tokenAccepted: boolean; + // Non-secret token status used for user-facing capture feedback. + tokenStatus: CaptureTokenUiStatus; +} + +type CaptureTokenUiStatus = CaptureTokenPolicyStatus; + +interface CaptureServiceStatusResponse { + participant_id: string; + instance_id: string; + study_id: string; + capture_enabled: boolean; + participant_status: string; + consent_status: string; + instance_status: string; + token_expires_at?: string | null; + server_time: string; + service_version: string; +} + +interface CaptureTokenRuntimeState { + tokenStatus: CaptureTokenUiStatus; + participantId: string; + instanceId: string; + studyId: string; + captureEnabled?: boolean; + participantStatus?: string; + consentStatus?: string; + instanceStatus?: string; + tokenExpiresAt?: string | null; + serviceVersion?: string; + lastStatusCheckAt?: string; + lastError?: string; +} + +// Derived state for local consent, session recording, and remote token status. +// This is the single source of truth for whether events may be recorded. +type CaptureSettingsState = + | "off" + | "paused" + | "recording" + | "waitingForConsent" + | "waitingForToken" + | "captureDisabled"; + +const CAPTURE_SCHEMA_VERSION = 2; +const CAPTURE_EVENT_SOURCE = "vscode_extension"; +const CAPTURE_SERVICE_DEFAULT_BASE_URL = + "https://9m2nbv2rvc.execute-api.us-east-2.amazonaws.com/dev"; +const CAPTURE_TOKEN_SECRET_KEY = "CodeChatEditor.Capture.Token"; +const CAPTURE_PARTICIPANT_GLOBAL_KEY = + "CodeChatEditor.Capture.ServiceParticipantId"; +const CAPTURE_INSTANCE_GLOBAL_KEY = "CodeChatEditor.Capture.InstanceId"; +const CAPTURE_STUDY_GLOBAL_KEY = "CodeChatEditor.Capture.StudyId"; +const CAPTURE_ENABLED_GLOBAL_KEY = "CodeChatEditor.Capture.CaptureEnabled"; +const CAPTURE_TOKEN_HASH_GLOBAL_KEY = "CodeChatEditor.Capture.TokenHash"; +const CAPTURE_SERVICE_BASE_GLOBAL_KEY = + "CodeChatEditor.Capture.IdentityServiceBaseUrl"; +// Audit label for the user-facing recording toggle. This is intentionally not +// a persisted setting; recording is scoped to the current VS Code window. +const CAPTURE_RECORD_AUDIT_LABEL = "RecordStudyEvents"; +const DEFAULT_REFLECTION_PROMPTS = [ + "What changed in your understanding of this code?", + "What assumption are you making, and how could you test it?", + "What would another developer need to know before maintaining this?", +]; + +// Output channel used for capture diagnostics that should not interrupt normal +// editor use. +let capture_output_channel: vscode.OutputChannel | undefined; +// Extension context provides SecretStorage and global non-secret token metadata. +let extension_context: vscode.ExtensionContext | undefined; +let captureTokenRuntimeState: CaptureTokenRuntimeState = { + tokenStatus: "missing", + participantId: "", + instanceId: "", + studyId: "", +}; +// True after the first failed send is logged to the console, suppressing repeat +// console warnings while still writing detailed failures to the output channel. +let captureFailureLogged = false; +// True once the CodeChat Client and Server have completed enough startup +// handshake work for capture events to be accepted. +let captureTransportReady = false; +// True after a capture-enabled extension session has emitted `session_start`. +let extensionCaptureSessionStarted = false; +// Recording is intentionally scoped to this VS Code extension host session. +// Consent persists in settings, but recording must be +// re-enabled after VS Code restarts and can be toggled independently in each +// open VS Code window. +let sessionRecordStudyEvents = false; +// Monotonic per-extension event sequence number used to order events produced +// by this VS Code session. +let captureSequenceNumber = 0; +// Status bar item that reports capture health and opens the capture controls. +let capture_status_bar_item: vscode.StatusBarItem | undefined; +// Timer used to refresh capture status from the running server. +let capture_status_timer: NodeJS.Timeout | undefined; +// Last capture settings snapshot used to audit user-visible setting changes +// without double-logging when a command and VS Code's configuration event both +// observe the same transition. +let lastCaptureSettings: StudySettings | undefined; +// Token mutations are authoritative. They cancel refresh/status work, but +// refreshes must never cancel an enter or clear mutation. +let captureTokenMutationGeneration = 0; +let captureTokenRefreshGeneration = 0; +// Serialize token work so SecretStorage, persisted identity, and runtime state +// writes happen in a predictable order. Clear still invalidates immediately so +// it can stop Rust uploads ASAP. +let captureTokenOperationQueue: Promise = Promise.resolve(); + +// Simple classification of what the user is currently doing. `doc` means +// prose/documentation activity, whether in a Markdown/RST document or a +// CodeChat doc block; write events from the server provide the more precise +// doc-block classification when it is available. +type ActivityKind = "doc" | "code" | "other"; + +// Language IDs that we treat as "documentation" for the dissertation metrics. +// You can refine this later if you want. +const DOC_LANG_IDS = new Set([ + "markdown", + "plaintext", + "latex", + "restructuredtext", +]); + +// Track the last activity kind and when a reflective-writing (doc) session +// started. +let lastActivityKind: ActivityKind = "other"; +let docSessionStart: number | null = null; +// Capture events can be generated by synchronous VS Code callbacks. Serialize +// their async sends so activity, save, run, and compile rows stay in causal +// order and rejected sends are always handled. +let captureEventQueue: Promise = Promise.resolve(); + +function optionalString(value: unknown): string | undefined { + return typeof value === "string" && value.trim().length > 0 + ? value.trim() + : undefined; +} + +function captureServiceBaseUrl(): string { + const inspected = vscode.workspace + .getConfiguration("CodeChatEditor.Capture") + .inspect("ServiceBaseUrl"); + // This endpoint receives bearer tokens. Honor only application/user-level + // settings so a repository cannot redirect a user's stored token. + return trustedCaptureServiceBaseUrl( + inspected, + CAPTURE_SERVICE_DEFAULT_BASE_URL, + ); +} + +function captureTokenStatusLabel(status: CaptureTokenUiStatus): string { + switch (status) { + case "missing": + return "Missing"; + case "unverified": + return "Stored, not verified"; + case "accepted": + return "Accepted"; + case "rejected": + return "Rejected"; + case "capture_disabled": + return "Capture disabled"; + case "service_unavailable": + return "Service unavailable"; + } +} + +function loadStudySettings(): StudySettings { + const config = vscode.workspace.getConfiguration("CodeChatEditor.Capture"); + const participantId = captureTokenRuntimeState.participantId; + const tokenAccepted = captureTokenCanRecord( + participantId, + captureTokenRuntimeState.captureEnabled, + captureTokenRuntimeState.tokenStatus, + ); + return { + // Recording is session-local so capture starts paused in every VS Code + // window/restart. Consent persists in settings; token identity comes + // from CaptureWebService status. + enabled: sessionRecordStudyEvents, + consentEnabled: config.get("ConsentEnabled", false), + participantId, + tokenAccepted, + tokenStatus: captureTokenRuntimeState.tokenStatus, + }; +} + +// Convert raw settings into the explicit four-row state table. Keeping this as +// a separate helper prevents callers from inventing their own partial rules. +function captureSettingsState(settings: StudySettings): CaptureSettingsState { + if ( + settings.tokenStatus === "capture_disabled" && + settings.consentEnabled && + settings.enabled + ) { + return "captureDisabled"; + } + if ( + settings.consentEnabled && + settings.enabled && + !settings.tokenAccepted + ) { + return "waitingForToken"; + } + if (settings.consentEnabled && settings.enabled) { + return "recording"; + } + if (settings.consentEnabled) { + return "paused"; + } + if (settings.enabled) { + return "waitingForConsent"; + } + return "off"; +} + +// Compare complete settings snapshots so command-triggered changes and VS Code +// configuration notifications do not emit duplicate audit rows. +function captureSettingsEqual(a: StudySettings, b: StudySettings): boolean { + return ( + a.enabled === b.enabled && + a.consentEnabled === b.consentEnabled && + a.participantId === b.participantId && + a.tokenAccepted === b.tokenAccepted && + a.tokenStatus === b.tokenStatus + ); +} + +// Human-readable labels used in status-bar tooltips and QuickPick details. +function captureStateDescription(state: CaptureSettingsState): string { + switch (state) { + case "recording": + return "Capture records study events."; + case "paused": + return "Consent is retained, but recording is paused."; + case "waitingForConsent": + return "Capture waits for consent before recording."; + case "waitingForToken": + return "Capture waits for a valid portal token before recording."; + case "captureDisabled": + return "Capture is disabled by the portal or capture service."; + case "off": + return "Capture is off."; + } +} + +// Build the status bar text and tooltip from the same state table used for +// gating events. This keeps UI feedback and recording behavior aligned. +function captureSettingsStatus(settings: StudySettings): { + label: string; + tooltip: string; + state: CaptureSettingsState; +} { + const state = captureSettingsState(settings); + let label: string; + switch (state) { + case "recording": + label = "Capture: Recording"; + break; + case "paused": + label = "Capture: Paused"; + break; + case "waitingForConsent": + label = "Capture: Waiting for consent"; + break; + case "waitingForToken": + label = "Capture: Waiting for token"; + break; + case "captureDisabled": + label = "Capture: Disabled by portal"; + break; + case "off": + label = "Capture: Off"; + break; + } + + return { + label, + state, + tooltip: [ + `Consent Enabled: ${settings.consentEnabled ? "On" : "Off"}`, + `Record Study Events: ${settings.enabled ? "On" : "Off"}`, + `Token: ${captureTokenStatusLabel(settings.tokenStatus)}`, + settings.participantId + ? `Participant ID: ${settings.participantId}` + : "Participant ID: unavailable", + captureTokenRuntimeState.instanceId + ? `Instance ID: ${captureTokenRuntimeState.instanceId}` + : "", + captureTokenRuntimeState.lastError + ? `Capture Service: ${captureTokenRuntimeState.lastError}` + : "", + `State: ${captureStateDescription(state)}`, + ] + .filter((line) => line.length > 0) + .join("\n"), + }; +} + +// Normal capture events are allowed only in the `recording` row. Audit and +// control events can bypass this through explicit send options. +function captureDisabledReason(settings: StudySettings): string | undefined { + const state = captureSettingsState(settings); + if (state !== "recording") { + return captureStateDescription(state); + } + return undefined; +} + +// Listener fast path. `sendCaptureEvent` deliberately checks the settings +// again after queued work starts because recording may stop while it waits. +function captureIsRecording(): boolean { + return captureDisabledReason(loadStudySettings()) === undefined; +} + +async function updateCaptureSetting( + name: string, + value: string | boolean, +): Promise { + const config = vscode.workspace.getConfiguration("CodeChatEditor.Capture"); + await config.update(name, value, vscode.ConfigurationTarget.Global); +} + +function getExtensionContext(): vscode.ExtensionContext { + if (extension_context === undefined) { + throw new Error("CodeChat extension context is not initialized."); + } + return extension_context; +} + +async function getStoredCaptureToken(): Promise { + return optionalString( + await getExtensionContext().secrets.get(CAPTURE_TOKEN_SECRET_KEY), + ); +} + +async function storeCaptureToken(token: string): Promise { + await getExtensionContext().secrets.store(CAPTURE_TOKEN_SECRET_KEY, token); +} + +async function deleteStoredCaptureToken(): Promise { + await getExtensionContext().secrets.delete(CAPTURE_TOKEN_SECRET_KEY); +} + +function loadPersistedCaptureIdentity(context: vscode.ExtensionContext): void { + const storedBaseUrl = optionalString( + context.globalState.get(CAPTURE_SERVICE_BASE_GLOBAL_KEY), + ); + let currentBaseUrl: string | undefined; + try { + currentBaseUrl = normalizeCaptureServiceBaseUrl( + captureServiceBaseUrl(), + ); + } catch { + currentBaseUrl = undefined; + } + if (storedBaseUrl === undefined || storedBaseUrl !== currentBaseUrl) { + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + participantId: "", + instanceId: "", + studyId: "", + captureEnabled: false, + }; + return; + } + const captureEnabled = context.globalState.get( + CAPTURE_ENABLED_GLOBAL_KEY, + ); + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + participantId: + optionalString( + context.globalState.get(CAPTURE_PARTICIPANT_GLOBAL_KEY), + ) ?? "", + instanceId: + optionalString( + context.globalState.get(CAPTURE_INSTANCE_GLOBAL_KEY), + ) ?? "", + studyId: + optionalString(context.globalState.get(CAPTURE_STUDY_GLOBAL_KEY)) ?? + "", + captureEnabled: + typeof captureEnabled === "boolean" ? captureEnabled : undefined, + }; +} + +async function persistCaptureIdentity( + status: CaptureServiceStatusResponse, + tokenHash: string, + serviceBaseUrl: string, +): Promise { + const context = getExtensionContext(); + await context.globalState.update( + CAPTURE_PARTICIPANT_GLOBAL_KEY, + status.participant_id, + ); + await context.globalState.update( + CAPTURE_INSTANCE_GLOBAL_KEY, + status.instance_id, + ); + await context.globalState.update(CAPTURE_STUDY_GLOBAL_KEY, status.study_id); + await context.globalState.update( + CAPTURE_ENABLED_GLOBAL_KEY, + status.capture_enabled, + ); + await context.globalState.update(CAPTURE_TOKEN_HASH_GLOBAL_KEY, tokenHash); + await context.globalState.update( + CAPTURE_SERVICE_BASE_GLOBAL_KEY, + serviceBaseUrl, + ); +} + +async function clearPersistedCaptureIdentity(): Promise { + const context = getExtensionContext(); + await context.globalState.update(CAPTURE_PARTICIPANT_GLOBAL_KEY, undefined); + await context.globalState.update(CAPTURE_INSTANCE_GLOBAL_KEY, undefined); + await context.globalState.update(CAPTURE_STUDY_GLOBAL_KEY, undefined); + await context.globalState.update(CAPTURE_ENABLED_GLOBAL_KEY, undefined); + await context.globalState.update(CAPTURE_TOKEN_HASH_GLOBAL_KEY, undefined); + await context.globalState.update( + CAPTURE_SERVICE_BASE_GLOBAL_KEY, + undefined, + ); +} + +function hashText(value: string): string { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +function buildFileFields( + filePath: string | undefined, +): Pick { + if (filePath === undefined) { + return { + language_id: vscode.window.activeTextEditor?.document.languageId, + }; + } + const document = get_document(filePath); + return { + // Send the path only to the local Rust server so it can apply the same + // privacy-preserving hash rule used by server-generated capture events. + file_path: filePath, + language_id: document?.languageId, + }; +} + +function captureLog(message: string): void { + capture_output_channel?.appendLine( + `${new Date().toISOString()} ${message}`, + ); +} + +function capturePayloadSummary(payload: CaptureEventWire): string { + return [ + `type=${payload.event_type}`, + `event_id=${payload.event_id}`, + `sequence=${payload.sequence_number?.toString()}`, + `schema=${payload.schema_version}`, + `user_id=${payload.user_id}`, + `session_id=${payload.session_id}`, + `source=${payload.event_source}`, + `language=${payload.language_id ?? ""}`, + payload.file_path ? "file_path=present" : "", + payload.file_hash ? `file_hash=${payload.file_hash}` : "", + ] + .filter((part) => part.length > 0) + .join(" "); +} + +function captureStatusSummary(status: CaptureStatus): string { + return [ + `state=${status.state}`, + `token=${status.token_status}`, + `enabled=${status.enabled}`, + `queued=${status.queued_events}`, + `spooled=${status.spooled_events}`, + `uploaded=${status.uploaded_events}`, + `quarantined=${status.quarantined_events}`, + `failed=${status.failed_events}`, + status.participant_id ? `participant_id=${status.participant_id}` : "", + status.instance_id ? `instance_id=${status.instance_id}` : "", + status.capture_enabled !== null + ? `capture_enabled=${status.capture_enabled}` + : "", + status.service_version + ? `service_version=${status.service_version}` + : "", + status.last_error ? `last_error=${status.last_error}` : "", + status.spool_path ? `spool_path=${status.spool_path}` : "", + ] + .filter((part) => part.length > 0) + .join(" "); +} + +type CaptureStatusJson = Omit< + CaptureStatus, + | "queued_events" + | "spooled_events" + | "uploaded_events" + | "failed_events" + | "quarantined_events" +> & { + queued_events: number; + spooled_events: number; + uploaded_events: number; + failed_events: number; + quarantined_events: number; +}; + +function parseCaptureStatus(json: string): CaptureStatus { + const status = JSON.parse(json) as CaptureStatusJson; + // Rust exports these counters as u64, which ts-rs maps to bigint. JSON + // carries them as numbers, so convert them immediately after parsing to + // keep the runtime value aligned with the generated TypeScript type. + return { + ...status, + queued_events: BigInt(status.queued_events), + spooled_events: BigInt(status.spooled_events), + uploaded_events: BigInt(status.uploaded_events), + failed_events: BigInt(status.failed_events), + quarantined_events: BigInt(status.quarantined_events), + }; +} + +function captureStatusUrl(serviceBaseUrl: string): string { + return `${serviceBaseUrl}/v1/capture/status`; +} + +function leadingHttpStatusCode(message: string): number | undefined { + const match = /^(\d{3})(?:\s|$)/.exec(message); + return match === null ? undefined : Number.parseInt(match[1], 10); +} + +function requestCaptureStatus( + token: string, + serviceBaseUrl: string, +): Promise { + const url = new URL(captureStatusUrl(serviceBaseUrl)); + const client = url.protocol === "http:" ? http : https; + + return new Promise((resolve, reject) => { + const req = client.request( + url, + { + method: "GET", + headers: { + Accept: "application/json", + Authorization: `Bearer ${token}`, + }, + }, + (res) => { + const chunks: Buffer[] = []; + res.on("data", (chunk: Buffer) => chunks.push(chunk)); + res.on("end", () => { + const body = Buffer.concat(chunks).toString("utf8"); + const statusCode = res.statusCode ?? 0; + if (statusCode < 200 || statusCode >= 300) { + reject( + new Error( + `${statusCode} ${ + body || + res.statusMessage || + "request failed" + }`, + ), + ); + return; + } + try { + resolve( + JSON.parse(body) as CaptureServiceStatusResponse, + ); + } catch (err) { + reject( + new Error( + `Unable to parse capture status response: ${ + err instanceof Error + ? err.message + : String(err) + }`, + ), + ); + } + }); + }, + ); + req.setTimeout(10000, () => { + req.destroy(new Error("capture status request timed out")); + }); + req.on("error", reject); + req.end(); + }); +} + +async function applyCaptureServiceStatus( + status: CaptureServiceStatusResponse, + tokenHash: string, + serviceBaseUrl: string, +): Promise { + captureTokenRuntimeState = { + tokenStatus: status.capture_enabled ? "accepted" : "capture_disabled", + participantId: status.participant_id, + instanceId: status.instance_id, + studyId: status.study_id, + captureEnabled: status.capture_enabled, + participantStatus: status.participant_status, + consentStatus: status.consent_status, + instanceStatus: status.instance_status, + tokenExpiresAt: status.token_expires_at, + serviceVersion: status.service_version, + lastStatusCheckAt: new Date().toISOString(), + lastError: status.capture_enabled + ? undefined + : "Capture is disabled by the portal or capture service.", + }; + await persistCaptureIdentity(status, tokenHash, serviceBaseUrl); +} + +async function configureRustCaptureService( + token: string | undefined, + serviceBaseUrl?: string, +): Promise { + if (codeChatEditorServer === undefined) { + return true; + } + try { + if (token === undefined) { + codeChatEditorServer.clearCaptureToken(); + return true; + } + if (serviceBaseUrl === undefined) { + throw new Error("Capture service URL is not configured."); + } + codeChatEditorServer.configureCaptureService(serviceBaseUrl, token); + return true; + } catch (err) { + reportCaptureFailure( + `capture service configuration failed: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + return false; + } +} + +type CaptureServiceConfigurationResult = "configured" | "stale" | "failed"; + +function beginCaptureTokenMutation(): CaptureTokenCurrentnessSnapshot { + captureTokenMutationGeneration += 1; + captureTokenRefreshGeneration += 1; + return { + mutationGeneration: captureTokenMutationGeneration, + }; +} + +function beginCaptureTokenRefresh(): CaptureTokenCurrentnessSnapshot { + captureTokenRefreshGeneration += 1; + return { + mutationGeneration: captureTokenMutationGeneration, + refreshGeneration: captureTokenRefreshGeneration, + }; +} + +function captureTokenOperationSnapshotIsCurrent( + snapshot: CaptureTokenCurrentnessSnapshot, +): boolean { + return captureTokenSnapshotStillCurrent( + snapshot, + captureTokenMutationGeneration, + captureTokenRefreshGeneration, + ); +} + +function enqueueCaptureTokenOperation( + operation: () => Promise, +): Promise { + const task = captureTokenOperationQueue + .catch(() => undefined) + .then(operation); + captureTokenOperationQueue = task.catch(() => undefined); + return task; +} + +async function captureTokenOperationStillCurrent( + snapshot: CaptureTokenCurrentnessSnapshot, + expectedTokenHash?: string, + expectedBaseUrl?: string, +): Promise { + if (!captureTokenOperationSnapshotIsCurrent(snapshot)) { + return false; + } + let storedTokenHash: string | undefined; + if (expectedTokenHash !== undefined) { + const token = await getStoredCaptureToken(); + storedTokenHash = token === undefined ? undefined : hashText(token); + } + let currentBaseUrl: string | undefined; + if (expectedBaseUrl !== undefined) { + try { + currentBaseUrl = normalizeCaptureServiceBaseUrl( + captureServiceBaseUrl(), + ); + } catch { + currentBaseUrl = undefined; + } + } + return captureTokenSnapshotStillCurrent( + snapshot, + captureTokenMutationGeneration, + captureTokenRefreshGeneration, + storedTokenHash, + expectedTokenHash, + currentBaseUrl, + expectedBaseUrl, + ); +} + +async function configureRustCaptureServiceIfCurrent( + snapshot: CaptureTokenCurrentnessSnapshot, + token: string | undefined, + serviceBaseUrl?: string, + expectedTokenHash?: string, + expectedBaseUrl?: string, +): Promise { + if ( + !(await captureTokenOperationStillCurrent( + snapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return "stale"; + } + if (!captureTokenOperationSnapshotIsCurrent(snapshot)) { + return "stale"; + } + return (await configureRustCaptureService(token, serviceBaseUrl)) + ? "configured" + : "failed"; +} + +async function refreshCaptureTokenState( + notify: boolean = false, +): Promise { + const operationSnapshot = beginCaptureTokenRefresh(); + await enqueueCaptureTokenOperation(() => + refreshCaptureTokenStateInner(operationSnapshot, notify), + ); +} + +async function refreshCaptureTokenStateInner( + operationSnapshot: CaptureTokenCurrentnessSnapshot, + notify: boolean, +): Promise { + const token = await getStoredCaptureToken(); + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + + if (token === undefined) { + const configurationResult = await configureRustCaptureServiceIfCurrent( + operationSnapshot, + undefined, + ); + if (configurationResult === "stale") { + return; + } + captureTokenRuntimeState = captureTokenClearedState(); + await clearPersistedCaptureIdentity(); + await refreshCaptureStatus(); + return; + } + + let expectedBaseUrl: string; + try { + expectedBaseUrl = normalizeCaptureServiceBaseUrl( + captureServiceBaseUrl(), + ); + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const configurationResult = await configureRustCaptureServiceIfCurrent( + operationSnapshot, + undefined, + ); + if (configurationResult === "stale") { + return; + } + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + tokenStatus: "service_unavailable", + captureEnabled: false, + lastError: message, + }; + captureLog(`capture token status failed: ${message}`); + if (notify) { + vscode.window.showWarningMessage( + `CodeChat capture token could not be verified: ${message}`, + ); + } + await refreshCaptureStatus(); + return; + } + + const expectedTokenHash = hashText(token); + const configurationResult = await configureRustCaptureServiceIfCurrent( + operationSnapshot, + token, + expectedBaseUrl, + expectedTokenHash, + expectedBaseUrl, + ); + if (configurationResult === "stale") { + return; + } + if (configurationResult === "failed") { + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + tokenStatus: "service_unavailable", + captureEnabled: false, + lastError: "Capture service configuration failed.", + }; + await refreshCaptureStatus(); + return; + } + + const context = getExtensionContext(); + const storedTokenHash = optionalString( + context.globalState.get(CAPTURE_TOKEN_HASH_GLOBAL_KEY), + ); + const storedBaseUrl = optionalString( + context.globalState.get(CAPTURE_SERVICE_BASE_GLOBAL_KEY), + ); + if ( + !(await captureTokenOperationStillCurrent( + operationSnapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return; + } + if ( + storedTokenHash !== expectedTokenHash || + storedBaseUrl !== expectedBaseUrl + ) { + await clearPersistedCaptureIdentity(); + if ( + !(await captureTokenOperationStillCurrent( + operationSnapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return; + } + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + participantId: "", + instanceId: "", + studyId: "", + captureEnabled: false, + }; + } + + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + tokenStatus: "unverified", + lastError: undefined, + }; + + try { + const status = await requestCaptureStatus(token, expectedBaseUrl); + if ( + !(await captureTokenOperationStillCurrent( + operationSnapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return; + } + await applyCaptureServiceStatus( + status, + expectedTokenHash, + expectedBaseUrl, + ); + captureLog( + `capture token status: ${captureTokenStatusLabel( + captureTokenRuntimeState.tokenStatus, + )} participant_id=${status.participant_id} instance_id=${status.instance_id}`, + ); + if (notify) { + vscode.window.showInformationMessage( + status.capture_enabled + ? "CodeChat capture token accepted." + : "CodeChat capture is disabled by the portal.", + ); + } + } catch (err) { + const message = err instanceof Error ? err.message : String(err); + const statusCode = leadingHttpStatusCode(message); + if ( + !(await captureTokenOperationStillCurrent( + operationSnapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return; + } + const clearIdentity = captureStatusFailureClearsIdentity(statusCode); + if (clearIdentity) { + await clearPersistedCaptureIdentity(); + if ( + !(await captureTokenOperationStillCurrent( + operationSnapshot, + expectedTokenHash, + expectedBaseUrl, + )) + ) { + return; + } + } + const tokenStatus = captureTokenStatusForStatusFailure(statusCode); + captureTokenRuntimeState = { + ...(clearIdentity + ? { + participantId: "", + instanceId: "", + studyId: "", + captureEnabled: false, + } + : captureTokenRuntimeState), + tokenStatus, + lastError: message, + }; + captureLog(`capture token status failed: ${message}`); + if (notify) { + vscode.window.showWarningMessage( + `CodeChat capture token could not be verified: ${message}`, + ); + } + } + + await refreshCaptureStatus(); +} + +interface CaptureSendOptions { + // Permit audit/control events even when normal capture is paused or waiting + // for consent. + ignoreCaptureSettings?: boolean; + // Update server-side capture state without spooling this event for upload. + controlOnly?: boolean; + // Explicit active flag carried to the server so it can enable/disable + // translation-generated write events. + captureActive?: boolean; + // Audit rows for consent being turned off still need the participant ID + // that existed before the setting changed. + userId?: string; +} + +// Helper to send a capture event to the Rust server. +async function sendCaptureEvent( + eventType: CaptureEventType, + filePath?: string, + data: CaptureEventData = {}, + options: CaptureSendOptions = {}, +): Promise { + const settings = loadStudySettings(); + const disabledReason = captureDisabledReason(settings); + // User activity events stop here unless both consent and recording are on. + if (!options.ignoreCaptureSettings && disabledReason !== undefined) { + captureLog(`capture skipped: ${eventType} (${disabledReason})`); + const status = captureSettingsStatus(settings); + updateCaptureStatusBar(status.label, status.tooltip); + return; + } + // Control-only messages may run after consent is off, so they must not + // generate a fresh participant ID. + const participantId = options.userId + ? options.userId + : options.controlOnly + ? settings.participantId || "capture_control" + : settings.participantId; + if (!options.controlOnly && participantId.length === 0) { + captureLog(`capture skipped: ${eventType} (no verified capture token)`); + updateCaptureStatusBar( + "Capture: Waiting for token", + "Enter and verify a portal-issued capture token before recording.", + ); + return; + } + const fileFields = buildFileFields(filePath); + // The server uses `capture_active` to decide whether it may generate + // classified write_doc/write_code rows from translated edits. + const captureActive = + options.captureActive ?? + (eventType !== "session_end" && + captureSettingsState(settings) === "recording"); + const payload: CaptureEventWire = { + event_id: crypto.randomUUID(), + sequence_number: BigInt(++captureSequenceNumber), + schema_version: CAPTURE_SCHEMA_VERSION, + user_id: participantId, + session_id: CAPTURE_SESSION_ID, + event_source: CAPTURE_EVENT_SOURCE, + ...fileFields, + event_type: eventType, + client_tz_offset_min: new Date().getTimezoneOffset(), + data: { + ...data, + capture_active: captureActive, + // A control-only event updates the server's capture context but is + // intentionally not inserted into capture storage. + ...(options.controlOnly ? { capture_control_only: true } : {}), + }, + }; + + if (codeChatEditorServer === undefined) { + captureLog( + `capture skipped: ${capturePayloadSummary(payload)} (server not running)`, + ); + reportCaptureFailure("CodeChat server is not running"); + return; + } + if (!captureTransportReady) { + captureLog( + `capture skipped before server handshake: ${capturePayloadSummary(payload)}`, + ); + return; + } + + try { + const messageId = await codeChatEditorServer.sendCaptureEvent( + stringifyCapturePayload(payload), + ); + captureFailureLogged = false; + captureLog( + `${options.controlOnly ? "capture control queued" : "capture queued"} message_id=${messageId}: ${capturePayloadSummary(payload)}`, + ); + await refreshCaptureStatus(); + } catch (err) { + reportCaptureFailure(err instanceof Error ? err.message : String(err)); + } +} + +function stringifyCapturePayload(payload: CaptureEventWire): string { + return JSON.stringify(payload, (_key, value) => + typeof value === "bigint" ? Number(value) : value, + ); +} + +function reportCaptureFailure(message: string) { + captureLog(`capture send failed: ${message}`); + updateCaptureStatusBar("Capture: Error", message); + if (captureFailureLogged) { + return; + } + captureFailureLogged = true; + console.warn(`CodeChat capture event was not queued: ${message}`); +} + +function runCaptureTask( + description: string, + operation: () => Promise, +): void { + void operation().catch((err: unknown) => { + reportCaptureFailure( + `${description} failed: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + }); +} + +function updateCaptureStatusBar(text: string, tooltip?: string) { + if (capture_status_bar_item === undefined) { + return; + } + capture_status_bar_item.text = text; + capture_status_bar_item.tooltip = tooltip; + capture_status_bar_item.show(); +} + +async function refreshCaptureStatus(): Promise { + const settings = loadStudySettings(); + const settingsStatus = captureSettingsStatus(settings); + // When the settings are not in the recording row, the settings state is the + // authoritative status regardless of the server upload worker state. + if (settingsStatus.state !== "recording") { + updateCaptureStatusBar(settingsStatus.label, settingsStatus.tooltip); + return; + } + if (codeChatEditorServer === undefined) { + updateCaptureStatusBar( + "Capture: Waiting", + `${settingsStatus.tooltip}\nServer: CodeChat server is not running`, + ); + return; + } + + try { + const status = parseCaptureStatus( + codeChatEditorServer.getCaptureStatus(), + ); + let label: string; + switch (status.state) { + case "remote": + label = "Capture: Remote"; + break; + case "spooling": + label = "Capture: Queued"; + break; + case "uploading": + label = "Capture: Uploading"; + break; + case "starting": + label = "Capture: Starting"; + break; + case "auth_failed": + label = "Capture: Token rejected"; + break; + case "capture_disabled": + label = "Capture: Disabled by portal"; + break; + case "service_unavailable": + label = "Capture: Service unavailable"; + break; + default: + label = "Capture: Off"; + break; + } + updateCaptureStatusBar( + label, + [ + settingsStatus.tooltip, + captureStatusSummary(status).split(" ").join("\n"), + ].join("\n"), + ); + } catch (err) { + updateCaptureStatusBar( + "Capture: Error", + err instanceof Error ? err.message : String(err), + ); + } +} + +// A status-bar QuickPick action. Each item owns the async work needed after the +// student chooses it, keeping the capture UI small and easy to scan. +interface CaptureStatusAction extends vscode.QuickPickItem { + run: () => Promise; +} + +function captureStatusDetails(): string { + const tooltip = capture_status_bar_item?.tooltip; + return typeof tooltip === "string" + ? tooltip + : (tooltip?.value ?? "Capture status unavailable"); +} + +async function setRecordStudyEvents(enabled: boolean): Promise { + // Save the previous settings before updating so the audit event can record + // exactly what changed. + const previousSettings = loadStudySettings(); + sessionRecordStudyEvents = enabled; + await reconcileCaptureSettings( + "manage_capture_record_study_events", + previousSettings, + ); + + const updatedSettings = loadStudySettings(); + if (enabled && captureSettingsState(updatedSettings) === "recording") { + vscode.window.showInformationMessage( + "CodeChat capture is recording study events.", + ); + } else if (enabled) { + vscode.window.showInformationMessage( + captureStateDescription(captureSettingsState(updatedSettings)), + ); + } else { + vscode.window.showInformationMessage( + "CodeChat capture recording is paused.", + ); + } +} + +async function setCaptureConsent(enabled: boolean): Promise { + // Save the previous settings before updating so the audit event can record + // consent transitions, including consent being turned off. + const previousSettings = loadStudySettings(); + + await updateCaptureSetting("ConsentEnabled", enabled); + await reconcileCaptureSettings( + "manage_capture_consent_enabled", + previousSettings, + ); + + const updatedSettings = loadStudySettings(); + if (enabled && captureSettingsState(updatedSettings) === "recording") { + vscode.window.showInformationMessage( + "CodeChat capture consent is recorded and recording is on.", + ); + } else if (enabled) { + vscode.window.showInformationMessage( + "CodeChat capture consent is recorded.", + ); + } else { + vscode.window.showInformationMessage( + "CodeChat capture consent is off.", + ); + } +} + +async function giveConsentAndRecordStudyEvents(): Promise { + // This command intentionally changes both user-facing settings together, + // then lets the common reconcile path emit one combined audit event. + const previousSettings = loadStudySettings(); + + await updateCaptureSetting("ConsentEnabled", true); + sessionRecordStudyEvents = true; + await reconcileCaptureSettings( + "manage_capture_give_consent_and_record", + previousSettings, + ); + vscode.window.showInformationMessage( + "CodeChat capture consent is recorded and recording is on.", + ); +} + +async function sendCaptureSettingsChangedEvent( + previous: StudySettings, + current: StudySettings, + changedBy: string, + filePath?: string, +): Promise { + // Only the consent and recording checkboxes are study-state transitions. + // Other capture settings, such as path hashing, should not create audit + // rows in the dissertation event stream. + const changedSettings: string[] = []; + if (previous.consentEnabled !== current.consentEnabled) { + changedSettings.push("ConsentEnabled"); + } + if (previous.enabled !== current.enabled) { + changedSettings.push(CAPTURE_RECORD_AUDIT_LABEL); + } + if (changedSettings.length === 0) { + return; + } + + // Prefer the current participant ID, but fall back to the previous value so + // turning consent off can still be attributed to the participant who opted + // out. + const participantId = current.participantId || previous.participantId; + if (participantId.length === 0) { + captureLog( + `capture settings change skipped: ${changedSettings.join(",")} (no participant id)`, + ); + return; + } + + const previousState = captureSettingsState(previous); + const currentState = captureSettingsState(current); + // This audit event is deliberately allowed even when capture is no longer + // active, because the transition itself is analytically important. + await sendCaptureEvent( + "capture_settings_changed", + filePath, + { + changed_by: changedBy, + changed_settings: changedSettings, + previous_state: previousState, + new_state: currentState, + previous_consent_enabled: previous.consentEnabled, + new_consent_enabled: current.consentEnabled, + previous_record_study_events: previous.enabled, + new_record_study_events: current.enabled, + capture_active_before: previousState === "recording", + capture_active_after: currentState === "recording", + }, + { + ignoreCaptureSettings: true, + captureActive: currentState === "recording", + userId: participantId, + }, + ); +} + +async function reconcileCaptureSettings( + changedBy: string = "settings_ui", + previousSettings?: StudySettings, +): Promise { + const active = vscode.window.activeTextEditor; + const filePath = active?.document.fileName; + const settings = loadStudySettings(); + // The first reconciliation after activation uses the snapshot captured at + // activation; command callers may also provide the pre-change snapshot. + const previous = lastCaptureSettings ?? previousSettings; + + // Commands update settings and VS Code then fires a configuration event. + // This guard keeps the capture audit trail to one row per actual transition. + if ( + lastCaptureSettings !== undefined && + captureSettingsEqual(lastCaptureSettings, settings) + ) { + await refreshCaptureStatus(); + return; + } + + // Write the audit row before changing the server active flag, so turning + // capture off records the transition but not any later edit events. + if (previous !== undefined) { + await sendCaptureSettingsChangedEvent( + previous, + settings, + changedBy, + filePath, + ); + } + + const updatedSettings = loadStudySettings(); + // Recording starts only when both checkboxes are on. + if (captureSettingsState(updatedSettings) === "recording") { + await startExtensionCaptureSession(filePath); + } else if ( + // If capture was active before this transition, send a control-only stop + // so the Rust translation layer stops emitting write_doc/write_code + // events from stale context. + extensionCaptureSessionStarted || + (previous !== undefined && + captureSettingsState(previous) === "recording") + ) { + await endExtensionCaptureSession(filePath, changedBy, { + controlOnly: true, + }); + } else { + // A stop-control is harmless when a server is present and keeps the + // server context inactive after settings-only transitions. + await sendCaptureStopControl(filePath, changedBy); + } + + // Refresh the dedupe snapshot after any participant ID generation or audit + // send that may have touched settings. + lastCaptureSettings = loadStudySettings(); + await refreshCaptureStatus(); +} + +async function copyParticipantId(): Promise { + const participantId = captureTokenRuntimeState.participantId; + if (participantId.length === 0) { + vscode.window.showWarningMessage( + "Enter and verify a capture token before copying a participant ID.", + ); + return; + } + await vscode.env.clipboard.writeText(participantId); + vscode.window.showInformationMessage( + "CodeChat capture participant ID copied.", + ); +} + +async function enterCaptureToken(): Promise { + const previousSettings = loadStudySettings(); + const token = await vscode.window.showInputBox({ + title: "Enter CodeChat Capture Token", + prompt: "Paste the portal-issued capture token.", + password: true, + ignoreFocusOut: true, + validateInput: (value) => + value.trim().length === 0 ? "Capture token is required." : null, + }); + if (token === undefined) { + return; + } + const operationSnapshot = beginCaptureTokenMutation(); + const tokenText = token.trim(); + let mutationApplied = false; + await enqueueCaptureTokenOperation(async () => { + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + await storeCaptureToken(tokenText); + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + captureTokenRuntimeState = { + ...captureTokenRuntimeState, + tokenStatus: "unverified", + lastError: undefined, + }; + mutationApplied = true; + }); + if ( + mutationApplied && + captureTokenOperationSnapshotIsCurrent(operationSnapshot) + ) { + await refreshCaptureTokenState(true); + } + await reconcileCaptureSettings( + "manage_capture_enter_token", + previousSettings, + ); +} + +async function clearCaptureToken(): Promise { + const operationSnapshot = beginCaptureTokenMutation(); + // Keep deletion, Rust reconfiguration, and runtime-state changes in one + // queue position so later refreshes cannot observe a partially-cleared + // token state. + await enqueueCaptureTokenOperation(async () => { + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + await deleteStoredCaptureToken(); + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + await clearPersistedCaptureIdentity(); + if (!(await captureTokenOperationStillCurrent(operationSnapshot))) { + return; + } + const configurationResult = await configureRustCaptureServiceIfCurrent( + operationSnapshot, + undefined, + ); + if (configurationResult === "stale") { + return; + } + captureTokenRuntimeState = captureTokenClearedState(); + await reconcileCaptureSettings("manage_capture_clear_token"); + vscode.window.showInformationMessage("CodeChat capture token cleared."); + }); +} + +async function validateCaptureToken(): Promise { + const previousSettings = loadStudySettings(); + await refreshCaptureTokenState(true); + await reconcileCaptureSettings( + "manage_capture_validate_token", + previousSettings, + ); +} + +async function showCaptureStatus(): Promise { + await refreshCaptureStatus(); + const settings = loadStudySettings(); + const settingsStatus = captureSettingsStatus(settings); + // The QuickPick exposes the same two independent switches as Settings, plus + // one convenience action that turns both on at once. + const actions: CaptureStatusAction[] = [ + { + label: "Show Current Capture State", + description: captureStateDescription(settingsStatus.state), + detail: settingsStatus.tooltip, + run: async () => { + captureLog(`capture status: ${settingsStatus.tooltip}`); + vscode.window.showInformationMessage(settingsStatus.tooltip); + }, + }, + ]; + + if (!settings.consentEnabled || !settings.enabled) { + actions.push({ + label: "Give Consent and Record Study Events", + description: "Turn both capture settings on.", + run: giveConsentAndRecordStudyEvents, + }); + } + + actions.push({ + label: "Enter Capture Token", + description: `Token: ${captureTokenStatusLabel(settings.tokenStatus)}`, + run: enterCaptureToken, + }); + + if (settings.tokenStatus !== "missing") { + actions.push({ + label: "Validate Capture Token", + description: "Check token status with CaptureWebService.", + run: validateCaptureToken, + }); + } + + actions.push({ + label: settings.consentEnabled ? "Turn Consent Off" : "Turn Consent On", + description: settings.consentEnabled + ? "Stop recording if active; keep the recording setting unchanged." + : "Record participant consent; keep the recording setting unchanged.", + run: () => setCaptureConsent(!settings.consentEnabled), + }); + + actions.push({ + label: settings.enabled + ? "Turn Record Study Events Off" + : "Turn Record Study Events On", + description: settings.enabled + ? "Stop recording; keep consent unchanged." + : "Start recording only if consent is already on.", + run: () => setRecordStudyEvents(!settings.enabled), + }); + + if (settings.tokenStatus !== "missing") { + actions.push({ + label: "Clear Capture Token", + description: "Remove the locally stored portal token.", + run: clearCaptureToken, + }); + } + + actions.push( + { + label: "Copy Participant ID", + description: settings.participantId || "Verify a token first.", + run: copyParticipantId, + }, + { + label: "Show Capture Details", + description: captureStatusDetails().split("\n")[0], + run: async () => { + captureLog(`capture status: ${captureStatusDetails()}`); + vscode.window.showInformationMessage(captureStatusDetails()); + }, + }, + ); + + const selected = await vscode.window.showQuickPick(actions, { + placeHolder: "Manage CodeChat capture", + }); + if (selected !== undefined) { + await selected.run(); + } +} + +async function recordStudyLifecycleEvent( + eventType: CaptureEventType, +): Promise { + if (!captureIsRecording()) { + return; + } + const active = vscode.window.activeTextEditor; + await sendCaptureEvent(eventType, active?.document.fileName, { + command: eventType, + languageId: active?.document.languageId, + }); +} + +function reflectionPromptText(languageId: string, prompt: string): string { + if (languageId === "markdown") { + return `\n\n### Reflection\n\n${prompt}\n\n`; + } + if (languageId === "restructuredtext") { + return `\n\nReflection\n----------\n\n${prompt}\n\n`; + } + if (languageId === "plaintext" || languageId === "latex") { + return `\n${prompt}\n`; + } + const commentPrefix = + languageId === "python" || + languageId === "shellscript" || + languageId === "powershell" || + languageId === "ruby" + ? "#" + : "//"; + return `\n${commentPrefix} Reflection: ${prompt}\n`; +} + +async function insertReflectionPrompt(): Promise { + const editor = vscode.window.activeTextEditor; + if (editor === undefined) { + vscode.window.showInformationMessage("Open a text editor first."); + return; + } + const prompt = await vscode.window.showQuickPick( + DEFAULT_REFLECTION_PROMPTS, + { + placeHolder: "Select a reflection prompt", + }, + ); + if (prompt === undefined) { + return; + } + + await editor.insertSnippet( + new vscode.SnippetString( + reflectionPromptText(editor.document.languageId, prompt), + ), + ); + await sendCaptureEvent( + "reflection_prompt_inserted", + editor.document.fileName, + { + prompt_hash: hashText(prompt), + prompt_length: prompt.length, + languageId: editor.document.languageId, + }, + ); +} + +async function startExtensionCaptureSession(filePath?: string) { + if (extensionCaptureSessionStarted) { + return; + } + if (!captureIsRecording()) { + return; + } + // Mark this before sending so recursive status refreshes do not emit a + // second session_start for the same extension session. + extensionCaptureSessionStarted = true; + await sendCaptureEvent("session_start", filePath, { + mode: "vscode_extension", + }); +} + +async function endExtensionCaptureSession( + filePath: string | undefined, + closedBy: string, + options: { controlOnly?: boolean } = {}, +): Promise { + if (!extensionCaptureSessionStarted) { + return; + } + if (options.controlOnly) { + // Consent/recording changes must stop server-side write classification + // without inserting a synthetic session_end row after the user opted + // out or paused recording. + docSessionStart = null; + await sendCaptureStopControl(filePath, closedBy); + extensionCaptureSessionStarted = false; + return; + } + await closeDocSession(filePath, closedBy); + await sendCaptureEvent("session_end", filePath, { + mode: "vscode_extension", + closed_by: closedBy, + }); + extensionCaptureSessionStarted = false; +} + +async function sendCaptureStopControl( + filePath: string | undefined, + closedBy: string, +): Promise { + if (codeChatEditorServer === undefined || !captureTransportReady) { + return; + } + // This message is sent through the normal capture channel so the server can + // clear its active capture context, but `capture_control_only` prevents it + // from being spooled for upload. + await sendCaptureEvent( + "session_end", + filePath, + { + mode: "vscode_extension", + closed_by: closedBy, + }, + { + ignoreCaptureSettings: true, + controlOnly: true, + captureActive: false, + }, + ); +} + +async function closeDocSession( + filePath: string | undefined, + closedBy: string, +): Promise { + if (docSessionStart === null) { + return; + } + + const durationMs = Date.now() - docSessionStart; + docSessionStart = null; + await sendCaptureEvent("doc_session", filePath, { + duration_ms: durationMs, + duration_seconds: durationMs / 1000.0, + closed_by: closedBy, + }); + await sendCaptureEvent("session_end", filePath, { + mode: "doc", + closed_by: closedBy, + }); +} + +// Update activity state and emit switch/doc-session events. Markdown/RST prose +// and CodeChat doc-block edits are both documentation activity for analysis; +// server-side write events classify CodeChat doc-block edits precisely, while +// this extension-side activity tracker uses the best cursor/file context +// available before translation. +async function noteActivity(kind: ActivityKind, filePath?: string) { + const now = Date.now(); + + // Handle entering / leaving a "doc" session. + if (kind === "doc") { + if (docSessionStart === null) { + // Starting a new reflective-writing session. + docSessionStart = now; + await sendCaptureEvent("session_start", filePath, { + mode: "doc", + }); + } + } else { + if (docSessionStart !== null) { + // Ending a reflective-writing session. + const closedBy = + kind === "code" ? "switch_to_code" : "activity_change"; + const durationMs = now - docSessionStart; + docSessionStart = null; + await sendCaptureEvent("doc_session", filePath, { + duration_ms: durationMs, + duration_seconds: durationMs / 1000.0, + closed_by: closedBy, + }); + await sendCaptureEvent("session_end", filePath, { + mode: "doc", + closed_by: closedBy, + }); + } + } + + // If we switched between doc and code, log a switch\_pane event. + const docOrCode = (k: ActivityKind) => k === "doc" || k === "code"; + if ( + docOrCode(lastActivityKind) && + docOrCode(kind) && + kind !== lastActivityKind + ) { + await sendCaptureEvent("switch_pane", filePath, { + from: lastActivityKind, + to: kind, + }); + } + + lastActivityKind = kind; +} + +function queueCaptureOperation( + description: string, + operation: () => Promise, +): void { + captureEventQueue = appendSerializedCaptureOperation( + captureEventQueue, + operation, + (err: unknown) => { + reportCaptureFailure( + `${description} capture failed: ${ + err instanceof Error ? err.message : String(err) + }`, + ); + }, + ); +} + +function queueActivityCapture(kind: ActivityKind, filePath?: string): void { + queueCaptureOperation("activity", () => noteActivity(kind, filePath)); +} + +function queueCaptureEvent( + eventType: CaptureEventType, + filePath?: string, + data: CaptureEventData = {}, +): void { + queueCaptureOperation(eventType, () => + sendCaptureEvent(eventType, filePath, data), + ); +} + // Activation/deactivation // ----------------------- // // This is invoked when the extension is activated. It either creates a new // CodeChat Editor Server instance or reveals the currently running one. export const activate = (context: vscode.ExtensionContext) => { + extension_context = context; + loadPersistedCaptureIdentity(context); + lastCaptureSettings = loadStudySettings(); + capture_output_channel = + vscode.window.createOutputChannel("CodeChat Capture"); + context.subscriptions.push(capture_output_channel); + capture_status_bar_item = vscode.window.createStatusBarItem( + vscode.StatusBarAlignment.Left, + 100, + ); + capture_status_bar_item.command = "extension.codeChatCaptureStatus"; + context.subscriptions.push(capture_status_bar_item); + capture_status_timer = setInterval(() => { + runCaptureTask("capture status refresh", refreshCaptureStatus); + }, 5000); + context.subscriptions.push({ + dispose: () => { + if (capture_status_timer !== undefined) { + clearInterval(capture_status_timer); + capture_status_timer = undefined; + } + }, + }); + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration((event) => { + if (event.affectsConfiguration("CodeChatEditor.Capture")) { + runCaptureTask("capture settings reconciliation", async () => { + await refreshCaptureTokenState(); + await reconcileCaptureSettings("settings_ui"); + }); + } + }), + ); + runCaptureTask("capture token initialization", refreshCaptureTokenState); + runCaptureTask("capture status initialization", refreshCaptureStatus); + context.subscriptions.push( + vscode.commands.registerCommand( + "extension.codeChatCaptureStatus", + showCaptureStatus, + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureEnterToken", + enterCaptureToken, + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureValidateToken", + validateCaptureToken, + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureClearToken", + clearCaptureToken, + ), + vscode.commands.registerCommand( + "extension.codeChatInsertReflectionPrompt", + insertReflectionPrompt, + ), + // Study lifecycle commands are registered for optional study + // automation/keybindings, but they are not contributed to the Command + // Palette. Normal users should only see status and reflection commands. + vscode.commands.registerCommand( + "extension.codeChatCaptureTaskStart", + () => recordStudyLifecycleEvent("task_start"), + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureTaskSubmit", + () => recordStudyLifecycleEvent("task_submit"), + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureDebugTaskStart", + () => recordStudyLifecycleEvent("debug_task_start"), + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureDebugTaskSubmit", + () => recordStudyLifecycleEvent("debug_task_submit"), + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureHandoffStart", + () => recordStudyLifecycleEvent("handoff_start"), + ), + vscode.commands.registerCommand( + "extension.codeChatCaptureHandoffEnd", + () => recordStudyLifecycleEvent("handoff_end"), + ), vscode.commands.registerCommand( "extension.codeChatEditorDeactivate", deactivate, @@ -149,6 +2132,21 @@ export const activate = (context: vscode.ExtensionContext) => { event.reason }, ${format_struct(event.contentChanges)}.`, ); + + // CAPTURE: update session/switch state. The server + // classifies write_* events after parsing. + if (captureIsRecording()) { + const doc = event.document; + const firstChange = event.contentChanges[0]; + const pos = firstChange.range.start; + const kind = classifyAtPosition(doc, pos); + + const filePath = doc.fileName; + // Update our notion of current activity + doc + // session. + queueActivityCapture(kind, filePath); + } + send_update(true); }), ); @@ -173,24 +2171,124 @@ export const activate = (context: vscode.ExtensionContext) => { ) { return; } + + // CAPTURE: update activity + possible + // switch\_pane/doc\_session. + if (captureIsRecording()) { + const doc = event.document; + const pos = + event.selection?.active ?? + new vscode.Position(0, 0); + const kind = classifyAtPosition(doc, pos); + + const filePath = doc.fileName; + queueActivityCapture(kind, filePath); + } + send_update(true); }), ); context.subscriptions.push( vscode.window.onDidChangeTextEditorSelection( - (_event) => { + (event) => { if (ignore_selection_change) { ignore_selection_change = false; return; } + console_log( "CodeChat Editor extension: sending updated cursor/scroll position.", ); + + // CAPTURE: treat a selection change as "activity" + // in this document. + if (captureIsRecording()) { + const doc = event.textEditor.document; + const pos = + event.selections?.[0]?.active ?? + event.textEditor.selection.active; + const kind = classifyAtPosition(doc, pos); + const filePath = doc.fileName; + queueActivityCapture(kind, filePath); + } + send_update(false); }, ), ); + + // CAPTURE: listen for file saves. + context.subscriptions.push( + vscode.workspace.onDidSaveTextDocument((doc) => { + if (!captureIsRecording()) { + return; + } + queueCaptureEvent("save", doc.fileName, { + reason: "manual_save", + languageId: doc.languageId, + lineCount: doc.lineCount, + }); + }), + ); + + // CAPTURE: start and end of a debug/run session. + context.subscriptions.push( + vscode.debug.onDidStartDebugSession((session) => { + if (!captureIsRecording()) { + return; + } + const active = vscode.window.activeTextEditor; + const filePath = active?.document.fileName; + queueCaptureEvent("run", filePath, { + sessionName: session.name, + sessionType: session.type, + }); + }), + vscode.debug.onDidTerminateDebugSession((session) => { + if (!captureIsRecording()) { + return; + } + const active = vscode.window.activeTextEditor; + const filePath = active?.document.fileName; + queueCaptureEvent("run_end", filePath, { + sessionName: session.name, + sessionType: session.type, + }); + }), + ); + + // CAPTURE: start and end compile/build events via VS Code + // tasks. + context.subscriptions.push( + vscode.tasks.onDidStartTaskProcess((e) => { + if (!captureIsRecording()) { + return; + } + const active = vscode.window.activeTextEditor; + const filePath = active?.document.fileName; + const task = e.execution.task; + queueCaptureEvent("compile", filePath, { + taskName: task.name, + taskSource: task.source, + definition: task.definition, + processId: e.processId, + }); + }), + vscode.tasks.onDidEndTaskProcess((e) => { + if (!captureIsRecording()) { + return; + } + const active = vscode.window.activeTextEditor; + const filePath = active?.document.fileName; + const task = e.execution.task; + queueCaptureEvent("compile_end", filePath, { + taskName: task.name, + taskSource: task.source, + exitCode: e.exitCode, + }); + }), + ); } // Get the CodeChat Client's location from the VSCode @@ -277,7 +2375,17 @@ export const activate = (context: vscode.ExtensionContext) => { // Start the server. console_log("CodeChat Editor extension: starting server."); - codeChatEditorServer = new CodeChatEditorServer(); + codeChatEditorServer = new CodeChatEditorServer( + vscode.Uri.joinPath( + context.globalStorageUri, + "capture-spool", + ).fsPath, + ); + captureFailureLogged = false; + captureTransportReady = false; + extensionCaptureSessionStarted = false; + await refreshCaptureTokenState(); + runCaptureTask("capture status refresh", refreshCaptureStatus); const hosted_in_ide = codechat_client_location === @@ -286,6 +2394,7 @@ export const activate = (context: vscode.ExtensionContext) => { `CodeChat Editor extension: sending message Opened(${hosted_in_ide}).`, ); await codeChatEditorServer.sendMessageOpened(hosted_in_ide); + // For the external browser, we can immediately send the // `CurrentFile` message. For the WebView, we must first wait to // receive the HTML for the WebView (the `ClientHtml` message). @@ -293,6 +2402,11 @@ export const activate = (context: vscode.ExtensionContext) => { codechat_client_location === CodeChatEditorClientLocation.browser ) { + captureTransportReady = true; + const active = vscode.window.activeTextEditor; + await startExtensionCaptureSession( + active?.document.fileName, + ); send_update(false); } @@ -302,6 +2416,7 @@ export const activate = (context: vscode.ExtensionContext) => { console_log("CodeChat Editor extension: queue closed."); break; } + // Parse the data into a message. const { id, message } = JSON.parse( message_raw, @@ -336,16 +2451,19 @@ export const activate = (context: vscode.ExtensionContext) => { } if (current_update.contents !== undefined) { const source = current_update.contents.source; + // This will produce a change event, which we'll // ignore. The change may also produce a // selection change, which should also be // ignored. ignore_text_document_change = true; ignore_selection_change = true; + // Use a workspace edit, since calls to // `TextEditor.edit` must be made to the active // editor only. const wse = new vscode.WorkspaceEdit(); + // Is this plain text, or a diff? if ("Plain" in source) { wse.replace( @@ -362,6 +2480,7 @@ export const activate = (context: vscode.ExtensionContext) => { ); } else { assert("Diff" in source); + // If this diff was not made against the // text we currently have, reject it. if (source.Diff.version !== version) { @@ -381,8 +2500,8 @@ export const activate = (context: vscode.ExtensionContext) => { } const diffs = source.Diff.doc; for (const diff of diffs) { - // Convert from character offsets from the - // beginning of the document to a + // Convert from character offsets from + // the beginning of the document to a // `Position` (line, then offset on that // line) needed by VSCode. const from = doc.positionAt(diff.from); @@ -416,11 +2535,12 @@ export const activate = (context: vscode.ExtensionContext) => { // Update the cursor and scroll position if // provided. const editor = get_text_editor(doc); + const scroll_line = current_update.scroll_position; if (scroll_line !== undefined && editor) { - // Don't set `ignore_scroll_position` here, - // since `revealRange` doesn't change the - // editor's text selection. + // Don't set `ignore_selection_change` here: + // `revealRange` doesn't change the editor's + // text selection. const scroll_position = new vscode.Position( // The VSCode line is zero-based; the // CodeMirror line is one-based. @@ -440,9 +2560,15 @@ export const activate = (context: vscode.ExtensionContext) => { const cursor_position = current_update.cursor_position; - if (cursor_position !== undefined && editor) { - assert("Line" in cursor_position); - const cursor_line = cursor_position.Line; + if ( + cursor_position !== undefined && + typeof cursor_position === "object" && + "Line" in cursor_position && + editor + ) { + const cursor_line = ( + cursor_position as { Line: number } + ).Line; ignore_selection_change = true; const vscode_cursor_position = new vscode.Position( @@ -464,6 +2590,18 @@ export const activate = (context: vscode.ExtensionContext) => { // Instead, depend on the event to always clear // this flag (a source of potential bugs). } + if ( + cursor_position !== undefined && + typeof cursor_position === "object" && + "DomLocation" in cursor_position + ) { + // VS Code can only apply line-based cursor + // locations. DOM locations should be converted + // by the server before reaching the extension. + console_log( + "CodeChat Editor extension: ignoring DOM cursor location in VS Code update.", + ); + } await sendResult(id); break; } @@ -510,19 +2648,13 @@ export const activate = (context: vscode.ExtensionContext) => { .executeCommand( "vscode.open", vscode.Uri.file(current_file), - { - viewColumn: - current_editor?.viewColumn, - }, + { viewColumn: current_editor?.viewColumn }, ) .then( async () => await sendResult(id), async (reason) => await sendResult(id, { - OpenFileFailed: [ - current_file, - reason, - ], + OpenFileFailed: [current_file, reason], }), ); */ @@ -596,6 +2728,11 @@ export const activate = (context: vscode.ExtensionContext) => { assert(webview_panel !== undefined); webview_panel.webview.html = client_html; await sendResult(id); + captureTransportReady = true; + const active = vscode.window.activeTextEditor; + await startExtensionCaptureSession( + active?.document.fileName, + ); // Now that the Client is loaded, send the editor's // current file to the server. send_update(false); @@ -604,9 +2741,7 @@ export const activate = (context: vscode.ExtensionContext) => { default: console.error( - `Unhandled message ${key}(${format_struct( - value, - )}`, + `Unhandled message ${key}(${format_struct(value)}`, ); break; } @@ -619,7 +2754,8 @@ export const activate = (context: vscode.ExtensionContext) => { // On deactivation, close everything down. export const deactivate = async () => { console_log("CodeChat Editor extension: deactivating."); - await stop_client(); + + await stop_client("extension_deactivate"); webview_panel?.dispose(); console_log("CodeChat Editor extension: deactivated."); }; @@ -641,7 +2777,9 @@ const format_struct = (complex_data_structure: any): string => const sendResult = async (id: number, result?: ResultErrTypes) => { assert(codeChatEditorServer); console_log( - `CodeChat Editor extension: sending Result(id = ${id}, ${format_struct(result)}).`, + `CodeChat Editor extension: sending Result(id = ${id}, ${format_struct( + result, + )}).`, ); try { await codeChatEditorServer.sendResult( @@ -702,13 +2840,17 @@ const send_update = (this_is_dirty: boolean) => { const scroll_position = current_editor!.visibleRanges[0].start.line + 1; const file_path = current_editor!.document.fileName; + // Send contents only if necessary. const option_contents: null | [string, number] = is_dirty ? [current_editor!.document.getText(), (version = rand())] : null; is_dirty = false; + console_log( - `CodeChat Editor extension: sending Update(${file_path}, ${cursor_position}, ${scroll_position}, ${format_struct(option_contents)})`, + `CodeChat Editor extension: sending Update(${file_path}, ${cursor_position}, ${scroll_position}, ${format_struct( + option_contents, + )})`, ); await codeChatEditorServer!.sendMessageUpdatePlain( file_path, @@ -723,16 +2865,18 @@ const send_update = (this_is_dirty: boolean) => { // Gracefully shut down the render client if possible. Shut down the client as // well. -const stop_client = async () => { +const stop_client = async (closedBy: string = "client_stopped") => { console_log("CodeChat Editor extension: stopping client."); + const active = vscode.window.activeTextEditor; + await endExtensionCaptureSession(active?.document.fileName, closedBy); if (codeChatEditorServer !== undefined) { console_log("CodeChat Editor extension: stopping server."); await codeChatEditorServer.stopServer(); codeChatEditorServer = undefined; } + captureTransportReady = false; + await refreshCaptureStatus(); - // Shut the timer down after the client is undefined, to ensure it can't be - // started again by a call to `start_render()`. if (idle_timer !== undefined) { clearTimeout(idle_timer); idle_timer = undefined; @@ -749,7 +2893,6 @@ const show_error = (message: string) => { } console.error(`CodeChat Editor extension: ${message}`); if (webview_panel !== undefined) { - // If the panel was displaying other content, reset it for errors. if ( !webview_panel.webview.html.startsWith("

CodeChat Editor

") ) { diff --git a/extensions/VSCode/src/lib.rs b/extensions/VSCode/src/lib.rs index ceec586a..a828aef2 100644 --- a/extensions/VSCode/src/lib.rs +++ b/extensions/VSCode/src/lib.rs @@ -54,8 +54,10 @@ struct CodeChatEditorServer(ide::CodeChatEditorServer); #[allow(dead_code)] impl CodeChatEditorServer { #[napi(constructor)] - pub fn new() -> Result { - Ok(CodeChatEditorServer(ide::CodeChatEditorServer::new()?)) + pub fn new(capture_spool_path: String) -> Result { + Ok(CodeChatEditorServer( + ide::CodeChatEditorServer::new_with_capture_spool(PathBuf::from(capture_spool_path))?, + )) } // This returns an error if the conversion to JSON fails, `None` if the @@ -80,6 +82,48 @@ impl CodeChatEditorServer { self.0.send_message_opened(hosted_in_ide).await } + #[napi] + pub async fn send_capture_event(&self, capture_event_json: String) -> std::io::Result { + let capture_event = serde_json::from_str(&capture_event_json) + .map_err(|err| std::io::Error::other(err.to_string()))?; + self.0.send_capture_event(capture_event).await + } + + #[napi] + pub fn get_capture_status(&self) -> Result { + serde_json::to_string(&self.0.capture_status()) + .map_err(|err| Error::new(Status::GenericFailure, err.to_string())) + } + + #[napi] + pub fn configure_capture_service( + &self, + base_url: String, + token: Option, + ) -> Result<(), Error> { + self.0 + .configure_capture_service(base_url, token) + .map_err(|err| Error::new(Status::GenericFailure, err)) + } + + #[napi] + pub fn clear_capture_token(&self) -> Result<(), Error> { + self.0 + .clear_capture_token() + .map_err(|err| Error::new(Status::GenericFailure, err)) + } + + #[napi] + pub fn check_capture_service_status(&self) -> Result { + serde_json::to_string( + &self + .0 + .check_capture_service_status() + .map_err(|err| Error::new(Status::GenericFailure, err))?, + ) + .map_err(|err| Error::new(Status::GenericFailure, err.to_string())) + } + #[napi] pub async fn send_message_current_file(&self, url: String) -> std::io::Result { self.0.send_message_current_file(url).await diff --git a/server/Cargo.lock b/server/Cargo.lock index c402182f..f3b70eeb 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -73,7 +73,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "rand 0.10.2", - "sha1 0.11.0", + "sha1", "smallvec", "tokio", "tokio-util", @@ -88,7 +88,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -191,7 +191,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2 0.6.4", + "socket2 0.6.5", "time", "tracing", "url", @@ -206,7 +206,7 @@ dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -306,7 +306,7 @@ version = "4.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68b9d3370580a12f4b7a10fdcc18b28942c083ba570e3d954fe59d10951b85a2" dependencies = [ - "cssparser 0.37.0", + "cssparser", "html5ever", "maplit", "url", @@ -373,9 +373,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.103" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" [[package]] name = "arc-swap" @@ -427,13 +427,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.89" +version = "0.1.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -450,9 +450,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.1" +version = "1.17.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4342d8937fc7e5dd9b1c60292261c0670c882a2cd1719cfc11b1af41731e32ad" +checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" dependencies = [ "aws-lc-sys", "zeroize", @@ -460,9 +460,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.42.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d9ceb1da931507a12f4fccea479dccd00da1943e1b4ae72d8e502d707361444" +checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" dependencies = [ "cc", "cmake", @@ -498,9 +498,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bitvec" @@ -514,15 +514,6 @@ dependencies = [ "wyz", ] -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - [[package]] name = "block-buffer" version = "0.12.1" @@ -555,9 +546,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "regex-automata", @@ -595,12 +586,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - [[package]] name = "bytes" version = "1.12.1" @@ -630,9 +615,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.66" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996" +checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" dependencies = [ "find-msvc-tools", "jobserver", @@ -648,9 +633,9 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" [[package]] name = "cfg_aliases" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" @@ -659,7 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.1", ] @@ -672,15 +657,16 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", + "serde", "wasm-bindgen", "windows-link", ] [[package]] name = "clap" -version = "4.6.1" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +checksum = "dd059f9da4f5c36b3787f65d38ccaab1cc315f07b01f89abc8359ee6a8205011" dependencies = [ "clap_builder", "clap_derive", @@ -688,9 +674,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.6.0" +version = "4.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +checksum = "f09628afdcc538b57f3c6341e9c8e9970f18e4a481690a64974d7023bd33548b" dependencies = [ "anstream", "anstyle", @@ -707,7 +693,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -725,12 +711,6 @@ dependencies = [ "cc", ] -[[package]] -name = "cmov" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" - [[package]] name = "cobs" version = "0.3.0" @@ -742,7 +722,7 @@ dependencies = [ [[package]] name = "codechat-editor-server" -version = "0.1.61" +version = "0.2.0" dependencies = [ "actix-files", "actix-http", @@ -790,11 +770,11 @@ dependencies = [ "regex", "serde", "serde_json", + "sha2", "test_utils", "thirtyfour", "thiserror", "tokio", - "tokio-postgres", "tokio-tungstenite", "tracing", "tracing-log", @@ -844,23 +824,9 @@ checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-str" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21077772762a1002bb421c3af42ac1725fa56066bfc53d9a55bb79905df2aaf3" -dependencies = [ - "const-str-proc-macro", -] - -[[package]] -name = "const-str-proc-macro" -version = "0.3.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e1e0fdd2e5d3041e530e1b21158aeeef8b5d0e306bc5c1e3d6cf0930d10e25a" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] +checksum = "18f12cc9948ed9604230cdddc7c86e270f9401ccbe3c2e98a4378c5e7632212f" [[package]] name = "const_format" @@ -934,15 +900,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -986,16 +943,6 @@ version = "0.8.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "typenum", -] - [[package]] name = "crypto-common" version = "0.2.2" @@ -1005,56 +952,36 @@ dependencies = [ "hybrid-array", ] -[[package]] -name = "cssparser" -version = "0.33.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be934d936a0fbed5bcdc01042b770de1398bf79d0e192f49fa7faea0e99281e" -dependencies = [ - "cssparser-macros", - "dtoa-short", - "itoa", - "phf 0.11.3", - "smallvec", -] - [[package]] name = "cssparser" version = "0.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" dependencies = [ + "cssparser-macros", "dtoa-short", "itoa", + "phf 0.13.1", "smallvec", ] [[package]] name = "cssparser-color" -version = "0.1.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "556c099a61d85989d7af52b692e35a8d68a57e7df8c6d07563dc0778b3960c9f" +checksum = "bbaa233e1dcd9c13a5d3e3a8a2c0f5a727bac380398345dbcb31db4597edc86b" dependencies = [ - "cssparser 0.33.0", + "cssparser", ] [[package]] name = "cssparser-macros" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" +checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf" dependencies = [ "quote", - "syn 2.0.118", -] - -[[package]] -name = "ctutils" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" -dependencies = [ - "cmov", + "syn 2.0.119", ] [[package]] @@ -1110,7 +1037,7 @@ dependencies = [ "proc-macro2", "quote", "rustc_version", - "syn 2.0.118", + "syn 2.0.119", "unicode-xid", ] @@ -1132,26 +1059,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "crypto-common 0.1.7", -] - [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer 0.12.1", + "block-buffer", "const-oid", - "crypto-common 0.2.2", - "ctutils", + "crypto-common", ] [[package]] @@ -1183,16 +1099,15 @@ checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "dprint-core" -version = "0.67.4" +version = "0.68.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1d827947704a9495f705d6aeed270fa21a67f825f22902c28f38dc3af7a9ae" +checksum = "7eb1dc2cac6929b352e64ee15d45cecb990f081eebbec99be0444edb01e642a8" dependencies = [ - "anyhow", "bumpalo", "hashbrown 0.15.5", "indexmap", @@ -1213,15 +1128,15 @@ dependencies = [ [[package]] name = "dprint-plugin-markdown" -version = "0.21.1" -source = "git+https://github.com/bjones1/dprint-plugin-markdown.git?branch=all-fixes#edcf25b35e1702f90d0c898a01dbec69d74a775e" +version = "0.22.1" +source = "git+https://github.com/bjones1/dprint-plugin-markdown.git?branch=all-fixes#3e767b74c195fdeb88033db99b9a35cf16d4a248" dependencies = [ - "anyhow", "dprint-core", "dprint-core-macros", "pulldown-cmark 0.11.3", "regex", "serde", + "thiserror", "unicode-width", ] @@ -1295,17 +1210,11 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fallible-iterator" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" - [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "file-id" @@ -1413,9 +1322,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" dependencies = [ "futures-channel", "futures-core", @@ -1428,9 +1337,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" dependencies = [ "futures-core", "futures-sink", @@ -1438,15 +1347,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" +checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" [[package]] name = "futures-executor" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" dependencies = [ "futures-core", "futures-task", @@ -1455,38 +1364,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" [[package]] name = "futures-macro" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "futures-sink" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" [[package]] name = "futures-task" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" +checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" [[package]] name = "futures-util" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-channel", "futures-core", @@ -1499,16 +1408,6 @@ dependencies = [ "slab", ] -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - [[package]] name = "getrandom" version = "0.2.17" @@ -1518,7 +1417,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "wasm-bindgen", ] @@ -1550,9 +1449,9 @@ dependencies = [ [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -1632,15 +1531,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hmac" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" -dependencies = [ - "digest 0.11.3", -] - [[package]] name = "htmd" version = "0.5.4" @@ -1694,9 +1584,9 @@ dependencies = [ [[package]] name = "http-body" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" dependencies = [ "bytes", "http 1.4.2", @@ -1704,9 +1594,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" dependencies = [ "bytes", "futures-core", @@ -1800,7 +1690,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.6.5", "tokio", "tower-service", "tracing", @@ -1935,9 +1825,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7" +checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" dependencies = [ "crossbeam-deque", "globset", @@ -2069,7 +1959,7 @@ dependencies = [ "quote", "rustc_version", "simd_cesu8", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2088,7 +1978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" dependencies = [ "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2182,14 +2072,14 @@ dependencies = [ [[package]] name = "lightningcss" -version = "1.0.0-alpha.71" +version = "1.0.0-alpha.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb6314c2f0590ac93c86099b98bb7ba8abcf759bfd89604ffca906472bb54937" +checksum = "d31b760f96e8fdfe1d0c295e4bf76c503d6f15d2d470d53bd8cd1f7aa8c7d934" dependencies = [ "ahash 0.8.12", "bitflags", "const-str", - "cssparser 0.33.0", + "cssparser", "cssparser-color", "dashmap", "data-encoding", @@ -2290,7 +2180,7 @@ dependencies = [ "log-mdc", "mock_instant", "parking_lot", - "rand 0.9.4", + "rand 0.9.5", "serde", "serde-value", "serde_json", @@ -2352,16 +2242,6 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5" -[[package]] -name = "md-5" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" -dependencies = [ - "cfg-if", - "digest 0.11.3", -] - [[package]] name = "memchr" version = "2.8.3" @@ -2443,13 +2323,13 @@ checksum = "659579df697b372ef9e36f02fcbb41f6d6f157dcec7db9c9618fa0f23cf0fc20" [[package]] name = "mio" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", + "wasi", "windows-sys 0.61.2", ] @@ -2575,15 +2455,6 @@ dependencies = [ "objc2-encode", ] -[[package]] -name = "objc2-core-foundation" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" -dependencies = [ - "bitflags", -] - [[package]] name = "objc2-encode" version = "4.1.0" @@ -2600,15 +2471,6 @@ dependencies = [ "objc2", ] -[[package]] -name = "objc2-system-configuration" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7216bd11cbda54ccabcab84d523dc93b858ec75ecfb3a7d89513fa22464da396" -dependencies = [ - "objc2-core-foundation", -] - [[package]] name = "once_cell" version = "1.21.4" @@ -2662,9 +2524,9 @@ checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "oxc-browserslist" -version = "3.0.9" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373741e2febe6df186995b668f295e46fd402ee12f312ea2fda8b3b6312c0885" +checksum = "50a403a3c6be65be7bc7730d07d959ec6c143e4ff8117da485df78cd5582260a" dependencies = [ "miniz_oxide 0.9.1", "postcard", @@ -2696,7 +2558,7 @@ checksum = "b237422b014f8f8fff75bb9379e697d13f8d57551a22c88bebb39f073c1bf696" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -2738,7 +2600,7 @@ dependencies = [ "phf 0.13.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3023,12 +2885,12 @@ dependencies = [ [[package]] name = "parcel_selectors" -version = "0.28.2" +version = "0.28.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54fd03f1ad26cb6b3ec1b7414fa78a3bd639e7dbb421b1a60513c96ce886a196" +checksum = "d05f71e01edca03d245ab0a9f7ce13a974ceb79baaae8faf2ba0b11de6b90913" dependencies = [ "bitflags", - "cssparser 0.33.0", + "cssparser", "log", "phf 0.11.3", "phf_codegen 0.11.3", @@ -3134,7 +2996,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3152,7 +3014,6 @@ version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" dependencies = [ - "phf_macros 0.11.3", "phf_shared 0.11.3", ] @@ -3205,7 +3066,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" dependencies = [ "phf_shared 0.11.3", - "rand 0.8.6", + "rand 0.8.7", ] [[package]] @@ -3228,19 +3089,6 @@ dependencies = [ "phf_shared 0.14.0", ] -[[package]] -name = "phf_macros" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" -dependencies = [ - "phf_generator 0.11.3", - "phf_shared 0.11.3", - "proc-macro2", - "quote", - "syn 2.0.118", -] - [[package]] name = "phf_macros" version = "0.13.1" @@ -3251,7 +3099,7 @@ dependencies = [ "phf_shared 0.13.1", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3264,7 +3112,7 @@ dependencies = [ "phf_shared 0.14.0", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -3318,36 +3166,6 @@ dependencies = [ "serde", ] -[[package]] -name = "postgres-protocol" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08808e3c483c46e999108051c78334f473d5adb59d78bb80a1268c7e6aa6c514" -dependencies = [ - "base64", - "byteorder", - "bytes", - "fallible-iterator", - "hmac", - "md-5", - "memchr", - "rand 0.10.2", - "sha2", - "stringprep", -] - -[[package]] -name = "postgres-types" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "851ca9db4932932d69f3ea811b1abe63087a0f740a47692619dd40d4899b68be" -dependencies = [ - "bytes", - "chrono", - "fallible-iterator", - "postgres-protocol", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -3420,9 +3238,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] @@ -3489,7 +3307,7 @@ dependencies = [ "quinn-udp", "rustc-hash", "rustls", - "socket2 0.6.4", + "socket2 0.6.5", "thiserror", "tokio", "tracing", @@ -3528,16 +3346,16 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.6.5", "tracing", "windows-sys 0.61.2", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] @@ -3562,18 +3380,18 @@ checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" [[package]] name = "rand" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" dependencies = [ "rand_core 0.6.4", ] [[package]] name = "rand" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3672,9 +3490,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -3684,9 +3502,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -3827,9 +3645,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.41" +version = "0.23.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b92b125634d9b795e7beca796cc790df15a7fb38323bf3196fda83292d06b1f" +checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" dependencies = [ "aws-lc-rs", "once_cell", @@ -3967,9 +3785,9 @@ dependencies = [ [[package]] name = "self_cell" -version = "1.2.2" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" [[package]] name = "semver" @@ -3985,9 +3803,9 @@ checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" [[package]] name = "serde" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" dependencies = [ "serde_core", "serde_derive", @@ -4014,29 +3832,29 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] name = "serde_json" -version = "1.0.150" +version = "1.0.151" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" dependencies = [ "indexmap", "itoa", @@ -4048,13 +3866,13 @@ dependencies = [ [[package]] name = "serde_repr" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -4082,17 +3900,6 @@ dependencies = [ "unsafe-libyaml", ] -[[package]] -name = "sha1" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha1" version = "0.11.0" @@ -4100,8 +3907,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4111,8 +3918,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", - "digest 0.11.3", + "cpufeatures", + "digest", ] [[package]] @@ -4151,15 +3958,15 @@ dependencies = [ [[package]] name = "simd-adler32" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" [[package]] name = "simd_cesu8" -version = "1.1.1" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" dependencies = [ "rustc_version", "simdutf8", @@ -4207,9 +4014,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.6.4" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", "windows-sys 0.61.2", @@ -4261,17 +4068,6 @@ dependencies = [ "regex", ] -[[package]] -name = "stringprep" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" -dependencies = [ - "unicode-bidi", - "unicode-normalization", - "unicode-properties", -] - [[package]] name = "strsim" version = "0.11.1" @@ -4297,9 +4093,20 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", @@ -4323,7 +4130,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4441,27 +4248,27 @@ checksum = "5cf0ffc3ba4368e99597bd6afd83f4ff6febad66d9ae541ab46e697d32285fc0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] name = "thiserror" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.18" +version = "2.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 3.0.2", ] [[package]] @@ -4485,9 +4292,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.53" +version = "0.3.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18dfaaeddcb932337b5e7866ee7d0ce9b76d2fd092997146f187ec09b4558a50" +checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" dependencies = [ "deranged", "num-conv", @@ -4505,9 +4312,9 @@ checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" [[package]] name = "time-macros" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c431b87111666e491a90baa837f914fb45cd5dc3c268591b0220ff5057f2085f" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" dependencies = [ "num-conv", "time-core", @@ -4540,9 +4347,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.3" +version = "1.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" dependencies = [ "bytes", "libc", @@ -4550,46 +4357,20 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2 0.6.4", + "socket2 0.6.5", "tokio-macros", "windows-sys 0.61.2", ] [[package]] name = "tokio-macros" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", -] - -[[package]] -name = "tokio-postgres" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a528f7d280f6d5b9cd149635c8705b0dd049754bc67d81d31fa25169a93809d3" -dependencies = [ - "async-trait", - "byteorder", - "bytes", - "fallible-iterator", - "futures-channel", - "futures-util", - "log", - "parking_lot", - "percent-encoding", - "phf 0.13.1", - "pin-project-lite", - "postgres-protocol", - "postgres-types", - "rand 0.10.2", - "socket2 0.6.4", - "tokio", - "tokio-util", - "whoami", + "syn 2.0.119", ] [[package]] @@ -4616,9 +4397,9 @@ dependencies = [ [[package]] name = "tokio-tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f72a05e828585856dacd553fba484c242c46e391fb0e58917c942ee9202915c" +checksum = "17a073bfed563fa236697a068031408a93cd9522e08abf9933ead3e73411bd71" dependencies = [ "futures-util", "log", @@ -4705,7 +4486,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -4771,23 +4552,23 @@ checksum = "38d90eea51bc7988ef9e674bf80a85ba6804739e535e9cab48e4bb34a8b652aa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "termcolor", ] [[package]] name = "tungstenite" -version = "0.29.0" +version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c01152af293afb9c7c2a57e4b559c5620b421f6d133261c60dd2d0cdb38e6b8" +checksum = "e48ac77174b19c110a50ab2128b24215ac9cb40e0e12e093fb602d175c569d22" dependencies = [ "bytes", "data-encoding", "http 1.4.2", "httparse", "log", - "rand 0.9.4", - "sha1 0.10.7", + "rand 0.10.2", + "sha1", "thiserror", ] @@ -4824,12 +4605,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-bidi" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" - [[package]] name = "unicode-id-start" version = "1.4.0" @@ -4848,21 +4623,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" - [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -4934,9 +4694,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.4" +version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf80a72845275afea99e7f2b434723d3bc7e38470fcd1c7ed39a599c73319a53" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ "js-sys", "wasm-bindgen", @@ -5006,15 +4766,6 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" -[[package]] -name = "wasi" -version = "0.14.7+wasi-0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "883478de20367e224c0090af9cf5f9fa85bed63a95c1abf3afc5c083ebc06e8c" -dependencies = [ - "wasip2", -] - [[package]] name = "wasip2" version = "1.0.4+wasi-0.2.12" @@ -5024,15 +4775,6 @@ dependencies = [ "wit-bindgen", ] -[[package]] -name = "wasite" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66fe902b4a6b8028a753d5424909b764ccf79b7a209eac9bf97e59cda9f71a42" -dependencies = [ - "wasi 0.14.7+wasi-0.2.4", -] - [[package]] name = "wasm-bindgen" version = "0.2.126" @@ -5075,7 +4817,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "wasm-bindgen-shared", ] @@ -5151,26 +4893,13 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d46a5a140e6f7afeccd8eae97eff335163939eac8b929834875168b29b3d267" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" dependencies = [ "rustls-pki-types", ] -[[package]] -name = "whoami" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" -dependencies = [ - "libc", - "libredox", - "objc2-system-configuration", - "wasite", - "web-sys", -] - [[package]] name = "winapi" version = "0.3.9" @@ -5255,7 +4984,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5266,7 +4995,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5534,7 +5263,7 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5555,7 +5284,7 @@ checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5575,7 +5304,7 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", "synstructure", ] @@ -5615,7 +5344,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn 2.0.118", + "syn 2.0.119", ] [[package]] @@ -5640,9 +5369,9 @@ checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" [[package]] name = "zopfli" diff --git a/server/Cargo.toml b/server/Cargo.toml index f0aa59e0..23f29691 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,7 +32,7 @@ license = "GPL-3.0-only" name = "codechat-editor-server" readme = "../README.md" repository = "https://github.com/bjones1/CodeChat_Editor" -version = "0.1.61" +version = "0.2.0" # This library allows other packages to use core CodeChat Editor features. [lib] @@ -52,9 +52,9 @@ actix-ws = "0.4" ammonia = "4.1.2" anyhow = "1.0.100" bytes = { version = "1", features = ["serde"] } -chrono = "0.4" +chrono = { version = "0.4", features = ["serde"] } clap = { version = "4", features = ["derive"] } -dprint-plugin-markdown = { git = "https://github.com/bjones1/dprint-plugin-markdown.git", branch = "all-fixes", version = "0.21.0" } +dprint-plugin-markdown = { git = "https://github.com/bjones1/dprint-plugin-markdown.git", branch = "all-fixes" } dunce = "1.0.5" futures-util = "0.3.29" htmd = { git = "https://github.com/bjones1/htmd.git", branch = "dom-interface", version = "0.5" } @@ -88,10 +88,10 @@ rand = "0.10" regex = "1" serde = { version = "1", features = ["derive"] } serde_json = "1" +sha2 = "0.11" test_utils = { path = "../test_utils" } thiserror = "2.0.12" tokio = { version = "1", features = ["full"] } -tokio-postgres = { version = "0.7", features = ["with-chrono-0_4"] } tracing = "0.1.44" tracing-log = "0.2.0" tracing-subscriber = { version = "0.3", features = ["env-filter"] } @@ -114,7 +114,7 @@ futures = "0.3.31" predicates = "3.1.2" pretty_assertions = "1.4.1" thirtyfour = "0.37" -tokio-tungstenite = "0.29" +tokio-tungstenite = "0.30" # #### Use local packages for development [patch.crates-io] diff --git a/server/log4rs.yml b/server/log4rs.yml index 544068f2..d534ba2a 100644 --- a/server/log4rs.yml +++ b/server/log4rs.yml @@ -40,7 +40,7 @@ loggers: level: warn root: - level: info + level: debug appenders: - console_appender - file_appender \ No newline at end of file diff --git a/server/scripts/capture_events_schema.sql b/server/scripts/capture_events_schema.sql new file mode 100644 index 00000000..9326e7a7 --- /dev/null +++ b/server/scripts/capture_events_schema.sql @@ -0,0 +1,170 @@ +-- Capture Events Schema +-- ===================== +-- +-- CodeChat capture event schema for dissertation analysis. +-- +-- This script updates an existing legacy `events` table to the lean capture +-- schema used for dissertation telemetry. It converts `timestamp` and `data` to +-- analysis-friendly PostgreSQL types and backfills typed telemetry from +-- older JSON payloads where possible. New capture code writes known telemetry +-- metadata to first-class columns and reserves `data` for event-specific +-- details. Study metadata such as course, group, +-- assignment, condition, and task is intentionally omitted: those values are +-- joined during analysis from researcher-managed participant/date mappings. + +BEGIN; + +CREATE TABLE IF NOT EXISTS public.events ( + id BIGSERIAL PRIMARY KEY, + event_id TEXT, + sequence_number BIGINT, + schema_version INTEGER, + user_id TEXT NOT NULL, + session_id TEXT, + event_source TEXT, + language_id TEXT, + file_hash TEXT, + event_type TEXT NOT NULL, + "timestamp" TIMESTAMPTZ NOT NULL DEFAULT now(), + client_tz_offset_min INTEGER, + data JSONB NOT NULL DEFAULT '{}'::jsonb, + inserted_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS event_id TEXT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS sequence_number BIGINT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS schema_version INTEGER; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS session_id TEXT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS event_source TEXT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS language_id TEXT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS file_hash TEXT; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS client_tz_offset_min INTEGER; +ALTER TABLE public.events ADD COLUMN IF NOT EXISTS inserted_at TIMESTAMPTZ NOT NULL DEFAULT now(); + +ALTER TABLE public.events DROP COLUMN IF EXISTS assignment_id; +ALTER TABLE public.events DROP COLUMN IF EXISTS group_id; +ALTER TABLE public.events DROP COLUMN IF EXISTS condition; +ALTER TABLE public.events DROP COLUMN IF EXISTS course_id; +ALTER TABLE public.events DROP COLUMN IF EXISTS task_id; +ALTER TABLE public.events DROP COLUMN IF EXISTS capture_mode; +ALTER TABLE public.events DROP COLUMN IF EXISTS file_path; +ALTER TABLE public.events DROP COLUMN IF EXISTS path_privacy; +ALTER TABLE public.events DROP COLUMN IF EXISTS server_timestamp_ms; +ALTER TABLE public.events DROP COLUMN IF EXISTS client_timestamp_ms; + +DO $$ +DECLARE + current_type TEXT; +BEGIN + SELECT data_type INTO current_type + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'events' + AND column_name = 'timestamp'; + + IF current_type IS DISTINCT FROM 'timestamp with time zone' THEN + ALTER TABLE public.events + ALTER COLUMN "timestamp" TYPE TIMESTAMPTZ + USING COALESCE(NULLIF("timestamp"::text, '')::timestamptz, now()); + END IF; +END $$; + +DO $$ +DECLARE + current_type TEXT; +BEGIN + SELECT data_type INTO current_type + FROM information_schema.columns + WHERE table_schema = 'public' + AND table_name = 'events' + AND column_name = 'data'; + + IF current_type IS DISTINCT FROM 'jsonb' THEN + ALTER TABLE public.events + ALTER COLUMN data TYPE JSONB + USING CASE + WHEN data IS NULL OR btrim(data::text) = '' THEN '{}'::jsonb + ELSE data::jsonb + END; + END IF; +END $$; + +UPDATE public.events +SET data = '{}'::jsonb +WHERE data IS NULL; + +ALTER TABLE public.events ALTER COLUMN data SET DEFAULT '{}'::jsonb; +ALTER TABLE public.events ALTER COLUMN data SET NOT NULL; +ALTER TABLE public.events ALTER COLUMN "timestamp" SET DEFAULT now(); +ALTER TABLE public.events ALTER COLUMN "timestamp" SET NOT NULL; + +UPDATE public.events +SET + event_id = COALESCE(event_id, NULLIF(data->>'event_id', '')), + sequence_number = COALESCE( + sequence_number, + CASE + WHEN data->>'sequence_number' ~ '^-?[0-9]+$' + THEN (data->>'sequence_number')::bigint + END + ), + schema_version = COALESCE( + schema_version, + CASE + WHEN data->>'schema_version' ~ '^-?[0-9]+$' + THEN (data->>'schema_version')::integer + END + ), + session_id = COALESCE(session_id, NULLIF(data->>'session_id', '')), + event_source = COALESCE(event_source, NULLIF(data->>'event_source', '')), + language_id = COALESCE( + language_id, + NULLIF(data->>'language_id', ''), + NULLIF(data->>'languageId', '') + ), + file_hash = COALESCE(file_hash, NULLIF(data->>'file_hash', '')), + client_tz_offset_min = COALESCE( + client_tz_offset_min, + CASE + WHEN data->>'client_tz_offset_min' ~ '^-?[0-9]+$' + THEN (data->>'client_tz_offset_min')::integer + END + ); + +CREATE INDEX IF NOT EXISTS events_timestamp_idx + ON public.events ("timestamp"); + +CREATE INDEX IF NOT EXISTS events_type_timestamp_idx + ON public.events (event_type, "timestamp"); + +CREATE INDEX IF NOT EXISTS events_participant_session_idx + ON public.events (user_id, session_id); + +CREATE INDEX IF NOT EXISTS events_file_hash_idx + ON public.events (file_hash) + WHERE file_hash IS NOT NULL; + +CREATE INDEX IF NOT EXISTS events_event_id_idx + ON public.events (event_id) + WHERE event_id IS NOT NULL; + +CREATE INDEX IF NOT EXISTS events_data_gin_idx + ON public.events USING GIN (data); + +COMMENT ON TABLE public.events IS + 'CodeChat dissertation capture events. Course, group, assignment, condition, and task context are joined during analysis from participant/date mappings.'; +COMMENT ON COLUMN public.events.event_id IS 'Opaque stable per-event ID for correlation and future deduplication; not used for event ordering.'; +COMMENT ON COLUMN public.events.sequence_number IS 'Client-local event order within one VS Code extension session, useful for ordering and detecting gaps.'; +COMMENT ON COLUMN public.events.user_id IS 'Pseudonymous participant UUID authorized by the portal-issued capture token.'; +COMMENT ON COLUMN public.events.session_id IS 'Capture session UUID emitted by the VS Code extension.'; +COMMENT ON COLUMN public.events.file_hash IS 'SHA-256 hash of the local file path; raw local paths are not stored.'; +COMMENT ON COLUMN public.events."timestamp" IS 'Server receive/record timestamp in UTC.'; +COMMENT ON COLUMN public.events.client_tz_offset_min IS 'Client timezone offset in minutes, used with timestamp to derive local time of day without storing location or full timezone name.'; +COMMENT ON COLUMN public.events.data IS 'Event-specific JSON payload. Known telemetry metadata lives in typed columns.'; + +-- CodeChat clients do not connect to PostgreSQL. Public clients submit events +-- only to CaptureWebService with a portal-issued bearer token; any database +-- writer role and credentials must remain server-side inside the web service +-- deployment. + +COMMIT; diff --git a/server/src/capture.rs b/server/src/capture.rs index 3f8f7c15..21e1bb26 100644 --- a/server/src/capture.rs +++ b/server/src/capture.rs @@ -13,227 +13,2308 @@ // You should have received a copy of the GNU General Public License along with // the CodeChat Editor. If not, see // [http://www.gnu.org/licenses](http://www.gnu.org/licenses). -/// # `Capture.rs` -- Capture CodeChat Editor Events -// ## Submodules + +// `capture.rs` -- Capture CodeChat Editor Events +// ============================================================================ // -// ## Imports +// This module provides a durable local FIFO spool and an HTTPS upload worker for +// CaptureWebService. CodeChat never stores PostgreSQL credentials and never +// connects directly to the capture database. The VS Code extension stores the +// portal-issued bearer token in SecretStorage, then passes it to this worker in +// memory so pending capture events can upload through the public web service. + +// Imports +// ------- // -// Standard library -use indoc::indoc; -use std::fs; -use std::io; -use std::path::Path; -use std::sync::Arc; - -// Third-party -use chrono::Local; -use log::{error, info}; +// ### Standard library +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, + process, + sync::atomic::{AtomicU64, Ordering}, + sync::{ + Arc, Mutex, + mpsc::{self, RecvTimeoutError, Sender}, + }, + thread, + time::Duration, +}; + +// ### Third-party +use chrono::{DateTime, Utc}; +use log::{debug, error, info, warn}; use serde::{Deserialize, Serialize}; -use tokio::sync::Mutex; -use tokio_postgres::{Client, NoTls}; +use sha2::{Digest, Sha256}; +use ts_rs::TS; -// Local +static NEXT_CAPTURE_EVENT_ID: AtomicU64 = AtomicU64::new(1); +static NEXT_SPOOL_FILE_ID: AtomicU64 = AtomicU64::new(1); -/* ## The Event Structure: +const DEFAULT_CAPTURE_SCHEMA_VERSION: i32 = 2; +const MAX_CAPTURE_BATCH_EVENTS: usize = 100; +const MAX_CAPTURE_BATCH_BYTES: usize = 524_288; +const INITIAL_RETRY_DELAY_MS: u64 = 1_000; +const MAX_RETRY_DELAY_MS: u64 = 60_000; +const CAPTURE_SERVICE_HTTP_TIMEOUT_SECS: u64 = 10; - The `Event` struct represents an event to be stored in the database. +/// Canonical event types. Keep the serialized strings stable for analysis. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export)] +pub enum CaptureEventType { + /// Edit to documentation/prose. In CodeChat files this means doc blocks; + /// fenced or embedded code content is classified as `WriteCode`. + WriteDoc, + /// Edit to executable source code, including code inside CodeChat blocks. + WriteCode, + /// Editor activity moved between documentation and code contexts. + SwitchPane, + /// Duration summary for a documentation/prose activity interval. + DocSession, + /// File save observed by the editor. + Save, + /// Compile/build task started. + Compile, + /// Debug/run session started. + Run, + /// Capture or activity session started. + SessionStart, + /// Capture or activity session ended. + SessionEnd, + /// Consent or recording settings changed. + CaptureSettingsChanged, + /// Compile/build task ended. + CompileEnd, + /// Debug/run session ended. + RunEnd, + /// Study task started by an external study workflow. + TaskStart, + /// Study task submitted by an external study workflow. + TaskSubmit, + /// Debugging study task started by an external study workflow. + DebugTaskStart, + /// Debugging study task submitted by an external study workflow. + DebugTaskSubmit, + /// Collaboration handoff interval started. + HandoffStart, + /// Collaboration handoff interval ended. + HandoffEnd, + /// A built-in reflection prompt was inserted into the active editor. + ReflectionPromptInserted, +} - Fields: - `user_id`: The ID of the user associated with the event. - - `event_type`: The type of event (e.g., "keystroke", "file_open"). - `data`: - Optional additional data associated with the event. +impl CaptureEventType { + pub const fn as_str(self) -> &'static str { + match self { + Self::WriteDoc => "write_doc", + Self::WriteCode => "write_code", + Self::SwitchPane => "switch_pane", + Self::DocSession => "doc_session", + Self::Save => "save", + Self::Compile => "compile", + Self::Run => "run", + Self::SessionStart => "session_start", + Self::SessionEnd => "session_end", + Self::CaptureSettingsChanged => "capture_settings_changed", + Self::CompileEnd => "compile_end", + Self::RunEnd => "run_end", + Self::TaskStart => "task_start", + Self::TaskSubmit => "task_submit", + Self::DebugTaskStart => "debug_task_start", + Self::DebugTaskSubmit => "debug_task_submit", + Self::HandoffStart => "handoff_start", + Self::HandoffEnd => "handoff_end", + Self::ReflectionPromptInserted => "reflection_prompt_inserted", + } + } +} + +impl std::fmt::Display for CaptureEventType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.as_str()) + } +} + +/// Hash a local file path before it enters capture storage. The hash is stable +/// enough to group edits to the same file while avoiding raw path collection. +pub fn hash_capture_path(path: &str) -> String { + hash_capture_text(path) +} - ### Example +fn hash_capture_token(token: &str) -> String { + hash_capture_text(token) +} - let event = Event { user_id: "user123".to_string(), event_type: - "keystroke".to_string(), data: Some("Pressed key A".to_string()), }; -*/ +fn hash_capture_text(value: &str) -> String { + Sha256::digest(value.as_bytes()) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect() +} -#[derive(Deserialize, Debug)] -pub struct Event { +/// JSON payload received from local clients for capture events. +/// +/// The server supplies the authoritative timestamp and hashes any raw local file +/// path before storage. Study metadata such as course, assignment, group, +/// condition, and task is inferred later from researcher-managed mappings keyed +/// by the pseudonymous `user_id` and event timestamps. +#[derive(Debug, Serialize, Deserialize, PartialEq, TS)] +#[ts(export, optional_fields)] +pub struct CaptureEventWire { + /// Client-generated unique event identifier. Unlike `sequence_number`, this + /// is an opaque stable ID for correlation and possible future deduplication + /// across capture transports or retries. + #[serde(skip_serializing_if = "Option::is_none")] + pub event_id: Option, + /// Event order within one `(session_id, event_source)` stream. + #[serde(skip_serializing_if = "Option::is_none")] + pub sequence_number: Option, + /// Capture payload schema version. + #[serde(skip_serializing_if = "Option::is_none")] + pub schema_version: Option, + /// Pseudonymous participant UUID from CaptureWebService token status. pub user_id: String, - pub event_type: String, - pub data: Option, + /// Logical capture session UUID. + #[serde(skip_serializing_if = "Option::is_none")] + pub session_id: Option, + /// Source of this event, such as the VS Code extension or server translation. + #[serde(skip_serializing_if = "Option::is_none")] + pub event_source: Option, + /// VS Code language identifier for the active file, when known. + #[serde(skip_serializing_if = "Option::is_none")] + pub language_id: Option, + /// Raw local file path from a trusted local client. This value exists only + /// long enough for the Rust server to hash it; it is never spooled or sent. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_path: Option, + /// SHA-256 hash of the local file path. + #[serde(skip_serializing_if = "Option::is_none")] + pub file_hash: Option, + /// Canonical capture event type. + pub event_type: CaptureEventType, + /// Optional client timezone offset in minutes (JS Date().getTimezoneOffset()). + #[serde(skip_serializing_if = "Option::is_none")] + pub client_tz_offset_min: Option, + /// Event-specific data. Do not store source text or raw local paths here. + #[serde(skip_serializing_if = "Option::is_none")] + #[ts(type = "unknown")] + pub data: Option, } -/* - ## The Config Structure: +/// Participant and session metadata remembered from client capture events. +#[derive(Clone, Debug, Default)] +pub(crate) struct CaptureContext { + active: bool, + user_id: Option, + event_source: Option, + session_id: Option, + client_tz_offset_min: Option, + schema_version: Option, + server_sequence_number: i64, +} - The `Config` struct represents the database connection parameters read from - `config.json`. +impl CaptureContext { + /// Refresh server-side capture identity and active/inactive state from an + /// extension capture message. + pub(crate) fn update_from_wire(&mut self, wire: &CaptureEventWire) { + match wire.event_type { + CaptureEventType::SessionStart => self.active = true, + CaptureEventType::SessionEnd => self.active = false, + _ => {} + } + if !wire.user_id.trim().is_empty() { + self.user_id = Some(wire.user_id.clone()); + } + if let Some(event_source) = &wire.event_source { + self.event_source = Some(event_source.clone()); + } + if let Some(session_id) = &wire.session_id { + if self.session_id.as_ref() != Some(session_id) { + self.server_sequence_number = 0; + } + self.session_id = Some(session_id.clone()); + } + if let Some(schema_version) = wire.schema_version { + self.schema_version = Some(schema_version); + } + if let Some(client_tz_offset_min) = wire.client_tz_offset_min { + self.client_tz_offset_min = Some(client_tz_offset_min); + } + if let Some(serde_json::Value::Object(data)) = &wire.data + && let Some(active) = data + .get("capture_active") + .and_then(serde_json::Value::as_bool) + { + self.active = active; + } + } - Fields: - `db_host`: The hostname or IP address of the database server. - - `db_user`: The username for the database connection. - `db_password`: The - password for the database connection. - `db_name`: The name of the database. + pub(crate) fn is_active(&self) -> bool { + self.active + } + + pub(crate) fn capture_event( + &mut self, + event_type: CaptureEventType, + file_path: Option, + data: serde_json::Value, + ) -> Option { + if !self.active { + return None; + } + let mut data = match data { + serde_json::Value::Object(map) => map, + other => { + let mut map = serde_json::Map::new(); + map.insert("value".to_string(), other); + map + } + }; + data.entry("source".to_string()) + .or_insert_with(|| serde_json::json!("server_translation")); + + self.server_sequence_number += 1; + + Some(CaptureEventWire { + event_id: None, + sequence_number: Some(self.server_sequence_number), + schema_version: self.schema_version, + user_id: self.user_id.clone()?, + session_id: self.session_id.clone(), + event_source: Some("server_translation".to_string()), + language_id: None, + file_path, + file_hash: None, + event_type, + client_tz_offset_min: self.client_tz_offset_min, + data: Some(serde_json::Value::Object(data)), + }) + } +} - let config = Config { db_host: "localhost".to_string(), db_user: - "your_db_user".to_string(), db_password: "your_db_password".to_string(), - db_name: "your_db_name".to_string(), }; -*/ +/// True for a capture message that should update `CaptureContext` only. +pub(crate) fn capture_control_only(wire: &CaptureEventWire) -> bool { + matches!( + &wire.data, + Some(serde_json::Value::Object(data)) + if data + .get("capture_control_only") + .and_then(serde_json::Value::as_bool) + .unwrap_or(false) + ) +} -#[derive(Deserialize, Serialize, Debug)] -pub struct Config { - pub db_ip: String, - pub db_user: String, - pub db_password: String, - pub db_name: String, +/// Runtime service configuration supplied by the VS Code extension. The bearer +/// token is stored in VS Code SecretStorage and held only in memory here. +#[derive(Debug, Clone, Default)] +pub struct CaptureServiceConfig { + pub base_url: Option, + token: Option, + pub participant_id: Option, + pub instance_id: Option, + generation: u64, } -/* +impl CaptureServiceConfig { + fn configured(base_url: String, token: Option) -> Result { + let base_url = normalize_service_base_url(&base_url)?; + Ok(Self { + base_url: Some(base_url), + token: token.filter(|token| !token.trim().is_empty()), + participant_id: None, + instance_id: None, + generation: 0, + }) + } - ## The EventCapture Structure: + fn token(&self) -> Option<&str> { + self.token + .as_deref() + .filter(|token| !token.trim().is_empty()) + } - The `EventCapture` struct provides methods to interact with the database. It -holds a `tokio_postgres::Client` for database operations. + fn status_url(&self) -> Option { + self.base_url + .as_deref() + .map(|base_url| format!("{base_url}/v1/capture/status")) + } + + fn events_url(&self) -> Option { + self.base_url + .as_deref() + .map(|base_url| format!("{base_url}/v1/capture/events")) + } + + fn token_hash(&self) -> Option { + self.token().map(hash_capture_token) + } -### Usage Example + fn spool_identity(&self) -> Option { + if self.token().is_none() && self.base_url.is_none() { + return None; + } + Some(SpoolIdentity { + token_hash: self.token_hash(), + service_base_url: self.base_url.clone(), + participant_id: self.participant_id.clone(), + instance_id: self.instance_id.clone(), + }) + } -#\[tokio::main\] async fn main() -> Result<(), Box> { + fn matches_request_snapshot(&self, snapshot: &Self) -> bool { + self.generation == snapshot.generation + && self.base_url == snapshot.base_url + && self.token_hash() == snapshot.token_hash() + } +} -``` - // Create an instance of EventCapture using the configuration file - let event_capture = EventCapture::new("config.json").await?; +fn normalize_service_base_url(value: &str) -> Result { + let mut url = value.trim().trim_end_matches('/').to_string(); + if url.is_empty() { + return Err("capture service URL must not be empty".to_string()); + } + for suffix in ["/v1/capture/events", "/v1/capture/status", "/v1/health"] { + if let Some(base) = url.strip_suffix(suffix) { + url = base.trim_end_matches('/').to_string(); + break; + } + } - // Create an event - let event = Event { - user_id: "user123".to_string(), - event_type: "keystroke".to_string(), - data: Some("Pressed key A".to_string()), - }; + let mut parsed = + url::Url::parse(&url).map_err(|_| "capture service URL must be absolute".to_string())?; + if !parsed.username().is_empty() || parsed.password().is_some() { + return Err("capture service URL must not include credentials".to_string()); + } + let local_http = parsed.scheme() == "http" + && matches!(parsed.host_str(), Some("localhost" | "127.0.0.1" | "::1")); + if parsed.scheme() != "https" && !local_http { + return Err("capture service URL must use https:// except for localhost".to_string()); + } - // Insert the event into the database - event_capture.insert_event(event).await?; + parsed.set_query(None); + parsed.set_fragment(None); + let trimmed_path = parsed.path().trim_end_matches('/').to_string(); + parsed.set_path(&trimmed_path); + Ok(parsed.as_str().trim_end_matches('/').to_string()) +} - Ok(()) -``` -} */ +/// Known capture worker states reported to the VS Code status UI. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export)] +pub enum CaptureState { + /// Capture worker is not available. + Disabled, + /// Capture worker is starting. + Starting, + /// Events are being written to the local FIFO spool. + Spooling, + /// Events are being uploaded to CaptureWebService. + Uploading, + /// The local spool is empty and the remote service is reachable. + Remote, + /// The token was rejected by the service. + AuthFailed, + /// The service knows the token, but capture is currently not allowed. + CaptureDisabled, + /// The service or network is temporarily unavailable. + ServiceUnavailable, +} -pub struct EventCapture { - db_client: Arc>, +/// Non-secret token state shown in the capture UI. +#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, TS)] +#[serde(rename_all = "snake_case")] +#[ts(export)] +pub enum CaptureTokenStatus { + Missing, + Unverified, + Accepted, + Rejected, + CaptureDisabled, } -/* - ## The EventCapture Implementation -*/ +/// Capture worker health exposed to the VS Code status item. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)] +#[ts(export)] +pub struct CaptureStatus { + pub enabled: bool, + pub state: CaptureState, + pub token_status: CaptureTokenStatus, + pub queued_events: u64, + pub spooled_events: u64, + pub uploaded_events: u64, + pub failed_events: u64, + pub quarantined_events: u64, + pub last_error: Option, + pub spool_path: Option, + pub service_base_url: Option, + pub participant_id: Option, + pub instance_id: Option, + pub capture_enabled: Option, + pub participant_status: Option, + pub consent_status: Option, + pub instance_status: Option, + pub token_expires_at: Option, + pub service_version: Option, + pub last_status_check_at: Option, + pub last_upload_at: Option, +} + +impl CaptureStatus { + pub fn disabled() -> Self { + Self { + enabled: false, + state: CaptureState::Disabled, + token_status: CaptureTokenStatus::Missing, + queued_events: 0, + spooled_events: 0, + uploaded_events: 0, + failed_events: 0, + quarantined_events: 0, + last_error: None, + spool_path: None, + service_base_url: None, + participant_id: None, + instance_id: None, + capture_enabled: None, + participant_status: None, + consent_status: None, + instance_status: None, + token_expires_at: None, + service_version: None, + last_status_check_at: None, + last_upload_at: None, + } + } + + fn starting(spool_path: PathBuf) -> Self { + Self { + enabled: true, + state: CaptureState::Starting, + token_status: CaptureTokenStatus::Missing, + spool_path: Some(spool_path), + ..Self::disabled() + } + } +} + +/// The in-memory representation of a single capture event. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CaptureEvent { + pub event_id: Option, + pub sequence_number: Option, + pub schema_version: Option, + pub user_id: String, + pub session_id: Option, + pub event_source: Option, + pub language_id: Option, + pub file_hash: Option, + pub event_type: CaptureEventType, + pub timestamp: DateTime, + pub client_tz_offset_min: Option, + pub data: serde_json::Value, +} + +impl CaptureEvent { + pub fn new( + user_id: String, + file_hash: Option, + event_type: CaptureEventType, + timestamp: DateTime, + data: serde_json::Value, + ) -> Self { + Self { + event_id: None, + sequence_number: None, + schema_version: None, + user_id, + session_id: None, + event_source: None, + language_id: None, + file_hash, + event_type, + timestamp, + client_tz_offset_min: None, + data, + } + } + + #[allow(clippy::too_many_arguments)] + pub fn with_columns( + event_id: Option, + sequence_number: Option, + schema_version: Option, + user_id: String, + session_id: Option, + event_source: Option, + language_id: Option, + file_hash: Option, + event_type: CaptureEventType, + timestamp: DateTime, + client_tz_offset_min: Option, + data: serde_json::Value, + ) -> Self { + Self { + event_id, + sequence_number, + schema_version, + user_id, + session_id, + event_source, + language_id, + file_hash, + event_type, + timestamp, + client_tz_offset_min, + data, + } + } + + pub fn now( + user_id: String, + file_hash: Option, + event_type: CaptureEventType, + data: serde_json::Value, + ) -> Self { + Self::new(user_id, file_hash, event_type, Utc::now(), data) + } +} + +/// Generate a server-side event ID for events classified after the original +/// extension message has been processed. +pub fn generate_capture_event_id(prefix: &str) -> String { + let counter = NEXT_CAPTURE_EVENT_ID.fetch_add(1, Ordering::Relaxed); + format!( + "{prefix}-{}-{}-{counter}", + process::id(), + Utc::now().timestamp_micros() + ) +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +struct SpoolIdentity { + token_hash: Option, + service_base_url: Option, + participant_id: Option, + instance_id: Option, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SpoolRecord { + spooled_at: DateTime, + #[serde(default)] + identity: Option, + event: CaptureEvent, +} + +#[derive(Debug, Serialize)] +struct CaptureBatchRequest { + schema_version: i32, + #[serde(skip_serializing_if = "Option::is_none")] + participant_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + instance_id: Option, + client_sent_at: String, + events: Vec, +} + +#[derive(Debug, Clone, Serialize)] +struct CaptureServiceEvent { + event_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + sequence_number: Option, + #[serde(skip_serializing_if = "Option::is_none")] + schema_version: Option, + user_id: String, + session_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + event_source: Option, + #[serde(skip_serializing_if = "Option::is_none")] + language_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + file_hash: Option, + event_type: String, + #[serde(skip_serializing_if = "Option::is_none")] + client_tz_offset_min: Option, + client_event_time: String, + data: serde_json::Value, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CaptureServiceStatusResponse { + pub participant_id: String, + pub instance_id: String, + pub study_id: String, + pub capture_enabled: bool, + pub participant_status: String, + pub consent_status: String, + pub instance_status: String, + pub token_expires_at: Option, + pub server_time: String, + pub service_version: String, +} + +#[derive(Debug, Deserialize)] +struct CaptureBatchAcceptedResponse { + batch_id: String, + accepted: u64, + server_time: String, +} + +#[derive(Debug)] +struct SpoolBatch { + files: Vec, + body: Vec, + event_count: u64, +} + +#[derive(Debug)] +enum NextBatch { + Batch(SpoolBatch), + Empty, + NoMatchingIdentity, +} + +#[derive(Debug)] +struct CaptureHttpError { + status_code: Option, + message: String, +} + +impl CaptureHttpError { + fn transport(message: impl Into) -> Self { + Self { + status_code: None, + message: message.into(), + } + } + + fn response(status_code: i32, message: impl Into) -> Self { + Self { + status_code: Some(status_code), + message: message.into(), + } + } + + fn is_transient(&self) -> bool { + matches!(self.status_code, None | Some(429 | 500 | 503)) + || matches!(self.status_code, Some(code) if code >= 500) + } +} + +enum WorkerMsg { + Flush, +} + +/// Handle used by the rest of the server to record events. +#[derive(Clone)] +pub struct EventCapture { + tx: Sender, + status: Arc>, + config: Arc>, + spool_path: PathBuf, +} impl EventCapture { - /* - Creates a new `EventCapture` instance by reading the database connection parameters from the `config.json` file and connecting to the PostgreSQL database. - # Arguments - - config_path: The file path to the config.json file. + pub fn new(spool_path: PathBuf) -> Result { + fs::create_dir_all(&spool_path)?; + fs::create_dir_all(quarantine_path(&spool_path))?; - # Returns + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + update_spool_count(&spool_path, &status); - A `Result` containing an `EventCapture` instance - */ + let config = Arc::new(Mutex::new(CaptureServiceConfig::default())); + let (tx, rx) = mpsc::channel::(); + let status_worker = status.clone(); + let config_worker = config.clone(); + let spool_worker = spool_path.clone(); - pub async fn new>(config_path: P) -> Result { - // Read the configuration file - let config_content = fs::read_to_string(config_path).map_err(io::Error::other)?; - let config: Config = serde_json::from_str(&config_content) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?; + thread::Builder::new() + .name("codechat-capture-upload".to_string()) + .spawn(move || upload_worker(rx, config_worker, status_worker, spool_worker)) + .map_err(|err| { + io::Error::other(format!("Capture: failed to start upload worker: {err}")) + })?; - // Build the connection string for the PostgreSQL database - let conn_str = format!( - "host={} user={} password={} dbname={}", - config.db_ip, config.db_user, config.db_password, config.db_name - ); + Ok(Self { + tx, + status, + config, + spool_path, + }) + } + + pub fn configure_service(&self, base_url: String, token: Option) -> Result<(), String> { + let mut new_config = CaptureServiceConfig::configured(base_url, token)?; + let token_status = if new_config.token().is_some() { + CaptureTokenStatus::Unverified + } else { + CaptureTokenStatus::Missing + }; + { + let mut config = self + .config + .lock() + .map_err(|_| "capture service config lock is poisoned".to_string())?; + new_config.generation = config.generation.saturating_add(1); + *config = new_config.clone(); + } + update_status(&self.status, |status| { + status.enabled = true; + status.state = CaptureState::Spooling; + status.token_status = token_status; + status.service_base_url = new_config.base_url.clone(); + status.participant_id = None; + status.instance_id = None; + status.capture_enabled = None; + status.participant_status = None; + status.consent_status = None; + status.instance_status = None; + status.token_expires_at = None; + status.service_version = None; + status.last_error = None; + }); + update_spool_count(&self.spool_path, &self.status); + self.signal_flush(); + Ok(()) + } - info!( - "Attempting Capture Database Connection. IP:[{}] Username:[{}] Database Name:[{}]", - config.db_ip, config.db_user, config.db_name + pub fn clear_token(&self) { + if let Ok(mut config) = self.config.lock() { + config.generation = config.generation.saturating_add(1); + config.token = None; + config.participant_id = None; + config.instance_id = None; + } + update_status(&self.status, |status| { + status.state = CaptureState::Spooling; + status.token_status = CaptureTokenStatus::Missing; + status.participant_id = None; + status.instance_id = None; + status.capture_enabled = None; + status.participant_status = None; + status.consent_status = None; + status.instance_status = None; + status.token_expires_at = None; + status.service_version = None; + status.last_error = Some("Capture token is not configured".to_string()); + }); + update_spool_count(&self.spool_path, &self.status); + } + + pub fn check_service_status(&self) -> Result { + let cfg = self + .config + .lock() + .map_err(|_| "capture service config lock is poisoned".to_string())? + .clone(); + let response = request_capture_status(&cfg).map_err(|err| { + apply_http_error_if_current(&self.config, &self.status, &cfg, &err); + err.message + })?; + if !apply_capture_service_status_if_current( + &self.config, + &self.status, + &cfg, + response.clone(), + ) { + return Err("Capture service status response was stale".to_string()); + } + Ok(response) + } + + /// Durably append an event to the local FIFO spool, then ask the worker to + /// upload as soon as service access permits. + pub fn log(&self, event: CaptureEvent) { + debug!( + "Capture: spooling event: type={}, user_id={}, file_hash={:?}", + event.event_type, event.user_id, event.file_hash ); - // Connect to the database asynchronously - let (client, connection) = tokio_postgres::connect(&conn_str, NoTls) - .await - .map_err(|e| io::Error::new(io::ErrorKind::ConnectionRefused, e))?; + let spool_identity = match self.config.lock() { + Ok(config) => config.spool_identity(), + Err(err) => { + error!("Capture: FAILED to read capture config before spooling: {err}"); + update_status(&self.status, |status| { + status.failed_events += 1; + status.last_error = Some(format!( + "Failed to read capture config before spooling: {err}" + )); + }); + return; + } + }; + + match append_spool_event(&self.spool_path, &event, spool_identity) { + Ok(()) => { + update_status(&self.status, |status| { + if matches!( + status.state, + CaptureState::Starting | CaptureState::Disabled + ) { + status.state = CaptureState::Spooling; + } + status.last_error = None; + }); + update_spool_count(&self.spool_path, &self.status); + self.signal_flush(); + } + Err(err) => { + error!("Capture: FAILED to append event to spool: {err}"); + update_status(&self.status, |status| { + status.failed_events += 1; + status.last_error = + Some(format!("Failed to append capture event to spool: {err}")); + }); + } + } + } + + fn signal_flush(&self) { + if let Err(err) = self.tx.send(WorkerMsg::Flush) { + error!("Capture: FAILED to notify upload worker: {err}"); + update_status(&self.status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Failed to notify upload worker: {err}")); + }); + } + } + + pub fn status(&self) -> CaptureStatus { + self.status + .lock() + .map(|status| status.clone()) + .unwrap_or_else(|_| { + let mut status = CaptureStatus::disabled(); + status.last_error = Some("Capture status lock is poisoned".to_string()); + status + }) + } +} + +fn update_status(status: &Arc>, f: impl FnOnce(&mut CaptureStatus)) { + match status.lock() { + Ok(mut guard) => f(&mut guard), + Err(err) => error!("Capture: unable to update status: {err}"), + } +} + +fn upload_worker( + rx: mpsc::Receiver, + config: Arc>, + status: Arc>, + spool_path: PathBuf, +) { + info!("Capture: upload worker started with spool at {spool_path:?}."); + let mut retry_delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS); + loop { + match upload_next_batch(&spool_path, &config, &status) { + UploadOutcome::UploadedBatch => { + retry_delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS); + continue; + } + UploadOutcome::NoEvents => { + retry_delay = Duration::from_millis(INITIAL_RETRY_DELAY_MS); + } + UploadOutcome::NotConfigured | UploadOutcome::Paused => {} + UploadOutcome::TransientFailure => { + warn!( + "Capture: transient upload failure; retrying in {} ms.", + retry_delay.as_millis() + ); + } + } - // Spawn a task to manage the database connection in the background - tokio::spawn(async move { - if let Err(e) = connection.await { - error!("Database connection error: [{e}]"); + let wait_for = if matches!( + capture_status_state(&status), + CaptureState::ServiceUnavailable + ) { + retry_delay + } else { + Duration::from_secs(30) + }; + + match rx.recv_timeout(wait_for) { + Ok(WorkerMsg::Flush) => {} + Err(RecvTimeoutError::Timeout) => {} + Err(RecvTimeoutError::Disconnected) => { + warn!("Capture: upload worker channel closed; worker exiting."); + break; } + } + + if matches!( + capture_status_state(&status), + CaptureState::ServiceUnavailable + ) { + retry_delay = retry_delay + .saturating_mul(2) + .min(Duration::from_millis(MAX_RETRY_DELAY_MS)); + } + } +} + +fn capture_status_state(status: &Arc>) -> CaptureState { + status + .lock() + .map(|status| status.state) + .unwrap_or(CaptureState::Disabled) +} + +#[derive(Debug, PartialEq, Eq)] +enum UploadOutcome { + UploadedBatch, + NoEvents, + NotConfigured, + Paused, + TransientFailure, +} + +fn upload_next_batch( + spool_path: &Path, + config: &Arc>, + status: &Arc>, +) -> UploadOutcome { + update_spool_count(spool_path, status); + + let cfg = match config.lock() { + Ok(config) => config.clone(), + Err(err) => { + update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Capture config lock failed: {err}")); + }); + return UploadOutcome::Paused; + } + }; + + let Some(token) = cfg.token().map(str::to_string) else { + update_status(status, |status| { + status.state = CaptureState::Spooling; + status.token_status = CaptureTokenStatus::Missing; + status.last_error = Some("Capture token is not configured".to_string()); }); + return UploadOutcome::NotConfigured; + }; + let Some(events_url) = cfg.events_url() else { + update_status(status, |status| { + status.state = CaptureState::Spooling; + status.last_error = Some("Capture service URL is not configured".to_string()); + }); + return UploadOutcome::NotConfigured; + }; - info!( - "Connected to Database [{}] as User [{}]", - config.db_name, config.db_user - ); + if cfg.participant_id.is_none() || cfg.instance_id.is_none() { + match request_capture_status(&cfg) { + Ok(service_status) => { + if !apply_capture_service_status_if_current(config, status, &cfg, service_status) { + return UploadOutcome::Paused; + } + } + Err(err) => { + apply_http_error_if_current(config, status, &cfg, &err); + return if err.is_transient() { + UploadOutcome::TransientFailure + } else { + UploadOutcome::Paused + }; + } + } + } + + let cfg = match config.lock() { + Ok(config) => config.clone(), + Err(err) => { + update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Capture config lock failed: {err}")); + }); + return UploadOutcome::Paused; + } + }; + + if !matches!( + capture_status_state(status), + CaptureState::Remote | CaptureState::Spooling | CaptureState::Uploading + ) { + return UploadOutcome::Paused; + } + + let batch = match build_next_batch(spool_path, &cfg, status) { + NextBatch::Batch(batch) => batch, + NextBatch::Empty => { + update_status(status, |status| { + if !matches!( + status.state, + CaptureState::AuthFailed | CaptureState::CaptureDisabled + ) { + status.state = CaptureState::Remote; + } + }); + return UploadOutcome::NoEvents; + } + NextBatch::NoMatchingIdentity => { + update_status(status, |status| { + status.state = CaptureState::Spooling; + status.last_error = + Some("Pending capture events belong to a different capture token".to_string()); + }); + return UploadOutcome::NoEvents; + } + }; + + if !run_if_capture_config_snapshot_is_current(config, &cfg, || { + update_status(status, |status| { + status.state = CaptureState::Uploading; + status.last_error = None; + }); + }) { + return UploadOutcome::Paused; + } + + match post_capture_batch(&events_url, &token, &batch.body) { + Ok(accepted) => { + if !run_if_capture_config_snapshot_is_current(config, &cfg, || { + for file in &batch.files { + if let Err(err) = fs::remove_file(file) { + warn!("Capture: unable to remove uploaded spool file {file:?}: {err}"); + } + } + update_status(status, |status| { + status.state = CaptureState::Remote; + status.uploaded_events += accepted.accepted; + status.spooled_events = status.spooled_events.saturating_sub(batch.event_count); + status.last_upload_at = Some(accepted.server_time.clone()); + status.last_error = None; + }); + }) { + return UploadOutcome::Paused; + } + update_spool_count(spool_path, status); + debug!( + "Capture: uploaded batch {} with {} event(s).", + accepted.batch_id, accepted.accepted + ); + UploadOutcome::UploadedBatch + } + Err(err) => match err.status_code { + Some(401) => { + apply_http_error_if_current(config, status, &cfg, &err); + UploadOutcome::Paused + } + Some(403) => { + apply_http_error_if_current(config, status, &cfg, &err); + UploadOutcome::Paused + } + Some(400 | 413) => { + if !run_if_capture_config_snapshot_is_current(config, &cfg, || { + quarantine_files( + spool_path, + &batch.files, + &format!("Capture service rejected batch: {}", err.message), + status, + ); + }) { + return UploadOutcome::Paused; + } + update_spool_count(spool_path, status); + UploadOutcome::UploadedBatch + } + _ if err.is_transient() => { + apply_http_error_if_current(config, status, &cfg, &err); + UploadOutcome::TransientFailure + } + _ => { + apply_http_error_if_current(config, status, &cfg, &err); + UploadOutcome::Paused + } + }, + } +} + +fn append_spool_event( + spool_path: &Path, + event: &CaptureEvent, + identity: Option, +) -> io::Result<()> { + fs::create_dir_all(spool_path)?; + let counter = NEXT_SPOOL_FILE_ID.fetch_add(1, Ordering::Relaxed); + let timestamp = Utc::now().format("%Y%m%d%H%M%S%6f"); + let path = spool_path.join(format!( + "{}-{}-{:020}.json", + timestamp, + process::id(), + counter + )); + let tmp_path = path.with_extension("tmp"); + let mut event = event.clone(); + event.data = sanitize_capture_data(event.data).map_err(io::Error::other)?; + let record = SpoolRecord { + spooled_at: Utc::now(), + identity, + event, + }; + + { + let mut file = File::create(&tmp_path)?; + serde_json::to_writer(&mut file, &record) + .map_err(|err| io::Error::other(err.to_string()))?; + writeln!(file)?; + file.sync_all()?; + } + fs::rename(tmp_path, path)?; + Ok(()) +} + +fn pending_spool_files(spool_path: &Path) -> io::Result> { + let mut files = Vec::new(); + if !spool_path.exists() { + return Ok(files); + } + for entry in fs::read_dir(spool_path)? { + let entry = entry?; + let path = entry.path(); + if path.is_file() && path.extension().and_then(|ext| ext.to_str()) == Some("json") { + files.push(path); + } + } + files.sort_by(|a, b| a.file_name().cmp(&b.file_name())); + Ok(files) +} + +fn update_spool_count(spool_path: &Path, status: &Arc>) { + match pending_spool_files(spool_path) { + Ok(files) => update_status(status, |status| { + let count = files.len() as u64; + status.queued_events = count; + status.spooled_events = count; + }), + Err(err) => update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Unable to inspect capture spool: {err}")); + }), + } +} + +fn build_next_batch( + spool_path: &Path, + cfg: &CaptureServiceConfig, + status: &Arc>, +) -> NextBatch { + let files = match pending_spool_files(spool_path) { + Ok(files) => files, + Err(err) => { + update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Unable to read capture spool: {err}")); + }); + return NextBatch::Empty; + } + }; + if files.is_empty() { + return NextBatch::Empty; + } + + let mut selected_files = Vec::new(); + let mut events = Vec::new(); + let mut body = Vec::new(); + let mut skipped_identity_mismatch = false; + + for file in files { + if selected_files.len() >= MAX_CAPTURE_BATCH_EVENTS { + break; + } + + let record = match read_spool_record(&file) { + Ok(record) => record, + Err(err) => { + quarantine_files( + spool_path, + std::slice::from_ref(&file), + &format!("Invalid local spool record: {err}"), + status, + ); + continue; + } + }; + + if !spool_record_matches_current_identity(&record, cfg) { + skipped_identity_mismatch = true; + continue; + } + + if let Some(participant_id) = cfg.participant_id.as_deref() + && record.event.user_id != participant_id + { + quarantine_files( + spool_path, + std::slice::from_ref(&file), + "Capture event user_id does not match current capture token", + status, + ); + continue; + } + + let event = match service_event_from_capture_event(record.event) { + Ok(event) => event, + Err(err) => { + quarantine_files( + spool_path, + std::slice::from_ref(&file), + &format!("Invalid capture event for service upload: {err}"), + status, + ); + continue; + } + }; + + let mut candidate_events = events.clone(); + candidate_events.push(event); + let candidate_batch = CaptureBatchRequest { + schema_version: DEFAULT_CAPTURE_SCHEMA_VERSION, + participant_id: cfg.participant_id.clone(), + instance_id: cfg.instance_id.clone(), + client_sent_at: Utc::now().to_rfc3339(), + events: candidate_events, + }; - Ok(EventCapture { - db_client: Arc::new(Mutex::new(client)), + let candidate_body = match serde_json::to_vec(&candidate_batch) { + Ok(body) => body, + Err(err) => { + quarantine_files( + spool_path, + std::slice::from_ref(&file), + &format!("Unable to serialize capture batch: {err}"), + status, + ); + continue; + } + }; + + if candidate_body.len() > MAX_CAPTURE_BATCH_BYTES { + if selected_files.is_empty() { + quarantine_files( + spool_path, + std::slice::from_ref(&file), + "Single capture event exceeds service payload size limit", + status, + ); + continue; + } + break; + } + + events = candidate_batch.events; + body = candidate_body; + selected_files.push(file); + } + + if selected_files.is_empty() { + if skipped_identity_mismatch { + NextBatch::NoMatchingIdentity + } else { + NextBatch::Empty + } + } else { + NextBatch::Batch(SpoolBatch { + event_count: events.len() as u64, + files: selected_files, + body, }) } +} - /* - Inserts an event into the database. - - # Arguments - - `event`: An `Event` instance containing the event data to insert. - - # Returns - A `Result` indicating success or containing a `tokio_postgres::Error`. - - # Example - #[tokio::main] - async fn main() -> Result<(), Box> { - let event_capture = EventCapture::new("config.json").await?; - - let event = Event { - user_id: "user123".to_string(), - event_type: "keystroke".to_string(), - data: Some("Pressed key A".to_string()), - }; - - event_capture.insert_event(event).await?; - Ok(()) - } - */ - - pub async fn insert_event(&self, event: Event) -> Result<(), io::Error> { - let current_time = Local::now(); - let formatted_time = current_time.to_rfc3339(); - - // SQL statement to insert the event into the 'events' table - let stmt = indoc! {" - INSERT INTO events (user_id, event_type, timestamp, data) - VALUES ($1, $2, $3, $4) - "}; - - // Acquire a lock on the database client for thread-safe access - let client = self.db_client.lock().await; - - // Execute the SQL statement with the event data - client - .execute( - stmt, - &[ - &event.user_id, - &event.event_type, - &formatted_time, - &event.data, - ], - ) - .await - .map_err(io::Error::other)?; +fn spool_record_matches_current_identity(record: &SpoolRecord, cfg: &CaptureServiceConfig) -> bool { + if let Some(identity) = &record.identity { + return identity.token_hash == cfg.token_hash() + && identity.service_base_url == cfg.base_url; + } - info!("Event inserted into database: {event:?}"); + cfg.participant_id + .as_deref() + .map(|participant_id| record.event.user_id == participant_id) + .unwrap_or(false) +} - Ok(()) +fn read_spool_record(path: &Path) -> Result { + let text = fs::read_to_string(path).map_err(|err| err.to_string())?; + serde_json::from_str(&text).map_err(|err| err.to_string()) +} + +fn service_event_from_capture_event( + mut event: CaptureEvent, +) -> Result { + let event_id = event + .event_id + .take() + .filter(|event_id| !event_id.trim().is_empty()) + .ok_or_else(|| "event_id is required".to_string())?; + if event_id.len() > 128 { + return Err("event_id exceeds 128 characters".to_string()); + } + let session_id = event + .session_id + .take() + .filter(|session_id| !session_id.trim().is_empty()) + .ok_or_else(|| "session_id is required".to_string())?; + if session_id.len() > 128 { + return Err("session_id exceeds 128 characters".to_string()); + } + + let data = sanitize_capture_data(event.data)?; + Ok(CaptureServiceEvent { + event_id, + sequence_number: event.sequence_number, + schema_version: event.schema_version, + user_id: event.user_id, + session_id, + event_source: event.event_source, + language_id: event.language_id, + file_hash: event.file_hash, + event_type: event.event_type.as_str().to_string(), + client_tz_offset_min: event.client_tz_offset_min, + client_event_time: event.timestamp.to_rfc3339(), + data, + }) +} + +fn sanitize_capture_data(value: serde_json::Value) -> Result { + let serde_json::Value::Object(map) = value else { + return Err("capture event data must be a JSON object".to_string()); + }; + Ok(serde_json::Value::Object(sanitize_capture_object(map))) +} + +fn sanitize_capture_object( + map: serde_json::Map, +) -> serde_json::Map { + const FORBIDDEN_KEYS: &[&str] = &["file_path", "path", "absolute_path", "workspace_path"]; + map.into_iter() + .filter_map(|(key, value)| { + if FORBIDDEN_KEYS.contains(&key.as_str()) { + return None; + } + Some((key, sanitize_capture_value(value))) + }) + .collect() +} + +fn sanitize_capture_value(value: serde_json::Value) -> serde_json::Value { + match value { + serde_json::Value::Object(map) => serde_json::Value::Object(sanitize_capture_object(map)), + serde_json::Value::Array(values) => { + serde_json::Value::Array(values.into_iter().map(sanitize_capture_value).collect()) + } + other => other, + } +} + +fn post_capture_batch( + events_url: &str, + token: &str, + body: &[u8], +) -> Result { + post_capture_batch_with_timeout(events_url, token, body, CAPTURE_SERVICE_HTTP_TIMEOUT_SECS) +} + +fn post_capture_batch_with_timeout( + events_url: &str, + token: &str, + body: &[u8], + timeout_secs: u64, +) -> Result { + let response = minreq::post(events_url) + .with_header("Authorization", format!("Bearer {token}")) + .with_header("Content-Type", "application/json") + .with_body(body.to_vec()) + .with_timeout(timeout_secs) + .send() + .map_err(|err| CaptureHttpError::transport(err.to_string()))?; + + if response.status_code != 202 { + return Err(CaptureHttpError::response( + i32::from(response.status_code), + response + .as_str() + .unwrap_or(response.reason_phrase.as_str()) + .to_string(), + )); + } + serde_json::from_slice(response.as_bytes()).map_err(|err| { + CaptureHttpError::response(202, format!("Invalid capture accepted response: {err}")) + }) +} + +fn request_capture_status( + cfg: &CaptureServiceConfig, +) -> Result { + request_capture_status_with_timeout(cfg, CAPTURE_SERVICE_HTTP_TIMEOUT_SECS) +} + +fn request_capture_status_with_timeout( + cfg: &CaptureServiceConfig, + timeout_secs: u64, +) -> Result { + let token = cfg + .token() + .ok_or_else(|| CaptureHttpError::response(401, "Capture token is not configured"))?; + let status_url = cfg + .status_url() + .ok_or_else(|| CaptureHttpError::transport("Capture service URL is not configured"))?; + let response = minreq::get(status_url) + .with_header("Authorization", format!("Bearer {token}")) + .with_timeout(timeout_secs) + .send() + .map_err(|err| CaptureHttpError::transport(err.to_string()))?; + + if response.status_code != 200 { + return Err(CaptureHttpError::response( + i32::from(response.status_code), + response + .as_str() + .unwrap_or(response.reason_phrase.as_str()) + .to_string(), + )); + } + serde_json::from_slice(response.as_bytes()).map_err(|err| { + CaptureHttpError::response(200, format!("Invalid capture status response: {err}")) + }) +} + +fn run_if_capture_config_snapshot_is_current( + config: &Arc>, + snapshot: &CaptureServiceConfig, + action: impl FnOnce(), +) -> bool { + let Ok(current) = config.lock() else { + return false; + }; + if !current.matches_request_snapshot(snapshot) { + return false; + } + action(); + true +} + +fn apply_capture_service_status_if_current( + config: &Arc>, + status: &Arc>, + snapshot: &CaptureServiceConfig, + response: CaptureServiceStatusResponse, +) -> bool { + if let Ok(mut config) = config.lock() { + if !config.matches_request_snapshot(snapshot) { + return false; + } + config.participant_id = Some(response.participant_id.clone()); + config.instance_id = Some(response.instance_id.clone()); + update_status(status, |status| { + status.token_status = if response.capture_enabled { + CaptureTokenStatus::Accepted + } else { + CaptureTokenStatus::CaptureDisabled + }; + status.state = if response.capture_enabled { + CaptureState::Remote + } else { + CaptureState::CaptureDisabled + }; + status.participant_id = Some(response.participant_id); + status.instance_id = Some(response.instance_id); + status.capture_enabled = Some(response.capture_enabled); + status.participant_status = Some(response.participant_status); + status.consent_status = Some(response.consent_status); + status.instance_status = Some(response.instance_status); + status.token_expires_at = response.token_expires_at; + status.service_version = Some(response.service_version); + status.last_status_check_at = Some(Utc::now().to_rfc3339()); + status.last_error = if response.capture_enabled { + None + } else { + Some("Capture is disabled by the portal/service".to_string()) + }; + }); + } else { + return false; + } + true +} + +fn apply_http_error(status: &Arc>, err: &CaptureHttpError) { + update_status(status, |status| { + status.failed_events += 1; + match err.status_code { + Some(401) => { + status.state = CaptureState::AuthFailed; + status.token_status = CaptureTokenStatus::Rejected; + } + Some(403) => { + status.state = CaptureState::CaptureDisabled; + status.token_status = CaptureTokenStatus::CaptureDisabled; + } + _ if err.is_transient() => { + status.state = CaptureState::ServiceUnavailable; + } + _ => {} + } + status.last_error = Some(err.message.clone()); + }); +} + +fn apply_http_error_if_current( + config: &Arc>, + status: &Arc>, + snapshot: &CaptureServiceConfig, + err: &CaptureHttpError, +) -> bool { + run_if_capture_config_snapshot_is_current(config, snapshot, || { + apply_http_error(status, err); + }) +} + +fn quarantine_path(spool_path: &Path) -> PathBuf { + spool_path.join("quarantine") +} + +fn quarantine_files( + spool_path: &Path, + files: &[PathBuf], + reason: &str, + status: &Arc>, +) { + let quarantine = quarantine_path(spool_path); + if let Err(err) = fs::create_dir_all(&quarantine) { + update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Unable to create quarantine directory: {err}")); + }); + return; + } + + for file in files { + let file_name = file + .file_name() + .map(|name| name.to_owned()) + .unwrap_or_else(|| "capture-event.json".into()); + let target = quarantine.join(file_name); + if let Err(err) = fs::rename(file, &target).or_else(|_| { + fs::copy(file, &target)?; + fs::remove_file(file) + }) { + update_status(status, |status| { + status.failed_events += 1; + status.last_error = Some(format!("Unable to quarantine {file:?}: {err}")); + }); + continue; + } + let reason_path = target.with_extension("reason.txt"); + if let Err(err) = fs::write(&reason_path, reason) { + warn!("Capture: unable to write quarantine reason {reason_path:?}: {err}"); + } + update_status(status, |status| { + status.quarantined_events += 1; + status.last_error = Some(reason.to_string()); + }); } } -/* Database Schema (SQL DDL) +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + use std::{ + fs, + io::{Read, Write}, + net::TcpListener, + thread, + time::{Duration, Instant}, + }; + + fn temp_spool_path(test_name: &str) -> PathBuf { + std::env::temp_dir().join(format!( + "codechat-capture-{test_name}-{}-{}", + std::process::id(), + Utc::now().timestamp_nanos_opt().unwrap_or_default() + )) + } + + fn capture_test_event(user_id: &str, event_id: &str) -> CaptureEvent { + CaptureEvent::with_columns( + Some(event_id.to_string()), + Some(1), + Some(2), + user_id.to_string(), + Some("session-1".to_string()), + Some("vscode_extension".to_string()), + Some("rust".to_string()), + Some("file-hash".to_string()), + CaptureEventType::Save, + Utc::now(), + Some(360), + json!({ "reason": "unit_test" }), + ) + } + + fn capture_service_config(token: &str, generation: u64) -> CaptureServiceConfig { + let mut cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some(token.to_string()), + ) + .expect("capture service config should parse"); + cfg.generation = generation; + cfg + } + + fn capture_service_config_with_base_url( + base_url: &str, + token: &str, + generation: u64, + participant_id: &str, + ) -> CaptureServiceConfig { + let mut cfg = + CaptureServiceConfig::configured(base_url.to_string(), Some(token.to_string())) + .expect("capture service config should parse"); + cfg.generation = generation; + cfg.participant_id = Some(participant_id.to_string()); + cfg.instance_id = Some(format!("{participant_id}-instance")); + cfg + } + + fn capture_service_status_response(participant_id: &str) -> CaptureServiceStatusResponse { + CaptureServiceStatusResponse { + participant_id: participant_id.to_string(), + instance_id: format!("{participant_id}-instance"), + study_id: "study-2026".to_string(), + capture_enabled: true, + participant_status: "active".to_string(), + consent_status: "consented".to_string(), + instance_status: "active".to_string(), + token_expires_at: None, + server_time: "2026-07-12T16:10:04Z".to_string(), + service_version: "0.1.0".to_string(), + } + } + + fn start_delayed_capture_events_server( + status_code: u16, + body: &'static str, + ) -> ( + String, + std::sync::mpsc::Receiver<()>, + std::sync::mpsc::Sender<()>, + thread::JoinHandle<()>, + ) { + let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind"); + let base_url = format!( + "http://{}", + listener.local_addr().expect("listener should have address") + ); + let (request_seen_tx, request_seen_rx) = std::sync::mpsc::channel(); + let (respond_tx, respond_rx) = std::sync::mpsc::channel(); + let handle = thread::spawn(move || { + let (mut stream, _addr) = listener.accept().expect("request should arrive"); + let mut request = Vec::new(); + let mut buffer = [0; 1024]; + loop { + let read = stream.read(&mut buffer).expect("request should read"); + if read == 0 { + break; + } + request.extend_from_slice(&buffer[..read]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + break; + } + } + request_seen_tx + .send(()) + .expect("request notification should send"); + respond_rx.recv().expect("response release should arrive"); + let reason = match status_code { + 202 => "Accepted", + 400 => "Bad Request", + 413 => "Payload Too Large", + _ => "Response", + }; + write!( + stream, + "HTTP/1.1 {status_code} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .expect("response should write"); + }); + (base_url, request_seen_rx, respond_tx, handle) + } + + fn mark_config_replaced_during_upload( + config: &Arc>, + status: &Arc>, + base_url: &str, + ) { + *config.lock().expect("config lock should not be poisoned") = + capture_service_config_with_base_url(base_url, "new-token", 2, "new-user"); + update_status(status, |status| { + status.state = CaptureState::Spooling; + status.token_status = CaptureTokenStatus::Unverified; + status.participant_id = None; + status.instance_id = None; + status.uploaded_events = 0; + status.quarantined_events = 0; + status.last_upload_at = None; + status.last_error = None; + }); + } + + #[test] + fn capture_event_type_uses_stable_serialized_strings() { + assert_eq!( + serde_json::to_value(CaptureEventType::WriteDoc).unwrap(), + json!("write_doc") + ); + assert_eq!( + serde_json::from_value::(json!("compile_end")).unwrap(), + CaptureEventType::CompileEnd + ); + assert_eq!( + serde_json::to_value(CaptureEventType::CaptureSettingsChanged).unwrap(), + json!("capture_settings_changed") + ); + assert!(serde_json::from_value::(json!("random")).is_err()); + } -The following SQL statement creates the `events` table used by this library: + #[test] + fn capture_event_new_sets_all_fields() { + let ts = Utc::now(); -CREATE TABLE events ( id SERIAL PRIMARY KEY, user_id TEXT NOT NULL, -event_type TEXT NOT NULL, timestamp TEXT NOT NULL, data TEXT ); + let ev = CaptureEvent::new( + "user123".to_string(), + Some("hashed-path".to_string()), + CaptureEventType::WriteDoc, + ts, + json!({ "chars_typed": 42 }), + ); + + assert_eq!(ev.user_id, "user123"); + assert_eq!(ev.file_hash.as_deref(), Some("hashed-path")); + assert_eq!(ev.event_type, CaptureEventType::WriteDoc); + assert_eq!(ev.timestamp, ts); + assert!(ev.event_id.is_none()); + assert_eq!(ev.data, json!({ "chars_typed": 42 })); + } + + #[test] + fn capture_event_now_uses_current_time_and_fields() { + let before = Utc::now(); + let ev = CaptureEvent::now( + "user123".to_string(), + None, + CaptureEventType::Save, + json!({ "reason": "manual" }), + ); + let after = Utc::now(); -- **`id SERIAL PRIMARY KEY`**: Auto-incrementing primary key. -- **`user_id TEXT NOT NULL`**: The ID of the user associated with the event. -- **`event_type TEXT NOT NULL`**: The type of event. -- **`timestamp TEXT NOT NULL`**: The timestamp of the event. -- **`data TEXT`**: Optional additional data associated with the event. - **Note:** Ensure this table exists in your PostgreSQL database before using - the library. */ + assert_eq!(ev.user_id, "user123"); + assert!(ev.file_hash.is_none()); + assert_eq!(ev.event_type, CaptureEventType::Save); + assert_eq!(ev.data, json!({ "reason": "manual" })); + assert!(ev.timestamp >= before); + assert!(ev.timestamp <= after); + } + + #[test] + fn capture_spool_writes_fifo_json_records() { + let spool_path = temp_spool_path("spool-test"); + let _ = fs::remove_dir_all(&spool_path); + + let capture = EventCapture::new(spool_path.clone()).expect("capture worker should start"); + capture.log(CaptureEvent::with_columns( + Some("event-1".to_string()), + Some(1), + Some(2), + "participant".to_string(), + Some("session".to_string()), + Some("test".to_string()), + Some("rust".to_string()), + Some("file-hash".to_string()), + CaptureEventType::Save, + Utc::now(), + Some(360), + json!({ "reason": "unit_test" }), + )); + + let mut text = String::new(); + for _ in 0..20 { + if let Ok(files) = pending_spool_files(&spool_path) + && let Some(path) = files.first() + && let Ok(contents) = fs::read_to_string(path) + { + text = contents; + if text.contains("\"event_id\":\"event-1\"") { + break; + } + } + thread::sleep(Duration::from_millis(50)); + } + + assert!(text.contains("\"event_type\":\"save\"")); + assert!(text.contains("\"spooled_at\"")); + assert_eq!(capture.status().state, CaptureState::Spooling); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_spool_sanitizes_forbidden_path_keys_before_disk() { + let spool_path = temp_spool_path("spool-sanitize-test"); + let _ = fs::remove_dir_all(&spool_path); + let mut event = capture_test_event("participant", "event-sanitized"); + event.data = json!({ + "file_path": "C:/secret.rs", + "nested": { "path": "/secret" }, + "items": [ + { "absolute_path": "/secret2", "ok": true }, + [[{ "workspace_path": "/secret3", "nested_ok": true }]] + ] + }); + + append_spool_event(&spool_path, &event, None).expect("event should spool"); + let files = pending_spool_files(&spool_path).expect("spool should list"); + let record = read_spool_record(&files[0]).expect("spool record should parse"); + + assert!(record.event.data.get("file_path").is_none()); + assert!(record.event.data.pointer("/nested/path").is_none()); + assert!( + record + .event + .data + .pointer("/items/0/absolute_path") + .is_none() + ); + assert_eq!(record.event.data.pointer("/items/0/ok"), Some(&json!(true))); + assert!( + record + .event + .data + .pointer("/items/1/0/0/workspace_path") + .is_none() + ); + assert_eq!( + record.event.data.pointer("/items/1/0/0/nested_ok"), + Some(&json!(true)) + ); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_event_with_columns_sets_analysis_columns() { + let ts = Utc::now(); + + let ev = CaptureEvent::with_columns( + Some("abc-123".to_string()), + Some(42), + Some(2), + "user123".to_string(), + Some("session-1".to_string()), + Some("vscode_extension".to_string()), + Some("rust".to_string()), + Some("hash".to_string()), + CaptureEventType::WriteCode, + ts, + Some(-360), + json!({ "chars_typed": 42 }), + ); + + assert_eq!(ev.event_id.as_deref(), Some("abc-123")); + assert_eq!(ev.sequence_number, Some(42)); + assert_eq!(ev.schema_version, Some(2)); + assert_eq!(ev.session_id.as_deref(), Some("session-1")); + assert_eq!(ev.event_source.as_deref(), Some("vscode_extension")); + assert_eq!(ev.language_id.as_deref(), Some("rust")); + assert_eq!(ev.file_hash.as_deref(), Some("hash")); + assert_eq!(ev.client_tz_offset_min, Some(-360)); + assert_eq!(ev.data, json!({ "chars_typed": 42 })); + } + + #[test] + fn capture_service_payload_sanitizes_forbidden_path_keys() { + let ev = CaptureEvent::with_columns( + Some("event-1".to_string()), + Some(1), + Some(2), + "user123".to_string(), + Some("session-1".to_string()), + Some("vscode_extension".to_string()), + Some("rust".to_string()), + Some(hash_capture_path("src/lib.rs")), + CaptureEventType::Save, + Utc::now(), + Some(360), + json!({ + "reason": "manual", + "file_path": "C:/secret.rs", + "nested": { "path": "/secret" }, + "items": [ + { "absolute_path": "/secret2", "ok": true }, + [[{ "workspace_path": "/secret3", "nested_ok": true }]] + ] + }), + ); + let service_event = service_event_from_capture_event(ev).expect("event should convert"); + + assert_eq!(service_event.event_type, "save"); + assert_eq!(service_event.session_id, "session-1"); + assert!(service_event.data.get("file_path").is_none()); + assert!(service_event.data.pointer("/nested/path").is_none()); + assert!( + service_event + .data + .pointer("/items/0/absolute_path") + .is_none() + ); + assert_eq!( + service_event.data.pointer("/items/0/ok"), + Some(&json!(true)) + ); + assert!( + service_event + .data + .pointer("/items/1/0/0/workspace_path") + .is_none() + ); + assert_eq!( + service_event.data.pointer("/items/1/0/0/nested_ok"), + Some(&json!(true)) + ); + } + + #[test] + fn stale_capture_status_response_does_not_overwrite_current_token_identity() { + let old_snapshot = capture_service_config("old-token", 1); + let new_config = capture_service_config("new-token", 2); + let config = Arc::new(Mutex::new(new_config.clone())); + let status = Arc::new(Mutex::new(CaptureStatus::starting(temp_spool_path( + "stale-status-test", + )))); + + assert!(!apply_capture_service_status_if_current( + &config, + &status, + &old_snapshot, + capture_service_status_response("old-user"), + )); + + let current = config.lock().expect("config lock should not be poisoned"); + assert_eq!(current.token_hash(), new_config.token_hash()); + assert_eq!(current.participant_id, None); + assert_eq!(current.instance_id, None); + drop(current); + + let status = status.lock().expect("status lock should not be poisoned"); + assert_eq!(status.participant_id, None); + assert_ne!(status.token_status, CaptureTokenStatus::Accepted); + } + + #[test] + fn stale_capture_http_error_does_not_mark_current_token_rejected() { + let old_snapshot = capture_service_config("old-token", 1); + let new_config = capture_service_config("new-token", 2); + let config = Arc::new(Mutex::new(new_config)); + let status = Arc::new(Mutex::new(CaptureStatus::starting(temp_spool_path( + "stale-error-test", + )))); + let err = CaptureHttpError::response(401, "old token rejected"); + + assert!(!apply_http_error_if_current( + &config, + &status, + &old_snapshot, + &err, + )); + + let status = status.lock().expect("status lock should not be poisoned"); + assert_ne!(status.state, CaptureState::AuthFailed); + assert_ne!(status.token_status, CaptureTokenStatus::Rejected); + assert_eq!(status.last_error, None); + } + + #[test] + fn stale_capture_post_success_does_not_delete_spooled_events() { + let spool_path = temp_spool_path("stale-post-success-test"); + let _ = fs::remove_dir_all(&spool_path); + let (base_url, request_seen_rx, respond_tx, server_handle) = + start_delayed_capture_events_server( + 202, + r#"{"batch_id":"batch-1","accepted":1,"server_time":"2026-07-12T16:10:05Z"}"#, + ); + let old_cfg = capture_service_config_with_base_url(&base_url, "old-token", 1, "old-user"); + append_spool_event( + &spool_path, + &capture_test_event("old-user", "old-post-success-event"), + old_cfg.spool_identity(), + ) + .expect("old event should spool"); + let config = Arc::new(Mutex::new(old_cfg)); + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + update_status(&status, |status| { + status.state = CaptureState::Spooling; + }); + let upload_config = config.clone(); + let upload_status = status.clone(); + let upload_spool_path = spool_path.clone(); + let upload_handle = thread::spawn(move || { + upload_next_batch(&upload_spool_path, &upload_config, &upload_status) + }); + + request_seen_rx + .recv_timeout(Duration::from_secs(2)) + .expect("upload request should start"); + mark_config_replaced_during_upload(&config, &status, &base_url); + respond_tx.send(()).expect("response should release"); + + assert_eq!( + upload_handle.join().expect("upload thread should finish"), + UploadOutcome::Paused, + ); + server_handle.join().expect("server thread should finish"); + assert_eq!( + pending_spool_files(&spool_path) + .expect("spool should list") + .len(), + 1 + ); + let status = status.lock().expect("status lock should not be poisoned"); + assert_eq!(status.state, CaptureState::Spooling); + assert_eq!(status.uploaded_events, 0); + assert_eq!(status.last_upload_at, None); + drop(status); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn stale_capture_post_validation_failure_does_not_quarantine_spooled_events() { + let spool_path = temp_spool_path("stale-post-validation-test"); + let _ = fs::remove_dir_all(&spool_path); + let (base_url, request_seen_rx, respond_tx, server_handle) = + start_delayed_capture_events_server(400, r#"{"error":{"message":"invalid"}}"#); + let old_cfg = capture_service_config_with_base_url(&base_url, "old-token", 1, "old-user"); + append_spool_event( + &spool_path, + &capture_test_event("old-user", "old-post-validation-event"), + old_cfg.spool_identity(), + ) + .expect("old event should spool"); + let config = Arc::new(Mutex::new(old_cfg)); + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + update_status(&status, |status| { + status.state = CaptureState::Spooling; + }); + let upload_config = config.clone(); + let upload_status = status.clone(); + let upload_spool_path = spool_path.clone(); + let upload_handle = thread::spawn(move || { + upload_next_batch(&upload_spool_path, &upload_config, &upload_status) + }); + + request_seen_rx + .recv_timeout(Duration::from_secs(2)) + .expect("upload request should start"); + mark_config_replaced_during_upload(&config, &status, &base_url); + respond_tx.send(()).expect("response should release"); + + assert_eq!( + upload_handle.join().expect("upload thread should finish"), + UploadOutcome::Paused, + ); + server_handle.join().expect("server thread should finish"); + assert_eq!( + pending_spool_files(&spool_path) + .expect("spool should list") + .len(), + 1 + ); + assert_eq!( + pending_spool_files(&quarantine_path(&spool_path)) + .expect("quarantine should list") + .len(), + 0 + ); + let status = status.lock().expect("status lock should not be poisoned"); + assert_eq!(status.state, CaptureState::Spooling); + assert_eq!(status.quarantined_events, 0); + assert_eq!(status.last_error, None); + drop(status); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_batch_skips_spool_records_for_other_tokens() { + let spool_path = temp_spool_path("spool-token-test"); + let _ = fs::remove_dir_all(&spool_path); + + let mut old_cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some("old-token".to_string()), + ) + .expect("old config should parse"); + old_cfg.participant_id = Some("old-user".to_string()); + old_cfg.instance_id = Some("old-instance".to_string()); + let mut new_cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some("new-token".to_string()), + ) + .expect("new config should parse"); + new_cfg.participant_id = Some("new-user".to_string()); + new_cfg.instance_id = Some("new-instance".to_string()); + + append_spool_event( + &spool_path, + &capture_test_event("old-user", "old-event"), + old_cfg.spool_identity(), + ) + .expect("old event should spool"); + append_spool_event( + &spool_path, + &capture_test_event("new-user", "new-event"), + new_cfg.spool_identity(), + ) + .expect("new event should spool"); + + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + let batch = match build_next_batch(&spool_path, &new_cfg, &status) { + NextBatch::Batch(batch) => batch, + other => panic!("expected matching batch, got {other:?}"), + }; + let body: serde_json::Value = + serde_json::from_slice(&batch.body).expect("batch body should parse"); + + assert_eq!(batch.files.len(), 1); + assert_eq!( + body.pointer("/events/0/event_id"), + Some(&json!("new-event")) + ); + assert_eq!(body.pointer("/events/0/user_id"), Some(&json!("new-user"))); + assert_eq!(body.pointer("/events/1"), None); + assert_eq!( + pending_spool_files(&spool_path) + .expect("spool should list") + .len(), + 2 + ); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_batch_reports_when_only_other_token_records_are_pending() { + let spool_path = temp_spool_path("spool-token-mismatch-test"); + let _ = fs::remove_dir_all(&spool_path); + + let old_cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some("old-token".to_string()), + ) + .expect("old config should parse"); + let mut new_cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some("new-token".to_string()), + ) + .expect("new config should parse"); + new_cfg.participant_id = Some("new-user".to_string()); + + append_spool_event( + &spool_path, + &capture_test_event("old-user", "old-event"), + old_cfg.spool_identity(), + ) + .expect("old event should spool"); + + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + assert!(matches!( + build_next_batch(&spool_path, &new_cfg, &status), + NextBatch::NoMatchingIdentity + )); + assert_eq!( + pending_spool_files(&spool_path) + .expect("spool should list") + .len(), + 1 + ); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_batch_does_not_upload_legacy_records_before_identity_known() { + let spool_path = temp_spool_path("spool-legacy-unknown-identity-test"); + let _ = fs::remove_dir_all(&spool_path); + + let cfg = CaptureServiceConfig::configured( + "https://capture.example/dev".to_string(), + Some("token".to_string()), + ) + .expect("config should parse"); + + append_spool_event( + &spool_path, + &capture_test_event("participant", "legacy-event"), + None, + ) + .expect("legacy event should spool"); + + let status = Arc::new(Mutex::new(CaptureStatus::starting(spool_path.clone()))); + assert!(matches!( + build_next_batch(&spool_path, &cfg, &status), + NextBatch::NoMatchingIdentity + )); + assert_eq!( + pending_spool_files(&spool_path) + .expect("spool should list") + .len(), + 1 + ); + let _ = fs::remove_dir_all(&spool_path); + } + + #[test] + fn capture_http_upload_uses_request_timeout() { + let listener = TcpListener::bind("127.0.0.1:0").expect("listener should bind"); + let addr = listener.local_addr().expect("listener should have address"); + thread::spawn(move || { + if let Ok((_stream, _addr)) = listener.accept() { + thread::sleep(Duration::from_secs(3)); + } + }); + + let started = Instant::now(); + let err = post_capture_batch_with_timeout( + &format!("http://{addr}/v1/capture/events"), + "token", + br#"{"events":[]}"#, + 1, + ) + .expect_err("silent local server should time out"); + + assert!(err.status_code.is_none()); + assert!(started.elapsed() < Duration::from_secs(3)); + } + + #[test] + fn service_url_normalization_accepts_dev_base_and_routes() { + assert_eq!( + normalize_service_base_url( + "https://9m2nbv2rvc.execute-api.us-east-2.amazonaws.com/dev/v1/capture/events" + ) + .unwrap(), + "https://9m2nbv2rvc.execute-api.us-east-2.amazonaws.com/dev" + ); + assert_eq!( + normalize_service_base_url("http://localhost:8787/v1/capture/status").unwrap(), + "http://localhost:8787" + ); + assert_eq!( + normalize_service_base_url("http://127.0.0.1:8787/dev/").unwrap(), + "http://127.0.0.1:8787/dev" + ); + assert!(normalize_service_base_url("postgres://example").is_err()); + assert!(normalize_service_base_url("http://capture.example/dev").is_err()); + assert!(normalize_service_base_url("http://localhost.evil/dev").is_err()); + assert!(normalize_service_base_url("https://user:pass@example.com/dev").is_err()); + } +} diff --git a/server/src/ide.rs b/server/src/ide.rs index eb352945..e7d8cede 100644 --- a/server/src/ide.rs +++ b/server/src/ide.rs @@ -63,13 +63,14 @@ use tokio::{ // ### Local use crate::{ + capture::CaptureEventWire, ide::vscode::{connection_id_raw_to_str, vscode_ide_core}, processing::{CodeChatForWeb, CodeMirror, CodeMirrorDiffable, SourceFileMetadata}, translation::{CreatedTranslationQueues, create_translation_queues}, webserver::{ self, CursorPosition, EditorMessage, EditorMessageContents, INITIAL_IDE_MESSAGE_ID, MESSAGE_ID_INCREMENT, REPLY_TIMEOUT_MS, ResultErrTypes, ResultOkTypes, - UpdateMessageContents, WebAppState, setup_server, + UpdateMessageContents, WebAppState, }, }; @@ -94,6 +95,7 @@ async fn start_server( // Provide a class to start and stop the server. All its fields are opaque, // since only Rust should use them. pub struct CodeChatEditorServer { + app_state: WebAppState, server_handle: ServerHandle, from_ide_tx: Sender, to_ide_rx: Arc>>, @@ -106,11 +108,19 @@ pub struct CodeChatEditorServer { impl CodeChatEditorServer { // Creating the server could fail, so this must return an `io::Result`. pub fn new() -> std::io::Result { + let capture_spool_path = webserver::ROOT_PATH.lock().unwrap().join("capture-spool"); + Self::new_with_capture_spool(capture_spool_path) + } + + pub fn new_with_capture_spool( + capture_spool_path: std::path::PathBuf, + ) -> std::io::Result { // Start the server. - let (server, app_state) = setup_server( + let (server, app_state) = webserver::setup_server_with_capture_spool( // A port of 0 requests the OS to assign an open port. &SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 0), None, + capture_spool_path, )?; let server_handle = server.handle(); @@ -142,6 +152,7 @@ impl CodeChatEditorServer { let (expired_messages_tx, expired_messages_rx) = mpsc::channel(100); Ok(CodeChatEditorServer { + app_state, server_handle, from_ide_tx: websocket_queues.from_websocket_tx, to_ide_rx: Arc::new(Mutex::new(websocket_queues.to_websocket_rx)), @@ -252,6 +263,49 @@ impl CodeChatEditorServer { .await } + pub async fn send_capture_event( + &self, + capture_event: CaptureEventWire, + ) -> std::io::Result { + self.send_message_timeout(EditorMessageContents::Capture(Box::new(capture_event))) + .await + } + + pub fn capture_status(&self) -> crate::capture::CaptureStatus { + webserver::capture_status(&self.app_state) + } + + pub fn configure_capture_service( + &self, + base_url: String, + token: Option, + ) -> Result<(), String> { + self.app_state + .capture + .as_ref() + .ok_or_else(|| "Capture worker is not available".to_string())? + .configure_service(base_url, token) + } + + pub fn clear_capture_token(&self) -> Result<(), String> { + self.app_state + .capture + .as_ref() + .ok_or_else(|| "Capture worker is not available".to_string())? + .clear_token(); + Ok(()) + } + + pub fn check_capture_service_status( + &self, + ) -> Result { + self.app_state + .capture + .as_ref() + .ok_or_else(|| "Capture worker is not available".to_string())? + .check_service_status() + } + // Send a `CurrentFile` message. The other parameter (true if text/false if // binary/None if ignored) is ignored by the server, so it's always sent as // `None`. diff --git a/server/src/ide/filewatcher.rs b/server/src/ide/filewatcher.rs index 308056cc..3a524c8a 100644 --- a/server/src/ide/filewatcher.rs +++ b/server/src/ide/filewatcher.rs @@ -674,6 +674,7 @@ async fn processing_task( EditorMessageContents::Opened(_) | EditorMessageContents::OpenUrl(_) | + EditorMessageContents::Capture(_) | EditorMessageContents::ClientHtml(_) | EditorMessageContents::RequestClose => { let err = ResultErrTypes::ClientIllegalMessage; diff --git a/server/src/ide/vscode/tests.rs b/server/src/ide/vscode/tests.rs index 8d29298d..226ee88a 100644 --- a/server/src/ide/vscode/tests.rs +++ b/server/src/ide/vscode/tests.rs @@ -86,6 +86,8 @@ lazy_static! { // --------- /// The default port on which the server listens for incoming connections. pub const IP_PORT: u16 = 8080; +/// Allow slower CI runners enough time to start the shared test webserver. +const WEBSERVER_START_TIMEOUT: Duration = Duration::from_secs(6); // Support functions // ----------------- @@ -202,7 +204,7 @@ async fn _prep_test( let _ = &*WEBSERVER_HANDLE; let now = SystemTime::now(); let mut started = false; - while now.elapsed().unwrap().as_millis() < 500 { + while now.elapsed().unwrap() < WEBSERVER_START_TIMEOUT { if minreq::get(format!("http://127.0.0.1:{IP_PORT}/ping",)) .send() .is_ok_and(|response| response.as_bytes() == b"pong") diff --git a/server/src/main.rs b/server/src/main.rs index 21e504fb..99c06299 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -332,10 +332,15 @@ fn port_in_range(s: &str) -> Result { fn parse_credentials(s: &str) -> Result { // For simplicity, require a username to have no colons. - let split: Vec<_> = s.splitn(2, ":").collect(); + let Some((username, password)) = s.split_once(':') else { + return Err("auth must use the form username:password".to_string()); + }; + if username.is_empty() { + return Err("auth username may not be empty".to_string()); + } Ok(Credentials { - username: split[0].to_string(), - password: split[1].to_string(), + username: username.to_string(), + password: password.to_string(), }) } diff --git a/server/src/processing.rs b/server/src/processing.rs index dab63c57..60fa7cf2 100644 --- a/server/src/processing.rs +++ b/server/src/processing.rs @@ -39,6 +39,7 @@ use std::{ // ### Third-party use ammonia::Builder; use dprint_plugin_markdown::{ + FormatError, configuration::{ Configuration, ConfigurationBuilder, EmphasisKind, HeadingKind, StrongKind, TextWrap, UnorderedListKind, @@ -539,7 +540,7 @@ pub enum HtmlToMarkdownWrappedError { #[error("unable to convert from HTML to markdown")] HtmlToMarkdownFailed(#[from] std::io::Error), #[error("unable to word wrap Markdown")] - WordWrapFailed(#[from] anyhow::Error), + WordWrapFailed(#[from] FormatError), } impl HtmlToMarkdownWrapped { diff --git a/server/src/translation.rs b/server/src/translation.rs index c467b044..393a22ed 100644 --- a/server/src/translation.rs +++ b/server/src/translation.rs @@ -206,7 +206,13 @@ // ------- // // ### Standard library -use std::{collections::HashMap, ffi::OsStr, fmt::Debug, path::PathBuf, rc::Rc}; +use std::{ + collections::HashMap, + ffi::OsStr, + fmt::Debug, + path::{Path, PathBuf}, + rc::Rc, +}; use htmd::Node; // ### Third-party @@ -222,6 +228,7 @@ use tokio::{ // ### Local use crate::{ + capture::{CaptureContext, CaptureEventType, capture_control_only}, lexer::{CodeDocBlock, DocBlock, supported_languages::MARKDOWN_MODE}, processing::{ CodeChatForWeb, CodeMirror, CodeMirrorDiff, CodeMirrorDiffable, CodeMirrorDocBlock, @@ -235,8 +242,8 @@ use crate::{ CursorPosition, EditorMessage, EditorMessageContents, INITIAL_MESSAGE_ID, MESSAGE_ID_INCREMENT, ProcessingTaskHttpRequest, ProcessingTaskHttpRequestFlags, ResultErrTypes, ResultOkTypes, SimpleHttpResponse, SimpleHttpResponseError, - UpdateMessageContents, WebAppState, WebsocketQueues, file_to_response, path_to_url, - send_response, try_canonicalize, try_read_as_text, url_to_path, + UpdateMessageContents, WebAppState, WebsocketQueues, file_to_response, log_capture_event, + path_to_url, send_response, try_canonicalize, try_read_as_text, url_to_path, }, }; @@ -387,6 +394,7 @@ pub fn create_translation_queues( /// allows factoring out lengthy contents in the loop into subfunctions. struct TranslationTask { // These parameters are passed to us. + app_state: WebAppState, connection_id_raw: String, prefix: &'static [&'static str], allow_source_diffs: bool, @@ -435,6 +443,10 @@ struct TranslationTask { /// Has the full (non-diff) version of the current file been sent? Don't /// send diffs until this is sent. sent_full: bool, + /// Most recent capture metadata supplied by the IDE. Server-generated + /// capture events reuse this so translated write events retain the same + /// participant/session identity as the extension events. + capture_context: CaptureContext, } /// This is the processing task for the Visual Studio Code IDE. It handles all @@ -466,6 +478,7 @@ pub async fn translation_task( let mut continue_loop = true; let mut tt = TranslationTask { + app_state: app_state.clone(), connection_id_raw, prefix, allow_source_diffs, @@ -489,6 +502,7 @@ pub async fn translation_task( version: 0.0, // Don't send diffs until this is sent. sent_full: false, + capture_context: CaptureContext::default(), }; while continue_loop { select! { @@ -515,6 +529,19 @@ pub async fn translation_task( EditorMessageContents::Result(_) => continue_loop = tt.ide_result(ide_message).await, EditorMessageContents::Update(_) => continue_loop = tt.ide_update(ide_message).await, + EditorMessageContents::Capture(capture_event) => { + // Capture messages affect both upload spooling and the + // translation-layer context used for future + // server-classified write events. + let control_only = capture_control_only(&capture_event); + tt.capture_context.update_from_wire(&capture_event); + if control_only { + debug!("Updated capture context from control-only IDE event."); + } else { + log_capture_event(&app_state, *capture_event); + } + send_response(&tt.to_ide_tx, ide_message.id, Ok(ResultOkTypes::Void)).await; + }, // Update the current file; translate it to a URL then // pass it to the Client. @@ -610,6 +637,18 @@ pub async fn translation_task( }, EditorMessageContents::Update(_) => continue_loop = tt.client_update(client_message).await, + EditorMessageContents::Capture(capture_event) => { + // Same capture handling as IDE messages: update the + // context first, then store only non-control events. + let control_only = capture_control_only(&capture_event); + tt.capture_context.update_from_wire(&capture_event); + if control_only { + debug!("Updated capture context from control-only Client event."); + } else { + log_capture_event(&app_state, *capture_event); + } + send_response(&tt.to_client_tx, client_message.id, Ok(ResultOkTypes::Void)).await; + }, // Update the current file; translate it to a URL then // pass it to the IDE. @@ -700,6 +739,122 @@ pub async fn translation_task( // These provide translation for messages passing through the Server. impl TranslationTask { + fn capture_file_path(file_path: &Path) -> Option { + file_path.to_str().map(str::to_string) + } + + fn log_server_capture_event( + &mut self, + event_type: CaptureEventType, + file_path: &Path, + data: serde_json::Value, + ) { + let Some(capture_event) = self.capture_context.capture_event( + event_type, + Self::capture_file_path(file_path), + data, + ) else { + debug!("Skipping server-classified capture event; capture identity is not known yet."); + return; + }; + log_capture_event(&self.app_state, capture_event); + } + + fn log_raw_write_event(&mut self, file_path: &Path, after: &str) { + let before = self.source_code.as_str(); + if before == after { + return; + } + let code_diff = diff_str(before, after); + self.log_server_capture_event( + CaptureEventType::WriteCode, + file_path, + serde_json::json!({ + "source": "server_translation", + "classification_basis": "raw_text", + "diff": &code_diff, + }), + ); + } + + fn log_code_mirror_write_events( + &mut self, + file_path: &Path, + metadata: &SourceFileMetadata, + after: &CodeMirror, + after_source: Option<&str>, + source: &str, + ) { + if metadata.mode == MARKDOWN_MODE { + let markdown_diff = { + let before_source = self.source_code.as_str(); + let after_source = after_source.unwrap_or(&after.doc); + (before_source != after_source).then(|| diff_str(before_source, after_source)) + }; + if let Some(diff) = markdown_diff { + self.log_server_capture_event( + CaptureEventType::WriteDoc, + file_path, + serde_json::json!({ + "source": source, + "classification_basis": "markdown_source", + "mode": metadata.mode, + "diff": diff, + }), + ); + } + return; + } + + let code_diff = { + let before_doc = self.code_mirror_doc.as_str(); + (before_doc != after.doc).then(|| diff_str(before_doc, &after.doc)) + }; + let (doc_blocks_changed, doc_block_count_before, doc_block_diff) = { + let before_doc_blocks = self.code_mirror_doc_blocks.as_ref(); + let doc_blocks_changed = match before_doc_blocks { + Some(before) => !doc_blocks_compare(before, &after.doc_blocks), + None => !after.doc_blocks.is_empty(), + }; + let doc_block_diff = before_doc_blocks.map(|before| { + serde_json::json!(diff_code_mirror_doc_blocks(before, &after.doc_blocks)) + }); + ( + doc_blocks_changed, + before_doc_blocks.map_or(0, Vec::len), + doc_block_diff, + ) + }; + + if let Some(diff) = code_diff { + self.log_server_capture_event( + CaptureEventType::WriteCode, + file_path, + serde_json::json!({ + "source": source, + "classification_basis": "codemirror_code_text", + "mode": metadata.mode, + "diff": &diff, + }), + ); + } + + if doc_blocks_changed { + self.log_server_capture_event( + CaptureEventType::WriteDoc, + file_path, + serde_json::json!({ + "source": source, + "classification_basis": "codemirror_doc_blocks", + "mode": metadata.mode, + "doc_block_count_before": doc_block_count_before, + "doc_block_count_after": after.doc_blocks.len(), + "doc_block_diff": doc_block_diff, + }), + ); + } + } + // Pass a `Result` message to the Client, unless it's a `LoadFile` result. async fn ide_result(&mut self, ide_message: EditorMessage) -> bool { let EditorMessageContents::Result(ref result) = ide_message.message else { @@ -895,6 +1050,15 @@ impl TranslationTask { else { panic!("Unexpected diff value."); }; + if self.capture_context.is_active() { + self.log_code_mirror_write_events( + &clean_file_path, + &ccfw.metadata, + code_mirror_translated, + Some(&code_mirror.doc), + "ide", + ); + } // Send a diff if possible. let client_contents = if self.sent_full { self.diff_code_mirror( @@ -940,6 +1104,12 @@ impl TranslationTask { Err(ResultErrTypes::TodoBinarySupport) } TranslationResultsString::Unknown => { + if self.capture_context.is_active() { + self.log_raw_write_event( + &clean_file_path, + &code_mirror.doc, + ); + } // Send the new raw contents. debug!("Sending translated contents to Client."); queue_send_func!(self.to_client_tx.send(EditorMessage { @@ -956,13 +1126,16 @@ impl TranslationTask { mode: "".to_string(), }, source: CodeMirrorDiffable::Plain(CodeMirror { - doc: code_mirror.doc, + doc: code_mirror.doc.clone(), doc_blocks: vec![] }), version: contents.version }), }), })); + self.source_code = code_mirror.doc; + self.code_mirror_doc = self.source_code.clone(); + self.code_mirror_doc_blocks = Some(vec![]); Ok(ResultOkTypes::Void) } TranslationResultsString::Toc(_) => { @@ -1045,12 +1218,21 @@ impl TranslationTask { // what we just received. This must be updated // before we can translate back to check for changes // (the next step). - let CodeMirrorDiffable::Plain(code_mirror) = cfw.source else { + let CodeMirrorDiffable::Plain(ref code_mirror) = cfw.source else { // TODO: support diffable! panic!("Diff not supported."); }; - self.code_mirror_doc = code_mirror.doc; - self.code_mirror_doc_blocks = Some(code_mirror.doc_blocks); + if self.capture_context.is_active() { + self.log_code_mirror_write_events( + &clean_file_path, + &cfw.metadata, + code_mirror, + Some(&new_source_code), + "client", + ); + } + self.code_mirror_doc = code_mirror.doc.clone(); + self.code_mirror_doc_blocks = Some(code_mirror.doc_blocks.clone()); // We may need to change this version if we send a // diff back to the Client. let mut cfw_version = cfw.version; @@ -1402,7 +1584,108 @@ fn debug_shorten(val: T) -> String { // ----- #[cfg(test)] mod tests { - use crate::{processing::CodeMirrorDocBlock, translation::doc_blocks_compare}; + use crate::{ + capture::{CaptureContext, CaptureEventType, CaptureEventWire, capture_control_only}, + processing::CodeMirrorDocBlock, + translation::doc_blocks_compare, + }; + + /// Minimal test helper for feeding lifecycle/control messages into the + /// translation-layer capture context. + fn capture_wire( + event_type: crate::capture::CaptureEventType, + data: serde_json::Value, + ) -> CaptureEventWire { + CaptureEventWire { + event_id: None, + sequence_number: None, + schema_version: Some(2), + user_id: "participant".to_string(), + session_id: Some("session".to_string()), + event_source: Some("vscode_extension".to_string()), + language_id: None, + file_path: None, + file_hash: None, + event_type, + client_tz_offset_min: Some(360), + data: Some(data), + } + } + + #[test] + fn capture_context_only_generates_events_while_active() { + let mut context = CaptureContext::default(); + // Without an active capture session, translated writes must be skipped. + assert!( + context + .capture_event(CaptureEventType::WriteCode, None, serde_json::json!({})) + .is_none() + ); + + context.update_from_wire(&capture_wire( + CaptureEventType::SessionStart, + serde_json::json!({ + "capture_active": true, + }), + )); + // A session_start activates server-side translated write capture. + assert!( + context + .capture_event(CaptureEventType::WriteCode, None, serde_json::json!({})) + .is_some() + ); + + context.update_from_wire(&capture_wire( + CaptureEventType::SessionEnd, + serde_json::json!({ + "capture_active": false, + }), + )); + // A session_end deactivates translated write capture so stale context + // cannot continue generating spooled capture events. + assert!( + context + .capture_event(CaptureEventType::WriteCode, None, serde_json::json!({})) + .is_none() + ); + } + + #[test] + fn capture_control_only_is_detected_from_data() { + // Control-only events are the extension's way to update server capture + // state without storing the stop signal as a normal event row. + let wire = capture_wire( + CaptureEventType::SessionEnd, + serde_json::json!({ + "capture_active": false, + "capture_control_only": true, + }), + ); + + assert!(capture_control_only(&wire)); + } + + #[test] + fn server_generated_capture_events_have_session_sequence_numbers() { + let mut context = CaptureContext::default(); + context.update_from_wire(&capture_wire( + CaptureEventType::SessionStart, + serde_json::json!({ + "capture_active": true, + }), + )); + let first = context + .capture_event(CaptureEventType::WriteCode, None, serde_json::json!({})) + .expect("active context should generate a capture event"); + let second = context + .capture_event(CaptureEventType::WriteDoc, None, serde_json::json!({})) + .expect("active context should generate a capture event"); + + assert_eq!(first.sequence_number, Some(1)); + assert_eq!(second.sequence_number, Some(2)); + assert_eq!(first.event_source.as_deref(), Some("server_translation")); + assert_eq!(second.event_source.as_deref(), Some("server_translation")); + } #[test] fn test_x1() { diff --git a/server/src/webserver.rs b/server/src/webserver.rs index e3c76a06..2831d061 100644 --- a/server/src/webserver.rs +++ b/server/src/webserver.rs @@ -38,6 +38,7 @@ use std::{ // ### Third-party use actix_files; + use actix_web::{ App, HttpRequest, HttpResponse, HttpServer, dev::{Server, ServerHandle, ServiceFactory, ServiceRequest}, @@ -47,6 +48,7 @@ use actix_web::{ middleware, web::{self, Data}, }; + use actix_web_httpauth::{extractors::basic::BasicAuth, middleware::HttpAuthentication}; use actix_ws::AggregatedMessage; use bytes::Bytes; @@ -95,6 +97,13 @@ use crate::{ }, }; +use crate::capture::{ + CaptureEvent, CaptureEventWire, CaptureStatus, EventCapture, generate_capture_event_id, + hash_capture_path, +}; + +use chrono::Utc; + // Data structures // --------------- // @@ -201,6 +210,8 @@ pub enum EditorMessageContents { // Server will determine the value if needed. Option, ), + /// Record an instrumentation event. Valid destinations: Server. + Capture(Box), // #### These messages may only be sent by the IDE. /// This is the first message sent when the IDE starts up. It may only be @@ -405,6 +416,8 @@ pub struct AppState { pub connection_id: Mutex>, /// The auth credentials if authentication is used. credentials: Option, + // Added to support capture - JDS - 11/2025 + pub capture: Option, } pub type WebAppState = web::Data; @@ -449,7 +462,7 @@ macro_rules! queue_send_func { // The timeout for a reply from a websocket, in ms. Use a short timeout to speed // up unit tests. pub const REPLY_TIMEOUT_MS: Duration = if cfg!(test) { - Duration::from_millis(500) + Duration::from_millis(2500) } else { Duration::from_millis(15000) }; @@ -567,6 +580,61 @@ async fn stop(app_state: WebAppState) -> HttpResponse { HttpResponse::NoContent().finish() } +/// Log a capture event if capture is enabled. +pub fn log_capture_event(app_state: &WebAppState, wire: CaptureEventWire) -> CaptureStatus { + if let Some(capture) = &app_state.capture { + let server_timestamp = Utc::now(); + // Default missing data to empty object + let data = wire.data.unwrap_or_else(|| serde_json::json!({})); + + let data = if data.is_object() { + data + } else { + serde_json::json!({ "value": data }) + }; + // Prefer hashing a raw local path on the server so all capture + // transports use the same path-to-hash rule. The raw path is not stored; + // `file_hash` remains only as a backward-compatible/server-originated + // alternative. + let file_hash = wire + .file_path + .as_deref() + .map(hash_capture_path) + .or(wire.file_hash); + + let event = CaptureEvent::with_columns( + Some( + wire.event_id + .unwrap_or_else(|| generate_capture_event_id("server")), + ), + wire.sequence_number, + wire.schema_version, + wire.user_id, + wire.session_id, + wire.event_source, + wire.language_id, + file_hash, + wire.event_type, + server_timestamp, + wire.client_tz_offset_min, + data, + ); + + capture.log(event); + capture.status() + } else { + CaptureStatus::disabled() + } +} + +pub fn capture_status(app_state: &WebAppState) -> CaptureStatus { + app_state + .capture + .as_ref() + .map(EventCapture::status) + .unwrap_or_else(CaptureStatus::disabled) +} + // Get the `mode` query parameter to determine `is_test_mode`; default to // `false`. pub fn get_test_mode(req: &HttpRequest) -> bool { @@ -1414,12 +1482,18 @@ pub fn setup_server( addr: &SocketAddr, credentials: Option, ) -> std::io::Result<(Server, Data)> { - // Connect to the Capture Database - //let _event_capture = EventCapture::new("config.json").await?; + let capture_spool_path = ROOT_PATH.lock().unwrap().join("capture-spool"); + setup_server_with_capture_spool(addr, credentials, capture_spool_path) +} +pub fn setup_server_with_capture_spool( + addr: &SocketAddr, + credentials: Option, + capture_spool_path: PathBuf, +) -> std::io::Result<(Server, Data)> { // Pre-load the bundled files before starting the webserver. let _ = &*BUNDLED_FILES_MAP; - let app_data = make_app_data(credentials); + let app_data = make_app_data_with_capture_spool(credentials, capture_spool_path); let app_data_server = app_data.clone(); let server = match HttpServer::new(move || { let auth = HttpAuthentication::with_fn(basic_validator); @@ -1492,6 +1566,28 @@ pub fn configure_logger(level: LevelFilter) -> Result<(), Box) -> WebAppState { + let capture_spool_path = ROOT_PATH.lock().unwrap().join("capture-spool"); + make_app_data_with_capture_spool(credentials, capture_spool_path) +} + +fn make_app_data_with_capture_spool( + credentials: Option, + capture_spool_path: PathBuf, +) -> WebAppState { + // Initialize capture with a durable local upload spool. The VS Code + // extension supplies the CaptureWebService endpoint and bearer token at + // runtime; this server never reads database credentials from disk or env. + let capture: Option = match EventCapture::new(capture_spool_path.clone()) { + Ok(ec) => { + info!("Capture: enabled with local upload spool at {capture_spool_path:?}"); + Some(ec) + } + Err(err) => { + warn!("Capture: failed to initialize local upload spool: {err}"); + None + } + }; + web::Data::new(AppState { server_handle: Mutex::new(None), filewatcher_next_connection_id: Mutex::new(0), @@ -1502,6 +1598,7 @@ pub fn make_app_data(credentials: Option) -> WebAppState { client_queues: Arc::new(Mutex::new(HashMap::new())), connection_id: Mutex::new(HashSet::new()), credentials, + capture, }) } diff --git a/server/tests/fixtures/overall_1/test_updates/test.py b/server/tests/fixtures/overall_1/test_client_updates/test.py similarity index 100% rename from server/tests/fixtures/overall_1/test_updates/test.py rename to server/tests/fixtures/overall_1/test_client_updates/test.py diff --git a/server/tests/fixtures/overall_1/test_updates/toc.md b/server/tests/fixtures/overall_1/test_client_updates/toc.md similarity index 100% rename from server/tests/fixtures/overall_1/test_updates/toc.md rename to server/tests/fixtures/overall_1/test_client_updates/toc.md diff --git a/server/tests/overall_1.rs b/server/tests/overall_1.rs index 9b223351..4eae324e 100644 --- a/server/tests/overall_1.rs +++ b/server/tests/overall_1.rs @@ -36,14 +36,14 @@ use std::{path::PathBuf, time::Duration}; use dunce::canonicalize; use indoc::indoc; use pretty_assertions::assert_eq; -use thirtyfour::{By, Key, WebDriver, error::WebDriverError}; -use tokio::time::sleep; +use thirtyfour::{ + By, Key, WebDriver, WebElement, error::WebDriverError, prelude::ElementQueryable, +}; // ### Local use crate::overall_common::{ CodeChatEditorServerLog, ExpectedMessages, TIMEOUT, assert_no_more_messages, beginning_of_line, - end_of_line, get_version, goto_line, optional_message, perform_loadfile, - select_codechat_iframe, + get_version, goto_line, optional_message, perform_loadfile, select_codechat_iframe, }; use code_chat_editor::{ lexer::supported_languages::MARKDOWN_MODE, @@ -714,13 +714,9 @@ async fn test_client_core( .await .unwrap(); - // Wait for the tests to run. - sleep(Duration::from_millis(3000)).await; - // Look for the test results. codechat_iframe.clone().enter_frame().await.unwrap(); - let mocha_results = driver.find(By::Css("#mocha-stats .result")).await.unwrap(); - assert_eq!(mocha_results.inner_html().await.unwrap(), "✓"); + wait_for_mocha_success(&driver).await.unwrap(); server_id -= MESSAGE_ID_INCREMENT; assert_eq!( @@ -737,9 +733,51 @@ async fn test_client_core( Ok(()) } -make_test!(test_updates, test_updates_core); +async fn wait_for_mocha_success(driver: &WebDriver) -> Result<(), WebDriverError> { + const MOCHA_TEST_TIMEOUT: Duration = Duration::from_millis(30000); + + let mocha_results = driver + .query(By::Css("#mocha-stats .result")) + .wait(MOCHA_TEST_TIMEOUT, Duration::from_millis(200)) + .with_filter(|element: WebElement| async move { + let result = element.inner_html().await?; + Ok(result == "✓" || result == "✖") + }) + .first() + .await?; + let result = mocha_results.inner_html().await?; + if result == "✓" { + Ok(()) + } else { + panic!( + "Browser Mocha tests failed:\n{}", + mocha_failure_text(driver).await + ); + } +} + +async fn mocha_failure_text(driver: &WebDriver) -> String { + let failures = driver + .find_all(By::Css("#mocha-report .fail")) + .await + .unwrap_or_default(); + let mut failure_texts = Vec::new(); + for failure in failures { + let text = failure.text().await.unwrap_or_default(); + if !text.trim().is_empty() { + failure_texts.push(text); + } + } + if failure_texts.is_empty() { + "Mocha reported a failure, but no failure details were found.".to_string() + } else { + failure_texts.join("\n\n") + } +} + +make_test!(test_client_updates, test_client_updates_core); -async fn test_updates_core( +async fn test_client_updates_core( codechat_server: CodeChatEditorServerLog, driver: WebDriver, test_dir: PathBuf, @@ -770,53 +808,52 @@ async fn test_updates_core( // Target the iframe containing the Client. select_codechat_iframe(&driver).await; - // Select the doc block and add to the line, causing a word wrap. + // Focus the doc block, then wait for the async handoff to the shared inline + // TinyMCE editor before typing. Otherwise WebDriver can type into the + // transient contenteditable div, moving the cursor without marking the doc + // block dirty on macOS Chrome. let contents_css = ".CodeChat-CodeMirror .CodeChat-doc-contents"; let doc_block_contents = driver.find(By::Css(contents_css)).await.unwrap(); doc_block_contents.click().await.unwrap(); - let mut client_id = INITIAL_CLIENT_MESSAGE_ID; - assert_eq!( - codechat_server.get_message_timeout(TIMEOUT).await.unwrap(), - EditorMessage { - id: client_id, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.clone(), - cursor_position: Some(CursorPosition::Line(1)), - scroll_position: Some(1.0), - is_re_translation: false, - contents: None, - }) - } - ); - codechat_server.send_result(client_id, None).await.unwrap(); - client_id += MESSAGE_ID_INCREMENT; + let doc_block_contents = driver + .query(By::Css( + ".CodeChat-CodeMirror #TinyMCE-inst:not(.CodeChat-doc-hidden)", + )) + .first() + .await + .unwrap(); - let doc_block_contents = driver.find(By::Css(contents_css)).await.unwrap(); - end_of_line(&doc_block_contents, " testing").await.unwrap(); + // Add to the line, causing a word wrap. + doc_block_contents + .send_keys(Key::End + " testing") + .await + .unwrap(); - // Get the next message, which could be a cursor update followed by a text + // Get the next message, which could be cursor updates followed by a text // update, or just the text update. - let mut msg = codechat_server.get_message_timeout(TIMEOUT).await.unwrap(); - if let EditorMessageContents::Update(ref update) = msg.message + let mut client_id = INITIAL_CLIENT_MESSAGE_ID; + let mut msg = codechat_server + .get_message_timeout(TIMEOUT) + .await + .expect("expected client update after editing doc block"); + while let EditorMessageContents::Update(ref update) = msg.message && update.contents.is_none() { - // Sometimes, we get just a cursor update. If so, verify this then wait - // for the text update. - assert_eq!( - msg, - EditorMessage { - id: client_id, - message: EditorMessageContents::Update(UpdateMessageContents { - file_path: path_str.clone(), - cursor_position: Some(CursorPosition::Line(1)), - scroll_position: Some(1.0), - is_re_translation: false, - contents: None, - }) - } + // Sometimes, we get cursor-only updates. If so, verify and acknowledge + // them, then keep waiting for the text update. + assert_eq!(msg.id, client_id); + assert_eq!(update.file_path, path_str); + assert!(!update.is_re_translation); + assert!( + update.cursor_position.is_some() || update.scroll_position.is_some(), + "cursor-only update must include cursor or scroll state: {msg:#?}", ); + codechat_server.send_result(client_id, None).await.unwrap(); client_id += MESSAGE_ID_INCREMENT; - msg = codechat_server.get_message_timeout(TIMEOUT).await.unwrap(); + msg = codechat_server + .get_message_timeout(TIMEOUT) + .await + .expect("expected content update after cursor-only update"); } // Verify the updated text. @@ -850,6 +887,7 @@ async fn test_updates_core( ); codechat_server.send_result(client_id, None).await.unwrap(); client_id += MESSAGE_ID_INCREMENT; + assert!(client_id >= 7.0); // The Server sends the Client a wrapped version of the text; the Client // replies with a Result(Ok). @@ -862,7 +900,6 @@ async fn test_updates_core( ); server_id += MESSAGE_ID_INCREMENT; - // After this, ID is 13. goto_line(&codechat_server, &driver, &mut client_id, &path_str, 4) .await .unwrap(); diff --git a/server/tests/overall_common/mod.rs b/server/tests/overall_common/mod.rs index be812320..2139e86e 100644 --- a/server/tests/overall_common/mod.rs +++ b/server/tests/overall_common/mod.rs @@ -237,8 +237,14 @@ impl ExpectedMessages { } } -// Time to wait for `ExpectedMessages`. -pub const TIMEOUT: Duration = Duration::from_millis(3000); +// Time to wait for browser/WebDriver-backed client-server messages. This +// matches the client-side response window and gives CI enough room for autosave +// and loadfile acknowledgements under matrix load. +pub const TIMEOUT: Duration = Duration::from_millis(15000); + +// Browser-backed tests share a single WebDriver endpoint. Safari on macOS CI is +// unreliable with overlapping sessions, so serialize the harness. +pub(crate) static WEB_DRIVER_TEST_LOCK: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(()); // ### Test harness // @@ -253,6 +259,7 @@ pub async fn harness< // The output from calling `prep_test_dir!()`. prep_test_dir: (TempDir, PathBuf), ) -> Result<(), Box> { + let _webdriver_test_lock = WEB_DRIVER_TEST_LOCK.lock().await; // Send log events to the tracing subscriber, since the code currently uses // a log-based framework. As below, ignore re-initialization errors. let _ = LogTracer::init(); @@ -355,11 +362,10 @@ pub async fn harness< // Report any errors produced when removing the temporary directory. temp_dir.close()?; - ret.unwrap_or_else(|err| - // Convert a panic to an error. - Err::<(), Box>(Box::from(format!( - "{err:#?}" - )))) + ret.unwrap_or_else( + // Convert a panic to an error. + |err| Err::<(), Box>(Box::from(format!("{err:#?}"))), + ) } /// Decode a `BrowserLogEntry::message` produced by a `console.*` call. diff --git a/test_utils/Cargo.lock b/test_utils/Cargo.lock index b4462133..822cd10f 100644 --- a/test_utils/Cargo.lock +++ b/test_utils/Cargo.lock @@ -43,15 +43,15 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.13.0" +version = "2.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" [[package]] name = "bstr" -version = "1.12.3" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cee35f73844aa3014bb606320a6c1f010249dbdf43342fe54b5a4f6a8ed4b79" +checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" dependencies = [ "memchr", "serde_core", @@ -100,15 +100,15 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.4.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" [[package]] name = "globset" -version = "0.4.18" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3" +checksum = "e47d37d2ae4464254884b60ab7071be2b876a9c35b696bd018ddcc76847309cd" dependencies = [ "aho-corasick", "bstr", @@ -130,9 +130,9 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.28" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2adf14691c72bcfc1058740436a35bdd3ae9c07d1a941ef00b749e9ea16aefa7" +checksum = "7b009b6744c1445efd7244084e25e498636412effb6760b55067553baa925cc7" dependencies = [ "crossbeam-deque", "globset", @@ -203,27 +203,27 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" dependencies = [ "proc-macro2", ] [[package]] name = "regex" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick", "memchr", @@ -233,9 +233,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.15" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick", "memchr", @@ -272,18 +272,18 @@ dependencies = [ [[package]] name = "serde_core" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.228" +version = "1.0.229" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", @@ -292,9 +292,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.118" +version = "3.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" dependencies = [ "proc-macro2", "quote", diff --git a/test_utils/src/test_utils.rs b/test_utils/src/test_utils.rs index f764323d..44541001 100644 --- a/test_utils/src/test_utils.rs +++ b/test_utils/src/test_utils.rs @@ -20,9 +20,10 @@ // ------- // // ### Standard library -use std::env; -use std::path::MAIN_SEPARATOR_STR; -use std::path::PathBuf; +use std::{ + env, fs, + path::{MAIN_SEPARATOR_STR, PathBuf}, +}; // ### Third-party use assert_fs::{TempDir, fixture::PathCopy}; @@ -132,23 +133,32 @@ pub fn prep_test_dir_impl( let source_path_tmp = source_path.clone(); let test_name = source_path_tmp.file_name().unwrap().to_str().unwrap(); source_path.pop(); - // For debugging, append // [.into\_persistent()](https://docs.rs/assert_fs/latest/assert_fs/fixture/struct.TempDir.html#method.into_persistent). let temp_dir = TempDir::new().unwrap(); - // Create a temporary directory, then copy everything needed for this test - // to it. Since the `patterns` parameter is a glob, append `/**` to the - // directory to copy to get all files/subdirectories. - if let Err(err) = temp_dir.copy_from(&source_path, &[format!("{test_name}/**")]) { - panic!( - "Unable to copy files from {}{MAIN_SEPARATOR_STR}{}: {err}", - source_path.to_string_lossy(), - test_name - ); - } - // This is a path where testing takes place. let test_dir = temp_dir.path().join(test_name); + let fixture_dir = source_path.join(test_name); + if fixture_dir.is_dir() { + // Create a temporary directory, then copy everything needed for this test + // to it. Since the `patterns` parameter is a glob, append `/**` to the + // directory to copy to get all files/subdirectories. + if let Err(err) = temp_dir.copy_from(&source_path, &[format!("{test_name}/**")]) { + panic!( + "Unable to copy files from {}{MAIN_SEPARATOR_STR}{}: {err}", + source_path.to_string_lossy(), + test_name + ); + } + } else { + // Some tests only need an isolated empty directory, with no fixture files. + fs::create_dir_all(&test_dir).unwrap_or_else(|err| { + panic!( + "Unable to create empty test directory {}: {err}", + test_dir.to_string_lossy() + ) + }); + } (temp_dir, test_dir) } diff --git a/toc.md b/toc.md index 2d11d4d5..e2baa292 100644 --- a/toc.md +++ b/toc.md @@ -30,6 +30,7 @@ Implementation * [python.pest](server/src/lexer/pest/python.pest) * [webserver.rs](server/src/webserver.rs) * [log4rs.yml](server/log4rs.yml) + * [Capture events schema](server/scripts/capture_events_schema.sql) * [ide.rs](server/src/ide.rs) * [filewatcher.rs](server/src/ide/filewatcher.rs) * [vscode.rs](server/src/ide/vscode.rs)