diff --git a/.github/workflows/production-publish-images.yml b/.github/workflows/production-publish-images.yml new file mode 100644 index 0000000000..5e4315b3e9 --- /dev/null +++ b/.github/workflows/production-publish-images.yml @@ -0,0 +1,60 @@ +name: Build and Publish Images to ECR + +on: + push: + branches: + - self-hosting-setup #remove + - main + +concurrency: + group: production-publish-images + +jobs: + publish_images: + runs-on: ubuntu-latest-8-cores + strategy: + matrix: + service: [multiplayer, files, connection, client, api] + steps: + - uses: actions/checkout@v4 + + - name: Configure AWS Credentials + uses: aws-actions/configure-aws-credentials@v4 + with: + aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} + aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + aws-region: us-east-1 + + - name: Login to Amazon ECR Public + id: login-ecr + uses: aws-actions/amazon-ecr-login@v2 + with: + registry-type: public + + - name: Define repository name + id: repo-name + run: | + echo "REPO_NAME=quadratic-${{ matrix.service }}" >> $GITHUB_OUTPUT + + - name: Create Public ECR Repository if not exists + id: create-ecr + env: + REPO_NAME: ${{ steps.repo-name.outputs.REPO_NAME }} + run: | + aws ecr-public create-repository --repository-name $REPO_NAME || true + REPO_INFO=$(aws ecr-public describe-repositories --repository-names $REPO_NAME) + ECR_URL=$(echo $REPO_INFO | jq -r '.repositories[0].repositoryUri') + echo "ECR_URL=$ECR_URL" >> $GITHUB_OUTPUT + + - name: Read VERSION file + id: version + run: echo "VERSION=$(cat VERSION)" >> $GITHUB_OUTPUT + + - name: Build, Tag, and Push Image to Amazon ECR Public + env: + ECR_URL: ${{ steps.create-ecr.outputs.ECR_URL }} + IMAGE_TAG: ${{ steps.version.outputs.VERSION }} + run: | + docker build -t $ECR_URL:$IMAGE_TAG -t $ECR_URL:latest -f quadratic-${{ matrix.service }}/Dockerfile . + docker push $ECR_URL:$IMAGE_TAG + docker push $ECR_URL:latest \ No newline at end of file diff --git a/.gitignore b/.gitignore index fabad2af45..f11f767e7e 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,7 @@ quadratic-connection/target/ quadratic-core/target/ quadratic-core/tmp.txt quadratic-files/target/ +quadratic-files/storage quadratic-multiplayer/target/ quadratic-multiplayer/updateAlertVersion.json @@ -64,6 +65,7 @@ docker/mysql/data docker/postgres/data docker/redis/data docker/static/html +docker/file-storage docker/postgres-connection/data docker/mysql-connection/data docker/snowflake-connection/data diff --git a/Cargo.lock b/Cargo.lock index f68f62fa9b..e9382f8814 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,18 +10,29 @@ checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" [[package]] name = "addr2line" -version = "0.22.0" +version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" dependencies = [ "gimli", ] [[package]] -name = "adler" -version = "1.0.2" +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aes" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] [[package]] name = "ahash" @@ -105,9 +116,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.86" +version = "1.0.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" +checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arrayvec" @@ -144,23 +155,23 @@ dependencies = [ [[package]] name = "arrow" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45aef0d9cf9a039bf6cd1acc451b137aca819977b0928dece52bd92811b640ba" +checksum = "a9ba0d7248932f4e2a12fb37f0a2e3ec82b3bdedbac2a1dce186e036843b8f8c" dependencies = [ - "arrow-arith 53.0.0", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-csv 53.0.0", - "arrow-data 53.0.0", - "arrow-ipc 53.0.0", - "arrow-json 53.0.0", - "arrow-ord 53.0.0", - "arrow-row 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", - "arrow-string 53.0.0", + "arrow-arith 53.1.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-cast 53.1.0", + "arrow-csv 53.1.0", + "arrow-data 53.1.0", + "arrow-ipc 53.1.0", + "arrow-json 53.1.0", + "arrow-ord 53.1.0", + "arrow-row 53.1.0", + "arrow-schema 53.1.0", + "arrow-select 53.1.0", + "arrow-string 53.1.0", ] [[package]] @@ -180,14 +191,14 @@ dependencies = [ [[package]] name = "arrow-arith" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03675e42d1560790f3524800e41403b40d0da1c793fe9528929fde06d8c7649a" +checksum = "d60afcdc004841a5c8d8da4f4fa22d64eb19c0c01ef4bcedd77f175a7cf6e38f" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "chrono", "half", "num", @@ -211,14 +222,14 @@ dependencies = [ [[package]] name = "arrow-array" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd2bf348cf9f02a5975c5962c7fa6dee107a2009a7b41ac5fb1a027e12dc033f" +checksum = "7f16835e8599dbbb1659fd869d865254c4cf32c6c2bb60b6942ac9fc36bfa5da" dependencies = [ "ahash 0.8.11", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "chrono", "half", "hashbrown 0.14.5", @@ -238,9 +249,9 @@ dependencies = [ [[package]] name = "arrow-buffer" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3092e37715f168976012ce52273c3989b5793b0db5f06cbaa246be25e5f0924d" +checksum = "1a1f34f0faae77da6b142db61deba2cb6d60167592b178be317b341440acba80" dependencies = [ "bytes", "half", @@ -262,28 +273,28 @@ dependencies = [ "base64 0.22.1", "chrono", "half", - "lexical-core", + "lexical-core 0.8.5", "num", "ryu", ] [[package]] name = "arrow-cast" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ce1018bb710d502f9db06af026ed3561552e493e989a79d0d0f5d9cf267a785" +checksum = "450e4abb5775bca0740bec0bcf1b1a5ae07eff43bd625661c4436d8e8e4540c4" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", + "arrow-select 53.1.0", "atoi", "base64 0.22.1", "chrono", "comfy-table", "half", - "lexical-core", + "lexical-core 1.0.2", "num", "ryu", ] @@ -303,26 +314,26 @@ dependencies = [ "csv", "csv-core", "lazy_static", - "lexical-core", + "lexical-core 0.8.5", "regex", ] [[package]] name = "arrow-csv" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd178575f45624d045e4ebee714e246a05d9652e41363ee3f57ec18cca97f740" +checksum = "d3a4e4d63830a341713e35d9a42452fbc6241d5f42fa5cf6a4681b8ad91370c4" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-cast 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "chrono", "csv", "csv-core", "lazy_static", - "lexical-core", + "lexical-core 1.0.2", "regex", ] @@ -340,12 +351,12 @@ dependencies = [ [[package]] name = "arrow-data" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4ac0c4ee79150afe067dc4857154b3ee9c1cd52b5f40d59a77306d0ed18d65" +checksum = "2b1e618bbf714c7a9e8d97203c806734f012ff71ae3adc8ad1b075689f540634" dependencies = [ - "arrow-buffer 53.0.0", - "arrow-schema 53.0.0", + "arrow-buffer 53.1.0", + "arrow-schema 53.1.0", "half", "num", ] @@ -366,15 +377,15 @@ dependencies = [ [[package]] name = "arrow-ipc" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb307482348a1267f91b0912e962cd53440e5de0f7fb24c5f7b10da70b38c94a" +checksum = "f98e983549259a2b97049af7edfb8f28b8911682040e99a94e4ceb1196bd65c2" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-cast 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "flatbuffers 24.3.25", ] @@ -391,8 +402,8 @@ dependencies = [ "arrow-schema 51.0.0", "chrono", "half", - "indexmap 2.3.0", - "lexical-core", + "indexmap 2.6.0", + "lexical-core 0.8.5", "num", "serde", "serde_json", @@ -400,19 +411,19 @@ dependencies = [ [[package]] name = "arrow-json" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24805ba326758effdd6f2cbdd482fcfab749544f21b134701add25b33f474e6" +checksum = "b198b9c6fcf086501730efbbcb483317b39330a116125af7bb06467d04b352a3" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-cast 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "chrono", "half", - "indexmap 2.3.0", - "lexical-core", + "indexmap 2.6.0", + "lexical-core 1.0.2", "num", "serde", "serde_json", @@ -435,15 +446,15 @@ dependencies = [ [[package]] name = "arrow-ord" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644046c479d80ae8ed02a7f1e1399072ea344ca6a7b0e293ab2d5d9ed924aa3b" +checksum = "2427f37b4459a4b9e533045abe87a5183a5e0995a3fc2c2fd45027ae2cc4ef3f" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", + "arrow-select 53.1.0", "half", "num", ] @@ -465,15 +476,15 @@ dependencies = [ [[package]] name = "arrow-row" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a29791f8eb13b340ce35525b723f5f0df17ecb955599e11f65c2a94ab34e2efb" +checksum = "15959657d92e2261a7a323517640af87f5afd9fd8a6492e424ebee2203c567f6" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "half", ] @@ -485,9 +496,9 @@ checksum = "02d9483aaabe910c4781153ae1b6ae0393f72d9ef757d38d09d450070cf2e528" [[package]] name = "arrow-schema" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85320a3a2facf2b2822b57aa9d6d9d55edb8aee0b6b5d3b8df158e503d10858" +checksum = "fbf0388a18fd7f7f3fe3de01852d30f54ed5182f9004db700fbe3ba843ed2794" [[package]] name = "arrow-select" @@ -505,15 +516,15 @@ dependencies = [ [[package]] name = "arrow-select" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cc7e6b582e23855fd1625ce46e51647aa440c20ea2e71b1d748e0839dd73cba" +checksum = "b83e5723d307a38bf00ecd2972cd078d1339c7fd3eb044f609958a9a24463f3a" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", "num", ] @@ -531,24 +542,35 @@ dependencies = [ "memchr", "num", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] name = "arrow-string" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0775b6567c66e56ded19b87a954b6b1beffbdd784ef95a3a2b03f59570c1d230" +checksum = "7ab3db7c09dd826e74079661d84ed01ed06547cf75d52c2818ef776d0d852305" dependencies = [ - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-data 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-data 53.1.0", + "arrow-schema 53.1.0", + "arrow-select 53.1.0", "memchr", "num", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", +] + +[[package]] +name = "assert-json-diff" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4259cbe96513d2f1073027a259fc2ca917feb3026a5a8d984e3628e490255cc0" +dependencies = [ + "extend", + "serde", + "serde_json", ] [[package]] @@ -567,7 +589,7 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "syn 1.0.109", ] @@ -596,9 +618,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.12" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fec134f64e2bc57411226dfc4e52dec859ddfc7e711fc5e07b612584f000e4aa" +checksum = "0cb8f1d480b0ea3783ab015936d2a55c87e219676f0c0b7dec61494043f21857" dependencies = [ "flate2", "futures-core", @@ -753,9 +775,9 @@ dependencies = [ [[package]] name = "async-stream" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" dependencies = [ "async-stream-impl", "futures-core", @@ -764,13 +786,13 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -781,13 +803,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.81" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -831,15 +853,15 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "aws-config" -version = "1.5.4" +version = "1.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf6cfe2881cb1fcbba9ae946fb9a6480d3b7a714ca84c74925014a89ef3387a" +checksum = "7198e6f03240fdceba36656d8be440297b6b82270325908c7381f37d826a74f6" dependencies = [ "aws-credential-types", "aws-runtime", @@ -857,7 +879,6 @@ dependencies = [ "fastrand", "hex", "http 0.2.12", - "hyper 0.14.30", "ring", "time", "tokio", @@ -868,9 +889,9 @@ dependencies = [ [[package]] name = "aws-credential-types" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16838e6c9e12125face1c1eff1343c75e3ff540de98ff7ebd61874a89bcfeb9" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" dependencies = [ "aws-smithy-async", "aws-smithy-runtime-api", @@ -880,9 +901,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f95446d919226d587817a7d21379e6eb099b97b45110a7f272a444ca5c54070" +checksum = "cdd82dba44d209fddb11c190e0a94b78651f95299598e472215667417a03ff1d" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -892,9 +913,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.21.2" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3ddc4a5b231dd6958b140ff3151b6412b3f4321fab354f399eec8f14b06df62" +checksum = "df7a4168111d7eb622a31b214057b8509c0a7e1794f44c546d742330dc793972" dependencies = [ "bindgen", "cc", @@ -907,15 +928,16 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.3.1" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87c5f920ffd1e0526ec9e70e50bf444db50b204395a0fa7016bbf9e31ea1698f" +checksum = "a10d5c055aa540164d9561a0e2e74ad30f0dcf7393c3a92f6733ddf9c5762468" dependencies = [ "aws-credential-types", "aws-sigv4", "aws-smithy-async", "aws-smithy-eventstream", "aws-smithy-http", + "aws-smithy-runtime", "aws-smithy-runtime-api", "aws-smithy-types", "aws-types", @@ -923,6 +945,7 @@ dependencies = [ "fastrand", "http 0.2.12", "http-body 0.4.6", + "once_cell", "percent-encoding", "pin-project-lite", "tracing", @@ -931,9 +954,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.42.0" +version = "1.57.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "558bbcec8db82a1a8af1610afcb3b10d00652d25ad366a0558eecdff2400a1d1" +checksum = "8888c238bf93c77c5df8274b3999fd7fc1bb3fb658616f40dfde9e4fcd9efd94" dependencies = [ "ahash 0.8.11", "aws-credential-types", @@ -966,9 +989,9 @@ dependencies = [ [[package]] name = "aws-sdk-sso" -version = "1.36.0" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6acca681c53374bf1d9af0e317a41d12a44902ca0f2d1e10e5cb5bb98ed74f35" +checksum = "0dc2faec3205d496c7e57eff685dd944203df7ce16a4116d0281c44021788a7b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -988,9 +1011,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.37.0" +version = "1.47.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79c6bdfe612503a526059c05c9ccccbf6bd9530b003673cb863e547fd7c0c9a" +checksum = "c93c241f52bc5e0476e259c953234dab7e2a35ee207ee202e86c0095ec4951dc" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1010,9 +1033,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.36.0" +version = "1.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e6ecdb2bd756f3b2383e6f0588dc10a4e65f5d551e70a56e0bfe0c884673ce" +checksum = "b259429be94a3459fa1b00c5684faee118d74f9577cc50aebadc36e507c63b5f" dependencies = [ "aws-credential-types", "aws-runtime", @@ -1033,9 +1056,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.2.3" +version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +checksum = "cc8db6904450bafe7473c6ca9123f88cc11089e41a025408f992db4e22d3be68" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -1073,9 +1096,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.60.11" +version = "0.60.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c4134cf3adaeacff34d588dbe814200357b0c466d730cf1c0d8054384a2de4" +checksum = "598b1689d001c4d4dc3cb386adb07d37786783aee3ac4b324bcadac116bf3d23" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -1094,9 +1117,9 @@ dependencies = [ [[package]] name = "aws-smithy-eventstream" -version = "0.60.4" +version = "0.60.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6363078f927f612b970edf9d1903ef5cef9a64d1e8423525ebb1f0a1633c858" +checksum = "cef7d0a272725f87e51ba2bf89f8c21e4df61b9e49ae1ac367a6d69916ef7c90" dependencies = [ "aws-smithy-types", "bytes", @@ -1105,9 +1128,9 @@ dependencies = [ [[package]] name = "aws-smithy-http" -version = "0.60.9" +version = "0.60.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cd0ae3d97daa0a2bf377a4d8e8e1362cae590c4a1aad0d40058ebca18eb91e" +checksum = "5c8bc3e8fdc6b8d07d976e301c02fe553f72a39b7a9fea820e023268467d7ab6" dependencies = [ "aws-smithy-eventstream", "aws-smithy-runtime-api", @@ -1133,6 +1156,25 @@ dependencies = [ "aws-smithy-types", ] +[[package]] +name = "aws-smithy-protocol-test" +version = "0.63.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b92b62199921f10685c6b588fdbeb81168ae4e7950ae3e5f50145a01bb5f1ad" +dependencies = [ + "assert-json-diff 1.1.0", + "aws-smithy-runtime-api", + "base64-simd", + "cbor-diag", + "ciborium", + "http 0.2.12", + "pretty_assertions", + "regex-lite", + "roxmltree", + "serde_json", + "thiserror", +] + [[package]] name = "aws-smithy-query" version = "0.60.7" @@ -1145,12 +1187,13 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.6.2" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce87155eba55e11768b8c1afa607f3e864ae82f03caf63258b37455b0ad02537" +checksum = "a065c0fe6fdbdf9f11817eb68582b2ab4aff9e9c39e986ae48f7ec576c6322db" dependencies = [ "aws-smithy-async", "aws-smithy-http", + "aws-smithy-protocol-test", "aws-smithy-runtime-api", "aws-smithy-types", "bytes", @@ -1160,21 +1203,25 @@ dependencies = [ "http-body 0.4.6", "http-body 1.0.1", "httparse", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-rustls 0.24.2", + "indexmap 2.6.0", "once_cell", "pin-project-lite", "pin-utils", "rustls 0.21.12", + "serde", + "serde_json", "tokio", "tracing", + "tracing-subscriber", ] [[package]] name = "aws-smithy-runtime-api" -version = "1.7.1" +version = "1.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30819352ed0a04ecf6a2f3477e344d2d1ba33d43e0f09ad9047c12e0d923616f" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" dependencies = [ "aws-smithy-async", "aws-smithy-types", @@ -1189,9 +1236,9 @@ dependencies = [ [[package]] name = "aws-smithy-types" -version = "1.2.0" +version = "1.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe321a6b21f5d8eabd0ade9c55d3d0335f3c3157fc2b3e87f05f34b539e4df5" +checksum = "147100a7bea70fa20ef224a6bad700358305f5dc0f84649c53769761395b355b" dependencies = [ "base64-simd", "bytes", @@ -1215,9 +1262,9 @@ dependencies = [ [[package]] name = "aws-smithy-xml" -version = "0.60.8" +version = "0.60.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +checksum = "ab0b0166827aa700d3dc519f72f8b3a91c35d0b8d042dc5d643a91e6f80648fc" dependencies = [ "xmlparser", ] @@ -1238,19 +1285,20 @@ dependencies = [ [[package]] name = "axum" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" dependencies = [ "async-trait", "axum-core", - "base64 0.21.7", + "axum-macros", + "base64 0.22.1", "bytes", "futures-util", "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "itoa", "matchit", @@ -1266,8 +1314,8 @@ dependencies = [ "sha1", "sync_wrapper 1.0.1", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.24.0", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -1275,9 +1323,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -1288,7 +1336,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper 1.0.1", "tower-layer", "tower-service", "tracing", @@ -1296,9 +1344,9 @@ dependencies = [ [[package]] name = "axum-extra" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" dependencies = [ "axum", "axum-core", @@ -1311,7 +1359,7 @@ dependencies = [ "mime", "pin-project-lite", "serde", - "tower", + "tower 0.5.1", "tower-layer", "tower-service", "tracing", @@ -1319,29 +1367,28 @@ dependencies = [ [[package]] name = "axum-macros" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c055ee2d014ae5981ce1016374e8213682aa14d9bf40e48ab48b5f3ef20eaa" +checksum = "57d123550fa8d071b7255cb0cc04dc302baa6c8c4a79f55701552684d8399bce" dependencies = [ - "heck 0.4.1", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] name = "backtrace" -version = "0.3.73" +version = "0.3.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" dependencies = [ "addr2line", - "cc", "cfg-if", "libc", "miniz_oxide", "object", "rustc-demangle", + "windows-targets 0.52.6", ] [[package]] @@ -1414,24 +1461,24 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.69.4" +version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ "bitflags 2.6.0", "cexpr", "clang-sys", - "itertools 0.10.5", + "itertools 0.12.1", "lazy_static", "lazycell", "log", "prettyplease", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "regex", "rustc-hash 1.1.0", "shlex", - "syn 2.0.79", + "syn 2.0.83", "which", ] @@ -1486,6 +1533,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + [[package]] name = "blocking" version = "1.6.1" @@ -1517,12 +1573,21 @@ checksum = "c3ef8005764f53cd4dca619f5bf64cafd4664dada50ece25e4d81de54c80cc0b" dependencies = [ "once_cell", "proc-macro-crate", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", "syn_derive", ] +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -1546,8 +1611,8 @@ version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -1565,9 +1630,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" +checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da" dependencies = [ "serde", ] @@ -1605,14 +1670,43 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cbor-diag" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc245b6ecd09b23901a4fbad1ad975701fd5061ceaef6afa93a2d70605a64429" +dependencies = [ + "bs58", + "chrono", + "data-encoding", + "half", + "nom", + "num-bigint", + "num-rational", + "num-traits", + "separator", + "url", + "uuid", +] + [[package]] name = "cc" -version = "1.1.7" +version = "1.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26a5c3fd7bfa1ce3897a3a3501d362b2d87b7f2583ebcb4a949ec25911025cbc" +checksum = "c2e7962b54006dcfcc61cb72735f4d89bb97061dd6a7ed882ec6b8ee53714c6f" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -1678,6 +1772,16 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1844,15 +1948,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" dependencies = [ "libc", ] @@ -2035,10 +2139,10 @@ checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" dependencies = [ "fnv", "ident_case", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "strsim", - "syn 2.0.79", + "syn 2.0.83", ] [[package]] @@ -2048,8 +2152,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" dependencies = [ "darling_core", - "quote 1.0.36", - "syn 2.0.79", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -2122,6 +2226,12 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "339544cc9e2c4dc3fc7149fd630c5f22263a4fdf18a98afd0075784968b5cf00" +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "digest" version = "0.10.7" @@ -2148,14 +2258,14 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "dummy" -version = "0.7.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e57e12b69e57fad516e01e2b3960f122696fdb13420e1a88ed8e210316f2876" +checksum = "1cac124e13ae9aa56acc4241f8c8207501d93afdd8d8e62f0c1f2e12f6508c65" dependencies = [ "darling", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -2229,9 +2339,9 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de0d48a183585823424a4ce1aa132d174a6a81bd540895822eb4c8373a8e49e8" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -2297,11 +2407,23 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "extend" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f47da3a72ec598d9c8937a7ebca8962a5c7a1f28444e38c2b33c771ba3f55f05" +dependencies = [ + "proc-macro-error", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 1.0.109", +] + [[package]] name = "fake" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c25829bde82205da46e1823b2259db6273379f626fc211f126f65654a2669be" +checksum = "2d391ba4af7f1d93f01fcf7b2f29e2bc9348e109dfdbf4dcbdc51dfa38dab0b6" dependencies = [ "deunicode", "dummy", @@ -2310,9 +2432,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.1.0" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" [[package]] name = "ff" @@ -2346,9 +2468,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.0.31" +version = "1.0.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" +checksum = "a1b589b4dc103969ad3cf85c950899926ec64300a1a46d76c03a6072957036f0" dependencies = [ "crc32fast", "miniz_oxide", @@ -2356,9 +2478,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55ac459de2512911e4b674ce33cf20befaba382d05b62b008afc1c8b57cbf181" +checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095" dependencies = [ "futures-core", "futures-sink", @@ -2371,6 +2493,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2409,9 +2537,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" [[package]] name = "futures" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" dependencies = [ "futures-channel", "futures-core", @@ -2424,9 +2552,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -2434,15 +2562,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -2462,9 +2590,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -2481,26 +2609,26 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -2510,9 +2638,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", @@ -2562,9 +2690,9 @@ dependencies = [ [[package]] name = "gimli" -version = "0.29.0" +version = "0.31.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "glob" @@ -2607,7 +2735,7 @@ dependencies = [ "futures-sink", "futures-util", "http 0.2.12", - "indexmap 2.3.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2626,7 +2754,7 @@ dependencies = [ "futures-core", "futures-sink", "http 1.1.0", - "indexmap 2.3.0", + "indexmap 2.6.0", "slab", "tokio", "tokio-util", @@ -2663,6 +2791,17 @@ dependencies = [ "allocator-api2", ] +[[package]] +name = "hashbrown" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e087f84d4f86bf4b218b927129862374b72199ae7d8657835f1e89000eea4fb" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + [[package]] name = "hashlink" version = "0.9.1" @@ -2832,9 +2971,9 @@ checksum = "08a397c49fec283e3d6211adbe480be95aae5f304cfb923e9970e08956d5168a" [[package]] name = "httparse" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" [[package]] name = "httpdate" @@ -2847,7 +2986,7 @@ name = "httpmock" version = "0.8.0-alpha.1" source = "git+https://github.com/quadratichq/httpmock#bd58822ed1261a0f04a6615b5ddb43b118f5fa69" dependencies = [ - "assert-json-diff", + "assert-json-diff 2.0.2", "async-object-pool", "async-std", "async-trait", @@ -2860,14 +2999,14 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "lazy_static", "log", "path-tree", "regex", - "rustls 0.23.13", + "rustls 0.23.15", "serde", "serde_json", "serde_regex", @@ -2888,9 +3027,9 @@ checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" [[package]] name = "hyper" -version = "0.14.30" +version = "0.14.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +checksum = "8c08302e8fa335b151b788c775ff56e7a03ae64ff85c548ee820fecb70356e85" dependencies = [ "bytes", "futures-channel", @@ -2912,9 +3051,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +checksum = "bbbff0a806a4728c99295b254c8838933b5b082d75e3cb70c8dab21fdfbcfa9a" dependencies = [ "bytes", "futures-channel", @@ -2939,7 +3078,7 @@ checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" dependencies = [ "futures-util", "http 0.2.12", - "hyper 0.14.30", + "hyper 0.14.31", "log", "rustls 0.21.12", "rustls-native-certs 0.6.3", @@ -2955,10 +3094,10 @@ checksum = "08afdbb5c31130e3034af566421053ab03787c640246a446327f550d11bcb333" dependencies = [ "futures-util", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "log", - "rustls 0.23.13", + "rustls 0.23.15", "rustls-native-certs 0.8.0", "rustls-pki-types", "tokio", @@ -2974,7 +3113,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" dependencies = [ "bytes", - "hyper 0.14.30", + "hyper 0.14.31", "native-tls", "tokio", "tokio-native-tls", @@ -2982,29 +3121,28 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.6" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab92f4f49ee4fb4f997c784b7a2e0fa70050211e0b6a287f898c3c9785ca956" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" dependencies = [ "bytes", "futures-channel", "futures-util", "http 1.1.0", "http-body 1.0.1", - "hyper 1.4.1", + "hyper 1.5.0", "pin-project-lite", "socket2", "tokio", - "tower", "tower-service", "tracing", ] [[package]] name = "iana-time-zone" -version = "0.1.60" +version = "0.1.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -3062,15 +3200,25 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.3.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" +checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da" dependencies = [ "equivalent", - "hashbrown 0.14.5", + "hashbrown 0.15.0", "serde", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -3091,9 +3239,9 @@ checksum = "8bb03732005da905c88227371639bf1ad885cc712789c011c31c5fb3ab3ccf02" [[package]] name = "ipnet" -version = "2.9.0" +version = "2.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708" [[package]] name = "itertools" @@ -3104,6 +3252,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itertools" version = "0.13.0" @@ -3130,9 +3287,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" dependencies = [ "wasm-bindgen", ] @@ -3182,11 +3339,24 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", + "lexical-parse-float 0.8.5", + "lexical-parse-integer 0.8.6", + "lexical-util 0.8.5", + "lexical-write-float 0.8.5", + "lexical-write-integer 0.8.5", +] + +[[package]] +name = "lexical-core" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0431c65b318a590c1de6b8fd6e72798c92291d27762d94c9e6c37ed7a73d8458" +dependencies = [ + "lexical-parse-float 1.0.2", + "lexical-parse-integer 1.0.2", + "lexical-util 1.0.3", + "lexical-write-float 1.0.2", + "lexical-write-integer 1.0.2", ] [[package]] @@ -3195,8 +3365,19 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" dependencies = [ - "lexical-parse-integer", - "lexical-util", + "lexical-parse-integer 0.8.6", + "lexical-util 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-parse-float" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb17a4bdb9b418051aa59d41d65b1c9be5affab314a872e5ad7f06231fb3b4e0" +dependencies = [ + "lexical-parse-integer 1.0.2", + "lexical-util 1.0.3", "static_assertions", ] @@ -3206,7 +3387,17 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" dependencies = [ - "lexical-util", + "lexical-util 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df98f4a4ab53bf8b175b363a34c7af608fe31f93cc1fb1bf07130622ca4ef61" +dependencies = [ + "lexical-util 1.0.3", "static_assertions", ] @@ -3219,14 +3410,34 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "lexical-util" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85314db53332e5c192b6bca611fb10c114a80d1b831ddac0af1e9be1b9232ca0" +dependencies = [ + "static_assertions", +] + [[package]] name = "lexical-write-float" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" dependencies = [ - "lexical-util", - "lexical-write-integer", + "lexical-util 0.8.5", + "lexical-write-integer 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e7c3ad4e37db81c1cbe7cf34610340adc09c322871972f74877a712abc6c809" +dependencies = [ + "lexical-util 1.0.3", + "lexical-write-integer 1.0.2", "static_assertions", ] @@ -3236,7 +3447,17 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" dependencies = [ - "lexical-util", + "lexical-util 0.8.5", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb89e9f6958b83258afa3deed90b5de9ef68eef090ad5086c791cd2345610162" +dependencies = [ + "lexical-util 1.0.3", "static_assertions", ] @@ -3248,9 +3469,9 @@ checksum = "b14c52534dd690e23b687bdbbbe300d7ec5c45f25e9261f72b70751af67067d3" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libloading" @@ -3306,11 +3527,11 @@ dependencies = [ [[package]] name = "lru" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown 0.14.5", + "hashbrown 0.15.0", ] [[package]] @@ -3366,6 +3587,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "minicov" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "def6d99771d7c499c26ad4d40eb6645eafd3a1553b35fc26ea5a489a45e82d9a" +dependencies = [ + "cc", + "walkdir", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -3374,18 +3605,18 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.7.4" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" dependencies = [ - "adler", + "adler2", ] [[package]] name = "mio" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4569e456d394deccd22ce1c1913e6ea0e54519f577285001215d33557431afe4" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ "hermit-abi 0.3.9", "libc", @@ -3535,18 +3766,18 @@ dependencies = [ [[package]] name = "object" -version = "0.36.2" +version = "0.36.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f203fa8daa7bb185f760ae12bd8e097f63d17041dcdcaf675ac54cdf863170e" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" dependencies = [ "memchr", ] [[package]] name = "object_store" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25a0c4b3a0e31f8b66f71ad8064521efa773910196e2cde791436f13409f3b45" +checksum = "6eb4c22c6154a1e759d7099f9ffad7cc5ef8245f9efbab4a41b92623079c82f3" dependencies = [ "async-trait", "base64 0.22.1", @@ -3554,7 +3785,7 @@ dependencies = [ "chrono", "futures", "humantime", - "hyper 1.4.1", + "hyper 1.5.0", "itertools 0.13.0", "md-5", "parking_lot 0.12.3", @@ -3574,9 +3805,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.19.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "oorandom" @@ -3586,9 +3817,9 @@ checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" [[package]] name = "openssl" -version = "0.10.66" +version = "0.10.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5" dependencies = [ "bitflags 2.6.0", "cfg-if", @@ -3605,9 +3836,9 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -3618,18 +3849,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.3.1+3.3.1" +version = "300.4.0+3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +checksum = "a709e02f2b4aca747929cca5ed248880847c650233cf8b8cdc48f40aaf4898a6" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.103" +version = "0.9.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741" dependencies = [ "cc", "libc", @@ -3738,7 +3969,7 @@ checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.5.3", + "redox_syscall 0.5.7", "smallvec", "windows-targets 0.52.6", ] @@ -3774,18 +4005,18 @@ dependencies = [ [[package]] name = "parquet" -version = "53.0.0" +version = "53.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0fbf928021131daaa57d334ca8e3904fe9ae22f73c56244fc7db9b04eedc3d8" +checksum = "310c46a70a3ba90d98fec39fa2da6d9d731e544191da6fb56c9d199484d0dd3e" dependencies = [ "ahash 0.8.11", - "arrow-array 53.0.0", - "arrow-buffer 53.0.0", - "arrow-cast 53.0.0", - "arrow-data 53.0.0", - "arrow-ipc 53.0.0", - "arrow-schema 53.0.0", - "arrow-select 53.0.0", + "arrow-array 53.1.0", + "arrow-buffer 53.1.0", + "arrow-cast 53.1.0", + "arrow-data 53.1.0", + "arrow-ipc 53.1.0", + "arrow-schema 53.1.0", + "arrow-select 53.1.0", "base64 0.22.1", "bytes", "chrono", @@ -3843,22 +4074,22 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +checksum = "baf123a161dde1e524adf36f90bc5d8d3462824a9c43553ad07a8183161189ec" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.5" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +checksum = "a4502d8515ca9f32f1fb543d987f63d95a14934883db45bdb48060b6b69257f8" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -3917,9 +4148,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" [[package]] name = "polling" @@ -3963,14 +4194,24 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6fa0831dd7cc608c38a5e323422a0077678fa5744aa2be4ad91c4ece8eec8d5" +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" -version = "0.2.22" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +checksum = "910d41a655dac3b764f1ade94821093d3610248694320cd072303a8eedcf221d" dependencies = [ - "proc-macro2 1.0.86", - "syn 2.0.79", + "proc-macro2 1.0.89", + "syn 2.0.83", ] [[package]] @@ -3989,8 +4230,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" dependencies = [ "proc-macro-error-attr", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", "version_check", ] @@ -4001,8 +4242,8 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "version_check", ] @@ -4017,9 +4258,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e" dependencies = [ "unicode-ident", ] @@ -4038,7 +4279,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha 0.3.1", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -4076,8 +4317,8 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -4110,7 +4351,7 @@ dependencies = [ "headers", "http 1.1.0", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-util", "jsonwebtoken", "log", @@ -4125,7 +4366,7 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tower", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4157,7 +4398,7 @@ dependencies = [ "getrandom 0.2.15", "half", "htmlescape", - "indexmap 2.3.0", + "indexmap 2.6.0", "itertools 0.10.5", "js-sys", "lazy_static", @@ -4193,6 +4434,7 @@ version = "0.5.2" dependencies = [ "axum", "axum-extra", + "bytes", "chrono", "dotenv", "envy", @@ -4212,7 +4454,8 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tower", + "tokio-util", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4245,8 +4488,8 @@ dependencies = [ "strum_macros 0.25.3", "thiserror", "tokio", - "tokio-tungstenite", - "tower", + "tokio-tungstenite 0.21.0", + "tower 0.4.13", "tower-http", "tracing", "tracing-subscriber", @@ -4272,18 +4515,25 @@ dependencies = [ name = "quadratic-rust-shared" version = "0.5.2" dependencies = [ - "arrow 53.0.0", - "arrow-array 53.0.0", + "aes", + "arrow 53.1.0", + "arrow-array 53.1.0", "async-trait", "aws-config", "aws-sdk-s3", + "aws-smithy-async", + "aws-smithy-runtime", + "aws-smithy-runtime-api", "bigdecimal 0.4.5", "bytes", + "cbc", "chrono", "futures-util", + "hex", + "http 1.1.0", "httpmock", "jsonwebtoken", - "parquet 53.0.0", + "parquet 53.1.0", "redis", "reqwest 0.11.27", "rust_decimal", @@ -4298,6 +4548,7 @@ dependencies = [ "tokio", "tokio-util", "tracing", + "tracing-test", "uuid", ] @@ -4338,7 +4589,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.15", "socket2", "thiserror", "tokio", @@ -4355,7 +4606,7 @@ dependencies = [ "rand 0.8.5", "ring", "rustc-hash 2.0.0", - "rustls 0.23.13", + "rustls 0.23.15", "slab", "thiserror", "tinyvec", @@ -4364,15 +4615,15 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" dependencies = [ "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4386,11 +4637,11 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ - "proc-macro2 1.0.86", + "proc-macro2 1.0.89", ] [[package]] @@ -4531,32 +4782,23 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" -dependencies = [ - "bitflags 1.3.2", -] - -[[package]] -name = "redox_syscall" -version = "0.5.3" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +checksum = "9b6dfecf2c74bce2466cabf93f6664d6998a69eb21e39f4207930065b27b771f" dependencies = [ "bitflags 2.6.0", ] [[package]] name = "regex" -version = "1.10.6" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", ] [[package]] @@ -4570,13 +4812,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -4593,9 +4835,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "rend" @@ -4622,7 +4864,7 @@ dependencies = [ "h2 0.3.26", "http 0.2.12", "http-body 0.4.6", - "hyper 0.14.30", + "hyper 0.14.31", "hyper-tls", "ipnet", "js-sys", @@ -4665,7 +4907,7 @@ dependencies = [ "http 1.1.0", "http-body 1.0.1", "http-body-util", - "hyper 1.4.1", + "hyper 1.5.0", "hyper-rustls 0.27.3", "hyper-util", "ipnet", @@ -4676,7 +4918,7 @@ dependencies = [ "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.13", + "rustls 0.23.15", "rustls-native-certs 0.8.0", "rustls-pemfile 2.2.0", "rustls-pki-types", @@ -4723,7 +4965,7 @@ dependencies = [ "futures", "getrandom 0.2.15", "http 1.1.0", - "hyper 1.4.1", + "hyper 1.5.0", "parking_lot 0.11.2", "reqwest 0.12.8", "reqwest-middleware", @@ -4792,11 +5034,20 @@ version = "0.7.45" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] +[[package]] +name = "roxmltree" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "921904a62e410e37e215c40381b7117f830d9d89ba60ab5236170541dd25646b" +dependencies = [ + "xmlparser", +] + [[package]] name = "rsa" version = "0.9.6" @@ -4853,18 +5104,18 @@ checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" [[package]] name = "rustc_version" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "rustix" -version = "0.38.34" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags 2.6.0", "errno", @@ -4887,9 +5138,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.13" +version = "0.23.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8" +checksum = "5fbb44d7acc4e873d613422379f69f237a1b141928c02f6bc6ccfddddc2d7993" dependencies = [ "aws-lc-rs", "log", @@ -4946,9 +5197,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e696e35370c65c9c541198af4543ccd580cf17fc25d8e05c5a242b202488c55" +checksum = "16f1201b3c9a7ee8039bcadc17b7e605e2945b27eee7631788c1bd2b0643674b" [[package]] name = "rustls-webpki" @@ -4974,9 +5225,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" [[package]] name = "rusty-fork" @@ -5007,20 +5258,20 @@ dependencies = [ [[package]] name = "scc" -version = "2.1.7" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a870e34715d5d59c8536040d4d4e7a41af44d527dc50237036ba4090db7996fc" +checksum = "f2c1f7fc6deb21665a9060dfc7d271be784669295a31babdcd4dd2c79ae8cbfb" dependencies = [ "sdd", ] [[package]] name = "schannel" -version = "0.1.23" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +checksum = "01227be5826fa0690321a2ba6c5cd57a19cf3f6a09e76973b58e61de6ab9d1c1" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5047,9 +5298,9 @@ dependencies = [ [[package]] name = "sdd" -version = "2.1.0" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "177258b64c0faaa9ffd3c65cd3262c2bc7e2588dbbd9c1641d0346145c1bbda8" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" [[package]] name = "seahash" @@ -5086,9 +5337,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.11.1" +version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +checksum = "ea4a292869320c0272d7bc55a5a6aafaff59b4f63404a003887b679a2e05b4b6" dependencies = [ "core-foundation-sys", "libc", @@ -5100,6 +5351,12 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +[[package]] +name = "separator" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97841a747eef040fcd2e7b3b9a220a7205926e60488e673d9e4926d27772ce5" + [[package]] name = "seq-macro" version = "0.3.5" @@ -5108,9 +5365,9 @@ checksum = "a3f0bf26fd526d2a95683cd0f87bf103b8539e2ca1ef48ce002d67aad59aa0b4" [[package]] name = "serde" -version = "1.0.204" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12" +checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1" dependencies = [ "serde_derive", ] @@ -5128,21 +5385,22 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.204" +version = "1.0.213" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" +checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "d726bfaff4b320266d395898905d0eba0345aae23b54aee3a737e260fd46db03" dependencies = [ + "indexmap 2.6.0", "itoa", "memchr", "ryu", @@ -5175,9 +5433,9 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -5194,15 +5452,15 @@ dependencies = [ [[package]] name = "serde_with" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +checksum = "8e28bdad6db2b8340e449f7108f020b3b092e8583a9e3fb82713e1d4e71fe817" dependencies = [ "base64 0.22.1", "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.3.0", + "indexmap 2.6.0", "serde", "serde_derive", "serde_json", @@ -5212,14 +5470,14 @@ dependencies = [ [[package]] name = "serde_with_macros" -version = "3.9.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +checksum = "9d846214a9854ef724f3da161b426242d8de7c1fc7de2f89bb1efcb154dca79d" dependencies = [ "darling", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -5228,7 +5486,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.6.0", "itoa", "ryu", "serde", @@ -5255,9 +5513,9 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -5390,9 +5648,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -5406,7 +5664,7 @@ name = "snowflake-api" version = "0.10.0" source = "git+https://github.com/quadratichq/snowflake-rs#b7d6e0930b8f01f0b8f395bd68a56e0efe47f3c6" dependencies = [ - "arrow 53.0.0", + "arrow 53.1.0", "async-trait", "base64 0.22.1", "bytes", @@ -5484,9 +5742,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.4" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f895e3734318cc55f1fe66258926c9b910c124d47520339efecbb6c59cec7c1f" +checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790" dependencies = [ "nom", "unicode_categories", @@ -5528,7 +5786,7 @@ dependencies = [ "hashbrown 0.14.5", "hashlink", "hex", - "indexmap 2.3.0", + "indexmap 2.6.0", "log", "memchr", "native-tls", @@ -5554,11 +5812,11 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "sqlx-core", "sqlx-macros-core", - "syn 2.0.79", + "syn 2.0.83", ] [[package]] @@ -5572,8 +5830,8 @@ dependencies = [ "heck 0.5.0", "hex", "once_cell", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "serde", "serde_json", "sha2", @@ -5581,7 +5839,7 @@ dependencies = [ "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", - "syn 2.0.79", + "syn 2.0.83", "tempfile", "tokio", "url", @@ -5744,10 +6002,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" dependencies = [ "heck 0.4.1", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "rustversion", - "syn 2.0.79", + "syn 2.0.83", ] [[package]] @@ -5757,10 +6015,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" dependencies = [ "heck 0.5.0", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "rustversion", - "syn 2.0.79", + "syn 2.0.83", ] [[package]] @@ -5786,19 +6044,19 @@ version = "1.0.109" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "unicode-ident", ] [[package]] name = "syn" -version = "2.0.79" +version = "2.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +checksum = "01680f5d178a369f817f43f3d399650272873a8e7588a7872f7e90edc71d60a3" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "unicode-ident", ] @@ -5809,9 +6067,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1329189c02ff984e9736652b1631330da25eaa6bc639089ed4915d25446cbe7b" dependencies = [ "proc-macro-error", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -5871,8 +6129,8 @@ checksum = "99f688a08b54f4f02f0a3c382aefdb7884d3d69609f785bd253dc033243e3fe4" dependencies = [ "heck 0.4.1", "proc-macro-error", - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", "syn 1.0.109", ] @@ -5893,15 +6151,15 @@ checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" [[package]] name = "tempfile" -version = "3.11.0" +version = "3.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fcd239983515c23a32fb82099f97d0b11b8c72f654ed659363a95c3dad7a53" +checksum = "f0f2c9fc62d0beef6951ccffd757e241266a2c833136efbe35af6cd2567dca5b" dependencies = [ "cfg-if", "fastrand", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5921,22 +6179,22 @@ checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" [[package]] name = "thiserror" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.63" +version = "1.0.65" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -6056,9 +6314,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.41.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb" dependencies = [ "backtrace", "bytes", @@ -6078,9 +6336,9 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -6109,16 +6367,16 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" dependencies = [ - "rustls 0.23.13", + "rustls 0.23.15", "rustls-pki-types", "tokio", ] [[package]] name = "tokio-stream" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +checksum = "4f4e6ce100d0eb49a2734f8c0812bcd324cf357d21810932c5df6b96ef2b86f1" dependencies = [ "futures-core", "pin-project-lite", @@ -6147,14 +6405,26 @@ dependencies = [ "futures-util", "log", "tokio", - "tungstenite", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.24.0", ] [[package]] name = "tokio-util" -version = "0.7.11" +version = "0.7.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +checksum = "61e7c3654c13bcd040d4a03abee2c75b1d14a37b423cf5a813ceae1cc903ec6a" dependencies = [ "bytes", "futures-core", @@ -6172,11 +6442,11 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" -version = "0.22.20" +version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap 2.3.0", + "indexmap 2.6.0", "toml_datetime", "winnow", ] @@ -6191,6 +6461,21 @@ dependencies = [ "futures-util", "pin-project", "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", "tokio", "tower-layer", "tower-service", @@ -6220,7 +6505,7 @@ dependencies = [ "pin-project-lite", "tokio", "tokio-util", - "tower", + "tower 0.4.13", "tower-layer", "tower-service", "tracing", @@ -6228,15 +6513,15 @@ dependencies = [ [[package]] name = "tower-layer" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" [[package]] name = "tower-service" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" @@ -6256,9 +6541,9 @@ version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -6282,6 +6567,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.3.18" @@ -6292,12 +6587,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -6317,8 +6615,8 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" dependencies = [ - "quote 1.0.36", - "syn 2.0.79", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] @@ -6344,9 +6642,9 @@ version = "7.0.0" source = "git+https://github.com/quadratichq/ts-rs/?rev=812c1a8#812c1a8b5ff3128916426e95e228f51430eb02cc" dependencies = [ "Inflector", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", "termcolor", ] @@ -6369,6 +6667,24 @@ dependencies = [ "utf-8", ] +[[package]] +name = "tungstenite" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand 0.8.5", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "twox-hash" version = "1.6.3" @@ -6393,18 +6709,15 @@ checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" [[package]] name = "unicase" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] +checksum = "7e51b68083f157f853b6379db119d1c1be0e6e4dec98101079dec41f6f5cf6df" [[package]] name = "unicode-bidi" -version = "0.3.15" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" +checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893" [[package]] name = "unicode-case-mapping" @@ -6414,30 +6727,30 @@ checksum = "b92e07ac57786e12073609c8a91417884306b753d974108ca48c8434ed9b99cb" [[package]] name = "unicode-ident" -version = "1.0.12" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" [[package]] name = "unicode-normalization" -version = "0.1.23" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" dependencies = [ "tinyvec", ] [[package]] name = "unicode-properties" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" +checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0" [[package]] name = "unicode-width" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-xid" @@ -6494,9 +6807,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a" dependencies = [ "getrandom 0.2.15", "serde", @@ -6549,8 +6862,8 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e369bee1b05d510a7b4ed645f5faa90619e05437111783ea5848f28d97d3c2e" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", + "proc-macro2 1.0.89", + "quote 1.0.37", ] [[package]] @@ -6601,34 +6914,35 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" dependencies = [ "cfg-if", + "once_cell", "wasm-bindgen-macro", ] [[package]] name = "wasm-bindgen-backend" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" dependencies = [ "bumpalo", "log", "once_cell", - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.42" +version = "0.4.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" +checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b" dependencies = [ "cfg-if", "js-sys", @@ -6638,41 +6952,42 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" dependencies = [ - "quote 1.0.36", + "quote 1.0.37", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.92" +version = "0.2.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" [[package]] name = "wasm-bindgen-test" -version = "0.3.42" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9bf62a58e0780af3e852044583deee40983e5886da43a271dd772379987667b" +checksum = "d381749acb0943d357dcbd8f0b100640679883fcdeeef04def49daf8d33a5426" dependencies = [ "console_error_panic_hook", "js-sys", + "minicov", "scoped-tls", "wasm-bindgen", "wasm-bindgen-futures", @@ -6681,20 +6996,20 @@ dependencies = [ [[package]] name = "wasm-bindgen-test-macro" -version = "0.3.42" +version = "0.3.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7f89739351a2e03cb94beb799d47fb2cac01759b40ec441f7de39b00cbf7ef0" +checksum = "c97b2ef2c8d627381e51c071c2ab328eac606d3f69dd82bcbca20a9e389d95f0" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] name = "wasm-streams" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" +checksum = "4e072d4e72f700fb3443d8fe94a39315df013eef1104903cdb0a2abd322bbecd" dependencies = [ "futures-util", "js-sys", @@ -6720,9 +7035,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.69" +version = "0.3.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" +checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112" dependencies = [ "js-sys", "wasm-bindgen", @@ -6751,11 +7066,11 @@ dependencies = [ [[package]] name = "whoami" -version = "1.5.1" +version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44ab49fad634e88f55bf8f9bb3abd2f27d7204172a112c7c9987e01c1c94ea9" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" dependencies = [ - "redox_syscall 0.4.1", + "redox_syscall 0.5.7", "wasite", ] @@ -6992,9 +7307,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "winnow" -version = "0.6.18" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +checksum = "36c1fec1a2bb5866f07c25f68c26e565c4c200aebb96d7e55710c19d3e8ac49b" dependencies = [ "memchr", ] @@ -7024,6 +7339,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.7.35" @@ -7040,9 +7361,9 @@ version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ - "proc-macro2 1.0.86", - "quote 1.0.36", - "syn 2.0.79", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.83", ] [[package]] diff --git a/client.Dockerfile b/client.Dockerfile index 0f55931755..ea1366e10e 100644 --- a/client.Dockerfile +++ b/client.Dockerfile @@ -8,33 +8,45 @@ ENV PATH="/root/.cargo/bin:${PATH}" # Install wasm-pack RUN echo 'Installing wasm-pack...' && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh -# Install python -RUN apt-get update || : && apt-get install python-is-python3 -y && apt install python3-pip -y +# Install wasm32-unknown-unknown target +RUN rustup target add wasm32-unknown-unknown -# Install binaryen -RUN apt install binaryen -y +# Install python, binaryen & clean up +RUN apt-get update && apt-get install -y python-is-python3 python3-pip binaryen && apt-get clean && rm -rf /var/lib/apt/lists/* -# Copy the rest of the application code +# Install npm dependencies WORKDIR /app - COPY package.json . COPY package-lock.json . +COPY ./quadratic-kernels/python-wasm/package*.json ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/package*.json ./quadratic-core/ +COPY ./quadratic-rust-client/package*.json ./quadratic-rust-client/ +COPY ./quadratic-shared/package*.json ./quadratic-shared/ +COPY ./quadratic-client/package*.json ./quadratic-client/ +RUN npm install + +# Install typescript +RUN npm install -D typescript + +# Copy the rest of the application +WORKDIR /app COPY updateAlertVersion.json . -COPY ./quadratic-client/. ./quadratic-client/ -COPY ./quadratic-core/. ./quadratic-core/ COPY ./quadratic-kernels/python-wasm/. ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/. ./quadratic-core/ COPY ./quadratic-rust-client/. ./quadratic-rust-client/ COPY ./quadratic-shared/. ./quadratic-shared/ +COPY ./quadratic-client/. ./quadratic-client/ # Run the packaging script for quadratic_py +WORKDIR /app RUN ./quadratic-kernels/python-wasm/package.sh --no-poetry # Build wasm WORKDIR /app/quadratic-core -RUN rustup target add wasm32-unknown-unknown -RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs +RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs # Export TS/Rust types +WORKDIR /app/quadratic-core RUN echo 'Exporting TS/Rust types...' && cargo run --bin export_types # Build the quadratic-rust-client @@ -43,11 +55,19 @@ ARG GIT_COMMIT ENV GIT_COMMIT=$GIT_COMMIT RUN echo 'Building quadratic-rust-client...' && npm run build --workspace=quadratic-rust-client +# Build the quadratic-shared +WORKDIR /app +RUN echo 'Building quadratic-shared...' && npm run compile --workspace=quadratic-shared + # Build the front-end WORKDIR /app RUN echo 'Building front-end...' -RUN npm ci -RUN npx tsc ./quadratic-shared/*.ts +ENV VITE_DEBUG=VITE_DEBUG_VAL +ENV VITE_QUADRATIC_API_URL=VITE_QUADRATIC_API_URL_VAL +ENV VITE_QUADRATIC_MULTIPLAYER_URL=VITE_QUADRATIC_MULTIPLAYER_URL_VAL +ENV VITE_QUADRATIC_CONNECTION_URL=VITE_QUADRATIC_CONNECTION_URL_VAL +ENV VITE_AUTH_TYPE=VITE_AUTH_TYPE_VAL +ENV VITE_ORY_HOST=VITE_ORY_HOST_VAL RUN npm run build --workspace=quadratic-client # The default command to run the application @@ -58,4 +78,11 @@ COPY --from=build /app/build /usr/share/nginx/html EXPOSE 80 443 3000 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] + + + + + + + diff --git a/docker-compose.base.yml b/docker-compose.base.yml index 5c36d88738..278c217a08 100644 --- a/docker-compose.base.yml +++ b/docker-compose.base.yml @@ -23,7 +23,6 @@ services: POSTGRES_USER: postgres PGUSER: postgres POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] interval: 10s diff --git a/docker-compose.yml b/docker-compose.yml index 3c8229bac7..db29d07208 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -14,8 +14,11 @@ services: extends: file: docker-compose.base.yml service: postgres + environment: + ADDITIONAL_DATABASES: kratos volumes: - ./docker/postgres/data:/var/lib/postgresql/data + - ./docker/postgres/scripts:/docker-entrypoint-initdb.d profiles: - base @@ -83,14 +86,13 @@ services: build: context: . dockerfile: client.Dockerfile - env_file: - - quadratic-client/.env.local - - quadratic-client/.env.docker - # override env vars here environment: + VITE_DEBUG: 1 VITE_QUADRATIC_API_URL: http://localhost:8000 - VITE_QUADRATIC_MULTIPLAYER_URL: ws://localhost:3001 - VITE_QUADRATIC_CONNECTION_URL: http://0.0.0.0:3003 + VITE_QUADRATIC_MULTIPLAYER_URL: ws://localhost:3001/ws + VITE_QUADRATIC_CONNECTION_URL: http://localhost:3003 + VITE_AUTH_TYPE: ory + VITE_ORY_HOST: http://localhost:4433 restart: "always" ports: # - "3000:3000" @@ -209,10 +211,82 @@ services: # condition: service_healthy # quadratic-api: # condition: service_started + profiles: - backend - quadratic-connection + # Auth Providers + + ory-auth: + image: oryd/kratos:v1.2.0 + ports: + - "4433:4433" # public + - "4434:4434" # admin + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + volumes: + - ./docker/ory-auth/config:/etc/config/kratos + environment: + DSN: postgresql://postgres:postgres@host.docker.internal:5432/kratos?sslmode=disable + LOG_LEVEL: trace + restart: unless-stopped + depends_on: + - postgres + - ory-auth-migrate + profiles: + - ory + - all + networks: + - host + + ory-auth-migrate: + image: oryd/kratos:v1.2.0 + command: migrate -c /etc/config/kratos/kratos.yml sql -e --yes + volumes: + - ./docker/ory-auth/config:/etc/config/kratos + environment: + DSN: postgresql://postgres:postgres@host.docker.internal:5432/kratos?sslmode=disable + restart: on-failure + depends_on: + - postgres + profiles: + - ory + - all + networks: + - host + + ory-auth-node: + image: oryd/kratos-selfservice-ui-node:v1.2.0 + ports: + - "4455:4455" + environment: + PORT: 4455 + SECURITY_MODE: + KRATOS_PUBLIC_URL: http://host.docker.internal:4433/ + KRATOS_BROWSER_URL: http://localhost:4433/ + COOKIE_SECRET: changeme + CSRF_COOKIE_NAME: ory_csrf_ui + CSRF_COOKIE_SECRET: changeme + restart: on-failure + profiles: + - ory + - all + networks: + - host + + ory-auth-mail: + image: oryd/mailslurper:latest-smtps + ports: + - "1025:1025" + - "4436:4436" + - "4437:4437" + - "8080:8080" + profiles: + - ory + - all + networks: + - host + # Databases to be used for testing by the connection service postgres-connection: diff --git a/docker/ory-auth/config/identity.schema.json b/docker/ory-auth/config/identity.schema.json new file mode 100644 index 0000000000..a953fc68ec --- /dev/null +++ b/docker/ory-auth/config/identity.schema.json @@ -0,0 +1,47 @@ +{ + "$id": "https://schemas.ory.sh/presets/kratos/quickstart/email-password/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Person", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "E-Mail", + "minLength": 3, + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + }, + "verification": { + "via": "email" + }, + "recovery": { + "via": "email" + } + } + }, + "name": { + "type": "object", + "properties": { + "first": { + "title": "First Name", + "type": "string" + }, + "last": { + "title": "Last Name", + "type": "string" + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + } +} diff --git a/docker/ory-auth/config/kratos.yml b/docker/ory-auth/config/kratos.yml new file mode 100644 index 0000000000..c815b5713f --- /dev/null +++ b/docker/ory-auth/config/kratos.yml @@ -0,0 +1,133 @@ +# https://raw.githubusercontent.com/ory/kratos/v1.2.0/.schemastore/config.schema.json +version: v1.2.0 + +dsn: memory + +serve: + public: + base_url: http://localhost:4433/ + cors: + enabled: true + allowed_origins: + - http://localhost:3000 + allowed_methods: + - POST + - GET + - PUT + - PATCH + - DELETE + allowed_headers: + - Authorization + - Access-Control-Allow-Origin + - Cookie + - Content-Type + exposed_headers: + - Content-Type + - Set-Cookie + admin: + base_url: http://kratos:4434/ + +selfservice: + default_browser_return_url: http://localhost:3000 + allowed_return_urls: + - http://localhost + - http://localhost:4455 + - http://localhost:3000 + - http://localhost:19006/Callback + - exp://localhost:8081/--/Callback + + methods: + password: + enabled: true + totp: + config: + issuer: Kratos + enabled: true + lookup_secret: + enabled: true + link: + enabled: true + code: + enabled: true + + flows: + error: + ui_url: http://localhost:4455/error + + settings: + ui_url: http://localhost:4455/settings + privileged_session_max_age: 15m + required_aal: highest_available + + recovery: + enabled: true + ui_url: http://localhost:4455/recovery + use: link + + verification: + # we disable verification for self-hosting + enabled: false + ui_url: http://localhost:4455/verification + use: link + after: + default_browser_return_url: http://localhost:3000 + + logout: + after: + default_browser_return_url: http://localhost:4455/login + + login: + ui_url: http://localhost:4455/login + lifespan: 10m + + registration: + lifespan: 10m + ui_url: http://localhost:4455/registration + after: + default_browser_return_url: http://localhost:3000/login-result + password: + default_browser_return_url: http://localhost:3000/login-result + hooks: + - hook: session + - hook: show_verification_ui + +session: + whoami: + tokenizer: + templates: + jwt_template: + jwks_url: http://host.docker.internal:3000/.well-known/jwks.json + # claims_mapper_url: base64://... # A JsonNet template for modifying the claims + ttl: 24h # 24 hours (defaults to 10 minutes) + +log: + level: debug + format: text + leak_sensitive_values: true + +secrets: + cookie: + - PLEASE-CHANGE-ME-I-AM-VERY-INSECURE + cipher: + - 32-LONG-SECRET-NOT-SECURE-AT-ALL + +ciphers: + algorithm: xchacha20-poly1305 + +hashers: + algorithm: bcrypt + bcrypt: + cost: 8 + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +courier: + smtp: + connection_uri: smtps://test:test@host.docker.internal:1025/?skip_ssl_verify=true + +feature_flags: + use_continue_with_transitions: true \ No newline at end of file diff --git a/docker/postgres/scripts/init.sh b/docker/postgres/scripts/init.sh new file mode 100755 index 0000000000..5e5b12df77 --- /dev/null +++ b/docker/postgres/scripts/init.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -e +set -u + +function create_user_and_database() { + local database=$1 + echo "Creating database '$database' with user '$POSTGRES_USER'" + psql -c "CREATE DATABASE $database;" || { echo "Failed to create database '$database'"; exit 1; } + echo "Database '$database' created" +} + +if [ -n "$ADDITIONAL_DATABASES" ]; then + for i in ${ADDITIONAL_DATABASES//,/ } + do + create_user_and_database $i + done +fi diff --git a/docker/snowflake-connection/data/cache/machine.json b/docker/snowflake-connection/data/cache/machine.json new file mode 100644 index 0000000000..32a03c0569 --- /dev/null +++ b/docker/snowflake-connection/data/cache/machine.json @@ -0,0 +1 @@ +{"machine_id": "gen_49a3c8c6-017"} \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/server.test.pem b/docker/snowflake-connection/data/cache/server.test.pem new file mode 100644 index 0000000000..59edcd2b5b --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem @@ -0,0 +1,169 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPW8q9f0r+EoF8 +5PzNkhLel7JVliMPTLkCngU5W/cloPyQ7Erpq6MVMQzy5ESsM9AcRrTNYXVj6bm9 +MBCLU2Gvik0SJ6UeGu3nQZtnnW56bEnuJNldwtCu+l2YdOEPkb/5zTMPnZxS7taS +rthj0nomwdcBvfTaQtWixCycIXLKoJx+M06sauHnuofANpSNwYN0RlJHxcT0/ePc +rgxtA0ZmArrYuG+Vw9Gy0rJx1yobMjlg5WXNiQrpE/uUUWT/EncG7nbS2eMT7325 +98ZkFU9aNTrbEuCzBxgSp54SudjgwJz+CncgDqRiRFR7FxdgvdKNIJb/HyiN8s5T +cWzCQBdPAgMBAAECggEAGv/7SqhoBeQ3+yC/8C6MiXJcNLu7bfMSBg64ZGsep8Yq +DN7PtFR2hDxiUMA7VubaOsxUH4gIpo1Y85LuHI4rYpWSCoKiA+UCxEFtMFU1/Pfb +uogOy6Ah1x7fkAnsAkB6rFa1RtvBbqUNyIS+xWSzJhfIXMA0wTTBp5N+sYfDcDG0 +myrpuT5H84Is9vJmQytUhtp4uSaPERNj12HBpcADsH+Bk6ez+CLTiSvwVLCkaK+E +zGRes4868K5Lq2pj/msDxZa0YjkV1BU1jAve55O1zVaqZANPWjn8yKhvfgAVV4sP +p7/ORUrzeSA9X/HLbzTdOL/Kv3oCJRsEL2RJAiGS9QKBgQDrTwF6hOulGLWGJPJi +7Nbgz/tEdUhtiKE6i3phRKuYfqVKEg4x4hGw7RY2+V7Is22lx55MzdSFBNy8sGGs +Ij2N/qm61lBXhHJaELK5sZ2mnHf6nMJRBrJDsoFAe0YV5VhTYtq9Yw45AA9nCIw/ +z0pDCbcg3iTzw1zugkGs09GnKwKBgQDhl5tmYbnKYdkO1SOrEGvtSjCjIColh7zb +ZJ+e32ThgJxzueRQ77c83e556/gCkT6GT8oDJXn6qrcAriVJ7jdRUnkGLVy9Tzqc +mJ9dQxsssS97WhaPt5Ipv+LsIsCQm/eWubJusRmn72SBrxsoaqNhvu4tzqLjp45p +JGfhsr6+bQKBgQDJYAWt6o8X7Tt8H6ZnzrReFN++SHjBdIo2ZiNHltMbYFboOud2 +/TeSqHO4fFUHgba2h00MAaJ8bBrUSEZuX6c6G9T5lmuPWkPanCu4Cy8V5RYwnXMW +kJqCoQNIQbdLCck7I4B7T4hec5S64m/UM/wjvu6/7BzHmEuxujumQmhLnQKBgAgn +k816oN2o9dCscbKgUFZuhR2QbxWWN4RyubZjeuEP5hfk01T9pVEE8LblibyGBY2T +WskMVMFz5FOY9+4ZN1SwN4G6qAyLzaGVfsU/RL8z1HSQCBq/1v+9WPWSOAXCLYv8 +QG/x5OyGIcrySngGistgvHlZa9fw2ZwBXePxsyVtAoGAIfNEpG8Wmzz+O0cWUDgt +CRuN6Gcx+RsUMBkhMPkbGXw0UFQq94KCPkUcU3q9LWw6PhJ5XUxw93koljoYdkfp +JdaAvBscSGIK/d+RHtNlQuwbUFBD0SZfNyOdTNXK1TW7JFagy24law+3xcr8BjHG +CAFPNoOWtBM/qVl/+jzB5iY= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIMjjCCCnagAwIBAgIRAO751+shvgIfZO3XzfgeF3gwDQYJKoZIhvcNAQEMBQAw +SzELMAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9T +U0wgUlNBIERvbWFpbiBTZWN1cmUgU2l0ZSBDQTAeFw0yNDA5MDUwMDAwMDBaFw0y +NDEyMDQyMzU5NTlaMCUxIzAhBgNVBAMTGmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNs +b3VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1vKvX9K/hKBfOT8 +zZIS3peyVZYjD0y5Ap4FOVv3JaD8kOxK6aujFTEM8uRErDPQHEa0zWF1Y+m5vTAQ +i1Nhr4pNEielHhrt50GbZ51uemxJ7iTZXcLQrvpdmHThD5G/+c0zD52cUu7Wkq7Y +Y9J6JsHXAb302kLVosQsnCFyyqCcfjNOrGrh57qHwDaUjcGDdEZSR8XE9P3j3K4M +bQNGZgK62LhvlcPRstKycdcqGzI5YOVlzYkK6RP7lFFk/xJ3Bu520tnjE+99uffG +ZBVPWjU62xLgswcYEqeeErnY4MCc/gp3IA6kYkRUexcXYL3SjSCW/x8ojfLOU3Fs +wkAXTwIDAQABo4IIkTCCCI0wHwYDVR0jBBgwFoAUyNl4aKLZGWjVPXLeXwo+3LWG +hqYwHQYDVR0OBBYEFKTGMH+/7ToKjQk2P2ZFSmZrJ1/ZMA4GA1UdDwEB/wQEAwIF +oDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJ +BgNVHSAEQjBAMDQGCysGAQQBsjEBAgJOMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8v +c2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBiAYIKwYBBQUHAQEEfDB6MEsGCCsG +AQUFBzAChj9odHRwOi8vemVyb3NzbC5jcnQuc2VjdGlnby5jb20vWmVyb1NTTFJT +QURvbWFpblNlY3VyZVNpdGVDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly96ZXJv +c3NsLm9jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB2 +/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAZHDW8C3AAAEAwBIMEYC +IQDx2ITaoZMYCR/GFCihmAZZGZCE904XyeIyk8DM7rQhJgIhANYZv74TdKeYS5fW +XryiwmxU2oWEAIy/2CIIqPXrYcyoAHUAPxdLT9ciR1iUHWUchL4NEu2QN38fhWrr +wb8ohez4ZG4AAAGRw1vAcwAABAMARjBEAiBP2zRB0e+sAlX2Z2kjEn9gYzhFPpSW +pqPXqyWCIxb/vAIgFin9WqFSFh/QC23dlChnDVVhZZgOjbmgmWUCEqIW8iowggYu +BgNVHREEggYlMIIGIYIabG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCJyouYW1w +bGlmeWFwcC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5jbG91ZGZyb250 +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgjEqLmRrci5lY3IuZXUtY2VudHJh +bC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IuZXUtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiAqLmVsYi5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZII0Ki5ldS1jZW50cmFsLTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5ldS13ZXN0LTEub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoKi5leGVjdXRlLWFwaS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZII0Ki5sYW1iZGEtdXJsLmV1LWNlbnRyYWwtMS5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLmV1LXdlc3Qt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVzLWVh +c3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVz +LWVhc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJs +LnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEt +dXJsLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIcKi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5vcGVuc2VhcmNoLmxvY2FsaG9zdC5s +b2NhbHN0YWNrLmNsb3VkgicqLnMzLXdlYnNpdGUubG9jYWxob3N0LmxvY2Fsc3Rh +Y2suY2xvdWSCHyouczMubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCICouc2Nt +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiYqLnNub3dmbGFrZS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTIub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTEub3BlbnNlYXJj +aC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTIub3BlbnNl +YXJjaC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIrc3FzLmV1LWNlbnRyYWwt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLmV1LXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMS5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMi5sb2NhbGhvc3QubG9jYWxz +dGFjay5jbG91ZIIoc3FzLnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5j +bG91ZIIoc3FzLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZDAN +BgkqhkiG9w0BAQwFAAOCAgEAZasJ/uh27MthaZ9smCugqhq3Q6gVuoPYummeZCGE +RDG+f1/9bFpFCoRRMywxlLYtgXxb275V/aeauWbyNiY+YBsReXqbyfo0iyZtxpYg +zglp/p38curzfvBl9FxR8KsiKA0qlSb24jRlpU60xQaKieE0wA3oA5j3EN6LDMfL +7uBTjI+QKj6A5kGqS+zJzz2M4SeqRAp4xgGfrUrq4ByrlTKQtHrdHv7g2FNr6Ktr +6lDY++Mggm5DBupIRGkT1iNc+LESFDnHGuk63gj7RRlThCaZ1AZcqiR/SDsphyj5 +5vMqDTCs2vGRnhvH8VXF8Se1ydbKVq7Qk6nKgXyIwB2Fp1uOY0bScA6sIDynpBqT +Ry07TvYekt+jsuKD9qmm8bFpGxTMLk9JLyR3aUfKeru1cjAzoGfFChtskWK1WNiZ +CGbZF5b2VuUMw7jK8yxvpOdQ8QWKi3NuaCrUkm0RLoej1Z6zCRtUpyKeejNvrB2h +MRTc8goLzDBFMipoIgiWzD6gzdvGUDggOmbgcSdELSEhaVXYPbg+1IatP5577S0r +GSx9XUnPxVfSLp30tb4d7oGKbXx8ZybpsaTUFn0s3Y96iD4U/PxfGHRTGztcYNxJ +g7xxSZ9x/bnwpQqjPoAq+umpj4ohsiNUUsui4UJT8e6bzF8NoulC9Z6hGG1APWSc +WGo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw +MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE +ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT +aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj +4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs +JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4 +t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF +WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc +RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U +gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD +hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt +iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF +x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W +2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp +wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA +FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c +tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC +TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1 +c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG +CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu +Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw +Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV +ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4 +YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA +Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ +qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd +4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW ++LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0 +2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W +kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4 +RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG +ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5 +S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- diff --git a/docker/snowflake-connection/data/cache/server.test.pem.crt b/docker/snowflake-connection/data/cache/server.test.pem.crt new file mode 100644 index 0000000000..846abd9a7b --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem.crt @@ -0,0 +1,141 @@ +-----BEGIN CERTIFICATE----- +MIIMjjCCCnagAwIBAgIRAO751+shvgIfZO3XzfgeF3gwDQYJKoZIhvcNAQEMBQAw +SzELMAkGA1UEBhMCQVQxEDAOBgNVBAoTB1plcm9TU0wxKjAoBgNVBAMTIVplcm9T +U0wgUlNBIERvbWFpbiBTZWN1cmUgU2l0ZSBDQTAeFw0yNDA5MDUwMDAwMDBaFw0y +NDEyMDQyMzU5NTlaMCUxIzAhBgNVBAMTGmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNs +b3VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz1vKvX9K/hKBfOT8 +zZIS3peyVZYjD0y5Ap4FOVv3JaD8kOxK6aujFTEM8uRErDPQHEa0zWF1Y+m5vTAQ +i1Nhr4pNEielHhrt50GbZ51uemxJ7iTZXcLQrvpdmHThD5G/+c0zD52cUu7Wkq7Y +Y9J6JsHXAb302kLVosQsnCFyyqCcfjNOrGrh57qHwDaUjcGDdEZSR8XE9P3j3K4M +bQNGZgK62LhvlcPRstKycdcqGzI5YOVlzYkK6RP7lFFk/xJ3Bu520tnjE+99uffG +ZBVPWjU62xLgswcYEqeeErnY4MCc/gp3IA6kYkRUexcXYL3SjSCW/x8ojfLOU3Fs +wkAXTwIDAQABo4IIkTCCCI0wHwYDVR0jBBgwFoAUyNl4aKLZGWjVPXLeXwo+3LWG +hqYwHQYDVR0OBBYEFKTGMH+/7ToKjQk2P2ZFSmZrJ1/ZMA4GA1UdDwEB/wQEAwIF +oDAMBgNVHRMBAf8EAjAAMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjBJ +BgNVHSAEQjBAMDQGCysGAQQBsjEBAgJOMCUwIwYIKwYBBQUHAgEWF2h0dHBzOi8v +c2VjdGlnby5jb20vQ1BTMAgGBmeBDAECATCBiAYIKwYBBQUHAQEEfDB6MEsGCCsG +AQUFBzAChj9odHRwOi8vemVyb3NzbC5jcnQuc2VjdGlnby5jb20vWmVyb1NTTFJT +QURvbWFpblNlY3VyZVNpdGVDQS5jcnQwKwYIKwYBBQUHMAGGH2h0dHA6Ly96ZXJv +c3NsLm9jc3Auc2VjdGlnby5jb20wggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdwB2 +/4g/Crb7lVHCYcz1h7o0tKTNuyncaEIKn+ZnTFo6dAAAAZHDW8C3AAAEAwBIMEYC +IQDx2ITaoZMYCR/GFCihmAZZGZCE904XyeIyk8DM7rQhJgIhANYZv74TdKeYS5fW +XryiwmxU2oWEAIy/2CIIqPXrYcyoAHUAPxdLT9ciR1iUHWUchL4NEu2QN38fhWrr +wb8ohez4ZG4AAAGRw1vAcwAABAMARjBEAiBP2zRB0e+sAlX2Z2kjEn9gYzhFPpSW +pqPXqyWCIxb/vAIgFin9WqFSFh/QC23dlChnDVVhZZgOjbmgmWUCEqIW8iowggYu +BgNVHREEggYlMIIGIYIabG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCJyouYW1w +bGlmeWFwcC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5jbG91ZGZyb250 +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgjEqLmRrci5lY3IuZXUtY2VudHJh +bC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IuZXUtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtZWFz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0xLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3Vkgi4qLmRrci5lY3IudXMtd2Vz +dC0yLmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiAqLmVsYi5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZII0Ki5ldS1jZW50cmFsLTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5ldS13ZXN0LTEub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoKi5leGVjdXRlLWFwaS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZII0Ki5sYW1iZGEtdXJsLmV1LWNlbnRyYWwtMS5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLmV1LXdlc3Qt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVzLWVh +c3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJsLnVz +LWVhc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEtdXJs +LnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi5sYW1iZGEt +dXJsLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIcKi5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIInKi5vcGVuc2VhcmNoLmxvY2FsaG9zdC5s +b2NhbHN0YWNrLmNsb3VkgicqLnMzLXdlYnNpdGUubG9jYWxob3N0LmxvY2Fsc3Rh +Y2suY2xvdWSCHyouczMubG9jYWxob3N0LmxvY2Fsc3RhY2suY2xvdWSCICouc2Nt +LmxvY2FsaG9zdC5sb2NhbHN0YWNrLmNsb3VkgiYqLnNub3dmbGFrZS5sb2NhbGhv +c3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTEub3BlbnNlYXJjaC5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy1lYXN0LTIub3BlbnNlYXJjaC5s +b2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTEub3BlbnNlYXJj +aC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIxKi51cy13ZXN0LTIub3BlbnNl +YXJjaC5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIrc3FzLmV1LWNlbnRyYWwt +MS5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLmV1LXdlc3QtMS5sb2Nh +bGhvc3QubG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMS5sb2NhbGhvc3Qu +bG9jYWxzdGFjay5jbG91ZIIoc3FzLnVzLWVhc3QtMi5sb2NhbGhvc3QubG9jYWxz +dGFjay5jbG91ZIIoc3FzLnVzLXdlc3QtMS5sb2NhbGhvc3QubG9jYWxzdGFjay5j +bG91ZIIoc3FzLnVzLXdlc3QtMi5sb2NhbGhvc3QubG9jYWxzdGFjay5jbG91ZDAN +BgkqhkiG9w0BAQwFAAOCAgEAZasJ/uh27MthaZ9smCugqhq3Q6gVuoPYummeZCGE +RDG+f1/9bFpFCoRRMywxlLYtgXxb275V/aeauWbyNiY+YBsReXqbyfo0iyZtxpYg +zglp/p38curzfvBl9FxR8KsiKA0qlSb24jRlpU60xQaKieE0wA3oA5j3EN6LDMfL +7uBTjI+QKj6A5kGqS+zJzz2M4SeqRAp4xgGfrUrq4ByrlTKQtHrdHv7g2FNr6Ktr +6lDY++Mggm5DBupIRGkT1iNc+LESFDnHGuk63gj7RRlThCaZ1AZcqiR/SDsphyj5 +5vMqDTCs2vGRnhvH8VXF8Se1ydbKVq7Qk6nKgXyIwB2Fp1uOY0bScA6sIDynpBqT +Ry07TvYekt+jsuKD9qmm8bFpGxTMLk9JLyR3aUfKeru1cjAzoGfFChtskWK1WNiZ +CGbZF5b2VuUMw7jK8yxvpOdQ8QWKi3NuaCrUkm0RLoej1Z6zCRtUpyKeejNvrB2h +MRTc8goLzDBFMipoIgiWzD6gzdvGUDggOmbgcSdELSEhaVXYPbg+1IatP5577S0r +GSx9XUnPxVfSLp30tb4d7oGKbXx8ZybpsaTUFn0s3Y96iD4U/PxfGHRTGztcYNxJ +g7xxSZ9x/bnwpQqjPoAq+umpj4ohsiNUUsui4UJT8e6bzF8NoulC9Z6hGG1APWSc +WGo= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIG1TCCBL2gAwIBAgIQbFWr29AHksedBwzYEZ7WvzANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMjAw +MTMwMDAwMDAwWhcNMzAwMTI5MjM1OTU5WjBLMQswCQYDVQQGEwJBVDEQMA4GA1UE +ChMHWmVyb1NTTDEqMCgGA1UEAxMhWmVyb1NTTCBSU0EgRG9tYWluIFNlY3VyZSBT +aXRlIENBMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAhmlzfqO1Mdgj +4W3dpBPTVBX1AuvcAyG1fl0dUnw/MeueCWzRWTheZ35LVo91kLI3DDVaZKW+TBAs +JBjEbYmMwcWSTWYCg5334SF0+ctDAsFxsX+rTDh9kSrG/4mp6OShubLaEIUJiZo4 +t873TuSd0Wj5DWt3DtpAG8T35l/v+xrN8ub8PSSoX5Vkgw+jWf4KQtNvUFLDq8mF +WhUnPL6jHAADXpvs4lTNYwOtx9yQtbpxwSt7QJY1+ICrmRJB6BuKRt/jfDJF9Jsc +RQVlHIxQdKAJl7oaVnXgDkqtk2qddd3kCDXd74gv813G91z7CjsGyJ93oJIlNS3U +gFbD6V54JMgZ3rSmotYbz98oZxX7MKbtCm1aJ/q+hTv2YK1yMxrnfcieKmOYBbFD +hnW5O6RMA703dBK92j6XRN2EttLkQuujZgy+jXRKtaWMIlkNkWJmOiHmErQngHvt +iNkIcjJumq1ddFX4iaTI40a6zgvIBtxFeDs2RfcaH73er7ctNUUqgQT5rFgJhMmF +x76rQgB5OZUkodb5k2ex7P+Gu4J86bS15094UuYcV09hVeknmTh5Ex9CBKipLS2W +2wKBakf+aVYnNCU6S0nASqt2xrZpGC1v7v6DhuepyyJtn3qSV2PoBiU5Sql+aARp +wUibQMGm44gjyNDqDlVp+ShLQlUH9x8CAwEAAaOCAXUwggFxMB8GA1UdIwQYMBaA +FFN5v1qqK0rPVIDh2JvAnfKyA2bLMB0GA1UdDgQWBBTI2XhootkZaNU9ct5fCj7c +tYaGpjAOBgNVHQ8BAf8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHSUE +FjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwIgYDVR0gBBswGTANBgsrBgEEAbIxAQIC +TjAIBgZngQwBAgEwUAYDVR0fBEkwRzBFoEOgQYY/aHR0cDovL2NybC51c2VydHJ1 +c3QuY29tL1VTRVJUcnVzdFJTQUNlcnRpZmljYXRpb25BdXRob3JpdHkuY3JsMHYG +CCsGAQUFBwEBBGowaDA/BggrBgEFBQcwAoYzaHR0cDovL2NydC51c2VydHJ1c3Qu +Y29tL1VTRVJUcnVzdFJTQUFkZFRydXN0Q0EuY3J0MCUGCCsGAQUFBzABhhlodHRw +Oi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEBDAUAA4ICAQAVDwoIzQDV +ercT0eYqZjBNJ8VNWwVFlQOtZERqn5iWnEVaLZZdzxlbvz2Fx0ExUNuUEgYkIVM4 +YocKkCQ7hO5noicoq/DrEYH5IuNcuW1I8JJZ9DLuB1fYvIHlZ2JG46iNbVKA3ygA +Ez86RvDQlt2C494qqPVItRjrz9YlJEGT0DrttyApq0YLFDzf+Z1pkMhh7c+7fXeJ +qmIhfJpduKc8HEQkYQQShen426S3H0JrIAbKcBCiyYFuOhfyvuwVCFDfFvrjADjd +4jX1uQXd161IyFRbm89s2Oj5oU1wDYz5sx+hoCuh6lSs+/uPuWomIq3y1GDFNafW ++LsHBU16lQo5Q2yh25laQsKRgyPmMpHJ98edm6y2sHUabASmRHxvGiuwwE25aDU0 +2SAeepyImJ2CzB80YG7WxlynHqNhpE7xfC7PzQlLgmfEHdU+tHFeQazRQnrFkW2W +kqRGIq7cKRnyypvjPMkjeiV9lRdAM9fSJvsB3svUuu1coIG1xxI1yegoGM4r5QP4 +RGIVvYaiI76C0djoSbQ/dkIUUXQuB8AL5jyH34g3BZaaXyvpmnV4ilppMXVAnAYG +ON51WhJ6W0xNdNJwzYASZYH+tmCWI+N60Gv2NNMGHwMZ7e9bXgzUCZH5FaBFDGR5 +S9VWqHB73Q+OyIVvIbKYcSc2w/aSuFKGSA== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIIFgTCCBGmgAwIBAgIQOXJEOvkit1HX02wQ3TE1lTANBgkqhkiG9w0BAQwFADB7 +MQswCQYDVQQGEwJHQjEbMBkGA1UECAwSR3JlYXRlciBNYW5jaGVzdGVyMRAwDgYD +VQQHDAdTYWxmb3JkMRowGAYDVQQKDBFDb21vZG8gQ0EgTGltaXRlZDEhMB8GA1UE +AwwYQUFBIENlcnRpZmljYXRlIFNlcnZpY2VzMB4XDTE5MDMxMjAwMDAwMFoXDTI4 +MTIzMTIzNTk1OVowgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpOZXcgSmVyc2V5 +MRQwEgYDVQQHEwtKZXJzZXkgQ2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBO +ZXR3b3JrMS4wLAYDVQQDEyVVU0VSVHJ1c3QgUlNBIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAgBJlFzYOw9sI +s9CsVw127c0n00ytUINh4qogTQktZAnczomfzD2p7PbPwdzx07HWezcoEStH2jnG +vDoZtF+mvX2do2NCtnbyqTsrkfjib9DsFiCQCT7i6HTJGLSR1GJk23+jBvGIGGqQ +Ijy8/hPwhxR79uQfjtTkUcYRZ0YIUcuGFFQ/vDP+fmyc/xadGL1RjjWmp2bIcmfb +IWax1Jt4A8BQOujM8Ny8nkz+rwWWNR9XWrf/zvk9tyy29lTdyOcSOk2uTIq3XJq0 +tyA9yn8iNK5+O2hmAUTnAU5GU5szYPeUvlM3kHND8zLDU+/bqv50TmnHa4xgk97E +xwzf4TKuzJM7UXiVZ4vuPVb+DNBpDxsP8yUmazNt925H+nND5X4OpWaxKXwyhGNV +icQNwZNUMBkTrNN9N6frXTpsNVzbQdcS2qlJC9/YgIoJk2KOtWbPJYjNhLixP6Q5 +D9kCnusSTJV882sFqV4Wg8y4Z+LoE53MW4LTTLPtW//e5XOsIzstAL81VXQJSdhJ +WBp/kjbmUZIO8yZ9HE0XvMnsQybQv0FfQKlERPSZ51eHnlAfV1SoPv10Yy+xUGUJ +5lhCLkMaTLTwJUdZ+gQek9QmRkpQgbLevni3/GcV4clXhB4PY9bpYrrWX1Uu6lzG +KAgEJTm4Diup8kyXHAc/DVL17e8vgg8CAwEAAaOB8jCB7zAfBgNVHSMEGDAWgBSg +EQojPpbxB+zirynvgqV/0DCktDAdBgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rID +ZsswDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAG +BgRVHSAAMEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29t +L0FBQUNlcnRpZmljYXRlU2VydmljZXMuY3JsMDQGCCsGAQUFBwEBBCgwJjAkBggr +BgEFBQcwAYYYaHR0cDovL29jc3AuY29tb2RvY2EuY29tMA0GCSqGSIb3DQEBDAUA +A4IBAQAYh1HcdCE9nIrgJ7cz0C7M7PDmy14R3iJvm3WOnnL+5Nb+qh+cli3vA0p+ +rvSNb3I8QzvAP+u431yqqcau8vzY7qN7Q/aGNnwU4M309z/+3ri0ivCRlv79Q2R+ +/czSAaF9ffgZGclCKxO/WIu6pKJmBHaIkU4MiRTOok3JMrO66BQavHHxW/BBC5gA +CiIDEOUMsfnNkjcZ7Tvx5Dq2+UUTJnWvu6rvP3t3O9LEApE9GQDTF1w52z97GA1F +zZOFli9d31kWTz9RvdVFGD/tSo7oBmF0Ixa1DVBzJ0RHfxBdiSprhTEUxOipakyA +vGp4z7h/jnZymQyd/teRCBaho1+V +-----END CERTIFICATE----- \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/server.test.pem.key b/docker/snowflake-connection/data/cache/server.test.pem.key new file mode 100644 index 0000000000..f4e1c0b9cc --- /dev/null +++ b/docker/snowflake-connection/data/cache/server.test.pem.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDPW8q9f0r+EoF8 +5PzNkhLel7JVliMPTLkCngU5W/cloPyQ7Erpq6MVMQzy5ESsM9AcRrTNYXVj6bm9 +MBCLU2Gvik0SJ6UeGu3nQZtnnW56bEnuJNldwtCu+l2YdOEPkb/5zTMPnZxS7taS +rthj0nomwdcBvfTaQtWixCycIXLKoJx+M06sauHnuofANpSNwYN0RlJHxcT0/ePc +rgxtA0ZmArrYuG+Vw9Gy0rJx1yobMjlg5WXNiQrpE/uUUWT/EncG7nbS2eMT7325 +98ZkFU9aNTrbEuCzBxgSp54SudjgwJz+CncgDqRiRFR7FxdgvdKNIJb/HyiN8s5T +cWzCQBdPAgMBAAECggEAGv/7SqhoBeQ3+yC/8C6MiXJcNLu7bfMSBg64ZGsep8Yq +DN7PtFR2hDxiUMA7VubaOsxUH4gIpo1Y85LuHI4rYpWSCoKiA+UCxEFtMFU1/Pfb +uogOy6Ah1x7fkAnsAkB6rFa1RtvBbqUNyIS+xWSzJhfIXMA0wTTBp5N+sYfDcDG0 +myrpuT5H84Is9vJmQytUhtp4uSaPERNj12HBpcADsH+Bk6ez+CLTiSvwVLCkaK+E +zGRes4868K5Lq2pj/msDxZa0YjkV1BU1jAve55O1zVaqZANPWjn8yKhvfgAVV4sP +p7/ORUrzeSA9X/HLbzTdOL/Kv3oCJRsEL2RJAiGS9QKBgQDrTwF6hOulGLWGJPJi +7Nbgz/tEdUhtiKE6i3phRKuYfqVKEg4x4hGw7RY2+V7Is22lx55MzdSFBNy8sGGs +Ij2N/qm61lBXhHJaELK5sZ2mnHf6nMJRBrJDsoFAe0YV5VhTYtq9Yw45AA9nCIw/ +z0pDCbcg3iTzw1zugkGs09GnKwKBgQDhl5tmYbnKYdkO1SOrEGvtSjCjIColh7zb +ZJ+e32ThgJxzueRQ77c83e556/gCkT6GT8oDJXn6qrcAriVJ7jdRUnkGLVy9Tzqc +mJ9dQxsssS97WhaPt5Ipv+LsIsCQm/eWubJusRmn72SBrxsoaqNhvu4tzqLjp45p +JGfhsr6+bQKBgQDJYAWt6o8X7Tt8H6ZnzrReFN++SHjBdIo2ZiNHltMbYFboOud2 +/TeSqHO4fFUHgba2h00MAaJ8bBrUSEZuX6c6G9T5lmuPWkPanCu4Cy8V5RYwnXMW +kJqCoQNIQbdLCck7I4B7T4hec5S64m/UM/wjvu6/7BzHmEuxujumQmhLnQKBgAgn +k816oN2o9dCscbKgUFZuhR2QbxWWN4RyubZjeuEP5hfk01T9pVEE8LblibyGBY2T +WskMVMFz5FOY9+4ZN1SwN4G6qAyLzaGVfsU/RL8z1HSQCBq/1v+9WPWSOAXCLYv8 +QG/x5OyGIcrySngGistgvHlZa9fw2ZwBXePxsyVtAoGAIfNEpG8Wmzz+O0cWUDgt +CRuN6Gcx+RsUMBkhMPkbGXw0UFQq94KCPkUcU3q9LWw6PhJ5XUxw93koljoYdkfp +JdaAvBscSGIK/d+RHtNlQuwbUFBD0SZfNyOdTNXK1TW7JFagy24law+3xcr8BjHG +CAFPNoOWtBM/qVl/+jzB5iY= +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle b/docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle new file mode 100644 index 0000000000..939025e709 Binary files /dev/null and b/docker/snowflake-connection/data/cache/service-catalog-3_7_1_dev1-1_35_5.pickle differ diff --git a/package-lock.json b/package-lock.json index 6219b20016..c991d65442 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "quadratic-kernels/python-wasm" ], "dependencies": { + "@ory/kratos-client": "^1.2.1", "tsc": "^2.0.4", "vitest": "^1.5.0", "zod": "^3.23.8" @@ -4284,12 +4285,73 @@ "version": "0.3.1", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], - "license": "MIT", "optional": true, "os": [ "darwin" @@ -4298,6 +4360,276 @@ "node": ">=12" } }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "dev": true, @@ -6168,6 +6500,14 @@ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", "dev": true }, + "node_modules/@ory/kratos-client": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ory/kratos-client/-/kratos-client-1.2.1.tgz", + "integrity": "sha512-HvipnVQotCKjEQC9I9DPjSlfBEww4pjDycMAKdUPj3g/0WkNSq6wbPDyqeclFz99rsOOsFMcpOO8qiCYHSgQeA==", + "dependencies": { + "axios": "^1.6.1" + } + }, "node_modules/@pixi/accessibility": { "version": "6.5.10", "license": "MIT", @@ -7407,8 +7747,7 @@ }, "node_modules/@radix-ui/react-menubar": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menubar/-/react-menubar-1.1.1.tgz", - "integrity": "sha512-V05Hryq/BE2m+rs8d5eLfrS0jmSWSDHEbG7jEyLA5D5J9jTvWj/o3v3xDN9YsOlH6QIkJgiaNDaP+S4T1rdykw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7438,13 +7777,11 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-arrow": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0" }, @@ -7465,8 +7802,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-collection": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -7490,8 +7826,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7504,8 +7839,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7518,8 +7852,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7532,8 +7865,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-dismissable-layer": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-compose-refs": "1.1.0", @@ -7558,8 +7890,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-guards": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7572,8 +7903,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-focus-scope": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", @@ -7596,8 +7926,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -7613,8 +7942,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-menu": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.1.tgz", - "integrity": "sha512-oa3mXRRVjHi6DZu/ghuzdylyjaMXLymx83irM7hTxutQbD+7IhPKdMdRHD26Rm+kHRrWcrUkkRPv5pd47a2xFQ==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7652,8 +7980,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-popper": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", + "license": "MIT", "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.0", @@ -7683,8 +8010,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-portal": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", + "license": "MIT", "dependencies": { "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -7706,8 +8032,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-presence": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -7729,8 +8054,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -7751,8 +8075,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -7781,8 +8104,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -7798,8 +8120,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7812,8 +8133,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -7829,8 +8149,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-escape-keydown": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -7846,8 +8165,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -7860,8 +8178,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-rect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "license": "MIT", "dependencies": { "@radix-ui/rect": "1.1.0" }, @@ -7877,8 +8194,7 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/react-use-size": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -7894,13 +8210,11 @@ }, "node_modules/@radix-ui/react-menubar/node_modules/@radix-ui/rect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + "license": "MIT" }, "node_modules/@radix-ui/react-menubar/node_modules/react-remove-scroll": { "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", + "license": "MIT", "dependencies": { "react-remove-scroll-bar": "^2.3.4", "react-style-singleton": "^2.2.1", @@ -8311,8 +8625,7 @@ }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-primitive": "2.0.0", @@ -8335,8 +8648,7 @@ }, "node_modules/@radix-ui/react-toggle-group": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -8363,13 +8675,11 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-collection": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-context": "1.1.0", @@ -8393,8 +8703,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8407,8 +8716,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-context": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8421,8 +8729,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-direction": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8435,8 +8742,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-id": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.0" }, @@ -8452,8 +8758,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -8474,8 +8779,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", + "license": "MIT", "dependencies": { "@radix-ui/primitive": "1.1.0", "@radix-ui/react-collection": "1.1.0", @@ -8504,8 +8808,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -8521,8 +8824,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8535,8 +8837,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -8552,8 +8853,7 @@ }, "node_modules/@radix-ui/react-toggle-group/node_modules/@radix-ui/react-use-layout-effect": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8566,13 +8866,11 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/primitive": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" + "license": "MIT" }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8585,8 +8883,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-primitive": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", + "license": "MIT", "dependencies": { "@radix-ui/react-slot": "1.1.0" }, @@ -8607,8 +8904,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "license": "MIT", "dependencies": { "@radix-ui/react-compose-refs": "1.1.0" }, @@ -8624,8 +8920,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "license": "MIT", "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -8638,8 +8933,7 @@ }, "node_modules/@radix-ui/react-toggle/node_modules/@radix-ui/react-use-controllable-state": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "license": "MIT", "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.0" }, @@ -8906,11 +9200,10 @@ }, "node_modules/@rollup/rollup-darwin-arm64": { "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", - "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", "cpu": [ "arm64" ], + "license": "MIT", "optional": true, "os": [ "darwin" @@ -9013,9 +9306,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.22.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", - "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", "cpu": [ "x64" ], @@ -14758,9 +15051,10 @@ "optional": true }, "node_modules/esbuild": { - "version": "0.20.2", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "hasInstallScript": true, - "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -14768,29 +15062,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/esbuild-wasm": { @@ -21704,7 +21998,7 @@ } }, "node_modules/openai": { - "version": "4.61.0", + "version": "4.60.0", "license": "Apache-2.0", "dependencies": { "@types/node": "^18.11.18", @@ -22367,8 +22661,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.0", - "license": "ISC" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==" }, "node_modules/picomatch": { "version": "2.3.1", @@ -22405,6 +22700,16 @@ "pixelmatch": "bin/pixelmatch" } }, + "node_modules/pixi-viewport": { + "version": "4.38.0", + "license": "MIT", + "peerDependencies": { + "@pixi/display": "^6.5.8", + "@pixi/interaction": "^6.5.8", + "@pixi/math": "^6.5.8", + "@pixi/ticker": "^6.5.8" + } + }, "node_modules/pixi.js": { "version": "6.5.10", "license": "MIT", @@ -22568,7 +22873,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "funding": [ { "type": "opencollective", @@ -22583,11 +22890,10 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" @@ -23805,8 +24111,7 @@ }, "node_modules/rollup": { "version": "4.22.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", - "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -23837,6 +24142,18 @@ "fsevents": "~2.3.2" } }, + "node_modules/rollup/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/run-async": { "version": "3.0.0", "dev": true, @@ -24385,8 +24702,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "license": "BSD-3-Clause", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "engines": { "node": ">=0.10.0" } @@ -26580,13 +26898,13 @@ } }, "node_modules/vite": { - "version": "5.2.14", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.14.tgz", - "integrity": "sha512-TFQLuwWLPms+NBNlh0D9LZQ+HXW471COABxw/9TEUBrjuHMo9BrYBPrN/SYAwIuVL+rLerycxiLT41t4f5MZpA==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", - "rollup": "^4.13.0" + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -26605,6 +26923,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -26622,6 +26941,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, @@ -27805,6 +28127,7 @@ "@aws-sdk/client-s3": "^3.427.0", "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", + "@ory/kratos-client": "^1.2.1", "@prisma/client": "^4.12.0", "@sendgrid/mail": "^8.1.0", "@sentry/node": "^7.50.0", @@ -27885,6 +28208,7 @@ "@monaco-editor/react": "^4.3.1", "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", + "@ory/kratos-client": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", @@ -28026,17 +28350,6 @@ "node": ">=10" } }, - "quadratic-client/node_modules/pixi-viewport": { - "version": "4.38.0", - "resolved": "https://registry.npmjs.org/pixi-viewport/-/pixi-viewport-4.38.0.tgz", - "integrity": "sha512-TGj6Ymk/BU0wZcW4c1eP4e96aETJmB7jhjBflMjQU06/ZHPy7qHw8JyDqZ+C84SEg0ewCHjDNZ2vgR3Kjk74BQ==", - "peerDependencies": { - "@pixi/display": "^6.5.8", - "@pixi/interaction": "^6.5.8", - "@pixi/math": "^6.5.8", - "@pixi/ticker": "^6.5.8" - } - }, "quadratic-client/node_modules/prettier-plugin-tailwindcss": { "version": "0.4.1", "dev": true, diff --git a/package.json b/package.json index 643c913f3a..962f4d4195 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "gen:pyright:worker": "npm run gen:pyright:worker --workspace=quadratic-kernels/python-wasm" }, "dependencies": { + "@ory/kratos-client": "^1.2.1", "tsc": "^2.0.4", "vitest": "^1.5.0", "zod": "^3.23.8" diff --git a/quadratic-api/.env.docker b/quadratic-api/.env.docker index 96c46112c0..f4f8144b51 100644 --- a/quadratic-api/.env.docker +++ b/quadratic-api/.env.docker @@ -1,6 +1,20 @@ +CORS="*" +DATABASE_URL="postgresql://postgres:postgres@host.docker.internal:5432/postgres" ENVIRONMENT=docker -DATABASE_URL='postgresql://postgres:postgres@postgres:5432/postgres' -AWS_S3_ENDPOINT=http://localstack:4566 +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET +OPENAI_API_KEY= +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN # Hex string to be used as the key for enctyption, use npm run key:generate ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 +AWS_S3_ENDPOINT=http://localstack:4566 + +# Storage +STORAGE_TYPE=s3 + +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/.env.example b/quadratic-api/.env.example index fd1ce14945..466ea24bf8 100644 --- a/quadratic-api/.env.example +++ b/quadratic-api/.env.example @@ -4,27 +4,37 @@ DATABASE_URL=postgresql://postgres:postgres@0.0.0.0:5432/postgres CORS='*' +OPENAI_API_KEY= +ANTHROPIC_API_KEY= + +SENTRY_DSN= + +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN + +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET + +ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 # auth0 or ory AUTH0_JWKS_URI=https://quadratic-community.us.auth0.com/.well-known/jwks.json AUTH0_ISSUER=https://quadratic-community.us.auth0.com/ AUTH0_DOMAIN=quadratic-community.us.auth0.com AUTH0_CLIENT_ID=DCPCvqyU5Q0bJD8Q3QmJEoV48x1zLH7W AUTH0_CLIENT_SECRET=94dp3PDcxlI9ZDqBSvkdjQHWgGdx0ZSeyTr5-Rn3Kcts-ZyTdj1FLlJjCyqrTXEG AUTH0_AUDIENCE=community-quadratic +ORY_JWKS_URI='http://host.docker.internal:3000/.well-known/jwks.json' +ORY_ADMIN_HOST=http://0.0.0.0:4434 +# Storage +STORAGE_TYPE=s3 # s3 or file-system +QUADRATIC_FILE_URI=http://localhost:3002 +QUADRATIC_FILE_URI_PUBLIC=http://localhost:3002 AWS_S3_REGION=us-east-2 AWS_S3_ACCESS_KEY_ID=test AWS_S3_SECRET_ACCESS_KEY=test AWS_S3_BUCKET_NAME=quadratic-api-docker -AWS_S3_ENDPOINT=http://0.0.0.0:4566 -ANTHROPIC_API_KEY= -OPENAI_API_KEY= - -SENTRY_DSN= - -M2M_AUTH_TOKEN=M2M_AUTH_TOKEN - -STRIPE_SECRET_KEY=STRIPE_SECRET_KEY -STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET - -ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/.env.test b/quadratic-api/.env.test index 0aa0efdebc..27d0e2cfca 100644 --- a/quadratic-api/.env.test +++ b/quadratic-api/.env.test @@ -1,16 +1,32 @@ +# DEBUG=express:* + DATABASE_URL='postgresql://prisma:prisma@localhost:5433/quadratic-api’' +M2M_AUTH_TOKEN=M2M_AUTH_TOKEN +STRIPE_SECRET_KEY=STRIPE_SECRET_KEY +STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET + +# Hex string to be used as the key for enctyption, use npm run key:generate +ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc + +# Auth +AUTH_TYPE=auth0 # auth0 or ory AUTH0_JWKS_URI='https://dev.us.auth0.com/.well-known/jwks.json' AUTH0_ISSUER='https://auth-dev.quadratic.to/' AUTH0_CLIENT_ID="AUTH0_CLIENT_ID" AUTH0_CLIENT_SECRET="AUTH0_CLIENT_SECRET" AUTH0_DOMAIN="AUTH0_DOMAIN" +ORY_JWKS_URI='http://host.docker.internal:3000/.well-known/jwks.json' +ORY_ADMIN_HOST=http://0.0.0.0:4434 + +# Storage +STORAGE_TYPE=s3 # s3 or file-system +QUADRATIC_FILE_URI=http://localhost:3002 +QUADRATIC_FILE_URI_PUBLIC=http://localhost:3002 AWS_S3_REGION=us-west-2 AWS_S3_BUCKET_NAME=AWS_S3_BUCKET_NAME AWS_S3_ACCESS_KEY_ID=AWS_S3_ACCESS_KEY_ID AWS_S3_SECRET_ACCESS_KEY=AWS_S3_SECRET_ACCESS_KEY -M2M_AUTH_TOKEN=M2M_AUTH_TOKEN -STRIPE_SECRET_KEY=STRIPE_SECRET_KEY -STRIPE_WEBHOOK_SECRET=STRIPE_WEBHOOK_SECRET +AWS_S3_ENDPOINT=http://0.0.0.0:4566 -# Hex string to be used as the key for enctyption, use npm run key:generate -ENCRYPTION_KEY=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc +# Admin +LICENSE_KEY=LICENSE_KEY diff --git a/quadratic-api/Dockerfile b/quadratic-api/Dockerfile index 7ac0726780..77a8ba4e16 100644 --- a/quadratic-api/Dockerfile +++ b/quadratic-api/Dockerfile @@ -10,5 +10,6 @@ FROM node:18-slim AS runtime WORKDIR /app COPY --from=builder /app . RUN apt-get update && apt install -y openssl +RUN npm run postinstall --workspace=quadratic-api RUN npm run build:prod --workspace=quadratic-api CMD ["npm", "start:prod"] diff --git a/quadratic-api/jest.setup.js b/quadratic-api/jest.setup.js index 6f9e6c3d93..ce11ad241b 100644 --- a/quadratic-api/jest.setup.js +++ b/quadratic-api/jest.setup.js @@ -1,3 +1,5 @@ +const { multerS3Storage } = require('./src/storage/s3'); + // For auth we expect the following Authorization header format: // Bearer ValidToken {user.sub} jest.mock('./src/middleware/validateAccessToken', () => { @@ -16,13 +18,32 @@ jest.mock('./src/middleware/validateAccessToken', () => { }; }); -jest.mock('./src/aws/s3', () => { +const licenseClientResponse = { + limits: { + seats: 10, + }, + status: 'active', +}; + +jest.mock('./src/licenseClient', () => { + return { + licenseClient: { + post: async () => licenseClientResponse, + checkFromServer: async () => licenseClientResponse, + check: async () => licenseClientResponse, + }, + }; +}); + +jest.mock('./src/storage/storage', () => { return { s3Client: {}, - generatePresignedUrl: jest.fn().mockImplementation(async (str) => str), - uploadStringAsFileS3: jest.fn().mockImplementation(async () => { + getFileUrl: jest.fn().mockImplementation(async (str) => str), + getPresignedFileUrl: jest.fn().mockImplementation(async (str) => str), + uploadFile: jest.fn().mockImplementation(async () => { return { bucket: 'test-bucket', key: 'test-key' }; }), + uploadMiddleware: multerS3Storage, }; }); diff --git a/quadratic-api/package.json b/quadratic-api/package.json index 2252f1bd22..af942ab1a7 100644 --- a/quadratic-api/package.json +++ b/quadratic-api/package.json @@ -35,6 +35,7 @@ "@aws-sdk/client-s3": "^3.427.0", "@aws-sdk/client-secrets-manager": "^3.441.0", "@aws-sdk/s3-request-presigner": "^3.427.0", + "@ory/kratos-client": "^1.2.1", "@prisma/client": "^4.12.0", "@sendgrid/mail": "^8.1.0", "@sentry/node": "^7.50.0", diff --git a/quadratic-api/prisma/schema.prisma b/quadratic-api/prisma/schema.prisma index 6c09546b85..a81721cb91 100644 --- a/quadratic-api/prisma/schema.prisma +++ b/quadratic-api/prisma/schema.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["native", "linux-arm64-openssl-3.0.x"] + binaryTargets = ["native", "linux-arm64-openssl-3.0.x", "debian-openssl-3.0.x"] } datasource db { diff --git a/quadratic-api/src/auth/auth.ts b/quadratic-api/src/auth/auth.ts new file mode 100644 index 0000000000..74df06ee67 --- /dev/null +++ b/quadratic-api/src/auth/auth.ts @@ -0,0 +1,53 @@ +import { AUTH_TYPE } from '../env-vars'; +import { getUsersFromAuth0, jwtConfigAuth0, lookupUsersFromAuth0ByEmail } from './auth0'; +import { getUsersFromOry, getUsersFromOryByEmail, jwtConfigOry } from './ory'; + +export type UsersRequest = { + id: number; + auth0Id: string; +}; + +export type User = { + id: number; + auth0Id: string; + email: string; + name?: string | undefined; + picture?: string | undefined; +}; + +export type ByEmailUser = { + user_id?: string; +}; + +export const getUsers = async (users: UsersRequest[]): Promise> => { + switch (AUTH_TYPE) { + case 'auth0': + return await getUsersFromAuth0(users); + case 'ory': + return await getUsersFromOry(users); + default: + throw new Error(`Unsupported auth type in getUsers(): ${AUTH_TYPE}`); + } +}; + +export const getUsersByEmail = async (email: string): Promise => { + switch (AUTH_TYPE) { + case 'auth0': + return await lookupUsersFromAuth0ByEmail(email); + case 'ory': + return await getUsersFromOryByEmail(email); + default: + throw new Error(`Unsupported auth type in getUsersByEmail(): ${AUTH_TYPE}`); + } +}; + +export const jwtConfig = () => { + switch (AUTH_TYPE) { + case 'auth0': + return jwtConfigAuth0; + case 'ory': + return jwtConfigOry; + default: + throw new Error(`Unsupported auth type in jwtConfig(): ${AUTH_TYPE}`); + } +}; diff --git a/quadratic-api/src/auth0/profile.ts b/quadratic-api/src/auth/auth0.ts similarity index 70% rename from quadratic-api/src/auth0/profile.ts rename to quadratic-api/src/auth/auth0.ts index 932b74e82d..8fbbe46378 100644 --- a/quadratic-api/src/auth0/profile.ts +++ b/quadratic-api/src/auth/auth0.ts @@ -1,6 +1,16 @@ import * as Sentry from '@sentry/node'; import { ManagementClient } from 'auth0'; -import { AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN } from '../env-vars'; +import { Algorithm } from 'jsonwebtoken'; +import jwksRsa, { GetVerificationKey } from 'jwks-rsa'; +import { + AUTH0_AUDIENCE, + AUTH0_CLIENT_ID, + AUTH0_CLIENT_SECRET, + AUTH0_DOMAIN, + AUTH0_ISSUER, + AUTH0_JWKS_URI, +} from '../env-vars'; +import { ByEmailUser } from './auth'; // Guide to Setting up on Auth0 // 1. Create an Auth0 Machine to Machine Application @@ -11,12 +21,20 @@ import { AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET, AUTH0_DOMAIN } from '../env-vars' // We need to use account linking to ensure only one account per user // https://auth0.com/docs/customize/extensions/account-link-extension -const auth0 = new ManagementClient({ - domain: AUTH0_DOMAIN, - clientId: AUTH0_CLIENT_ID, - clientSecret: AUTH0_CLIENT_SECRET, - scope: 'read:users', -}); +let auth0: ManagementClient | undefined; + +const getAuth0 = () => { + if (!auth0) { + auth0 = auth0 = new ManagementClient({ + domain: AUTH0_DOMAIN, + clientId: AUTH0_CLIENT_ID, + clientSecret: AUTH0_CLIENT_SECRET, + scope: 'read:users', + }); + } + + return auth0; +}; /** * Given a list of users from our system, we lookup their info in Auth0. @@ -45,7 +63,7 @@ export const getUsersFromAuth0 = async (users: { id: number; auth0Id: string }[] // Search for users on Auth0 const auth0Ids = users.map(({ auth0Id }) => auth0Id); - const auth0Users = await auth0.getUsers({ + const auth0Users = await getAuth0().getUsers({ q: `user_id:(${auth0Ids.join(' OR ')})`, }); @@ -92,7 +110,19 @@ export const getUsersFromAuth0 = async (users: { id: number; auth0Id: string }[] return usersById; }; -export const lookupUsersFromAuth0ByEmail = async (email: string) => { - const auth0Users = await auth0.getUsersByEmail(email); +export const lookupUsersFromAuth0ByEmail = async (email: string): Promise => { + const auth0Users = await getAuth0().getUsersByEmail(email); return auth0Users; }; + +export const jwtConfigAuth0 = { + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: AUTH0_JWKS_URI, + }) as GetVerificationKey, + audience: AUTH0_AUDIENCE, + issuer: AUTH0_ISSUER, + algorithms: ['RS256'] as Algorithm[], +}; diff --git a/quadratic-api/src/auth/ory.ts b/quadratic-api/src/auth/ory.ts new file mode 100644 index 0000000000..e818e76c97 --- /dev/null +++ b/quadratic-api/src/auth/ory.ts @@ -0,0 +1,85 @@ +import { Configuration, IdentityApi } from '@ory/kratos-client'; +import * as Sentry from '@sentry/node'; +import { Algorithm } from 'jsonwebtoken'; +import jwksRsa, { GetVerificationKey } from 'jwks-rsa'; +import { ORY_ADMIN_HOST, ORY_JWKS_URI } from '../env-vars'; +import { ByEmailUser, User } from './auth'; + +const config = new Configuration({ + basePath: ORY_ADMIN_HOST, + baseOptions: { + withCredentials: true, + }, +}); +const sdk = new IdentityApi(config); + +export const jwtConfigOry = { + secret: jwksRsa.expressJwtSecret({ + cache: true, + rateLimit: true, + jwksRequestsPerMinute: 5, + jwksUri: ORY_JWKS_URI, + }) as GetVerificationKey, + algorithms: ['RS256'] as Algorithm[], +}; + +export const getUsersFromOry = async (users: { id: number; auth0Id: string }[]): Promise> => { + // If we got nothing, we return an empty object + if (users.length === 0) return {}; + + const ids = users.map(({ auth0Id }) => auth0Id); + let identities; + + try { + identities = (await sdk.listIdentities({ ids })).data; + } catch (e) { + console.error(e); + return {}; + } + + // Map users by their Quadratic ID. If we didn't find a user, throw. + const usersById: Record = users.reduce((acc: Record, { id, auth0Id }) => { + const oryUser = identities.find(({ id }) => id === auth0Id); + + // If we're missing data we expect, log it to Sentry and skip this user + if (!oryUser || oryUser.traits.email === undefined) { + Sentry.captureException({ + message: 'Ory user returned without `email`', + level: 'error', + extra: { + auth0IdInOurDb: auth0Id, + oryUserResult: oryUser, + }, + }); + throw new Error('Failed to retrieve all user info from Ory'); + } + + const { email, name } = oryUser.traits; + + return { + ...acc, + [id]: { + id, + auth0Id, + email, + name: `${name.first} ${name.last}`, + picture: undefined, + }, + }; + }, {}); + + return usersById; +}; + +export const getUsersFromOryByEmail = async (email: string): Promise => { + let identities; + + try { + identities = (await sdk.listIdentities({ credentialsIdentifier: email })).data; + } catch (e) { + console.error(e); + return []; + } + + return identities.map(({ id }) => ({ user_id: id })); +}; diff --git a/quadratic-api/src/aws/s3.ts b/quadratic-api/src/aws/s3.ts deleted file mode 100644 index a42ef03cb7..0000000000 --- a/quadratic-api/src/aws/s3.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; -import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; -import { - AWS_S3_ACCESS_KEY_ID, - AWS_S3_BUCKET_NAME, - AWS_S3_ENDPOINT, - AWS_S3_REGION, - AWS_S3_SECRET_ACCESS_KEY, -} from '../env-vars'; -const endpoint = AWS_S3_ENDPOINT; - -// Initialize S3 client -export const s3Client = new S3Client({ - region: AWS_S3_REGION, - credentials: { - accessKeyId: AWS_S3_ACCESS_KEY_ID, - secretAccessKey: AWS_S3_SECRET_ACCESS_KEY, - }, - endpoint, - forcePathStyle: true, -}); - -export const uploadStringAsFileS3 = async (fileKey: string, contents: string) => { - const command = new PutObjectCommand({ - Bucket: AWS_S3_BUCKET_NAME, - Key: fileKey, - Body: new Uint8Array(Buffer.from(contents, 'base64')), - // Optionally, you can add other configuration like ContentType - // ContentType: 'text/plain' - }); - const response = await s3Client.send(command); - - // Check if the upload was successful - if (response && response.$metadata.httpStatusCode === 200) { - return { - bucket: AWS_S3_BUCKET_NAME, - key: fileKey, - }; - } else { - throw new Error('Failed to upload file to S3'); - } -}; - -// Get file URL from S3 -export const generatePresignedUrl = async (key: string) => { - const command = new GetObjectCommand({ - Bucket: AWS_S3_BUCKET_NAME, - Key: key, - }); - - return await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 * 24 * 7 }); // 7 days -}; diff --git a/quadratic-api/src/env-vars.ts b/quadratic-api/src/env-vars.ts index fc94885e7c..fc5c6224d4 100644 --- a/quadratic-api/src/env-vars.ts +++ b/quadratic-api/src/env-vars.ts @@ -10,34 +10,28 @@ export const NODE_ENV = process.env.NODE_ENV || 'development'; export const PORT = process.env.PORT || 8000; export const AWS_S3_ENDPOINT = process.env.AWS_S3_ENDPOINT || undefined; export const ENVIRONMENT = process.env.ENVIRONMENT; - -// Required export const AUTH0_DOMAIN = process.env.AUTH0_DOMAIN as string; export const AUTH0_CLIENT_ID = process.env.AUTH0_CLIENT_ID as string; export const AUTH0_CLIENT_SECRET = process.env.AUTH0_CLIENT_SECRET as string; export const AUTH0_JWKS_URI = process.env.AUTH0_JWKS_URI as string; export const AUTH0_ISSUER = process.env.AUTH0_ISSUER as string; export const AUTH0_AUDIENCE = process.env.AUTH0_AUDIENCE as string; +export const ORY_JWKS_URI = process.env.ORY_JWKS_URI as string; +export const ORY_ADMIN_HOST = process.env.ORY_ADMIN_HOST as string; +export const QUADRATIC_FILE_URI = process.env.QUADRATIC_FILE_URI as string; +export const QUADRATIC_FILE_URI_PUBLIC = process.env.QUADRATIC_FILE_URI_PUBLIC as string; export const AWS_S3_REGION = process.env.AWS_S3_REGION as string; export const AWS_S3_ACCESS_KEY_ID = process.env.AWS_S3_ACCESS_KEY_ID as string; export const AWS_S3_SECRET_ACCESS_KEY = process.env.AWS_S3_SECRET_ACCESS_KEY as string; export const AWS_S3_BUCKET_NAME = process.env.AWS_S3_BUCKET_NAME as string; + +// Required export const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY as string; export const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY as string; -[ - 'AUTH0_DOMAIN', - 'AUTH0_CLIENT_ID', - 'AUTH0_CLIENT_SECRET', - 'AUTH0_JWKS_URI', - 'AUTH0_ISSUER', - 'AUTH0_AUDIENCE', - 'AWS_S3_REGION', - 'AWS_S3_ACCESS_KEY_ID', - 'AWS_S3_SECRET_ACCESS_KEY', - 'AWS_S3_BUCKET_NAME', - 'STRIPE_SECRET_KEY', - 'ENCRYPTION_KEY', -].forEach(ensureEnvVarExists); +export const STORAGE_TYPE = process.env.STORAGE_TYPE as string; +export const AUTH_TYPE = process.env.AUTH_TYPE as string; +export const LICENSE_KEY = process.env.LICENSE_KEY as string; +['STRIPE_SECRET_KEY', 'ENCRYPTION_KEY', 'STORAGE_TYPE', 'AUTH_TYPE', 'LICENSE_KEY'].forEach(ensureEnvVarExists); // Required in prod, optional locally export const M2M_AUTH_TOKEN = process.env.M2M_AUTH_TOKEN; @@ -50,6 +44,10 @@ if (NODE_ENV === 'production') { ['M2M_AUTH_TOKEN', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', 'SLACK_FEEDBACK_URL'].forEach(ensureEnvVarExists); } +// Intentionally hard-coded to avoid this being environment-configurable +// NOTE: Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. +export const LICENSE_API_URI = 'https://selfhost.quadratichq.com'; + ensureSampleTokenNotUsedInProduction(); function ensureEnvVarExists(key: string) { diff --git a/quadratic-api/src/internal/addUserToTeam.ts b/quadratic-api/src/internal/addUserToTeam.ts index 2c8fadf2cd..0bce08cf28 100644 --- a/quadratic-api/src/internal/addUserToTeam.ts +++ b/quadratic-api/src/internal/addUserToTeam.ts @@ -1,5 +1,6 @@ import { TeamRole } from '@prisma/client'; import dbClient from '../dbClient'; +import { licenseClient } from '../licenseClient'; export const addUserToTeam = async (args: { userId: number; teamId: number; role: TeamRole }) => { const { userId, teamId, role } = args; @@ -13,6 +14,9 @@ export const addUserToTeam = async (args: { userId: number; teamId: number; role }, }); + // update user count in the license server + await licenseClient.check(true); + // Update the seat quantity on the team's stripe subscription // await updateSeatQuantity(teamId); diff --git a/quadratic-api/src/internal/removeUserFromTeam.ts b/quadratic-api/src/internal/removeUserFromTeam.ts index 1e5f2ac94d..63fb5e14cf 100644 --- a/quadratic-api/src/internal/removeUserFromTeam.ts +++ b/quadratic-api/src/internal/removeUserFromTeam.ts @@ -1,4 +1,5 @@ import dbClient from '../dbClient'; +import { licenseClient } from '../licenseClient'; export const removeUserFromTeam = async (userId: number, teamId: number) => { await dbClient.$transaction(async (prisma) => { @@ -23,6 +24,9 @@ export const removeUserFromTeam = async (userId: number, teamId: number) => { }); }); + // update user count in the license server + await licenseClient.check(true); + // Update the seat quantity on the team's stripe subscription // await updateSeatQuantity(teamId); }; diff --git a/quadratic-api/src/licenseClient.ts b/quadratic-api/src/licenseClient.ts new file mode 100644 index 0000000000..5a82ee3bf0 --- /dev/null +++ b/quadratic-api/src/licenseClient.ts @@ -0,0 +1,78 @@ +//! License Client +//! +//! Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. + +import axios from 'axios'; +import { LicenseSchema } from 'quadratic-shared/typesAndSchemas'; +import z from 'zod'; +import dbClient from './dbClient'; +import { LICENSE_API_URI, LICENSE_KEY } from './env-vars'; +import { hash } from './utils/crypto'; + +type LicenseResponse = z.infer; + +let cachedResult: LicenseResponse | null = null; +let lastCheckedTime: number | null = null; +const cacheDuration = 15 * 60 * 1000; // 15 minutes in milliseconds +// const cacheDuration = 0; // disable the cache for testing + +export const licenseClient = { + post: async (seats: number): Promise => { + try { + const body = { stats: { seats } }; + const response = await axios.post(`${LICENSE_API_URI}/api/license/${LICENSE_KEY}`, body); + + return LicenseSchema.parse(response.data) as LicenseResponse; + } catch (err) { + if (err instanceof Error) { + console.error('Failed to get the license info from the license service:', err.message); + } + + return null; + } + }, + checkFromServer: async (): Promise => { + // NOTE: Modifying this license check is violating the Quadratic Terms and Conditions and is stealing software, and we will come after you. + if (hash(LICENSE_KEY) === '2ef876ddfe6cc783b83ac63cbef0ae84e6807c69fa72066801f130706e2a935a') { + return licenseClient.adminLicenseResponse(); + } + + const userCount = await dbClient.user.count(); + + return licenseClient.post(userCount); + }, + adminLicenseResponse: async (): Promise => { + return { + limits: { + seats: 100000000000, + }, + status: 'active', + }; + }, + /** + * + * @param force boolean to force a license check (ignoring the cache) + * @returns + */ + check: async (force: boolean): Promise => { + const currentTime = Date.now(); + + if (!force && cachedResult && lastCheckedTime && currentTime - lastCheckedTime < cacheDuration) { + // Use cached result if within the cache duration + return cachedResult; + } + // Otherwise, perform the check + const result = await licenseClient.checkFromServer(); + + // don't cache errors or non-active licenses + if (!result || result.status === 'revoked') { + return null; + } + + // Cache the result and update the last checked time + cachedResult = result; + lastCheckedTime = currentTime; + + return result; + }, +}; diff --git a/quadratic-api/src/middleware/user.ts b/quadratic-api/src/middleware/user.ts index 4bf5dfe2cd..e2fe6f2fb9 100644 --- a/quadratic-api/src/middleware/user.ts +++ b/quadratic-api/src/middleware/user.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from 'express'; -import { getUsersFromAuth0 } from '../auth0/profile'; +import { getUsers } from '../auth/auth'; import dbClient from '../dbClient'; import { addUserToTeam } from '../internal/addUserToTeam'; import { RequestWithAuth, RequestWithOptionalAuth, RequestWithUser } from '../types/Request'; @@ -8,7 +8,7 @@ const runFirstTimeUserLogic = async (user: Awaited { auth0Id, }, }); + if (user) { return user; } diff --git a/quadratic-api/src/middleware/validateAccessToken.ts b/quadratic-api/src/middleware/validateAccessToken.ts index b256bff7d9..e73dab1ecb 100644 --- a/quadratic-api/src/middleware/validateAccessToken.ts +++ b/quadratic-api/src/middleware/validateAccessToken.ts @@ -1,16 +1,5 @@ -import { GetVerificationKey, expressjwt } from 'express-jwt'; -import jwksRsa from 'jwks-rsa'; -import { AUTH0_AUDIENCE, AUTH0_ISSUER, AUTH0_JWKS_URI } from '../env-vars'; +import { Params, expressjwt } from 'express-jwt'; +import { jwtConfig } from '../auth/auth'; // based on implementation from https://github.com/auth0-developer-hub/api_express_typescript_hello-world/blob/main/src/middleware/auth0.middleware.ts -export const validateAccessToken = expressjwt({ - secret: jwksRsa.expressJwtSecret({ - cache: true, - rateLimit: true, - jwksRequestsPerMinute: 5, - jwksUri: AUTH0_JWKS_URI, - }) as GetVerificationKey, - audience: AUTH0_AUDIENCE, - issuer: AUTH0_ISSUER, - algorithms: ['RS256'], -}); +export const validateAccessToken = expressjwt(jwtConfig() as Params); diff --git a/quadratic-api/src/routes/v0/education.POST.ts b/quadratic-api/src/routes/v0/education.POST.ts index c3f2002c89..97c35274eb 100644 --- a/quadratic-api/src/routes/v0/education.POST.ts +++ b/quadratic-api/src/routes/v0/education.POST.ts @@ -1,7 +1,7 @@ import { Response } from 'express'; import { sanityClient } from 'quadratic-shared/sanityClient'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { getUsersFromAuth0 } from '../../auth0/profile'; +import { getUsers } from '../../auth/auth'; import universityDomains from '../../data/universityDomains'; import dbClient from '../../dbClient'; import { userMiddleware } from '../../middleware/user'; @@ -19,7 +19,7 @@ async function handler(req: RequestWithUser, res: Response { diff --git a/quadratic-api/src/routes/v0/examples.POST.ts b/quadratic-api/src/routes/v0/examples.POST.ts index 663f1c2561..dc40cdc36c 100644 --- a/quadratic-api/src/routes/v0/examples.POST.ts +++ b/quadratic-api/src/routes/v0/examples.POST.ts @@ -1,3 +1,4 @@ +import axios from 'axios'; import { Response } from 'express'; import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas'; import z from 'zod'; @@ -27,6 +28,12 @@ async function handler(req: RequestWithUser, res: Response res.json())) as ApiTypes['/v0/files/:uuid.GET.response']; + } = (await axios.get(apiUrl).then((res) => res.data)) as ApiTypes['/v0/files/:uuid.GET.response']; // Fetch the contents of the file - const fileContents = await fetch(lastCheckpointDataUrl).then((res) => res.arrayBuffer()); + const fileContents = await axios + .get(lastCheckpointDataUrl, { responseType: 'arraybuffer' }) + .then((res) => res.data); const buffer = new Uint8Array(fileContents); // Create a private file for the user in the requested team @@ -56,6 +65,7 @@ async function handler(req: RequestWithUser, res: Response void) => { - cb(null, { fieldName: file.fieldname }); - }, - key: (req: Request, file: Express.Multer.File, cb: (error: Error | null, key: string) => void) => { - const fileUuid = req.params.uuid; - cb(null, `${fileUuid}-${file.originalname}`); - }, - }) as StorageEngine, -}); - async function handler(req: RequestWithUser & RequestWithFile, res: Response) { const { params: { uuid }, @@ -65,6 +48,6 @@ export default [ ), validateAccessToken, userMiddleware, - uploadThumbnailToS3.single('thumbnail'), + uploadMiddleware().single('thumbnail'), handler, ]; diff --git a/quadratic-api/src/routes/v0/files.GET.ts b/quadratic-api/src/routes/v0/files.GET.ts index 1f51c9119c..71be69ef34 100644 --- a/quadratic-api/src/routes/v0/files.GET.ts +++ b/quadratic-api/src/routes/v0/files.GET.ts @@ -1,9 +1,9 @@ import { Response } from 'express'; import { ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { generatePresignedUrl } from '../../aws/s3'; import dbClient from '../../dbClient'; import { userMiddleware } from '../../middleware/user'; import { validateAccessToken } from '../../middleware/validateAccessToken'; +import { getFileUrl } from '../../storage/storage'; import { RequestWithUser } from '../../types/Request'; import { ResponseError } from '../../types/Response'; @@ -44,7 +44,7 @@ async function handler(req: RequestWithUser, res: Response { if (file.thumbnail) { - file.thumbnail = await generatePresignedUrl(file.thumbnail); + file.thumbnail = await getFileUrl(file.thumbnail); } }) ); diff --git a/quadratic-api/src/routes/v0/files.POST.ts b/quadratic-api/src/routes/v0/files.POST.ts index b10b45c31c..12b5e63d1b 100644 --- a/quadratic-api/src/routes/v0/files.POST.ts +++ b/quadratic-api/src/routes/v0/files.POST.ts @@ -23,6 +23,12 @@ async function handler(req: RequestWithUser, res: Response) { +async function handler(req: Request, res: Response) { const { params: { uuid }, } = parseRequest(req, schema); @@ -88,14 +91,35 @@ async function handler(req: Request, res: Response user)); + // Get user info from auth + const authUsersById = await getUsers(dbUsers.map(({ user }) => user)); + + // IDEA: (enhancement) we could put this in /sharing and just return the userCount + // then require the data for the team share modal to be a seaparte network request + const users = dbUsers + .filter(({ userId: id }) => authUsersById[id]) + .map(({ userId: id, role }) => { + const { email, name, picture } = authUsersById[id]; + return { + id, + email, + role, + name, + picture, + }; + }); + + const license = await licenseClient.check(false); + + if (!license) { + throw new ApiError(500, 'Unable to retrieve license'); + } // Get signed thumbnail URLs await Promise.all( dbFiles.map(async (file) => { if (file.thumbnail) { - file.thumbnail = await generatePresignedUrl(file.thumbnail); + file.thumbnail = await getPresignedFileUrl(file.thumbnail); } }) ); @@ -115,18 +139,7 @@ async function handler(req: Request, res: Response { - const { email, name, picture } = auth0UsersById[id]; - return { - id, - email, - role, - name, - picture, - }; - }), + users, invites: dbInvites.map(({ email, role, id }) => ({ email, role, id })), files: dbFiles .filter((file) => !file.ownerUserId) @@ -172,6 +185,7 @@ async function handler(req: Request, res: Response ({ uuid: connection.uuid, name: connection.name, diff --git a/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts b/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts index 0af3fe7461..2e3866304f 100644 --- a/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts +++ b/quadratic-api/src/routes/v0/teams.$uuid.invites.POST.ts @@ -2,7 +2,7 @@ import * as Sentry from '@sentry/node'; import { Response } from 'express'; import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas'; import { z } from 'zod'; -import { getUsersFromAuth0, lookupUsersFromAuth0ByEmail } from '../../auth0/profile'; +import { getUsers, getUsersByEmail } from '../../auth/auth'; import dbClient from '../../dbClient'; import { sendEmail } from '../../email/sendEmail'; import { templates } from '../../email/templates'; @@ -68,7 +68,7 @@ async function handler(req: RequestWithUser, res: Response { diff --git a/quadratic-api/src/storage/fileSystem.ts b/quadratic-api/src/storage/fileSystem.ts new file mode 100644 index 0000000000..d43f56eb4a --- /dev/null +++ b/quadratic-api/src/storage/fileSystem.ts @@ -0,0 +1,114 @@ +import axios from 'axios'; +import { Request } from 'express'; +import multer from 'multer'; +import stream, { Readable } from 'node:stream'; +import { QUADRATIC_FILE_URI, QUADRATIC_FILE_URI_PUBLIC } from '../env-vars'; +import { UploadFile } from '../types/Request'; +import { encryptFromEnv } from '../utils/crypto'; +import { UploadFileResponse } from './storage'; + +const generateUrl = (key: string, isPublic: boolean): string => { + const baseUrl = isPublic ? QUADRATIC_FILE_URI_PUBLIC : QUADRATIC_FILE_URI; + return `${baseUrl}/storage/${key}`; +}; + +const generatePresignedUrl = (key: string): string => generateUrl(`presigned/${key}`, true); + +// Get the URL for a given file (key) for the file service. +export const getStorageUrl = (key: string): string => { + return generateUrl(key, true); +}; + +// Get a presigned URL for a given file (key) for the file service. +export const getPresignedStorageUrl = (key: string): string => { + const encrypted = encryptFromEnv(key); + return generatePresignedUrl(encrypted); +}; + +// Upload a file to the file service. +export const upload = async (key: string, contents: string | Uint8Array, jwt: string): Promise => { + const url = generateUrl(key, false); + + if (typeof contents === 'string') { + contents = new Uint8Array(Buffer.from(contents, 'base64')); + } + + try { + const response = await axios + .post(url, contents, { + headers: { + 'Content-Type': 'text/plain', + Authorization: `${jwt}`, + }, + }) + .then((res) => res.data); + + return response; + } catch (e) { + console.error(e); + throw new Error(`Failed to upload file to ${url}`); + } +}; + +// Collect a full stream and place in a byte array. +function streamToByteArray(stream: Readable): Promise { + return new Promise((resolve, reject) => { + const chunks: Buffer[] = []; + + stream.on('data', (chunk: Buffer) => { + chunks.push(chunk); + }); + + stream.on('end', () => { + const buffer = Buffer.concat(chunks); + resolve(new Uint8Array(buffer)); + }); + + stream.on('error', (err: Error) => { + reject(err); + }); + }); +} + +// Multer storage engine for file-system storage. +// +// This middleware is used to handled client upload files and send them to +// the file service. +export const multerFileSystemStorage: multer.Multer = multer({ + storage: { + _handleFile( + req: Request, + file: Express.Multer.File & UploadFile, + cb: (error?: any, info?: Partial) => void + ): void { + const fileUuid = req.params.uuid; + const key = `${fileUuid}-${file.originalname}`; + const jwt = req.header('Authorization'); + + file.key = key; + + if (!jwt) { + cb('No authorization header'); + return; + } + + // Create a pass-through stream to pipe the file stream to + const passThrough = new stream.PassThrough(); + file.stream.pipe(passThrough); + + // Collect the stream and upload to the file service + streamToByteArray(passThrough) + .then((data) => { + upload(key, data, jwt) + .then((_response) => cb(null, file)) + .catch((error) => cb(error)); + }) + .catch((error) => cb(error)); + }, + + // only implement if needed (not currently used) + _removeFile(_req: Request, _file: Express.Multer.File, cb: (error: Error | null) => void): void { + cb(null); + }, + }, +}); diff --git a/quadratic-api/src/storage/s3.ts b/quadratic-api/src/storage/s3.ts new file mode 100644 index 0000000000..4fe6db1b23 --- /dev/null +++ b/quadratic-api/src/storage/s3.ts @@ -0,0 +1,81 @@ +import { GetObjectCommand, PutObjectCommand, S3Client } from '@aws-sdk/client-s3'; +import { getSignedUrl } from '@aws-sdk/s3-request-presigner'; +import { Request } from 'express'; +import multer, { StorageEngine } from 'multer'; +import multerS3 from 'multer-s3'; +import { + AWS_S3_ACCESS_KEY_ID, + AWS_S3_BUCKET_NAME, + AWS_S3_ENDPOINT, + AWS_S3_REGION, + AWS_S3_SECRET_ACCESS_KEY, +} from '../env-vars'; +import { UploadFileResponse } from './storage'; + +const endpoint = AWS_S3_ENDPOINT; +let s3Client: S3Client; + +// Get S3 client slngleton +const getS3Client = () => { + if (!s3Client) { + s3Client = new S3Client({ + region: AWS_S3_REGION, + credentials: { + accessKeyId: AWS_S3_ACCESS_KEY_ID, + secretAccessKey: AWS_S3_SECRET_ACCESS_KEY, + }, + endpoint, + forcePathStyle: true, + }); + } + + return s3Client; +}; + +// Upload a string as a file to S3 +export const uploadStringAsFileS3 = async (fileKey: string, contents: string): Promise => { + const command = new PutObjectCommand({ + Bucket: AWS_S3_BUCKET_NAME, + Key: fileKey, + Body: new Uint8Array(Buffer.from(contents, 'base64')), + // Optionally, you can add other configuration like ContentType + // ContentType: 'text/plain' + }); + const response = await getS3Client().send(command); + + // Check if the upload was successful + if (response && response.$metadata.httpStatusCode === 200) { + return { + bucket: AWS_S3_BUCKET_NAME, + key: fileKey, + }; + } else { + throw new Error('Failed to upload file to S3'); + } +}; + +// Multer storage engine for S3 +export const multerS3Storage = (): multer.Multer => + multer({ + storage: multerS3({ + s3: getS3Client(), + bucket: AWS_S3_BUCKET_NAME, + metadata: (req: Request, file: Express.Multer.File, cb: (error: Error | null, metadata: any) => void) => { + cb(null, { fieldName: file.fieldname }); + }, + key: (req: Request, file: Express.Multer.File, cb: (error: Error | null, key: string) => void) => { + const fileUuid = req.params.uuid; + cb(null, `${fileUuid}-${file.originalname}`); + }, + }) as StorageEngine, + }); + +// Get the presigned file URL from S3 +export const generatePresignedUrl = async (key: string): Promise => { + const command = new GetObjectCommand({ + Bucket: AWS_S3_BUCKET_NAME, + Key: key, + }); + + return await getSignedUrl(s3Client, command, { expiresIn: 60 * 60 * 24 * 7 }); // 7 days +}; diff --git a/quadratic-api/src/storage/storage.ts b/quadratic-api/src/storage/storage.ts new file mode 100644 index 0000000000..0960194079 --- /dev/null +++ b/quadratic-api/src/storage/storage.ts @@ -0,0 +1,57 @@ +import multer from 'multer'; +import { STORAGE_TYPE } from '../env-vars'; +import { getPresignedStorageUrl, getStorageUrl, multerFileSystemStorage, upload } from './fileSystem'; +import { generatePresignedUrl, multerS3Storage, uploadStringAsFileS3 } from './s3'; + +export type UploadFileResponse = { + bucket: string; + key: string; +}; + +// Get the URL for a given file (key). +export const getFileUrl = async (key: string) => { + switch (STORAGE_TYPE) { + case 's3': + return await generatePresignedUrl(key); + case 'file-system': + return getStorageUrl(key); + default: + throw new Error(`Unsupported storage type in getFileUrl(): ${STORAGE_TYPE}`); + } +}; + +// Get a presigned URL for a given file (key). +export const getPresignedFileUrl = async (key: string) => { + switch (STORAGE_TYPE) { + case 's3': + return await generatePresignedUrl(key); + case 'file-system': + return getPresignedStorageUrl(key); + default: + throw new Error(`Unsupported storage type in getPresignedFileUrl(): ${STORAGE_TYPE}`); + } +}; + +// Upload a file (key). +export const uploadFile = async (key: string, contents: string, jwt: string): Promise => { + switch (STORAGE_TYPE) { + case 's3': + return await uploadStringAsFileS3(key, contents); + case 'file-system': + return await upload(key, contents, jwt); + default: + throw new Error(`Unsupported storage type in uploadFile(): ${STORAGE_TYPE}`); + } +}; + +// Multer middleware for file uploads. +export const uploadMiddleware = (): multer.Multer => { + switch (STORAGE_TYPE) { + case 's3': + return multerS3Storage(); + case 'file-system': + return multerFileSystemStorage as unknown as multer.Multer; + default: + throw new Error(`Unsupported storage type in uploadMiddleware(): ${STORAGE_TYPE}`); + } +}; diff --git a/quadratic-api/src/utils/createFile.ts b/quadratic-api/src/utils/createFile.ts index 50a91cd446..9a4f5a533a 100644 --- a/quadratic-api/src/utils/createFile.ts +++ b/quadratic-api/src/utils/createFile.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import path from 'path'; -import { uploadStringAsFileS3 } from '../aws/s3'; import dbClient from '../dbClient'; +import { uploadFile } from '../storage/storage'; export async function createFile({ contents, @@ -10,6 +10,7 @@ export async function createFile({ version, teamId, isPrivate, + jwt, }: { contents?: string; name: string; @@ -17,6 +18,7 @@ export async function createFile({ version: string; teamId: number; isPrivate?: boolean; + jwt: string; }) { return await dbClient.$transaction(async (transaction) => { // Create file in db @@ -44,7 +46,7 @@ export async function createFile({ // Upload file contents to S3 and create a checkpoint const { uuid, id: fileId } = dbFile; - const response = await uploadStringAsFileS3(`${uuid}-0.grid`, contents); + const response = await uploadFile(`${uuid}-0.grid`, contents, jwt); await transaction.fileCheckpoint.create({ data: { diff --git a/quadratic-api/src/utils/crypto.test.ts b/quadratic-api/src/utils/crypto.test.ts index 43e70b79d6..a228b42fdf 100644 --- a/quadratic-api/src/utils/crypto.test.ts +++ b/quadratic-api/src/utils/crypto.test.ts @@ -1,5 +1,5 @@ import crypto from 'crypto'; -import { decrypt, encrypt } from './crypto'; +import { decrypt, encrypt, hash } from './crypto'; // Convert a hex string to a buffer. // @@ -13,6 +13,13 @@ describe('Encryption and Decryption', () => { const key = hexStringToBuffer(keyBytes.toString('hex')); const text = 'Hello, world!'; + it('should hash a value', () => { + const hashed = hash(text); + const expected = '315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3'; + + expect(hashed).toEqual(expected); + }); + it('should convert a hex to a buffer', () => { const hex = keyBytes.toString('hex'); const buffer = hexStringToBuffer(hex); diff --git a/quadratic-api/src/utils/crypto.ts b/quadratic-api/src/utils/crypto.ts index 4f7e5a0360..98a290c859 100644 --- a/quadratic-api/src/utils/crypto.ts +++ b/quadratic-api/src/utils/crypto.ts @@ -7,6 +7,13 @@ const algorithm = 'aes-256-cbc'; // Get the encryption key from the env and convert it to a buffer. const encryption_key = Buffer.from(ENCRYPTION_KEY, 'hex'); +export const hash = (text: string): string => { + const hash = crypto.createHash('sha256'); + hash.update(text); + + return hash.digest('hex'); +}; + // Encrypts the given text using the given key. // Store the IV with the encrypted text (prepended). export const encrypt = (key: Buffer, text: string): string => { diff --git a/quadratic-client/.env.docker b/quadratic-client/.env.docker index 944db6d51b..ee0c206b50 100644 --- a/quadratic-client/.env.docker +++ b/quadratic-client/.env.docker @@ -2,3 +2,8 @@ VITE_DEBUG=1 // use =1 to enable debug flags VITE_QUADRATIC_API_URL=http://localhost:8000 VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=ory +VITE_STORAGE_TYPE=file-system +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/.env.example b/quadratic-client/.env.example index 8f9ca0adfd..59862ad5c7 100644 --- a/quadratic-client/.env.example +++ b/quadratic-client/.env.example @@ -3,10 +3,15 @@ VITE_AMPLITUDE_ANALYTICS_API_KEY= VITE_MIXPANEL_ANALYTICS_KEY= VITE_SENTRY_DSN=https://xxxxxxxxxxxxxxxxxx@xxxxxxxxxxxx.ingest.sentry.io/xxxxxxxxxxxx VITE_DEBUG=0 // use =1 to enable debug flags +VITE_QUADRATIC_API_URL=http://localhost:8000 +VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws +VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=auth0 +VITE_STORAGE_TYPE=s3 VITE_AUTH0_ISSUER=https://quadratic-community.us.auth0.com/ VITE_AUTH0_DOMAIN=quadratic-community.us.auth0.com VITE_AUTH0_CLIENT_ID=DCPCvqyU5Q0bJD8Q3QmJEoV48x1zLH7W VITE_AUTH0_AUDIENCE=community-quadratic -VITE_QUADRATIC_API_URL=http://localhost:8000 -VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws -VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/.env.test b/quadratic-client/.env.test new file mode 100644 index 0000000000..2d1cdff22f --- /dev/null +++ b/quadratic-client/.env.test @@ -0,0 +1,9 @@ +VITE_DEBUG=1 // use =1 to enable debug flags +VITE_QUADRATIC_API_URL=http://localhost:8000 +VITE_QUADRATIC_MULTIPLAYER_URL=ws://localhost:3001/ws +VITE_QUADRATIC_CONNECTION_URL=http://localhost:3003 + +# Auth +VITE_AUTH_TYPE=auth0 +VITE_STORAGE_TYPE=s3 +VITE_ORY_HOST=http://localhost:4433 diff --git a/quadratic-client/Dockerfile b/quadratic-client/Dockerfile index e14d164e17..e814a28455 100644 --- a/quadratic-client/Dockerfile +++ b/quadratic-client/Dockerfile @@ -1,13 +1,82 @@ -FROM node:18-alpine AS builder +# Use an official node image as a parent image +FROM node:18 AS build + +# Install rustup +RUN echo 'Installing rustup...' && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +# Install wasm-pack +RUN echo 'Installing wasm-pack...' && curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh +RUN echo 'wasm-pack version:' && wasm-pack --version + +# Install wasm32-unknown-unknown target +# RUN rustup target add wasm32-unknown-unknown + +# Install python & clean up +RUN apt-get update && apt-get install -y python-is-python3 python3-pip && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install npm dependencies WORKDIR /app COPY package.json . COPY package-lock.json . -COPY updateAlertVersion.json . -COPY quadratic-client ./quadratic-client -COPY quadratic-shared ./quadratic-shared +COPY ./quadratic-kernels/python-wasm/package*.json ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/package*.json ./quadratic-core/ +COPY ./quadratic-rust-client/package*.json ./quadratic-rust-client/ +COPY ./quadratic-shared/package*.json ./quadratic-shared/ +COPY ./quadratic-client/package*.json ./quadratic-client/ RUN npm install -FROM node:18-slim AS runtime +# Install typescript +RUN npm install -D typescript + +# Copy the rest of the application +WORKDIR /app +COPY updateAlertVersion.json . +COPY ./quadratic-kernels/python-wasm/. ./quadratic-kernels/python-wasm/ +COPY ./quadratic-core/. ./quadratic-core/ +COPY ./quadratic-rust-client/. ./quadratic-rust-client/ +COPY ./quadratic-shared/. ./quadratic-shared/ +COPY ./quadratic-client/. ./quadratic-client/ + +# Run the packaging script for quadratic_py +WORKDIR /app +RUN ./quadratic-kernels/python-wasm/package.sh --no-poetry + +# Build wasm +WORKDIR /app/quadratic-core +RUN echo 'Building wasm...' && wasm-pack build --target web --out-dir ../quadratic-client/src/app/quadratic-core --weak-refs + +# Export TS/Rust types +WORKDIR /app/quadratic-core +RUN echo 'Exporting TS/Rust types...' && cargo run --bin export_types + +# Build the quadratic-rust-client WORKDIR /app -COPY --from=builder /app . -CMD ["npm", "start"] +ARG GIT_COMMIT +ENV GIT_COMMIT=$GIT_COMMIT +RUN echo 'Building quadratic-rust-client...' && npm run build --workspace=quadratic-rust-client + +# Build the quadratic-shared +WORKDIR /app +RUN echo 'Building quadratic-shared...' && npm run compile --workspace=quadratic-shared + +# Build the front-end +WORKDIR /app +RUN echo 'Building front-end...' +ENV VITE_DEBUG=VITE_DEBUG_VAL +ENV VITE_QUADRATIC_API_URL=VITE_QUADRATIC_API_URL_VAL +ENV VITE_QUADRATIC_MULTIPLAYER_URL=VITE_QUADRATIC_MULTIPLAYER_URL_VAL +ENV VITE_QUADRATIC_CONNECTION_URL=VITE_QUADRATIC_CONNECTION_URL_VAL +ENV VITE_AUTH_TYPE=VITE_AUTH_TYPE_VAL +ENV VITE_ORY_HOST=VITE_ORY_HOST_VAL +RUN npm run build --workspace=quadratic-client + +# The default command to run the application +# CMD ["npm", "run", "start:production"] + +FROM nginx:stable-alpine +COPY --from=build /app/build /usr/share/nginx/html + +EXPOSE 80 443 3000 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/quadratic-client/package.json b/quadratic-client/package.json index 965d193447..327f399626 100644 --- a/quadratic-client/package.json +++ b/quadratic-client/package.json @@ -21,6 +21,7 @@ "@monaco-editor/react": "^4.3.1", "@mui/icons-material": "^5.2.0", "@mui/material": "^5.2.2", + "@ory/kratos-client": "^1.2.1", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", diff --git a/quadratic-client/public/.well-known/jwks.json b/quadratic-client/public/.well-known/jwks.json new file mode 100644 index 0000000000..fe8f34c5f7 --- /dev/null +++ b/quadratic-client/public/.well-known/jwks.json @@ -0,0 +1,26 @@ +{ + "keys": [ + { + "p": "8cs6LVWfWM3_TOQZdNWG09sqq8qGbuSejp3rcvDedVh_NAO9D5byE7cpdM2_4_enh1wXoUzzpL0MSHFLAAErJywKLUgyGmjdmJdA7IFuOV4lPNydcSuyyHm4pXVSc_ZtB0MfVPdAh1TO5zyjkk5IbIC8IYOICI1dxu8namdP5MM", + "kty": "RSA", + "q": "uK9v3Hp3X_FESMl3Tbv1ZF-7-oAwdpSq_hMnzb0CCVJ1nVK7cs4RtYhZoVLDlPg98oe35HGjemdrk_WVduUH3H2wbbK0bE9v_yG-WPor4GPhxmmw8e7KV0qkOK3y2x8gC0P2IlY7PpuxfOIHl-z9PFaddfXxQaNOym_naiK1jnc", + "d": "cGVslzyvoWR487B2gXnrg3MPFFFpyD4a7epTKFa7baGd_5oBxHDgZrZcYW6wrlHNuN_ZDXucNneZeg7m5ZLUG6Uz9cYh7aBmOXiAU3Ag7ImFEVMSIKUHSGq83eKsLS8hiowEx9LeinGHr8gEHYJ9JqYV8yZOuc0_V3MQuZnCi6Xg_WmYRN8eMBV8jPKIILQX10ifrgkVSF3xXi11jN1fUiC17xCRyUArWM7c22CfONhxIXp4inGzJjoNMU7BhLJnpdgBpm9RCmNESRP2U0Yhd659upFv9NFFWmSILTwFkdYW5puaVfkHBIZV7_g5OJV7DsE2Cti5jv3SLk4CyCj68Q", + "e": "AQAB", + "use": "sig", + "kid": "ory-example", + "qi": "4Ji4_LTZURiRJBp72ULUbEIukrXwigrGKqIMKA7M2fYB6PlZ5RjxNsdGrTttaMzKyHdDPQWY01fBNzWvaZCNndUu-PsjDj2tO0a-EfRys4onIeV0srSfk7QXlH-u-gCqYulEvMDXSDrzjW8HBq4n3Z94GeZxa5kE0XD13qf89NU", + "dp": "ofZKivFuonKiD2Q_NQaOoLyPEbHAaOmU190qSLzVlm7oDfRvINEwaEppZ4cmgVJzknT6kx5TmcbUQnY5EdC2ki-qxXg1r4EM5lhysbllFuJcOS9h-tuVjzoRmCtFRs4LbDDm_Of9_mitizEQNEFhu-RjoGNVrLzc0xOBKIH5fzc", + "alg": "RS256", + "dq": "H71OzSZi46M0KAovrbVSu_hT9v4W1hpAtL-YBJyp_-4i9nGkc1uE4ZzYQohVwoFTLB409VauULf7XgdDs5Yy3qrfKksfBMo2JjOnYeVEqyCfSZkaZsmyDoRuaqtCZHQZ7rW0VDxbnCvnud2ijnKVJsx_7SjiWHR3cwT-UVg7uYs", + "n": "rm_FZLcTUKdiCnv5zc5284DBQ2RO0f-VLpD4CcJ6Y3Po0zYoMiniOCdmTn1I5klau6BfVQWpDfdqV-G-HhRhLpdDy30Zs-t1veN-YxXgBOnF6neqww5tivwtJ--SS5S2m4UyiNxqlWy4-1FttpCKwu-Dm8d2Q7ppUal6wQojGOnCje8P499a0x9JjMZbh0DcUke2mn_ScmVTV8IEC7caMyo3D_HVdaMuNDN2N2O-7fRUJTVn8pgsjUfw1xP8tB-8-k6rK07X9yi_-oUyXqaqj8IhCPNMOc1UaQbrY3vvdMarQQrykkyXDPp6IL4vA3dw8q46BJvfLRsOCa1g-uaApQ" + }, + { + "kty": "RSA", + "e": "AQAB", + "use": "sig", + "kid": "ory-example", + "alg": "RS256", + "n": "rm_FZLcTUKdiCnv5zc5284DBQ2RO0f-VLpD4CcJ6Y3Po0zYoMiniOCdmTn1I5klau6BfVQWpDfdqV-G-HhRhLpdDy30Zs-t1veN-YxXgBOnF6neqww5tivwtJ--SS5S2m4UyiNxqlWy4-1FttpCKwu-Dm8d2Q7ppUal6wQojGOnCje8P499a0x9JjMZbh0DcUke2mn_ScmVTV8IEC7caMyo3D_HVdaMuNDN2N2O-7fRUJTVn8pgsjUfw1xP8tB-8-k6rK07X9yi_-oUyXqaqj8IhCPNMOc1UaQbrY3vvdMarQQrykkyXDPp6IL4vA3dw8q46BJvfLRsOCa1g-uaApQ" + } + ] +} diff --git a/quadratic-client/src/app/ui/menus/AIAssistant/useAIRequestToAPI.tsx b/quadratic-client/src/app/ui/menus/AIAssistant/useAIRequestToAPI.tsx index b85583a256..0481370dde 100644 --- a/quadratic-client/src/app/ui/menus/AIAssistant/useAIRequestToAPI.tsx +++ b/quadratic-client/src/app/ui/menus/AIAssistant/useAIRequestToAPI.tsx @@ -1,5 +1,5 @@ import { MODEL_OPTIONS } from '@/app/ui/menus/AIAssistant/MODELS'; -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { AI } from '@/shared/constants/routes'; import { AIMessage, diff --git a/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayer.ts b/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayer.ts index 759d5045f8..a76a85e52c 100644 --- a/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayer.ts +++ b/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayer.ts @@ -8,9 +8,8 @@ import { pixiAppSettings } from '@/app/gridGL/pixiApp/PixiAppSettings'; import { SheetPosTS } from '@/app/gridGL/types/size'; import type { CodeRun } from '@/app/web-workers/CodeRun'; import { LanguageState } from '@/app/web-workers/languageTypes'; -import { authClient, parseDomain } from '@/auth'; +import { User, authClient, parseDomain } from '@/auth/auth'; import { displayName } from '@/shared/utils/userUtil'; -import { User } from '@auth0/auth0-spa-js'; import * as Sentry from '@sentry/react'; import { v4 as uuid } from 'uuid'; import updateAlertVersion from '../../../../../updateAlertVersion.json'; diff --git a/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayerClientMessages.ts b/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayerClientMessages.ts index ea7a056178..b03939b6a4 100644 --- a/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayerClientMessages.ts +++ b/quadratic-client/src/app/web-workers/multiplayerWebWorker/multiplayerClientMessages.ts @@ -1,4 +1,4 @@ -import { User } from '@auth0/auth0-spa-js'; +import { User } from '@/auth/auth'; import { CellEdit, ReceiveRoom, UserUpdate } from './multiplayerTypes'; export type MultiplayerState = diff --git a/quadratic-client/src/app/web-workers/multiplayerWebWorker/worker/multiplayerServer.ts b/quadratic-client/src/app/web-workers/multiplayerWebWorker/worker/multiplayerServer.ts index fa52701ca1..ea9ebf74cd 100644 --- a/quadratic-client/src/app/web-workers/multiplayerWebWorker/worker/multiplayerServer.ts +++ b/quadratic-client/src/app/web-workers/multiplayerWebWorker/worker/multiplayerServer.ts @@ -2,7 +2,7 @@ * Communication between the multiplayer web worker and the quadratic-multiplayer server */ -import { User } from '@auth0/auth0-spa-js'; +import { User } from '@/auth/auth'; import * as Sentry from '@sentry/react'; import { Buffer } from 'buffer'; import sharedConstants from '../../../../../../updateAlertVersion.json'; diff --git a/quadratic-client/src/app/web-workers/pythonLanguageServer/client.ts b/quadratic-client/src/app/web-workers/pythonLanguageServer/client.ts index df5d1d584e..190afc1852 100644 --- a/quadratic-client/src/app/web-workers/pythonLanguageServer/client.ts +++ b/quadratic-client/src/app/web-workers/pythonLanguageServer/client.ts @@ -1,4 +1,4 @@ -import EventEmitter from 'events'; +import EventEmitter from 'eventemitter3'; import { CompletionList, CompletionParams, @@ -50,9 +50,12 @@ export class LanguageServerClient extends EventEmitter { super(); } - on(event: 'diagnostics', listener: (params: PublishDiagnosticsParams) => void): this { - super.on(event, listener); - return this; + on(event: T, listener: (...args: any[]) => void): this { + return super.on(event, listener); + } + + onDiagnostics(listener: (params: PublishDiagnosticsParams) => void): this { + return super.on('diagnostics', listener); } currentDiagnostics(uri: string): Diagnostic[] { diff --git a/quadratic-client/src/app/web-workers/pythonWebWorker/pythonWebWorker.ts b/quadratic-client/src/app/web-workers/pythonWebWorker/pythonWebWorker.ts index 4e7899871c..7bbf4266d4 100644 --- a/quadratic-client/src/app/web-workers/pythonWebWorker/pythonWebWorker.ts +++ b/quadratic-client/src/app/web-workers/pythonWebWorker/pythonWebWorker.ts @@ -1,6 +1,6 @@ import { events } from '@/app/events/events'; import { LanguageState } from '@/app/web-workers/languageTypes'; -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import mixpanel from 'mixpanel-browser'; import { quadraticCore } from '../quadraticCore/quadraticCore'; import { ClientPythonMessage, PythonClientGetJwt, PythonClientMessage } from './pythonClientMessages'; diff --git a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts index 0e95049b22..77d0b5a6b8 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/quadraticCore.ts @@ -28,7 +28,7 @@ import { SummarizeSelectionResult, Validation, } from '@/app/quadratic-core-types'; -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { renderWebWorker } from '../renderWebWorker/renderWebWorker'; import { ClientCoreCellHasContent, diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts index 2816b6dbe7..29b791e136 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/core.ts @@ -59,8 +59,15 @@ class Core { private clientQueue: Function[] = []; private renderQueue: Function[] = []; - private async loadGridFile(file: string): Promise { - const res = await fetch(file); + private async loadGridFile(file: string, addToken: boolean): Promise { + let requestInit = {}; + + if (addToken) { + const jwt = await coreClient.getJwt(); + requestInit = { headers: { Authorization: `Bearer ${jwt}` } }; + } + + const res = await fetch(file, requestInit); return new Uint8Array(await res.arrayBuffer()); } @@ -83,9 +90,13 @@ class Core { }; // Creates a Grid from a file. Initializes bother coreClient and coreRender w/metadata. - async loadFile(message: ClientCoreLoad, renderPort: MessagePort): Promise<{ version: string } | { error: string }> { + async loadFile( + message: ClientCoreLoad, + renderPort: MessagePort, + addToken: boolean + ): Promise<{ version: string } | { error: string }> { coreRender.init(renderPort); - const results = await Promise.all([this.loadGridFile(message.url), initCore()]); + const results = await Promise.all([this.loadGridFile(message.url, addToken), initCore()]); try { this.gridController = GridController.newFromFile(results[0], message.sequenceNumber, true); } catch (e) { diff --git a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts index d2b368d029..1d1fed38aa 100644 --- a/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts +++ b/quadratic-client/src/app/web-workers/quadraticCore/worker/coreClient.ts @@ -137,10 +137,13 @@ class CoreClient { switch (e.data.type) { case 'clientCoreLoad': await offline.init(e.data.fileId); + + const addToken = this.env.VITE_STORAGE_TYPE === 'file-system'; + this.send({ type: 'coreClientLoad', id: e.data.id, - ...(await core.loadFile(e.data, e.ports[0])), + ...(await core.loadFile(e.data, e.ports[0], addToken)), }); return; diff --git a/quadratic-client/src/auth.ts b/quadratic-client/src/auth.ts deleted file mode 100644 index 78204f06a5..0000000000 --- a/quadratic-client/src/auth.ts +++ /dev/null @@ -1,188 +0,0 @@ -import { Auth0Client, User, createAuth0Client } from '@auth0/auth0-spa-js'; -import * as Sentry from '@sentry/react'; -import { useEffect } from 'react'; -import { LoaderFunction, LoaderFunctionArgs, redirect } from 'react-router-dom'; -import { ROUTES } from './shared/constants/routes'; - -const AUTH0_DOMAIN = import.meta.env.VITE_AUTH0_DOMAIN || ''; -const AUTH0_CLIENT_ID = import.meta.env.VITE_AUTH0_CLIENT_ID || ''; -const AUTH0_AUDIENCE = import.meta.env.VITE_AUTH0_AUDIENCE; -const AUTH0_ISSUER = import.meta.env.VITE_AUTH0_ISSUER; - -// verify all AUTH0 env variables are set -if (!(AUTH0_DOMAIN && AUTH0_CLIENT_ID && AUTH0_AUDIENCE && AUTH0_ISSUER)) { - const message = 'Auth0 variables are not configured correctly.'; - Sentry.captureEvent({ - message, - level: 'fatal', - }); -} - -// Create the client as a module-scoped promise so all loaders will wait -// for this one single instance of client to resolve -let auth0ClientPromise: Promise; -async function getClient() { - if (!auth0ClientPromise) { - auth0ClientPromise = createAuth0Client({ - domain: AUTH0_DOMAIN, - clientId: AUTH0_CLIENT_ID, - issuer: AUTH0_ISSUER, - authorizationParams: { - audience: AUTH0_AUDIENCE, - }, - cacheLocation: 'localstorage', - useRefreshTokens: true, - // remove the subdomain from the cookie domain so that the ws server can access it - cookieDomain: parseDomain(window.location.host), - }); - } - const auth0Client = await auth0ClientPromise; - return auth0Client; -} - -interface AuthClient { - isAuthenticated(): Promise; - user(): Promise; - login(redirectTo: string, isSignupFlow?: boolean): Promise; - handleSigninRedirect(): Promise; - logout(): Promise; - getTokenOrRedirect(): Promise; -} - -export const authClient: AuthClient = { - async isAuthenticated() { - const client = await getClient(); - const isAuthenticated = await client.isAuthenticated(); - return isAuthenticated; - }, - async user() { - const client = await getClient(); - const user = await client.getUser(); - return user; - }, - async login(redirectTo: string, isSignupFlow: boolean = false) { - const client = await getClient(); - await client.loginWithRedirect({ - authorizationParams: { - screen_hint: isSignupFlow ? 'signup' : 'login', - redirect_uri: - window.location.origin + - ROUTES.LOGIN_RESULT + - '?' + - new URLSearchParams([['redirectTo', redirectTo]]).toString(), - }, - }); - await waitForAuth0ClientToRedirect(); - }, - async handleSigninRedirect() { - const query = window.location.search; - if (query.includes('code=') && query.includes('state=')) { - const client = await getClient(); - await client.handleRedirectCallback(); - } - }, - async logout() { - const client = await getClient(); - await client.logout({ logoutParams: { returnTo: window.location.origin } }); - await waitForAuth0ClientToRedirect(); - }, - /** - * Tries to get a token for the current user from the auth0 client. - * If the token is still valid, it'll pull it from a cache. If it’s expired, - * it will fail and we will manually redirect the user to auth0 to re-authenticate - * and get a new token. - */ - async getTokenOrRedirect() { - const client = await getClient(); - try { - const token = await client.getTokenSilently(); - return token; - } catch (e) { - await this.login(new URL(window.location.href).pathname); - return ''; - } - }, -}; - -/** - * Utility function for use in route loaders. - * If the user is not logged in (or don't have an auth token) and tries to - * access a protected route, we redirect them to the login page with a `from` - * parameter that allows login to redirect back to current page upon successful - * authentication. - */ -export function protectedRouteLoaderWrapper(loaderFn: LoaderFunction): LoaderFunction { - return async (loaderFnArgs: LoaderFunctionArgs) => { - const { request } = loaderFnArgs; - const isAuthenticated = await authClient.isAuthenticated(); - - // If the user isn't authenticated, redirect them to login & preserve their - // original request URL - if (!isAuthenticated) { - const originalRequestUrl = new URL(request.url); - let searchParams = new URLSearchParams(); - searchParams.set('from', originalRequestUrl.pathname + originalRequestUrl.search); - return redirect(ROUTES.LOGIN + '?' + searchParams.toString()); - } - - // If the user is authenticated, make sure we have a valid token - // before we load any of the app - await authClient.getTokenOrRedirect(); - - return loaderFn(loaderFnArgs); - }; -} - -/** - * In cases where we call the auth0 client and it redirects the user to the - * auth0 website (e.g. for `.login` and `.logout`, presumably via changing - * `window.location`) we have to manually wait for the auth0 client. - * - * Why? Because even though auth0's client APIs are async, they seem to - * complete immediately and our app's code continues before `window.location` - * kicks in. - * - * So this function ensures our whole app pauses while the auth0 lib does its - * thing and kicks the user over to auth0.com - * - * We only use this when we _want_ to pause everything and wait to redirect - */ -export function waitForAuth0ClientToRedirect() { - return new Promise((resolve) => setTimeout(resolve, 10000)); -} - -/** - * Utility function parse the domain from a url - */ -export function parseDomain(url: string): string { - // check for classic URLs - let matches = url.match(/([^.]*\.[^.]{2,3}(?:\.[^.]{2,3})?$)/); - - if (matches) { - return '.' + matches[0]; - } else { - // check for IP addresses or localhost (ignore ports) or just return the url - return url.match(/(?:(?!:).)*/)![0] ?? url; - } -} - -/** - * Used in the dashboard and the app to ensure the user’s auth token always - * remains valid. If at any point it expires, we redirect for a new one. - * - * Because this runs in both the app and the dashboard, we only want to check - * for a token if the user is authenticated. If they're not, it's probably - * a shared public file in the app that doesn't require auth to view. - */ -export function useCheckForAuthorizationTokenOnWindowFocus() { - const fn = async () => { - const isAuthenticated = await authClient.isAuthenticated(); - if (isAuthenticated) { - await authClient.getTokenOrRedirect(); - } - }; - useEffect(() => { - window.addEventListener('focus', fn); - return () => window.removeEventListener('focus', fn); - }, []); -} diff --git a/quadratic-client/src/auth/auth.test.ts b/quadratic-client/src/auth/auth.test.ts new file mode 100644 index 0000000000..8785104667 --- /dev/null +++ b/quadratic-client/src/auth/auth.test.ts @@ -0,0 +1,15 @@ +import { describe, expect, it } from 'vitest'; +import { parseDomain } from './auth'; + +describe('auth', () => { + it('parses domains', () => { + const localhostWithPort = parseDomain('localhost:3000'); + expect(localhostWithPort).toBe('localhost'); + + const hasSubdomain = parseDomain('app.quadratichq.com'); + expect(hasSubdomain).toBe('quadratichq.com'); + + const ipAddress = parseDomain('35.161.33.29'); + expect(ipAddress).toBe('35.161.33.29'); + }); +}); diff --git a/quadratic-client/src/auth/auth.ts b/quadratic-client/src/auth/auth.ts new file mode 100644 index 0000000000..fab4351bc3 --- /dev/null +++ b/quadratic-client/src/auth/auth.ts @@ -0,0 +1,129 @@ +import { auth0Client } from '@/auth/auth0'; +import { oryClient } from '@/auth/ory'; +import { useEffect } from 'react'; +import { LoaderFunction, LoaderFunctionArgs, redirect } from 'react-router-dom'; +import { ROUTES } from '../shared/constants/routes'; + +const AUTH_TYPE = import.meta.env.VITE_AUTH_TYPE || ''; + +export interface User { + name?: string; + given_name?: string; + family_name?: string; + picture?: string; + email?: string; + index?: number; + sub?: string; +} + +export interface AuthClient { + isAuthenticated(): Promise; + user(): Promise; + login(redirectTo: string, isSignupFlow?: boolean): Promise; + handleSigninRedirect(): Promise; + logout(): Promise; + getTokenOrRedirect(): Promise; +} + +const getAuthClient = () => { + switch (AUTH_TYPE) { + case 'auth0': + return auth0Client; + case 'ory': + return oryClient; + default: + throw new Error(`Unsupported auth type in getAuthClient(): ${AUTH_TYPE}`); + } +}; + +export const authClient: AuthClient = getAuthClient(); + +/** + * Utility function for use in route loaders. + * If the user is not logged in (or don't have an auth token) and tries to + * access a protected route, we redirect them to the login page with a `from` + * parameter that allows login to redirect back to current page upon successful + * authentication. + */ +export function protectedRouteLoaderWrapper(loaderFn: LoaderFunction): LoaderFunction { + return async (loaderFnArgs: LoaderFunctionArgs) => { + const { request } = loaderFnArgs; + const isAuthenticated = await authClient.isAuthenticated(); + + // If the user isn't authenciated, redirect them to login & preserve their + // original request URL + if (!isAuthenticated) { + const originalRequestUrl = new URL(request.url); + let searchParams = new URLSearchParams(); + searchParams.set('from', originalRequestUrl.pathname + originalRequestUrl.search); + return redirect(ROUTES.LOGIN + '?' + searchParams.toString()); + } + + // If the user is authenticated, make sure we have a valid token + // before we load any of the app + await authClient.getTokenOrRedirect(); + + return loaderFn(loaderFnArgs); + }; +} + +/** + * In cases where we call the auth client and it redirects the user to the + * auth website (e.g. for `.login` and `.logout`, presumably via changing + * `window.location`) we have to manually wait for the auth client. + * + * Why? Because even though auth's client APIs are async, they seem to + * complete immediately and our app's code continues before `window.location` + * kicks in. + * + * So this function ensures our whole app pauses while the auth lib does its + * thing and kicks the user over to auth.com + * + * We only use this when we _want_ to pause everything and wait to redirect + */ +export function waitForAuthClientToRedirect() { + return new Promise((resolve) => setTimeout(resolve, 10000)); +} + +/** + * Utility function parse the domain from a url + */ +export function parseDomain(url: string): string { + // remove the port if present + const [hostname] = url.split(':'); + + // check if the hostname is an IP address + const isIpAddress = /^[\d.]+$/.test(hostname); + + if (isIpAddress) return hostname; + + const parts = hostname.split('.'); + + // remove subdomain + if (parts.length > 2) { + return parts.slice(-2).join('.'); + } + + return hostname; +} + +/** + * Used in the dashboard and the app to ensure the user’s auth token always + * remains valid. If at any point it expires, we redirect for a new one. + * + * Because this runs in both the app and the dashboard, we only want to check + * for a token if the user is authenticated. If they're not, it's probably + * a shared public file in the app that doesn't require auth to view. + */ +export function useCheckForAuthorizationTokenOnWindowFocus() { + const fn = async () => { + const isAuthenticated = await authClient.isAuthenticated(); + if (isAuthenticated) { + await authClient.getTokenOrRedirect(); + } + }; + useEffect(() => { + window.addEventListener('focus', fn); + return () => window.removeEventListener('focus', fn); + }, []); +} diff --git a/quadratic-client/src/auth/auth0.ts b/quadratic-client/src/auth/auth0.ts new file mode 100644 index 0000000000..9dc3490521 --- /dev/null +++ b/quadratic-client/src/auth/auth0.ts @@ -0,0 +1,97 @@ +import { Auth0Client, createAuth0Client } from '@auth0/auth0-spa-js'; +import * as Sentry from '@sentry/react'; +import { ROUTES } from '../shared/constants/routes'; +import { AuthClient, parseDomain, waitForAuthClientToRedirect } from './auth'; + +const AUTH0_DOMAIN = import.meta.env.VITE_AUTH0_DOMAIN || ''; +const AUTH0_CLIENT_ID = import.meta.env.VITE_AUTH0_CLIENT_ID || ''; +const AUTH0_AUDIENCE = import.meta.env.VITE_AUTH0_AUDIENCE; +const AUTH0_ISSUER = import.meta.env.VITE_AUTH0_ISSUER; + +// verify all AUTH0 env variables are set +if (!(AUTH0_DOMAIN && AUTH0_CLIENT_ID && AUTH0_AUDIENCE && AUTH0_ISSUER)) { + const message = 'Auth0 variables are not configured correctly.'; + Sentry.captureEvent({ + message, + level: 'fatal', + }); +} + +// Create the client as a module-scoped promise so all loaders will wait +// for this one single instance of client to resolve +let auth0ClientPromise: Promise; +async function getClient() { + if (!auth0ClientPromise) { + auth0ClientPromise = createAuth0Client({ + domain: AUTH0_DOMAIN, + clientId: AUTH0_CLIENT_ID, + issuer: AUTH0_ISSUER, + authorizationParams: { + audience: AUTH0_AUDIENCE, + }, + cacheLocation: 'localstorage', + useRefreshTokens: true, + // remove the subdomain from the cookie domain so that the ws server can access it + cookieDomain: parseDomain(window.location.host), + }); + } + const auth0Client = await auth0ClientPromise; + return auth0Client; +} + +type Auth0AuthClient = AuthClient; + +export const auth0Client: Auth0AuthClient = { + async isAuthenticated() { + const client = await getClient(); + const isAuthenticated = await client.isAuthenticated(); + return isAuthenticated; + }, + async user() { + const client = await getClient(); + const user = await client.getUser(); + return user; + }, + async login(redirectTo: string, isSignupFlow: boolean = false) { + const client = await getClient(); + await client.loginWithRedirect({ + authorizationParams: { + screen_hint: isSignupFlow ? 'signup' : 'login', + redirect_uri: + window.location.origin + + ROUTES.LOGIN_RESULT + + '?' + + new URLSearchParams([['redirectTo', redirectTo]]).toString(), + }, + }); + await waitForAuthClientToRedirect(); + }, + async handleSigninRedirect() { + const query = window.location.search; + if (query.includes('code=') && query.includes('state=')) { + const client = await getClient(); + await client.handleRedirectCallback(); + } + }, + async logout() { + const client = await getClient(); + await client.logout({ logoutParams: { returnTo: window.location.origin } }); + await waitForAuthClientToRedirect(); + }, + /** + * Tries to get a token for the current user from the auth0 client. + * If the token is still valid, it'll pull it from a cache. If it’s expired, + * it will fail and we will manually redirect the user to auth0 to re-authenticate + * and get a new token. + */ + async getTokenOrRedirect() { + const client = await getClient(); + try { + const token = await client.getTokenSilently(); + return token; + } catch (e) { + await this.login(new URL(window.location.href).pathname); + return ''; + } + }, +}; diff --git a/quadratic-client/src/auth/ory.ts b/quadratic-client/src/auth/ory.ts new file mode 100644 index 0000000000..70a028662c --- /dev/null +++ b/quadratic-client/src/auth/ory.ts @@ -0,0 +1,128 @@ +import { Configuration, FrontendApi, Session } from '@ory/kratos-client'; +import * as Sentry from '@sentry/react'; +import { AuthClient, User, waitForAuthClientToRedirect } from './auth'; + +const ORY_HOST = import.meta.env.VITE_ORY_HOST; + +// verify all Ory env variables are set +if (!ORY_HOST) { + const message = 'Ory variables are not configured correctly.'; + Sentry.captureEvent({ + message, + level: 'fatal', + }); +} + +const config = new Configuration({ + basePath: ORY_HOST, + baseOptions: { + withCredentials: true, + }, +}); + +const sdk = new FrontendApi(config); + +// singleton session +let session: Session | undefined; + +/** + * Get the current session from Ory. + * If the session is not cached or expired, fetch a new one. + * Return false if the session cannot be fetched. + */ +const getSession = async (): Promise => { + // if the session exists and is not expired, return it + if (session && session.expires_at && Date.parse(session.expires_at) > Date.now()) { + return session; + } + + try { + session = (await sdk.toSession({ tokenizeAs: 'jwt_template' })).data; + return session; + } catch (e) { + return false; + } +}; + +type OryAuthClient = AuthClient; + +export const oryClient: OryAuthClient = { + /** + * Retuen whether the user is authenticated and the session is valid. + */ + async isAuthenticated(): Promise { + return (await getSession()) !== false; + }, + + /** + * Get the current authenticated user from Ory. + */ + async user(): Promise { + const session = await getSession(); + + if (!session) return; + + const { first, last } = session.identity?.traits.name; + const data = { + name: `${first} ${last}`, + given_name: first, + family_name: last, + email: session.identity?.traits.email, + sub: session.identity?.id, + }; + + return data; + }, + + /** + * Login the user in Ory and create a new session. + * If `isSignupFlow` is true, the user will be redirected to the registration flow. + */ + async login(redirectTo: string, isSignupFlow: boolean = false) { + const urlSegment = isSignupFlow ? 'registration' : 'login'; + const url = new URL(`${ORY_HOST}/self-service/${urlSegment}/browser`); + url.searchParams.set('return_to', redirectTo); + + // redirect to the login/signup flow + window.location.assign(url); + + await waitForAuthClientToRedirect(); + }, + + /** + * Currently this is a noop since state and code cannot both be present in the URL + */ + async handleSigninRedirect() {}, + + /** + * Logout the user in Ory and terminate the singleton session. + * Take the user back to the login page (as defined in the Ory config). + */ + async logout() { + const { data: flow } = await sdk.createBrowserLogoutFlow(); + + // clear the singleton session + session = undefined; + + window.location.assign(flow.logout_url); + + await waitForAuthClientToRedirect(); + }, + + /** + * Tries to get a token for the current user from the Ory client. + * If the token is still valid, it'll pull it from a cache. If it’s expired, + * it will fail and we will manually redirect the user to auth0 to re-authenticate + * and get a new token. + */ + async getTokenOrRedirect() { + const session = await getSession(); + + if (!session || !session.tokenized) { + await this.login('/'); + return ''; + } + + return session.tokenized; + }, +}; diff --git a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx index 8ce46f8c35..b41141a99b 100644 --- a/quadratic-client/src/dashboard/components/DashboardSidebar.tsx +++ b/quadratic-client/src/dashboard/components/DashboardSidebar.tsx @@ -29,6 +29,8 @@ import { ReactNode, useState } from 'react'; import { NavLink, useLocation, useNavigation, useSearchParams, useSubmit } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; +const SHOW_EXAMPLES = import.meta.env.VITE_STORAGE_TYPE !== 'file-system'; + /** * Dashboard Navbar */ @@ -112,7 +114,7 @@ export function DashboardSidebar({ isLoading }: { isLoading: boolean }) { Resources
- {canEditTeam && ( + {canEditTeam && SHOW_EXAMPLES && ( Examples diff --git a/quadratic-client/src/dashboard/components/EducationDialog.tsx b/quadratic-client/src/dashboard/components/EducationDialog.tsx index bbceafa957..c35af2365c 100644 --- a/quadratic-client/src/dashboard/components/EducationDialog.tsx +++ b/quadratic-client/src/dashboard/components/EducationDialog.tsx @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { useDashboardRouteLoaderData } from '@/routes/_dashboard'; import { useRootRouteLoaderData } from '@/routes/_root'; import { SEARCH_PARAMS } from '@/shared/constants/routes'; diff --git a/quadratic-client/src/dashboard/components/NewFileDialog.tsx b/quadratic-client/src/dashboard/components/NewFileDialog.tsx index ae94a521d3..d561cc6034 100644 --- a/quadratic-client/src/dashboard/components/NewFileDialog.tsx +++ b/quadratic-client/src/dashboard/components/NewFileDialog.tsx @@ -17,6 +17,8 @@ import { useCallback, useMemo, useState } from 'react'; import { Link, useLocation, useNavigation } from 'react-router-dom'; import { useSetRecoilState } from 'recoil'; +const SHOW_EXAMPLES = import.meta.env.VITE_STORAGE_TYPE !== 'file-system'; + type Props = { connections: ConnectionList; onClose: () => void; @@ -141,19 +143,21 @@ export function NewFileDialog({ connections, teamUuid, onClose, isPrivate: initi Fetch data from an API -
  • - - - - - Learn from an example file - -
  • + {SHOW_EXAMPLES && ( +
  • + + + + + Learn from an example file + +
  • + )}
  • diff --git a/quadratic-client/src/dashboard/components/OnboardingBanner.tsx b/quadratic-client/src/dashboard/components/OnboardingBanner.tsx index 834535e1f2..9708000de3 100644 --- a/quadratic-client/src/dashboard/components/OnboardingBanner.tsx +++ b/quadratic-client/src/dashboard/components/OnboardingBanner.tsx @@ -36,6 +36,8 @@ import { FormEvent, useEffect, useRef, useState } from 'react'; import { Link, useSubmit } from 'react-router-dom'; import { z } from 'zod'; +const SHOW_EXAMPLES = import.meta.env.VITE_STORAGE_TYPE !== 'file-system'; + export function OnboardingBanner() { const { activeTeam: { @@ -89,16 +91,18 @@ export function OnboardingBanner() { Create blank file - + {SHOW_EXAMPLES && ( + + )}

    Or bring your own data:

    diff --git a/quadratic-client/src/router.tsx b/quadratic-client/src/router.tsx index da8a79e4ba..ca1dc74a8a 100644 --- a/quadratic-client/src/router.tsx +++ b/quadratic-client/src/router.tsx @@ -13,7 +13,7 @@ import { createRoutesFromElements, redirect, } from 'react-router-dom'; -import { protectedRouteLoaderWrapper } from './auth'; +import { protectedRouteLoaderWrapper } from './auth/auth'; import * as RootRoute from './routes/_root'; export const router = createBrowserRouter( diff --git a/quadratic-client/src/routes/_dashboard.tsx b/quadratic-client/src/routes/_dashboard.tsx index 558bf17b6b..e139089340 100644 --- a/quadratic-client/src/routes/_dashboard.tsx +++ b/quadratic-client/src/routes/_dashboard.tsx @@ -1,4 +1,4 @@ -import { useCheckForAuthorizationTokenOnWindowFocus } from '@/auth'; +import { useCheckForAuthorizationTokenOnWindowFocus } from '@/auth/auth'; import { newFileDialogAtom } from '@/dashboard/atoms/newFileDialogAtom'; import { DashboardSidebar } from '@/dashboard/components/DashboardSidebar'; import { EducationDialog } from '@/dashboard/components/EducationDialog'; @@ -8,7 +8,7 @@ import { NewFileDialog } from '@/dashboard/components/NewFileDialog'; import { apiClient } from '@/shared/api/apiClient'; import { MenuIcon } from '@/shared/components/Icons'; import { ROUTES, ROUTE_LOADER_IDS, SEARCH_PARAMS } from '@/shared/constants/routes'; -import { CONTACT_URL } from '@/shared/constants/urls'; +import { CONTACT_URL, SCHEDULE_MEETING } from '@/shared/constants/urls'; import { Button } from '@/shared/shadcn/ui/button'; import { Sheet, SheetContent, SheetTrigger } from '@/shared/shadcn/ui/sheet'; import { TooltipProvider } from '@/shared/shadcn/ui/tooltip'; @@ -269,7 +269,31 @@ export const ErrorBoundary = () => {
    ); + const actionsLicenseRevoked = ( + + ); + if (isRouteErrorResponse(error)) { + if (error.status === 402) + return ( + + ); if (error.status === 403) return ( { export const ErrorBoundary = () => { const error = useRouteError(); - const actions = ( + const actionsDefault = ( ); + const actionsLicenseRevoked = ( + + ); + if (isRouteErrorResponse(error)) { let title = ''; let description: string = ''; + let actions = actionsDefault; if (error.status === 404) { title = 'File not found'; @@ -145,6 +161,10 @@ export const ErrorBoundary = () => { } else if (error.status === 400) { title = 'Bad file request'; description = 'Check the URL and try again.'; + } else if (error.status === 402) { + title = 'License Revoked'; + description = 'Your license has been revoked. Please contact Quadratic Support.'; + actions = actionsLicenseRevoked; } else if (error.status === 403) { title = 'Permission denied'; description = 'You do not have permission to view this file. Try reaching out to the file owner.'; @@ -178,7 +198,7 @@ export const ErrorBoundary = () => { title="Unexpected error" description="Something went wrong loading this file. If the error continues, contact us." Icon={ExclamationTriangleIcon} - actions={actions} + actions={actionsDefault} severity="error" showLoggedInUser /> diff --git a/quadratic-client/src/routes/login-result.tsx b/quadratic-client/src/routes/login-result.tsx index b2c5ca1ba5..10678d1279 100644 --- a/quadratic-client/src/routes/login-result.tsx +++ b/quadratic-client/src/routes/login-result.tsx @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { apiClient } from '@/shared/api/apiClient'; import { redirect } from 'react-router-dom'; diff --git a/quadratic-client/src/routes/login.tsx b/quadratic-client/src/routes/login.tsx index f02db54da3..c2bbb771b3 100644 --- a/quadratic-client/src/routes/login.tsx +++ b/quadratic-client/src/routes/login.tsx @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { LoaderFunctionArgs, redirect } from 'react-router-dom'; export const loader = async ({ request }: LoaderFunctionArgs) => { diff --git a/quadratic-client/src/routes/logout.tsx b/quadratic-client/src/routes/logout.tsx index 1ff3102b61..3f0216b992 100644 --- a/quadratic-client/src/routes/logout.tsx +++ b/quadratic-client/src/routes/logout.tsx @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { redirect } from 'react-router-dom'; export const loader = async () => { diff --git a/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx b/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx index 423fcaf3d4..e5ba8b3c10 100644 --- a/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx +++ b/quadratic-client/src/routes/teams.$teamUuid.files.create.tsx @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { apiClient } from '@/shared/api/apiClient'; import { snackbarMsgQueryParam, snackbarSeverityQueryParam } from '@/shared/components/GlobalSnackbarProvider'; import { ROUTES } from '@/shared/constants/routes'; diff --git a/quadratic-client/src/shared/api/apiClient.ts b/quadratic-client/src/shared/api/apiClient.ts index f934836983..1b8372a23c 100644 --- a/quadratic-client/src/shared/api/apiClient.ts +++ b/quadratic-client/src/shared/api/apiClient.ts @@ -4,7 +4,7 @@ import * as Sentry from '@sentry/react'; import { Buffer } from 'buffer'; import mixpanel from 'mixpanel-browser'; import { ApiSchemas, ApiTypes } from 'quadratic-shared/typesAndSchemas'; -import { fetchFromApi } from './fetchFromApi'; +import { ApiError, fetchFromApi } from './fetchFromApi'; // TODO(ddimaria): make this dynamic const CURRENT_FILE_VERSION = '1.6'; @@ -15,7 +15,22 @@ export const apiClient = { return fetchFromApi(`/v0/teams`, { method: 'GET' }, ApiSchemas['/v0/teams.GET.response']); }, async get(uuid: string) { - return fetchFromApi(`/v0/teams/${uuid}`, { method: 'GET' }, ApiSchemas['/v0/teams/:uuid.GET.response']); + try { + const response = await fetchFromApi( + `/v0/teams/${uuid}`, + { method: 'GET' }, + ApiSchemas['/v0/teams/:uuid.GET.response'] + ); + + if (response.license.status === 'revoked') { + throw new ApiError('License Revoked', 402, undefined); + } + + return response; + } catch (err) { + console.error('Error retrieving license key', err); + throw new ApiError('License Revoked', 402, undefined); + } }, async update(uuid: string, body: ApiTypes['/v0/teams/:uuid.PATCH.request']) { return fetchFromApi( @@ -92,7 +107,22 @@ export const apiClient = { return fetchFromApi(url, { method: 'GET' }, ApiSchemas['/v0/files.GET.response']); }, async get(uuid: string) { - return fetchFromApi(`/v0/files/${uuid}`, { method: 'GET' }, ApiSchemas['/v0/files/:uuid.GET.response']); + try { + let response = await fetchFromApi( + `/v0/files/${uuid}`, + { method: 'GET' }, + ApiSchemas['/v0/files/:uuid.GET.response'] + ); + + if (response.license.status === 'revoked') { + throw new ApiError('License Revoked', 402, undefined); + } + + return response; + } catch (err) { + console.error('Error retrieving license key', err); + throw new ApiError('License Revoked', 402, undefined); + } }, async create({ file, diff --git a/quadratic-client/src/shared/api/connectionClient.ts b/quadratic-client/src/shared/api/connectionClient.ts index 44f8093e0c..59f5d88db5 100644 --- a/quadratic-client/src/shared/api/connectionClient.ts +++ b/quadratic-client/src/shared/api/connectionClient.ts @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { ConnectionType, ConnectionTypeDetails } from 'quadratic-shared/typesAndSchemasConnections'; import z from 'zod'; const API_URL = import.meta.env.VITE_QUADRATIC_CONNECTION_URL; diff --git a/quadratic-client/src/shared/api/fetchFromApi.ts b/quadratic-client/src/shared/api/fetchFromApi.ts index 91c6e75596..341b9be746 100644 --- a/quadratic-client/src/shared/api/fetchFromApi.ts +++ b/quadratic-client/src/shared/api/fetchFromApi.ts @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import * as Sentry from '@sentry/react'; import z from 'zod'; import { apiClient } from './apiClient'; @@ -61,6 +61,7 @@ export async function fetchFromApi( // Compare the response to the expected schema const result = schema.safeParse(json); + if (!result.success) { console.error(`Zod schema validation failed at: ${path}`, JSON.stringify(result.error, null, 2)); diff --git a/quadratic-client/src/shared/api/xhrFromApi.ts b/quadratic-client/src/shared/api/xhrFromApi.ts index 9e237d807d..7f7b7d1b39 100644 --- a/quadratic-client/src/shared/api/xhrFromApi.ts +++ b/quadratic-client/src/shared/api/xhrFromApi.ts @@ -1,4 +1,4 @@ -import { authClient } from '@/auth'; +import { authClient } from '@/auth/auth'; import { ApiError } from '@/shared/api/fetchFromApi'; import * as Sentry from '@sentry/react'; import z from 'zod'; diff --git a/quadratic-client/src/shared/components/ShareDialog.tsx b/quadratic-client/src/shared/components/ShareDialog.tsx index 670d39e78d..1fa7eeaa04 100644 --- a/quadratic-client/src/shared/components/ShareDialog.tsx +++ b/quadratic-client/src/shared/components/ShareDialog.tsx @@ -62,6 +62,7 @@ export function ShareTeamDialog({ data }: { data: ApiTypes['/v0/teams/:uuid.GET. users, invites, team: { uuid }, + license, } = data; const action = useMemo(() => ROUTES.TEAM(uuid), [uuid]); const numberOfOwners = useMemo(() => users.filter((user) => user.role === 'OWNER').length, [users]); @@ -118,6 +119,27 @@ export function ShareTeamDialog({ data }: { data: ApiTypes['/v0/teams/:uuid.GET. /> )} + {license.status === 'exceeded' && ( +
    +
    + Over the user limit! +
    + + You are over your user limit of {license.limits.seats}. Please contact Quadratic Support to increase your + limit. + +
    + )} + + {license.status === 'revoked' && ( +
    +
    + License Revoked! +
    + Your license has been revoked. Please contact Quadratic Support. +
    + )} + {users.map((user) => { const isLoggedInUser = userMakingRequest.id === user.id; const canDelete = isLoggedInUser diff --git a/quadratic-client/src/shared/constants/urls.ts b/quadratic-client/src/shared/constants/urls.ts index 7a0fba4a00..5bd2576910 100644 --- a/quadratic-client/src/shared/constants/urls.ts +++ b/quadratic-client/src/shared/constants/urls.ts @@ -17,3 +17,4 @@ export const CONTACT_URL = 'https://quadratichq.com/contact'; export const WEBSITE_CONNECTIONS = 'https://www.quadratichq.com/connections'; export const WEBSITE_EXAMPLES = 'https://www.quadratichq.com/examples'; export const WEBSITE_CHANGELOG = 'https://www.quadratichq.com/changelog'; +export const SCHEDULE_MEETING = 'https://calendly.com/d/ckz9-g8t-stb/quadratic-demo'; diff --git a/quadratic-client/src/shared/utils/analytics.ts b/quadratic-client/src/shared/utils/analytics.ts index 09577633cf..9f60062fac 100644 --- a/quadratic-client/src/shared/utils/analytics.ts +++ b/quadratic-client/src/shared/utils/analytics.ts @@ -1,12 +1,12 @@ import { debugShow } from '@/app/debugFlags'; +import { User as AuthUser } from '@/auth/auth'; import * as amplitude from '@amplitude/analytics-browser'; -import { User as Auth0User } from '@auth0/auth0-spa-js'; import { setUser } from '@sentry/react'; import mixpanel from 'mixpanel-browser'; // Quadratic only shares analytics on the QuadraticHQ.com hosted version where the environment variables are set. -type User = Auth0User | undefined; +type User = AuthUser | undefined; export function googleAnalyticsAvailable(): boolean { return import.meta.env.VITE_GOOGLE_ANALYTICS_GTAG && import.meta.env.VITE_GOOGLE_ANALYTICS_GTAG !== 'none'; diff --git a/quadratic-client/src/shared/utils/userUtil.ts b/quadratic-client/src/shared/utils/userUtil.ts index 4267f29850..25d43a895f 100644 --- a/quadratic-client/src/shared/utils/userUtil.ts +++ b/quadratic-client/src/shared/utils/userUtil.ts @@ -1,5 +1,5 @@ import { MultiplayerUser } from '@/app/web-workers/multiplayerWebWorker/multiplayerTypes'; -import { User } from '@auth0/auth0-spa-js'; +import { User } from '@/auth/auth'; export const displayName = (user: User | MultiplayerUser | undefined, you: boolean): string => { let name = ''; @@ -37,5 +37,5 @@ export const displayInitials = (user: User | MultiplayerUser | undefined): strin return user.email[0]; } } - return user?.index !== undefined ? user.index + 1 : '0'; + return user?.index !== undefined ? String(user.index + 1) : '0'; }; diff --git a/quadratic-connection/.env.docker b/quadratic-connection/.env.docker index 87620b829e..522914b032 100644 --- a/quadratic-connection/.env.docker +++ b/quadratic-connection/.env.docker @@ -2,8 +2,8 @@ HOST=0.0.0.0 PORT=3003 ENVIRONMENT=docker -AUTH0_JWKS_URI=https://dev-nje7dw8s.us.auth0.com/.well-known/jwks.json -QUADRATIC_API_URI=http://localhost:8000 +AUTH0_JWKS_URI=http://host.docker.internal:3000/.well-known/jwks.json +QUADRATIC_API_URI=http://host.docker.internal:8000 M2M_AUTH_TOKEN=M2M_AUTH_TOKEN MAX_RESPONSE_BYTES=15728640 # 15MB STATIC_IPS=0.0.0.0,127.0.0.1 diff --git a/quadratic-connection/src/error.rs b/quadratic-connection/src/error.rs index cb9e218881..d1ea9598a1 100644 --- a/quadratic-connection/src/error.rs +++ b/quadratic-connection/src/error.rs @@ -9,7 +9,7 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; -use quadratic_rust_shared::SharedError; +use quadratic_rust_shared::{clean_errors, SharedError}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -52,17 +52,6 @@ pub(crate) fn proxy_error(e: impl ToString) -> ConnectionError { ConnectionError::Proxy(e.to_string()) } -fn clean_errors(error: impl ToString) -> String { - let mut cleaned = error.to_string(); - let remove = vec!["error returned from database: "]; - - for r in remove { - cleaned = format!("{:?}", cleaned).replace(r, ""); - } - - cleaned -} - impl From for ConnectionError { fn from(error: SharedError) -> Self { match error { diff --git a/quadratic-connection/src/server.rs b/quadratic-connection/src/server.rs index 20959e98e5..2150f39a65 100644 --- a/quadratic-connection/src/server.rs +++ b/quadratic-connection/src/server.rs @@ -8,11 +8,11 @@ use axum::{ routing::{any, get, post}, Extension, Json, Router, }; -use jsonwebtoken::jwk::JwkSet; -use quadratic_rust_shared::{auth::jwt::get_jwks, sql::Connection}; +use quadratic_rust_shared::auth::jwt::get_jwks; +use quadratic_rust_shared::sql::Connection; use serde::{Deserialize, Serialize}; use std::{iter::once, time::Duration}; -use tokio::{sync::OnceCell, time}; +use tokio::time; use tower_http::{ cors::{Any, CorsLayer}, sensitive_headers::SetSensitiveHeadersLayer, @@ -37,21 +37,6 @@ use crate::{ const HEALTHCHECK_INTERVAL_S: u64 = 5; -static JWKS: OnceCell = OnceCell::const_new(); - -/// Get the constant JWKS for use throughout the application -/// The panics are intentional and will happen at startup -pub(crate) async fn get_const_jwks() -> &'static JwkSet { - JWKS.get_or_init(|| async { - let config = config().expect("Invalid config"); - - get_jwks(&config.auth0_jwks_uri) - .await - .expect("Unable to get JWKS") - }) - .await -} - #[derive(Serialize, Deserialize)] pub(crate) struct SqlQuery { pub(crate) query: String, @@ -167,7 +152,7 @@ pub(crate) async fn serve() -> Result<()> { .init(); let config = config()?; - let jwks = get_const_jwks().await; + let jwks = get_jwks(&config.auth0_jwks_uri).await?; let state = State::new(&config, Some(jwks.clone()))?; let app = app(state.clone())?; diff --git a/quadratic-files/.env.docker b/quadratic-files/.env.docker index 661e33c2bb..1691e5bfd9 100644 --- a/quadratic-files/.env.docker +++ b/quadratic-files/.env.docker @@ -6,16 +6,25 @@ TRUNCATE_FILE_CHECK_S=60 TRUNCATE_TRANSACTION_AGE_DAYS=5 # ENVIRONMENT=docker -QUADRATIC_API_URI=http://quadratic-api:8000 +AUTH0_JWKS_URI=http://host.docker.internal:3000/.well-known/jwks.json +QUADRATIC_API_URI=http://host.docker.internal:8000 M2M_AUTH_TOKEN=M2M_AUTH_TOKEN -PUBSUB_HOST=redis +PUBSUB_HOST=host.docker.internal PUBSUB_PORT=6379 PUBSUB_PASSWORD= PUBSUB_ACTIVE_CHANNELS=active_channels PUBSUB_PROCESSED_TRANSACTIONS_CHANNEL=processed_transactions +# Storage +STORAGE_TYPE=file-system # s3 or file-system + +# Storage: s3 AWS_S3_REGION= AWS_S3_BUCKET_NAME=quadratic-api-docker AWS_S3_ACCESS_KEY_ID= AWS_S3_SECRET_ACCESS_KEY= + +# Storage: file-system +STORAGE_DIR=/file-storage +STORAGE_ENCRYPTION_KEYS=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc diff --git a/quadratic-files/.env.example b/quadratic-files/.env.example index 525b4a593f..8d18ad5335 100644 --- a/quadratic-files/.env.example +++ b/quadratic-files/.env.example @@ -6,6 +6,7 @@ FILES_PER_CHECK=100 TRUNCATE_FILE_CHECK_S=3600 # 1 hour TRUNCATE_TRANSACTION_AGE_DAYS=5 # 5 days +AUTH0_JWKS_URI=https://dev-nje7dw8s.us.auth0.com/.well-known/jwks.json QUADRATIC_API_URI=http://localhost:8000 M2M_AUTH_TOKEN=M2M_AUTH_TOKEN @@ -15,7 +16,15 @@ PUBSUB_PASSWORD= PUBSUB_ACTIVE_CHANNELS=active_channels PUBSUB_PROCESSED_TRANSACTIONS_CHANNEL=processed_transactions +# Storage +STORAGE_TYPE=s3 # s3 or file-system + +# Storage: s3 AWS_S3_REGION=us-east-2 AWS_S3_BUCKET_NAME=quadratic-api-docker AWS_S3_ACCESS_KEY_ID=test -AWS_S3_SECRET_ACCESS_KEY=test \ No newline at end of file +AWS_S3_SECRET_ACCESS_KEY=test + +# Storage: file-system +STORAGE_DIR=./../docker/file-storage +STORAGE_ENCRYPTION_KEYS=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc \ No newline at end of file diff --git a/quadratic-files/.env.test b/quadratic-files/.env.test index ca08711da8..fd3e72165d 100644 --- a/quadratic-files/.env.test +++ b/quadratic-files/.env.test @@ -6,6 +6,7 @@ TRUNCATE_FILE_CHECK_S=3600 # 1 hour TRUNCATE_TRANSACTION_AGE_DAYS=5 # 5 days ENVIRONMENT=test +AUTH0_JWKS_URI=https://dev-nje7dw8s.us.auth0.com/.well-known/jwks.json QUADRATIC_API_URI=http://localhost:8000 M2M_AUTH_TOKEN=M2M_AUTH_TOKEN @@ -15,7 +16,15 @@ PUBSUB_PASSWORD= PUBSUB_ACTIVE_CHANNELS=active_channels PUBSUB_PROCESSED_TRANSACTIONS_CHANNEL=processed_transactions +# Storage +STORAGE_TYPE=s3 # s3 or file-system + +# Storage: s3 AWS_S3_REGION= -AWS_S3_BUCKET_NAME= +AWS_S3_BUCKET_NAME=quadratic-api-docker AWS_S3_ACCESS_KEY_ID= -AWS_S3_SECRET_ACCESS_KEY= \ No newline at end of file +AWS_S3_SECRET_ACCESS_KEY= + +# Storage: file-system +STORAGE_DIR=./../docker/file-storage +STORAGE_ENCRYPTION_KEYS=eb4758047f74bdb2603cce75c4370327ca2c3662c4786867659126da8e64dfcc \ No newline at end of file diff --git a/quadratic-files/Cargo.toml b/quadratic-files/Cargo.toml index a34edefaeb..b41e0c7297 100644 --- a/quadratic-files/Cargo.toml +++ b/quadratic-files/Cargo.toml @@ -5,8 +5,9 @@ edition = "2021" authors = ["David DiMaria "] [dependencies] -axum = { version = "0.7.1", features = ["ws"] } +axum = { version = "0.7.1", features = ["macros"] } axum-extra = { version = "0.9.0", features = ["typed-header"] } +bytes = "1.6.1" chrono = { version = "0.4.31", features = ["serde"] } dotenv = "0.15.0" envy = "0.4.2" @@ -28,8 +29,9 @@ strum = "0.26.3" strum_macros = "0.25.3" thiserror = "1.0.50" tokio = { version = "1.34.0", features = ["full"] } +tokio-util = "0.7.11" tower = { version = "0.4.13", features = ["util"] } -tower-http = { version = "0.5.0", features = ["fs", "trace"] } +tower-http = { version = "0.5.0", features = ["cors", "fs", "trace", "validate-request"] } tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } uuid = { version = "1.6.1", features = ["serde", "v4"] } diff --git a/quadratic-files/src/auth.rs b/quadratic-files/src/auth.rs new file mode 100644 index 0000000000..66b7708d00 --- /dev/null +++ b/quadratic-files/src/auth.rs @@ -0,0 +1,71 @@ +//! Authentication and authorization middleware. +//! +//! + +use axum::{ + async_trait, + extract::{FromRef, FromRequestParts}, + http::request::Parts, + RequestPartsExt, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use jsonwebtoken::jwk::JwkSet; +use quadratic_rust_shared::auth::jwt::authorize; +use serde::{Deserialize, Serialize}; + +use crate::error::{FilesError, Result}; + +/// The claims from the Quadratic/Auth0 JWT token. +/// We need our own implementation of this because we need to impl on it. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Claims { + pub sub: String, + pub exp: usize, +} + +/// Instance of Axum's middleware that also contains a copy of state +#[cfg(not(test))] +pub fn get_middleware(jwks: JwkSet) -> axum::middleware::FromExtractorLayer { + axum::middleware::from_extractor_with_state::(jwks) +} + +// Middleware that accepts json for tests +#[cfg(test)] +pub fn get_middleware( + _jwks: JwkSet, +) -> tower_http::validate_request::ValidateRequestHeaderLayer< + tower_http::validate_request::AcceptHeader, +> { + tower_http::validate_request::ValidateRequestHeaderLayer::accept("application/json") +} + +/// Extract the claims from the request. +/// Anytime a claims parameter is added to a handler, this will automatically +/// be called. +#[async_trait] +impl FromRequestParts for Claims +where + JwkSet: FromRef, + S: Send + Sync, +{ + type Rejection = FilesError; + + async fn from_request_parts(parts: &mut Parts, jwks: &S) -> Result { + let jwks = JwkSet::from_ref(jwks); + // Extract the token from the authorization header + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|e| FilesError::Authentication(e.to_string()))?; + + let token_data = authorize(&jwks, bearer.token(), false, true)?; + + Ok(token_data.claims) + } +} + +#[cfg(test)] +pub(crate) mod tests {} diff --git a/quadratic-files/src/config.rs b/quadratic-files/src/config.rs index 8ecaaa362a..ea0061e32f 100644 --- a/quadratic-files/src/config.rs +++ b/quadratic-files/src/config.rs @@ -9,6 +9,14 @@ use crate::error::{FilesError, Result}; use dotenv::dotenv; use quadratic_rust_shared::environment::Environment; use serde::Deserialize; +use strum_macros::Display; + +#[derive(Deserialize, Debug, Display)] +#[serde(rename_all = "kebab-case")] +pub(crate) enum StorageType { + S3, + FileSystem, +} #[allow(dead_code)] #[derive(Deserialize, Debug)] @@ -27,13 +35,22 @@ pub(crate) struct Config { pub(crate) pubsub_active_channels: String, pub(crate) pubsub_processed_transactions_channel: String, + pub(crate) auth0_jwks_uri: String, pub(crate) quadratic_api_uri: String, pub(crate) m2m_auth_token: String, - pub(crate) aws_s3_region: String, - pub(crate) aws_s3_bucket_name: String, - pub(crate) aws_s3_access_key_id: String, - pub(crate) aws_s3_secret_access_key: String, + // Storage Type: s3 or file-system + pub(crate) storage_type: StorageType, + + // StorageType::S3 + pub(crate) aws_s3_region: Option, + pub(crate) aws_s3_bucket_name: Option, + pub(crate) aws_s3_access_key_id: Option, + pub(crate) aws_s3_secret_access_key: Option, + + // StorageType::FileSystem + pub(crate) storage_dir: Option, + pub(crate) storage_encryption_keys: Option>, } /// Load the global configuration from the environment into Config. diff --git a/quadratic-files/src/error.rs b/quadratic-files/src/error.rs index 7444ba7e6d..762fb58a18 100644 --- a/quadratic-files/src/error.rs +++ b/quadratic-files/src/error.rs @@ -5,14 +5,18 @@ //! Convert third party crate errors to application errors. //! Convert errors to responses. -use quadratic_rust_shared::{Aws, SharedError}; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, +}; +use quadratic_rust_shared::{clean_errors, storage::error::Storage as StorageError, SharedError}; use serde::{Deserialize, Serialize}; use thiserror::Error; pub(crate) type Result = std::result::Result; #[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub(crate) enum FilesError { +pub enum FilesError { #[error("Authentication error: {0}")] Authentication(String), @@ -34,12 +38,18 @@ pub(crate) enum FilesError { #[error("Internal server error: {0}")] InternalServer(String), - #[error("Unable to load file {0} from bucket {1}: {2}")] - LoadFile(String, String, String), + #[error("Unable to load file {0}: {1}")] + LoadFile(String, String), + + #[error("Not Found: {0}")] + NotFound(String), #[error("PubSub error: {0}")] PubSub(String), + #[error("QuadraticApi error: {0}")] + QuadraticApi(String), + #[error("Error requesting data: {0}")] Request(String), @@ -49,6 +59,9 @@ pub(crate) enum FilesError { #[error("Error serializing or deserializing: {0}")] Serialization(String), + #[error("File storage error: {0}")] + Storage(String), + #[error("Transaction queue error: {0}")] TransactionQueue(String), @@ -59,14 +72,37 @@ pub(crate) enum FilesError { Unknown(String), } +// Convert FilesErrors into readable responses with appropriate status codes. +// These are the errors that are returned to the client. +impl IntoResponse for FilesError { + fn into_response(self) -> Response { + let (status, error) = match &self { + FilesError::Authentication(error) => (StatusCode::UNAUTHORIZED, clean_errors(error)), + FilesError::InternalServer(error) => { + (StatusCode::INTERNAL_SERVER_ERROR, clean_errors(error)) + } + FilesError::NotFound(error) => (StatusCode::NOT_FOUND, clean_errors(error)), + _ => (StatusCode::INTERNAL_SERVER_ERROR, "Unknown".into()), + }; + + tracing::warn!("{:?}", self); + + (status, error).into_response() + } +} + impl From for FilesError { fn from(error: SharedError) -> Self { match error { - SharedError::Aws(aws) => match aws { - Aws::S3(error) => FilesError::S3(error), - }, + SharedError::Auth(error) => FilesError::Authentication(error.to_string()), + SharedError::Aws(error) => FilesError::S3(error.to_string()), SharedError::PubSub(error) => FilesError::PubSub(error), - _ => FilesError::Unknown(format!("Unknown Quadratic API error: {error}")), + SharedError::QuadraticApi(error) => FilesError::QuadraticApi(error), + SharedError::Storage(error) => match error { + StorageError::Read(key, _) => FilesError::NotFound(format!("File {key} not found")), + _ => FilesError::Storage(error.to_string()), + }, + _ => FilesError::Unknown(format!("Unknown SharedError: {error}")), } } } diff --git a/quadratic-files/src/file.rs b/quadratic-files/src/file.rs index 9cab93da44..ce3e8e9e8f 100644 --- a/quadratic-files/src/file.rs +++ b/quadratic-files/src/file.rs @@ -15,12 +15,9 @@ use quadratic_core::{ }, }; use quadratic_rust_shared::{ - aws::{ - s3::{download_object, upload_object}, - Client, - }, pubsub::PubSub as PubSubTrait, quadratic_api::{get_file_checkpoint, set_file_checkpoint}, + storage::{Storage, StorageContainer}, }; use crate::{ @@ -48,18 +45,14 @@ pub(crate) fn apply_transaction(grid: &mut GridController, operations: Vec Result { - let file = download_object(client, bucket, key).await?; - let body = file - .body - .collect() + let body = storage + .read(key) .await - .map_err(|e| FilesError::LoadFile(key.into(), bucket.to_string(), e.to_string()))? - .into_bytes(); + .map_err(|e| FilesError::LoadFile(key.into(), e.to_string()))?; let grid = load_file(key, body.to_vec())?; Ok(GridController::from_grid(grid, sequence_num)) @@ -71,16 +64,14 @@ pub(crate) fn key(file_id: Uuid, sequence: u64) -> String { /// Load a file from S3, add it to memory, process transactions and upload it back to S3 pub(crate) async fn process_transactions( - client: &Client, - bucket: &str, + storage: &StorageContainer, file_id: Uuid, checkpoint_sequence_num: u64, final_sequence_num: u64, operations: Vec, ) -> Result { let mut grid = get_and_load_object( - client, - bucket, + storage, &key(file_id, checkpoint_sequence_num), checkpoint_sequence_num, ) @@ -91,7 +82,7 @@ pub(crate) async fn process_transactions( apply_transaction(&mut grid, operations); let body = export_file(&key, grid.into_grid())?; - upload_object(client, bucket, &key, &body).await?; + storage.write(&key, &body.into()).await?; Ok(final_sequence_num) } @@ -106,8 +97,7 @@ pub(crate) async fn process_queue_for_room( let channel = &file_id.to_string(); let Settings { - aws_client, - aws_s3_bucket_name, + storage, quadratic_api_uri, m2m_auth_token, .. @@ -174,8 +164,7 @@ pub(crate) async fn process_queue_for_room( // process the transactions and save the file to S3 let last_sequence_num = process_transactions( - aws_client, - aws_s3_bucket_name, + storage, *file_id, checkpoint_sequence_num, last_sequence_num, @@ -200,6 +189,7 @@ pub(crate) async fn process_queue_for_room( // update the checkpoint in quadratic-api let key = &key(*file_id, last_sequence_num); + set_file_checkpoint( quadratic_api_uri, m2m_auth_token, @@ -207,7 +197,7 @@ pub(crate) async fn process_queue_for_room( last_sequence_num, CURRENT_VERSION.into(), key.to_owned(), - aws_s3_bucket_name.to_owned(), + storage.path().to_owned(), ) .await?; diff --git a/quadratic-files/src/main.rs b/quadratic-files/src/main.rs index 880d4258c1..a2499ff47f 100644 --- a/quadratic-files/src/main.rs +++ b/quadratic-files/src/main.rs @@ -3,11 +3,13 @@ //! A file servic for that consumes transactions from a queue, applies them to //! a grid and writes them to S3. +mod auth; mod config; mod error; mod file; mod server; mod state; +mod storage; #[cfg(test)] mod test_util; mod truncate; diff --git a/quadratic-files/src/server.rs b/quadratic-files/src/server.rs index a9ecdeba7b..8a829f3599 100644 --- a/quadratic-files/src/server.rs +++ b/quadratic-files/src/server.rs @@ -3,33 +3,80 @@ //! Handle bootstrapping and starting the HTTP server. Adds global state //! to be shared across all requests and threads. Adds tracing/logging. -use axum::http::StatusCode; +use axum::http::{Method, StatusCode}; use axum::response::IntoResponse; use axum::{routing::get, Extension, Router}; +use quadratic_rust_shared::auth::jwt::get_jwks; +use quadratic_rust_shared::storage::Storage; use std::time::Duration; use std::{net::SocketAddr, sync::Arc}; use tokio::time; +use tower_http::cors::{Any, CorsLayer}; use tower_http::trace::{DefaultMakeSpan, TraceLayer}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use crate::storage::{get_presigned_storage, get_storage}; use crate::truncate::truncate_processed_transactions; use crate::{ + auth::get_middleware, config::config, error::{FilesError, Result}, file::process, state::State, + storage::upload_storage, }; -const HEALTHCHECK_INTERVAL_S: u64 = 5; +const HEALTHCHECK_INTERVAL_S: u64 = 30; /// Construct the application router. This is separated out so that it can be /// integration tested. pub(crate) fn app(state: Arc) -> Router { + // get the auth middleware + let jwks = state + .settings + .jwks + .as_ref() + .expect("JWKS not found in state") + .to_owned(); + let auth = get_middleware(jwks); + let path = state.settings.storage.path().to_owned(); + + let cors = CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any) + .allow_headers(Any) + .expose_headers(Any); + + tracing::info!("Serving files from {path}"); + Router::new() - // routes + // PROTECTED ROUTES (via JWT) + // + // get a file from storage + .route( + "/storage/:key", + get(get_storage) + // + // upload a file + .post(upload_storage), + ) + // + // auth middleware + .route_layer(auth) + // + // UNPROTECTED ROUTES + // .route("/health", get(healthcheck)) + // + // presigned urls + .route("/storage/presigned/:key", get(get_presigned_storage)) + // // state .layer(Extension(state)) + // + // cors + .layer(cors) + // // logger .layer( TraceLayer::new_for_http() @@ -49,7 +96,8 @@ pub(crate) async fn serve() -> Result<()> { .init(); let config = config()?; - let state = Arc::new(State::new(&config).await?); + let jwks = get_jwks(&config.auth0_jwks_uri).await?; + let state = Arc::new(State::new(&config, Some(jwks)).await?); let app = app(Arc::clone(&state)); let listener = tokio::net::TcpListener::bind(format!("{}:{}", config.host, config.port)) diff --git a/quadratic-files/src/state/mod.rs b/quadratic-files/src/state/mod.rs index cb6bc80108..b850330412 100644 --- a/quadratic-files/src/state/mod.rs +++ b/quadratic-files/src/state/mod.rs @@ -7,6 +7,7 @@ pub mod pubsub; pub mod settings; pub mod stats; +use jsonwebtoken::jwk::JwkSet; use quadratic_rust_shared::pubsub::redis_streams::RedisStreamsConfig; use quadratic_rust_shared::pubsub::Config as PubSubConfig; use tokio::sync::Mutex; @@ -26,7 +27,7 @@ pub(crate) struct State { } impl State { - pub(crate) async fn new(config: &Config) -> Result { + pub(crate) async fn new(config: &Config, jwks: Option) -> Result { let pubsub_config = PubSubConfig::RedisStreams(RedisStreamsConfig { host: config.pubsub_host.to_owned(), port: config.pubsub_port.to_owned(), @@ -36,7 +37,7 @@ impl State { Ok(State { pubsub: Mutex::new(PubSub::new(pubsub_config).await?), - settings: Settings::new(config).await, + settings: Settings::new(config, jwks).await, stats: Mutex::new(Stats::new()), }) } diff --git a/quadratic-files/src/state/settings.rs b/quadratic-files/src/state/settings.rs index fa4b920aeb..92db063cca 100644 --- a/quadratic-files/src/state/settings.rs +++ b/quadratic-files/src/state/settings.rs @@ -1,33 +1,60 @@ -use quadratic_rust_shared::aws::{client, Client}; +use jsonwebtoken::jwk::JwkSet; +use quadratic_rust_shared::aws::client; use quadratic_rust_shared::environment::Environment; +use quadratic_rust_shared::storage::file_system::{FileSystem, FileSystemConfig}; +use quadratic_rust_shared::storage::s3::{S3Config, S3}; +use quadratic_rust_shared::storage::StorageContainer; -use crate::config::Config; +use crate::config::{Config, StorageType}; #[derive(Debug)] pub(crate) struct Settings { + pub(crate) jwks: Option, pub(crate) quadratic_api_uri: String, pub(crate) m2m_auth_token: String, - pub(crate) aws_client: Client, - pub(crate) aws_s3_bucket_name: String, + pub(crate) storage: StorageContainer, pub(crate) pubsub_processed_transactions_channel: String, } impl Settings { - pub(crate) async fn new(config: &Config) -> Self { + // Create a new Settings struct from the provided Config. + // Panics are OK here since this is set at startup and we want to fail fast. + pub(crate) async fn new(config: &Config, jwks: Option) -> Self { let is_local = config.environment == Environment::Docker || config.environment == Environment::Local; + let expected = |val: &Option, var: &str| { + val.to_owned() + .unwrap_or_else(|| panic!("Expected {} to have a value", var)) + }; + + let storage = match config.storage_type { + StorageType::S3 => StorageContainer::S3(S3::new(S3Config { + client: client( + &expected(&config.aws_s3_access_key_id, "AWS_S3_ACCESS_KEY_ID"), + &expected(&config.aws_s3_secret_access_key, "AWS_S3_SECRET_ACCESS_KEY"), + &expected(&config.aws_s3_region, "AWS_S3_REGION"), + "Quadratic File Service", + is_local, + ) + .await, + bucket: expected(&config.aws_s3_bucket_name, "AWS_S3_BUCKET_NAME"), + })), + StorageType::FileSystem => { + StorageContainer::FileSystem(FileSystem::new(FileSystemConfig { + path: expected(&config.storage_dir, "STORAGE_DIR"), + encryption_keys: config + .storage_encryption_keys + .to_owned() + .expect("Expected STORAGE_ENCRYPTION_KEYS to have a value"), + })) + } + }; + Settings { + jwks, quadratic_api_uri: config.quadratic_api_uri.to_owned(), m2m_auth_token: config.m2m_auth_token.to_owned(), - aws_client: client( - &config.aws_s3_access_key_id, - &config.aws_s3_secret_access_key, - &config.aws_s3_region, - "Quadratic File Service", - is_local, - ) - .await, - aws_s3_bucket_name: config.aws_s3_bucket_name.to_owned(), + storage, pubsub_processed_transactions_channel: config .pubsub_processed_transactions_channel .to_owned(), diff --git a/quadratic-files/src/storage.rs b/quadratic-files/src/storage.rs new file mode 100644 index 0000000000..296e1f70c7 --- /dev/null +++ b/quadratic-files/src/storage.rs @@ -0,0 +1,82 @@ +use axum::{ + body::to_bytes, + extract::{Path, Request}, + response::IntoResponse, + Extension, Json, +}; +use quadratic_rust_shared::{ + crypto::aes_cbc::decrypt_from_api, + storage::{Storage, StorageConfig}, +}; +use serde::Serialize; +use std::sync::Arc; + +use crate::error::{FilesError, Result}; +use crate::state::State; + +#[derive(Debug, Serialize)] +pub(crate) struct UploadStorageResponse { + bucket: String, + key: String, +} + +/// Get a file from storage +pub(crate) async fn get_storage( + Path(file_name): Path, + state: Extension>, +) -> Result { + tracing::trace!("Get file {}", file_name); + + let file = state.settings.storage.read(&file_name).await?; + Ok(file.into_response()) +} + +/// Get a file from storage from a presigned URL (encrypted) +pub(crate) async fn get_presigned_storage( + Path(encrypted_file_name): Path, + state: Extension>, +) -> Result { + tracing::trace!("Get presigned file {}", encrypted_file_name); + + match state.settings.storage.config() { + StorageConfig::FileSystem(config) => { + // For now, we only support one encryption key. + // In the future, implement key traversal on decryption failures. + let key = config + .encryption_keys + .first() + .ok_or(FilesError::Storage("No encryption keys found".to_string()))?; + let file_name = decrypt_from_api(key, &encrypted_file_name)?; + let file = state.settings.storage.read(&file_name).await?; + + Ok(file.into_response()) + } + _ => Err(FilesError::Storage( + "Presigned URLs only supported in FileSystem storage options".to_string(), + )), + } +} + +/// Upload a file to storage +pub(crate) async fn upload_storage( + Path(file_name): Path, + state: Extension>, + request: Request, +) -> Result> { + tracing::trace!( + "Uploading file {} to {}", + file_name, + state.settings.storage.path() + ); + + let bytes = to_bytes(request.into_body(), usize::MAX) + .await + .map_err(|e| FilesError::Storage(e.to_string()))?; + + state.settings.storage.write(&file_name, &bytes).await?; + + Ok(Json(UploadStorageResponse { + bucket: state.settings.storage.path().to_owned(), + key: file_name, + })) +} diff --git a/quadratic-files/src/test_util.rs b/quadratic-files/src/test_util.rs index 953693c847..d1d33932ca 100644 --- a/quadratic-files/src/test_util.rs +++ b/quadratic-files/src/test_util.rs @@ -12,7 +12,7 @@ use crate::state::State; pub(crate) async fn new_state() -> State { let config = config().unwrap(); - State::new(&config).await.unwrap() + State::new(&config, None).await.unwrap() } pub(crate) async fn new_arc_state() -> Arc { diff --git a/quadratic-multiplayer/.env.docker b/quadratic-multiplayer/.env.docker index 2f33cf116d..a6dfb7fa0f 100644 --- a/quadratic-multiplayer/.env.docker +++ b/quadratic-multiplayer/.env.docker @@ -2,14 +2,14 @@ HOST=0.0.0.0 PORT=3001 HEARTBEAT_CHECK_S=3 HEARTBEAT_TIMEOUT_S=600 -QUADRATIC_API_URI=http://quadratic-api:8000 +QUADRATIC_API_URI=http://host.docker.internal:8000 M2M_AUTH_TOKEN=M2M_AUTH_TOKEN ENVIRONMENT=docker -PUBSUB_HOST=redis +PUBSUB_HOST=host.docker.internal PUBSUB_PORT=6379 PUBSUB_PASSWORD= PUBSUB_ACTIVE_CHANNELS=active_channels -AUTH0_JWKS_URI=https://dev-nje7dw8s.us.auth0.com/.well-known/jwks.json +AUTH0_JWKS_URI=http://host.docker.internal:3000/.well-known/jwks.json AUTHENTICATE_JWT=true \ No newline at end of file diff --git a/quadratic-multiplayer/src/error.rs b/quadratic-multiplayer/src/error.rs index 5ab5a3bdf3..460fe19727 100644 --- a/quadratic-multiplayer/src/error.rs +++ b/quadratic-multiplayer/src/error.rs @@ -5,7 +5,7 @@ //! Convert third party crate errors to application errors. //! Convert errors to responses. -use quadratic_rust_shared::{Aws, SharedError}; +use quadratic_rust_shared::{aws::error::Aws as AwsError, SharedError}; use serde::{Deserialize, Serialize}; use strum_macros::{Display, EnumString}; use thiserror::Error; @@ -108,7 +108,7 @@ impl From for MpError { match error { SharedError::QuadraticApi(error) => MpError::FilePermissions(error), SharedError::Aws(aws) => match aws { - Aws::S3(error) => MpError::S3(error), + AwsError::S3(error) => MpError::S3(error), }, SharedError::PubSub(error) => MpError::PubSub(error), _ => MpError::Unknown(format!("Unknown Quadratic API error: {error}")), diff --git a/quadratic-rust-shared/Cargo.toml b/quadratic-rust-shared/Cargo.toml index 00b647908a..fa98bb2427 100644 --- a/quadratic-rust-shared/Cargo.toml +++ b/quadratic-rust-shared/Cargo.toml @@ -6,6 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aes = "0.8.4" arrow = "53.0.0" arrow-array = "53.0.0" async-trait = "0.1.80" @@ -13,8 +14,10 @@ aws-config = { version = "1.1.1", features = ["behavior-version-latest"] } aws-sdk-s3 = { version = "1.12.0", features = ["behavior-version-latest", "rt-tokio"] } bigdecimal = "0.4.5" # need this fixed to the sqlx dependency bytes = "1.6.0" +cbc = { version = "0.1.2", features = ["alloc"] } chrono = "0.4.31" futures-util = "0.3.30" +hex = "0.4.3" httpmock = { git = "https://github.com/quadratichq/httpmock", version = "0.8.0-alpha.1", features = [ "proxy", "record", @@ -51,7 +54,12 @@ test = ["httpmock"] [dev-dependencies] arrow = { version = "53.0.0", features = ["prettyprint"] } +aws-smithy-async = { version = "1.2.1", features = ["test-util"] } +aws-smithy-runtime = {version = "1.6.2", features = ["test-util"] } +aws-smithy-runtime-api = "1.7.1" httpmock = { git = "https://github.com/quadratichq/httpmock", version = "0.8.0-alpha.1", features = [ "proxy", "record", ] } +http = "1.1.0" +tracing-test = "0.2.4" diff --git a/quadratic-rust-shared/package.json b/quadratic-rust-shared/package.json new file mode 100644 index 0000000000..39dba83e85 --- /dev/null +++ b/quadratic-rust-shared/package.json @@ -0,0 +1,19 @@ +{ + "name": "quadratic-rust-shared", + "description": "Shared Rust code in Quadratic", + "version": "0.1.0", + "dependencies": {}, + "devDependencies": {}, + "scripts": { + "start": "RUST_LOG=info cargo run", + "build": "cargo build", + "dev": "RUST_LOG=info cargo watch -x 'run'", + "test": "cargo test", + "test:watch": "RUST_LOG=info cargo watch -x 'test'", + "lint": "cargo clippy --all-targets --all-features -- -D warnings", + "coverage": "npm run coverage:gen && npm run coverage:html && npm run coverage:view", + "coverage:gen": "CARGO_INCREMENTAL=0 RUSTFLAGS='-Cinstrument-coverage' LLVM_PROFILE_FILE='coverage/cargo-test-%p-%m.profraw' cargo test", + "coverage:html": "grcov . --binary-path ./target/debug/deps/ -s . -t html --branch --ignore-not-existing --ignore '../*' --ignore '/*' -o coverage/html", + "coverage:view": "open coverage/html/index.html" + } +} diff --git a/quadratic-rust-shared/src/arrow/error.rs b/quadratic-rust-shared/src/arrow/error.rs new file mode 100644 index 0000000000..b2204c5734 --- /dev/null +++ b/quadratic-rust-shared/src/arrow/error.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Arrow { + #[error("Arrow error: {0}")] + External(String), +} diff --git a/quadratic-rust-shared/src/arrow/mod.rs b/quadratic-rust-shared/src/arrow/mod.rs index 0ffd363580..5c86d7b4e3 100644 --- a/quadratic-rust-shared/src/arrow/mod.rs +++ b/quadratic-rust-shared/src/arrow/mod.rs @@ -1 +1,2 @@ pub mod arrow_type; +pub mod error; diff --git a/quadratic-rust-shared/src/auth/error.rs b/quadratic-rust-shared/src/auth/error.rs new file mode 100644 index 0000000000..cf82f97170 --- /dev/null +++ b/quadratic-rust-shared/src/auth/error.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Auth { + #[error("JWT error: {0}")] + Jwt(String), +} diff --git a/quadratic-rust-shared/src/auth/jwt.rs b/quadratic-rust-shared/src/auth/jwt.rs index 90ec2afac8..9e6383b334 100644 --- a/quadratic-rust-shared/src/auth/jwt.rs +++ b/quadratic-rust-shared/src/auth/jwt.rs @@ -1,9 +1,20 @@ -use jsonwebtoken::jwk::AlgorithmParameters; +use jsonwebtoken::jwk::{AlgorithmParameters, JwkSet}; use jsonwebtoken::{decode, decode_header, jwk, Algorithm, DecodingKey, TokenData, Validation}; use serde::de::DeserializeOwned; use std::str::FromStr; +use tokio::sync::OnceCell; -use crate::error::{Auth, Result, SharedError}; +use crate::auth::error::Auth; +use crate::error::{Result, SharedError}; + +pub static JWKS: OnceCell = OnceCell::const_new(); + +/// Get the constant JWKS for use throughout the application +/// The panics are intentional and will happen at startup +pub async fn get_const_jwks(jwks_uri: &str) -> &'static JwkSet { + JWKS.get_or_init(|| async { get_jwks(jwks_uri).await.expect("Unable to get JWKS") }) + .await +} /// Get the JWK set from a given URL. pub async fn get_jwks(url: &str) -> Result { diff --git a/quadratic-rust-shared/src/auth/mod.rs b/quadratic-rust-shared/src/auth/mod.rs index 417233c083..73491e1474 100644 --- a/quadratic-rust-shared/src/auth/mod.rs +++ b/quadratic-rust-shared/src/auth/mod.rs @@ -1 +1,2 @@ +pub mod error; pub mod jwt; diff --git a/quadratic-rust-shared/src/aws/error.rs b/quadratic-rust-shared/src/aws/error.rs new file mode 100644 index 0000000000..2846e2197d --- /dev/null +++ b/quadratic-rust-shared/src/aws/error.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Aws { + #[error("Error communicating with AWS: {0}")] + S3(String), +} diff --git a/quadratic-rust-shared/src/aws/mod.rs b/quadratic-rust-shared/src/aws/mod.rs index b4048e379f..8ec4d50623 100644 --- a/quadratic-rust-shared/src/aws/mod.rs +++ b/quadratic-rust-shared/src/aws/mod.rs @@ -1,3 +1,4 @@ +pub mod error; pub mod s3; pub use aws_config::{retry::RetryConfig, BehaviorVersion, Region}; diff --git a/quadratic-rust-shared/src/aws/s3.rs b/quadratic-rust-shared/src/aws/s3.rs index dccb37fd4a..fae7188580 100644 --- a/quadratic-rust-shared/src/aws/s3.rs +++ b/quadratic-rust-shared/src/aws/s3.rs @@ -4,7 +4,8 @@ use aws_sdk_s3::{ Client, }; -use crate::error::{Aws, Result, SharedError}; +use crate::aws::error::Aws as AwsError; +use crate::error::{Result, SharedError}; pub async fn download_object(client: &Client, bucket: &str, key: &str) -> Result { client @@ -14,7 +15,7 @@ pub async fn download_object(client: &Client, bucket: &str, key: &str) -> Result .send() .await .map_err(|error| { - SharedError::Aws(Aws::S3(format!( + SharedError::Aws(AwsError::S3(format!( "Error retrieving file {} from bucket {}: {:?}.", key, bucket, error ))) @@ -37,7 +38,7 @@ pub async fn upload_object( .send() .await .map_err(|error| { - SharedError::Aws(Aws::S3(format!( + SharedError::Aws(AwsError::S3(format!( "Error uploading file {key} to bucket {bucket}: {:?}.", error ))) @@ -45,4 +46,49 @@ pub async fn upload_object( } #[cfg(test)] -mod tests {} +pub mod tests { + // use aws_config::{imds::Client as ImdsClient, provider_config::ProviderConfig}; + // use aws_sdk_s3::primitives::SdkBody; + // use aws_smithy_async::test_util::InstantSleep; + // use aws_smithy_runtime::client::http::test_util::{ReplayEvent, StaticReplayClient}; + // use aws_smithy_runtime_api::client::orchestrator::{HttpRequest, HttpResponse}; + // use http::Uri; + + // pub fn imds_request(path: &'static str, token: &str) -> HttpRequest { + // http::Request::builder() + // .uri(Uri::from_static(path)) + // .method("GET") + // .header("x-aws-ec2-metadata-token", token) + // .body(SdkBody::empty()) + // .unwrap() + // .try_into() + // .unwrap() + // } + + // pub fn imds_response(body: &'static str) -> HttpResponse { + // HttpResponse::try_from( + // http::Response::builder() + // .status(200) + // .body(SdkBody::from(body)) + // .unwrap(), + // ) + // .unwrap() + // } + + // pub fn make_imds_client(http_client: &StaticReplayClient) -> ImdsClient { + // tokio::time::pause(); + // ImdsClient::builder() + // .configure( + // &ProviderConfig::empty() + // .with_sleep_impl(InstantSleep::unlogged()) + // .with_http_client(http_client.clone()), + // ) + // .build() + // } + + // pub fn mock_imds_client(events: Vec) -> (ImdsClient, StaticReplayClient) { + // let http_client = StaticReplayClient::new(events); + // let client = make_imds_client(&http_client); + // (client, http_client) + // } +} diff --git a/quadratic-rust-shared/src/crypto/aes_cbc.rs b/quadratic-rust-shared/src/crypto/aes_cbc.rs new file mode 100644 index 0000000000..94313a3cf6 --- /dev/null +++ b/quadratic-rust-shared/src/crypto/aes_cbc.rs @@ -0,0 +1,75 @@ +use std::fmt::Debug; + +use aes::{ + cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit}, + Aes256, +}; +use bytes::Bytes; +use cbc::{Decryptor, Encryptor}; + +use crate::{crypto::error::Crypto as CryptoError, error::Result, SharedError}; + +type Aes256CbcEnc = Encryptor; +type Aes256CbcDec = Decryptor; + +/// Encrypt data using AES-256-CBC. +pub fn encrypt(key: &[u8; 32], iv: &[u8; 16], data: &[u8]) -> Result { + let encryptor = Aes256CbcEnc::new(key.into(), iv.into()); + let encrypted = encryptor.encrypt_padded_vec_mut::(data).to_owned(); + + Ok(encrypted.into()) +} + +/// Convenience function to handle errors when decrypting data. +fn decrypt_error(e: impl Debug) -> SharedError { + let error = CryptoError::AesCbcDecode(format!("Error decoding data: {:?}", e)); + SharedError::Crypto(error) +} + +/// Decrypt data using AES-256-CBC. +pub fn decrypt(key: &[u8; 32], iv: &[u8; 16], data: &[u8]) -> Result { + let decryptor = Aes256CbcDec::new(key.into(), iv.into()); + let decrypted = decryptor + .decrypt_padded_vec_mut::(data) + .map_err(decrypt_error)? + .to_owned(); + + Ok(decrypted.into()) +} + +/// Decrypt data from the Quadratic API, which prepends the IV to the data and is hex encoded. +pub fn decrypt_from_api(key: &str, data: &str) -> Result { + let key = hex::decode(key).map_err(decrypt_error)?; + let key = key.try_into().map_err(decrypt_error)?; + let parts = data.split(":").collect::>(); + let decoded_iv = hex::decode(parts[0]).map_err(decrypt_error)?; + let iv = decoded_iv.as_slice().try_into().map_err(decrypt_error)?; + let decoded_data = hex::decode(parts[1]).map_err(decrypt_error)?; + let data = decoded_data.as_slice(); + let decrypted = decrypt(&key, iv, data)?; + let decrypted_string = String::from_utf8(decrypted.to_vec()).map_err(decrypt_error)?; + + Ok(decrypted_string) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn encrypt_and_decrypt_aes_cbc() { + let key = [0x42; 32]; + let iv = [0x24; 16]; + let text = b"Hello, world!"; + + let encrypted = encrypt(&key, &iv, text).unwrap(); + let decrypted = decrypt(&key, &iv, &encrypted).unwrap(); + + assert_eq!(text, decrypted.as_ref()); + + let api_data = format!("{}:{}", hex::encode(iv), hex::encode(encrypted)); + let decrypted = decrypt_from_api(&hex::encode(key), &api_data).unwrap(); + + assert_eq!(text, decrypted.as_bytes()); + } +} diff --git a/quadratic-rust-shared/src/crypto/error.rs b/quadratic-rust-shared/src/crypto/error.rs new file mode 100644 index 0000000000..1f6fdaf22d --- /dev/null +++ b/quadratic-rust-shared/src/crypto/error.rs @@ -0,0 +1,8 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Crypto { + #[error("Error decoding {0}")] + AesCbcDecode(String), +} diff --git a/quadratic-rust-shared/src/crypto/mod.rs b/quadratic-rust-shared/src/crypto/mod.rs new file mode 100644 index 0000000000..b0681b7703 --- /dev/null +++ b/quadratic-rust-shared/src/crypto/mod.rs @@ -0,0 +1,2 @@ +pub mod aes_cbc; +pub mod error; diff --git a/quadratic-rust-shared/src/error.rs b/quadratic-rust-shared/src/error.rs index 4dc197acaf..626bc32150 100644 --- a/quadratic-rust-shared/src/error.rs +++ b/quadratic-rust-shared/src/error.rs @@ -8,40 +8,14 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; -pub type Result = std::result::Result; - -#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub enum Arrow { - #[error("Arrow error: {0}")] - External(String), -} - -#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub enum Auth { - #[error("JWT error: {0}")] - Jwt(String), -} - -#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub enum Aws { - #[error("Error communicating with AWS: {0}")] - S3(String), -} +use crate::arrow::error::Arrow; +use crate::auth::error::Auth; +use crate::aws::error::Aws; +use crate::crypto::error::Crypto; +use crate::sql::error::Sql; +use crate::storage::error::Storage; -#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] -pub enum Sql { - #[error("Error connecting to database: {0}")] - Connect(String), - - #[error("Error converting results to Parquet: {0}")] - ParquetConversion(String), - - #[error("Error executing query: {0}")] - Query(String), - - #[error("Error creating schema: {0}")] - Schema(String), -} +pub type Result = std::result::Result; #[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] pub enum SharedError { @@ -54,6 +28,8 @@ pub enum SharedError { #[error("Error communicating with AWS: {0}")] Aws(Aws), + #[error("Error with Crypto: {0}")] + Crypto(Crypto), #[error("{0}")] Generic(String), @@ -72,10 +48,24 @@ pub enum SharedError { #[error("Error with SQL connector: {0}")] Sql(Sql), + #[error("Error with Storage: {0}")] + Storage(Storage), + #[error("Error with Uuid: {0}")] Uuid(String), } +pub fn clean_errors(error: impl ToString) -> String { + let mut cleaned = error.to_string(); + let remove = vec!["error returned from database: "]; + + for r in remove { + cleaned = format!("{:?}", cleaned).replace(r, ""); + } + + cleaned +} + impl From for SharedError { fn from(error: redis::RedisError) -> Self { SharedError::PubSub(error.to_string()) diff --git a/quadratic-rust-shared/src/lib.rs b/quadratic-rust-shared/src/lib.rs index 259e209d29..d2b1aecb4d 100644 --- a/quadratic-rust-shared/src/lib.rs +++ b/quadratic-rust-shared/src/lib.rs @@ -1,12 +1,14 @@ pub mod arrow; pub mod auth; pub mod aws; +pub mod crypto; pub mod environment; pub mod error; pub mod parquet; pub mod pubsub; pub mod quadratic_api; pub mod sql; +pub mod storage; pub mod utils; #[cfg(any(test, feature = "test"))] diff --git a/quadratic-rust-shared/src/sql/error.rs b/quadratic-rust-shared/src/sql/error.rs new file mode 100644 index 0000000000..ec9974c3ed --- /dev/null +++ b/quadratic-rust-shared/src/sql/error.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Sql { + #[error("Error connecting to database: {0}")] + Connect(String), + + #[error("Error converting results to Parquet: {0}")] + ParquetConversion(String), + + #[error("Error executing query: {0}")] + Query(String), + + #[error("Error creating schema: {0}")] + Schema(String), +} diff --git a/quadratic-rust-shared/src/sql/mod.rs b/quadratic-rust-shared/src/sql/mod.rs index f4367962db..1ab3f21cdf 100644 --- a/quadratic-rust-shared/src/sql/mod.rs +++ b/quadratic-rust-shared/src/sql/mod.rs @@ -16,6 +16,7 @@ use self::{ postgres_connection::PostgresConnection, }; +pub mod error; pub mod mssql_connection; pub mod mysql_connection; pub mod postgres_connection; diff --git a/quadratic-rust-shared/src/sql/mssql_connection.rs b/quadratic-rust-shared/src/sql/mssql_connection.rs index 8ef90d4813..bb2a9330cd 100644 --- a/quadratic-rust-shared/src/sql/mssql_connection.rs +++ b/quadratic-rust-shared/src/sql/mssql_connection.rs @@ -17,7 +17,8 @@ use tokio_util::compat::{Compat, TokioAsyncWriteCompatExt}; use uuid::Uuid; use crate::arrow::arrow_type::ArrowType; -use crate::error::{Result, SharedError, Sql}; +use crate::error::{Result, SharedError}; +use crate::sql::error::Sql as SqlError; use crate::sql::schema::{DatabaseSchema, SchemaColumn, SchemaTable}; use crate::sql::Connection; @@ -52,13 +53,13 @@ impl MsSqlConnection { let mut row_stream = client .query(sql, &[]) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))? + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))? .into_row_stream(); while let Some(row_result) = row_stream.next().await { match row_result { Ok(row) => rows.push(row), - Err(e) => return Err(SharedError::Sql(Sql::Query(e.to_string()))), + Err(e) => return Err(SharedError::Sql(SqlError::Query(e.to_string()))), } } @@ -91,7 +92,9 @@ impl Connection for MsSqlConnection { if let Some(port) = &self.port { config.port(port.parse::().map_err(|_| { - SharedError::Sql(Sql::Connect("Could not parse port into a number".into())) + SharedError::Sql(SqlError::Connect( + "Could not parse port into a number".into(), + )) })?); } @@ -104,16 +107,17 @@ impl Connection for MsSqlConnection { config.trust_cert(); - let tcp = TcpStream::connect(config.get_addr()) - .await - .map_err(|e| SharedError::Sql(Sql::Connect(format!("Failed to connect: {}", e))))?; - tcp.set_nodelay(true) - .map_err(|e| SharedError::Sql(Sql::Connect(format!("Failed to set nodelay: {}", e))))?; + let tcp = TcpStream::connect(config.get_addr()).await.map_err(|e| { + SharedError::Sql(SqlError::Connect(format!("Failed to connect: {}", e))) + })?; + tcp.set_nodelay(true).map_err(|e| { + SharedError::Sql(SqlError::Connect(format!("Failed to set nodelay: {}", e))) + })?; let client = Client::connect(config, tcp.compat_write()) .await .map_err(|e| { - SharedError::Sql(Sql::Connect(format!("Failed to create client: {}", e))) + SharedError::Sql(SqlError::Connect(format!("Failed to create client: {}", e))) })?; Ok(client) @@ -134,7 +138,7 @@ impl Connection for MsSqlConnection { let mut row_stream = client .query(sql, &[]) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))? + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))? .into_row_stream(); while let Some(row_result) = row_stream.next().await { @@ -149,7 +153,7 @@ impl Connection for MsSqlConnection { rows.push(row); } - Err(e) => return Err(SharedError::Sql(Sql::Query(e.to_string()))), + Err(e) => return Err(SharedError::Sql(SqlError::Query(e.to_string()))), } } } else { @@ -185,13 +189,13 @@ ORDER BY let row_stream = client .query(sql, &[]) .await - .map_err(|e| SharedError::Sql(Sql::Schema(e.to_string())))? + .map_err(|e| SharedError::Sql(SqlError::Schema(e.to_string())))? .into_row_stream(); let rows: Vec = row_stream .try_collect() .await - .map_err(|e| SharedError::Sql(Sql::Schema(e.to_string())))?; + .map_err(|e| SharedError::Sql(SqlError::Schema(e.to_string())))?; let mut schema = DatabaseSchema { database: self.database.to_owned(), diff --git a/quadratic-rust-shared/src/sql/mysql_connection.rs b/quadratic-rust-shared/src/sql/mysql_connection.rs index 30c3914ba5..fd6cf627ae 100644 --- a/quadratic-rust-shared/src/sql/mysql_connection.rs +++ b/quadratic-rust-shared/src/sql/mysql_connection.rs @@ -15,11 +15,11 @@ use sqlx::{ Column, ConnectOptions, MySqlConnection as SqlxMySqlConnection, Row, TypeInfo, }; -use crate::arrow::arrow_type::ArrowType; use crate::convert_mysql_type; -use crate::error::{Result, SharedError, Sql}; +use crate::error::{Result, SharedError}; +use crate::sql::error::Sql as SqlError; use crate::sql::schema::{DatabaseSchema, SchemaColumn, SchemaTable}; -use crate::sql::Connection; +use crate::sql::{ArrowType, Connection}; #[derive(Debug, Serialize, Deserialize)] pub struct MySqlConnection { @@ -51,7 +51,7 @@ impl MySqlConnection { let rows = sqlx::query(sql) .fetch_all(pool) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; Ok(rows) } @@ -90,14 +90,15 @@ impl Connection for MySqlConnection { if let Some(ref port) = self.port { options = options.port(port.parse::().map_err(|_| { - SharedError::Sql(Sql::Connect("Could not parse port into a number".into())) + SharedError::Sql(SqlError::Connect( + "Could not parse port into a number".into(), + )) })?); } - let pool = options - .connect() - .await - .map_err(|e| SharedError::Sql(Sql::Connect(format!("{:?}: {e}", self.database))))?; + let pool = options.connect().await.map_err(|e| { + SharedError::Sql(SqlError::Connect(format!("{:?}: {e}", self.database))) + })?; Ok(pool) } @@ -116,7 +117,7 @@ impl Connection for MySqlConnection { let mut stream = sqlx::query(sql).fetch(pool); while let Some(row) = stream.next().await { - let row = row.map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + let row = row.map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; bytes += row.len() as u64; if bytes > max_bytes { diff --git a/quadratic-rust-shared/src/sql/postgres_connection.rs b/quadratic-rust-shared/src/sql/postgres_connection.rs index 6c4d164037..285a8a97d7 100644 --- a/quadratic-rust-shared/src/sql/postgres_connection.rs +++ b/quadratic-rust-shared/src/sql/postgres_connection.rs @@ -15,11 +15,11 @@ use sqlx::{ Column, ConnectOptions, PgConnection, Row, TypeInfo, }; -use crate::arrow::arrow_type::ArrowType; use crate::convert_pg_type; -use crate::error::{Result, SharedError, Sql}; +use crate::error::{Result, SharedError}; +use crate::sql::error::Sql as SqlError; use crate::sql::schema::{DatabaseSchema, SchemaColumn, SchemaTable}; -use crate::sql::Connection; +use crate::sql::{ArrowType, Connection}; #[derive(Debug, Serialize, Deserialize)] pub struct PostgresConnection { @@ -51,7 +51,7 @@ impl PostgresConnection { let rows = sqlx::query(sql) .fetch_all(pool) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; Ok(rows) } @@ -90,14 +90,15 @@ impl Connection for PostgresConnection { if let Some(ref port) = self.port { options = options.port(port.parse::().map_err(|_| { - SharedError::Sql(Sql::Connect("Could not parse port into a number".into())) + SharedError::Sql(SqlError::Connect( + "Could not parse port into a number".into(), + )) })?); } - let pool = options - .connect() - .await - .map_err(|e| SharedError::Sql(Sql::Connect(format!("{:?}: {e}", self.database))))?; + let pool = options.connect().await.map_err(|e| { + SharedError::Sql(SqlError::Connect(format!("{:?}: {e}", self.database))) + })?; Ok(pool) } @@ -116,7 +117,7 @@ impl Connection for PostgresConnection { let mut stream = sqlx::query(sql).fetch(pool); while let Some(row) = stream.next().await { - let row = row.map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + let row = row.map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; bytes += row.len() as u64; if bytes > max_bytes { @@ -131,7 +132,7 @@ impl Connection for PostgresConnection { rows = sqlx::query(sql) .fetch_all(pool) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; } let (bytes, num_records) = Self::to_parquet(rows)?; diff --git a/quadratic-rust-shared/src/sql/snowflake_connection.rs b/quadratic-rust-shared/src/sql/snowflake_connection.rs index 23f17150eb..5ab4b85fa0 100644 --- a/quadratic-rust-shared/src/sql/snowflake_connection.rs +++ b/quadratic-rust-shared/src/sql/snowflake_connection.rs @@ -11,7 +11,8 @@ use std::collections::BTreeMap; use std::sync::Arc; use crate::arrow::arrow_type::ArrowType; -use crate::error::{Result, SharedError, Sql}; +use crate::error::{Result, SharedError}; +use crate::sql::error::Sql as SqlError; use crate::sql::schema::{DatabaseSchema, SchemaColumn, SchemaTable}; use crate::sql::Connection; use crate::utils::array::transpose; @@ -86,7 +87,9 @@ impl Connection for SnowflakeConnection { &self.password, ) .map_err(|e| { - SharedError::Sql(Sql::Connect(format!("Error connecting to snowflake: {e}"))) + SharedError::Sql(SqlError::Connect(format!( + "Error connecting to snowflake: {e}" + ))) })?; Ok(client) @@ -98,7 +101,7 @@ impl Connection for SnowflakeConnection { sql: &str, max_bytes: Option, ) -> Result<(Bytes, bool, usize)> { - let query_error = |e: String| SharedError::Sql(Sql::Query(e)); + let query_error = |e: String| SharedError::Sql(SqlError::Query(e)); #[cfg(any(test, feature = "test"))] let (mut _client, _recording) = tests::get_mocked(&self, "snowflake-connection").await; @@ -152,7 +155,7 @@ impl Connection for SnowflakeConnection { } } - Err(SharedError::Sql(Sql::Query( + Err(SharedError::Sql(SqlError::Query( "Could not convert to Arrow".to_string(), ))) } @@ -196,7 +199,7 @@ impl Connection for SnowflakeConnection { let row_stream = _client .exec(&sql) .await - .map_err(|e| SharedError::Sql(Sql::Query(e.to_string())))?; + .map_err(|e| SharedError::Sql(SqlError::Query(e.to_string())))?; #[cfg(all(any(test, feature = "test"), feature = "record-request-mock"))] record_stop(scenario, _recording).await; diff --git a/quadratic-rust-shared/src/storage/error.rs b/quadratic-rust-shared/src/storage/error.rs new file mode 100644 index 0000000000..dffd5ab46b --- /dev/null +++ b/quadratic-rust-shared/src/storage/error.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +#[derive(Error, Debug, Serialize, Deserialize, PartialEq, Clone)] +pub enum Storage { + #[error("Error creating directory {0}: {1}")] + CreateDirectory(String, String), + + #[error("Invalid key: {0}")] + InvalidKey(String), + + #[error("Error reading key {0}: {1}")] + Read(String, String), + + #[error("Error writing key {0}: {1}")] + Write(String, String), +} diff --git a/quadratic-rust-shared/src/storage/file_system.rs b/quadratic-rust-shared/src/storage/file_system.rs new file mode 100644 index 0000000000..e058f7314b --- /dev/null +++ b/quadratic-rust-shared/src/storage/file_system.rs @@ -0,0 +1,135 @@ +use async_trait::async_trait; +use bytes::Bytes; +use std::path::{Path, PathBuf}; +use tokio::fs::{create_dir_all, File}; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; + +use super::Storage; +use crate::error::Result; +use crate::storage::error::Storage as StorageError; +use crate::SharedError; + +#[derive(Debug, Clone)] +pub struct FileSystemConfig { + pub path: String, + pub encryption_keys: Vec, +} + +#[derive(Debug, Clone)] +pub struct FileSystem { + pub config: FileSystemConfig, +} + +#[async_trait] +impl Storage for FileSystem { + type Config = FileSystemConfig; + + /// Read the file from the file system and return the bytes. + async fn read(&self, key: &str) -> Result { + let file_path = self.full_path(key, false).await?.0; + let mut bytes = vec![]; + let mut file = File::open(file_path) + .await + .map_err(|e| Self::read_error(key, &e))?; + + file.read_to_end(&mut bytes) + .await + .map_err(|e| Self::read_error(key, &e))?; + + Ok(bytes.into()) + } + + /// Write the bytes to the file system. + async fn write<'a>(&self, key: &'a str, data: &'a Bytes) -> Result<()> { + let file_path = self.full_path(key, true).await?.0; + let mut file = File::create(file_path) + .await + .map_err(|e| Self::write_error(key, &e))?; + file.write_all(data) + .await + .map_err(|e| Self::write_error(key, &e))?; + + Ok(()) + } + + /// Return the path to the file system. + fn path(&self) -> &str { + &self.config.path + } + + /// Return the configuration + fn config(&self) -> Self::Config { + self.config.clone() + } +} + +impl FileSystem { + pub fn new(config: FileSystemConfig) -> Self { + Self { config } + } + + /// Return the full path to the file and the directory. + pub async fn full_path(&self, key: &str, create_dir: bool) -> Result<(PathBuf, PathBuf)> { + let FileSystemConfig { path, .. } = &self.config; + let parts = key.split('-').collect::>(); + let invalid_key = || SharedError::Storage(StorageError::InvalidKey(key.to_owned())); + + // expecting uuid-sequence_number.grid + // e.g. aad29798-0bf9-4b25-ab45-e22efd37d446-0.grid + if parts.len() < 5 { + return Err(invalid_key()); + } + + let uuid = &parts[0..parts.len() - 1].join("-"); + let file_name = parts.last().ok_or_else(invalid_key)?; + let dir = Path::new(path).join(uuid); + let full_path = dir.join(file_name); + + if create_dir { + create_dir_all(dir.to_owned()).await.map_err(|e| { + SharedError::Storage(StorageError::CreateDirectory( + dir.to_string_lossy().into_owned(), + e.to_string(), + )) + })?; + } + + Ok((full_path, dir)) + } +} + +#[cfg(test)] +mod tests { + use tokio::fs::{remove_dir, remove_file}; + use uuid::Uuid; + + use super::*; + use std::env; + + fn config() -> FileSystemConfig { + FileSystemConfig { + path: env::temp_dir().to_str().unwrap().to_string(), + encryption_keys: vec![], + } + } + + #[tokio::test] + async fn file_system_write_and_read() { + let config = config(); + let storage = FileSystem { config }; + let file_name = Uuid::new_v4().to_string(); + let seqence_number = 0; + let key = &format!("{}-{}.grid", file_name, seqence_number); + let data = &Bytes::from("Hello, world!"); + + storage.write(key, data).await.unwrap(); + let read_data = storage.read(key).await.unwrap(); + + // cleanup + let (full_path, dir) = storage.full_path(key, false).await.unwrap(); + remove_file(full_path).await.unwrap(); + remove_dir(dir).await.unwrap(); + + assert_eq!(data, &read_data); + } +} diff --git a/quadratic-rust-shared/src/storage/mod.rs b/quadratic-rust-shared/src/storage/mod.rs new file mode 100644 index 0000000000..4a8660db62 --- /dev/null +++ b/quadratic-rust-shared/src/storage/mod.rs @@ -0,0 +1,74 @@ +use async_trait::async_trait; +use bytes::Bytes; +use file_system::FileSystemConfig; +use s3::S3Config; + +use crate::{error::Result, storage::error::Storage as StorageError, SharedError}; + +pub mod error; +pub mod file_system; +pub mod s3; + +#[derive(Debug)] +pub enum StorageConfig { + S3(S3Config), + FileSystem(FileSystemConfig), +} + +#[derive(Debug)] +pub enum StorageContainer { + S3(s3::S3), + FileSystem(file_system::FileSystem), +} + +#[async_trait] +pub trait Storage { + type Config; + + async fn read(&self, key: &str) -> Result; + async fn write<'a>(&self, key: &'a str, data: &'a Bytes) -> Result<()>; + fn path(&self) -> &str; + fn config(&self) -> Self::Config; + + fn read_error(key: &str, e: impl ToString) -> SharedError { + SharedError::Storage(StorageError::Read(key.into(), e.to_string())) + } + + fn write_error(key: &str, e: impl ToString) -> SharedError { + SharedError::Storage(StorageError::Write(key.into(), e.to_string())) + } +} + +// TODO(ddimaria): this is a temp hack to get around some trait issues, do something better +#[async_trait] +impl Storage for StorageContainer { + type Config = StorageConfig; + + async fn read(&self, key: &str) -> Result { + match self { + Self::S3(s3) => s3.read(key).await, + Self::FileSystem(fs) => fs.read(key).await, + } + } + + async fn write<'a>(&self, key: &'a str, data: &'a Bytes) -> Result<()> { + match self { + Self::S3(s3) => s3.write(key, data).await, + Self::FileSystem(fs) => fs.write(key, data).await, + } + } + + fn path(&self) -> &str { + match self { + Self::S3(s3) => s3.path(), + Self::FileSystem(fs) => fs.path(), + } + } + + fn config(&self) -> StorageConfig { + match self { + Self::S3(s3) => StorageConfig::S3(s3.config()), + Self::FileSystem(fs) => StorageConfig::FileSystem(fs.config()), + } + } +} diff --git a/quadratic-rust-shared/src/storage/s3.rs b/quadratic-rust-shared/src/storage/s3.rs new file mode 100644 index 0000000000..f9fcdd70c2 --- /dev/null +++ b/quadratic-rust-shared/src/storage/s3.rs @@ -0,0 +1,75 @@ +use async_trait::async_trait; +use aws_sdk_s3::Client; +use bytes::Bytes; + +use super::Storage; +use crate::{ + aws::s3::{download_object, upload_object}, + error::Result, +}; + +#[derive(Debug, Clone)] +pub struct S3Config { + pub client: Client, + pub bucket: String, +} + +#[derive(Debug, Clone)] +pub struct S3 { + pub config: S3Config, +} + +#[async_trait] +impl Storage for S3 { + type Config = S3Config; + + /// Read the file from the S3 bucket and return the bytes. + async fn read(&self, key: &str) -> Result { + let S3Config { client, bucket } = &self.config; + + let file = download_object(client, bucket, key) + .await + .map_err(|e| Self::read_error(key, &e))?; + + let bytes = file + .body + .collect() + .await + .map_err(|e| Self::read_error(key, &e))? + .into_bytes(); + + Ok(bytes) + } + + /// Write the bytes to the S3 bucket. + async fn write<'a>(&self, key: &'a str, data: &'a Bytes) -> Result<()> { + let S3Config { client, bucket } = &self.config; + + upload_object(client, bucket, key, data) + .await + .map_err(|e| Self::write_error(key, &e))?; + + Ok(()) + } + + /// Return the S3 bucket. + fn path(&self) -> &str { + &self.config.bucket + } + + /// Return the configuration + fn config(&self) -> Self::Config { + self.config.clone() + } +} + +impl S3 { + pub fn new(config: S3Config) -> Self { + Self { config } + } +} + +#[cfg(test)] +mod tests { + // TODO(ddimaria): add tests once we have S3 mocks in place +} diff --git a/quadratic-shared/typesAndSchemas.ts b/quadratic-shared/typesAndSchemas.ts index a51b6c1794..37aa5c1ceb 100644 --- a/quadratic-shared/typesAndSchemas.ts +++ b/quadratic-shared/typesAndSchemas.ts @@ -104,6 +104,13 @@ const TeamUserMakingRequestSchema = z.object({ export const TeamClientDataKvSchema = z.record(z.any()); +export const LicenseSchema = z.object({ + limits: z.object({ + seats: z.number(), + }), + status: z.enum(['active', 'exceeded', 'revoked']), +}); + // Zod schemas for API endpoints export const ApiSchemas = { /** @@ -154,6 +161,7 @@ export const ApiSchemas = { teamPermissions: z.array(TeamPermissionSchema).optional(), teamRole: UserTeamRoleSchema.optional(), }), + license: LicenseSchema, }), '/v0/files/:uuid.DELETE.response': z.object({ message: z.string(), @@ -325,6 +333,7 @@ export const ApiSchemas = { ), users: z.array(TeamUserSchema), invites: z.array(z.object({ email: emailSchema, role: UserTeamRoleSchema, id: z.number() })), + license: LicenseSchema, connections: ConnectionListSchema, clientDataKv: TeamClientDataKvSchema, }),