diff --git a/kez-chat/Cargo.lock b/kez-chat/Cargo.lock index 13bb6d2..6044537 100644 --- a/kez-chat/Cargo.lock +++ b/kez-chat/Cargo.lock @@ -88,6 +88,24 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -96,7 +114,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -121,7 +139,7 @@ dependencies = [ "axum-core", "bytes", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -154,7 +172,7 @@ dependencies = [ "async-trait", "bytes", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "mime", @@ -166,6 +184,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + [[package]] name = "base64" version = "0.22.1" @@ -184,6 +220,34 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "binstring" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0669d5a35b64fdb5ab7fb19cae13148b6b5cbdf4b8247faf54ece47f699c8cef" + +[[package]] +name = "bip39" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "rand_core 0.6.4", + "serde", + "unicode-normalization", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9901a56e133a1fc86eeb1113e2591f45f4682451ca893bff494d2f88918e3f" +dependencies = [ + "hex-conservative", +] + [[package]] name = "bitflags" version = "2.11.1" @@ -205,12 +269,27 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + [[package]] name = "bytes" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.62" @@ -280,7 +359,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -289,12 +368,38 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "coarsetime" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58eb270476aa4fc7843849f8a35063e8743b4dbcdf6dd0f8ea0886980c204c2" +dependencies = [ + "libc", + "wasix", + "wasm-bindgen", +] + [[package]] name = "colorchoice" version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6f2aa4d0537bcc1c74df8755072bd31c1ef1a3a1b85a68e8404a8c353b7b8b" + [[package]] name = "const-oid" version = "0.9.6" @@ -316,6 +421,24 @@ dependencies = [ "libc", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[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 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -326,6 +449,43 @@ dependencies = [ "typenum", ] +[[package]] +name = "ct-codecs" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fb0c6640b4507ebd99ff67677009e381ba5eee1d14df78de4a3d16eb123c39" + +[[package]] +name = "curl" +version = "0.4.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79fc3b6dd0b87ba36e565715bf9a2ced221311db47bd18011676f24a6066edbc" +dependencies = [ + "curl-sys", + "libc", + "openssl-probe", + "openssl-sys", + "schannel", + "socket2", + "windows-sys 0.59.0", +] + +[[package]] +name = "curl-sys" +version = "0.4.87+curl-8.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a460380f0ef783703dcbe909107f39c162adeac050d73c850055118b5b6327" +dependencies = [ + "cc", + "libc", + "libnghttp2-sys", + "libz-sys", + "openssl-sys", + "pkg-config", + "vcpkg", + "windows-sys 0.59.0", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -350,7 +510,28 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" +dependencies = [ + "const-oid 0.6.2", + "der_derive", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.6.0", + "zeroize", ] [[package]] @@ -359,10 +540,23 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", "zeroize", ] +[[package]] +name = "der_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8aed3b3c608dc56cf36c45fe979d04eda51242e6703d8d0bb03426ef7c41db6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + [[package]] name = "digest" version = "0.10.7" @@ -370,7 +564,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", + "const-oid 0.9.6", "crypto-common", + "subtle", ] [[package]] @@ -381,7 +577,39 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest", + "elliptic-curve", + "rfc6979", + "signature 2.2.0", + "spki 0.7.3", +] + +[[package]] +name = "ece" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2ea1d2f2cc974957a4e2575d8e5bb494549bab66338d6320c2789abcfff5746" +dependencies = [ + "base64 0.21.7", + "byteorder", + "hex", + "hkdf", + "lazy_static", + "once_cell", + "openssl", + "serde", + "sha2", + "thiserror 1.0.69", ] [[package]] @@ -390,8 +618,18 @@ version = "2.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" dependencies = [ - "pkcs8", - "signature", + "pkcs8 0.10.2", + "signature 2.2.0", +] + +[[package]] +name = "ed25519-compact" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5c0284a5d4b1a2fae017a9fe55fd7d01699711f1b572493f16593e173ea2801" +dependencies = [ + "ct-codecs", + "getrandom 0.4.2", ] [[package]] @@ -409,6 +647,36 @@ dependencies = [ "zeroize", ] +[[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", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -425,6 +693,27 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -443,6 +732,16 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "fiat-crypto" version = "0.2.9" @@ -455,12 +754,33 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + [[package]] name = "form_urlencoded" version = "1.2.2" @@ -518,6 +838,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + [[package]] name = "futures-macro" version = "0.3.32" @@ -526,7 +859,7 @@ checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -566,6 +899,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -602,10 +936,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi 6.0.0", "wasip2", "wasip3", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", ] [[package]] @@ -647,12 +994,80 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha1-compact" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" +dependencies = [ + "digest", +] + +[[package]] +name = "hmac-sha512" +version = "1.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "019ece39bbefc17f13f677a690328cb978dbf6790e141a3c24e66372cb38588b" +dependencies = [ + "digest", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" version = "1.4.0" @@ -670,7 +1085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http", + "http 1.4.0", ] [[package]] @@ -681,7 +1096,7 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "pin-project-lite", ] @@ -714,7 +1129,7 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http", + "http 1.4.0", "http-body", "httparse", "httpdate", @@ -731,7 +1146,7 @@ version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ - "http", + "http 1.4.0", "hyper", "hyper-util", "rustls", @@ -747,11 +1162,11 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-channel", "futures-util", - "http", + "http 1.4.0", "http-body", "hyper", "ipnet", @@ -921,6 +1336,32 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "isahc" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82d93e1769c5c2b13a8e0d8ca9b6466c60bafade047326f25e3bcb97a947d875" +dependencies = [ + "async-channel", + "castaway", + "crossbeam-utils", + "curl", + "curl-sys", + "encoding_rs", + "event-listener", + "futures-lite", + "http 0.2.12", + "log", + "mime", + "polling", + "slab", + "sluice", + "tracing", + "tracing-futures", + "url", + "waker-fn", +] + [[package]] name = "itoa" version = "1.0.18" @@ -949,37 +1390,82 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jwt-simple" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "357892bb32159d763abdea50733fadcb9a8e1c319a9aa77592db8555d05af83e" +dependencies = [ + "anyhow", + "binstring", + "coarsetime", + "ct-codecs", + "ed25519-compact", + "hmac-sha1-compact", + "hmac-sha256", + "hmac-sha512", + "k256", + "p256", + "p384", + "rand 0.8.6", + "rsa", + "serde", + "serde_json", + "spki 0.6.0", + "thiserror 1.0.69", + "zeroize", +] + +[[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", + "once_cell", + "sha2", + "signature 2.2.0", +] + [[package]] name = "kez-chat-server" version = "0.1.0" dependencies = [ "anyhow", "axum", + "base64 0.22.1", "chrono", "clap", "futures", "hex", "kez-core", + "p256", + "rand 0.8.6", "reqwest", "rusqlite", "serde", "serde_json", "sha2", "tempfile", - "thiserror", + "thiserror 2.0.18", "tokio", "tokio-stream", "tower-http", "tracing", "tracing-subscriber", + "web-push", ] [[package]] name = "kez-core" version = "0.1.0" dependencies = [ - "base64", + "base64 0.22.1", "bech32", + "bip39", "chrono", "ed25519-dalek", "hex", @@ -989,7 +1475,7 @@ dependencies = [ "serde_jcs", "serde_json", "sha2", - "thiserror", + "thiserror 2.0.18", "zstd", ] @@ -998,6 +1484,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leb128fmt" @@ -1011,6 +1500,22 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libnghttp2-sys" +version = "0.1.13+1.68.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "492e00167f1418c15648144f42bbfc63099806ecee9bf8d09a6353d6b4856b3c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "libsqlite3-sys" version = "0.30.1" @@ -1022,6 +1527,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libz-sys" +version = "1.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85bc9657773828b90eeb625adff10eeac83cc21bbfd8e23a03eaa8a33c9e28d9" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -1103,6 +1620,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -1110,6 +1663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -1124,26 +1678,180 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "openssl" +version = "0.10.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.116" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.1", + "once_cell", + "regex", +] + +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + +[[package]] +name = "pem-rfc7468" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d159833a9105500e0398934e205e0773f0b27529557134ecfc51c27646adac" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff33bdbdfc54cc98a2eca766ebdec3e1b8fb7387523d5c9c9a2891da856f719" +dependencies = [ + "der 0.6.1", + "pkcs8 0.9.0", + "spki 0.6.0", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der 0.6.1", + "spki 0.6.0", +] + [[package]] name = "pkcs8" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" dependencies = [ - "der", - "spki", + "der 0.7.10", + "spki 0.7.3", ] [[package]] @@ -1152,6 +1860,20 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -1177,7 +1899,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2", - "syn", + "syn 2.0.117", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", ] [[package]] @@ -1203,7 +1934,7 @@ dependencies = [ "rustc-hash", "rustls", "socket2", - "thiserror", + "thiserror 2.0.18", "tokio", "tracing", "web-time", @@ -1224,7 +1955,7 @@ dependencies = [ "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.18", "tinyvec", "tracing", "web-time", @@ -1324,6 +2055,18 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -1347,10 +2090,10 @@ version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64", + "base64 0.22.1", "bytes", "futures-core", - "http", + "http 1.4.0", "http-body", "http-body-util", "hyper", @@ -1379,6 +2122,16 @@ dependencies = [ "webpki-roots", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -1393,6 +2146,27 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rsa" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "094052d5470cbcef561cb848a7209968c9f12dfa6d668f4bca048ac5de51099c" +dependencies = [ + "byteorder", + "digest", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.9.0", + "rand_core 0.6.4", + "signature 1.6.4", + "smallvec", + "subtle", + "zeroize", +] + [[package]] name = "rusqlite" version = "0.32.1" @@ -1488,6 +2262,40 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "sec1_decode" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6326ddc956378a0739200b2c30892dccaf198992dfd7323274690b9e188af23" +dependencies = [ + "der 0.4.5", + "pem 0.8.3", + "thiserror 1.0.69", +] + [[package]] name = "secp256k1" version = "0.29.1" @@ -1540,7 +2348,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1626,12 +2434,23 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "signature" version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" dependencies = [ + "digest", "rand_core 0.6.4", ] @@ -1641,6 +2460,17 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "sluice" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "160b744a45e8261307bcfe03c98e2f8274502207d534c9a64b675c4db1b6bd58" +dependencies = [ + "async-channel", + "futures-core", + "futures-io", +] + [[package]] name = "smallvec" version = "1.15.1" @@ -1657,6 +2487,22 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der 0.6.1", +] + [[package]] name = "spki" version = "0.7.3" @@ -1664,7 +2510,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" dependencies = [ "base64ct", - "der", + "der 0.7.10", ] [[package]] @@ -1685,6 +2531,17 @@ version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" version = "2.0.117" @@ -1705,6 +2562,18 @@ dependencies = [ "futures-core", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + [[package]] name = "synstructure" version = "0.13.2" @@ -1713,7 +2582,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1729,13 +2598,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", ] [[package]] @@ -1746,7 +2635,7 @@ checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1807,7 +2696,7 @@ checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1871,7 +2760,7 @@ dependencies = [ "bytes", "futures-core", "futures-util", - "http", + "http 1.4.0", "http-body", "http-body-util", "http-range-header", @@ -1921,7 +2810,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -1934,6 +2823,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.2.0" @@ -1987,6 +2886,15 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-xid" version = "0.2.6" @@ -2041,6 +2949,12 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + [[package]] name = "want" version = "0.3.1" @@ -2074,6 +2988,15 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasix" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1757e0d1f8456693c7e5c6c629bdb54884e032aa0bb53c155f6a39f94440d332" +dependencies = [ + "wasi", +] + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -2116,7 +3039,7 @@ dependencies = [ "bumpalo", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wasm-bindgen-shared", ] @@ -2163,6 +3086,28 @@ dependencies = [ "semver", ] +[[package]] +name = "web-push" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f2332e5400bb42c21bcab3ca2cd3400ab4b1d5ecbe276b533ce9acb59c56602" +dependencies = [ + "async-trait", + "base64 0.13.1", + "chrono", + "ece", + "futures-lite", + "http 0.2.12", + "isahc", + "jwt-simple", + "log", + "pem 3.0.6", + "sec1_decode", + "serde", + "serde_derive", + "serde_json", +] + [[package]] name = "web-sys" version = "0.3.99" @@ -2213,7 +3158,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2224,7 +3169,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2260,6 +3205,15 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.60.2" @@ -2443,7 +3397,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2459,7 +3413,7 @@ dependencies = [ "prettyplease", "proc-macro2", "quote", - "syn", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2526,8 +3480,8 @@ checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.117", + "synstructure 0.13.2", ] [[package]] @@ -2547,7 +3501,7 @@ checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] @@ -2567,8 +3521,8 @@ checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", - "syn", - "synstructure", + "syn 2.0.117", + "synstructure 0.13.2", ] [[package]] @@ -2607,7 +3561,7 @@ checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.117", ] [[package]] diff --git a/kez-chat/Cargo.toml b/kez-chat/Cargo.toml index f945751..5c14b5b 100644 --- a/kez-chat/Cargo.toml +++ b/kez-chat/Cargo.toml @@ -19,6 +19,10 @@ thiserror = "2" tokio = { version = "1.48", features = ["macros", "rt-multi-thread", "sync", "signal"] } tokio-stream = { version = "0.1", features = ["sync"] } futures = "0.3" +web-push = "0.10" +base64 = "0.22" +p256 = { version = "0.13", features = ["pem"] } +rand = "0.8" tower-http = { version = "0.6", features = ["trace", "cors", "fs"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } diff --git a/kez-chat/src/api.rs b/kez-chat/src/api.rs index cb8ef91..b0b8988 100644 --- a/kez-chat/src/api.rs +++ b/kez-chat/src/api.rs @@ -28,6 +28,8 @@ pub struct AppState { pub store: Store, pub config: Config, pub broker: crate::broker::Broker, + pub vapid: crate::push::VapidKeys, + pub push: crate::push::PushSender, } pub fn router(state: AppState) -> axum::Router { @@ -45,6 +47,9 @@ pub fn router(state: AppState) -> axum::Router { .route("/v1/messages", post(crate::messages::send_message)) .route("/v1/inbox/:handle", get(crate::messages::inbox)) .route("/v1/inbox/:handle/stream", get(crate::messages::stream_inbox)) + .route("/v1/push/vapid-public-key", get(push_vapid_key)) + .route("/v1/push/subscribe/:handle", post(push_subscribe)) + .route("/v1/push/unsubscribe/:handle", post(push_unsubscribe)) .route("/.well-known/webfinger", get(webfinger)) .route("/internal/nats/auth", post(nats_auth_callout)); @@ -239,6 +244,145 @@ fn verify_profile_auth( Ok(()) } +// ───────────────────────────────────────────────────────────────────────────── +// Web Push — VAPID key + subscribe/unsubscribe +// ───────────────────────────────────────────────────────────────────────────── +// +// Auth model is identical to the proofs endpoint: the caller signs a +// canonical request line with their primary Ed25519 key and puts +// `:` in `X-KEZ-Auth`. The subscribe/unsubscribe +// bodies stay tiny so push.js can hand us the SubscriptionJSON +// straight from the browser. + +#[derive(Debug, Serialize)] +struct VapidKeyResponse { + /// uncompressed P-256 point, base64url-no-pad — passed straight to + /// `PushManager.subscribe({applicationServerKey})`. + key: String, +} + +async fn push_vapid_key(State(state): State) -> Json { + Json(VapidKeyResponse { + key: state.vapid.public_b64url.clone(), + }) +} + +#[derive(Debug, Deserialize)] +pub struct PushSubscribeRequest { + pub endpoint: String, + pub p256dh: String, + pub auth: String, +} + +/// Canonical message the caller signs for push subscribe/unsubscribe. +/// Bound to the endpoint so a stolen header can't be replayed against +/// a different subscription (e.g. attacker swapping in their own URL). +pub fn canonical_push_message(verb: &str, handle: &str, endpoint: &str, ts: i64) -> String { + format!("{verb}\n/v1/push/{verb}/{handle}\n{endpoint}\n{ts}") +} + +fn verify_push_auth( + auth: &str, + verb: &str, + handle: &str, + endpoint: &str, + pubkey_hex: &str, + now_ts: i64, +) -> Result<(), ApiError> { + let (ts_str, sig_hex) = auth + .split_once(':') + .ok_or_else(|| ApiError::Unauthorized("X-KEZ-Auth must be :".into()))?; + let ts: i64 = ts_str + .parse() + .map_err(|_| ApiError::Unauthorized("auth ts must be a unix timestamp".into()))?; + if (now_ts - ts).abs() > 60 { + return Err(ApiError::Unauthorized("auth header is stale".into())); + } + let message = canonical_push_message(verb, handle, endpoint, ts); + kez_core::verify_ed25519_hex(pubkey_hex, message.as_bytes(), sig_hex) + .map_err(|_| ApiError::Unauthorized("signature did not verify".into()))?; + Ok(()) +} + +async fn push_subscribe( + State(state): State, + Path(handle): Path, + headers: axum::http::HeaderMap, + Json(req): Json, +) -> Result { + validate_handle(&handle) + .map_err(|e| ApiError::BadRequest(format!("invalid handle: {e}")))?; + let record = state + .store + .lookup(&handle) + .await? + .ok_or(ApiError::NotFound)?; + + let auth = headers + .get("X-KEZ-Auth") + .ok_or_else(|| ApiError::Unauthorized("missing X-KEZ-Auth header".into()))? + .to_str() + .map_err(|_| ApiError::Unauthorized("non-ASCII X-KEZ-Auth".into()))?; + verify_push_auth( + auth, + "subscribe", + &handle, + &req.endpoint, + record.primary.value(), + Utc::now().timestamp(), + )?; + + state + .store + .upsert_push_subscription( + &handle, + &crate::push::StoredSubscription { + endpoint: req.endpoint, + p256dh: req.p256dh, + auth: req.auth, + }, + ) + .await?; + Ok(StatusCode::NO_CONTENT) +} + +#[derive(Debug, Deserialize)] +pub struct PushUnsubscribeRequest { + pub endpoint: String, +} + +async fn push_unsubscribe( + State(state): State, + Path(handle): Path, + headers: axum::http::HeaderMap, + Json(req): Json, +) -> Result { + validate_handle(&handle) + .map_err(|e| ApiError::BadRequest(format!("invalid handle: {e}")))?; + let record = state + .store + .lookup(&handle) + .await? + .ok_or(ApiError::NotFound)?; + + let auth = headers + .get("X-KEZ-Auth") + .ok_or_else(|| ApiError::Unauthorized("missing X-KEZ-Auth header".into()))? + .to_str() + .map_err(|_| ApiError::Unauthorized("non-ASCII X-KEZ-Auth".into()))?; + verify_push_auth( + auth, + "unsubscribe", + &handle, + &req.endpoint, + record.primary.value(), + Utc::now().timestamp(), + )?; + + state.store.delete_push_subscription(&req.endpoint).await?; + Ok(StatusCode::NO_CONTENT) +} + // ───────────────────────────────────────────────────────────────────────────── // GET /.well-known/webfinger — fediverse-style discovery // ───────────────────────────────────────────────────────────────────────────── diff --git a/kez-chat/src/config.rs b/kez-chat/src/config.rs index 5a90b60..07d71c6 100644 --- a/kez-chat/src/config.rs +++ b/kez-chat/src/config.rs @@ -37,4 +37,21 @@ pub struct Config { /// output). If unset, `/` serves a built-in placeholder page. #[arg(long, env = "KEZ_CHAT_WEB_DIR")] pub web_dir: Option, + + /// Where the Web Push VAPID private key is stored. Auto-generated + /// on first startup if the file doesn't exist (raw 32-byte P-256 + /// scalar, base64-encoded). Public key is exposed at + /// /v1/push/vapid-public-key. + #[arg(long, env = "KEZ_CHAT_VAPID_KEY", default_value = "vapid-key.txt")] + pub vapid_key_path: PathBuf, + + /// Subject for VAPID JWTs — typically a mailto: URL of the + /// operator. Push providers (FCM / Mozilla / APNs) use it to + /// contact the operator if subscriptions misbehave. + #[arg( + long, + env = "KEZ_CHAT_VAPID_SUBJECT", + default_value = "mailto:admin@kez.lat" + )] + pub vapid_subject: String, } diff --git a/kez-chat/src/lib.rs b/kez-chat/src/lib.rs index 56b8538..4d62b52 100644 --- a/kez-chat/src/lib.rs +++ b/kez-chat/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub mod error; pub mod handles; pub mod messages; +pub mod push; pub mod registration; pub mod store; diff --git a/kez-chat/src/main.rs b/kez-chat/src/main.rs index e12a72b..1fd9d19 100644 --- a/kez-chat/src/main.rs +++ b/kez-chat/src/main.rs @@ -26,10 +26,15 @@ async fn main() -> Result<()> { ); let store = Store::open(&config.db)?; + let vapid = + kez_chat_server::push::load_or_generate_vapid(&config.vapid_key_path)?; + let push = kez_chat_server::push::PushSender::new(&vapid, &config.vapid_subject)?; let state = AppState { store, config: config.clone(), broker: kez_chat_server::broker::Broker::new(), + vapid, + push, }; let app = router(state) diff --git a/kez-chat/src/messages.rs b/kez-chat/src/messages.rs index 67b74a0..67ccaa1 100644 --- a/kez-chat/src/messages.rs +++ b/kez-chat/src/messages.rs @@ -113,6 +113,22 @@ pub async fn send_message( ) .await; + // Web Push fanout — fire-and-forget so the HTTP request still + // returns fast. The push payload is intentionally tiny and + // contains only metadata: the recipient's own client will pull + // the real (encrypted) envelope from the inbox/SSE on wake-up. + let push = state.push.clone(); + let store = state.store.clone(); + let recipient_handle = recipient.handle.clone(); + let payload = serde_json::json!({ + "type": "kez-chat/new-message", + "to": recipient_handle, + "seq": seq, + }); + tokio::spawn(async move { + push.fanout(&store, &recipient_handle, &payload).await; + }); + Ok(Json(SendMessageResponse { seq })) } diff --git a/kez-chat/src/push.rs b/kez-chat/src/push.rs new file mode 100644 index 0000000..4c81bb9 --- /dev/null +++ b/kez-chat/src/push.rs @@ -0,0 +1,305 @@ +//! Web Push (RFC 8030 / RFC 8291 / VAPID RFC 8292) for kez-chat. +//! +//! Lets the chat-server fire notifications to the user's browser even +//! when the kez-chat PWA is fully closed. The browser registers a push +//! subscription with its push provider (FCM for Chrome/Edge, Mozilla +//! autopush for Firefox, Apple Push Notification Service for Safari); +//! we get back an endpoint URL + a pair of opaque keys (`p256dh`, `auth`). +//! When a new chat message lands, we POST a (tiny, possibly empty) +//! payload to the endpoint with a VAPID JWT proving the message is from +//! us. The push provider forwards it to the user's device; the device's +//! service worker wakes up briefly and calls `showNotification(...)`. +//! +//! Trust model: +//! - The push payload is opaque to the push provider (e.g. Google / +//! Apple) — we encrypt it under p256dh+auth as per RFC 8291. +//! - We deliberately send a *near-empty* payload: just the sender's +//! handle (which the recipient already knows about anyway) and the +//! message sequence. The actual ciphertext stays on our SSE/inbox +//! path, where the recipient's client pulls it and decrypts with the +//! KEZ E2E key. So even if a push provider went rogue, they wouldn't +//! see plaintext. +//! - 410 Gone from the provider → drop the subscription (the user +//! removed the app, the install expired, or the OS revoked it). + +use std::path::Path; + +use base64::Engine; +use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64URL; +use chrono::Utc; +use p256::SecretKey; +use p256::elliptic_curve::sec1::ToEncodedPoint; +use p256::pkcs8::EncodePrivateKey; +use rusqlite::params; +use serde::{Deserialize, Serialize}; +use tokio::sync::Mutex; +// Note: WebPushClient is a trait that provides the .send() method on +// IsahcWebPushClient — keep it in scope even though it looks "unused". +use web_push::{ + ContentEncoding, IsahcWebPushClient, SubscriptionInfo, SubscriptionKeys, + VapidSignatureBuilder, WebPushClient, WebPushError, WebPushMessageBuilder, +}; + +use crate::error::ApiError; +use crate::store::Store; + +// ───────────────────────────────────────────────────────────────────────────── +// VAPID keys +// ───────────────────────────────────────────────────────────────────────────── + +/// In-memory VAPID material. The private key is held as PEM bytes +/// because that's the form web-push's `VapidSignatureBuilder::from_pem` +/// consumes. We re-parse on every send (cheap) so we don't have to +/// juggle a long-lived signer object across an async boundary. +#[derive(Clone)] +pub struct VapidKeys { + /// PEM-encoded P-256 private key (PKCS#8). + pub private_pem: String, + /// Uncompressed P-256 point, 65 bytes (0x04 || X || Y), encoded + /// as base64url-no-pad — the form `applicationServerKey` expects + /// in `PushManager.subscribe()`. + pub public_b64url: String, +} + +/// Load VAPID keys from `path`, generating a new pair on first run. +/// The on-disk file is a standard PKCS#8 PEM — `openssl ec -in ` +/// will read it. +pub fn load_or_generate_vapid>(path: P) -> anyhow::Result { + let path = path.as_ref(); + if path.exists() { + let private_pem = std::fs::read_to_string(path)?; + let public_b64url = derive_public_b64url(&private_pem)?; + tracing::info!(?path, "loaded VAPID key"); + return Ok(VapidKeys { + private_pem, + public_b64url, + }); + } + + let secret = SecretKey::random(&mut rand::thread_rng()); + let private_pem = secret + .to_pkcs8_pem(p256::pkcs8::LineEnding::LF) + .map_err(|e| anyhow::anyhow!("pkcs8 encode: {e}"))? + .to_string(); + + if let Some(parent) = path.parent() { + if !parent.as_os_str().is_empty() { + std::fs::create_dir_all(parent)?; + } + } + std::fs::write(path, &private_pem)?; + // Best-effort tightening — failures are non-fatal on Windows. + let _ = std::fs::set_permissions(path, perms_0600()); + + let public_b64url = derive_public_b64url(&private_pem)?; + tracing::info!(?path, public_b64url = %public_b64url, "generated new VAPID key"); + Ok(VapidKeys { + private_pem, + public_b64url, + }) +} + +fn derive_public_b64url(private_pem: &str) -> anyhow::Result { + use p256::pkcs8::DecodePrivateKey; + let secret = SecretKey::from_pkcs8_pem(private_pem) + .map_err(|e| anyhow::anyhow!("pkcs8 decode: {e}"))?; + let public_point = secret.public_key().to_encoded_point(false); // uncompressed + Ok(B64URL.encode(public_point.as_bytes())) +} + +#[cfg(unix)] +fn perms_0600() -> std::fs::Permissions { + use std::os::unix::fs::PermissionsExt; + std::fs::Permissions::from_mode(0o600) +} +#[cfg(not(unix))] +fn perms_0600() -> std::fs::Permissions { + // No-op stand-in; Windows ACLs are out of scope for v0.1. + use std::fs::Permissions; + Permissions::from(std::fs::Metadata::default().permissions()) +} + +// ───────────────────────────────────────────────────────────────────────────── +// Subscription record + store API +// ───────────────────────────────────────────────────────────────────────────── + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StoredSubscription { + pub endpoint: String, + pub p256dh: String, + pub auth: String, +} + +impl StoredSubscription { + fn to_subscription_info(&self) -> SubscriptionInfo { + SubscriptionInfo { + endpoint: self.endpoint.clone(), + keys: SubscriptionKeys { + p256dh: self.p256dh.clone(), + auth: self.auth.clone(), + }, + } + } +} + +impl Store { + /// Insert (or replace, by endpoint) a push subscription for `handle`. + pub async fn upsert_push_subscription( + &self, + handle: &str, + sub: &StoredSubscription, + ) -> Result<(), ApiError> { + let conn = Store::inner_lock(self).await; + let now = Utc::now().to_rfc3339(); + conn.execute( + "INSERT INTO push_subscriptions (handle, endpoint, p256dh, auth, created_at) + VALUES (?1, ?2, ?3, ?4, ?5) + ON CONFLICT(endpoint) DO UPDATE SET handle = ?1, p256dh = ?3, auth = ?4", + params![handle, sub.endpoint, sub.p256dh, sub.auth, now], + )?; + Ok(()) + } + + /// Drop one subscription by endpoint (used both for explicit + /// unsubscribe and for 410 Gone cleanup on send failure). + pub async fn delete_push_subscription(&self, endpoint: &str) -> Result<(), ApiError> { + let conn = Store::inner_lock(self).await; + conn.execute( + "DELETE FROM push_subscriptions WHERE endpoint = ?1", + params![endpoint], + )?; + Ok(()) + } + + /// Every active subscription for `handle`. Empty Vec is fine — + /// just means the user hasn't enabled push (yet) on any device. + pub async fn list_push_subscriptions( + &self, + handle: &str, + ) -> Result, ApiError> { + let conn = Store::inner_lock(self).await; + let mut stmt = conn.prepare( + "SELECT endpoint, p256dh, auth FROM push_subscriptions WHERE handle = ?1", + )?; + let rows = stmt + .query_map(params![handle], |row| { + Ok(StoredSubscription { + endpoint: row.get(0)?, + p256dh: row.get(1)?, + auth: row.get(2)?, + }) + })? + .collect::, _>>()?; + Ok(rows) + } +} + +/// SQL fragment for the push_subscriptions table, run by store::init_schema. +pub const SCHEMA: &str = " + CREATE TABLE IF NOT EXISTS push_subscriptions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + handle TEXT NOT NULL, + endpoint TEXT NOT NULL UNIQUE, + p256dh TEXT NOT NULL, + auth TEXT NOT NULL, + created_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS idx_push_handle + ON push_subscriptions (handle); +"; + +// ───────────────────────────────────────────────────────────────────────────── +// Sender +// ───────────────────────────────────────────────────────────────────────────── + +/// Push notifier — clones cheaply (Arc-shared client + key material). +#[derive(Clone)] +pub struct PushSender { + inner: std::sync::Arc, +} + +struct PushInner { + client: IsahcWebPushClient, + vapid_private_pem: String, + vapid_subject: String, + // Reserved for future provider tweaks without re-plumbing. + _lock: Mutex<()>, +} + +impl PushSender { + pub fn new(vapid: &VapidKeys, vapid_subject: &str) -> anyhow::Result { + Ok(Self { + inner: std::sync::Arc::new(PushInner { + client: IsahcWebPushClient::new()?, + vapid_private_pem: vapid.private_pem.clone(), + vapid_subject: vapid_subject.to_owned(), + _lock: Mutex::new(()), + }), + }) + } + + /// Send a small payload to every subscription registered for + /// `recipient_handle`. Subscriptions that come back 410 Gone are + /// dropped from the store (the user removed the app, browser + /// rotated, etc.). All other errors are logged and ignored — push + /// is best-effort; the actual chat envelope is already in the + /// recipient's inbox. + pub async fn fanout( + &self, + store: &Store, + recipient_handle: &str, + payload: &serde_json::Value, + ) { + let subs = match store.list_push_subscriptions(recipient_handle).await { + Ok(s) => s, + Err(e) => { + tracing::warn!(error = %e, "push: list_subscriptions failed"); + return; + } + }; + if subs.is_empty() { + return; + } + let body = match serde_json::to_vec(payload) { + Ok(b) => b, + Err(e) => { + tracing::warn!(error = %e, "push: payload serialize failed"); + return; + } + }; + for sub in subs { + if let Err(e) = self.send_one(&sub, &body).await { + match e { + WebPushError::EndpointNotValid | WebPushError::EndpointNotFound => { + // 410 Gone / 404 → subscription is dead; drop it. + tracing::info!(endpoint = %sub.endpoint, "push: dropping expired subscription"); + let _ = store.delete_push_subscription(&sub.endpoint).await; + } + other => { + tracing::warn!(endpoint = %sub.endpoint, error = ?other, "push: send failed"); + } + } + } + } + } + + async fn send_one( + &self, + sub: &StoredSubscription, + body: &[u8], + ) -> Result<(), WebPushError> { + let sub_info = sub.to_subscription_info(); + let mut sig_builder = VapidSignatureBuilder::from_pem( + self.inner.vapid_private_pem.as_bytes(), + &sub_info, + )?; + sig_builder.add_claim("sub", self.inner.vapid_subject.as_str()); + let signature = sig_builder.build()?; + + let mut msg = WebPushMessageBuilder::new(&sub_info); + msg.set_payload(ContentEncoding::Aes128Gcm, body); + msg.set_vapid_signature(signature); + + self.inner.client.send(msg.build()?).await + } +} diff --git a/kez-chat/src/store.rs b/kez-chat/src/store.rs index 6cf95ca..5e76d22 100644 --- a/kez-chat/src/store.rs +++ b/kez-chat/src/store.rs @@ -43,6 +43,12 @@ impl Store { }) } + /// Crate-internal accessor to the connection mutex, for impl blocks + /// living in sibling modules (e.g. push.rs's subscription helpers). + pub(crate) async fn inner_lock(&self) -> tokio::sync::MutexGuard<'_, Connection> { + self.inner.lock().await + } + /// Reserve a handle for a primary key. Fails with Conflict if the /// handle is already taken, or if this primary key has already /// registered a (different) handle. @@ -162,7 +168,12 @@ fn init_schema(conn: &Connection) -> Result<(), rusqlite::Error> { ); CREATE INDEX IF NOT EXISTS idx_messages_recipient ON messages (recipient_handle, seq);", - ) + )?; + + // Web Push subscription store. Schema kept in push.rs so it lives + // next to the feature it backs; spliced in here so the table is + // created on first run. + conn.execute_batch(crate::push::SCHEMA) } // ───────────────────────────────────────────────────────────────────────────── diff --git a/kez-chat/web/package-lock.json b/kez-chat/web/package-lock.json index 55812e0..f31f206 100644 --- a/kez-chat/web/package-lock.json +++ b/kez-chat/web/package-lock.json @@ -30,7 +30,10 @@ "tailwindcss": "^4.0.0", "typescript": "^5.6.0", "vite": "^5.4.0", - "vite-plugin-pwa": "^1.3.0" + "vite-plugin-pwa": "^1.3.0", + "workbox-precaching": "^7.4.1", + "workbox-routing": "^7.4.1", + "workbox-strategies": "^7.4.1" } }, "node_modules/@apideck/better-ajv-errors": { diff --git a/kez-chat/web/package.json b/kez-chat/web/package.json index 913d2ca..a3deb09 100644 --- a/kez-chat/web/package.json +++ b/kez-chat/web/package.json @@ -32,6 +32,9 @@ "tailwindcss": "^4.0.0", "typescript": "^5.6.0", "vite": "^5.4.0", - "vite-plugin-pwa": "^1.3.0" + "vite-plugin-pwa": "^1.3.0", + "workbox-precaching": "^7.4.1", + "workbox-routing": "^7.4.1", + "workbox-strategies": "^7.4.1" } } diff --git a/kez-chat/web/src/lib/push.ts b/kez-chat/web/src/lib/push.ts new file mode 100644 index 0000000..b7d4d05 --- /dev/null +++ b/kez-chat/web/src/lib/push.ts @@ -0,0 +1,223 @@ +// Web Push subscription helpers — wraps the browser's PushManager and +// the chat-server's /v1/push/* endpoints into a small surface the +// Settings page can call. +// +// Flow: +// +// enablePush(handle, seed) +// 1. fetch /v1/push/vapid-public-key +// 2. browser PushManager.subscribe({applicationServerKey}) +// 3. sign + POST the subscription JSON to /v1/push/subscribe/:handle +// +// disablePush(handle, seed) +// 1. SW.pushManager.getSubscription() → unsubscribe locally +// 2. sign + POST /v1/push/unsubscribe/:handle with the endpoint URL +// +// All auth uses the same X-KEZ-Auth: : scheme as the rest +// of the API (see kez-chat/src/api.rs: canonical_push_message). + +import { ed25519 } from "@noble/curves/ed25519"; +import { bytesToHex } from "@noble/hashes/utils"; +import { ApiError } from "./api.js"; + +const API_BASE = import.meta.env.VITE_API_BASE ?? ""; + +function url(path: string): string { + return `${API_BASE}${path}`; +} + +/** + * Web Push is only usable if the browser supports it AND the page is + * served from a secure context (HTTPS or localhost). On iOS Safari it + * additionally requires the site to be installed as a PWA — Safari + * exposes the PushManager API only after add-to-home-screen. + */ +export function pushSupported(): boolean { + return ( + typeof window !== "undefined" && + "serviceWorker" in navigator && + "PushManager" in window && + "Notification" in window + ); +} + +/** Current permission state — does NOT prompt the user. */ +export function pushPermission(): NotificationPermission | "unsupported" { + if (!pushSupported()) return "unsupported"; + return Notification.permission; +} + +/** + * VAPID applicationServerKey arrives as base64url-no-pad; PushManager + * wants a Uint8Array. Pad + replace, then decode. + */ +function decodeB64Url(b64: string): Uint8Array { + const padded = b64.replace(/-/g, "+").replace(/_/g, "/"); + const pad = padded.length % 4 === 0 ? "" : "=".repeat(4 - (padded.length % 4)); + const bin = atob(padded + pad); + const out = new Uint8Array(bin.length); + for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i); + return out; +} + +/** Encode an ArrayBuffer as base64url-no-pad — server expects this form. */ +function encodeB64Url(buf: ArrayBuffer | null): string { + if (!buf) return ""; + const bytes = new Uint8Array(buf); + let bin = ""; + for (let i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} + +async function vapidPublicKey(): Promise { + const resp = await fetch(url("/v1/push/vapid-public-key")); + if (!resp.ok) { + throw new ApiError(resp.status, `vapid key fetch → ${resp.status}`); + } + const body = (await resp.json()) as { key: string }; + return body.key; +} + +interface SubscriptionPayload { + endpoint: string; + p256dh: string; + auth: string; +} + +function subscriptionPayload(sub: PushSubscription): SubscriptionPayload { + return { + endpoint: sub.endpoint, + p256dh: encodeB64Url(sub.getKey("p256dh")), + auth: encodeB64Url(sub.getKey("auth")), + }; +} + +function signPushAuth( + verb: "subscribe" | "unsubscribe", + handle: string, + endpoint: string, + seed: Uint8Array, +): string { + const ts = Math.floor(Date.now() / 1000); + const msg = `${verb}\n/v1/push/${verb}/${handle}\n${endpoint}\n${ts}`; + const sig = ed25519.sign(new TextEncoder().encode(msg), seed); + return `${ts}:${bytesToHex(sig)}`; +} + +/** + * Enable Web Push for `handle`. Prompts for permission if needed, + * subscribes to the browser's push provider, and registers the + * subscription with the chat-server. + * + * Returns `false` if the user declined permission. Throws on + * network/server failures. + */ +export async function enablePush( + handle: string, + seed: Uint8Array, +): Promise { + if (!pushSupported()) { + throw new Error("Web Push not supported in this browser"); + } + // iOS Safari requires the PWA to be installed (display-mode: standalone). + // We don't *block* on this — the subscribe() call will surface a clearer + // error than we could — but settings can use this as a hint. + const perm = await Notification.requestPermission(); + if (perm !== "granted") return false; + + const reg = await navigator.serviceWorker.ready; + const appServerKey = decodeB64Url(await vapidPublicKey()); + + // If a stale subscription exists (e.g. for a different handle), drop + // it first — we want exactly one active subscription per browser. + const existing = await reg.pushManager.getSubscription(); + if (existing) { + await existing.unsubscribe().catch(() => {}); + } + + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + // The TS DOM lib types `applicationServerKey` as ArrayBufferView, + // but our Uint8Array is over the generic ArrayBufferLike. The runtime is + // happy with either — cast to keep tsc quiet. + applicationServerKey: appServerKey as unknown as BufferSource, + }); + + const payload = subscriptionPayload(sub); + const authHeader = signPushAuth("subscribe", handle, payload.endpoint, seed); + const resp = await fetch(url(`/v1/push/subscribe/${encodeURIComponent(handle)}`), { + method: "POST", + headers: { + "content-type": "application/json", + "X-KEZ-Auth": authHeader, + }, + body: JSON.stringify(payload), + }); + if (!resp.ok) { + // Best-effort: drop the local subscription too so the user can retry + // cleanly. Otherwise the browser stays subscribed but the server + // doesn't know about it and notifications never arrive. + await sub.unsubscribe().catch(() => {}); + throw new ApiError(resp.status, `push subscribe → ${resp.status}`); + } + return true; +} + +/** + * Disable Web Push for `handle`. Tells both the browser and the + * chat-server to forget the subscription. Idempotent. + */ +export async function disablePush( + handle: string, + seed: Uint8Array, +): Promise { + if (!pushSupported()) return; + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.getSubscription(); + if (!sub) return; + + // Tell the server first so we still have a valid auth-able endpoint + // string. If the server call fails we still unsubscribe locally — + // 410 cleanup on the server side will catch the stale row on next + // fanout attempt anyway. + const authHeader = signPushAuth("unsubscribe", handle, sub.endpoint, seed); + await fetch(url(`/v1/push/unsubscribe/${encodeURIComponent(handle)}`), { + method: "POST", + headers: { + "content-type": "application/json", + "X-KEZ-Auth": authHeader, + }, + body: JSON.stringify({ endpoint: sub.endpoint }), + }).catch(() => {}); + + await sub.unsubscribe().catch(() => {}); +} + +/** + * Is this browser currently subscribed to push? Used by Settings to + * render the toggle in the right state. + */ +export async function isPushSubscribed(): Promise { + if (!pushSupported()) return false; + const reg = await navigator.serviceWorker.ready; + const sub = await reg.pushManager.getSubscription(); + return sub !== null; +} + +/** + * iOS PWA detection — Safari only exposes PushManager once installed + * to the home screen. We use this to render a "Tap Share → Add to Home + * Screen" callout instead of a broken toggle. + */ +export function isStandalonePwa(): boolean { + if (typeof window === "undefined") return false; + // iOS Safari's legacy `standalone` bool, plus the modern matchMedia. + const legacy = (window.navigator as unknown as { standalone?: boolean }).standalone === true; + const modern = window.matchMedia?.("(display-mode: standalone)").matches ?? false; + return legacy || modern; +} + +export function isIos(): boolean { + if (typeof navigator === "undefined") return false; + return /iPhone|iPad|iPod/.test(navigator.userAgent); +} diff --git a/kez-chat/web/src/main.ts b/kez-chat/web/src/main.ts index b7300a3..9a51725 100644 --- a/kez-chat/web/src/main.ts +++ b/kez-chat/web/src/main.ts @@ -26,6 +26,20 @@ if ("serviceWorker" in navigator) { refreshing = true; window.location.reload(); }); + + // Bridge from the service worker's `notificationclick` handler: when + // the user taps a push notification and we found an existing tab to + // focus, the SW posts `{type: "kez-chat/navigate", to: }` + // and we route the SPA there. Hash routing means we just set + // `location.hash` — svelte-spa-router picks it up. + navigator.serviceWorker.addEventListener("message", (event) => { + const data = event.data as { type?: string; to?: string } | undefined; + if (data?.type === "kez-chat/navigate" && typeof data.to === "string") { + // svelte-spa-router uses #/path — convert plain path to that form. + const target = data.to.startsWith("#") ? data.to : `#${data.to}`; + window.location.hash = target; + } + }); } export default app; diff --git a/kez-chat/web/src/routes/Messages.svelte b/kez-chat/web/src/routes/Messages.svelte index 1053e3f..f94df63 100644 --- a/kez-chat/web/src/routes/Messages.svelte +++ b/kez-chat/web/src/routes/Messages.svelte @@ -435,19 +435,51 @@ -
+
{#each activeConv.messages as m (m.seq + ":" + m.direction)} {@const boost = emojiOnlyBoost(m.body)} {@const out = m.direction === "out"} -
+ +
{#if boost} -
{m.body}
+
{m.body}
{:else}
{m.body}
+ class={[ + "relative max-w-[75%] px-3 pt-1.5 pb-1 text-sm shadow-sm", + // Bubble corners: rounded everywhere except the + // "tail" corner (matches WhatsApp shape). + out + ? "bg-accent text-accent-contrast rounded-2xl rounded-br-md" + : "bg-bubble-recv text-text rounded-2xl rounded-bl-md", + ].join(" ")} + > + {m.body} + + + + {formatTime(m.ts)} +
{/if} -

{formatTime(m.ts)}

{/each} {#if activeConv.messages.length === 0} diff --git a/kez-chat/web/src/routes/Settings.svelte b/kez-chat/web/src/routes/Settings.svelte index 15ec8fa..9f8b90a 100644 --- a/kez-chat/web/src/routes/Settings.svelte +++ b/kez-chat/web/src/routes/Settings.svelte @@ -17,6 +17,14 @@ requestNotificationsPermission, fireTestNotification, } from "../lib/inbox-service.svelte.js"; + import { + pushSupported, + enablePush, + disablePush, + isPushSubscribed, + isStandalonePwa, + isIos, + } from "../lib/push.js"; import { theme, type ThemeChoice } from "../lib/theme.svelte.js"; import { onboarding } from "../lib/onboarding.svelte.js"; @@ -42,6 +50,13 @@ let notifPerm = $state("default"); let testNotifResult = $state<{ ok: boolean; reason?: string } | null>(null); + let webPushOk = $state(false); // browser supports it at all + let webPushOn = $state(false); // currently subscribed + let webPushBusy = $state(false); + let webPushError = $state(null); + let pwaInstalled = $state(false); + let ios = $state(false); + onMount(async () => { if (!session.unlocked) { push("/unlock"); @@ -50,6 +65,12 @@ await refreshBiometricStatus(); notifSupported = notificationsSupported(); notifPerm = notificationsPermission(); + webPushOk = pushSupported(); + pwaInstalled = isStandalonePwa(); + ios = isIos(); + if (webPushOk) { + webPushOn = await isPushSubscribed(); + } }); async function refreshBiometricStatus() { @@ -93,6 +114,26 @@ setTimeout(() => (testNotifResult = null), 5_000); } + async function toggleWebPush() { + if (!session.unlocked) return; + webPushBusy = true; + webPushError = null; + try { + if (webPushOn) { + await disablePush(session.unlocked.handle, session.unlocked.seed); + webPushOn = false; + } else { + const ok = await enablePush(session.unlocked.handle, session.unlocked.seed); + webPushOn = ok; + if (!ok) webPushError = "Permission denied."; + } + } catch (e) { + webPushError = (e as Error).message; + } finally { + webPushBusy = false; + } + } + async function showSeed() { if (!session.unlocked) return; const phrase = session.unlocked.phrase; @@ -230,6 +271,57 @@ {/if} + +
+

+ Background notifications (Web Push) +

+

+ Get pinged even when kez-chat is fully closed. The push only + carries metadata — message content is decrypted locally when you + open the app. +

+ + {#if !webPushOk} + {#if ios && !pwaInstalled} +

+ iOS only supports Web Push for installed PWAs. Tap the + Share button in Safari and choose + Add to Home Screen, then reopen kez-chat from + the home-screen icon to enable this. +

+ {:else} +

+ Not supported in this browser. +

+ {/if} + {:else if webPushOn} +
+

+ ✓ Subscribed on this device. +

+ +
+ {:else} + + {/if} + {#if webPushError} +

{webPushError}

+ {/if} +
+

Account

diff --git a/kez-chat/web/src/sw.ts b/kez-chat/web/src/sw.ts new file mode 100644 index 0000000..336b48d --- /dev/null +++ b/kez-chat/web/src/sw.ts @@ -0,0 +1,121 @@ +// Custom service worker for kez-chat. +// +// Built by vite-plugin-pwa in `injectManifest` mode. We own this file +// so we can handle two events the auto-generated SW doesn't: +// +// 1. `push` — wake on a Web Push notification from the +// chat-server and call `showNotification`. +// 2. `notificationclick` — focus an existing tab or open the SPA. +// +// Caching strategy is hand-rolled (rather than the long list of +// runtimeCaching rules the generateSW config used) because the surface +// is small: precache the SPA shell, network-only for /v1/* API calls, +// SPA fallback for navigation requests. + +/// + +import { precacheAndRoute, cleanupOutdatedCaches, createHandlerBoundToURL } from "workbox-precaching"; +import { NavigationRoute, registerRoute } from "workbox-routing"; +import { NetworkOnly } from "workbox-strategies"; + +declare const self: ServiceWorkerGlobalScope; + +// vite-plugin-pwa injects the precache manifest at build time. +precacheAndRoute(self.__WB_MANIFEST); +cleanupOutdatedCaches(); + +// SPA fallback: same-origin navigation requests (other than /v1/*) +// resolve to index.html so deep links (e.g. /chats/alice) work offline. +const navigationHandler = createHandlerBoundToURL("index.html"); +const navigationRoute = new NavigationRoute(navigationHandler, { + denylist: [/^\/v1\//, /^\/internal\//, /^\/\.well-known\//], +}); +registerRoute(navigationRoute); + +// Never cache API responses — they're authenticated + dynamic. +registerRoute(({ url }) => url.pathname.startsWith("/v1/"), new NetworkOnly()); + +// Skip waiting + claim control of open pages — paired with the +// controllerchange reload in main.ts, deploys land on the first refresh +// instead of the second. +self.addEventListener("install", () => { + self.skipWaiting(); +}); +self.addEventListener("activate", (event) => { + event.waitUntil(self.clients.claim()); +}); + +// ─── Web Push ─────────────────────────────────────────────────────────────── +// +// Server fanout sends a tiny JSON payload (see kez-chat/src/messages.rs): +// { "type": "kez-chat/new-message", "to": "", "seq": } +// +// We DON'T put plaintext (or even ciphertext) in the push payload — the +// recipient's client pulls the real envelope from /v1/inbox on wake-up +// and decrypts there. This keeps the push provider (Apple/Google/Mozilla) +// from ever seeing message content even theoretically. + +interface PushPayload { + type?: string; + to?: string; + seq?: number; +} + +self.addEventListener("push", (event: PushEvent) => { + let data: PushPayload = {}; + if (event.data) { + try { + data = event.data.json() as PushPayload; + } catch { + // Some providers send a wake-up "" payload — fall through and + // show a generic notification so the user knows to open the app. + } + } + + const title = "New kez-chat message"; + const body = + data.to !== undefined + ? `You have a new message in @${data.to}` + : "Open kez-chat to view it."; + + // `renotify` is widely supported but isn't in the baseline TS DOM lib; + // build the options as a plain object and cast. + const options = { + body, + icon: "/pwa-192x192.png", + badge: "/pwa-64x64.png", + // Group same-conversation pings — iOS especially gets spammy + // otherwise. `renotify` lets the next one still vibrate. + tag: data.to ? `kez-chat:${data.to}` : "kez-chat:new", + renotify: true, + data, + } as NotificationOptions; + + event.waitUntil(self.registration.showNotification(title, options)); +}); + +self.addEventListener("notificationclick", (event: NotificationEvent) => { + event.notification.close(); + const data = event.notification.data as PushPayload | undefined; + const targetUrl = data?.to ? `/chats/${encodeURIComponent(data.to)}` : "/chats"; + + event.waitUntil( + (async () => { + const clientList = await self.clients.matchAll({ + type: "window", + includeUncontrolled: true, + }); + for (const client of clientList) { + const url = new URL(client.url); + if (url.origin === self.location.origin) { + // Found an already-open kez-chat tab — focus it and ask the + // SPA to navigate; cheaper than spawning a fresh window. + await client.focus(); + client.postMessage({ type: "kez-chat/navigate", to: targetUrl }); + return; + } + } + await self.clients.openWindow(targetUrl); + })(), + ); +}); diff --git a/kez-chat/web/vite.config.ts b/kez-chat/web/vite.config.ts index 5d1f83f..defd501 100644 --- a/kez-chat/web/vite.config.ts +++ b/kez-chat/web/vite.config.ts @@ -42,6 +42,18 @@ export default defineConfig({ // stuck on an old build. registerType: "autoUpdate", injectRegister: "auto", + // We need a custom Service Worker so we can handle `push` and + // `notificationclick` events. `injectManifest` keeps the workbox + // precache list autogenerated but lets us own the SW source. + strategies: "injectManifest", + srcDir: "src", + filename: "sw.ts", + injectManifest: { + // zstd wasm is ~350 KB; raise the per-file cap so the precache + // doesn't silently skip it. + maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, + globPatterns: ["**/*.{js,css,html,svg,png,ico,wasm}"], + }, manifest: { name: "kez-chat", short_name: "kez-chat", @@ -65,32 +77,9 @@ export default defineConfig({ }, ], }, - workbox: { - // Activate new SW immediately + take control of existing pages - // without waiting for them to close. Paired with the - // controllerchange reload in main.ts, this means deploys land - // on the first refresh instead of the second. - skipWaiting: true, - clientsClaim: true, - // Precache the SPA shell. Chat data is fetched live from /v1/* - // and we DON'T want it cached — see runtimeCaching below. - globPatterns: ["**/*.{js,css,html,svg,png,ico,wasm}"], - // zstd wasm is ~350 KB; raise the per-file cap. - maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, - // Same-origin navigation requests fall back to the SPA shell so - // /messages, /claims, etc. work after a refresh while offline. - navigateFallback: "index.html", - navigateFallbackDenylist: [/^\/v1\//, /^\/internal\//, /^\/\.well-known\//], - runtimeCaching: [ - { - // Never cache API responses — they're authenticated + dynamic. - urlPattern: /\/v1\//, - handler: "NetworkOnly", - }, - ], - navigationPreload: true, - cleanupOutdatedCaches: true, - }, + // No `workbox: {...}` here — that key only applies in + // `generateSW` mode. In injectManifest mode, the SW source itself + // (src/sw.ts) handles caching strategies + push events. devOptions: { enabled: false, },