diff --git a/.env.local.example b/.env.local.example index d081a52..d4755a5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -134,3 +134,19 @@ NEXT_PUBLIC_X_PIXEL_ID=r9lkr # Format: Long OAuth 2.0 Bearer token (e.g., AAAAAAAAAAAAAAAAAAAAAMLheAAAAAAA0%2BuSeid...) # IMPORTANT: This is different from your main X API token - it's specifically for Conversions API X_CONVERSIONS_API_TOKEN=your_x_api_token_here + +# Native store billing (server-only). See docs/mobile-billing.md before enabling. +MOBILE_BILLING_ENABLED=false +MOBILE_BILLING_TOKEN_KEY= +APPLE_IAP_ISSUER_ID= +APPLE_IAP_KEY_ID= +APPLE_IAP_PRIVATE_KEY= +APPLE_APP_ID= +APPLE_STORE_ENVIRONMENT=Production +GOOGLE_PLAY_SERVICE_ACCOUNT_JSON= +GOOGLE_PLAY_ALLOW_TEST_PURCHASES=false +GOOGLE_RTDN_AUDIENCE= +GOOGLE_RTDN_SERVICE_ACCOUNT_EMAIL= + +# Ciphertext-only mobile sync: run npm run vault:migrate before enabling. +VAULT_SYNC_ENABLED=false diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..2bbff47 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +apps/android/gradlew.bat text eol=crlf diff --git a/.github/workflows/native-clients.yml b/.github/workflows/native-clients.yml new file mode 100644 index 0000000..f7895aa --- /dev/null +++ b/.github/workflows/native-clients.yml @@ -0,0 +1,81 @@ +name: Native clients + +on: + pull_request: + workflow_dispatch: + +jobs: + android: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: 21 + - uses: android-actions/setup-android@v3 + with: + packages: platform-tools + - run: sdkmanager 'platforms;android-35' 'build-tools;35.0.0' 'ndk;27.2.12479018' + - run: python3 scripts/prepare-public-sample.py + - run: node scripts/build-native.mjs android arm64-v8a + - run: apps/android/gradlew -p apps/android assembleDebug testDebugUnitTest lintDebug --no-daemon + - uses: actions/upload-artifact@v7 + with: + name: explorer-android-debug + path: apps/android/app/build/outputs/apk/debug/app-debug.apk + - run: node scripts/build-native.mjs android x86_64 + - name: Enable emulator KVM + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + - uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 35 + arch: x86_64 + script: apps/android/gradlew -p apps/android -PexplorerAbis=x86_64 connectedDebugAndroidTest --no-daemon + + - uses: actions/upload-artifact@v7 + if: always() + with: + name: explorer-android-test-results + path: | + apps/android/app/build/reports/androidTests + apps/android/app/build/outputs/androidTest-results + + ios: + runs-on: macos-15 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: 24 + - run: brew install xcodegen + - run: python3 scripts/prepare-public-sample.py + - run: node scripts/build-native.mjs ios simulator + - name: Run native binding tests in iOS Simulator + shell: bash + run: | + simulator_id=$(xcrun simctl list devices available -j | python3 -c 'import json,sys; d=json.load(sys.stdin); print(next(v["udid"] for devices in d["devices"].values() for v in devices if v["name"].startswith("iPhone")))') + xcodebuild -resultBundlePath apps/ios/ExplorerTests.xcresult -project apps/ios/Explorer.xcodeproj -scheme Explorer -destination "platform=iOS Simulator,id=$simulator_id" -derivedDataPath apps/ios/build test CODE_SIGNING_ALLOWED=NO + + - name: Export iOS visual snapshots + if: always() + run: | + if [ -d apps/ios/ExplorerTests.xcresult ]; then + xcrun xcresulttool export attachments --path apps/ios/ExplorerTests.xcresult --output-path apps/ios/test-attachments + fi + - uses: actions/upload-artifact@v7 + if: always() + with: + name: explorer-ios-visual-snapshots + path: apps/ios/test-attachments + - uses: actions/upload-artifact@v7 + if: always() + with: + name: explorer-ios-test-results + path: apps/ios/ExplorerTests.xcresult diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml index 0aa3caa..b8c327d 100644 --- a/.github/workflows/pr-build.yml +++ b/.github/workflows/pr-build.yml @@ -10,6 +10,19 @@ jobs: build: runs-on: ubuntu-latest + services: + billing-postgres: + image: postgres:17 + env: + POSTGRES_USER: explorer + POSTGRES_PASSWORD: billing-test-only + POSTGRES_DB: billing_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U explorer -d billing_test" + --health-interval 5s --health-timeout 5s --health-retries 10 + strategy: matrix: node-version: [24.x] @@ -27,6 +40,26 @@ jobs: - name: Install dependencies run: npm ci + - name: Verify store billing and atomic purchase ledger + run: | + node scripts/migrate-mobile-billing.mjs + npm run test:billing + npm run vault:migrate + npm run test:vault-sync + env: + POSTGRES_DB: postgresql://explorer:billing-test-only@127.0.0.1:5432/billing_test + BILLING_DB_TEST: 'true' + VAULT_DB_TEST: 'true' + + - name: Install shared core toolchain + run: rustup show active-toolchain + + - name: Test Rust and WASM contracts + run: | + npm run core:test + npm test + npm run test:run-all + - name: Run linter run: npm run lint @@ -38,6 +71,36 @@ jobs: env: NODE_ENV: production + - name: Prepare full public 23andMe sample and browser + run: | + npm run test:public-sample:prepare + npx playwright install --with-deps chromium + + - name: Verify public sample import, scores, export and portable vault in Chromium + run: | + node node_modules/next/dist/bin/next start > /tmp/explorer-public-sample-server.log 2>&1 & + app_pid=$! + trap 'kill "$app_pid"' EXIT + node --input-type=module <<'JS' + let ready=false; + for(let attempt=0;attempt<60;attempt++) { + try { if((await fetch('http://127.0.0.1:3000')).ok){ready=true;break;} } catch {} + await new Promise(resolve=>setTimeout(resolve,500)); + } + if(!ready)throw new Error('Production server did not start'); + JS + npm run test:public-sample:web + + - name: Upload public sample browser evidence + uses: actions/upload-artifact@v7 + with: + name: public-sample-browser-evidence + include-hidden-files: true + path: | + .test-data/public-sample/*.png + .test-data/public-sample/web-results.tsv + retention-days: 7 + - name: Upload build artifacts if: matrix.node-version == '24.x' uses: actions/upload-artifact@v7 diff --git a/.gitignore b/.gitignore index b5847ff..4da964d 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,25 @@ yarn-error.log* /CLAUDE.md /scripts/ga-performance.env /.junie/ + +# Shared Rust and generated native bindings +/target/ +/lib/generated/ +/apps/android/.gradle/ +/apps/android/local.properties +/apps/android/**/build/ +/apps/android/app/src/main/jniLibs/ +/apps/android/app/src/main/java/com/monadicdna/explorer/core/ +/apps/ios/Generated/ +/apps/ios/build/ +/apps/ios/*.xcodeproj/ +/apps/ios/*.xcworkspace/ +/.test-data/ +/apps/ios/TestData/ +/apps/android/app/src/androidTest/assets/public-sample/ + +# Kotlin compiler workspace cache +apps/android/.kotlin/ + +apps/ios/*.xcresult/ +apps/ios/test-attachments/ diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..c1bbe46 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1316 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", + "zeroize", +] + +[[package]] +name = "askama" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7" +dependencies = [ + "askama_derive", + "itoa", + "percent-encoding", + "serde", + "serde_json", +] + +[[package]] +name = "askama_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac" +dependencies = [ + "askama_parser", + "basic-toml", + "memchr", + "proc-macro2", + "quote", + "rustc-hash", + "serde", + "serde_derive", + "syn 2.0.119", +] + +[[package]] +name = "askama_parser" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f" +dependencies = [ + "memchr", + "serde", + "serde_derive", + "winnow", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "basic-toml" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "camino" +version = "1.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbad30e4b4c14a39e3cc8aed085a12a327257c316619c93581e017bc52be591" +dependencies = [ + "serde_core", +] + +[[package]] +name = "cargo-platform" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[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 = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ba106e844045ddc02c20768ac4b0b7cb45f2cc2ecbd833ac9f5dacb183c2055" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622f3fc73690be383c7214310406f28a90e6edeadc3cea882f9d71e495b9711a" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc74980687109a3b14c72fd458107bf0baa1da1a1a805e178d15501ba9b86d9d" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest", + "ff", + "generic-array", + "group", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "explorer-bindgen" +version = "1.0.0" +dependencies = [ + "uniffi", +] + +[[package]] +name = "explorer-core" +version = "1.0.0" +dependencies = [ + "aes-gcm", + "argon2", + "base64", + "flate2", + "hmac", + "k256", + "md-5", + "pbkdf2", + "rayon", + "regex", + "serde", + "serde_json", + "sha2", + "uniffi", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "glob" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b" + +[[package]] +name = "goblin" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "k256" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "scroll" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "subtle", + "zeroize", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "smawk" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "textwrap" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ecfad6c3abc80a577f2b91c1e412ee57e7a060d430b553c1b0c940974ebcd49" +dependencies = [ + "smawk", + "unicode-width", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "uniffi" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f" +dependencies = [ + "anyhow", + "camino", + "cargo_metadata", + "clap", + "uniffi_bindgen", + "uniffi_core", + "uniffi_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_bindgen" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e" +dependencies = [ + "anyhow", + "askama", + "camino", + "cargo_metadata", + "fs-err", + "glob", + "goblin", + "heck", + "indexmap", + "once_cell", + "serde", + "tempfile", + "textwrap", + "toml", + "uniffi_internal_macros", + "uniffi_meta", + "uniffi_pipeline", + "uniffi_udl", +] + +[[package]] +name = "uniffi_core" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98" +dependencies = [ + "anyhow", + "bytes", + "once_cell", + "static_assertions", +] + +[[package]] +name = "uniffi_internal_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933" +dependencies = [ + "anyhow", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "uniffi_macros" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152" +dependencies = [ + "camino", + "fs-err", + "once_cell", + "proc-macro2", + "quote", + "serde", + "syn 2.0.119", + "toml", + "uniffi_meta", +] + +[[package]] +name = "uniffi_meta" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff" +dependencies = [ + "anyhow", + "siphasher", + "uniffi_internal_macros", + "uniffi_pipeline", +] + +[[package]] +name = "uniffi_pipeline" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "tempfile", + "uniffi_internal_macros", +] + +[[package]] +name = "uniffi_udl" +version = "0.29.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716" +dependencies = [ + "anyhow", + "textwrap", + "uniffi_meta", + "weedle2", +] + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "weedle2" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e" +dependencies = [ + "nom", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "serde", + "zeroize", +] + +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..376e271 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[workspace] +members = ["crates/explorer-core", "tools/bindgen"] +resolver = "2" + +[workspace.package] +edition = "2021" +license-file = "LICENSE" +version = "1.0.0" + +[profile.release] +lto = true +codegen-units = 1 +opt-level = "s" +strip = "debuginfo" + +# Matching is a hot path on every client; prioritize throughput over minimal code size. +[profile.release.package.explorer-core] +opt-level = 3 diff --git a/README.md b/README.md index be53c57..2729241 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,9 @@ Match your DNA data against an open ended catalogue of DNA traits with private L ### Running the Monadic DNA Explorer -The repository includes a Next.js single-page application for exploring GWAS studies. +The repository contains a Next.js web app, a native Kotlin/Compose Android app, a native SwiftUI iOS app, and a shared Rust client core. See [cross-platform development](docs/clients.md) for architecture, mobile builds, and validation. + +Install Rust with rustup before running the web app; `rust-toolchain.toml` selects the toolchain and WASM target. The npm lifecycle scripts build the shared core automatically. ```bash # Set PostgreSQL connection string @@ -444,3 +446,5 @@ See [LICENSE](LICENSE) for full dual license details and [LICENSE-COMMERCIAL.md] Cerezo M, Sollis E, Ji Y, Lewis E, Abid A, Bircan KO, Hall P, Hayhurst J, John S, Mosaku A, Ramachandran S, Foreman A, Ibrahim A, McLaughlin J, Pendlington Z, Stefancsik R, Lambert SA, McMahon A, Morales J, Keane T, Inouye M, Parkinson H, Harris LW. doi.org/10.1093/nar/gkae1070 Nucleic Acids Research, Volume 53, Issue D1, 6 January 2025, Pages D998–D1005 + +See [secure persistence, recovery and encrypted sync](docs/secure-data-vault.md) for device key protection, cross-platform transfers and backend setup. diff --git a/app/api/check-subscription/route.ts b/app/api/check-subscription/route.ts index 63934b3..a302b89 100644 --- a/app/api/check-subscription/route.ts +++ b/app/api/check-subscription/route.ts @@ -3,6 +3,8 @@ import '@/lib/fetch-polyfill'; import { NextRequest, NextResponse } from 'next/server'; import { checkCombinedSubscription } from '@/lib/subscription-manager'; +import { mobileBillingEnabled, mobileEntitlements } from '@/lib/store-billing/ledger'; +import { verifyWalletAuth } from '@/lib/dynamic-auth'; export async function POST(request: NextRequest) { try { @@ -23,6 +25,19 @@ export async function POST(request: NextRequest) { ); } + if (mobileBillingEnabled() && request.headers.get('authorization')) { + const auth = await verifyWalletAuth(request.headers.get('authorization'), walletAddress); + if (!auth.ok) return NextResponse.json({ error:auth.error }, { status:auth.status || 401 }); + const mobile = await mobileEntitlements(walletAddress); + if (mobile.isActive) { + return NextResponse.json({ success:true, subscription:{ + isActive:true, expiresAt:mobile.expiresAt?.toISOString(), + daysRemaining:Math.max(0,Math.ceil((mobile.expiresAt!.getTime()-Date.now())/86400000)), + totalDaysPurchased:0, totalPaid:0, paymentCount:0, source:'app_store_or_google_play', + } }, { headers:{'Cache-Control':'no-store'} }); + } + } + // Query both blockchain (Alchemy) and Stripe payments // Combined subscription includes payments from both sources console.log('[Subscription Check] Checking combined subscription (blockchain + Stripe)'); diff --git a/app/api/mobile-billing/account/route.ts b/app/api/mobile-billing/account/route.ts new file mode 100644 index 0000000..fc80327 --- /dev/null +++ b/app/api/mobile-billing/account/route.ts @@ -0,0 +1,15 @@ +import { NextResponse } from 'next/server'; +import { billingAccount, tokenKey } from '@/lib/store-billing/ledger'; +import { authenticatedBillingWallet, billingFailure } from '@/lib/store-billing/http'; +import { assertStoreConfigured } from '@/lib/store-billing/verification'; +import { BillingError, catalog } from '@/lib/store-billing/catalog'; +export const runtime = 'nodejs'; +export async function POST(request: Request) { + try { + const body = await request.json(); + const wallet = await authenticatedBillingWallet(request,body.walletAddress); + if (body.store !== 'apple' && body.store !== 'google') throw new BillingError('Invalid store'); + tokenKey(); assertStoreConfigured(body.store); + return NextResponse.json({ accountId:await billingAccount(wallet), products:catalog },{ headers:{'Cache-Control':'no-store'} }); + } catch (error) { return billingFailure(error); } +} diff --git a/app/api/mobile-billing/apple-notifications/route.ts b/app/api/mobile-billing/apple-notifications/route.ts new file mode 100644 index 0000000..f143808 --- /dev/null +++ b/app/api/mobile-billing/apple-notifications/route.ts @@ -0,0 +1,23 @@ +import { NextResponse } from 'next/server'; +import { appleServices, verifyStorePurchase } from '@/lib/store-billing/verification'; +import { mobileBillingEnabled, recordPurchase } from '@/lib/store-billing/ledger'; +import { BillingError } from '@/lib/store-billing/catalog'; +import { billingFailure } from '@/lib/store-billing/http'; +import { getDb } from '@/lib/db'; +export const runtime = 'nodejs'; +export async function POST(request: Request) { + try { + if (!mobileBillingEnabled()) throw new BillingError('Billing is not configured',503); + const body = await request.json(); + if (typeof body.signedPayload !== 'string' || body.signedPayload.length > 100000) throw new BillingError('Invalid notification'); + const { verifier } = appleServices(); + const notification = await verifier.verifyAndDecodeNotification(body.signedPayload); + if (notification.data?.signedTransactionInfo) { + const tx = await verifier.verifyAndDecodeTransaction(notification.data.signedTransactionInfo); + if (!tx.appAccountToken || !tx.transactionId || !tx.productId) throw new BillingError('Missing purchase account'); + const owner = await getDb().query('SELECT id FROM mobile_billing_accounts WHERE id=$1',[tx.appAccountToken]); + if (owner.rowCount) await recordPurchase(await verifyStorePurchase('apple',tx.transactionId,tx.productId,tx.appAccountToken.toLowerCase())); + } + return new NextResponse(null,{status:204}); + } catch (error) { return billingFailure(error); } +} diff --git a/app/api/mobile-billing/google-notifications/route.ts b/app/api/mobile-billing/google-notifications/route.ts new file mode 100644 index 0000000..cdde5a2 --- /dev/null +++ b/app/api/mobile-billing/google-notifications/route.ts @@ -0,0 +1,50 @@ +import { NextResponse } from 'next/server'; +import { OAuth2Client } from 'google-auth-library'; +import { createHash } from 'node:crypto'; +import { readGooglePurchase, verifyStorePurchase, completeGooglePurchase } from '@/lib/store-billing/verification'; +import { mobileBillingEnabled, recordPurchase } from '@/lib/store-billing/ledger'; +import { BillingError, catalog, required } from '@/lib/store-billing/catalog'; +import { billingFailure } from '@/lib/store-billing/http'; +import { getDb } from '@/lib/db'; +export const runtime = 'nodejs'; +export async function POST(request: Request) { + try { + if (!mobileBillingEnabled()) throw new BillingError('Billing is not configured',503); + const authorization = request.headers.get('authorization'); + if (!authorization?.startsWith('Bearer ')) throw new BillingError('Authentication required',401); + const ticket = await new OAuth2Client().verifyIdToken({ idToken:authorization.slice(7), audience:required('GOOGLE_RTDN_AUDIENCE') }); + const identity = ticket.getPayload(); + if (identity?.email !== required('GOOGLE_RTDN_SERVICE_ACCOUNT_EMAIL') || identity.email_verified !== true) throw new BillingError('Invalid notification identity',403); + const body = await request.json(); + if (typeof body.message?.data !== 'string' || body.message.data.length > 100000) throw new BillingError('Invalid notification'); + const notification = JSON.parse(Buffer.from(body.message.data,'base64').toString('utf8')); + if (notification.packageName !== 'com.monadicdna.explorer') throw new BillingError('Incorrect package'); + const oneTime = notification.oneTimeProductNotification; + const subscription = notification.subscriptionNotification; + const voided = notification.voidedPurchaseNotification; + const proof = oneTime?.purchaseToken || subscription?.purchaseToken || voided?.purchaseToken; + if (typeof proof !== 'string' || !proof || proof.length > 8192) { + if (notification.testNotification) return new NextResponse(null,{status:204}); + throw new BillingError('Missing purchase token'); + } + let productId: string | undefined = oneTime?.sku; + let accountId: string | undefined; + if (voided) { + const existing = await getDb().query('SELECT product_id,account_id FROM mobile_store_purchases WHERE store=$1 AND purchase_id=$2',['google',createHash('sha256').update(proof).digest('hex')]); + productId = existing.rows[0]?.product_id; accountId = existing.rows[0]?.account_id; + if (!accountId) return new NextResponse(null,{status:204}); + } else { + const data = await readGooglePurchase(proof,productId); + productId ||= data.lineItems?.find(line => catalog.some(product => product.id===line.productId))?.productId; + accountId = oneTime ? data.obfuscatedExternalAccountId : data.externalAccountIdentifiers?.obfuscatedExternalAccountId; + } + if (!productId || !accountId || !/^[0-9a-f-]{36}$/i.test(accountId)) throw new BillingError('Missing purchase account/product'); + const owner = await getDb().query('SELECT id FROM mobile_billing_accounts WHERE id=$1',[accountId]); + if (owner.rowCount) { + const purchase = await verifyStorePurchase('google',proof,productId,accountId); + await recordPurchase(purchase); + await completeGooglePurchase(purchase); + } + return new NextResponse(null,{status:204}); + } catch (error) { return billingFailure(error); } +} diff --git a/app/api/mobile-billing/verify/route.ts b/app/api/mobile-billing/verify/route.ts new file mode 100644 index 0000000..9b3477f --- /dev/null +++ b/app/api/mobile-billing/verify/route.ts @@ -0,0 +1,17 @@ +import { NextResponse } from 'next/server'; +import { billingAccount, recordPurchase } from '@/lib/store-billing/ledger'; +import { authenticatedBillingWallet, billingFailure } from '@/lib/store-billing/http'; +import { BillingError } from '@/lib/store-billing/catalog'; +import { completeGooglePurchase, verifyStorePurchase } from '@/lib/store-billing/verification'; +export const runtime = 'nodejs'; +export async function POST(request: Request) { + try { + const body = await request.json(); + const wallet = await authenticatedBillingWallet(request,body.walletAddress); + if ((body.store !== 'apple' && body.store !== 'google') || typeof body.proof !== 'string' || typeof body.productId !== 'string') throw new BillingError('Invalid purchase'); + const purchase = await verifyStorePurchase(body.store,body.proof,body.productId,await billingAccount(wallet)); + await recordPurchase(purchase); // Idempotent durable commit happens before client finish/acknowledgement/consumption. + if (purchase.store === 'google') await completeGooglePurchase(purchase); + return NextResponse.json({ accepted:true },{ headers:{'Cache-Control':'no-store'} }); + } catch (error) { return billingFailure(error); } +} diff --git a/app/api/report-access/route.ts b/app/api/report-access/route.ts index b5bf8c2..5e01684 100644 --- a/app/api/report-access/route.ts +++ b/app/api/report-access/route.ts @@ -3,6 +3,7 @@ import { randomUUID } from 'crypto'; import Stripe from 'stripe'; import { isPaidReportType, PAID_REPORT_TYPES, PaidReportType } from '@/lib/report-access'; import { verifyWalletAuth } from '@/lib/dynamic-auth'; +import { mobileBillingEnabled, mobileEntitlements, consumeMobileReport } from '@/lib/store-billing/ledger'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || 'sk_test_placeholder', { apiVersion: '2026-09-30.endive', @@ -128,16 +129,22 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Invalid wallet address format' }, { status: 400 }); } - if (!process.env.STRIPE_SECRET_KEY) { - return NextResponse.json({ error: 'Stripe is not configured' }, { status: 500 }); + const includeMobile = mobileBillingEnabled() && !!request.headers.get('authorization'); + if (includeMobile || action === 'consume') { + const auth = await verifyWalletAuth(request.headers.get('authorization'), normalizedWallet); + if (!auth.ok) return NextResponse.json({ error:auth.error }, { status:auth.status || 401 }); + } + const mobile = includeMobile ? await mobileEntitlements(normalizedWallet) : null; + if (!process.env.STRIPE_SECRET_KEY && !includeMobile) { + return NextResponse.json({ error:'Payments are not configured' }, { status:503 }); } if (action === 'check') { - const payments = await collectUnusedPayments(normalizedWallet, sessionId); + const payments = process.env.STRIPE_SECRET_KEY ? await collectUnusedPayments(normalizedWallet, sessionId) : []; const availableReports = Object.fromEntries( PAID_REPORT_TYPES.map(type => [ type, - payments.filter(payment => payment.metadata.reportType === type).length, + payments.filter(payment => payment.metadata.reportType === type).length + (mobile?.availableReports[type] || 0), ]) ); @@ -147,21 +154,15 @@ export async function POST(request: NextRequest) { }); } - // Consuming passes mutates paid state, so it requires proof that the - // caller owns the wallet. - if (action === 'consume') { - const auth = await verifyWalletAuth(request.headers.get('authorization'), normalizedWallet); - if (!auth.ok) { - return NextResponse.json({ error: auth.error }, { status: auth.status || 401 }); - } - } - if (action === 'consume') { if (!isPaidReportType(reportType)) { return NextResponse.json({ error: 'Invalid report type' }, { status: 400 }); } - const payments = await collectUnusedPayments(normalizedWallet, sessionId, reportType); + if (includeMobile && await consumeMobileReport(normalizedWallet, reportType)) { + return NextResponse.json({ success:true, source:'mobile_store' }); + } + const payments = process.env.STRIPE_SECRET_KEY ? await collectUnusedPayments(normalizedWallet, sessionId, reportType) : []; const consumedPaymentIntentId = await consumeOnePayment(payments); if (!consumedPaymentIntentId) { diff --git a/app/api/studies/route.ts b/app/api/studies/route.ts index f4b32a4..7ad0a60 100644 --- a/app/api/studies/route.ts +++ b/app/api/studies/route.ts @@ -539,6 +539,9 @@ export async function GET(request: NextRequest) { ? `,\n (1 - (se.embedding <=> ${`'${JSON.stringify(queryEmbedding)}'::vector`})) as similarity` : ''; + // Concurrent Run All page requests must select stable, disjoint catalog ranges. + if(fetchAll && isRunAllQuery)orderByClause="ORDER BY gc.id"; + const baseQuery = `SELECT ${idSelection}, gc.study_accession, gc.study, @@ -922,6 +925,13 @@ export async function GET(request: NextRequest) { snps: s.snps, strongest_snp_risk_allele: s.strongest_snp_risk_allele, or_or_beta: s.or_or_beta, + // CI text encodes beta direction/type; omitting it misclassifies beta as OR. + ci_text: s.ci_text, + p_value: s.p_value, + pvalue_mlog: s.pvalue_mlog, + mapped_gene: s.mapped_gene, + initial_sample_size: s.initial_sample_size, + replication_sample_size: s.replication_sample_size, })); return NextResponse.json({ diff --git a/app/api/vault-sync/route.ts b/app/api/vault-sync/route.ts new file mode 100644 index 0000000..e2d4cb0 --- /dev/null +++ b/app/api/vault-sync/route.ts @@ -0,0 +1,31 @@ +import { NextResponse } from 'next/server'; +import { verifyWalletAuth } from '@/lib/dynamic-auth'; +import { deleteSyncedVault, MAX_SYNC_BYTES, readSyncedVault, VaultSyncError, writeSyncedVault } from '@/lib/vault-sync'; +export const runtime='nodejs'; +async function body(request:Request):Promise> { + if(Number(request.headers.get('content-length'))>MAX_SYNC_BYTES)throw new VaultSyncError('Encrypted vault is too large',413); + const reader=request.body?.getReader();if(!reader) throw new VaultSyncError('Missing request'); + const parts:Uint8Array[]=[];let size=0; + while(true) {const {done,value}=await reader.read();if(done) break;size+=value.length;if(size>MAX_SYNC_BYTES){await reader.cancel();throw new VaultSyncError('Encrypted vault is too large',413);}parts.push(value);} + try{const value=JSON.parse(Buffer.concat(parts).toString('utf8'));if(!value || typeof value!=='object' || Array.isArray(value))throw new Error();return value;}catch{throw new VaultSyncError('Invalid request');} +} +async function wallet(request:Request,value:unknown) { + if(process.env.VAULT_SYNC_ENABLED!=='true') throw new VaultSyncError('Encrypted sync is not configured',503); + if(typeof value!=='string' || !/^0x[0-9a-f]{40}$/i.test(value)) throw new VaultSyncError('Invalid account'); + const auth=await verifyWalletAuth(request.headers.get('authorization'),value); + if(!auth.ok) throw new VaultSyncError(auth.error || 'Authentication required',auth.status || 401); + return value.toLowerCase(); +} +function failure(error:unknown) {const known=error instanceof VaultSyncError?error:new VaultSyncError('Encrypted sync is unavailable',503);return NextResponse.json({error:known.message},{status:known.status,headers:{'Cache-Control':'no-store'}});} +export async function POST(request:Request) { + try { + const data=await body(request), owner=await wallet(request,data.walletAddress); + if(['write','delete'].includes(String(data.action)) && typeof data.baseRevision!=='number')throw new VaultSyncError('Invalid sync revision'); + switch(data.action) { + case 'read':return NextResponse.json(await readSyncedVault(owner,typeof data.knownRevision==='number'?data.knownRevision:undefined,typeof data.knownGenotype==='string'?data.knownGenotype:undefined),{headers:{'Cache-Control':'no-store'}}); + case 'write':return NextResponse.json({revision:await writeSyncedVault(owner,Number(data.baseRevision),data.archive as Parameters[2])},{headers:{'Cache-Control':'no-store'}}); + case 'delete':await deleteSyncedVault(owner,Number(data.baseRevision));return NextResponse.json({deleted:true},{headers:{'Cache-Control':'no-store'}}); + default:throw new VaultSyncError('Invalid sync action'); + } + } catch(error) {return failure(error);} +} diff --git a/app/components/AuthProvider.tsx b/app/components/AuthProvider.tsx index aaa1661..5a77484 100644 --- a/app/components/AuthProvider.tsx +++ b/app/components/AuthProvider.tsx @@ -1,6 +1,6 @@ "use client"; -import { DynamicContextProvider, DynamicWidget, useDynamicContext } from '@dynamic-labs/sdk-react-core'; +import { DynamicContextProvider, DynamicWidget, useDynamicContext, getAuthToken } from '@dynamic-labs/sdk-react-core'; import { EthereumWalletConnectors } from '@dynamic-labs/ethereum'; import { createContext, useContext, useEffect, useState, useCallback, ReactNode } from 'react'; import { trackSignInStarted, trackUserLoggedIn, trackUserLoggedOut } from '@/lib/analytics'; @@ -125,7 +125,7 @@ export function AuthProvider({ children }: { children: ReactNode }) { // Query API directly - no caching const response = await fetch('/api/check-subscription', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...(getAuthToken() ? { Authorization:`Bearer ${getAuthToken()}` } : {}) }, body: JSON.stringify({ walletAddress }), }); diff --git a/app/components/ConversionOnboarding.tsx b/app/components/ConversionOnboarding.tsx index 41128c0..b4627aa 100644 --- a/app/components/ConversionOnboarding.tsx +++ b/app/components/ConversionOnboarding.tsx @@ -220,7 +220,7 @@ export default function ConversionOnboarding({ mode = "guided", }: ConversionOnboardingProps) { const { genotypeData, isUploaded, isLoading, error, originalFileName, uploadGenotype } = useGenotype(); - const { addResultsBatch, savedResults, getTopResultsByRelevance } = useResults(); + const { addResultsBatch, savedResults, getTopResultsByRelevance, hasResult } = useResults(); const [mounted, setMounted] = useState(false); const [currentStep, setCurrentStep] = useState("intro"); @@ -508,7 +508,7 @@ RESPONSE STRUCTURE: const results = await runAllAnalysisOnboarding( genotypeData, (progress) => setRunAllProgress(progress), - (studyId) => savedResults.some((result) => result.studyId === studyId) + hasResult ); await addResultsBatch(results); @@ -541,7 +541,7 @@ RESPONSE STRUCTURE: message, })); } - }, [addResultsBatch, genotypeData, savedResults]); + }, [addResultsBatch, genotypeData, savedResults, hasResult]); useEffect(() => { if (!isOpen || currentStep !== "run_all" || !genotypeData || genotypeData.size === 0 || analysisStartedRef.current) { diff --git a/app/components/CustomizationContext.tsx b/app/components/CustomizationContext.tsx index 98e682e..c932f9e 100644 --- a/app/components/CustomizationContext.tsx +++ b/app/components/CustomizationContext.tsx @@ -4,6 +4,7 @@ import { createContext, useContext, useState, useEffect, useRef, ReactNode } fro import { useVault, LEGACY_PERSONALIZATION_KEY } from "./VaultContext"; export interface UserCustomization { + additionalContext?: string; ethnicities: string[]; countriesOfOrigin: string[]; genderAtBirth: string; diff --git a/app/components/CustomizationModal.tsx b/app/components/CustomizationModal.tsx index 1ecaedd..fdb8a2c 100644 --- a/app/components/CustomizationModal.tsx +++ b/app/components/CustomizationModal.tsx @@ -32,6 +32,7 @@ export default function CustomizationModal({ isOpen, onClose }: CustomizationMod const [alcoholUse, setAlcoholUse] = useState(''); const [medications, setMedications] = useState(''); const [diet, setDiet] = useState(''); + const [additionalContext, setAdditionalContext] = useState(''); useEffect(() => { if (isOpen) { @@ -64,6 +65,7 @@ export default function CustomizationModal({ isOpen, onClose }: CustomizationMod setAlcoholUse(customization.alcoholUse || ''); setMedications((customization.medications || []).join(', ')); setDiet(customization.diet || ''); + setAdditionalContext(customization.additionalContext || ''); } else { setIsUnlockMode(false); // Reset form for new customization @@ -78,6 +80,7 @@ export default function CustomizationModal({ isOpen, onClose }: CustomizationMod setAlcoholUse(''); setMedications(''); setDiet(''); + setAdditionalContext(''); } } }, [isOpen, status, customization]); @@ -136,6 +139,7 @@ export default function CustomizationModal({ isOpen, onClose }: CustomizationMod alcoholUse: alcoholUse as any, medications: medications.split(',').map(s => s.trim()).filter(Boolean), diet: diet as any, + additionalContext, }; await saveCustomization(data, needsPassword ? password : undefined); @@ -395,6 +399,10 @@ export default function CustomizationModal({ isOpen, onClose }: CustomizationMod +
+ +